Skip to main content

servo_media_audio/
periodic_wave.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::f32::consts::PI as PI_32;
6use std::f64::consts::PI;
7use std::iter::zip;
8
9use log::error;
10use malloc_size_of_derive::MallocSizeOf;
11use num_complex::Complex32;
12use num_traits::Zero;
13use realfft::RealFftPlanner;
14
15use crate::oscillator_node::OscillatorType;
16
17// https://webaudio.github.io/web-audio-api/#PeriodicWave
18// A conforming implementation must support up to at least 8192 elements
19// The wavetable design is based on Blink's implementation
20// https://github.com/mozilla-firefox/firefox/blob/main/dom/media/webaudio/blink/PeriodicWave.cpp
21const FFT_MAX_SIZE: usize = 8192;
22const FFT_MIN_SIZE: usize = 4096;
23const CENTS_PER_RANGE: u32 = 1200 / 3; // Represents 1/3 of an octave
24
25#[derive(Clone, Debug, Default, MallocSizeOf)]
26pub struct PeriodicWaveOptions {
27    imag: Vec<f32>,
28    real: Vec<f32>,
29    disable_normalization: bool,
30}
31
32impl PeriodicWaveOptions {
33    pub fn new(imag: Vec<f32>, real: Vec<f32>, disable_normalization: bool) -> Self {
34        Self {
35            imag,
36            real,
37            disable_normalization,
38        }
39    }
40}
41
42#[derive(Clone, Debug, MallocSizeOf)]
43pub struct PeriodicWave {
44    pub imag: Vec<f32>,
45    pub real: Vec<f32>,
46    pub normalize: bool,
47    size: usize,
48    wavetable: Wavetable,
49}
50
51type Wave = Vec<f32>;
52
53#[derive(Clone, Debug, MallocSizeOf)]
54struct Wavetable {
55    max_number_of_partials_in_band_limited_table: usize,
56    normalization_factor: Option<f64>,
57    waves: Vec<Wave>,
58    size: usize,
59}
60
61impl Wavetable {
62    fn new(size: usize, number_of_waves: usize) -> Self {
63        Wavetable {
64            max_number_of_partials_in_band_limited_table: 0,
65            normalization_factor: None,
66            waves: vec![Vec::with_capacity(size); number_of_waves],
67            size,
68        }
69    }
70
71    fn max_number_of_partials(&self) -> usize {
72        self.size / 2
73    }
74
75    // The range index maps a frequency to a bucket that represents 1/3 of an octave
76    fn number_of_partials_for_range_index(&self, index: usize) -> usize {
77        // Number of cents below Nyquist where we drop the partials
78        let cents_threshold = index as f64 * CENTS_PER_RANGE as f64;
79
80        // Represents the fraction of partials we keep. Essentially, each octave higher halves the
81        // number of partials
82        let fraction_to_keep = (-cents_threshold / 1200.0).exp2();
83        (fraction_to_keep * self.max_number_of_partials() as f64) as usize
84    }
85
86    // Approximate the band limited wave by calculating an inverse FFT
87    fn calculate_unnormalized_wave(
88        &self,
89        real: &[f32],
90        imag: &[f32],
91        frequency: f64,
92        sample_rate: f64,
93        num_partials: usize,
94    ) -> Wave {
95        let coefficients_length = real.len();
96        let mut num_partials = num_partials.min(coefficients_length);
97
98        // Limit number of partials to those below Nyquist frequency
99        let nyquist = 0.5 * sample_rate;
100        if !frequency.is_zero() {
101            num_partials = num_partials.min((nyquist / frequency) as usize);
102        }
103
104        // The length of real output needs to be the size of the periodic wave, N
105        // Complex input must be length N/2 + 1. The max number of partials we take from the given
106        // Fourier coefficients is constructed to be N/2
107        let input_length = self.size / 2 + 1;
108        let mut complex_input = vec![Complex32::ZERO; input_length];
109
110        for (i, (a, b)) in zip(real, imag).take(num_partials).enumerate() {
111            complex_input[i] = Complex32::new(*a, -(*b));
112        }
113
114        let mut planner = RealFftPlanner::new();
115        let complex_to_real_fft = planner.plan_fft_inverse(self.size);
116        let mut real_output = complex_to_real_fft.make_output_vec();
117        if complex_to_real_fft
118            .process(&mut complex_input, &mut real_output)
119            .is_ok()
120        {
121            real_output
122        } else {
123            // This should not happen given how input_length has been calculated
124            error!(
125                "Error calculating IFFT from periodic wave coefficients. By construction lengths of complex input and real output should be N/2 + 1 and N respectively"
126            );
127            Vec::with_capacity(self.size)
128        }
129    }
130
131    fn insert_wave(
132        &mut self,
133        real: &[f32],
134        imag: &[f32],
135        frequency: f64,
136        sample_rate: f64,
137        normalize: bool,
138        range_index: usize,
139    ) {
140        let mut wave = self.calculate_unnormalized_wave(
141            real,
142            imag,
143            frequency,
144            sample_rate,
145            self.number_of_partials_for_range_index(range_index),
146        );
147        let normalization_factor = if normalize {
148            // Calculate the normalization factor if necessary
149            self.normalization_factor.unwrap_or_else(|| {
150                wave.iter()
151                    .map(|x| (*x as f64).abs())
152                    .fold(0.0, |acc, f| f.max(acc))
153            })
154        } else {
155            // If normalize is false, we still need to divide the IFFT output by 2.
156            // The reason is that realfft takes the complex input as the positive frequencies and
157            // then constructs the negative frequencies in a symmetric manner (reverse order of
158            // complex conjugate of the input).
159            2.0
160        };
161        if !normalization_factor.is_zero() {
162            wave = wave
163                .iter()
164                .map(|x| ((*x as f64) / normalization_factor) as f32)
165                .collect();
166        }
167        self.waves[range_index] = wave;
168    }
169
170    fn get_wave(&self, range_index: usize) -> Option<&Wave> {
171        self.waves.get(range_index)
172    }
173
174    fn has_wave(&self, range_index: usize) -> bool {
175        self.waves
176            .get(range_index)
177            .is_some_and(|wave| !wave.is_empty())
178    }
179}
180
181impl PeriodicWave {
182    pub fn new(options: PeriodicWaveOptions) -> Self {
183        let number_of_components = options.imag.len();
184        let size = if number_of_components <= FFT_MIN_SIZE {
185            FFT_MIN_SIZE
186        } else {
187            number_of_components.next_power_of_two().min(FFT_MAX_SIZE)
188        };
189        // 3 tables per octave
190        let number_of_waves = (3 * size.ilog2()) as usize;
191        let wavetable = Wavetable::new(size, number_of_waves);
192        Self {
193            imag: options.imag,
194            real: options.real,
195            normalize: !options.disable_normalization,
196            size,
197            wavetable,
198        }
199    }
200
201    fn get_or_insert_wave(&mut self, frequency: f64, sample_rate: f64) -> (&Wave, &Wave, f64) {
202        // Frequencies can be negative, so alias to the positive frequency
203        let frequency = frequency.abs();
204        // If frequency is low enough such that we can take more partials from the given Fourier
205        // coefficients, reconstruct the wavetable
206        let mut num_partials = self.wavetable.number_of_partials_for_range_index(0);
207        let nyquist = 0.5 * sample_rate;
208        if !frequency.is_zero() {
209            num_partials = num_partials.min((nyquist / frequency) as usize);
210        }
211        if num_partials > self.wavetable.max_number_of_partials_in_band_limited_table {
212            let number_of_waves = self.wavetable.waves.capacity();
213            let mut wavetable = Wavetable::new(self.size, number_of_waves);
214            // Create the first table in order to get the new normalization factor. The first table
215            // is constructed such that it has the most partials, as it maps to the lowest frequencies
216            wavetable.insert_wave(
217                &self.real,
218                &self.imag,
219                frequency,
220                sample_rate,
221                self.normalize,
222                0,
223            );
224            wavetable.max_number_of_partials_in_band_limited_table = num_partials;
225            self.wavetable = wavetable;
226        }
227
228        let number_of_waves = self.wavetable.waves.capacity();
229
230        // Calculate the pitch range.
231        let min_fundamental_frequency = sample_rate / (self.size as f64);
232        let ratio = if frequency.is_zero() {
233            0.5
234        } else {
235            frequency / min_fundamental_frequency
236        };
237        let cents_above_lowest_frequency = ratio.log2() * 1200.0;
238
239        // Round up to the next range to truncate partials before aliasing occurs
240        let mut pitch_range = 1.0 + cents_above_lowest_frequency / CENTS_PER_RANGE as f64;
241        pitch_range = pitch_range.max(0.0);
242        pitch_range = pitch_range.min(self.wavetable.waves.capacity() as f64 - 1.0);
243
244        // The words "lower" and "higher" refer to the wave data having the lower and higher numbers of partials.
245        // As the range index gets larger, the more partials we cull out.
246        // The "lower" table data will have a larger range index.
247        // Conceptually, higher frequencies will lookup higher range index,
248        // which have less partials due to higher orders exceeding Nyquist.
249        let higher_data_range_index = pitch_range as usize;
250        let lower_data_range_index = if higher_data_range_index < number_of_waves - 1 {
251            higher_data_range_index + 1
252        } else {
253            higher_data_range_index
254        };
255
256        // Check if the wavetable has a wave for given range index. If not, create it.
257        if !self.wavetable.has_wave(lower_data_range_index) {
258            self.wavetable.insert_wave(
259                &self.real,
260                &self.imag,
261                frequency,
262                sample_rate,
263                self.normalize,
264                lower_data_range_index,
265            );
266        }
267        if !self.wavetable.has_wave(higher_data_range_index) {
268            self.wavetable.insert_wave(
269                &self.real,
270                &self.imag,
271                frequency,
272                sample_rate,
273                self.normalize,
274                higher_data_range_index,
275            );
276        }
277        let lower_wave_data = self
278            .wavetable
279            .get_wave(lower_data_range_index)
280            .expect("Range index is calculated to be within bounds");
281        let higher_wave_data = self
282            .wavetable
283            .get_wave(higher_data_range_index)
284            .expect("Range index is calculated to be within bounds");
285
286        // Ranges from 0 -> 1 to interpolate between lower -> higher.
287        let table_interpolation_factor = lower_data_range_index as f64 - pitch_range;
288        (
289            lower_wave_data,
290            higher_wave_data,
291            table_interpolation_factor,
292        )
293    }
294
295    pub(crate) fn calculate_waveform(
296        &mut self,
297        frequency: f64,
298        sample_rate: f64,
299        phase: f64,
300    ) -> f64 {
301        // Convert the phase which is in radians [0, 2PI), to the position of the
302        // approximated wave which ranges from [0, N)
303        let position = phase / (2.0 * PI) * self.size as f64;
304        let position_floor = position.floor();
305        let position_interpolation_factor = position - position_floor;
306        let index_mask = self.size - 1;
307        let (lower_wave_data, higher_wave_data, table_interpolation_factor) =
308            self.get_or_insert_wave(frequency, sample_rate);
309        let position_index = position_floor as usize;
310        // Use an index mask to handle the wrap around case where position = self.size. This is more
311        // efficient than checking the position index, less branching
312        let lower_position_index = position_index & index_mask;
313        let higher_position_index = (lower_position_index + 1) & index_mask;
314        // Linear interpolation of the higher and lower position index
315        // to calculate the lower and higher wave values
316        let lower = (1.0 - position_interpolation_factor) *
317            lower_wave_data[lower_position_index] as f64 +
318            position_interpolation_factor * lower_wave_data[higher_position_index] as f64;
319        let higher = (1.0 - position_interpolation_factor) *
320            higher_wave_data[lower_position_index] as f64 +
321            position_interpolation_factor * higher_wave_data[higher_position_index] as f64;
322        // Linear interpolation of the lower and higher waves
323        (1.0 - table_interpolation_factor) * lower + table_interpolation_factor * higher
324    }
325
326    pub(crate) fn generate_waveform_coefficients(oscillator_type: OscillatorType) -> PeriodicWave {
327        let mut periodic_wave = PeriodicWave::new(PeriodicWaveOptions::new(vec![], vec![], false));
328        periodic_wave.real = vec![0.0; periodic_wave.size / 2 + 1];
329        periodic_wave.imag = vec![0.0; periodic_wave.size / 2 + 1];
330        match oscillator_type {
331            // Custom will default to Sine in this case
332            OscillatorType::Sine => {
333                periodic_wave.imag[1] = 1.0;
334            },
335            OscillatorType::Square => {
336                for n in 1..periodic_wave.imag.len() {
337                    periodic_wave.imag[n] =
338                        (2.0 / (n as f32 * PI_32)) * (1.0 - (-1.0_f32).powi(n as i32));
339                }
340            },
341            OscillatorType::Sawtooth => {
342                for n in 1..periodic_wave.imag.len() {
343                    periodic_wave.imag[n] =
344                        (-1.0_f32).powi(n as i32 + 1) * 2.0 / (n as f32 * PI_32);
345                }
346            },
347            OscillatorType::Triangle => {
348                for n in 1..periodic_wave.imag.len() {
349                    periodic_wave.imag[n] =
350                        (8.0 * (n as f32 * PI_32 / 2.0).sin()) / (PI_32 * n as f32).powi(2);
351                }
352            },
353            OscillatorType::Custom(w) => {
354                return w;
355            },
356        }
357        periodic_wave
358    }
359}