use core::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct Biquad {
pub b: [f32; 3],
pub a: [f32; 2],
state: [f32; 4],
}
impl Biquad {
#[inline]
fn process(&mut self, x: f32) -> f32 {
let y = self.b[0] * x + self.b[1] * self.state[0] + self.b[2] * self.state[1]
- self.a[0] * self.state[2]
- self.a[1] * self.state[3];
self.state[1] = self.state[0];
self.state[0] = x;
self.state[3] = self.state[2];
self.state[2] = y;
y
}
#[inline]
fn reset(&mut self) {
self.state = [0.0; 4];
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct KFilter {
pre: Biquad,
rlb: Biquad,
}
impl KFilter {
pub(crate) fn new(sample_rate: u32) -> Self {
let (pre, rlb) = derive_coefficients(sample_rate);
KFilter { pre, rlb }
}
#[inline]
pub(crate) fn process(&mut self, x: f32) -> f32 {
let y1 = self.pre.process(x);
self.rlb.process(y1)
}
#[inline]
pub(crate) fn reset(&mut self) {
self.pre.reset();
self.rlb.reset();
}
}
const PRE_F0: f64 = 1681.974450955533;
const PRE_Q: f64 = 0.7071752369554196;
const PRE_GAIN_DB: f64 = 3.999843853973347;
const RLB_F0: f64 = 38.13547087602444;
const RLB_Q: f64 = 0.5003270373238773;
fn derive_coefficients(sample_rate: u32) -> (Biquad, Biquad) {
let fs = sample_rate as f64;
let k = libm::tan(PI * PRE_F0 / fs);
let vh = libm::pow(10.0, PRE_GAIN_DB / 20.0);
let vb = libm::pow(vh, 0.4996667741545416);
let kk = k * k;
let kq = k / PRE_Q;
let a0_pre = 1.0 + kq + kk;
let pre_b0 = (vh + vb * kq + kk) / a0_pre;
let pre_b1 = 2.0 * (kk - vh) / a0_pre;
let pre_b2 = (vh - vb * kq + kk) / a0_pre;
let pre_a1 = 2.0 * (kk - 1.0) / a0_pre;
let pre_a2 = (1.0 - kq + kk) / a0_pre;
let pre = Biquad {
b: [pre_b0 as f32, pre_b1 as f32, pre_b2 as f32],
a: [pre_a1 as f32, pre_a2 as f32],
state: [0.0; 4],
};
let k2 = libm::tan(PI * RLB_F0 / fs);
let kk2 = k2 * k2;
let kq2 = k2 / RLB_Q;
let a0_rlb = 1.0 + kq2 + kk2;
let rlb_b0 = 1.0_f64;
let rlb_b1 = -2.0_f64;
let rlb_b2 = 1.0_f64;
let rlb_a1 = 2.0 * (kk2 - 1.0) / a0_rlb;
let rlb_a2 = (1.0 - kq2 + kk2) / a0_rlb;
let rlb = Biquad {
b: [rlb_b0 as f32, rlb_b1 as f32, rlb_b2 as f32],
a: [rlb_a1 as f32, rlb_a2 as f32],
state: [0.0; 4],
};
(pre, rlb)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn coefficients_match_reference_at_48k() {
let (pre, rlb) = derive_coefficients(48_000);
let eps = 1e-4_f32;
assert!((pre.b[0] - 1.535_124_8_f32).abs() < eps);
assert!((pre.b[1] - -2.691_696_2_f32).abs() < eps);
assert!((pre.b[2] - 1.198_392_8_f32).abs() < eps);
assert!((pre.a[0] - -1.690_659_3_f32).abs() < eps);
assert!((pre.a[1] - 0.732_480_8_f32).abs() < eps);
assert!((rlb.b[0] - 1.0_f32).abs() < eps);
assert!((rlb.b[1] - -2.0_f32).abs() < eps);
assert!((rlb.b[2] - 1.0_f32).abs() < eps);
assert!((rlb.a[0] - -1.990_047_5_f32).abs() < eps);
assert!((rlb.a[1] - 0.990_072_2_f32).abs() < eps);
}
#[test]
fn impulse_response_decays() {
let mut f = KFilter::new(48_000);
let _ = f.process(1.0);
for _ in 0..100_000 {
let v = f.process(0.0);
assert!(v.abs() < 10.0, "filter blew up");
}
}
#[test]
fn reset_zeros_state() {
let mut f = KFilter::new(48_000);
for _ in 0..1_000 {
f.process(0.5);
}
f.reset();
let v = f.process(0.0);
assert!(v.abs() < 1e-6);
}
}