use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct BiquadFilter {
b0: f64,
b1: f64,
b2: f64,
a1: f64,
a2: f64,
x1: f64, x2: f64, y1: f64, y2: f64, }
impl BiquadFilter {
pub fn highpass_butterworth(cutoff_hz: f32, sample_rate: u32) -> Self {
let fs = sample_rate as f64;
let fc = cutoff_hz as f64;
assert!(
fc < fs / 2.0,
"cutoff frequency must be below Nyquist frequency"
);
let q = std::f64::consts::FRAC_1_SQRT_2;
let omega = 2.0 * PI * fc / fs;
let cos_omega = omega.cos();
let sin_omega = omega.sin();
let alpha = sin_omega / (2.0 * q);
let b0 = (1.0 + cos_omega) / 2.0;
let b1 = -(1.0 + cos_omega);
let b2 = (1.0 + cos_omega) / 2.0;
let a0 = 1.0 + alpha;
let a1 = -2.0 * cos_omega;
let a2 = 1.0 - alpha;
Self {
b0: b0 / a0,
b1: b1 / a0,
b2: b2 / a0,
a1: a1 / a0,
a2: a2 / a0,
x1: 0.0,
x2: 0.0,
y1: 0.0,
y2: 0.0,
}
}
#[inline]
pub fn process_sample(&mut self, x: f64) -> f64 {
let y = self.b0 * x + self.b1 * self.x1 + self.b2 * self.x2
- self.a1 * self.y1
- self.a2 * self.y2;
self.x2 = self.x1;
self.x1 = x;
self.y2 = self.y1;
self.y1 = y;
y
}
pub fn process_i16(&mut self, samples: &mut [i16]) {
for sample in samples.iter_mut() {
let x = *sample as f64;
let y = self.process_sample(x);
*sample = y.round().clamp(-32768.0, 32767.0) as i16;
}
}
pub fn process_i16_to_vec(&mut self, samples: &[i16]) -> Vec<i16> {
samples
.iter()
.map(|&s| {
let y = self.process_sample(s as f64);
y.round().clamp(-32768.0, 32767.0) as i16
})
.collect()
}
pub fn reset(&mut self) {
self.x1 = 0.0;
self.x2 = 0.0;
self.y1 = 0.0;
self.y2 = 0.0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_highpass_creation() {
let filter = BiquadFilter::highpass_butterworth(80.0, 16000);
assert!(filter.b0.is_finite());
assert!(filter.a1.is_finite());
}
#[test]
#[should_panic(expected = "cutoff frequency must be below Nyquist")]
fn test_highpass_invalid_cutoff() {
BiquadFilter::highpass_butterworth(8000.0, 16000);
}
#[test]
fn test_highpass_attenuates_dc() {
let mut filter = BiquadFilter::highpass_butterworth(100.0, 16000);
let dc_samples: Vec<i16> = vec![10000; 1000];
let output = filter.process_i16_to_vec(&dc_samples);
let last_100: i32 = output[900..].iter().map(|&s| s.abs() as i32).sum();
let avg = last_100 / 100;
assert!(
avg < 100,
"DC should be heavily attenuated, got avg abs: {avg}"
);
}
#[test]
fn test_highpass_passes_high_frequencies() {
let mut filter = BiquadFilter::highpass_butterworth(100.0, 16000);
let sample_rate = 16000.0;
let freq = 1000.0;
let samples: Vec<i16> = (0..1600)
.map(|i| {
let t = i as f64 / sample_rate;
(10000.0 * (2.0 * PI * freq * t).sin()) as i16
})
.collect();
let output = filter.process_i16_to_vec(&samples);
let input_rms: f64 = (samples[800..]
.iter()
.map(|&s| (s as f64).powi(2))
.sum::<f64>()
/ 800.0)
.sqrt();
let output_rms: f64 = (output[800..]
.iter()
.map(|&s| (s as f64).powi(2))
.sum::<f64>()
/ 800.0)
.sqrt();
let ratio = output_rms / input_rms;
assert!(
ratio > 0.9,
"1kHz should pass with >90% amplitude, got {:.1}%",
ratio * 100.0
);
}
#[test]
fn test_highpass_attenuates_low_frequencies() {
let mut filter = BiquadFilter::highpass_butterworth(200.0, 16000);
let sample_rate = 16000.0;
let freq = 50.0;
let samples: Vec<i16> = (0..3200) .map(|i| {
let t = i as f64 / sample_rate;
(10000.0 * (2.0 * PI * freq * t).sin()) as i16
})
.collect();
let output = filter.process_i16_to_vec(&samples);
let input_rms: f64 = (samples[1600..]
.iter()
.map(|&s| (s as f64).powi(2))
.sum::<f64>()
/ 1600.0)
.sqrt();
let output_rms: f64 = (output[1600..]
.iter()
.map(|&s| (s as f64).powi(2))
.sum::<f64>()
/ 1600.0)
.sqrt();
let ratio = output_rms / input_rms;
assert!(
ratio < 0.35,
"50Hz should be attenuated to <35% at 200Hz cutoff, got {:.1}%",
ratio * 100.0
);
}
#[test]
fn test_reset() {
let mut filter = BiquadFilter::highpass_butterworth(100.0, 16000);
let samples: Vec<i16> = vec![1000; 100];
filter.process_i16_to_vec(&samples);
filter.reset();
assert_eq!(filter.x1, 0.0);
assert_eq!(filter.x2, 0.0);
assert_eq!(filter.y1, 0.0);
assert_eq!(filter.y2, 0.0);
}
}