use super::window::{WindowType, design_fir_filter};
use num_complex::Complex;
pub struct LowPassFir {
fir: Vec<f32>,
}
impl LowPassFir {
pub fn new(cutoff_freq: f32, sample_rate: f32, taps: usize) -> Self {
assert!(taps > 0, "Number of taps must be greater than 0");
assert!(sample_rate > 0.0, "Sample rate must be greater than 0");
let norm_cutoff = cutoff_freq / sample_rate;
let fir = design_fir_filter(norm_cutoff, taps, WindowType::Blackman);
Self { fir }
}
pub fn process(&self, samples: &[f32]) -> Vec<f32> {
let taps = self.fir.len();
let mid = taps / 2;
let mut out = vec![0.0f32; samples.len()];
for (i, out_elem) in out.iter_mut().enumerate() {
let mut acc = 0.0f32;
for j in 0..taps {
let idx = i as isize + j as isize - mid as isize;
if idx >= 0 && (idx as usize) < samples.len() {
acc += samples[idx as usize] * self.fir[j];
}
}
*out_elem = acc;
}
out
}
pub fn taps(&self) -> usize {
self.fir.len()
}
pub fn coefficients(&self) -> &[f32] {
&self.fir
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_lowpass_fir_creation() {
let filter = LowPassFir::new(15_000.0, 240_000.0, 256);
assert_eq!(filter.taps(), 256);
assert_eq!(filter.coefficients().len(), 256);
}
#[test]
#[should_panic(expected = "Number of taps must be greater than 0")]
fn test_lowpass_fir_zero_taps() {
let _ = LowPassFir::new(15_000.0, 240_000.0, 0);
}
#[test]
#[should_panic(expected = "Sample rate must be greater than 0")]
fn test_lowpass_fir_zero_sample_rate() {
let _ = LowPassFir::new(15_000.0, 0.0, 256);
}
#[test]
fn test_lowpass_fir_coefficients_normalized() {
let filter = LowPassFir::new(15_000.0, 240_000.0, 256);
let sum: f32 = filter.coefficients().iter().sum();
assert_relative_eq!(sum, 1.0, epsilon = 1e-6);
}
#[test]
fn test_lowpass_fir_dc_gain() {
let filter = LowPassFir::new(15_000.0, 240_000.0, 256);
let dc_value = 1.0;
let input = vec![dc_value; 1000];
let output = filter.process(&input);
for &sample in output.iter().skip(200).take(600) {
assert_relative_eq!(sample, dc_value, epsilon = 0.01);
}
}
#[test]
fn test_lowpass_fir_impulse_response() {
let filter = LowPassFir::new(15_000.0, 240_000.0, 256);
let mut input = vec![0.0; 500];
input[250] = 1.0;
let output = filter.process(&input);
let max_val = output.iter().fold(0.0f32, |a, &b| a.max(b));
assert!(
max_val > 0.0,
"Impulse response should have non-zero output"
);
let peak_idx = output
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.map(|(idx, _)| idx)
.unwrap();
assert!(
(peak_idx as isize - 250).abs() < 10,
"Peak should be near impulse position"
);
}
#[test]
fn test_lowpass_fir_zero_input() {
let filter = LowPassFir::new(15_000.0, 240_000.0, 256);
let input = vec![0.0; 100];
let output = filter.process(&input);
assert_eq!(output.len(), 100);
for &sample in &output {
assert_eq!(sample, 0.0);
}
}
#[test]
fn test_lowpass_fir_empty_input() {
let filter = LowPassFir::new(15_000.0, 240_000.0, 256);
let input: Vec<f32> = vec![];
let output = filter.process(&input);
assert_eq!(output.len(), 0);
}
#[test]
fn test_lowpass_fir_output_length() {
let filter = LowPassFir::new(15_000.0, 240_000.0, 256);
for len in [1, 10, 100, 1000] {
let input = vec![0.5; len];
let output = filter.process(&input);
assert_eq!(output.len(), len);
}
}
#[test]
fn test_lowpass_fir_different_tap_counts() {
for taps in [16, 32, 64, 128, 256, 512] {
let filter = LowPassFir::new(15_000.0, 240_000.0, taps);
assert_eq!(filter.taps(), taps);
let sum: f32 = filter.coefficients().iter().sum();
assert_relative_eq!(sum, 1.0, epsilon = 1e-6);
}
}
#[test]
fn test_lowpass_fir_cutoff_frequency_effect() {
let low_cutoff = LowPassFir::new(5_000.0, 240_000.0, 256);
let high_cutoff = LowPassFir::new(50_000.0, 240_000.0, 256);
let low_max = low_cutoff
.coefficients()
.iter()
.fold(0.0f32, |a, &b| a.max(b.abs()));
let high_max = high_cutoff
.coefficients()
.iter()
.fold(0.0f32, |a, &b| a.max(b.abs()));
assert!(
low_max < high_max,
"Lower cutoff should have smaller peak coefficient"
);
}
}
pub struct StatefulLowPassFir {
fir: Vec<f32>,
buffer: Vec<f32>,
write_pos: usize,
}
impl StatefulLowPassFir {
pub fn new(cutoff_freq: f32, sample_rate: f32, taps: usize) -> Self {
assert!(taps > 0, "Number of taps must be greater than 0");
assert!(sample_rate > 0.0, "Sample rate must be greater than 0");
let norm_cutoff = cutoff_freq / sample_rate;
let fir = super::window::design_fir_filter(
norm_cutoff,
taps,
super::window::WindowType::Blackman,
);
Self {
fir,
buffer: vec![0.0; taps],
write_pos: 0,
}
}
pub fn process(&mut self, samples: &[f32]) -> Vec<f32> {
let mut out = Vec::with_capacity(samples.len());
for &x in samples {
self.buffer[self.write_pos] = x;
self.write_pos = (self.write_pos + 1) % self.buffer.len();
let mut acc = 0.0_f32;
let len = self.fir.len();
for i in 0..len {
let buf_idx = (self.write_pos + len - 1 - i) % len;
acc += self.buffer[buf_idx] * self.fir[i];
}
out.push(acc);
}
out
}
pub fn taps(&self) -> usize {
self.fir.len()
}
pub fn coefficients(&self) -> &[f32] {
&self.fir
}
}
#[derive(Debug, Clone)]
pub struct ButterworthFilter {
coeffs: Vec<f64>,
real_state: Vec<f64>,
imag_state: Vec<f64>,
real_pos: usize,
imag_pos: usize,
}
impl ButterworthFilter {
pub fn lowpass(cutoff: f64, sample_rate: f64, order: usize) -> Self {
let taps = taps_from_order(order);
let fir = LowPassFir::new(cutoff as f32, sample_rate as f32, taps);
let coeffs = fir.coefficients().iter().map(|&c| c as f64).collect();
Self::from_coeffs(coeffs)
}
pub fn bandpass(low: f64, high: f64, sample_rate: f64, order: usize) -> Self {
let taps = taps_from_order(order);
let lp_high = LowPassFir::new(high as f32, sample_rate as f32, taps);
let lp_low = LowPassFir::new(low as f32, sample_rate as f32, taps);
let coeffs = lp_high
.coefficients()
.iter()
.zip(lp_low.coefficients().iter())
.map(|(h, l)| (*h - *l) as f64)
.collect();
Self::from_coeffs(coeffs)
}
fn from_coeffs(coeffs: Vec<f64>) -> Self {
let len = coeffs.len();
Self {
coeffs,
real_state: vec![0.0; len],
imag_state: vec![0.0; len],
real_pos: 0,
imag_pos: 0,
}
}
pub fn filter(&mut self, input: &[f64]) -> Vec<f64> {
let mut output = Vec::with_capacity(input.len());
for &x in input {
output.push(fir_step(
x,
&self.coeffs,
&mut self.real_state,
&mut self.real_pos,
));
}
output
}
pub fn filter_complex(&mut self, input: &[Complex<f32>]) -> Vec<Complex<f32>> {
let mut output = Vec::with_capacity(input.len());
for &x in input {
let r = fir_step(
x.re as f64,
&self.coeffs,
&mut self.real_state,
&mut self.real_pos,
);
let i = fir_step(
x.im as f64,
&self.coeffs,
&mut self.imag_state,
&mut self.imag_pos,
);
output.push(Complex::new(r as f32, i as f32));
}
output
}
}
fn taps_from_order(order: usize) -> usize {
let base = (order.max(1) * 16) + 1;
if base % 2 == 1 { base } else { base + 1 }
}
fn fir_step(x: f64, coeffs: &[f64], state: &mut [f64], pos: &mut usize) -> f64 {
state[*pos] = x;
let mut y = 0.0;
let mut idx = *pos;
for &c in coeffs {
y += c * state[idx];
idx = if idx == 0 { state.len() - 1 } else { idx - 1 };
}
*pos += 1;
if *pos == state.len() {
*pos = 0;
}
y
}