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
8use crate::node::TABLE_LENGTH_USIZE;
9
10/// Options for constructing a [`PeriodicWave`]
11#[derive(Debug, Default, Clone)]
12pub struct PeriodicWaveOptions {
13    /// The real parameter represents an array of cosine terms of Fourier series.
14    ///
15    /// The first element (index 0) represents the DC-offset.
16    /// This offset has to be given but will not be taken into account
17    /// to build the custom periodic waveform.
18    ///
19    /// The following elements (index 1 and more) represent the fundamental and
20    /// harmonics of the periodic waveform.
21    pub real: Option<Vec<f32>>,
22    /// The imag parameter represents an array of sine terms of Fourier series.
23    ///
24    /// The first element (index 0) will not be taken into account
25    /// to build the custom periodic waveform.
26    ///
27    /// The following elements (index 1 and more) represent the fundamental and
28    /// harmonics of the periodic waveform.
29    pub imag: Option<Vec<f32>>,
30    /// By default PeriodicWave is build with normalization enabled (disable_normalization = false).
31    /// In this case, a peak normalization is applied to the given custom periodic waveform.
32    ///
33    /// If disable_normalization is enabled (disable_normalization = true), the normalization is
34    /// defined by the periodic waveform characteristics (img, and real fields).
35    pub disable_normalization: bool,
36}
37
38/// `PeriodicWave` represents an arbitrary periodic waveform to be used with an `OscillatorNode`.
39///
40/// - MDN documentation: <https://developer.mozilla.org/en-US/docs/Web/API/PeriodicWave>
41/// - specification: <https://webaudio.github.io/web-audio-api/#PeriodicWave>
42/// - see also: [`BaseAudioContext::create_periodic_wave`]
43/// - see also: [`OscillatorNode`](crate::node::OscillatorNode)
44///
45/// # Usage
46///
47/// ```no_run
48/// use web_audio_api::context::{BaseAudioContext, AudioContext};
49/// use web_audio_api::{PeriodicWave, PeriodicWaveOptions};
50/// use web_audio_api::node::{AudioNode, AudioScheduledSourceNode};
51///
52/// let context = AudioContext::default();
53///
54/// // generate a simple waveform with 2 harmonics
55/// let options = PeriodicWaveOptions {
56///   real: Some(vec![0., 0., 0.]),
57///   imag: Some(vec![0., 0.5, 0.5]),
58///   disable_normalization: false,
59/// };
60///
61/// let periodic_wave = PeriodicWave::new(&context, options);
62///
63/// let mut osc = context.create_oscillator();
64/// osc.set_periodic_wave(periodic_wave);
65/// osc.connect(&context.destination());
66/// osc.start();
67/// ```
68/// # Examples
69///
70/// - `cargo run --release --example oscillators`
71///
72// Basically a wrapper around Arc<Vec<f32>>, so `PeriodicWave`s are cheap to clone
73#[derive(Debug, Clone, Default)]
74pub struct PeriodicWave {
75    wavetable: Arc<Vec<f32>>,
76}
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 = Self::generate_wavetable(&real, &imag, normalize, TABLE_LENGTH_USIZE);
152
153        Self {
154            wavetable: Arc::new(wavetable),
155        }
156    }
157
158    pub(crate) fn as_slice(&self) -> &[f32] {
159        &self.wavetable[..]
160    }
161
162    // cf. https://webaudio.github.io/web-audio-api/#waveform-generation
163    fn generate_wavetable(reals: &[f32], imags: &[f32], normalize: bool, size: usize) -> Vec<f32> {
164        let mut wavetable = Vec::with_capacity(size);
165        let pi_2 = 2. * PI;
166
167        for i in 0..size {
168            let mut sample = 0.;
169            let phase = pi_2 * i as f32 / size as f32;
170
171            for j in 1..reals.len() {
172                let freq = j as f32;
173                let real = reals[j];
174                let imag = imags[j];
175                let rad = phase * freq;
176                let contrib = real * rad.cos() + imag * rad.sin();
177                sample += contrib;
178            }
179
180            wavetable.push(sample);
181        }
182
183        if normalize {
184            Self::normalize(&mut wavetable);
185        }
186
187        wavetable
188    }
189
190    fn normalize(wavetable: &mut [f32]) {
191        let mut max = 0.;
192
193        for sample in wavetable.iter() {
194            let abs = sample.abs();
195            if abs > max {
196                max = abs;
197            }
198        }
199
200        // prevent division by 0. (nothing to normalize anyway...)
201        if max > 0. {
202            let norm_factor = 1. / max;
203
204            for sample in wavetable.iter_mut() {
205                *sample *= norm_factor;
206            }
207        }
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use float_eq::assert_float_eq;
214    use std::f32::consts::PI;
215
216    use crate::context::AudioContext;
217    use crate::node::{TABLE_LENGTH_F32, TABLE_LENGTH_USIZE};
218
219    use super::{PeriodicWave, PeriodicWaveOptions};
220
221    #[test]
222    #[should_panic]
223    fn fails_to_build_when_only_real_is_defined_and_too_short() {
224        let context = AudioContext::default();
225
226        let options = PeriodicWaveOptions {
227            real: Some(vec![0.]),
228            imag: None,
229            disable_normalization: false,
230        };
231
232        let _periodic_wave = PeriodicWave::new(&context, options);
233    }
234
235    #[test]
236    #[should_panic]
237    fn fails_to_build_when_only_imag_is_defined_and_too_short() {
238        let context = AudioContext::default();
239
240        let options = PeriodicWaveOptions {
241            real: None,
242            imag: Some(vec![0.]),
243            disable_normalization: false,
244        };
245
246        let _periodic_wave = PeriodicWave::new(&context, options);
247    }
248
249    #[test]
250    #[should_panic]
251    fn fails_to_build_when_imag_and_real_not_equal_length() {
252        let context = AudioContext::default();
253
254        let options = PeriodicWaveOptions {
255            real: Some(vec![0., 0., 0.]),
256            imag: Some(vec![0., 0.]),
257            disable_normalization: false,
258        };
259
260        let _periodic_wave = PeriodicWave::new(&context, options);
261    }
262
263    #[test]
264    #[should_panic]
265    fn fails_to_build_when_imag_and_real_too_shorts() {
266        let context = AudioContext::default();
267
268        let options = PeriodicWaveOptions {
269            real: Some(vec![0.]),
270            imag: Some(vec![0.]),
271            disable_normalization: false,
272        };
273
274        let _periodic_wave = PeriodicWave::new(&context, options);
275    }
276
277    #[test]
278    fn wavetable_generate_sine() {
279        let reals = [0., 0.];
280        let imags = [0., 1.];
281
282        let result = PeriodicWave::generate_wavetable(&reals, &imags, true, TABLE_LENGTH_USIZE);
283        let mut expected = Vec::new();
284
285        for i in 0..TABLE_LENGTH_USIZE {
286            let sample = (i as f32 / TABLE_LENGTH_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 = PeriodicWave::generate_wavetable(&reals, &imags, false, TABLE_LENGTH_USIZE);
299        let mut expected = Vec::new();
300
301        for i in 0..TABLE_LENGTH_USIZE {
302            let mut sample = 0.;
303            // fundamental frequency
304            sample += 0.5 * (1. * i as f32 / TABLE_LENGTH_F32 * 2. * PI).sin();
305            // 1rst partial
306            sample += 0.5 * (2. * i as f32 / TABLE_LENGTH_F32 * 2. * PI).sin();
307
308            expected.push(sample);
309        }
310
311        assert_float_eq!(result[..], expected[..], abs_all <= 1e-6);
312    }
313
314    #[test]
315    fn normalize() {
316        {
317            let mut signal = [-0.5, 0.2];
318            PeriodicWave::normalize(&mut signal);
319            let expected = [-1., 0.4];
320
321            assert_float_eq!(signal[..], expected[..], abs_all <= 0.);
322        }
323
324        {
325            let mut signal = [0.5, -0.2];
326            PeriodicWave::normalize(&mut signal);
327            let expected = [1., -0.4];
328
329            assert_float_eq!(signal[..], expected[..], abs_all <= 0.);
330        }
331    }
332
333    #[test]
334    fn wavetable_generate_2f_norm() {
335        let reals = [0., 0., 0.];
336        let imags = [0., 0.5, 0.5];
337
338        let result = PeriodicWave::generate_wavetable(&reals, &imags, true, TABLE_LENGTH_USIZE);
339        let mut expected = Vec::new();
340
341        for i in 0..TABLE_LENGTH_USIZE {
342            let mut sample = 0.;
343            // fundamental frequency
344            sample += 0.5 * (1. * i as f32 / TABLE_LENGTH_F32 * 2. * PI).sin();
345            // 1rst partial
346            sample += 0.5 * (2. * i as f32 / TABLE_LENGTH_F32 * 2. * PI).sin();
347
348            expected.push(sample);
349        }
350
351        PeriodicWave::normalize(&mut expected);
352
353        assert_float_eq!(result[..], expected[..], abs_all <= 1e-6);
354    }
355}