use biquad::*;
fn filtfilt_core(x: &[f64], coeffs: Coefficients<f64>, passes: usize) -> Vec<f64> {
let mut y = x.to_vec();
for _ in 0..passes {
let mut df1 = DirectForm1::<f64>::new(coeffs);
for sample in y.iter_mut() {
*sample = df1.run(*sample);
}
}
y.reverse();
for _ in 0..passes {
let mut df1 = DirectForm1::<f64>::new(coeffs);
for sample in y.iter_mut() {
*sample = df1.run(*sample);
}
}
y.reverse();
y
}
pub fn filtfilt_lowpass(x: &[f64], cutoff: f64, fs: f64, order: usize) -> Vec<f64> {
let hz = cutoff.hz();
let sample_rate = fs.hz();
let coeffs =
Coefficients::<f64>::from_params(Type::LowPass, sample_rate, hz, Q_BUTTERWORTH_F64)
.unwrap();
filtfilt_core(x, coeffs, order.div_ceil(2))
}
pub fn filtfilt_bandpass(x: &[f64], lowcut: f64, highcut: f64, fs: f64, order: usize) -> Vec<f64> {
let center = (lowcut + highcut) / 2.0;
let bandwidth = highcut - lowcut;
let hz = center.hz();
let sample_rate = fs.hz();
let q = center / bandwidth;
let coeffs = Coefficients::<f64>::from_params(Type::BandPass, sample_rate, hz, q).unwrap();
filtfilt_core(x, coeffs, order.div_ceil(2))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_filtfilt_lowpass_length() {
let signal = vec![1.0; 100];
let filtered = filtfilt_lowpass(&signal, 100.0, 1000.0, 2);
assert_eq!(filtered.len(), 100);
}
#[test]
fn test_filtfilt_lowpass_dc_passthrough() {
let signal = vec![1.0; 1000];
let filtered = filtfilt_lowpass(&signal, 100.0, 1000.0, 4);
for &v in &filtered[100..900] {
assert!((v - 1.0).abs() < 0.05, "DC passthrough failed: {}", v);
}
}
#[test]
fn test_filtfilt_bandpass_length() {
let signal = vec![0.5; 100];
let filtered = filtfilt_bandpass(&signal, 100.0, 200.0, 1000.0, 2);
assert_eq!(filtered.len(), 100);
}
#[test]
fn test_filtfilt_bandpass_rejects_dc() {
let signal = vec![1.0; 1000];
let filtered = filtfilt_bandpass(&signal, 100.0, 200.0, 1000.0, 4);
for &v in &filtered[200..800] {
assert!(v.abs() < 0.1, "DC should be rejected: {}", v);
}
}
#[test]
fn test_filtfilt_zero_phase() {
let mut signal = vec![0.0; 500];
signal[250] = 1.0;
let filtered = filtfilt_lowpass(&signal, 100.0, 1000.0, 2);
let peak_idx = filtered
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.abs().partial_cmp(&b.abs()).unwrap())
.map(|(i, _)| i)
.unwrap();
assert!(
(peak_idx as i64 - 250).abs() <= 1,
"Peak shifted to {}, expected ~250",
peak_idx
);
}
}