Skip to main content

web_audio_api/node/
oscillator.rs

1use std::any::Any;
2use std::f32::consts::PI;
3use std::fmt::Debug;
4use std::sync::OnceLock;
5
6use crate::context::{AudioContextRegistration, AudioParamId, BaseAudioContext};
7use crate::param::{AudioParam, AudioParamDescriptor, AutomationRate};
8use crate::render::{
9    AudioParamValues, AudioProcessor, AudioRenderQuantum, AudioWorkletGlobalScope,
10};
11use crate::PeriodicWave;
12use crate::{assert_valid_time_value, RENDER_QUANTUM_SIZE};
13
14use super::{AudioNode, AudioNodeOptions, AudioScheduledSourceNode, ChannelConfig};
15
16const SINE_TABLE_LENGTH_USIZE: usize = 2048;
17const SINE_TABLE_LENGTH_F32: f32 = SINE_TABLE_LENGTH_USIZE as f32;
18
19/// Precomputed sine table
20fn precomputed_sine_table() -> &'static [f32] {
21    static INSTANCE: OnceLock<Vec<f32>> = OnceLock::new();
22    INSTANCE.get_or_init(|| {
23        // Compute one period sine wavetable of size SINE_TABLE_LENGTH.
24        (0..SINE_TABLE_LENGTH_USIZE)
25            .map(|x| ((x as f32) * 2.0 * PI * (1. / (SINE_TABLE_LENGTH_F32))).sin())
26            .collect()
27    })
28}
29
30fn get_phase_incr(freq: f32, detune: f32, sample_rate: f64) -> f64 {
31    let computed_freq = freq as f64 * (detune as f64 / 1200.).exp2();
32    let clamped = computed_freq.clamp(-sample_rate / 2., sample_rate / 2.);
33    clamped / sample_rate
34}
35
36/// Options for constructing an [`OscillatorNode`]
37// dictionary OscillatorOptions : AudioNodeOptions {
38//   OscillatorType type = "sine";
39//   float frequency = 440;
40//   float detune = 0;
41//   PeriodicWave periodicWave;
42// };
43//
44// @note - Does extend AudioNodeOptions but they are useless for source nodes as
45// they instruct how to upmix the inputs.
46// This is a common source of confusion, see e.g. https://github.com/mdn/content/pull/18472, and
47// an issue in the spec, see discussion in https://github.com/WebAudio/web-audio-api/issues/2496
48#[derive(Clone, Debug)]
49pub struct OscillatorOptions {
50    /// The shape of the periodic waveform
51    pub type_: OscillatorType,
52    /// The frequency of the fundamental frequency.
53    pub frequency: f32,
54    /// A detuning value (in cents) which will offset the frequency by the given amount.
55    pub detune: f32,
56    /// Optional custom waveform, if specified (set `type` to "custom")
57    pub periodic_wave: Option<PeriodicWave>,
58    /// channel config options
59    pub audio_node_options: AudioNodeOptions,
60}
61
62impl Default for OscillatorOptions {
63    fn default() -> Self {
64        Self {
65            type_: OscillatorType::default(),
66            frequency: 440.,
67            detune: 0.,
68            periodic_wave: None,
69            audio_node_options: AudioNodeOptions::default(),
70        }
71    }
72}
73
74/// Type of the waveform rendered by an `OscillatorNode`
75#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
76pub enum OscillatorType {
77    /// Sine wave
78    #[default]
79    Sine,
80    /// Square wave
81    Square,
82    /// Sawtooth wave
83    Sawtooth,
84    /// Triangle wave
85    Triangle,
86    /// type used when periodic_wave is specified
87    Custom,
88}
89
90impl From<u32> for OscillatorType {
91    fn from(i: u32) -> Self {
92        match i {
93            0 => OscillatorType::Sine,
94            1 => OscillatorType::Square,
95            2 => OscillatorType::Sawtooth,
96            3 => OscillatorType::Triangle,
97            4 => OscillatorType::Custom,
98            _ => unreachable!(),
99        }
100    }
101}
102
103/// Instructions to start or stop processing
104#[derive(Debug, Copy, Clone)]
105enum Schedule {
106    Start(f64),
107    Stop(f64),
108}
109
110/// `OscillatorNode` represents an audio source generating a periodic waveform.
111/// It can generate a few common waveforms (i.e. sine, square, sawtooth, triangle),
112/// or can be set to an arbitrary periodic waveform using a [`PeriodicWave`] object.
113///
114/// - MDN documentation: <https://developer.mozilla.org/en-US/docs/Web/API/OscillatorNode>
115/// - specification: <https://webaudio.github.io/web-audio-api/#OscillatorNode>
116/// - see also: [`BaseAudioContext::create_oscillator`]
117/// - see also: [`PeriodicWave`]
118///
119/// # Usage
120///
121/// ```no_run
122/// use web_audio_api::context::{BaseAudioContext, AudioContext};
123/// use web_audio_api::node::{AudioNode, AudioScheduledSourceNode};
124///
125/// let context = AudioContext::default();
126///
127/// let mut osc = context.create_oscillator();
128/// osc.frequency().set_value(200.);
129/// osc.connect(&context.destination());
130/// osc.start();
131/// ```
132///
133/// # Examples
134///
135/// - `cargo run --release --example oscillators`
136/// - `cargo run --release --example many_oscillators_with_env`
137/// - `cargo run --release --example amplitude_modulation`
138///
139#[derive(Debug)]
140pub struct OscillatorNode {
141    /// Represents the node instance and its associated audio context
142    registration: AudioContextRegistration,
143    /// Infos about audio node channel configuration
144    channel_config: ChannelConfig,
145    /// The frequency of the fundamental frequency.
146    frequency: AudioParam,
147    /// A detuning value (in cents) which will offset the frequency by the given amount.
148    detune: AudioParam,
149    /// Waveform of an oscillator
150    type_: OscillatorType,
151    /// Tracks whether `start` has been called already.
152    has_start: bool,
153}
154
155impl AudioNode for OscillatorNode {
156    fn registration(&self) -> &AudioContextRegistration {
157        &self.registration
158    }
159
160    fn channel_config(&self) -> &ChannelConfig {
161        &self.channel_config
162    }
163
164    /// `OscillatorNode` is a source node. A source node is by definition with no input
165    fn number_of_inputs(&self) -> usize {
166        0
167    }
168
169    /// `OscillatorNode` is a mono source node.
170    fn number_of_outputs(&self) -> usize {
171        1
172    }
173}
174
175impl AudioScheduledSourceNode for OscillatorNode {
176    fn start(&mut self) {
177        let when = self.registration.context().current_time();
178        self.start_at(when);
179    }
180
181    fn start_at(&mut self, when: f64) {
182        assert_valid_time_value(when);
183        assert!(
184            !self.has_start,
185            "InvalidStateError - Cannot call `start` twice"
186        );
187
188        self.has_start = true;
189        self.registration.post_message(Schedule::Start(when));
190    }
191
192    fn stop(&mut self) {
193        let when = self.registration.context().current_time();
194        self.stop_at(when);
195    }
196
197    fn stop_at(&mut self, when: f64) {
198        assert_valid_time_value(when);
199        assert!(self.has_start, "InvalidStateError cannot stop before start");
200
201        self.registration.post_message(Schedule::Stop(when));
202    }
203}
204
205impl OscillatorNode {
206    /// Returns an `OscillatorNode`
207    ///
208    /// # Arguments:
209    ///
210    /// * `context` - The `AudioContext`
211    /// * `options` - The OscillatorOptions
212    pub fn new<C: BaseAudioContext>(context: &C, options: OscillatorOptions) -> Self {
213        let OscillatorOptions {
214            type_,
215            frequency,
216            detune,
217            audio_node_options: channel_config,
218            periodic_wave,
219        } = options;
220
221        let mut node = context.base().register(move |registration| {
222            let sample_rate = context.sample_rate();
223            let nyquist = sample_rate / 2.;
224
225            // frequency audio parameter
226            let freq_param_options = AudioParamDescriptor {
227                name: String::new(),
228                min_value: -nyquist,
229                max_value: nyquist,
230                default_value: 440.,
231                automation_rate: AutomationRate::A,
232            };
233            let (f_param, f_proc) = context.create_audio_param(freq_param_options, &registration);
234            f_param.set_value(frequency);
235
236            // detune audio parameter
237            let det_param_options = AudioParamDescriptor {
238                name: String::new(),
239                min_value: -153_600.,
240                max_value: 153_600.,
241                default_value: 0.,
242                automation_rate: AutomationRate::A,
243            };
244            let (det_param, det_proc) =
245                context.create_audio_param(det_param_options, &registration);
246            det_param.set_value(detune);
247
248            let renderer = OscillatorRenderer {
249                type_,
250                frequency: f_proc,
251                detune: det_proc,
252                phase: 0.,
253                start_time: f64::MAX,
254                stop_time: f64::MAX,
255                started: false,
256                periodic_wave: None,
257                ended_triggered: false,
258                sine_table: precomputed_sine_table(),
259            };
260
261            let node = Self {
262                registration,
263                channel_config: channel_config.into(),
264                frequency: f_param,
265                detune: det_param,
266                type_,
267                has_start: false,
268            };
269
270            (node, Box::new(renderer))
271        });
272
273        // renderer has been sent to render thread, we can send it messages
274        if let Some(p_wave) = periodic_wave {
275            node.set_periodic_wave(p_wave);
276        }
277
278        node
279    }
280
281    /// A-rate [`AudioParam`] that defines the fundamental frequency of the
282    /// oscillator, expressed in Hz
283    ///
284    /// The final frequency is calculated as follow: frequency * 2^(detune/1200)
285    #[must_use]
286    pub fn frequency(&self) -> &AudioParam {
287        &self.frequency
288    }
289
290    /// A-rate [`AudioParam`] that defines a transposition according to the
291    /// frequency, expressed in cents.
292    ///
293    /// see <https://en.wikipedia.org/wiki/Cent_(music)>
294    ///
295    /// The final frequency is calculated as follow: frequency * 2^(detune/1200)
296    #[must_use]
297    pub fn detune(&self) -> &AudioParam {
298        &self.detune
299    }
300
301    /// Returns the oscillator type
302    #[must_use]
303    pub fn type_(&self) -> OscillatorType {
304        self.type_
305    }
306
307    /// Set the oscillator type
308    ///
309    /// # Arguments
310    ///
311    /// * `type_` - oscillator type (sine, square, triangle, sawtooth)
312    ///
313    /// # Panics
314    ///
315    /// if `type_` is `OscillatorType::Custom`
316    pub fn set_type(&mut self, type_: OscillatorType) {
317        assert_ne!(
318            type_,
319            OscillatorType::Custom,
320            "InvalidStateError: Custom type cannot be set manually"
321        );
322
323        // if periodic wave has been set specified, type_ changes are ignored
324        if self.type_ == OscillatorType::Custom {
325            return;
326        }
327
328        self.type_ = type_;
329        self.registration.post_message(type_);
330    }
331
332    /// Sets a `PeriodicWave` which describes a waveform to be used by the oscillator.
333    ///
334    /// Calling this sets the oscillator type to `custom`, once set to `custom`
335    /// the oscillator cannot be reverted back to a standard waveform.
336    pub fn set_periodic_wave(&mut self, periodic_wave: PeriodicWave) {
337        self.type_ = OscillatorType::Custom;
338        self.registration.post_message(periodic_wave);
339    }
340}
341
342/// Rendering component of the oscillator node
343struct OscillatorRenderer {
344    /// The shape of the periodic waveform
345    type_: OscillatorType,
346    /// The frequency of the fundamental frequency.
347    frequency: AudioParamId,
348    /// A detuning value (in cents) which will offset the frequency by the given amount.
349    detune: AudioParamId,
350    /// current phase of the oscillator
351    phase: f64,
352    /// start time
353    start_time: f64,
354    /// end time
355    stop_time: f64,
356    /// defines if the oscillator has started
357    started: bool,
358    /// wavetable placeholder for custom oscillators
359    periodic_wave: Option<PeriodicWave>,
360    /// defines if the `ended` events was already dispatched
361    ended_triggered: bool,
362    /// Precomputed sine table
363    sine_table: &'static [f32],
364}
365
366impl AudioProcessor for OscillatorRenderer {
367    fn process(
368        &mut self,
369        _inputs: &[AudioRenderQuantum],
370        outputs: &mut [AudioRenderQuantum],
371        params: AudioParamValues<'_>,
372        scope: &AudioWorkletGlobalScope,
373    ) -> bool {
374        // single output node
375        let output = &mut outputs[0];
376        // 1 channel output
377        output.set_number_of_channels(1);
378
379        let sample_rate = scope.sample_rate as f64;
380        let dt = 1. / sample_rate;
381        let num_frames = RENDER_QUANTUM_SIZE;
382        let next_block_time = scope.current_time + dt * num_frames as f64;
383
384        if self.stop_time <= scope.current_time {
385            output.make_silent();
386
387            if !self.ended_triggered {
388                scope.send_ended_event();
389                self.ended_triggered = true;
390            }
391
392            return false;
393        } else if self.start_time >= next_block_time {
394            output.make_silent();
395
396            if self.stop_time <= next_block_time {
397                if !self.ended_triggered {
398                    scope.send_ended_event();
399                    self.ended_triggered = true;
400                }
401
402                return false;
403            }
404
405            // #462 AudioScheduledSourceNodes that have not been scheduled to start can safely
406            // return tail_time false in order to be collected if their control handle drops.
407            return self.start_time != f64::MAX;
408        }
409
410        let channel_data = output.channel_data_mut(0);
411        let frequency_values = params.get(&self.frequency);
412        let detune_values = params.get(&self.detune);
413
414        let mut current_time = scope.current_time;
415
416        // Prevent scheduling in the past
417        //
418        // [spec] If 0 is passed in for this value or if the value is less than
419        // currentTime, then the sound will start playing immediately
420        // cf. https://webaudio.github.io/web-audio-api/#dom-audioscheduledsourcenode-start-when-when
421        if !self.started && self.start_time < current_time {
422            self.start_time = current_time;
423        }
424
425        if frequency_values.len() == 1 && detune_values.len() == 1 {
426            let phase_incr = get_phase_incr(frequency_values[0], detune_values[0], sample_rate);
427            channel_data
428                .iter_mut()
429                .for_each(|output| self.generate_sample(output, phase_incr, &mut current_time, dt));
430        } else {
431            channel_data
432                .iter_mut()
433                .zip(frequency_values.iter().cycle())
434                .zip(detune_values.iter().cycle())
435                .for_each(|((output, &f), &d)| {
436                    let phase_incr = get_phase_incr(f, d, sample_rate);
437                    self.generate_sample(output, phase_incr, &mut current_time, dt)
438                });
439        }
440
441        if self.stop_time <= next_block_time {
442            if !self.ended_triggered {
443                scope.send_ended_event();
444                self.ended_triggered = true;
445            }
446
447            return false;
448        }
449
450        true
451    }
452
453    fn onmessage(&mut self, msg: &mut dyn Any) {
454        if let Some(&type_) = msg.downcast_ref::<OscillatorType>() {
455            self.type_ = type_;
456            return;
457        }
458
459        if let Some(&schedule) = msg.downcast_ref::<Schedule>() {
460            match schedule {
461                Schedule::Start(v) => self.start_time = v,
462                Schedule::Stop(v) => self.stop_time = v,
463            }
464            return;
465        }
466
467        if let Some(periodic_wave) = msg.downcast_mut::<PeriodicWave>() {
468            if let Some(current_periodic_wave) = &mut self.periodic_wave {
469                // Avoid deallocation in the render thread by swapping the wavetable buffers.
470                std::mem::swap(current_periodic_wave, periodic_wave)
471            } else {
472                // The default wavetable buffer is empty and does not cause allocations.
473                self.periodic_wave = Some(std::mem::take(periodic_wave));
474            }
475            self.type_ = OscillatorType::Custom; // shared type is already updated by control
476            return;
477        }
478
479        log::warn!("OscillatorRenderer: Dropping incoming message {msg:?}");
480    }
481
482    fn before_drop(&mut self, scope: &AudioWorkletGlobalScope) {
483        if !self.ended_triggered
484            && (scope.current_time >= self.start_time || scope.current_time >= self.stop_time)
485        {
486            scope.send_ended_event();
487            self.ended_triggered = true;
488        }
489    }
490}
491impl OscillatorRenderer {
492    #[inline]
493    fn generate_sample(
494        &mut self,
495        output: &mut f32,
496        phase_incr: f64,
497        current_time: &mut f64,
498        dt: f64,
499    ) {
500        if *current_time < self.start_time || *current_time >= self.stop_time {
501            *output = 0.;
502            *current_time += dt;
503
504            return;
505        }
506
507        // first sample to render
508        if !self.started {
509            // if start time was between last frame and current frame
510            // we need to adjust the phase first
511            if *current_time > self.start_time {
512                let ratio = (*current_time - self.start_time) / dt;
513                self.phase = Self::unroll_phase(phase_incr * ratio);
514            }
515
516            self.started = true;
517        }
518
519        // @note: per spec all default oscillators should be rendered from a
520        // wavetable, define if it worth the assle...
521        // e.g. for now `generate_sine` and `generate_custom` are almost the sames
522        // cf. https://webaudio.github.io/web-audio-api/#oscillator-coefficients
523        *output = match self.type_ {
524            OscillatorType::Sine => self.generate_sine(),
525            OscillatorType::Sawtooth => self.generate_sawtooth(phase_incr),
526            OscillatorType::Square => self.generate_square(phase_incr),
527            OscillatorType::Triangle => self.generate_triangle(),
528            OscillatorType::Custom => self.generate_custom(),
529        };
530
531        *current_time += dt;
532
533        self.phase = Self::unroll_phase(self.phase + phase_incr);
534    }
535
536    #[inline]
537    fn generate_sine(&mut self) -> f32 {
538        let position = self.phase * SINE_TABLE_LENGTH_USIZE as f64;
539        let floored = position.floor();
540
541        let prev_index = floored as usize;
542        let mut next_index = prev_index + 1;
543        if next_index == SINE_TABLE_LENGTH_USIZE {
544            next_index = 0;
545        }
546
547        // linear interpolation into lookup table
548        let k = (position - floored) as f32;
549        self.sine_table[prev_index].mul_add(1. - k, self.sine_table[next_index] * k)
550    }
551
552    #[inline]
553    fn generate_sawtooth(&mut self, phase_incr: f64) -> f32 {
554        // offset phase to start at 0. (not -1.)
555        let phase = Self::unroll_phase(self.phase + 0.5);
556        let mut sample = 2.0 * phase - 1.0;
557        sample -= Self::poly_blep(phase, phase_incr, cfg!(test));
558
559        sample as f32
560    }
561
562    #[inline]
563    fn generate_square(&mut self, phase_incr: f64) -> f32 {
564        let mut sample = if self.phase < 0.5 { 1.0 } else { -1.0 };
565        sample += Self::poly_blep(self.phase, phase_incr, cfg!(test));
566
567        let shift_phase = Self::unroll_phase(self.phase + 0.5);
568        sample -= Self::poly_blep(shift_phase, phase_incr, cfg!(test));
569
570        sample as f32
571    }
572
573    #[inline]
574    fn generate_triangle(&mut self) -> f32 {
575        let mut sample = -4. * self.phase + 2.;
576
577        if sample > 1. {
578            sample = 2. - sample;
579        } else if sample < -1. {
580            sample = -2. - sample;
581        }
582
583        sample as f32
584    }
585
586    #[inline]
587    fn generate_custom(&mut self) -> f32 {
588        let periodic_wave = self.periodic_wave.as_ref().unwrap().as_slice();
589        let table_length = periodic_wave.len();
590        let position = self.phase * table_length as f64;
591        let floored = position.floor();
592
593        let prev_index = floored as usize;
594        let mut next_index = prev_index + 1;
595        if next_index == table_length {
596            next_index = 0;
597        }
598
599        // linear interpolation into lookup table
600        let k = (position - floored) as f32;
601        periodic_wave[prev_index].mul_add(1. - k, periodic_wave[next_index] * k)
602    }
603
604    // computes the `polyBLEP` corrections to apply to aliasing signal
605    // `polyBLEP` stands for `polyBandLimitedstEP`
606    // This basically soften the sharp edges in square and sawtooth signals
607    // to avoid infinite frequencies impulses (jumps from -1 to 1 or inverse).
608    // cf. http://www.martin-finke.de/blog/articles/audio-plugins-018-polyblep-oscillator/
609    //
610    // @note: do not apply in tests so we can avoid relying on snapshots
611    #[inline]
612    fn poly_blep(mut t: f64, dt: f64, is_test: bool) -> f64 {
613        if is_test {
614            0.
615        } else if t < dt {
616            t /= dt;
617            t + t - t * t - 1.0
618        } else if t > 1.0 - dt {
619            t = (t - 1.0) / dt;
620            t.mul_add(t, t) + t + 1.0
621        } else {
622            0.0
623        }
624    }
625
626    #[inline]
627    fn unroll_phase(mut phase: f64) -> f64 {
628        if phase >= 1. {
629            phase -= 1.
630        }
631
632        phase
633    }
634}
635
636#[cfg(test)]
637mod tests {
638    use float_eq::assert_float_eq;
639    use std::f64::consts::PI;
640
641    use crate::context::{BaseAudioContext, OfflineAudioContext};
642    use crate::node::{AudioNode, AudioScheduledSourceNode};
643    use crate::periodic_wave::{PeriodicWave, PeriodicWaveOptions};
644    use crate::RENDER_QUANTUM_SIZE;
645
646    use super::{OscillatorNode, OscillatorOptions, OscillatorRenderer, OscillatorType};
647
648    #[test]
649    fn assert_osc_default_build_with_factory_func() {
650        let default_freq = 440.;
651        let default_det = 0.;
652        let default_type = OscillatorType::Sine;
653
654        let mut context = OfflineAudioContext::new(2, 1, 44_100.);
655
656        let mut osc = context.create_oscillator();
657
658        let freq = osc.frequency.value();
659        assert_float_eq!(freq, default_freq, abs_all <= 0.);
660
661        let det = osc.detune.value();
662        assert_float_eq!(det, default_det, abs_all <= 0.);
663
664        assert_eq!(osc.type_(), default_type);
665
666        // should not panic when run
667        osc.start();
668        osc.connect(&context.destination());
669        let _ = context.start_rendering_sync();
670    }
671
672    #[test]
673    fn assert_osc_default_build() {
674        let default_freq = 440.;
675        let default_det = 0.;
676        let default_type = OscillatorType::Sine;
677
678        let mut context = OfflineAudioContext::new(2, 1, 44_100.);
679
680        let mut osc = OscillatorNode::new(&context, OscillatorOptions::default());
681
682        let freq = osc.frequency.value();
683        assert_float_eq!(freq, default_freq, abs_all <= 0.);
684
685        let det = osc.detune.value();
686        assert_float_eq!(det, default_det, abs_all <= 0.);
687
688        assert_eq!(osc.type_(), default_type);
689
690        // should not panic when run
691        osc.start();
692        osc.connect(&context.destination());
693        let _ = context.start_rendering_sync();
694    }
695
696    #[test]
697    #[should_panic]
698    fn set_type_to_custom_should_panic() {
699        let context = OfflineAudioContext::new(2, 1, 44_100.);
700        let mut osc = OscillatorNode::new(&context, OscillatorOptions::default());
701        osc.set_type(OscillatorType::Custom);
702    }
703
704    #[test]
705    fn type_is_custom_when_periodic_wave_is_some() {
706        let expected_type = OscillatorType::Custom;
707
708        let mut context = OfflineAudioContext::new(2, 1, 44_100.);
709
710        let periodic_wave = PeriodicWave::new(&context, PeriodicWaveOptions::default());
711
712        let options = OscillatorOptions {
713            periodic_wave: Some(periodic_wave),
714            ..OscillatorOptions::default()
715        };
716
717        let mut osc = OscillatorNode::new(&context, options);
718
719        assert_eq!(osc.type_(), expected_type);
720
721        // should not panic when run
722        osc.start();
723        osc.connect(&context.destination());
724        let _ = context.start_rendering_sync();
725    }
726
727    #[test]
728    fn set_type_is_ignored_when_periodic_wave_is_some() {
729        let expected_type = OscillatorType::Custom;
730
731        let mut context = OfflineAudioContext::new(2, 1, 44_100.);
732
733        let periodic_wave = PeriodicWave::new(&context, PeriodicWaveOptions::default());
734
735        let options = OscillatorOptions {
736            periodic_wave: Some(periodic_wave),
737            ..OscillatorOptions::default()
738        };
739
740        let mut osc = OscillatorNode::new(&context, options);
741
742        osc.set_type(OscillatorType::Sine);
743        assert_eq!(osc.type_(), expected_type);
744
745        // should not panic when run
746        osc.start();
747        osc.connect(&context.destination());
748        let _ = context.start_rendering_sync();
749    }
750
751    // # Test waveforms
752    //
753    // - for `square`, `triangle` and `sawtooth` the tests may appear a bit
754    //   tautological (and they actually are) as the code from the test is the
755    //   mostly as same as in the renderer, just written in a more compact way.
756    //   However they should help to prevent regressions, and/or allow testing
757    //   against trusted and simple implementation in case of future changes
758    //   in the renderer impl, e.g. performance improvements or spec compliance:
759    //   https://webaudio.github.io/web-audio-api/#oscillator-coefficients.
760    //
761    // - PolyBlep is not applied on `square` and `triangle` for tests, so we can
762    //   compare according to a crude waveforms
763
764    #[test]
765    fn sine_raw() {
766        // 1, 10, 100, 1_000, 10_000 Hz
767        for i in 0..5 {
768            let freq = 10_f32.powf(i as f32);
769            let sample_rate = 44_100;
770
771            let mut context = OfflineAudioContext::new(1, sample_rate, sample_rate as f32);
772
773            let mut osc = context.create_oscillator();
774            osc.connect(&context.destination());
775            osc.frequency().set_value(freq);
776            osc.start_at(0.);
777
778            let output = context.start_rendering_sync();
779            let result = output.get_channel_data(0);
780
781            let mut expected = Vec::<f32>::with_capacity(sample_rate);
782            let mut phase: f64 = 0.;
783            let phase_incr = freq as f64 / sample_rate as f64;
784
785            for _i in 0..sample_rate {
786                let sample = (phase * 2. * PI).sin();
787
788                expected.push(sample as f32);
789
790                phase += phase_incr;
791                if phase >= 1. {
792                    phase -= 1.;
793                }
794            }
795
796            assert_float_eq!(result[..], expected[..], abs_all <= 1e-5);
797        }
798    }
799
800    #[test]
801    fn sine_raw_exact_phase() {
802        // 1, 10, 100, 1_000, 10_000 Hz
803        for i in 0..5 {
804            let freq = 10_f32.powf(i as f32);
805            let sample_rate = 44_100;
806
807            let mut context = OfflineAudioContext::new(1, sample_rate, sample_rate as f32);
808
809            let mut osc = context.create_oscillator();
810            osc.connect(&context.destination());
811            osc.frequency().set_value(freq);
812            osc.start_at(0.);
813
814            let output = context.start_rendering_sync();
815            let result = output.get_channel_data(0);
816            let mut expected = Vec::<f32>::with_capacity(sample_rate);
817
818            for i in 0..sample_rate {
819                let phase = freq as f64 * i as f64 / sample_rate as f64;
820                let sample = (phase * 2. * PI).sin();
821                // phase += phase_incr;
822                expected.push(sample as f32);
823            }
824
825            assert_float_eq!(result[..], expected[..], abs_all <= 1e-5);
826        }
827    }
828
829    #[test]
830    fn square_raw() {
831        // 1, 10, 100, 1_000, 10_000 Hz
832        for i in 0..5 {
833            let freq = 10_f32.powf(i as f32);
834            let sample_rate = 44100;
835
836            let mut context = OfflineAudioContext::new(1, sample_rate, sample_rate as f32);
837
838            let mut osc = context.create_oscillator();
839            osc.connect(&context.destination());
840            osc.frequency().set_value(freq);
841            osc.set_type(OscillatorType::Square);
842            osc.start_at(0.);
843
844            let output = context.start_rendering_sync();
845            let result = output.get_channel_data(0);
846
847            let mut expected = Vec::<f32>::with_capacity(sample_rate);
848            let mut phase: f64 = 0.;
849            let phase_incr = freq as f64 / sample_rate as f64;
850
851            for _i in 0..sample_rate {
852                // 0.5 belongs to the second half of the waveform
853                let sample = if phase < 0.5 { 1. } else { -1. };
854
855                expected.push(sample as f32);
856
857                phase += phase_incr;
858                if phase >= 1. {
859                    phase -= 1.;
860                }
861            }
862
863            assert_float_eq!(result[..], expected[..], abs_all <= 1e-10);
864        }
865    }
866
867    #[test]
868    fn triangle_raw() {
869        // 1, 10, 100, 1_000, 10_000 Hz
870        for i in 0..5 {
871            let freq = 10_f32.powf(i as f32);
872            let sample_rate = 44_100;
873
874            let mut context = OfflineAudioContext::new(1, sample_rate, sample_rate as f32);
875
876            let mut osc = context.create_oscillator();
877            osc.connect(&context.destination());
878            osc.frequency().set_value(freq);
879            osc.set_type(OscillatorType::Triangle);
880            osc.start_at(0.);
881
882            let output = context.start_rendering_sync();
883            let result = output.get_channel_data(0);
884
885            let mut expected = Vec::<f32>::with_capacity(sample_rate);
886            let mut phase: f64 = 0.;
887            let phase_incr = freq as f64 / sample_rate as f64;
888
889            for _i in 0..sample_rate {
890                // triangle starts a 0.
891                // [0., 1.]  between [0, 0.25]
892                // [1., -1.] between [0.25, 0.75]
893                // [-1., 0.] between [0.75, 1]
894                let mut sample = -4. * phase + 2.;
895
896                if sample > 1. {
897                    sample = 2. - sample;
898                } else if sample < -1. {
899                    sample = -2. - sample;
900                }
901
902                expected.push(sample as f32);
903
904                phase += phase_incr;
905                if phase >= 1. {
906                    phase -= 1.;
907                }
908            }
909
910            assert_float_eq!(result[..], expected[..], abs_all <= 1e-10);
911        }
912    }
913
914    #[test]
915    fn sawtooth_raw() {
916        // 1, 10, 100, 1_000, 10_000 Hz
917        for i in 0..5 {
918            let freq = 10_f32.powf(i as f32);
919            let sample_rate = 44_100;
920
921            let mut context = OfflineAudioContext::new(1, sample_rate, sample_rate as f32);
922
923            let mut osc = context.create_oscillator();
924            osc.connect(&context.destination());
925            osc.frequency().set_value(freq);
926            osc.set_type(OscillatorType::Sawtooth);
927            osc.start_at(0.);
928
929            let output = context.start_rendering_sync();
930            let result = output.get_channel_data(0);
931
932            let mut expected = Vec::<f32>::with_capacity(sample_rate);
933            let mut phase: f64 = 0.;
934            let phase_incr = freq as f64 / sample_rate as f64;
935
936            for _i in 0..sample_rate {
937                // triangle starts a 0.
938                // [0, 1] between [0, 0.5]
939                // [-1, 0] between [0.5, 1]
940                let mut offset_phase = phase + 0.5;
941                if offset_phase >= 1. {
942                    offset_phase -= 1.;
943                }
944                let sample = 2. * offset_phase - 1.;
945
946                expected.push(sample as f32);
947
948                phase += phase_incr;
949                if phase >= 1. {
950                    phase -= 1.;
951                }
952            }
953
954            assert_float_eq!(result[..], expected[..], abs_all <= 1e-10);
955        }
956    }
957
958    #[test]
959    // this one should output exactly the same thing as sine_raw
960    fn periodic_wave_1f() {
961        // 1, 10, 100, 1_000, 10_000 Hz
962        for i in 0..5 {
963            let freq = 10_f32.powf(i as f32);
964            let sample_rate = 44_100;
965
966            let mut context = OfflineAudioContext::new(1, sample_rate, sample_rate as f32);
967
968            let options = PeriodicWaveOptions {
969                real: Some(vec![0., 0.]),
970                imag: Some(vec![0., 1.]), // sine is in imaginary component
971                disable_normalization: false,
972            };
973
974            let periodic_wave = context.create_periodic_wave(options);
975
976            let mut osc = context.create_oscillator();
977            osc.connect(&context.destination());
978            osc.set_periodic_wave(periodic_wave);
979            osc.frequency().set_value(freq);
980            osc.set_type(OscillatorType::Sawtooth);
981            osc.start_at(0.);
982
983            let output = context.start_rendering_sync();
984            let result = output.get_channel_data(0);
985
986            let mut expected = Vec::<f32>::with_capacity(sample_rate);
987            let mut phase: f64 = 0.;
988            let phase_incr = freq as f64 / sample_rate as f64;
989
990            for _i in 0..sample_rate {
991                let sample = (phase * 2. * PI).sin();
992
993                expected.push(sample as f32);
994
995                phase += phase_incr;
996                if phase >= 1. {
997                    phase -= 1.;
998                }
999            }
1000
1001            assert_float_eq!(result[..], expected[..], abs_all <= 1e-5);
1002        }
1003    }
1004
1005    #[test]
1006    fn periodic_wave_2f() {
1007        // 1, 10, 100, 1_000, 10_000 Hz
1008        for i in 0..5 {
1009            let freq = 10_f32.powf(i as f32);
1010            let sample_rate = 44_100;
1011
1012            let mut context = OfflineAudioContext::new(1, sample_rate, sample_rate as f32);
1013
1014            let options = PeriodicWaveOptions {
1015                real: Some(vec![0., 0., 0.]),
1016                imag: Some(vec![0., 0.5, 0.5]),
1017                // disable norm, is already tested in `PeriodicWave`
1018                disable_normalization: true,
1019            };
1020
1021            let periodic_wave = context.create_periodic_wave(options);
1022
1023            let mut osc = context.create_oscillator();
1024            osc.connect(&context.destination());
1025            osc.set_periodic_wave(periodic_wave);
1026            osc.frequency().set_value(freq);
1027            osc.start_at(0.);
1028
1029            let output = context.start_rendering_sync();
1030            let result = output.get_channel_data(0);
1031
1032            let mut expected = Vec::<f32>::with_capacity(sample_rate);
1033            let mut phase: f64 = 0.;
1034            let phase_incr = freq as f64 / sample_rate as f64;
1035
1036            for _i in 0..sample_rate {
1037                let mut sample = 0.;
1038                sample += 0.5 * (1. * phase * 2. * PI).sin();
1039                sample += 0.5 * (2. * phase * 2. * PI).sin();
1040
1041                expected.push(sample as f32);
1042
1043                phase += phase_incr;
1044                if phase >= 1. {
1045                    phase -= 1.;
1046                }
1047            }
1048
1049            assert_float_eq!(result[..], expected[..], abs_all <= 1e-5);
1050        }
1051    }
1052
1053    #[test]
1054    fn polyblep_isolated() {
1055        // @note: Only first branch of the polyblep seems to be used here.
1056        // May be due on the simplicity of the test itself where everything is
1057        // well aligned.
1058
1059        // square
1060        {
1061            let mut signal = [1., 1., 1., 1., -1., -1., -1., -1.];
1062            let len = signal.len() as f64;
1063            let dt = 1. / len;
1064
1065            for (index, s) in signal.iter_mut().enumerate() {
1066                let phase = index as f64 / len;
1067
1068                *s += OscillatorRenderer::poly_blep(phase, dt, false);
1069                *s -= OscillatorRenderer::poly_blep((phase + 0.5) % 1., dt, false);
1070            }
1071
1072            let expected = [0., 1., 1., 1., 0., -1., -1., -1.];
1073
1074            assert_float_eq!(signal[..], expected[..], abs_all <= 0.);
1075        }
1076
1077        // sawtooth
1078        {
1079            let mut signal = [0., 0.25, 0.75, 1., -1., -0.75, -0.5, -0.25];
1080            let len = signal.len() as f64;
1081            let dt = 1. / len;
1082
1083            for (index, s) in signal.iter_mut().enumerate() {
1084                let phase = index as f64 / len;
1085                *s -= OscillatorRenderer::poly_blep((phase + 0.5) % 1., dt, false);
1086            }
1087
1088            let expected = [0., 0.25, 0.75, 1., 0., -0.75, -0.5, -0.25];
1089            assert_float_eq!(signal[..], expected[..], abs_all <= 0.);
1090        }
1091    }
1092
1093    #[test]
1094    fn osc_sub_quantum_start() {
1095        let freq = 1.25;
1096        let sample_rate = 44_100;
1097
1098        let mut context = OfflineAudioContext::new(1, sample_rate, sample_rate as f32);
1099        let mut osc = context.create_oscillator();
1100        osc.connect(&context.destination());
1101        osc.frequency().set_value(freq);
1102        osc.start_at(2. / sample_rate as f64);
1103
1104        let output = context.start_rendering_sync();
1105        let result = output.get_channel_data(0);
1106
1107        let mut expected = Vec::<f32>::with_capacity(sample_rate);
1108        let mut phase: f64 = 0.;
1109        let phase_incr = freq as f64 / sample_rate as f64;
1110
1111        expected.push(0.);
1112        expected.push(0.);
1113
1114        for _i in 2..sample_rate {
1115            let sample = (phase * 2. * PI).sin();
1116            phase += phase_incr;
1117            expected.push(sample as f32);
1118        }
1119
1120        assert_float_eq!(result[..], expected[..], abs_all <= 1e-5);
1121    }
1122
1123    // # Test scheduling
1124
1125    #[test]
1126    fn osc_sub_sample_start() {
1127        let freq = 1.;
1128        let sample_rate = 96000;
1129
1130        let mut context = OfflineAudioContext::new(1, sample_rate, sample_rate as f32);
1131        let mut osc = context.create_oscillator();
1132        osc.connect(&context.destination());
1133        osc.frequency().set_value(freq);
1134        // start between second and third sample
1135        osc.start_at(1.3 / sample_rate as f64);
1136
1137        let output = context.start_rendering_sync();
1138        let result = output.get_channel_data(0);
1139
1140        let mut expected = Vec::<f32>::with_capacity(sample_rate);
1141        let phase_incr = freq as f64 / sample_rate as f64;
1142        // on first computed sample, phase is 0.7 (e.g. 2. - 1.3) * phase_incr
1143        let mut phase: f64 = 0.7 * phase_incr;
1144
1145        expected.push(0.);
1146        expected.push(0.);
1147
1148        for _i in 2..sample_rate {
1149            let sample = (phase * 2. * PI).sin();
1150            phase += phase_incr;
1151            expected.push(sample as f32);
1152        }
1153
1154        assert_float_eq!(result[..], expected[..], abs_all <= 1e-5);
1155    }
1156
1157    #[test]
1158    fn osc_sub_quantum_stop() {
1159        let freq = 2345.6;
1160        let sample_rate = 44_100;
1161
1162        let mut context = OfflineAudioContext::new(1, sample_rate, sample_rate as f32);
1163        let mut osc = context.create_oscillator();
1164        osc.connect(&context.destination());
1165        osc.frequency().set_value(freq);
1166        osc.start_at(0.);
1167        osc.stop_at(6. / sample_rate as f64);
1168
1169        let output = context.start_rendering_sync();
1170        let result = output.get_channel_data(0);
1171
1172        let mut expected = Vec::<f32>::with_capacity(sample_rate);
1173        let mut phase: f64 = 0.;
1174        let phase_incr = freq as f64 / sample_rate as f64;
1175
1176        for i in 0..sample_rate {
1177            if i < 6 {
1178                let sample = (phase * 2. * PI).sin();
1179                phase += phase_incr;
1180                expected.push(sample as f32);
1181            } else {
1182                expected.push(0.);
1183            }
1184        }
1185
1186        assert_float_eq!(result[..], expected[..], abs_all <= 1e-5);
1187    }
1188
1189    #[test]
1190    fn osc_stop_disarms_future_start() {
1191        let sample_rate = 44_100;
1192        let future_start = 2. / sample_rate as f64;
1193
1194        let mut context = OfflineAudioContext::new(1, 128, sample_rate as f32);
1195        let mut osc = context.create_oscillator();
1196        osc.connect(&context.destination());
1197        osc.start_at(future_start);
1198        osc.stop();
1199
1200        let output = context.start_rendering_sync();
1201        let result = output.get_channel_data(0);
1202
1203        assert_float_eq!(result[..], vec![0.; 128][..], abs_all <= 0.);
1204    }
1205
1206    #[test]
1207    fn osc_stop_before_start_triggers_onended_without_waiting_for_start_time() {
1208        use std::sync::atomic::{AtomicBool, Ordering};
1209        use std::sync::Arc;
1210
1211        let sample_rate = 44_100.;
1212        let future_start = 2. * RENDER_QUANTUM_SIZE as f64 / sample_rate;
1213        let suspend_at = RENDER_QUANTUM_SIZE as f64 / sample_rate;
1214
1215        let ended = Arc::new(AtomicBool::new(false));
1216        let ended_in_callback = Arc::clone(&ended);
1217        let ended_after_render = Arc::clone(&ended);
1218
1219        let mut context = OfflineAudioContext::new(1, RENDER_QUANTUM_SIZE * 4, sample_rate as f32);
1220        let mut osc = context.create_oscillator();
1221        osc.connect(&context.destination());
1222        osc.start_at(future_start);
1223        osc.set_onended(move |_| {
1224            ended_in_callback.store(true, Ordering::Relaxed);
1225        });
1226        osc.stop();
1227
1228        context.suspend_sync(suspend_at, move |_| {
1229            assert!(ended_after_render.load(Ordering::Relaxed));
1230        });
1231
1232        let _ = context.start_rendering_sync();
1233        assert!(ended.load(Ordering::Relaxed));
1234    }
1235
1236    #[test]
1237    fn osc_sub_sample_stop() {
1238        let freq = 8910.1;
1239        let sample_rate = 44_100;
1240
1241        let mut context = OfflineAudioContext::new(1, sample_rate, sample_rate as f32);
1242        let mut osc = context.create_oscillator();
1243        osc.connect(&context.destination());
1244        osc.frequency().set_value(freq);
1245        osc.start_at(0.);
1246        osc.stop_at(19.4 / sample_rate as f64);
1247
1248        let output = context.start_rendering_sync();
1249        let result = output.get_channel_data(0);
1250
1251        let mut expected = Vec::<f32>::with_capacity(sample_rate);
1252        let mut phase: f64 = 0.;
1253        let phase_incr = freq as f64 / sample_rate as f64;
1254
1255        for i in 0..sample_rate {
1256            if i < 20 {
1257                let sample = (phase * 2. * PI).sin();
1258                phase += phase_incr;
1259                expected.push(sample as f32);
1260            } else {
1261                expected.push(0.);
1262            }
1263        }
1264
1265        assert_float_eq!(result[..], expected[..], abs_all <= 1e-5);
1266    }
1267
1268    #[test]
1269    fn test_start_in_the_past() {
1270        let freq = 8910.1;
1271        let sample_rate = 44_100;
1272
1273        let mut context = OfflineAudioContext::new(1, sample_rate, sample_rate as f32);
1274
1275        context.suspend_sync(128. / sample_rate as f64, move |context| {
1276            let mut osc = context.create_oscillator();
1277            osc.connect(&context.destination());
1278            osc.frequency().set_value(freq);
1279            osc.start_at(0.);
1280        });
1281
1282        let output = context.start_rendering_sync();
1283        let result = output.get_channel_data(0);
1284
1285        let mut expected = Vec::<f32>::with_capacity(sample_rate);
1286        let mut phase: f64 = 0.;
1287        let phase_incr = freq as f64 / sample_rate as f64;
1288
1289        for i in 0..sample_rate {
1290            if i < 128 {
1291                expected.push(0.);
1292            } else {
1293                let sample = (phase * 2. * PI).sin();
1294                expected.push(sample as f32);
1295                phase += phase_incr;
1296            }
1297        }
1298
1299        assert_float_eq!(result[..], expected[..], abs_all <= 1e-5);
1300    }
1301}