Skip to main content

web_audio_api/node/
oscillator.rs

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