Skip to main content

web_audio_api/
periodic_wave.rs

1//! PeriodicWave interface
2
3use std::f32::consts::PI;
4use std::sync::Arc;
5
6use crate::context::BaseAudioContext;
7
8/// Options for constructing a [`PeriodicWave`]
9#[derive(Debug, Default, Clone)]
10pub struct PeriodicWaveOptions {
11    /// The real parameter represents an array of cosine terms of Fourier series.
12    ///
13    /// The first element (index 0) represents the DC-offset.
14    /// This offset has to be given but will not be taken into account
15    /// to build the custom periodic waveform.
16    ///
17    /// The following elements (index 1 and more) represent the fundamental and
18    /// harmonics of the periodic waveform.
19    pub real: Option<Vec<f32>>,
20    /// The imag parameter represents an array of sine terms of Fourier series.
21    ///
22    /// The first element (index 0) will not be taken into account
23    /// to build the custom periodic waveform.
24    ///
25    /// The following elements (index 1 and more) represent the fundamental and
26    /// harmonics of the periodic waveform.
27    pub imag: Option<Vec<f32>>,
28    /// By default PeriodicWave is build with normalization enabled (disable_normalization = false).
29    /// In this case, a peak normalization is applied to the given custom periodic waveform.
30    ///
31    /// If disable_normalization is enabled (disable_normalization = true), the normalization is
32    /// defined by the periodic waveform characteristics (img, and real fields).
33    pub disable_normalization: bool,
34}
35
36/// `PeriodicWave` represents an arbitrary periodic waveform to be used with an `OscillatorNode`.
37///
38/// - MDN documentation: <https://developer.mozilla.org/en-US/docs/Web/API/PeriodicWave>
39/// - specification: <https://webaudio.github.io/web-audio-api/#PeriodicWave>
40/// - see also: [`BaseAudioContext::create_periodic_wave`]
41/// - see also: [`OscillatorNode`](crate::node::OscillatorNode)
42///
43/// # Usage
44///
45/// ```no_run
46/// use web_audio_api::context::{BaseAudioContext, AudioContext};
47/// use web_audio_api::{PeriodicWave, PeriodicWaveOptions};
48/// use web_audio_api::node::{AudioNode, AudioScheduledSourceNode};
49///
50/// let context = AudioContext::default();
51///
52/// // generate a simple waveform with 2 harmonics
53/// let options = PeriodicWaveOptions {
54///   real: Some(vec![0., 0., 0.]),
55///   imag: Some(vec![0., 0.5, 0.5]),
56///   disable_normalization: false,
57/// };
58///
59/// let periodic_wave = PeriodicWave::new(&context, options);
60///
61/// let mut osc = context.create_oscillator();
62/// osc.set_periodic_wave(periodic_wave);
63/// osc.connect(&context.destination());
64/// osc.start();
65/// ```
66/// # Examples
67///
68/// - `cargo run --release --example oscillators`
69///
70// Basically a wrapper around Arc<Vec<f32>>, so `PeriodicWave`s are cheap to clone
71#[derive(Debug, Clone, Default)]
72pub struct PeriodicWave {
73    wavetable: Arc<Vec<f32>>,
74}
75
76const PERIODIC_WAVE_TABLE_LENGTH: usize = 8192;
77
78impl PeriodicWave {
79    /// Returns a `PeriodicWave`
80    ///
81    /// # Arguments
82    ///
83    /// * `real` - The real parameter represents an array of cosine terms of Fourier series.
84    /// * `imag` - The imag parameter represents an array of sine terms of Fourier series.
85    /// * `constraints` - The constraints parameter specifies the normalization mode of the `PeriodicWave`
86    ///
87    /// # Panics
88    ///
89    /// Will panic if:
90    ///
91    /// * `real` is defined and its length is less than 2
92    /// * `imag` is defined and its length is less than 2
93    /// * `real` and `imag` are defined and theirs lengths are not equal
94    /// * `PeriodicWave` is more than 8192 components
95    //
96    // @notes:
97    // - Current implementation is very naive and could be improved using inverse
98    // FFT or table lookup on SINETABLE. Such performance improvements should be
99    // however tested also against this implementation.
100    // - Built-in types of the `OscillatorNode` should use periodic waves
101    // c.f. https://webaudio.github.io/web-audio-api/#oscillator-coefficients
102    // - The question of bandlimited oscillators should also be handled
103    // e.g. https://www.dafx12.york.ac.uk/papers/dafx12_submission_69.pdf
104    pub fn new<C: BaseAudioContext>(_context: &C, options: PeriodicWaveOptions) -> Self {
105        let PeriodicWaveOptions {
106            real,
107            imag,
108            disable_normalization,
109        } = options;
110
111        let (real, imag) = match (real, imag) {
112            (Some(r), Some(i)) => {
113                assert_eq!(
114                    r.len(),
115                    i.len(),
116                    "IndexSizeError - `real` and `imag` length should be equal"
117                );
118                assert!(
119                    r.len() >= 2,
120                    "IndexSizeError - `real` and `imag` length should at least 2"
121                );
122
123                (r, i)
124            }
125            (Some(r), None) => {
126                assert!(
127                    r.len() >= 2,
128                    "IndexSizeError - `real` and `imag` length should at least 2"
129                );
130
131                let len = r.len();
132                (r, vec![0.; len])
133            }
134            (None, Some(i)) => {
135                assert!(
136                    i.len() >= 2,
137                    "IndexSizeError - `real` and `imag` length should at least 2"
138                );
139
140                let len = i.len();
141                (vec![0.; len], i)
142            }
143            // Defaults to sine wave
144            // [spec] Note: When setting this PeriodicWave on an OscillatorNode,
145            // this is equivalent to using the built-in type "sine".
146            _ => (vec![0., 0.], vec![0., 1.]),
147        };
148
149        let normalize = !disable_normalization;
150        // [spec] A conforming implementation MUST support PeriodicWave up to at least 8192 elements.
151        let wavetable =
152            Self::generate_wavetable(&real, &imag, normalize, PERIODIC_WAVE_TABLE_LENGTH);
153
154        Self {
155            wavetable: Arc::new(wavetable),
156        }
157    }
158
159    pub(crate) fn as_slice(&self) -> &[f32] {
160        &self.wavetable[..]
161    }
162
163    // cf. https://webaudio.github.io/web-audio-api/#waveform-generation
164    fn generate_wavetable(reals: &[f32], imags: &[f32], normalize: bool, size: usize) -> Vec<f32> {
165        let mut wavetable = Vec::with_capacity(size);
166        let pi_2 = 2. * PI;
167
168        for i in 0..size {
169            let mut sample = 0.;
170            let phase = pi_2 * i as f32 / size as f32;
171
172            for j in 1..reals.len() {
173                let freq = j as f32;
174                let real = reals[j];
175                let imag = imags[j];
176                let rad = phase * freq;
177                let contrib = real * rad.cos() + imag * rad.sin();
178                sample += contrib;
179            }
180
181            wavetable.push(sample);
182        }
183
184        if normalize {
185            Self::normalize(&mut wavetable);
186        }
187
188        wavetable
189    }
190
191    fn normalize(wavetable: &mut [f32]) {
192        let mut max = 0.;
193
194        for sample in wavetable.iter() {
195            let abs = sample.abs();
196            if abs > max {
197                max = abs;
198            }
199        }
200
201        // prevent division by 0. (nothing to normalize anyway...)
202        if max > 0. {
203            let norm_factor = 1. / max;
204
205            for sample in wavetable.iter_mut() {
206                *sample *= norm_factor;
207            }
208        }
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use float_eq::assert_float_eq;
215    use std::f32::consts::PI;
216
217    use super::{PeriodicWave, PeriodicWaveOptions, PERIODIC_WAVE_TABLE_LENGTH};
218    use crate::context::AudioContext;
219
220    #[test]
221    #[should_panic]
222    fn fails_to_build_when_only_real_is_defined_and_too_short() {
223        let context = AudioContext::default();
224
225        let options = PeriodicWaveOptions {
226            real: Some(vec![0.]),
227            imag: None,
228            disable_normalization: false,
229        };
230
231        let _periodic_wave = PeriodicWave::new(&context, options);
232    }
233
234    #[test]
235    #[should_panic]
236    fn fails_to_build_when_only_imag_is_defined_and_too_short() {
237        let context = AudioContext::default();
238
239        let options = PeriodicWaveOptions {
240            real: None,
241            imag: Some(vec![0.]),
242            disable_normalization: false,
243        };
244
245        let _periodic_wave = PeriodicWave::new(&context, options);
246    }
247
248    #[test]
249    #[should_panic]
250    fn fails_to_build_when_imag_and_real_not_equal_length() {
251        let context = AudioContext::default();
252
253        let options = PeriodicWaveOptions {
254            real: Some(vec![0., 0., 0.]),
255            imag: Some(vec![0., 0.]),
256            disable_normalization: false,
257        };
258
259        let _periodic_wave = PeriodicWave::new(&context, options);
260    }
261
262    #[test]
263    #[should_panic]
264    fn fails_to_build_when_imag_and_real_too_shorts() {
265        let context = AudioContext::default();
266
267        let options = PeriodicWaveOptions {
268            real: Some(vec![0.]),
269            imag: Some(vec![0.]),
270            disable_normalization: false,
271        };
272
273        let _periodic_wave = PeriodicWave::new(&context, options);
274    }
275
276    #[test]
277    fn wavetable_generate_sine() {
278        let reals = [0., 0.];
279        let imags = [0., 1.];
280
281        let result =
282            PeriodicWave::generate_wavetable(&reals, &imags, true, PERIODIC_WAVE_TABLE_LENGTH);
283        let mut expected = Vec::new();
284
285        for i in 0..PERIODIC_WAVE_TABLE_LENGTH {
286            let sample = (i as f32 / PERIODIC_WAVE_TABLE_LENGTH as f32 * 2. * PI).sin();
287            expected.push(sample);
288        }
289
290        assert_float_eq!(result[..], expected[..], abs_all <= 1e-6);
291    }
292
293    #[test]
294    fn wavetable_generate_2f_not_norm() {
295        let reals = [0., 0., 0.];
296        let imags = [0., 0.5, 0.5];
297
298        let result =
299            PeriodicWave::generate_wavetable(&reals, &imags, false, PERIODIC_WAVE_TABLE_LENGTH);
300        let mut expected = Vec::new();
301
302        for i in 0..PERIODIC_WAVE_TABLE_LENGTH {
303            let mut sample = 0.;
304            // fundamental frequency
305            sample += 0.5 * (1. * i as f32 / PERIODIC_WAVE_TABLE_LENGTH as f32 * 2. * PI).sin();
306            // 1rst partial
307            sample += 0.5 * (2. * i as f32 / PERIODIC_WAVE_TABLE_LENGTH as f32 * 2. * PI).sin();
308
309            expected.push(sample);
310        }
311
312        assert_float_eq!(result[..], expected[..], abs_all <= 1e-6);
313    }
314
315    #[test]
316    fn normalize() {
317        {
318            let mut signal = [-0.5, 0.2];
319            PeriodicWave::normalize(&mut signal);
320            let expected = [-1., 0.4];
321
322            assert_float_eq!(signal[..], expected[..], abs_all <= 0.);
323        }
324
325        {
326            let mut signal = [0.5, -0.2];
327            PeriodicWave::normalize(&mut signal);
328            let expected = [1., -0.4];
329
330            assert_float_eq!(signal[..], expected[..], abs_all <= 0.);
331        }
332    }
333
334    #[test]
335    fn wavetable_generate_2f_norm() {
336        let reals = [0., 0., 0.];
337        let imags = [0., 0.5, 0.5];
338
339        let result =
340            PeriodicWave::generate_wavetable(&reals, &imags, true, PERIODIC_WAVE_TABLE_LENGTH);
341        let mut expected = Vec::new();
342
343        for i in 0..PERIODIC_WAVE_TABLE_LENGTH {
344            let mut sample = 0.;
345            // fundamental frequency
346            sample += 0.5 * (1. * i as f32 / PERIODIC_WAVE_TABLE_LENGTH as f32 * 2. * PI).sin();
347            // 1rst partial
348            sample += 0.5 * (2. * i as f32 / PERIODIC_WAVE_TABLE_LENGTH as f32 * 2. * PI).sin();
349
350            expected.push(sample);
351        }
352
353        PeriodicWave::normalize(&mut expected);
354
355        assert_float_eq!(result[..], expected[..], abs_all <= 1e-6);
356    }
357}