use crate::params::FilterMode;
use core::f32::consts::PI;
pub struct SvFilter {
ic1eq: f32, ic2eq: f32, }
impl Default for SvFilter {
fn default() -> Self {
Self {
ic1eq: 0.0,
ic2eq: 0.0,
}
}
}
impl SvFilter {
pub fn reset(&mut self) {
self.ic1eq = 0.0;
self.ic2eq = 0.0;
}
pub fn process(
&mut self,
input: f32,
mode: FilterMode,
cutoff: f32,
resonance: f32,
drive: f32,
sample_rate: f32,
) -> f32 {
let x = if drive > 0.001 {
let gain = 1.0 + drive * 4.0;
let x = input * gain;
let clipped = x / crate::math::sqrtf(1.0 + x * x);
if clipped.is_finite() {
clipped
} else {
core::hint::cold_path();
1.0_f32.copysign(x)
}
} else {
input
};
let nyquist = (sample_rate * 0.499).max(20.0);
let fc = cutoff.clamp(20.0, nyquist);
let fc_norm = (fc / sample_rate).min(0.499_f32);
let g = crate::math::tanf(PI * fc_norm);
let k = (2.0 - 1.98 * resonance.clamp(0.0, 0.999)).max(0.01);
let a1 = 1.0 / (1.0 + g * (g + k));
let a2 = g * a1;
let a3 = g * a2;
let v3 = x - self.ic2eq;
let v1 = a1 * self.ic1eq + a2 * v3;
let v2 = self.ic2eq + a2 * self.ic1eq + a3 * v3;
self.ic1eq = 2.0 * v1 - self.ic1eq;
self.ic2eq = 2.0 * v2 - self.ic2eq;
self.ic1eq = clamp_denormal(self.ic1eq);
self.ic2eq = clamp_denormal(self.ic2eq);
let out = match mode {
FilterMode::LowPass => v2,
FilterMode::BandPass => fast_tanh(v1 * (1.0 + resonance * 0.5)),
FilterMode::HighPass => x - k * v1 - v2,
};
if out.is_finite() {
out
} else {
core::hint::cold_path();
0.0
}
}
}
#[inline]
fn clamp_denormal(x: f32) -> f32 {
if !x.is_finite() || x.abs() < 1e-15 {
core::hint::cold_path();
0.0
} else {
x
}
}
#[inline]
fn fast_tanh(x: f32) -> f32 {
if x >= 4.0 {
core::hint::cold_path();
return 1.0;
}
if x <= -4.0 {
core::hint::cold_path();
return -1.0;
}
let x2 = x * x;
let n = x * (135.0 + x2 * (17.0 + x2));
let d = 135.0 + x2 * (45.0 + x2 * 9.0);
(n / d).clamp(-1.0, 1.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(clippy::cast_precision_loss)] fn rms(samples: &[f32]) -> f32 {
let sum_sq: f32 = samples.iter().map(|&s| s * s).sum();
(sum_sq / samples.len() as f32).sqrt()
}
#[test]
#[allow(clippy::cast_precision_loss)] fn lp_attenuates_high_freq() {
let mut filt = SvFilter::default();
let sr = 44100.0_f32;
let tone: Vec<f32> = (0..4096)
.map(|i| crate::math::sinf(2.0 * core::f32::consts::PI * 1000.0 * i as f32 / sr))
.collect();
let filtered: Vec<f32> = tone
.iter()
.map(|&s| filt.process(s, FilterMode::LowPass, 200.0, 0.1, 0.0, sr))
.collect();
let ratio = rms(&filtered) / rms(&tone[2048..]); assert!(ratio < 0.5, "LP did not attenuate: ratio={ratio}");
}
#[test]
#[allow(clippy::cast_precision_loss)] fn hp_attenuates_low_freq() {
let mut filt = SvFilter::default();
let sr = 44100.0_f32;
let tone: Vec<f32> = (0..8192)
.map(|i| crate::math::sinf(2.0 * core::f32::consts::PI * 50.0 * i as f32 / sr))
.collect();
let filtered: Vec<f32> = tone
.iter()
.map(|&s| filt.process(s, FilterMode::HighPass, 500.0, 0.1, 0.0, sr))
.collect();
let ratio = rms(&filtered[4096..]) / rms(&tone[4096..]);
assert!(ratio < 0.5, "HP did not attenuate: ratio={ratio}");
}
#[test]
fn no_nan_under_extreme_params() {
let mut filt = SvFilter::default();
let input = 1.0_f32;
for &res in &[0.0_f32, 0.5, 0.95, 0.999] {
for &drive in &[0.0_f32, 0.5, 1.0] {
let out = filt.process(input, FilterMode::LowPass, 100.0, res, drive, 44100.0);
assert!(out.is_finite(), "NaN/Inf at res={res}, drive={drive}");
}
}
}
}