#![forbid(unsafe_code)]
#![allow(clippy::cast_precision_loss)]
use std::f32::consts::PI;
#[derive(Clone, Debug, PartialEq)]
#[allow(dead_code)]
pub struct BiquadCoeffs {
pub b0: f32,
pub b1: f32,
pub b2: f32,
pub a1: f32,
pub a2: f32,
}
impl BiquadCoeffs {
#[must_use]
pub fn identity() -> Self {
Self {
b0: 1.0,
b1: 0.0,
b2: 0.0,
a1: 0.0,
a2: 0.0,
}
}
#[must_use]
pub fn lowpass(freq_hz: f32, q: f32, sample_rate: f32) -> Self {
let w0 = 2.0 * PI * freq_hz / sample_rate;
let cos_w0 = w0.cos();
let sin_w0 = w0.sin();
let alpha = sin_w0 / (2.0 * q);
let b0 = (1.0 - cos_w0) / 2.0;
let b1 = 1.0 - cos_w0;
let b2 = (1.0 - cos_w0) / 2.0;
let a0 = 1.0 + alpha;
let a1 = -2.0 * cos_w0;
let a2 = 1.0 - alpha;
Self {
b0: b0 / a0,
b1: b1 / a0,
b2: b2 / a0,
a1: a1 / a0,
a2: a2 / a0,
}
}
#[must_use]
pub fn highpass(freq_hz: f32, q: f32, sample_rate: f32) -> Self {
let w0 = 2.0 * PI * freq_hz / sample_rate;
let cos_w0 = w0.cos();
let sin_w0 = w0.sin();
let alpha = sin_w0 / (2.0 * q);
let b0 = (1.0 + cos_w0) / 2.0;
let b1 = -(1.0 + cos_w0);
let b2 = (1.0 + cos_w0) / 2.0;
let a0 = 1.0 + alpha;
let a1 = -2.0 * cos_w0;
let a2 = 1.0 - alpha;
Self {
b0: b0 / a0,
b1: b1 / a0,
b2: b2 / a0,
a1: a1 / a0,
a2: a2 / a0,
}
}
#[must_use]
pub fn bandpass(freq_hz: f32, bw_hz: f32, sample_rate: f32) -> Self {
let w0 = 2.0 * PI * freq_hz / sample_rate;
let cos_w0 = w0.cos();
let sin_w0 = w0.sin();
let q = freq_hz / bw_hz.max(1e-6);
let alpha = sin_w0 / (2.0 * q);
let b0 = alpha;
let b1 = 0.0_f32;
let b2 = -alpha;
let a0 = 1.0 + alpha;
let a1 = -2.0 * cos_w0;
let a2 = 1.0 - alpha;
Self {
b0: b0 / a0,
b1: b1 / a0,
b2: b2 / a0,
a1: a1 / a0,
a2: a2 / a0,
}
}
#[must_use]
pub fn peaking_eq(freq_hz: f32, gain_db: f32, q: f32, sample_rate: f32) -> Self {
let w0 = 2.0 * PI * freq_hz / sample_rate;
let cos_w0 = w0.cos();
let sin_w0 = w0.sin();
let alpha = sin_w0 / (2.0 * q);
let a = 10.0_f32.powf(gain_db / 40.0);
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;
Self {
b0: b0 / a0,
b1: b1 / a0,
b2: b2 / a0,
a1: a1 / a0,
a2: a2 / a0,
}
}
}
#[derive(Clone, Debug, Default)]
#[allow(dead_code)]
pub struct BiquadState {
pub x1: f32,
pub x2: f32,
pub y1: f32,
pub y2: f32,
}
impl BiquadState {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn reset(&mut self) {
self.x1 = 0.0;
self.x2 = 0.0;
self.y1 = 0.0;
self.y2 = 0.0;
}
}
#[derive(Clone, Debug)]
#[allow(dead_code)]
pub struct BiquadFilter {
pub coeffs: BiquadCoeffs,
pub state: BiquadState,
}
impl BiquadFilter {
#[must_use]
pub fn new(coeffs: BiquadCoeffs) -> Self {
Self {
coeffs,
state: BiquadState::new(),
}
}
pub fn process(&mut self, sample: f32) -> f32 {
let output = self.coeffs.b0 * sample
+ self.coeffs.b1 * self.state.x1
+ self.coeffs.b2 * self.state.x2
- self.coeffs.a1 * self.state.y1
- self.coeffs.a2 * self.state.y2;
self.state.x2 = self.state.x1;
self.state.x1 = sample;
self.state.y2 = self.state.y1;
self.state.y1 = output;
output
}
#[must_use]
pub fn process_block(&mut self, samples: &[f32]) -> Vec<f32> {
samples.iter().map(|&s| self.process(s)).collect()
}
pub fn reset(&mut self) {
self.state.reset();
}
pub fn set_coeffs(&mut self, coeffs: BiquadCoeffs) {
self.coeffs = coeffs;
}
}
#[cfg(test)]
mod tests {
use super::*;
const SR: f32 = 48_000.0;
#[test]
fn test_identity_coefficients_values() {
let c = BiquadCoeffs::identity();
assert_eq!(c.b0, 1.0);
assert_eq!(c.b1, 0.0);
assert_eq!(c.b2, 0.0);
assert_eq!(c.a1, 0.0);
assert_eq!(c.a2, 0.0);
}
#[test]
fn test_identity_is_passthrough() {
let mut f = BiquadFilter::new(BiquadCoeffs::identity());
let sample = 0.75_f32;
assert!((f.process(sample) - sample).abs() < 1e-7);
}
#[test]
fn test_lowpass_coeffs_are_finite() {
let c = BiquadCoeffs::lowpass(1000.0, 0.707, SR);
assert!(c.b0.is_finite() && c.b1.is_finite() && c.b2.is_finite());
assert!(c.a1.is_finite() && c.a2.is_finite());
}
#[test]
fn test_highpass_coeffs_are_finite() {
let c = BiquadCoeffs::highpass(1000.0, 0.707, SR);
assert!(c.b0.is_finite() && c.b1.is_finite() && c.b2.is_finite());
assert!(c.a1.is_finite() && c.a2.is_finite());
}
#[test]
fn test_bandpass_b1_is_zero() {
let c = BiquadCoeffs::bandpass(1000.0, 200.0, SR);
assert!(c.b1.abs() < 1e-7, "b1 should be zero, got {}", c.b1);
assert!(
(c.b0 + c.b2).abs() < 1e-6,
"b0 should equal -b2, b0={} b2={}",
c.b0,
c.b2
);
}
#[test]
fn test_peaking_eq_zero_gain_is_nearly_identity() {
let c = BiquadCoeffs::peaking_eq(1000.0, 0.0, 1.0, SR);
let mut f = BiquadFilter::new(c);
let mut out = 0.0_f32;
for _ in 0..500 {
out = f.process(1.0);
}
assert!(
(out - 1.0).abs() < 0.02,
"0 dB peaking should be near unity at DC, got {out}"
);
}
#[test]
fn test_state_reset_clears_memory() {
let mut f = BiquadFilter::new(BiquadCoeffs::lowpass(500.0, 0.707, SR));
for _ in 0..100 {
f.process(1.0);
}
f.reset();
let out = f.process(0.0);
assert_eq!(out, 0.0, "Output after reset+silence should be 0");
}
#[test]
fn test_biquad_state_new_is_zeroed() {
let s = BiquadState::new();
assert_eq!(s.x1, 0.0);
assert_eq!(s.x2, 0.0);
assert_eq!(s.y1, 0.0);
assert_eq!(s.y2, 0.0);
}
#[test]
fn test_lowpass_passes_dc() {
let mut f = BiquadFilter::new(BiquadCoeffs::lowpass(4000.0, 0.707, SR));
let mut out = 0.0_f32;
for _ in 0..2000 {
out = f.process(1.0);
}
assert!(out > 0.9, "Low-pass should pass DC; got {out}");
}
#[test]
fn test_highpass_blocks_dc() {
let mut f = BiquadFilter::new(BiquadCoeffs::highpass(1000.0, 0.707, SR));
let mut out = 0.0_f32;
for _ in 0..2000 {
out = f.process(1.0);
}
assert!(out.abs() < 0.01, "High-pass should block DC; got {out}");
}
#[test]
fn test_peaking_boost_amplifies_at_center() {
let c = BiquadCoeffs::peaking_eq(1000.0, 12.0, 1.0, SR);
let mut f = BiquadFilter::new(c);
let freq = 1000.0_f32;
let mut peak_in = 0.0_f32;
let mut peak_out = 0.0_f32;
for i in 0..4800_usize {
let s = (2.0 * PI * freq * i as f32 / SR).sin();
peak_in = peak_in.max(s.abs());
let y = f.process(s);
if i > 960 {
peak_out = peak_out.max(y.abs());
}
}
assert!(
peak_out > peak_in * 1.5,
"Peaking boost should increase amplitude; in={peak_in} out={peak_out}"
);
}
#[test]
fn test_peaking_cut_attenuates_at_center() {
let c = BiquadCoeffs::peaking_eq(1000.0, -12.0, 1.0, SR);
let mut f = BiquadFilter::new(c);
let freq = 1000.0_f32;
let mut peak_out = 0.0_f32;
for i in 0..4800_usize {
let s = (2.0 * PI * freq * i as f32 / SR).sin();
let y = f.process(s);
if i > 960 {
peak_out = peak_out.max(y.abs());
}
}
assert!(
peak_out < 0.5,
"Peaking cut should attenuate; peak_out={peak_out}"
);
}
#[test]
fn test_process_block_matches_per_sample() {
let c = BiquadCoeffs::lowpass(2000.0, 0.707, SR);
let input: Vec<f32> = (0..128).map(|i| (i as f32 * 0.05).sin()).collect();
let mut f1 = BiquadFilter::new(c.clone());
let expected: Vec<f32> = input.iter().map(|&s| f1.process(s)).collect();
let mut f2 = BiquadFilter::new(c);
let got = f2.process_block(&input);
for (e, g) in expected.iter().zip(got.iter()) {
assert!((e - g).abs() < 1e-6, "Mismatch: expected {e} got {g}");
}
}
#[test]
fn test_process_block_output_length_matches_input() {
let mut f = BiquadFilter::new(BiquadCoeffs::highpass(500.0, 0.707, SR));
let input = vec![0.1_f32; 256];
let output = f.process_block(&input);
assert_eq!(output.len(), input.len());
}
#[test]
fn test_process_block_all_finite() {
let mut f = BiquadFilter::new(BiquadCoeffs::bandpass(1000.0, 200.0, SR));
let input: Vec<f32> = (0..256).map(|i| (i as f32).sin()).collect();
let output = f.process_block(&input);
assert!(
output.iter().all(|x| x.is_finite()),
"All outputs must be finite"
);
}
#[test]
fn test_set_coeffs_updates_filter() {
let mut f = BiquadFilter::new(BiquadCoeffs::identity());
let out_before = f.process(1.0);
assert!((out_before - 1.0).abs() < 1e-7);
f.set_coeffs(BiquadCoeffs::highpass(4000.0, 0.707, SR));
f.reset();
let mut out = 0.0_f32;
for _ in 0..2000 {
out = f.process(1.0);
}
assert!(
out.abs() < 0.05,
"HP should block DC after set_coeffs; got {out}"
);
}
}