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)]
38pub struct Biquad {
39 kind: BiquadKind,
40 frequency: f32,
41 q: f32,
42 gain_db: f32,
43 coefficients: [f64; 5],
45 z1: [f64; 2],
46 z2: [f64; 2],
47}
48
49impl Biquad {
50 #[must_use]
55 pub fn new(kind: BiquadKind, frequency: f32, q: f32, gain_db: f32) -> Self {
56 Self {
57 kind,
58 frequency,
59 q,
60 gain_db,
61 coefficients: [1.0, 0.0, 0.0, 0.0, 0.0],
62 z1: [0.0; 2],
63 z2: [0.0; 2],
64 }
65 }
66
67 fn update(&mut self, sample_rate: u32) {
68 let ceiling = (sample_rate as f32 * MAX_FREQUENCY_FRACTION).max(MIN_FREQUENCY_HZ);
71 let frequency = finite(self.frequency, 1_000.0).clamp(MIN_FREQUENCY_HZ, ceiling);
72 let q = finite(self.q, 0.707).clamp(*Q_RANGE.start(), *Q_RANGE.end());
73 let omega = 2.0 * PI * f64::from(frequency) / f64::from(sample_rate.max(1));
74 let (sin, cos) = libm::sincos(omega);
75 let half_sin = libm::sin(omega / 2.0);
78 let one_minus_cos = 2.0 * half_sin * half_sin;
79 let alpha = sin / (2.0 * f64::from(q));
80 let (b0, b1, b2, a0, a1, a2) = match self.kind {
81 BiquadKind::LowPass => (
82 one_minus_cos / 2.0,
83 one_minus_cos,
84 one_minus_cos / 2.0,
85 1.0 + alpha,
86 -2.0 * cos,
87 1.0 - alpha,
88 ),
89 BiquadKind::HighPass => (
90 1.0_f64.midpoint(cos),
91 -(1.0 + cos),
92 1.0_f64.midpoint(cos),
93 1.0 + alpha,
94 -2.0 * cos,
95 1.0 - alpha,
96 ),
97 BiquadKind::Peaking => {
98 let amplitude = libm::pow(
99 10.0,
100 f64::from(
101 finite(self.gain_db, 0.0).clamp(-MAX_PEAKING_GAIN_DB, MAX_PEAKING_GAIN_DB),
102 ) / 40.0,
103 );
104 (
105 1.0 + alpha * amplitude,
106 -2.0 * cos,
107 1.0 - alpha * amplitude,
108 1.0 + alpha / amplitude,
109 -2.0 * cos,
110 1.0 - alpha / amplitude,
111 )
112 }
113 };
114 self.coefficients = [b0 / a0, b1 / a0, b2 / a0, a1 / a0, a2 / a0];
115 }
116}
117
118impl Biquad {
119 fn run(&mut self, channel: usize, input: &[f32], output: &mut [f32], guarded: bool) {
122 let [b0, b1, b2, a1, a2] = self.coefficients;
123 for (&input, output) in input.iter().zip(output.iter_mut()) {
124 let input = f64::from(if guarded { finite(input, 0.0) } else { input });
125 let value = flush_denormal_f64(input * b0 + self.z1[channel]);
126 let z1 = flush_denormal_f64(input * b1 + self.z2[channel] - a1 * value);
127 let z2 = flush_denormal_f64(input * b2 - a2 * value);
128 let sample = value as f32;
129 if guarded && !(sample.is_finite() && z1.is_finite() && z2.is_finite()) {
130 self.z1[channel] = 0.0;
131 self.z2[channel] = 0.0;
132 *output = 0.0;
133 } else {
134 self.z1[channel] = z1;
135 self.z2[channel] = z2;
136 *output = sample;
137 }
138 }
139 }
140}
141
142impl Processor for Biquad {
143 fn history_samples(&self) -> u32 {
144 let a1 = self.coefficients[3];
145 let a2 = self.coefficients[4];
146 let discriminant = a1 * a1 - 4.0 * a2;
147 let radius = if discriminant < 0.0 {
148 libm::sqrt(a2.abs())
149 } else {
150 a1.abs().midpoint(libm::sqrt(discriminant))
151 };
152 crate::processor::settling_samples(radius).saturating_mul(2)
154 }
155
156 fn prepare(&mut self, sample_rate: u32, _max_block: usize) {
157 self.update(sample_rate);
158 }
159
160 fn process(&mut self, _ctx: &ProcessCtx, inputs: &[&[f32]], outputs: &mut [&mut [f32]]) {
161 for channel in 0..inputs.len().min(outputs.len()).min(2) {
162 let state = (self.z1[channel], self.z2[channel]);
163 self.run(channel, inputs[channel], outputs[channel], false);
164 let outputs_finite = outputs[channel]
168 [..inputs[channel].len().min(outputs[channel].len())]
169 .iter()
170 .all(|sample| sample.is_finite());
171 if !(outputs_finite && self.z1[channel].is_finite() && self.z2[channel].is_finite()) {
172 (self.z1[channel], self.z2[channel]) = state;
173 self.run(channel, inputs[channel], outputs[channel], true);
174 }
175 }
176 }
177
178 fn reset(&mut self) {
179 self.z1 = [0.0; 2];
180 self.z2 = [0.0; 2];
181 }
182 fn save_state(&self) -> StateBlob {
183 let mut bytes = Vec::with_capacity(32);
184 for value in self.z1.into_iter().chain(self.z2) {
185 bytes.extend_from_slice(&value.to_le_bytes());
186 }
187 StateBlob::new(bytes)
188 }
189 fn load_state(&mut self, state: &StateBlob) -> Result<(), StateError> {
190 let bytes: &[u8; 32] = state
191 .as_bytes()
192 .try_into()
193 .map_err(|_| StateError::Mismatch)?;
194 let values: [f64; 4] = core::array::from_fn(|index| {
195 f64::from_le_bytes(
196 bytes[index * 8..index * 8 + 8]
197 .try_into()
198 .expect("fixed range"),
199 )
200 });
201 if values.iter().any(|value| !value.is_finite()) {
202 return Err(StateError::Invalid);
203 }
204 self.z1 = [values[0], values[1]];
205 self.z2 = [values[2], values[3]];
206 Ok(())
207 }
208}
209
210fn finite(value: f32, fallback: f32) -> f32 {
211 if value.is_finite() { value } else { fallback }
212}
213pub(crate) fn flush_denormal(value: f32) -> f32 {
214 if value.abs() < 1.0e-30 { 0.0 } else { value }
215}
216fn flush_denormal_f64(value: f64) -> f64 {
219 if value.abs() < 1.0e-30 { 0.0 } else { value }
220}
221
222#[cfg(test)]
223mod tests {
224 #![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
225
226 use super::{Biquad, BiquadKind};
227 use crate::{ProcessCtx, Processor, TransportState};
228
229 const RATES: [u32; 11] = [
232 3_000, 8_000, 11_025, 22_050, 44_100, 48_000, 88_200, 96_000, 192_000, 384_000, 768_000,
233 ];
234
235 fn pole_radius(filter: &Biquad) -> f64 {
237 let a1 = filter.coefficients[3];
238 let a2 = filter.coefficients[4];
239 let discriminant = a1 * a1 - 4.0 * a2;
240 if discriminant < 0.0 {
241 libm::sqrt(a2.abs())
242 } else {
243 a1.abs().midpoint(libm::sqrt(discriminant))
244 }
245 }
246
247 fn run(filter: &mut Biquad, rate: u32, input: &[f32]) -> Vec<f32> {
249 let mut output = vec![0.0; input.len()];
250 for (block, (input, output)) in input.chunks(128).zip(output.chunks_mut(128)).enumerate() {
251 let sample_position = i64::try_from(block * 128).expect("position");
252 let ctx = ProcessCtx {
253 position: alpha2_time::MediaTime(0),
254 sample_position,
255 sample_rate: rate,
256 transport: TransportState::Playing,
257 };
258 filter.process(&ctx, &[input], &mut [output]);
259 }
260 output
261 }
262
263 fn measured_gain_db(kind: BiquadKind, frequency: f32, q: f32, gain_db: f32, rate: u32) -> f64 {
266 let mut filter = Biquad::new(kind, frequency, q, gain_db);
267 filter.prepare(rate, 128);
268 let settle = libm::ceil(20.0 / -libm::log(pole_radius(&filter))) as usize;
270 let measure = rate as usize;
271 let input: Vec<f32> = (0..settle + measure)
272 .map(|index| match kind {
273 BiquadKind::LowPass => 0.25,
274 BiquadKind::HighPass => {
275 if index % 2 == 0 {
276 0.25
277 } else {
278 -0.25
279 }
280 }
281 BiquadKind::Peaking => {
282 let phase = core::f64::consts::TAU * f64::from(frequency) * index as f64
283 / f64::from(rate);
284 (0.25 * libm::sin(phase)) as f32
285 }
286 })
287 .collect();
288 let output = run(&mut filter, rate, &input);
289 let energy = |samples: &[f32]| {
290 samples
291 .iter()
292 .map(|sample| f64::from(*sample) * f64::from(*sample))
293 .sum::<f64>()
294 };
295 10.0 * libm::log10(energy(&output[settle..]) / energy(&input[settle..]))
298 }
299
300 #[test]
301 fn every_filter_is_stable_and_accurate_at_every_rate() {
302 let mut failures = Vec::new();
303 for rate in RATES {
304 for kind in [
305 BiquadKind::LowPass,
306 BiquadKind::HighPass,
307 BiquadKind::Peaking,
308 ] {
309 for frequency in [10.0, 20.0, 40.0] {
310 for q in [0.05, 0.5, 0.707, 1.0, 4.0, 50.0] {
311 let mut filter = Biquad::new(kind, frequency, q, 12.0);
312 filter.prepare(rate, 128);
313 let radius = pole_radius(&filter);
314 if radius >= 1.0 {
315 failures.push(format!(
316 "{rate} Hz {kind:?} {frequency} Hz Q {q}: pole radius {radius}"
317 ));
318 continue;
319 }
320 if !(0.5..=4.0).contains(&q) {
323 continue;
324 }
325 let expected = if kind == BiquadKind::Peaking {
326 12.0
327 } else {
328 0.0
329 };
330 let gain = measured_gain_db(kind, frequency, q, 12.0, rate);
331 if (gain - expected).abs() > 0.1 {
332 failures.push(format!(
333 "{rate} Hz {kind:?} {frequency} Hz Q {q}: {gain:.3} dB, not {expected} dB"
334 ));
335 }
336 }
337 }
338 }
339 }
340 assert!(failures.is_empty(), "{}", failures.join("\n"));
341 }
342}