Skip to content

Commit ee1a3cf

Browse files
authored
Merge pull request #470 from orottier/feature/audioparam-various
AudioParam validations, compound limits, optimizations for single-valued params
2 parents 8753217 + 7fcb3bf commit ee1a3cf

File tree

6 files changed

+156
-93
lines changed

6 files changed

+156
-93
lines changed

benches/my_benchmark.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,17 @@ pub fn bench_audio_buffer_decode() {
5050
assert_eq!(buffer.length(), 101129);
5151
}
5252

53+
pub fn bench_constant_source() {
54+
let mut ctx = OfflineAudioContext::new(2, black_box(SAMPLES), SAMPLE_RATE);
55+
let mut src = ctx.create_constant_source();
56+
57+
src.connect(&ctx.destination());
58+
src.start_at(1.);
59+
src.stop_at(9.);
60+
61+
assert_eq!(ctx.start_rendering_sync().length(), SAMPLES);
62+
}
63+
5364
pub fn bench_sine() {
5465
let mut ctx = OfflineAudioContext::new(2, black_box(SAMPLES), SAMPLE_RATE);
5566
let mut osc = ctx.create_oscillator();
@@ -301,6 +312,7 @@ macro_rules! iai_or_criterion {
301312
iai_or_criterion!(
302313
bench_ctor,
303314
bench_audio_buffer_decode,
315+
bench_constant_source,
304316
bench_sine,
305317
bench_sine_gain,
306318
bench_sine_gain_delay,

src/node/biquad_filter.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ use crate::{MAX_CHANNELS, RENDER_QUANTUM_SIZE};
1414

1515
use super::{AudioNode, ChannelConfig, ChannelConfigOptions};
1616

17-
fn get_computed_freq(freq: f32, detune: f32) -> f32 {
18-
freq * (detune / 1200.).exp2()
17+
fn get_computed_freq(freq: f32, detune: f32, sample_rate: f32) -> f32 {
18+
freq * (detune / 1200.).exp2().clamp(0., sample_rate / 2.)
1919
}
2020

2121
/// Biquad filter coefficients normalized against a0
@@ -490,7 +490,8 @@ impl BiquadFilterNode {
490490
let q = self.q().value();
491491

492492
// get coefs
493-
let computed_freq = get_computed_freq(frequency, detune);
493+
let computed_freq = get_computed_freq(frequency, detune, sample_rate);
494+
494495
let Coefficients { b0, b1, b2, a1, a2 } = calculate_coefs(
495496
type_,
496497
sample_rate as f64,
@@ -614,7 +615,7 @@ impl AudioProcessor for BiquadFilterRenderer {
614615
let gain = params.get(&self.gain);
615616
let sample_rate_f64 = f64::from(sample_rate);
616617
// compute first coef and fill the coef list with this value
617-
let computed_freq = get_computed_freq(frequency[0], detune[0]);
618+
let computed_freq = get_computed_freq(frequency[0], detune[0], sample_rate);
618619
let coef = calculate_coefs(
619620
type_,
620621
sample_rate_f64,
@@ -635,7 +636,7 @@ impl AudioProcessor for BiquadFilterRenderer {
635636
.zip(gain.iter().cycle())
636637
.skip(1)
637638
.for_each(|((((coefs, &f), &d), &q), &g)| {
638-
let computed_freq = get_computed_freq(f, d);
639+
let computed_freq = get_computed_freq(f, d, sample_rate);
639640
*coefs = calculate_coefs(
640641
type_,
641642
sample_rate_f64,
@@ -703,15 +704,16 @@ mod tests {
703704

704705
#[test]
705706
fn test_computed_freq() {
707+
let sample_rate = 48000.;
706708
let g_sharp = 415.3;
707709
let a = 440.;
708710
let b_flat = 466.16;
709711

710712
// 100 cents is 1 semi tone up
711-
let res = get_computed_freq(a, 100.);
713+
let res = get_computed_freq(a, 100., sample_rate);
712714
assert_float_eq!(res, b_flat, abs <= 0.01);
713715
// -100 cents is 1 semi tone below
714-
let res = get_computed_freq(a, -100.);
716+
let res = get_computed_freq(a, -100., sample_rate);
715717
assert_float_eq!(res, g_sharp, abs <= 0.01);
716718
}
717719

src/node/constant_source.rs

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -201,23 +201,33 @@ impl AudioProcessor for ConstantSourceRenderer {
201201

202202
let offset = params.get(&self.offset);
203203
let output_channel = output.channel_data_mut(0);
204-
let mut current_time = scope.current_time;
205-
206-
output_channel
207-
.iter_mut()
208-
.zip(offset.iter().cycle())
209-
.for_each(|(o, &value)| {
210-
if current_time < self.start_time || current_time >= self.stop_time {
211-
*o = 0.;
212-
} else {
213-
// as we pick values directly from the offset param which is already
214-
// computed at sub-sample accuracy, we don't need to do more than
215-
// copying the values to their right place.
216-
*o = value;
217-
}
218-
219-
current_time += dt;
220-
});
204+
205+
// fast path
206+
if offset.len() == 1
207+
&& self.start_time <= scope.current_time
208+
&& self.stop_time >= next_block_time
209+
{
210+
output_channel.fill(offset[0]);
211+
} else {
212+
// sample accurate path
213+
let mut current_time = scope.current_time;
214+
215+
output_channel
216+
.iter_mut()
217+
.zip(offset.iter().cycle())
218+
.for_each(|(o, &value)| {
219+
if current_time < self.start_time || current_time >= self.stop_time {
220+
*o = 0.;
221+
} else {
222+
// as we pick values directly from the offset param which is already
223+
// computed at sub-sample accuracy, we don't need to do more than
224+
// copying the values to their right place.
225+
*o = value;
226+
}
227+
228+
current_time += dt;
229+
});
230+
}
221231

222232
// tail_time false when output has ended this quantum
223233
let still_running = self.stop_time >= next_block_time;

src/node/oscillator.rs

Lines changed: 65 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ use super::{
1414
ChannelConfigOptions, TABLE_LENGTH_USIZE,
1515
};
1616

17+
fn get_fase_incr(freq: f32, detune: f32, sample_rate: f64) -> f64 {
18+
let computed_freq = freq as f64 * (detune as f64 / 1200.).exp2();
19+
let clamped = computed_freq.clamp(-sample_rate / 2., sample_rate / 2.);
20+
clamped / sample_rate
21+
}
22+
1723
/// Options for constructing an [`OscillatorNode`]
1824
// dictionary OscillatorOptions : AudioNodeOptions {
1925
// OscillatorType type = "sine";
@@ -404,52 +410,21 @@ impl AudioProcessor for OscillatorRenderer {
404410
self.start_time = current_time;
405411
}
406412

407-
channel_data
408-
.iter_mut()
409-
.zip(frequency_values.iter().cycle())
410-
.zip(detune_values.iter().cycle())
411-
.for_each(|((o, &frequency), &detune)| {
412-
if current_time < self.start_time || current_time >= self.stop_time {
413-
*o = 0.;
414-
current_time += dt;
415-
416-
return;
417-
}
418-
419-
// @todo: we could avoid recompute that if both param lengths are 1
420-
let computed_frequency = frequency * (detune / 1200.).exp2();
421-
422-
// first sample to render
423-
if !self.started {
424-
// if start time was between last frame and current frame
425-
// we need to adjust the phase first
426-
if current_time > self.start_time {
427-
let phase_incr = computed_frequency as f64 / sample_rate;
428-
let ratio = (current_time - self.start_time) / dt;
429-
self.phase = Self::unroll_phase(phase_incr * ratio);
430-
}
431-
432-
self.started = true;
433-
}
434-
435-
let phase_incr = computed_frequency as f64 / sample_rate;
436-
437-
// @note: per spec all default oscillators should be rendered from a
438-
// wavetable, define if it worth the assle...
439-
// e.g. for now `generate_sine` and `generate_custom` are almost the sames
440-
// cf. https://webaudio.github.io/web-audio-api/#oscillator-coefficients
441-
*o = match self.type_ {
442-
OscillatorType::Sine => self.generate_sine(),
443-
OscillatorType::Sawtooth => self.generate_sawtooth(phase_incr),
444-
OscillatorType::Square => self.generate_square(phase_incr),
445-
OscillatorType::Triangle => self.generate_triangle(),
446-
OscillatorType::Custom => self.generate_custom(),
447-
};
448-
449-
current_time += dt;
450-
451-
self.phase = Self::unroll_phase(self.phase + phase_incr);
452-
});
413+
if frequency_values.len() == 1 && detune_values.len() == 1 {
414+
let phase_incr = get_fase_incr(frequency_values[0], detune_values[0], sample_rate);
415+
channel_data
416+
.iter_mut()
417+
.for_each(|output| self.generate_sample(output, phase_incr, &mut current_time, dt));
418+
} else {
419+
channel_data
420+
.iter_mut()
421+
.zip(frequency_values.iter().cycle())
422+
.zip(detune_values.iter().cycle())
423+
.for_each(|((output, &f), &d)| {
424+
let phase_incr = get_fase_incr(f, d, sample_rate);
425+
self.generate_sample(output, phase_incr, &mut current_time, dt)
426+
});
427+
}
453428

454429
true
455430
}
@@ -485,6 +460,50 @@ impl AudioProcessor for OscillatorRenderer {
485460
}
486461

487462
impl OscillatorRenderer {
463+
#[inline]
464+
fn generate_sample(
465+
&mut self,
466+
output: &mut f32,
467+
phase_incr: f64,
468+
current_time: &mut f64,
469+
dt: f64,
470+
) {
471+
if *current_time < self.start_time || *current_time >= self.stop_time {
472+
*output = 0.;
473+
*current_time += dt;
474+
475+
return;
476+
}
477+
478+
// first sample to render
479+
if !self.started {
480+
// if start time was between last frame and current frame
481+
// we need to adjust the phase first
482+
if *current_time > self.start_time {
483+
let ratio = (*current_time - self.start_time) / dt;
484+
self.phase = Self::unroll_phase(phase_incr * ratio);
485+
}
486+
487+
self.started = true;
488+
}
489+
490+
// @note: per spec all default oscillators should be rendered from a
491+
// wavetable, define if it worth the assle...
492+
// e.g. for now `generate_sine` and `generate_custom` are almost the sames
493+
// cf. https://webaudio.github.io/web-audio-api/#oscillator-coefficients
494+
*output = match self.type_ {
495+
OscillatorType::Sine => self.generate_sine(),
496+
OscillatorType::Sawtooth => self.generate_sawtooth(phase_incr),
497+
OscillatorType::Square => self.generate_square(phase_incr),
498+
OscillatorType::Triangle => self.generate_triangle(),
499+
OscillatorType::Custom => self.generate_custom(),
500+
};
501+
502+
*current_time += dt;
503+
504+
self.phase = Self::unroll_phase(self.phase + phase_incr);
505+
}
506+
488507
#[inline]
489508
fn generate_sine(&mut self) -> f32 {
490509
let position = self.phase * TABLE_LENGTH_USIZE as f64;

src/node/panner.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -841,26 +841,28 @@ impl AudioProcessor for PannerRenderer {
841841
&& listener_up_z.len() == 1;
842842

843843
if single_valued {
844+
let param_value = a_rate_params.next().unwrap();
844845
match input.number_of_channels() {
845846
1 => {
846847
*output = input.clone();
847848
output.mix(2, ChannelInterpretation::Speakers);
848849
let [left, right] = output.stereo_mut();
849-
std::iter::repeat(a_rate_params.next().unwrap())
850-
.zip(&mut left[..])
850+
left.iter_mut()
851851
.zip(&mut right[..])
852-
.for_each(|((p, l), r)| apply_mono_to_stereo_gain(p, l, r));
852+
.for_each(|(l, r)| apply_mono_to_stereo_gain(param_value, l, r));
853853
}
854854
2 => {
855855
output.set_number_of_channels(2);
856856
let [left, right] = output.stereo_mut();
857-
std::iter::repeat(a_rate_params.next().unwrap())
858-
.zip(input.channel_data(0).iter().copied())
857+
input
858+
.channel_data(0)
859+
.iter()
860+
.copied()
859861
.zip(input.channel_data(1).iter().copied())
860862
.zip(&mut left[..])
861863
.zip(&mut right[..])
862-
.for_each(|((((p, il), ir), ol), or)| {
863-
apply_stereo_to_stereo_gain(p, il, ir, ol, or)
864+
.for_each(|(((il, ir), ol), or)| {
865+
apply_stereo_to_stereo_gain(param_value, il, ir, ol, or)
864866
});
865867
}
866868
_ => unreachable!(),

0 commit comments

Comments
 (0)