1#![allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
2
3use core::f64::consts::PI;
4
5use crate::{ProcessCtx, Processor, StateBlob, StateError};
6
7pub const MIN_FREQUENCY_HZ: f32 = 10.0;
9
10pub const MAX_FREQUENCY_FRACTION: f32 = 0.49;
12
13pub const Q_RANGE: core::ops::RangeInclusive<f32> = 0.05..=50.0;
15
16pub const MAX_PEAKING_GAIN_DB: f32 = 36.0;
18
19#[derive(Copy, Clone, Debug, Eq, PartialEq)]
21pub enum BiquadKind {
22 Peaking,
24 LowPass,
26 HighPass,
28}
29
30#[derive(Clone, Debug)]
37pub struct Biquad {
38 kind: BiquadKind,
39 frequency: f32,
40 q: f32,
41 gain_db: f32,
42 coefficients: [f64; 5],
44 z1: [f64; 2],
45 z2: [f64; 2],
46}
47
48impl Biquad {
49 #[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 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 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 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 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 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}
213fn 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 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 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 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 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 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 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}