use std::f64::consts::{FRAC_1_SQRT_2, PI};
pub(super) struct BiquadHighPass {
b0: f32,
b1: f32,
b2: f32,
a1: f32,
a2: f32,
x1: f32,
x2: f32,
y1: f32,
y2: f32,
}
impl BiquadHighPass {
pub fn new(cutoff_hz: f32, sample_rate: u32) -> Self {
let omega = 2.0 * PI * (cutoff_hz as f64) / (sample_rate as f64);
let cos_omega = omega.cos();
let sin_omega = omega.sin();
let alpha = sin_omega / (2.0 * FRAC_1_SQRT_2);
let a0 = 1.0 + alpha;
let b0 = ((1.0 + cos_omega) / 2.0) / a0;
let b1 = (-(1.0 + cos_omega)) / a0;
let b2 = ((1.0 + cos_omega) / 2.0) / a0;
let a1 = (-2.0 * cos_omega) / a0;
let a2 = (1.0 - alpha) / a0;
Self {
b0: b0 as f32,
b1: b1 as f32,
b2: b2 as f32,
a1: a1 as f32,
a2: a2 as f32,
x1: 0.0,
x2: 0.0,
y1: 0.0,
y2: 0.0,
}
}
pub fn process(&mut self, sample: f32) -> f32 {
let output = self.b0 * sample + self.b1 * self.x1 + self.b2 * self.x2
- self.a1 * self.y1
- self.a2 * self.y2;
self.x2 = self.x1;
self.x1 = sample;
self.y2 = self.y1;
self.y1 = output;
output
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dc_rejection() {
let mut filter = BiquadHighPass::new(80.0, 44100);
for _ in 0..10000 {
filter.process(1.0);
}
let out = filter.process(1.0);
assert!(out.abs() < 0.001, "DC should be rejected, got {}", out);
}
#[test]
fn test_high_frequency_passthrough() {
let mut filter = BiquadHighPass::new(80.0, 44100);
let freq = 4000.0_f64; let sample_rate = 44100.0_f64;
for i in 0..4410 {
let sample = (2.0 * PI * freq * (i as f64) / sample_rate).sin() as f32;
filter.process(sample);
}
let cycle_samples = (sample_rate / freq).ceil() as usize;
let mut max_out: f32 = 0.0;
for i in 4410..(4410 + cycle_samples) {
let sample = (2.0 * PI * freq * (i as f64) / sample_rate).sin() as f32;
let out = filter.process(sample);
max_out = max_out.max(out.abs());
}
assert!(
max_out > 0.9,
"4kHz should pass through, got amplitude {}",
max_out
);
}
#[test]
fn test_below_cutoff_attenuated() {
let mut filter = BiquadHighPass::new(200.0, 44100);
let freq = 20.0_f64; let sample_rate = 44100.0_f64;
for i in 0..44100 {
let sample = (2.0 * PI * freq * (i as f64) / sample_rate).sin() as f32;
filter.process(sample);
}
let cycle_samples = (sample_rate / freq).ceil() as usize;
let mut max_out: f32 = 0.0;
for i in 44100..(44100 + cycle_samples) {
let sample = (2.0 * PI * freq * (i as f64) / sample_rate).sin() as f32;
let out = filter.process(sample);
max_out = max_out.max(out.abs());
}
assert!(
max_out < 0.05,
"20Hz should be attenuated below 200Hz cutoff, got amplitude {}",
max_out
);
}
#[test]
fn test_gain_at_cutoff_is_minus_3db() {
let cutoff = 200.0_f64;
let sample_rate = 44100.0_f64;
let mut filter = BiquadHighPass::new(cutoff as f32, sample_rate as u32);
for i in 0..44100 {
let sample = (2.0 * PI * cutoff * (i as f64) / sample_rate).sin() as f32;
filter.process(sample);
}
let cycle_samples = (sample_rate / cutoff).ceil() as usize;
let mut max_out: f32 = 0.0;
for i in 44100..(44100 + cycle_samples) {
let sample = (2.0 * PI * cutoff * (i as f64) / sample_rate).sin() as f32;
let out = filter.process(sample);
max_out = max_out.max(out.abs());
}
assert!(
max_out > 0.65 && max_out < 0.75,
"Expected ~0.707 gain at cutoff, got {}",
max_out
);
}
#[test]
fn test_zero_input() {
let mut filter = BiquadHighPass::new(80.0, 44100);
for _ in 0..1000 {
let out = filter.process(0.0);
assert!(
out.abs() < f32::EPSILON,
"Zero input should produce zero output, got {}",
out
);
}
}
}