use std::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FilterType {
LowPass,
HighPass,
BandPass,
Notch,
PeakingEQ,
LowShelf,
HighShelf,
AllPass,
}
#[derive(Debug, Clone, Copy)]
pub struct BiquadCoeffs {
pub b0: f64,
pub b1: f64,
pub b2: f64,
pub a1: f64,
pub a2: f64,
}
impl BiquadCoeffs {
pub fn design(
filter_type: FilterType,
sample_rate: f64,
freq: f64,
q: f64,
gain_db: f64,
) -> Self {
let w0 = 2.0 * PI * freq / sample_rate;
let cos_w0 = w0.cos();
let sin_w0 = w0.sin();
let alpha = sin_w0 / (2.0 * q);
let a = 10.0_f64.powf(gain_db / 40.0);
let (b0, b1, b2, a0, a1, a2) = match filter_type {
FilterType::LowPass => {
let b1 = 1.0 - cos_w0;
let b0 = b1 / 2.0;
let b2 = b0;
let a0 = 1.0 + alpha;
let a1 = -2.0 * cos_w0;
let a2 = 1.0 - alpha;
(b0, b1, b2, a0, a1, a2)
}
FilterType::HighPass => {
let b1 = -(1.0 + cos_w0);
let b0 = -b1 / 2.0;
let b2 = b0;
let a0 = 1.0 + alpha;
let a1 = -2.0 * cos_w0;
let a2 = 1.0 - alpha;
(b0, b1, b2, a0, a1, a2)
}
FilterType::BandPass => {
let b0 = alpha;
let b1 = 0.0;
let b2 = -alpha;
let a0 = 1.0 + alpha;
let a1 = -2.0 * cos_w0;
let a2 = 1.0 - alpha;
(b0, b1, b2, a0, a1, a2)
}
FilterType::Notch => {
let b0 = 1.0;
let b1 = -2.0 * cos_w0;
let b2 = 1.0;
let a0 = 1.0 + alpha;
let a1 = -2.0 * cos_w0;
let a2 = 1.0 - alpha;
(b0, b1, b2, a0, a1, a2)
}
FilterType::PeakingEQ => {
let b0 = 1.0 + alpha * a;
let b1 = -2.0 * cos_w0;
let b2 = 1.0 - alpha * a;
let a0 = 1.0 + alpha / a;
let a1 = -2.0 * cos_w0;
let a2 = 1.0 - alpha / a;
(b0, b1, b2, a0, a1, a2)
}
FilterType::LowShelf => {
let two_sqrt_a_alpha = 2.0 * a.sqrt() * alpha;
let b0 = a * ((a + 1.0) - (a - 1.0) * cos_w0 + two_sqrt_a_alpha);
let b1 = 2.0 * a * ((a - 1.0) - (a + 1.0) * cos_w0);
let b2 = a * ((a + 1.0) - (a - 1.0) * cos_w0 - two_sqrt_a_alpha);
let a0 = (a + 1.0) + (a - 1.0) * cos_w0 + two_sqrt_a_alpha;
let a1 = -2.0 * ((a - 1.0) + (a + 1.0) * cos_w0);
let a2 = (a + 1.0) + (a - 1.0) * cos_w0 - two_sqrt_a_alpha;
(b0, b1, b2, a0, a1, a2)
}
FilterType::HighShelf => {
let two_sqrt_a_alpha = 2.0 * a.sqrt() * alpha;
let b0 = a * ((a + 1.0) + (a - 1.0) * cos_w0 + two_sqrt_a_alpha);
let b1 = -2.0 * a * ((a - 1.0) + (a + 1.0) * cos_w0);
let b2 = a * ((a + 1.0) + (a - 1.0) * cos_w0 - two_sqrt_a_alpha);
let a0 = (a + 1.0) - (a - 1.0) * cos_w0 + two_sqrt_a_alpha;
let a1 = 2.0 * ((a - 1.0) - (a + 1.0) * cos_w0);
let a2 = (a + 1.0) - (a - 1.0) * cos_w0 - two_sqrt_a_alpha;
(b0, b1, b2, a0, a1, a2)
}
FilterType::AllPass => {
let b0 = 1.0 - alpha;
let b1 = -2.0 * cos_w0;
let b2 = 1.0 + alpha;
let a0 = 1.0 + alpha;
let a1 = -2.0 * cos_w0;
let a2 = 1.0 - alpha;
(b0, b1, b2, a0, a1, a2)
}
};
Self {
b0: b0 / a0,
b1: b1 / a0,
b2: b2 / a0,
a1: a1 / a0,
a2: a2 / a0,
}
}
}
#[derive(Debug, Clone)]
pub struct Biquad {
coeffs: BiquadCoeffs,
z1: f64,
z2: f64,
}
impl Biquad {
pub fn new(coeffs: BiquadCoeffs) -> Self {
Self {
coeffs,
z1: 0.0,
z2: 0.0,
}
}
pub fn set_coeffs(&mut self, coeffs: BiquadCoeffs) {
self.coeffs = coeffs;
}
pub fn reset(&mut self) {
self.z1 = 0.0;
self.z2 = 0.0;
}
#[inline]
pub fn process_sample(&mut self, input: f32) -> f32 {
let x = input as f64;
let c = &self.coeffs;
let y = c.b0 * x + self.z1;
self.z1 = c.b1 * x - c.a1 * y + self.z2;
self.z2 = c.b2 * x - c.a2 * y;
y as f32
}
#[inline]
pub fn process(&mut self, samples: &mut [f32]) {
for s in samples.iter_mut() {
*s = self.process_sample(*s);
}
}
pub fn process_interleaved(&mut self, samples: &mut [f32], channels: usize, channel: usize) {
for s in samples.iter_mut().skip(channel).step_by(channels) {
*s = self.process_sample(*s);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lowpass_dc_passthrough() {
let coeffs = BiquadCoeffs::design(FilterType::LowPass, 44100.0, 1000.0, 0.707, 0.0);
let mut filter = Biquad::new(coeffs);
let mut samples = vec![1.0f32; 1000];
filter.process(&mut samples);
assert!(
(samples[999] - 1.0).abs() < 0.001,
"DC should pass through LPF"
);
}
#[test]
fn test_lowpass_attenuates_high_freq() {
let coeffs = BiquadCoeffs::design(FilterType::LowPass, 44100.0, 100.0, 0.707, 0.0);
let mut filter = Biquad::new(coeffs);
let freq = 10000.0;
let sr = 44100.0;
let mut samples: Vec<f32> = (0..4410)
.map(|i| (2.0 * std::f64::consts::PI * freq * i as f64 / sr).sin() as f32)
.collect();
filter.process(&mut samples);
let rms: f32 = (samples[2000..].iter().map(|s| s * s).sum::<f32>()
/ (samples.len() - 2000) as f32)
.sqrt();
assert!(
rms < 0.05,
"10kHz should be heavily attenuated by 100Hz LPF, got rms={}",
rms
);
}
#[test]
fn test_highpass_blocks_dc() {
let coeffs = BiquadCoeffs::design(FilterType::HighPass, 44100.0, 1000.0, 0.707, 0.0);
let mut filter = Biquad::new(coeffs);
let mut samples = vec![1.0f32; 2000];
filter.process(&mut samples);
assert!(
samples[1999].abs() < 0.001,
"DC should be blocked by HPF, got {}",
samples[1999]
);
}
#[test]
fn test_notch_rejects_center() {
let center = 1000.0;
let sr = 44100.0;
let coeffs = BiquadCoeffs::design(FilterType::Notch, sr, center, 10.0, 0.0);
let mut filter = Biquad::new(coeffs);
let mut samples: Vec<f32> = (0..4410)
.map(|i| (2.0 * PI * center * i as f64 / sr).sin() as f32)
.collect();
filter.process(&mut samples);
let rms: f32 = (samples[2000..].iter().map(|s| s * s).sum::<f32>()
/ (samples.len() - 2000) as f32)
.sqrt();
assert!(
rms < 0.05,
"Center frequency should be rejected by notch, got rms={}",
rms
);
}
#[test]
fn test_peaking_eq_boost() {
let sr = 44100.0;
let freq = 1000.0;
let gain_db = 12.0;
let coeffs = BiquadCoeffs::design(FilterType::PeakingEQ, sr, freq, 1.0, gain_db);
let mut filter = Biquad::new(coeffs);
let mut samples: Vec<f32> = (0..4410)
.map(|i| (2.0 * PI * freq * i as f64 / sr).sin() as f32)
.collect();
filter.process(&mut samples);
let rms: f32 = (samples[2000..].iter().map(|s| s * s).sum::<f32>()
/ (samples.len() - 2000) as f32)
.sqrt();
assert!(
rms > 2.0,
"12dB peaking EQ should significantly boost, got rms={}",
rms
);
}
#[test]
fn test_reset_clears_state() {
let coeffs = BiquadCoeffs::design(FilterType::LowPass, 44100.0, 1000.0, 0.707, 0.0);
let mut filter = Biquad::new(coeffs);
filter.process(&mut [1.0; 100]);
filter.reset();
assert_eq!(filter.process_sample(0.0), 0.0);
}
#[test]
fn test_interleaved_stereo() {
let coeffs = BiquadCoeffs::design(FilterType::LowPass, 44100.0, 100.0, 0.707, 0.0);
let mut left = Biquad::new(coeffs);
let mut right = Biquad::new(coeffs);
let sr = 44100.0;
let n = 2000;
let mut samples: Vec<f32> = (0..n)
.flat_map(|i| {
let l = 1.0f32;
let r = (2.0 * PI * 10000.0 * i as f64 / sr).sin() as f32;
[l, r]
})
.collect();
left.process_interleaved(&mut samples, 2, 0);
right.process_interleaved(&mut samples, 2, 1);
let last_left = samples[(n - 1) * 2];
assert!(
(last_left - 1.0).abs() < 0.01,
"Left DC should pass, got {}",
last_left
);
let right_rms: f32 = (samples[2000..]
.iter()
.skip(1)
.step_by(2)
.map(|s| s * s)
.sum::<f32>()
/ (n - 1000) as f32)
.sqrt();
assert!(
right_rms < 0.05,
"Right 10kHz should be attenuated, got rms={}",
right_rms
);
}
}