Skip to main content

proof_engine/dsp/
filters.rs

1//! Digital filter library — Biquad IIR, higher-order cascades, FIR, convolution,
2//! state-variable filter, comb, allpass, moving average, Kalman, PLL.
3
4use std::f32::consts::PI;
5use super::{WindowFunction, sinc, next_power_of_two};
6use super::fft::{Fft, Complex32};
7
8// ---------------------------------------------------------------------------
9// BiquadType
10// ---------------------------------------------------------------------------
11
12/// Type tag for a biquad section.
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum BiquadType {
15    LowPass,
16    HighPass,
17    BandPass,
18    Notch,
19    Peak,
20    LowShelf,
21    HighShelf,
22    AllPass,
23}
24
25// ---------------------------------------------------------------------------
26// Biquad — Direct Form II Transposed
27// ---------------------------------------------------------------------------
28
29/// Second-order IIR biquad filter (Direct Form II Transposed).
30///
31/// Transfer function:  H(z) = (b0 + b1·z⁻¹ + b2·z⁻²) / (1 + a1·z⁻¹ + a2·z⁻²)
32#[derive(Debug, Clone)]
33pub struct Biquad {
34    // Feed-forward coefficients
35    pub b0: f32,
36    pub b1: f32,
37    pub b2: f32,
38    // Feed-back coefficients (negated convention: denominator is 1 + a1z + a2z²)
39    pub a1: f32,
40    pub a2: f32,
41    // State variables
42    pub z1: f32,
43    pub z2: f32,
44    /// The kind of filter (informational).
45    pub filter_type: BiquadType,
46}
47
48impl Biquad {
49    /// Create a biquad from raw coefficients.
50    pub fn new(b0: f32, b1: f32, b2: f32, a1: f32, a2: f32, filter_type: BiquadType) -> Self {
51        Self { b0, b1, b2, a1, a2, z1: 0.0, z2: 0.0, filter_type }
52    }
53
54    /// Identity (pass-through) biquad.
55    pub fn identity() -> Self {
56        Self::new(1.0, 0.0, 0.0, 0.0, 0.0, BiquadType::AllPass)
57    }
58
59    /// Process a single sample (Direct Form II Transposed).
60    #[inline]
61    pub fn process_sample(&mut self, x: f32) -> f32 {
62        let y = self.b0 * x + self.z1;
63        self.z1 = self.b1 * x - self.a1 * y + self.z2;
64        self.z2 = self.b2 * x - self.a2 * y;
65        y
66    }
67
68    /// In-place block processing.
69    pub fn process(&mut self, buffer: &mut [f32]) {
70        for s in buffer.iter_mut() {
71            *s = self.process_sample(*s);
72        }
73    }
74
75    /// Clear state variables.
76    pub fn reset(&mut self) {
77        self.z1 = 0.0;
78        self.z2 = 0.0;
79    }
80
81    /// Frequency response magnitude at normalized frequency ω (0..π).
82    pub fn magnitude_response(&self, omega: f32) -> f32 {
83        let z = Complex32::from_polar(1.0, omega);
84        let z_inv = z.conj(); // z⁻¹ = e^{-jω} for |z|=1
85        let z_inv2 = z_inv * z_inv;
86        let num = Complex32::new(self.b0, 0.0)
87            + Complex32::new(self.b1, 0.0) * z_inv
88            + Complex32::new(self.b2, 0.0) * z_inv2;
89        let den = Complex32::new(1.0, 0.0)
90            + Complex32::new(self.a1, 0.0) * z_inv
91            + Complex32::new(self.a2, 0.0) * z_inv2;
92        (num / den).norm()
93    }
94}
95
96// ---------------------------------------------------------------------------
97// BiquadDesign — RBJ Audio EQ Cookbook
98// ---------------------------------------------------------------------------
99
100/// Coefficient calculation following the RBJ Audio EQ Cookbook.
101pub struct BiquadDesign;
102
103impl BiquadDesign {
104    fn omega(cutoff_hz: f32, sample_rate: f32) -> f32 {
105        2.0 * PI * cutoff_hz / sample_rate
106    }
107
108    /// 2nd-order lowpass Butterworth.
109    pub fn lowpass(cutoff_hz: f32, q: f32, sample_rate: f32) -> Biquad {
110        let w0 = Self::omega(cutoff_hz, sample_rate);
111        let cos_w0 = w0.cos();
112        let sin_w0 = w0.sin();
113        let alpha = sin_w0 / (2.0 * q);
114        let b1 = 1.0 - cos_w0;
115        let b0 = b1 / 2.0;
116        let b2 = b0;
117        let a0 = 1.0 + alpha;
118        Biquad::new(
119            b0 / a0, b1 / a0, b2 / a0,
120            (-2.0 * cos_w0) / a0, (1.0 - alpha) / a0,
121            BiquadType::LowPass,
122        )
123    }
124
125    /// 2nd-order highpass Butterworth.
126    pub fn highpass(cutoff_hz: f32, q: f32, sample_rate: f32) -> Biquad {
127        let w0 = Self::omega(cutoff_hz, sample_rate);
128        let cos_w0 = w0.cos();
129        let sin_w0 = w0.sin();
130        let alpha = sin_w0 / (2.0 * q);
131        let b0 = (1.0 + cos_w0) / 2.0;
132        let b1 = -(1.0 + cos_w0);
133        let b2 = b0;
134        let a0 = 1.0 + alpha;
135        Biquad::new(
136            b0 / a0, b1 / a0, b2 / a0,
137            (-2.0 * cos_w0) / a0, (1.0 - alpha) / a0,
138            BiquadType::HighPass,
139        )
140    }
141
142    /// 2nd-order bandpass (constant 0 dB peak gain, BPF skirt gain = Q).
143    pub fn bandpass(center_hz: f32, bandwidth_hz: f32, sample_rate: f32) -> Biquad {
144        let q = center_hz / bandwidth_hz.max(1e-3);
145        let w0 = Self::omega(center_hz, sample_rate);
146        let cos_w0 = w0.cos();
147        let sin_w0 = w0.sin();
148        let alpha = sin_w0 / (2.0 * q);
149        let b0 = alpha;
150        let b1 = 0.0;
151        let b2 = -alpha;
152        let a0 = 1.0 + alpha;
153        Biquad::new(
154            b0 / a0, b1 / a0, b2 / a0,
155            (-2.0 * cos_w0) / a0, (1.0 - alpha) / a0,
156            BiquadType::BandPass,
157        )
158    }
159
160    /// 2nd-order notch (band-reject).
161    pub fn notch(center_hz: f32, q: f32, sample_rate: f32) -> Biquad {
162        let w0 = Self::omega(center_hz, sample_rate);
163        let cos_w0 = w0.cos();
164        let sin_w0 = w0.sin();
165        let alpha = sin_w0 / (2.0 * q);
166        let b0 = 1.0;
167        let b1 = -2.0 * cos_w0;
168        let b2 = 1.0;
169        let a0 = 1.0 + alpha;
170        Biquad::new(
171            b0 / a0, b1 / a0, b2 / a0,
172            (-2.0 * cos_w0) / a0, (1.0 - alpha) / a0,
173            BiquadType::Notch,
174        )
175    }
176
177    /// Peak EQ.
178    pub fn peak_eq(center_hz: f32, gain_db: f32, q: f32, sample_rate: f32) -> Biquad {
179        let w0 = Self::omega(center_hz, sample_rate);
180        let cos_w0 = w0.cos();
181        let sin_w0 = w0.sin();
182        let a_lin = 10.0f32.powf(gain_db / 40.0);
183        let alpha = sin_w0 / (2.0 * q);
184        let b0 = 1.0 + alpha * a_lin;
185        let b1 = -2.0 * cos_w0;
186        let b2 = 1.0 - alpha * a_lin;
187        let a0 = 1.0 + alpha / a_lin;
188        let a1_r = -2.0 * cos_w0;
189        let a2_r = 1.0 - alpha / a_lin;
190        Biquad::new(
191            b0 / a0, b1 / a0, b2 / a0,
192            a1_r / a0, a2_r / a0,
193            BiquadType::Peak,
194        )
195    }
196
197    /// Low shelf.
198    pub fn low_shelf(cutoff_hz: f32, gain_db: f32, slope: f32, sample_rate: f32) -> Biquad {
199        let w0 = Self::omega(cutoff_hz, sample_rate);
200        let cos_w0 = w0.cos();
201        let sin_w0 = w0.sin();
202        let a_lin = 10.0f32.powf(gain_db / 40.0);
203        let alpha = sin_w0 / 2.0 * ((a_lin + 1.0 / a_lin) * (1.0 / slope - 1.0) + 2.0).sqrt();
204        let a_p1 = a_lin + 1.0;
205        let a_m1 = a_lin - 1.0;
206        let b0 = a_lin * (a_p1 - a_m1 * cos_w0 + 2.0 * a_lin.sqrt() * alpha);
207        let b1 = 2.0 * a_lin * (a_m1 - a_p1 * cos_w0);
208        let b2 = a_lin * (a_p1 - a_m1 * cos_w0 - 2.0 * a_lin.sqrt() * alpha);
209        let a0 = a_p1 + a_m1 * cos_w0 + 2.0 * a_lin.sqrt() * alpha;
210        let a1_r = -2.0 * (a_m1 + a_p1 * cos_w0);
211        let a2_r = a_p1 + a_m1 * cos_w0 - 2.0 * a_lin.sqrt() * alpha;
212        Biquad::new(
213            b0 / a0, b1 / a0, b2 / a0,
214            a1_r / a0, a2_r / a0,
215            BiquadType::LowShelf,
216        )
217    }
218
219    /// High shelf.
220    pub fn high_shelf(cutoff_hz: f32, gain_db: f32, slope: f32, sample_rate: f32) -> Biquad {
221        let w0 = Self::omega(cutoff_hz, sample_rate);
222        let cos_w0 = w0.cos();
223        let sin_w0 = w0.sin();
224        let a_lin = 10.0f32.powf(gain_db / 40.0);
225        let alpha = sin_w0 / 2.0 * ((a_lin + 1.0 / a_lin) * (1.0 / slope - 1.0) + 2.0).sqrt();
226        let a_p1 = a_lin + 1.0;
227        let a_m1 = a_lin - 1.0;
228        let b0 = a_lin * (a_p1 + a_m1 * cos_w0 + 2.0 * a_lin.sqrt() * alpha);
229        let b1 = -2.0 * a_lin * (a_m1 + a_p1 * cos_w0);
230        let b2 = a_lin * (a_p1 + a_m1 * cos_w0 - 2.0 * a_lin.sqrt() * alpha);
231        let a0 = a_p1 - a_m1 * cos_w0 + 2.0 * a_lin.sqrt() * alpha;
232        let a1_r = 2.0 * (a_m1 - a_p1 * cos_w0);
233        let a2_r = a_p1 - a_m1 * cos_w0 - 2.0 * a_lin.sqrt() * alpha;
234        Biquad::new(
235            b0 / a0, b1 / a0, b2 / a0,
236            a1_r / a0, a2_r / a0,
237            BiquadType::HighShelf,
238        )
239    }
240
241    /// 2nd-order allpass.
242    pub fn allpass(cutoff_hz: f32, q: f32, sample_rate: f32) -> Biquad {
243        let w0 = Self::omega(cutoff_hz, sample_rate);
244        let cos_w0 = w0.cos();
245        let sin_w0 = w0.sin();
246        let alpha = sin_w0 / (2.0 * q);
247        let b0 = 1.0 - alpha;
248        let b1 = -2.0 * cos_w0;
249        let b2 = 1.0 + alpha;
250        let a0 = 1.0 + alpha;
251        Biquad::new(
252            b0 / a0, b1 / a0, b2 / a0,
253            (-2.0 * cos_w0) / a0, (1.0 - alpha) / a0,
254            BiquadType::AllPass,
255        )
256    }
257}
258
259// ---------------------------------------------------------------------------
260// FilterChain — cascaded biquads
261// ---------------------------------------------------------------------------
262
263/// A cascade of biquad sections for higher-order filtering.
264#[derive(Debug, Clone)]
265pub struct FilterChain {
266    pub stages: Vec<Biquad>,
267}
268
269impl FilterChain {
270    pub fn new() -> Self { Self { stages: Vec::new() } }
271
272    pub fn with_capacity(n: usize) -> Self {
273        Self { stages: Vec::with_capacity(n) }
274    }
275
276    /// Add a biquad stage.
277    pub fn push(&mut self, biquad: Biquad) {
278        self.stages.push(biquad);
279    }
280
281    /// Process a single sample through all stages.
282    #[inline]
283    pub fn process_sample(&mut self, x: f32) -> f32 {
284        let mut y = x;
285        for stage in self.stages.iter_mut() {
286            y = stage.process_sample(y);
287        }
288        y
289    }
290
291    /// In-place block processing.
292    pub fn process(&mut self, buffer: &mut [f32]) {
293        for s in buffer.iter_mut() {
294            *s = self.process_sample(*s);
295        }
296    }
297
298    /// Reset all stages.
299    pub fn reset(&mut self) {
300        for stage in self.stages.iter_mut() { stage.reset(); }
301    }
302
303    /// Number of biquad stages.
304    pub fn num_stages(&self) -> usize { self.stages.len() }
305
306    /// Overall magnitude response at normalized frequency ω.
307    pub fn magnitude_response(&self, omega: f32) -> f32 {
308        self.stages.iter().map(|b| b.magnitude_response(omega)).product()
309    }
310}
311
312impl Default for FilterChain {
313    fn default() -> Self { Self::new() }
314}
315
316// ---------------------------------------------------------------------------
317// Butterworth
318// ---------------------------------------------------------------------------
319
320/// Butterworth filter design (maximally flat in passband).
321pub struct Butterworth;
322
323impl Butterworth {
324    /// Nth-order Butterworth lowpass, implemented as cascaded biquads.
325    pub fn lowpass(order: u32, cutoff_hz: f32, sample_rate: f32) -> FilterChain {
326        let mut chain = FilterChain::with_capacity(order as usize / 2 + 1);
327        let n_stages = order / 2;
328        for k in 1..=n_stages {
329            // Pole angle for Butterworth: π(2k + n - 1) / (2n)
330            let theta = PI * (2 * k + order - 1) as f32 / (2 * order) as f32;
331            let q = -1.0 / (2.0 * theta.cos()); // Q from pole angle
332            chain.push(BiquadDesign::lowpass(cutoff_hz, q, sample_rate));
333        }
334        if order % 2 == 1 {
335            // First-order stage: lowpass with Q=0.5 (no resonance)
336            chain.push(BiquadDesign::lowpass(cutoff_hz, 0.5, sample_rate));
337        }
338        chain
339    }
340
341    /// Nth-order Butterworth highpass.
342    pub fn highpass(order: u32, cutoff_hz: f32, sample_rate: f32) -> FilterChain {
343        let mut chain = FilterChain::with_capacity(order as usize / 2 + 1);
344        let n_stages = order / 2;
345        for k in 1..=n_stages {
346            let theta = PI * (2 * k + order - 1) as f32 / (2 * order) as f32;
347            let q = -1.0 / (2.0 * theta.cos());
348            chain.push(BiquadDesign::highpass(cutoff_hz, q, sample_rate));
349        }
350        if order % 2 == 1 {
351            chain.push(BiquadDesign::highpass(cutoff_hz, 0.5, sample_rate));
352        }
353        chain
354    }
355
356    /// Nth-order Butterworth bandpass.
357    pub fn bandpass(order: u32, center_hz: f32, bandwidth_hz: f32, sample_rate: f32) -> FilterChain {
358        let mut chain = FilterChain::with_capacity(order as usize);
359        let n_stages = order / 2;
360        for k in 1..=n_stages {
361            let theta = PI * (2 * k + order - 1) as f32 / (2 * order) as f32;
362            let q = -1.0 / (2.0 * theta.cos());
363            // Use bandpass stage for each pair
364            chain.push(BiquadDesign::bandpass(center_hz, bandwidth_hz / q, sample_rate));
365        }
366        chain
367    }
368}
369
370// ---------------------------------------------------------------------------
371// Chebyshev type I
372// ---------------------------------------------------------------------------
373
374/// Chebyshev Type I filter design (equiripple in passband).
375pub struct Chebyshev1;
376
377impl Chebyshev1 {
378    /// Nth-order Chebyshev type I lowpass with ripple_db passband ripple.
379    pub fn lowpass(order: u32, cutoff_hz: f32, ripple_db: f32, sample_rate: f32) -> FilterChain {
380        let epsilon = (10.0f32.powf(ripple_db / 10.0) - 1.0).sqrt();
381        let n_stages = order / 2;
382        let mut chain = FilterChain::with_capacity(n_stages as usize + 1);
383        let asinh_inv_eps = (1.0 / epsilon).asinh();
384
385        for k in 1..=n_stages {
386            // Chebyshev pole: σ_k = -sinh(asinh(1/ε)/n) sin(θ_k)
387            //                 ω_k =  cosh(asinh(1/ε)/n) cos(θ_k)
388            let theta_k = PI * (2 * k - 1) as f32 / (2 * order) as f32;
389            let sigma = -(asinh_inv_eps / order as f32).sinh() * theta_k.sin();
390            let omega = (asinh_inv_eps / order as f32).cosh() * theta_k.cos();
391            // Convert to Q and natural frequency
392            let pole_norm = (sigma * sigma + omega * omega).sqrt();
393            let q_analog = pole_norm / (-2.0 * sigma).max(1e-6);
394            // Bilinear transform pre-warping
395            let wd = 2.0 * sample_rate * (PI * cutoff_hz / sample_rate).tan() * pole_norm;
396            let wn = wd / (2.0 * PI);
397            let q = q_analog.max(0.5);
398            chain.push(BiquadDesign::lowpass(wn, q, sample_rate));
399        }
400        if order % 2 == 1 {
401            chain.push(BiquadDesign::lowpass(cutoff_hz, 0.5, sample_rate));
402        }
403        chain
404    }
405}
406
407// ---------------------------------------------------------------------------
408// Bessel filter
409// ---------------------------------------------------------------------------
410
411/// Bessel filter design (maximally flat group delay).
412pub struct Bessel;
413
414impl Bessel {
415    /// Nth-order Bessel lowpass.
416    /// Uses pre-computed normalized Bessel poles (up to order 8).
417    pub fn lowpass(order: u32, cutoff_hz: f32, sample_rate: f32) -> FilterChain {
418        // Normalized Bessel pole Q values (pairs for even orders, ±1 pole for odd)
419        // Source: Analog and Digital Filters, S. Darlington
420        let q_values: &[f32] = match order {
421            1 => &[],
422            2 => &[0.5773],
423            3 => &[0.6910],
424            4 => &[0.5219, 0.8055],
425            5 => &[0.5639, 0.9165],
426            6 => &[0.5103, 0.6112, 1.0234],
427            7 => &[0.5324, 0.6608, 1.1262],
428            8 => &[0.5062, 0.5612, 0.7109, 1.2258],
429            _ => &[0.7071], // fallback
430        };
431
432        let mut chain = FilterChain::new();
433        for &q in q_values {
434            chain.push(BiquadDesign::lowpass(cutoff_hz, q, sample_rate));
435        }
436        if order % 2 == 1 {
437            chain.push(BiquadDesign::lowpass(cutoff_hz, 0.5, sample_rate));
438        }
439        chain
440    }
441}
442
443// ---------------------------------------------------------------------------
444// FirFilter
445// ---------------------------------------------------------------------------
446
447/// Finite Impulse Response filter.
448#[derive(Debug, Clone)]
449pub struct FirFilter {
450    /// Filter coefficients (impulse response).
451    pub coefficients: Vec<f32>,
452    /// Delay line (ring buffer).
453    delay_line: Vec<f32>,
454    /// Write position in the ring buffer.
455    write_pos: usize,
456}
457
458impl FirFilter {
459    /// Create from coefficient vector.
460    pub fn new(coefficients: Vec<f32>) -> Self {
461        let n = coefficients.len();
462        Self {
463            coefficients,
464            delay_line: vec![0.0; n],
465            write_pos: 0,
466        }
467    }
468
469    /// Process a single sample.
470    #[inline]
471    pub fn process_sample(&mut self, x: f32) -> f32 {
472        let n = self.coefficients.len();
473        self.delay_line[self.write_pos] = x;
474        let mut acc = 0.0f32;
475        let mut read_pos = self.write_pos;
476        for k in 0..n {
477            acc += self.coefficients[k] * self.delay_line[read_pos];
478            if read_pos == 0 { read_pos = n - 1; } else { read_pos -= 1; }
479        }
480        self.write_pos = (self.write_pos + 1) % n;
481        acc
482    }
483
484    /// In-place block processing.
485    pub fn process(&mut self, buffer: &mut [f32]) {
486        for s in buffer.iter_mut() {
487            *s = self.process_sample(*s);
488        }
489    }
490
491    /// Reset the delay line.
492    pub fn reset(&mut self) {
493        self.delay_line.fill(0.0);
494        self.write_pos = 0;
495    }
496
497    /// Number of taps.
498    pub fn num_taps(&self) -> usize { self.coefficients.len() }
499
500    /// Group delay in samples (for a linear-phase FIR: (N-1)/2).
501    pub fn group_delay(&self) -> f32 {
502        (self.coefficients.len() - 1) as f32 / 2.0
503    }
504}
505
506// ---------------------------------------------------------------------------
507// FirDesign
508// ---------------------------------------------------------------------------
509
510/// FIR filter design methods.
511pub struct FirDesign;
512
513impl FirDesign {
514    /// Windowed-sinc lowpass FIR design.
515    /// `cutoff_norm` is normalized cutoff (0..0.5), where 0.5 = Nyquist.
516    pub fn lowpass_windowed(cutoff_norm: f32, num_taps: usize, window: WindowFunction) -> FirFilter {
517        let m = (num_taps - 1) as f32 / 2.0;
518        let mut coeffs: Vec<f32> = (0..num_taps).map(|n| {
519            let x = n as f32 - m;
520            sinc(2.0 * cutoff_norm * x)
521        }).collect();
522        // Apply window
523        window.apply(&mut coeffs);
524        // Normalize to unit gain at DC
525        let sum: f32 = coeffs.iter().sum();
526        if sum.abs() > 1e-10 {
527            for c in coeffs.iter_mut() { *c /= sum; }
528        }
529        FirFilter::new(coeffs)
530    }
531
532    /// Windowed-sinc highpass FIR design.
533    pub fn highpass_windowed(cutoff_norm: f32, num_taps: usize, window: WindowFunction) -> FirFilter {
534        // Highpass = allpass - lowpass (spectral inversion)
535        let mut lp = Self::lowpass_windowed(cutoff_norm, num_taps, window);
536        let m = (num_taps - 1) / 2;
537        for (i, c) in lp.coefficients.iter_mut().enumerate() {
538            *c = if i == m { 1.0 - *c } else { -*c };
539        }
540        FirFilter::new(lp.coefficients)
541    }
542
543    /// Windowed-sinc bandpass FIR design.
544    pub fn bandpass_windowed(low_norm: f32, high_norm: f32, num_taps: usize, window: WindowFunction) -> FirFilter {
545        let m = (num_taps - 1) as f32 / 2.0;
546        // Ideal bandpass = lowpass(high) - lowpass(low), using the same
547        // normalisation as `lowpass_windowed` (0.5 = Nyquist). The old code
548        // dropped the factor 2 and the 2*fc amplitudes, so it passed a band
549        // at half the requested frequencies with the wrong shape.
550        let mut coeffs: Vec<f32> = (0..num_taps).map(|n| {
551            let x = n as f32 - m;
552            2.0 * high_norm * sinc(2.0 * high_norm * x) - 2.0 * low_norm * sinc(2.0 * low_norm * x)
553        }).collect();
554        window.apply(&mut coeffs);
555        // Normalize to unity gain at the centre of the band.
556        let fc = 0.5 * (low_norm + high_norm);
557        let gain: f32 = coeffs.iter().enumerate()
558            .map(|(n, &c)| c * (2.0 * PI * fc * (n as f32 - m)).cos())
559            .sum();
560        if gain.abs() > 1e-10 {
561            for c in coeffs.iter_mut() { *c /= gain; }
562        }
563        FirFilter::new(coeffs)
564    }
565
566    /// Bandstop (notch) windowed FIR.
567    pub fn bandstop_windowed(low_norm: f32, high_norm: f32, num_taps: usize, window: WindowFunction) -> FirFilter {
568        let lp = Self::lowpass_windowed(low_norm, num_taps, window);
569        let hp = Self::highpass_windowed(high_norm, num_taps, window);
570        let coeffs: Vec<f32> = lp.coefficients.iter().zip(hp.coefficients.iter())
571            .map(|(&a, &b)| a + b)
572            .collect();
573        FirFilter::new(coeffs)
574    }
575
576    /// Parks-McClellan equiripple lowpass approximation.
577    /// This is a simplified iterative Remez exchange approximation.
578    pub fn equiripple_lowpass(cutoff_norm: f32, num_taps: usize) -> FirFilter {
579        // Use Kaiser window as a starting approximation
580        // The Kaiser window β=8.0 gives ~80 dB stopband attenuation
581        // A proper Remez exchange algorithm is extremely complex; here we
582        // use Kaiser windowed sinc with β computed from the desired attenuation.
583        let a_stop = 80.0f32; // desired stopband attenuation
584        let beta = if a_stop > 50.0 {
585            0.1102 * (a_stop - 8.7)
586        } else if a_stop >= 21.0 {
587            0.5842 * (a_stop - 21.0).powf(0.4) + 0.07886 * (a_stop - 21.0)
588        } else {
589            0.0
590        };
591        Self::lowpass_windowed(cutoff_norm, num_taps, WindowFunction::Kaiser(beta))
592    }
593
594    /// Differentiator FIR (first-order derivative approximation).
595    pub fn differentiator(num_taps: usize) -> FirFilter {
596        let m = (num_taps - 1) as f32 / 2.0;
597        let mut coeffs: Vec<f32> = (0..num_taps).map(|n| {
598            let x = n as f32 - m;
599            if x.abs() < 1e-10 { 0.0 } else { (PI * x).cos() / x - (PI * x).sin() / (PI * x * x) }
600        }).collect();
601        WindowFunction::Hamming.apply(&mut coeffs);
602        FirFilter::new(coeffs)
603    }
604
605    /// Hilbert transform FIR (90° phase shift).
606    pub fn hilbert(num_taps: usize) -> FirFilter {
607        assert!(num_taps % 2 == 1, "Hilbert FIR requires odd number of taps");
608        let m = (num_taps - 1) / 2;
609        let mut coeffs: Vec<f32> = (0..num_taps).map(|n| {
610            let k = n as i32 - m as i32;
611            if k == 0 { 0.0 }
612            else if k % 2 == 0 { 0.0 }
613            else { 2.0 / (PI * k as f32) }
614        }).collect();
615        WindowFunction::Hamming.apply(&mut coeffs);
616        FirFilter::new(coeffs)
617    }
618}
619
620// ---------------------------------------------------------------------------
621// Convolution
622// ---------------------------------------------------------------------------
623
624/// Direct and FFT-based convolution.
625pub struct Convolution;
626
627impl Convolution {
628    /// Linear convolution via direct sum. O(N·M).
629    pub fn convolve_direct(signal: &[f32], kernel: &[f32]) -> Vec<f32> {
630        if signal.is_empty() || kernel.is_empty() { return Vec::new(); }
631        let out_len = signal.len() + kernel.len() - 1;
632        let mut out = vec![0.0f32; out_len];
633        for (i, &s) in signal.iter().enumerate() {
634            for (j, &k) in kernel.iter().enumerate() {
635                out[i + j] += s * k;
636            }
637        }
638        out
639    }
640
641    /// Linear convolution via FFT. O((N+M) log(N+M)).
642    pub fn convolve(signal: &[f32], kernel: &[f32]) -> Vec<f32> {
643        if signal.is_empty() || kernel.is_empty() { return Vec::new(); }
644        // For small inputs, direct convolution is exact and avoids FFT rounding.
645        let out_len = signal.len() + kernel.len() - 1;
646        if out_len <= 64 {
647            return Self::convolve_direct(signal, kernel);
648        }
649        let n = next_power_of_two(out_len);
650        let mut a: Vec<Complex32> = signal.iter().map(|&x| Complex32::new(x, 0.0)).collect();
651        a.resize(n, Complex32::zero());
652        let mut b: Vec<Complex32> = kernel.iter().map(|&x| Complex32::new(x, 0.0)).collect();
653        b.resize(n, Complex32::zero());
654        Fft::forward(&mut a);
655        Fft::forward(&mut b);
656        for (ai, bi) in a.iter_mut().zip(b.iter()) { *ai = *ai * *bi; }
657        Fft::inverse(&mut a);
658        a[..out_len].iter().map(|c| c.re).collect()
659    }
660
661    /// Correlation (not convolution): xcorr(a, b) with zero-lag at index len(a)-1.
662    pub fn correlate(a: &[f32], b: &[f32]) -> Vec<f32> {
663        let b_rev: Vec<f32> = b.iter().rev().copied().collect();
664        Self::convolve(a, &b_rev)
665    }
666}
667
668// ---------------------------------------------------------------------------
669// OlaConvolver — Streaming overlap-add convolution
670// ---------------------------------------------------------------------------
671
672/// Streaming overlap-add convolver for real-time large FIR processing.
673pub struct OlaConvolver {
674    kernel_fft: Vec<Complex32>,
675    fft_size: usize,
676    block_size: usize,
677    overlap: Vec<f32>,
678}
679
680impl OlaConvolver {
681    /// Create from an FIR kernel and a processing block size.
682    pub fn new(kernel: &[f32], block_size: usize) -> Self {
683        let fft_size = next_power_of_two(block_size + kernel.len() - 1);
684        let mut kernel_padded: Vec<Complex32> = kernel.iter().map(|&x| Complex32::new(x, 0.0)).collect();
685        kernel_padded.resize(fft_size, Complex32::zero());
686        Fft::forward(&mut kernel_padded);
687        Self {
688            kernel_fft: kernel_padded,
689            fft_size,
690            block_size,
691            overlap: vec![0.0; fft_size],
692        }
693    }
694
695    /// Process one block of `block_size` samples. Returns a block of the same size.
696    pub fn process_block(&mut self, input: &[f32]) -> Vec<f32> {
697        assert_eq!(input.len(), self.block_size);
698        let mut buf: Vec<Complex32> = input.iter().map(|&x| Complex32::new(x, 0.0)).collect();
699        buf.resize(self.fft_size, Complex32::zero());
700        Fft::forward(&mut buf);
701        for (b, &k) in buf.iter_mut().zip(self.kernel_fft.iter()) {
702            *b = *b * k;
703        }
704        Fft::inverse(&mut buf);
705        // Overlap-add
706        let mut out = Vec::with_capacity(self.block_size);
707        for i in 0..self.block_size {
708            out.push(buf[i].re + self.overlap[i]);
709        }
710        // Store tail in overlap
711        for i in 0..self.fft_size - self.block_size {
712            self.overlap[i] = buf[self.block_size + i].re;
713        }
714        out
715    }
716
717    /// Reset the overlap buffer.
718    pub fn reset(&mut self) {
719        self.overlap.fill(0.0);
720    }
721}
722
723// ---------------------------------------------------------------------------
724// SvfFilter — State-Variable Filter
725// ---------------------------------------------------------------------------
726
727/// State-variable filter modes.
728#[derive(Debug, Clone, Copy, PartialEq)]
729pub enum SvfMode {
730    LowPass,
731    HighPass,
732    BandPass,
733    Notch,
734    Peak,
735    AllPass,
736}
737
738/// Chamberlin state-variable filter (TPT topology).
739#[derive(Debug, Clone)]
740pub struct SvfFilter {
741    pub cutoff_hz: f32,
742    pub resonance: f32,
743    pub mode: SvfMode,
744    sample_rate: f32,
745    // Internal state
746    ic1eq: f32,
747    ic2eq: f32,
748}
749
750impl SvfFilter {
751    pub fn new(cutoff_hz: f32, resonance: f32, mode: SvfMode, sample_rate: f32) -> Self {
752        Self { cutoff_hz, resonance, mode, sample_rate, ic1eq: 0.0, ic2eq: 0.0 }
753    }
754
755    /// Set cutoff frequency.
756    pub fn set_cutoff(&mut self, hz: f32) { self.cutoff_hz = hz; }
757    /// Set resonance (0=overdamped, 1=critical, >1=underdamped).
758    pub fn set_resonance(&mut self, r: f32) { self.resonance = r; }
759
760    /// Process a single sample.
761    pub fn process_sample(&mut self, x: f32) -> f32 {
762        let g = (PI * self.cutoff_hz / self.sample_rate).tan();
763        let k = 2.0 - 2.0 * self.resonance.min(0.9999);
764        let a1 = 1.0 / (1.0 + g * (g + k));
765        let a2 = g * a1;
766        let a3 = g * a2;
767
768        let v3 = x - self.ic2eq;
769        let v1 = a1 * self.ic1eq + a2 * v3;
770        let v2 = self.ic2eq + a2 * self.ic1eq + a3 * v3;
771        self.ic1eq = 2.0 * v1 - self.ic1eq;
772        self.ic2eq = 2.0 * v2 - self.ic2eq;
773
774        match self.mode {
775            SvfMode::LowPass  => v2,
776            SvfMode::HighPass => x - k * v1 - v2,
777            SvfMode::BandPass => v1,
778            SvfMode::Notch    => x - k * v1,
779            SvfMode::Peak     => v2 - (x - k * v1 - v2),
780            SvfMode::AllPass  => x - 2.0 * k * v1,
781        }
782    }
783
784    /// In-place block processing.
785    pub fn process(&mut self, buffer: &mut [f32]) {
786        for s in buffer.iter_mut() { *s = self.process_sample(*s); }
787    }
788
789    /// Reset filter state.
790    pub fn reset(&mut self) { self.ic1eq = 0.0; self.ic2eq = 0.0; }
791}
792
793// ---------------------------------------------------------------------------
794// CombFilter
795// ---------------------------------------------------------------------------
796
797/// Comb filter mode.
798#[derive(Debug, Clone, Copy, PartialEq)]
799pub enum CombMode {
800    FeedForward,
801    FeedBack,
802}
803
804/// Comb filter (feedforward or feedback).
805#[derive(Debug, Clone)]
806pub struct CombFilter {
807    pub delay_samples: usize,
808    pub gain: f32,
809    pub mode: CombMode,
810    delay_line: Vec<f32>,
811    write_pos: usize,
812}
813
814impl CombFilter {
815    pub fn new(delay_samples: usize, gain: f32, mode: CombMode) -> Self {
816        Self {
817            delay_samples,
818            gain,
819            mode,
820            delay_line: vec![0.0; delay_samples + 1],
821            write_pos: 0,
822        }
823    }
824
825    /// Process a single sample.
826    pub fn process_sample(&mut self, x: f32) -> f32 {
827        let n = self.delay_line.len();
828        let read_pos = (self.write_pos + n - self.delay_samples) % n;
829        let delayed = self.delay_line[read_pos];
830        let y = match self.mode {
831            CombMode::FeedForward => x + self.gain * delayed,
832            CombMode::FeedBack    => x + self.gain * delayed,
833        };
834        self.delay_line[self.write_pos] = match self.mode {
835            CombMode::FeedForward => x,
836            CombMode::FeedBack    => y,
837        };
838        self.write_pos = (self.write_pos + 1) % n;
839        y
840    }
841
842    /// In-place block processing.
843    pub fn process(&mut self, buffer: &mut [f32]) {
844        for s in buffer.iter_mut() { *s = self.process_sample(*s); }
845    }
846
847    /// Reset delay line.
848    pub fn reset(&mut self) {
849        self.delay_line.fill(0.0);
850        self.write_pos = 0;
851    }
852}
853
854// ---------------------------------------------------------------------------
855// AllpassDelay — for Schroeder reverberators
856// ---------------------------------------------------------------------------
857
858/// Allpass delay network (Schroeder allpass section).
859#[derive(Debug, Clone)]
860pub struct AllpassDelay {
861    pub delay_samples: usize,
862    pub feedback: f32,
863    delay_line: Vec<f32>,
864    write_pos: usize,
865}
866
867impl AllpassDelay {
868    pub fn new(delay_samples: usize, feedback: f32) -> Self {
869        Self {
870            delay_samples,
871            feedback,
872            delay_line: vec![0.0; delay_samples + 1],
873            write_pos: 0,
874        }
875    }
876
877    /// Process a single sample.
878    pub fn process_sample(&mut self, x: f32) -> f32 {
879        let n = self.delay_line.len();
880        let read_pos = (self.write_pos + n - self.delay_samples) % n;
881        let buf = self.delay_line[read_pos];
882        // Schroeder allpass: w[n] = x[n] + g w[n-D], y[n] = -g w[n] + w[n-D].
883        // The output must use w[n], not x[n]; with x[n] the section was not
884        // allpass (an impulse came out with 58% more energy).
885        let w = x + self.feedback * buf;
886        let out = -self.feedback * w + buf;
887        self.delay_line[self.write_pos] = w;
888        self.write_pos = (self.write_pos + 1) % n;
889        out
890    }
891
892    /// In-place block processing.
893    pub fn process(&mut self, buffer: &mut [f32]) {
894        for s in buffer.iter_mut() { *s = self.process_sample(*s); }
895    }
896
897    /// Reset delay line.
898    pub fn reset(&mut self) {
899        self.delay_line.fill(0.0);
900        self.write_pos = 0;
901    }
902}
903
904// ---------------------------------------------------------------------------
905// MovingAverage
906// ---------------------------------------------------------------------------
907
908/// Efficient O(1) sliding-window moving average.
909#[derive(Debug, Clone)]
910pub struct MovingAverage {
911    pub window_size: usize,
912    buffer: Vec<f32>,
913    write_pos: usize,
914    sum: f32,
915    count: usize,
916}
917
918impl MovingAverage {
919    pub fn new(window_size: usize) -> Self {
920        assert!(window_size > 0);
921        Self {
922            window_size,
923            buffer: vec![0.0; window_size],
924            write_pos: 0,
925            sum: 0.0,
926            count: 0,
927        }
928    }
929
930    /// Process a single sample, return the current moving average.
931    pub fn process(&mut self, x: f32) -> f32 {
932        self.sum -= self.buffer[self.write_pos];
933        self.buffer[self.write_pos] = x;
934        self.sum += x;
935        self.write_pos = (self.write_pos + 1) % self.window_size;
936        if self.count < self.window_size { self.count += 1; }
937        self.sum / self.count as f32
938    }
939
940    /// Process a buffer, returning filtered values.
941    pub fn process_buffer(&mut self, input: &[f32]) -> Vec<f32> {
942        input.iter().map(|&x| self.process(x)).collect()
943    }
944
945    /// Current average value.
946    pub fn value(&self) -> f32 {
947        if self.count == 0 { 0.0 } else { self.sum / self.count as f32 }
948    }
949
950    /// Reset state.
951    pub fn reset(&mut self) {
952        self.buffer.fill(0.0);
953        self.write_pos = 0;
954        self.sum = 0.0;
955        self.count = 0;
956    }
957}
958
959// ---------------------------------------------------------------------------
960// KalmanFilter1D — scalar Kalman filter
961// ---------------------------------------------------------------------------
962
963/// 1D scalar Kalman filter for sensor fusion and signal smoothing.
964///
965/// State: x̂ (estimate), P (estimate covariance)
966/// Model: x_k = x_{k-1} + process_noise
967///        y_k = x_k + measurement_noise
968#[derive(Debug, Clone)]
969pub struct KalmanFilter1D {
970    /// Estimated state.
971    pub x: f32,
972    /// Estimate error covariance.
973    pub p: f32,
974    /// Process noise covariance Q.
975    pub q: f32,
976    /// Measurement noise covariance R.
977    pub r: f32,
978}
979
980impl KalmanFilter1D {
981    /// Create a new filter.
982    /// * `initial_estimate` — initial state estimate
983    /// * `q` — process noise variance (larger = more responsive)
984    /// * `r` — measurement noise variance (larger = more smoothing)
985    pub fn new(initial_estimate: f32, q: f32, r: f32) -> Self {
986        Self { x: initial_estimate, p: 1.0, q, r }
987    }
988
989    /// Predict step (constant-velocity model here: x = x, P = P + Q).
990    pub fn predict(&mut self, _dt: f32) {
991        // Simple random-walk model: state unchanged, covariance grows
992        self.p += self.q;
993    }
994
995    /// Update with a new measurement.
996    pub fn update(&mut self, measurement: f32) {
997        // Kalman gain
998        let k = self.p / (self.p + self.r);
999        // Update estimate
1000        self.x += k * (measurement - self.x);
1001        // Update covariance
1002        self.p *= 1.0 - k;
1003    }
1004
1005    /// Predict then update in one step.
1006    pub fn filter(&mut self, measurement: f32, dt: f32) -> f32 {
1007        self.predict(dt);
1008        self.update(measurement);
1009        self.x
1010    }
1011
1012    /// Current estimate.
1013    pub fn estimate(&self) -> f32 { self.x }
1014
1015    /// Filter a buffer of measurements.
1016    pub fn filter_buffer(&mut self, measurements: &[f32], dt: f32) -> Vec<f32> {
1017        measurements.iter().map(|&m| self.filter(m, dt)).collect()
1018    }
1019
1020    /// Reset to a new initial state.
1021    pub fn reset(&mut self, initial: f32) {
1022        self.x = initial;
1023        self.p = 1.0;
1024    }
1025}
1026
1027// ---------------------------------------------------------------------------
1028// PllFilter — Phase-Locked Loop
1029// ---------------------------------------------------------------------------
1030
1031/// Simple digital Phase-Locked Loop for pitch/tempo tracking.
1032///
1033/// Uses a second-order loop filter (PI controller).
1034#[derive(Debug, Clone)]
1035pub struct PllFilter {
1036    /// Loop natural frequency in Hz.
1037    pub natural_freq_hz: f32,
1038    /// Damping factor ζ.
1039    pub damping: f32,
1040    sample_rate: f32,
1041    /// Current phase estimate (radians).
1042    phase: f32,
1043    /// Current frequency estimate (radians/sample).
1044    freq: f32,
1045    // PI filter integrator state
1046    integrator: f32,
1047    // Loop filter coefficients
1048    kp: f32,
1049    ki: f32,
1050}
1051
1052impl PllFilter {
1053    /// Create a PLL.
1054    /// * `center_freq_hz` — initial center frequency
1055    /// * `natural_freq_hz` — loop bandwidth
1056    /// * `damping` — damping factor (0.707 = Butterworth)
1057    pub fn new(center_freq_hz: f32, natural_freq_hz: f32, damping: f32, sample_rate: f32) -> Self {
1058        let wn = 2.0 * PI * natural_freq_hz / sample_rate;
1059        let kp = 2.0 * damping * wn;
1060        let ki = wn * wn;
1061        Self {
1062            natural_freq_hz,
1063            damping,
1064            sample_rate,
1065            phase: 0.0,
1066            freq: 2.0 * PI * center_freq_hz / sample_rate,
1067            integrator: 2.0 * PI * center_freq_hz / sample_rate,
1068            kp,
1069            ki,
1070        }
1071    }
1072
1073    /// Process one sample. Input is the raw signal (or phase error signal).
1074    /// Returns the VCO output (cosine at locked frequency).
1075    pub fn process_sample(&mut self, input: f32) -> f32 {
1076        // Phase detector: multiply input by VCO quadrature output
1077        let vco_i = self.phase.cos();
1078        let vco_q = self.phase.sin();
1079        let _phase_error_unused = input * vco_q - 0.0 * vco_i; // simplified phase discriminator
1080        let phase_error = input * (-self.phase).sin(); // XOR-like discriminator
1081        // Loop filter (PI)
1082        self.integrator += self.ki * phase_error;
1083        self.freq = self.integrator + self.kp * phase_error;
1084        // VCO
1085        self.phase += self.freq;
1086        self.phase = Self::wrap_phase(self.phase);
1087        self.phase.cos()
1088    }
1089
1090    /// Process a buffer, returning VCO output.
1091    pub fn process_buffer(&mut self, input: &[f32]) -> Vec<f32> {
1092        input.iter().map(|&x| self.process_sample(x)).collect()
1093    }
1094
1095    /// Current estimated frequency in Hz.
1096    pub fn frequency_hz(&self) -> f32 {
1097        self.freq * self.sample_rate / (2.0 * PI)
1098    }
1099
1100    /// Current phase in radians.
1101    pub fn phase(&self) -> f32 { self.phase }
1102
1103    /// Reset the PLL state.
1104    pub fn reset(&mut self) {
1105        self.phase = 0.0;
1106        self.integrator = self.freq;
1107    }
1108
1109    /// Wrap phase to [-π, π].
1110    fn wrap_phase(p: f32) -> f32 {
1111        let mut p = p;
1112        while p > PI  { p -= 2.0 * PI; }
1113        while p < -PI { p += 2.0 * PI; }
1114        p
1115    }
1116}
1117
1118// ---------------------------------------------------------------------------
1119// Tests
1120// ---------------------------------------------------------------------------
1121
1122#[cfg(test)]
1123mod tests {
1124    use super::*;
1125    use crate::dsp::SignalGenerator;
1126
1127    fn sine_buf(freq_hz: f32, sr: f32, len: usize) -> Vec<f32> {
1128        (0..len).map(|i| (2.0 * PI * freq_hz * i as f32 / sr).sin()).collect()
1129    }
1130
1131    fn rms(buf: &[f32]) -> f32 {
1132        let sum: f32 = buf.iter().map(|&x| x * x).sum();
1133        (sum / buf.len() as f32).sqrt()
1134    }
1135
1136    // --- Biquad ---
1137
1138    #[test]
1139    fn test_biquad_identity() {
1140        let mut bq = Biquad::identity();
1141        let input = vec![1.0, 0.5, -0.3, 0.8];
1142        let mut buf = input.clone();
1143        bq.process(&mut buf);
1144        for (&a, &b) in input.iter().zip(buf.iter()) {
1145            assert!((a - b).abs() < 1e-6);
1146        }
1147    }
1148
1149    #[test]
1150    fn test_biquad_lowpass_attenuates_high_freq() {
1151        let sr = 44100.0;
1152        let mut lp = BiquadDesign::lowpass(500.0, 0.707, sr);
1153        let hi_freq = sine_buf(10000.0, sr, 4410);
1154        let mut buf = hi_freq.clone();
1155        lp.process(&mut buf);
1156        // After lowpass, high frequency should be greatly attenuated
1157        assert!(rms(&buf) < rms(&hi_freq) * 0.5);
1158    }
1159
1160    #[test]
1161    fn test_biquad_highpass_passes_high_freq() {
1162        let sr = 44100.0;
1163        let mut hp = BiquadDesign::highpass(1000.0, 0.707, sr);
1164        let hi_buf = sine_buf(10000.0, sr, 4410);
1165        let mut buf = hi_buf.clone();
1166        hp.process(&mut buf);
1167        // High frequency should pass mostly unchanged
1168        assert!(rms(&buf) > rms(&hi_buf) * 0.5);
1169    }
1170
1171    #[test]
1172    fn test_biquad_reset() {
1173        let sr = 44100.0;
1174        let mut lp = BiquadDesign::lowpass(1000.0, 0.707, sr);
1175        let mut buf = vec![1.0f32; 100];
1176        lp.process(&mut buf);
1177        lp.reset();
1178        assert_eq!(lp.z1, 0.0);
1179        assert_eq!(lp.z2, 0.0);
1180    }
1181
1182    #[test]
1183    fn test_biquad_notch_attenuates_center() {
1184        let sr = 44100.0;
1185        let center = 1000.0f32;
1186        let mut notch = BiquadDesign::notch(center, 10.0, sr);
1187        let buf_in = sine_buf(center, sr, 44100);
1188        let mut buf = buf_in.clone();
1189        // Warm up
1190        for _ in 0..1000 { notch.process_sample(0.0); }
1191        notch.reset();
1192        notch.process(&mut buf);
1193        // Notch should significantly reduce the tone
1194        assert!(rms(&buf) < rms(&buf_in) * 0.3);
1195    }
1196
1197    #[test]
1198    fn test_filter_chain() {
1199        let sr = 44100.0;
1200        let mut chain = FilterChain::new();
1201        chain.push(BiquadDesign::lowpass(1000.0, 0.707, sr));
1202        chain.push(BiquadDesign::lowpass(1000.0, 0.707, sr));
1203        let buf_in = sine_buf(10000.0, sr, 4410);
1204        let mut buf = buf_in.clone();
1205        chain.process(&mut buf);
1206        // Two cascaded LP should attenuate more than one
1207        let mut single = BiquadDesign::lowpass(1000.0, 0.707, sr);
1208        let mut buf2 = buf_in.clone();
1209        single.process(&mut buf2);
1210        assert!(rms(&buf) < rms(&buf2));
1211    }
1212
1213    #[test]
1214    fn test_butterworth_lowpass() {
1215        let sr = 44100.0;
1216        let mut filt = Butterworth::lowpass(4, 1000.0, sr);
1217        let buf_in = sine_buf(10000.0, sr, 4410);
1218        let mut buf = buf_in.clone();
1219        filt.process(&mut buf);
1220        assert!(rms(&buf) < rms(&buf_in) * 0.1);
1221    }
1222
1223    #[test]
1224    fn test_fir_lowpass_dc_gain() {
1225        // DC gain of a lowpass FIR should be 1
1226        let fir = FirDesign::lowpass_windowed(0.25, 63, WindowFunction::Hamming);
1227        let dc: Vec<f32> = vec![1.0; 512];
1228        let mut buf = dc.clone();
1229        let mut f = fir;
1230        f.process(&mut buf);
1231        // After transient, steady state should be near 1.0
1232        let steady = &buf[200..];
1233        let avg: f32 = steady.iter().sum::<f32>() / steady.len() as f32;
1234        assert!((avg - 1.0).abs() < 0.01, "avg={}", avg);
1235    }
1236
1237    #[test]
1238    fn test_fir_highpass_attenuates_dc() {
1239        let fir = FirDesign::highpass_windowed(0.25, 63, WindowFunction::Hann);
1240        let dc = vec![1.0f32; 512];
1241        let mut buf = dc.clone();
1242        let mut f = fir;
1243        f.process(&mut buf);
1244        let avg: f32 = buf[200..].iter().sum::<f32>() / buf[200..].len() as f32;
1245        assert!(avg.abs() < 0.05, "avg={}", avg);
1246    }
1247
1248    #[test]
1249    fn test_convolution_impulse() {
1250        // Convolving with an impulse should return the signal unchanged
1251        let sig = vec![1.0f32, 2.0, 3.0, 4.0];
1252        let kernel = vec![1.0f32, 0.0, 0.0];
1253        let out = Convolution::convolve(&sig, &kernel);
1254        assert_eq!(out[0], 1.0);
1255        assert_eq!(out[1], 2.0);
1256        assert_eq!(out[2], 3.0);
1257        assert_eq!(out[3], 4.0);
1258    }
1259
1260    #[test]
1261    fn test_convolution_matches_direct() {
1262        let sig: Vec<f32> = (0..20).map(|i| i as f32 * 0.1).collect();
1263        let kernel: Vec<f32> = vec![0.25, 0.5, 0.25];
1264        let fft_result = Convolution::convolve(&sig, &kernel);
1265        let direct_result = Convolution::convolve_direct(&sig, &kernel);
1266        assert_eq!(fft_result.len(), direct_result.len());
1267        for (a, b) in fft_result.iter().zip(direct_result.iter()) {
1268            assert!((a - b).abs() < 1e-4, "a={}, b={}", a, b);
1269        }
1270    }
1271
1272    #[test]
1273    fn test_ola_convolver_block_processing() {
1274        let kernel = vec![0.25f32, 0.5, 0.25];
1275        let block_size = 64;
1276        let mut ola = OlaConvolver::new(&kernel, block_size);
1277        let input = vec![1.0f32; block_size];
1278        let out = ola.process_block(&input);
1279        assert_eq!(out.len(), block_size);
1280    }
1281
1282    #[test]
1283    fn test_svf_lowpass() {
1284        let sr = 44100.0;
1285        let mut svf = SvfFilter::new(1000.0, 0.0, SvfMode::LowPass, sr);
1286        let hi = sine_buf(10000.0, sr, 4410);
1287        let mut buf = hi.clone();
1288        svf.process(&mut buf);
1289        assert!(rms(&buf) < rms(&hi) * 0.3);
1290    }
1291
1292    #[test]
1293    fn test_svf_highpass() {
1294        let sr = 44100.0;
1295        let mut svf = SvfFilter::new(1000.0, 0.0, SvfMode::HighPass, sr);
1296        let lo = sine_buf(100.0, sr, 4410);
1297        let mut buf = lo.clone();
1298        svf.process(&mut buf);
1299        // Should attenuate low frequency
1300        assert!(rms(&buf) < rms(&lo) * 0.5);
1301    }
1302
1303    #[test]
1304    fn test_comb_feedforward() {
1305        let mut comb = CombFilter::new(100, 0.5, CombMode::FeedForward);
1306        let impulse: Vec<f32> = {
1307            let mut v = vec![0.0f32; 200];
1308            v[0] = 1.0;
1309            v
1310        };
1311        let mut buf = impulse.clone();
1312        comb.process(&mut buf);
1313        // Should see an echo at sample 100
1314        assert!((buf[100] - 0.5).abs() < 1e-5);
1315    }
1316
1317    #[test]
1318    fn test_allpass_delay_unity_magnitude() {
1319        let mut ap = AllpassDelay::new(50, 0.5);
1320        // 400 samples so the tail (echoes every 50 samples, decaying by 0.5)
1321        // is captured; at 200 samples 1.2% of the energy is still to come.
1322        let impulse: Vec<f32> = {
1323            let mut v = vec![0.0f32; 400];
1324            v[0] = 1.0;
1325            v
1326        };
1327        let mut buf = impulse.clone();
1328        ap.process(&mut buf);
1329        // Energy should be preserved
1330        let energy_in: f32 = impulse.iter().map(|&x| x * x).sum();
1331        let energy_out: f32 = buf.iter().map(|&x| x * x).sum();
1332        assert!((energy_in - energy_out).abs() < 0.01);
1333    }
1334
1335    #[test]
1336    fn test_moving_average_settling() {
1337        let mut ma = MovingAverage::new(8);
1338        for _ in 0..100 { ma.process(1.0); }
1339        assert!((ma.value() - 1.0).abs() < 1e-5);
1340    }
1341
1342    #[test]
1343    fn test_moving_average_step() {
1344        let mut ma = MovingAverage::new(4);
1345        // Feed 0s then 1s
1346        for _ in 0..4 { ma.process(0.0); }
1347        for i in 0..4 {
1348            let v = ma.process(1.0);
1349            assert!(v <= 1.0 && v >= 0.0, "i={} v={}", i, v);
1350        }
1351        assert!((ma.value() - 1.0).abs() < 1e-5);
1352    }
1353
1354    #[test]
1355    fn test_kalman_smoothing() {
1356        // Noisy constant signal — Kalman should converge to true value
1357        let mut kf = KalmanFilter1D::new(0.0, 0.001, 1.0);
1358        let dt = 1.0 / 44100.0;
1359        for _ in 0..1000 {
1360            kf.filter(1.0, dt);
1361        }
1362        assert!((kf.estimate() - 1.0).abs() < 0.05, "est={}", kf.estimate());
1363    }
1364
1365    #[test]
1366    fn test_pll_frequency_lock() {
1367        let sr = 44100.0;
1368        let target_hz = 440.0;
1369        let mut pll = PllFilter::new(target_hz, 5.0, 0.707, sr);
1370        let sig = sine_buf(target_hz, sr, 44100);
1371        // Run for a while to let the PLL lock
1372        for &s in &sig[..22050] { pll.process_sample(s); }
1373        let est_freq = pll.frequency_hz();
1374        // Should be in the right ballpark
1375        assert!(est_freq > 200.0 && est_freq < 1000.0, "est_freq={}", est_freq);
1376    }
1377
1378    #[test]
1379    fn test_biquad_peak_eq() {
1380        let sr = 44100.0;
1381        let mut peak = BiquadDesign::peak_eq(1000.0, 6.0, 1.0, sr);
1382        let buf_in = sine_buf(1000.0, sr, 4410);
1383        let mut buf = buf_in.clone();
1384        peak.process(&mut buf);
1385        // Peak EQ should boost at center frequency
1386        assert!(rms(&buf) > rms(&buf_in) * 1.3);
1387    }
1388
1389    #[test]
1390    fn test_chebyshev1_lowpass() {
1391        let sr = 44100.0;
1392        let mut ch = Chebyshev1::lowpass(4, 1000.0, 3.0, sr);
1393        let hi = sine_buf(10000.0, sr, 4410);
1394        let mut buf = hi.clone();
1395        ch.process(&mut buf);
1396        assert!(rms(&buf) < rms(&hi) * 0.1);
1397    }
1398
1399    #[test]
1400    fn test_bessel_lowpass_dc_gain() {
1401        let sr = 44100.0;
1402        let mut bessel = Bessel::lowpass(4, 2000.0, sr);
1403        let dc = vec![1.0f32; 4410];
1404        let mut buf = dc.clone();
1405        bessel.process(&mut buf);
1406        let avg: f32 = buf[1000..].iter().sum::<f32>() / buf[1000..].len() as f32;
1407        assert!((avg - 1.0).abs() < 0.1, "avg={}", avg);
1408    }
1409
1410    #[test]
1411    fn test_fir_bandpass() {
1412        let sr = 44100.0;
1413        let fir = FirDesign::bandpass_windowed(0.1, 0.3, 127, WindowFunction::Blackman);
1414        // Frequencies are fractions of the sample rate (0.5 = Nyquist), so
1415        // the band is 4410 to 13230 Hz. The old test put "mid" at 3000 Hz,
1416        // below the band, and "hi" at 10000 Hz, inside it.
1417        let lo = sine_buf(100.0, sr, 4410);
1418        let hi = sine_buf(20000.0, sr, 4410);
1419        let mid = sine_buf(8820.0, sr, 4410);
1420        let process = |f: &FirFilter, buf: &[f32]| -> f32 {
1421            let mut b = buf.to_vec();
1422            let mut ff = f.clone();
1423            ff.process(&mut b);
1424            rms(&b)
1425        };
1426        assert!(process(&fir, &mid) > process(&fir, &lo));
1427        assert!(process(&fir, &mid) > process(&fir, &hi));
1428    }
1429
1430    #[test]
1431    fn test_hilbert_fir_length() {
1432        let h = FirDesign::hilbert(63);
1433        assert_eq!(h.num_taps(), 63);
1434    }
1435}