Skip to main content

alpha2_audio/
biquad.rs

1#![allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
2
3use core::f64::consts::PI;
4
5use crate::{ProcessCtx, Processor, StateBlob, StateError};
6
7/// Lowest frequency a filter renders, in hertz.
8pub const MIN_FREQUENCY_HZ: f32 = 10.0;
9
10/// Highest frequency a filter renders, as a fraction of the sample rate.
11pub const MAX_FREQUENCY_FRACTION: f32 = 0.49;
12
13/// The resonances a filter renders.
14pub const Q_RANGE: core::ops::RangeInclusive<f32> = 0.05..=50.0;
15
16/// Largest peaking gain a filter renders, in decibels either way.
17pub const MAX_PEAKING_GAIN_DB: f32 = 36.0;
18
19/// Common parametric equalizer shapes.
20#[derive(Copy, Clone, Debug, Eq, PartialEq)]
21pub enum BiquadKind {
22    /// Bell-shaped parametric band.
23    Peaking,
24    /// Second-order low-pass.
25    LowPass,
26    /// Second-order high-pass.
27    HighPass,
28}
29
30/// Two-channel transposed-direct-form-II biquad.
31///
32/// Coefficients and recursion are `f64`; samples are `f32`. Low-band poles sit within about
33/// `2 * pi * f / rate` of the unit circle, which `f32` cannot resolve (a 10 Hz low-pass at 96 kHz
34/// passes DC at +1.2 dB, and at 384 kHz the poles reach the circle). In `f64` every band up to
35/// 768 kHz is stable and within 0.1 dB of its design.
36#[derive(Clone, Debug)]
37pub struct Biquad {
38    kind: BiquadKind,
39    frequency: f32,
40    q: f32,
41    gain_db: f32,
42    /// `b0, b1, b2, a1, a2`, normalized by `a0`.
43    coefficients: [f64; 5],
44    z1: [f64; 2],
45    z2: [f64; 2],
46}
47
48impl Biquad {
49    /// Creates a parametric filter.
50    ///
51    /// [`Processor::prepare`] replaces non-finite parameters with 1 kHz, Q 0.707 and 0 dB, then
52    /// clamps them to this module's ranges.
53    #[must_use]
54    pub fn new(kind: BiquadKind, frequency: f32, q: f32, gain_db: f32) -> Self {
55        Self {
56            kind,
57            frequency,
58            q,
59            gain_db,
60            coefficients: [1.0, 0.0, 0.0, 0.0, 0.0],
61            z1: [0.0; 2],
62            z2: [0.0; 2],
63        }
64    }
65
66    fn update(&mut self, sample_rate: u32) {
67        // Below about 21 Hz the Nyquist cap falls under the floor; the floor wins so `clamp`
68        // cannot panic.
69        let ceiling = (sample_rate as f32 * MAX_FREQUENCY_FRACTION).max(MIN_FREQUENCY_HZ);
70        let frequency = finite(self.frequency, 1_000.0).clamp(MIN_FREQUENCY_HZ, ceiling);
71        let q = finite(self.q, 0.707).clamp(*Q_RANGE.start(), *Q_RANGE.end());
72        let omega = 2.0 * PI * f64::from(frequency) / f64::from(sample_rate.max(1));
73        let (sin, cos) = libm::sincos(omega);
74        // 1 - cos(omega) = 2 sin^2(omega / 2), avoiding cancellation at low frequencies.
75        let half_sin = libm::sin(omega / 2.0);
76        let one_minus_cos = 2.0 * half_sin * half_sin;
77        let alpha = sin / (2.0 * f64::from(q));
78        let (b0, b1, b2, a0, a1, a2) = match self.kind {
79            BiquadKind::LowPass => (
80                one_minus_cos / 2.0,
81                one_minus_cos,
82                one_minus_cos / 2.0,
83                1.0 + alpha,
84                -2.0 * cos,
85                1.0 - alpha,
86            ),
87            BiquadKind::HighPass => (
88                1.0_f64.midpoint(cos),
89                -(1.0 + cos),
90                1.0_f64.midpoint(cos),
91                1.0 + alpha,
92                -2.0 * cos,
93                1.0 - alpha,
94            ),
95            BiquadKind::Peaking => {
96                let amplitude = libm::pow(
97                    10.0,
98                    f64::from(
99                        finite(self.gain_db, 0.0).clamp(-MAX_PEAKING_GAIN_DB, MAX_PEAKING_GAIN_DB),
100                    ) / 40.0,
101                );
102                (
103                    1.0 + alpha * amplitude,
104                    -2.0 * cos,
105                    1.0 - alpha * amplitude,
106                    1.0 + alpha / amplitude,
107                    -2.0 * cos,
108                    1.0 - alpha / amplitude,
109                )
110            }
111        };
112        self.coefficients = [b0 / a0, b1 / a0, b2 / a0, a1 / a0, a2 / a0];
113    }
114}
115
116impl Biquad {
117    /// Runs one channel's block. When `guarded`, non-finite inputs and outputs that overflow
118    /// `f32` play as silence and clear the state.
119    fn run(&mut self, channel: usize, input: &[f32], output: &mut [f32], guarded: bool) {
120        let [b0, b1, b2, a1, a2] = self.coefficients;
121        for (&input, output) in input.iter().zip(output.iter_mut()) {
122            let input = f64::from(if guarded { finite(input, 0.0) } else { input });
123            let value = flush_denormal_f64(input * b0 + self.z1[channel]);
124            let z1 = flush_denormal_f64(input * b1 + self.z2[channel] - a1 * value);
125            let z2 = flush_denormal_f64(input * b2 - a2 * value);
126            let sample = value as f32;
127            if guarded && !(sample.is_finite() && z1.is_finite() && z2.is_finite()) {
128                self.z1[channel] = 0.0;
129                self.z2[channel] = 0.0;
130                *output = 0.0;
131            } else {
132                self.z1[channel] = z1;
133                self.z2[channel] = z2;
134                *output = sample;
135            }
136        }
137    }
138}
139
140impl Processor for Biquad {
141    fn history_samples(&self) -> u32 {
142        let a1 = self.coefficients[3];
143        let a2 = self.coefficients[4];
144        let discriminant = a1 * a1 - 4.0 * a2;
145        let radius = if discriminant < 0.0 {
146            libm::sqrt(a2.abs())
147        } else {
148            a1.abs().midpoint(libm::sqrt(discriminant))
149        };
150        // Extra margin for the polynomial term of a repeated pole.
151        crate::processor::settling_samples(radius).saturating_mul(2)
152    }
153
154    fn prepare(&mut self, sample_rate: u32, _max_block: usize) {
155        self.update(sample_rate);
156    }
157
158    fn process(&mut self, _ctx: &ProcessCtx, inputs: &[&[f32]], outputs: &mut [&mut [f32]]) {
159        for channel in 0..inputs.len().min(outputs.len()).min(2) {
160            let state = (self.z1[channel], self.z2[channel]);
161            self.run(channel, inputs[channel], outputs[channel], false);
162            // A non-finite or overflowing sample would poison the recursion, so rerun the block
163            // with such samples silenced.
164            let outputs_finite = outputs[channel]
165                [..inputs[channel].len().min(outputs[channel].len())]
166                .iter()
167                .all(|sample| sample.is_finite());
168            if !(outputs_finite && self.z1[channel].is_finite() && self.z2[channel].is_finite()) {
169                (self.z1[channel], self.z2[channel]) = state;
170                self.run(channel, inputs[channel], outputs[channel], true);
171            }
172        }
173    }
174
175    fn reset(&mut self) {
176        self.z1 = [0.0; 2];
177        self.z2 = [0.0; 2];
178    }
179    fn save_state(&self) -> StateBlob {
180        let mut bytes = Vec::with_capacity(32);
181        for value in self.z1.into_iter().chain(self.z2) {
182            bytes.extend_from_slice(&value.to_le_bytes());
183        }
184        StateBlob::new(bytes)
185    }
186    fn load_state(&mut self, state: &StateBlob) -> Result<(), StateError> {
187        let bytes: &[u8; 32] = state
188            .as_bytes()
189            .try_into()
190            .map_err(|_| StateError::Mismatch)?;
191        let values: [f64; 4] = core::array::from_fn(|index| {
192            f64::from_le_bytes(
193                bytes[index * 8..index * 8 + 8]
194                    .try_into()
195                    .expect("fixed range"),
196            )
197        });
198        if values.iter().any(|value| !value.is_finite()) {
199            return Err(StateError::Invalid);
200        }
201        self.z1 = [values[0], values[1]];
202        self.z2 = [values[2], values[3]];
203        Ok(())
204    }
205}
206
207fn finite(value: f32, fallback: f32) -> f32 {
208    if value.is_finite() { value } else { fallback }
209}
210pub(crate) fn flush_denormal(value: f32) -> f32 {
211    if value.abs() < 1.0e-30 { 0.0 } else { value }
212}
213/// [`flush_denormal`] for the `f64` state, so a decayed tail never reaches subnormal values.
214fn flush_denormal_f64(value: f64) -> f64 {
215    if value.abs() < 1.0e-30 { 0.0 } else { value }
216}
217
218#[cfg(test)]
219mod tests {
220    #![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
221
222    use super::{Biquad, BiquadKind};
223    use crate::{ProcessCtx, Processor, TransportState};
224
225    /// Project rates (8 to 96 kHz) and `AudioWorklet` rates (3 to 768 kHz).
226    const RATES: [u32; 11] = [
227        3_000, 8_000, 11_025, 22_050, 44_100, 48_000, 88_200, 96_000, 192_000, 384_000, 768_000,
228    ];
229
230    /// The radius of the filter's larger pole.
231    fn pole_radius(filter: &Biquad) -> f64 {
232        let a1 = filter.coefficients[3];
233        let a2 = filter.coefficients[4];
234        let discriminant = a1 * a1 - 4.0 * a2;
235        if discriminant < 0.0 {
236            libm::sqrt(a2.abs())
237        } else {
238            a1.abs().midpoint(libm::sqrt(discriminant))
239        }
240    }
241
242    fn run(filter: &mut Biquad, rate: u32, input: &[f32]) -> Vec<f32> {
243        let mut output = vec![0.0; input.len()];
244        for (block, (input, output)) in input.chunks(128).zip(output.chunks_mut(128)).enumerate() {
245            let sample_position = i64::try_from(block * 128).expect("position");
246            let ctx = ProcessCtx {
247                position: alpha2_time::MediaTime(0),
248                sample_position,
249                sample_rate: rate,
250                transport: TransportState::Playing,
251            };
252            filter.process(&ctx, &[input], &mut [output]);
253        }
254        output
255    }
256
257    /// Measured gain in dB where the ideal is exact: low-pass at DC, high-pass at Nyquist, and a
258    /// peaking band at its centre.
259    fn measured_gain_db(kind: BiquadKind, frequency: f32, q: f32, gain_db: f32, rate: u32) -> f64 {
260        let mut filter = Biquad::new(kind, frequency, q, gain_db);
261        filter.prepare(rate, 128);
262        // Long enough for the slower pole to fall by e^-20, then a second of signal to measure.
263        let settle = libm::ceil(20.0 / -libm::log(pole_radius(&filter))) as usize;
264        let measure = rate as usize;
265        let input: Vec<f32> = (0..settle + measure)
266            .map(|index| match kind {
267                BiquadKind::LowPass => 0.25,
268                BiquadKind::HighPass => {
269                    if index % 2 == 0 {
270                        0.25
271                    } else {
272                        -0.25
273                    }
274                }
275                BiquadKind::Peaking => {
276                    let phase = core::f64::consts::TAU * f64::from(frequency) * index as f64
277                        / f64::from(rate);
278                    (0.25 * libm::sin(phase)) as f32
279                }
280            })
281            .collect();
282        let output = run(&mut filter, rate, &input);
283        let energy = |samples: &[f32]| {
284            samples
285                .iter()
286                .map(|sample| f64::from(*sample) * f64::from(*sample))
287                .sum::<f64>()
288        };
289        // No phase shift at a peaking band's centre, so energies compare exactly.
290        10.0 * libm::log10(energy(&output[settle..]) / energy(&input[settle..]))
291    }
292
293    #[test]
294    fn every_filter_is_stable_and_accurate_at_every_rate() {
295        let mut failures = Vec::new();
296        for rate in RATES {
297            for kind in [
298                BiquadKind::LowPass,
299                BiquadKind::HighPass,
300                BiquadKind::Peaking,
301            ] {
302                for frequency in [10.0, 20.0, 40.0] {
303                    for q in [0.05, 0.5, 0.707, 1.0, 4.0, 50.0] {
304                        let mut filter = Biquad::new(kind, frequency, q, 12.0);
305                        filter.prepare(rate, 128);
306                        let radius = pole_radius(&filter);
307                        if radius >= 1.0 {
308                            failures.push(format!(
309                                "{rate} Hz {kind:?} {frequency} Hz Q {q}: pole radius {radius}"
310                            ));
311                            continue;
312                        }
313                        // Extreme resonances settle too slowly; their poles are checked above.
314                        if !(0.5..=4.0).contains(&q) {
315                            continue;
316                        }
317                        let expected = if kind == BiquadKind::Peaking {
318                            12.0
319                        } else {
320                            0.0
321                        };
322                        let gain = measured_gain_db(kind, frequency, q, 12.0, rate);
323                        if (gain - expected).abs() > 0.1 {
324                            failures.push(format!(
325                                "{rate} Hz {kind:?} {frequency} Hz Q {q}: {gain:.3} dB, not {expected} dB"
326                            ));
327                        }
328                    }
329                }
330            }
331        }
332        assert!(failures.is_empty(), "{}", failures.join("\n"));
333    }
334}