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        let mut coeffs: Vec<f32> = (0..num_taps).map(|n| {
547            let x = n as f32 - m;
548            sinc(high_norm * x) - sinc(low_norm * x)
549        }).collect();
550        window.apply(&mut coeffs);
551        // Normalize to peak unity at center frequency
552        let peak: f32 = coeffs.iter().map(|&c| c.abs()).fold(0.0, f32::max);
553        if peak > 1e-10 {
554            for c in coeffs.iter_mut() { *c /= peak; }
555        }
556        FirFilter::new(coeffs)
557    }
558
559    /// Bandstop (notch) windowed FIR.
560    pub fn bandstop_windowed(low_norm: f32, high_norm: f32, num_taps: usize, window: WindowFunction) -> FirFilter {
561        let lp = Self::lowpass_windowed(low_norm, num_taps, window);
562        let hp = Self::highpass_windowed(high_norm, num_taps, window);
563        let coeffs: Vec<f32> = lp.coefficients.iter().zip(hp.coefficients.iter())
564            .map(|(&a, &b)| a + b)
565            .collect();
566        FirFilter::new(coeffs)
567    }
568
569    /// Parks-McClellan equiripple lowpass approximation.
570    /// This is a simplified iterative Remez exchange approximation.
571    pub fn equiripple_lowpass(cutoff_norm: f32, num_taps: usize) -> FirFilter {
572        // Use Kaiser window as a starting approximation
573        // The Kaiser window β=8.0 gives ~80 dB stopband attenuation
574        // A proper Remez exchange algorithm is extremely complex; here we
575        // use Kaiser windowed sinc with β computed from the desired attenuation.
576        let a_stop = 80.0f32; // desired stopband attenuation
577        let beta = if a_stop > 50.0 {
578            0.1102 * (a_stop - 8.7)
579        } else if a_stop >= 21.0 {
580            0.5842 * (a_stop - 21.0).powf(0.4) + 0.07886 * (a_stop - 21.0)
581        } else {
582            0.0
583        };
584        Self::lowpass_windowed(cutoff_norm, num_taps, WindowFunction::Kaiser(beta))
585    }
586
587    /// Differentiator FIR (first-order derivative approximation).
588    pub fn differentiator(num_taps: usize) -> FirFilter {
589        let m = (num_taps - 1) as f32 / 2.0;
590        let mut coeffs: Vec<f32> = (0..num_taps).map(|n| {
591            let x = n as f32 - m;
592            if x.abs() < 1e-10 { 0.0 } else { (PI * x).cos() / x - (PI * x).sin() / (PI * x * x) }
593        }).collect();
594        WindowFunction::Hamming.apply(&mut coeffs);
595        FirFilter::new(coeffs)
596    }
597
598    /// Hilbert transform FIR (90° phase shift).
599    pub fn hilbert(num_taps: usize) -> FirFilter {
600        assert!(num_taps % 2 == 1, "Hilbert FIR requires odd number of taps");
601        let m = (num_taps - 1) / 2;
602        let mut coeffs: Vec<f32> = (0..num_taps).map(|n| {
603            let k = n as i32 - m as i32;
604            if k == 0 { 0.0 }
605            else if k % 2 == 0 { 0.0 }
606            else { 2.0 / (PI * k as f32) }
607        }).collect();
608        WindowFunction::Hamming.apply(&mut coeffs);
609        FirFilter::new(coeffs)
610    }
611}
612
613// ---------------------------------------------------------------------------
614// Convolution
615// ---------------------------------------------------------------------------
616
617/// Direct and FFT-based convolution.
618pub struct Convolution;
619
620impl Convolution {
621    /// Linear convolution via direct sum. O(N·M).
622    pub fn convolve_direct(signal: &[f32], kernel: &[f32]) -> Vec<f32> {
623        if signal.is_empty() || kernel.is_empty() { return Vec::new(); }
624        let out_len = signal.len() + kernel.len() - 1;
625        let mut out = vec![0.0f32; out_len];
626        for (i, &s) in signal.iter().enumerate() {
627            for (j, &k) in kernel.iter().enumerate() {
628                out[i + j] += s * k;
629            }
630        }
631        out
632    }
633
634    /// Linear convolution via FFT. O((N+M) log(N+M)).
635    pub fn convolve(signal: &[f32], kernel: &[f32]) -> Vec<f32> {
636        if signal.is_empty() || kernel.is_empty() { return Vec::new(); }
637        // For small inputs, direct convolution is exact and avoids FFT rounding.
638        let out_len = signal.len() + kernel.len() - 1;
639        if out_len <= 64 {
640            return Self::convolve_direct(signal, kernel);
641        }
642        let n = next_power_of_two(out_len);
643        let mut a: Vec<Complex32> = signal.iter().map(|&x| Complex32::new(x, 0.0)).collect();
644        a.resize(n, Complex32::zero());
645        let mut b: Vec<Complex32> = kernel.iter().map(|&x| Complex32::new(x, 0.0)).collect();
646        b.resize(n, Complex32::zero());
647        Fft::forward(&mut a);
648        Fft::forward(&mut b);
649        for (ai, bi) in a.iter_mut().zip(b.iter()) { *ai = *ai * *bi; }
650        Fft::inverse(&mut a);
651        a[..out_len].iter().map(|c| c.re).collect()
652    }
653
654    /// Correlation (not convolution): xcorr(a, b) with zero-lag at index len(a)-1.
655    pub fn correlate(a: &[f32], b: &[f32]) -> Vec<f32> {
656        let b_rev: Vec<f32> = b.iter().rev().copied().collect();
657        Self::convolve(a, &b_rev)
658    }
659}
660
661// ---------------------------------------------------------------------------
662// OlaConvolver — Streaming overlap-add convolution
663// ---------------------------------------------------------------------------
664
665/// Streaming overlap-add convolver for real-time large FIR processing.
666pub struct OlaConvolver {
667    kernel_fft: Vec<Complex32>,
668    fft_size: usize,
669    block_size: usize,
670    overlap: Vec<f32>,
671}
672
673impl OlaConvolver {
674    /// Create from an FIR kernel and a processing block size.
675    pub fn new(kernel: &[f32], block_size: usize) -> Self {
676        let fft_size = next_power_of_two(block_size + kernel.len() - 1);
677        let mut kernel_padded: Vec<Complex32> = kernel.iter().map(|&x| Complex32::new(x, 0.0)).collect();
678        kernel_padded.resize(fft_size, Complex32::zero());
679        Fft::forward(&mut kernel_padded);
680        Self {
681            kernel_fft: kernel_padded,
682            fft_size,
683            block_size,
684            overlap: vec![0.0; fft_size],
685        }
686    }
687
688    /// Process one block of `block_size` samples. Returns a block of the same size.
689    pub fn process_block(&mut self, input: &[f32]) -> Vec<f32> {
690        assert_eq!(input.len(), self.block_size);
691        let mut buf: Vec<Complex32> = input.iter().map(|&x| Complex32::new(x, 0.0)).collect();
692        buf.resize(self.fft_size, Complex32::zero());
693        Fft::forward(&mut buf);
694        for (b, &k) in buf.iter_mut().zip(self.kernel_fft.iter()) {
695            *b = *b * k;
696        }
697        Fft::inverse(&mut buf);
698        // Overlap-add
699        let mut out = Vec::with_capacity(self.block_size);
700        for i in 0..self.block_size {
701            out.push(buf[i].re + self.overlap[i]);
702        }
703        // Store tail in overlap
704        for i in 0..self.fft_size - self.block_size {
705            self.overlap[i] = buf[self.block_size + i].re;
706        }
707        out
708    }
709
710    /// Reset the overlap buffer.
711    pub fn reset(&mut self) {
712        self.overlap.fill(0.0);
713    }
714}
715
716// ---------------------------------------------------------------------------
717// SvfFilter — State-Variable Filter
718// ---------------------------------------------------------------------------
719
720/// State-variable filter modes.
721#[derive(Debug, Clone, Copy, PartialEq)]
722pub enum SvfMode {
723    LowPass,
724    HighPass,
725    BandPass,
726    Notch,
727    Peak,
728    AllPass,
729}
730
731/// Chamberlin state-variable filter (TPT topology).
732#[derive(Debug, Clone)]
733pub struct SvfFilter {
734    pub cutoff_hz: f32,
735    pub resonance: f32,
736    pub mode: SvfMode,
737    sample_rate: f32,
738    // Internal state
739    ic1eq: f32,
740    ic2eq: f32,
741}
742
743impl SvfFilter {
744    pub fn new(cutoff_hz: f32, resonance: f32, mode: SvfMode, sample_rate: f32) -> Self {
745        Self { cutoff_hz, resonance, mode, sample_rate, ic1eq: 0.0, ic2eq: 0.0 }
746    }
747
748    /// Set cutoff frequency.
749    pub fn set_cutoff(&mut self, hz: f32) { self.cutoff_hz = hz; }
750    /// Set resonance (0=overdamped, 1=critical, >1=underdamped).
751    pub fn set_resonance(&mut self, r: f32) { self.resonance = r; }
752
753    /// Process a single sample.
754    pub fn process_sample(&mut self, x: f32) -> f32 {
755        let g = (PI * self.cutoff_hz / self.sample_rate).tan();
756        let k = 2.0 - 2.0 * self.resonance.min(0.9999);
757        let a1 = 1.0 / (1.0 + g * (g + k));
758        let a2 = g * a1;
759        let a3 = g * a2;
760
761        let v3 = x - self.ic2eq;
762        let v1 = a1 * self.ic1eq + a2 * v3;
763        let v2 = self.ic2eq + a2 * self.ic1eq + a3 * v3;
764        self.ic1eq = 2.0 * v1 - self.ic1eq;
765        self.ic2eq = 2.0 * v2 - self.ic2eq;
766
767        match self.mode {
768            SvfMode::LowPass  => v2,
769            SvfMode::HighPass => x - k * v1 - v2,
770            SvfMode::BandPass => v1,
771            SvfMode::Notch    => x - k * v1,
772            SvfMode::Peak     => v2 - (x - k * v1 - v2),
773            SvfMode::AllPass  => x - 2.0 * k * v1,
774        }
775    }
776
777    /// In-place block processing.
778    pub fn process(&mut self, buffer: &mut [f32]) {
779        for s in buffer.iter_mut() { *s = self.process_sample(*s); }
780    }
781
782    /// Reset filter state.
783    pub fn reset(&mut self) { self.ic1eq = 0.0; self.ic2eq = 0.0; }
784}
785
786// ---------------------------------------------------------------------------
787// CombFilter
788// ---------------------------------------------------------------------------
789
790/// Comb filter mode.
791#[derive(Debug, Clone, Copy, PartialEq)]
792pub enum CombMode {
793    FeedForward,
794    FeedBack,
795}
796
797/// Comb filter (feedforward or feedback).
798#[derive(Debug, Clone)]
799pub struct CombFilter {
800    pub delay_samples: usize,
801    pub gain: f32,
802    pub mode: CombMode,
803    delay_line: Vec<f32>,
804    write_pos: usize,
805}
806
807impl CombFilter {
808    pub fn new(delay_samples: usize, gain: f32, mode: CombMode) -> Self {
809        Self {
810            delay_samples,
811            gain,
812            mode,
813            delay_line: vec![0.0; delay_samples + 1],
814            write_pos: 0,
815        }
816    }
817
818    /// Process a single sample.
819    pub fn process_sample(&mut self, x: f32) -> f32 {
820        let n = self.delay_line.len();
821        let read_pos = (self.write_pos + n - self.delay_samples) % n;
822        let delayed = self.delay_line[read_pos];
823        let y = match self.mode {
824            CombMode::FeedForward => x + self.gain * delayed,
825            CombMode::FeedBack    => x + self.gain * delayed,
826        };
827        self.delay_line[self.write_pos] = match self.mode {
828            CombMode::FeedForward => x,
829            CombMode::FeedBack    => y,
830        };
831        self.write_pos = (self.write_pos + 1) % n;
832        y
833    }
834
835    /// In-place block processing.
836    pub fn process(&mut self, buffer: &mut [f32]) {
837        for s in buffer.iter_mut() { *s = self.process_sample(*s); }
838    }
839
840    /// Reset delay line.
841    pub fn reset(&mut self) {
842        self.delay_line.fill(0.0);
843        self.write_pos = 0;
844    }
845}
846
847// ---------------------------------------------------------------------------
848// AllpassDelay — for Schroeder reverberators
849// ---------------------------------------------------------------------------
850
851/// Allpass delay network (Schroeder allpass section).
852#[derive(Debug, Clone)]
853pub struct AllpassDelay {
854    pub delay_samples: usize,
855    pub feedback: f32,
856    delay_line: Vec<f32>,
857    write_pos: usize,
858}
859
860impl AllpassDelay {
861    pub fn new(delay_samples: usize, feedback: f32) -> Self {
862        Self {
863            delay_samples,
864            feedback,
865            delay_line: vec![0.0; delay_samples + 1],
866            write_pos: 0,
867        }
868    }
869
870    /// Process a single sample.
871    pub fn process_sample(&mut self, x: f32) -> f32 {
872        let n = self.delay_line.len();
873        let read_pos = (self.write_pos + n - self.delay_samples) % n;
874        let buf = self.delay_line[read_pos];
875        let out = -self.feedback * x + buf;
876        self.delay_line[self.write_pos] = x + self.feedback * buf;
877        self.write_pos = (self.write_pos + 1) % n;
878        out
879    }
880
881    /// In-place block processing.
882    pub fn process(&mut self, buffer: &mut [f32]) {
883        for s in buffer.iter_mut() { *s = self.process_sample(*s); }
884    }
885
886    /// Reset delay line.
887    pub fn reset(&mut self) {
888        self.delay_line.fill(0.0);
889        self.write_pos = 0;
890    }
891}
892
893// ---------------------------------------------------------------------------
894// MovingAverage
895// ---------------------------------------------------------------------------
896
897/// Efficient O(1) sliding-window moving average.
898#[derive(Debug, Clone)]
899pub struct MovingAverage {
900    pub window_size: usize,
901    buffer: Vec<f32>,
902    write_pos: usize,
903    sum: f32,
904    count: usize,
905}
906
907impl MovingAverage {
908    pub fn new(window_size: usize) -> Self {
909        assert!(window_size > 0);
910        Self {
911            window_size,
912            buffer: vec![0.0; window_size],
913            write_pos: 0,
914            sum: 0.0,
915            count: 0,
916        }
917    }
918
919    /// Process a single sample, return the current moving average.
920    pub fn process(&mut self, x: f32) -> f32 {
921        self.sum -= self.buffer[self.write_pos];
922        self.buffer[self.write_pos] = x;
923        self.sum += x;
924        self.write_pos = (self.write_pos + 1) % self.window_size;
925        if self.count < self.window_size { self.count += 1; }
926        self.sum / self.count as f32
927    }
928
929    /// Process a buffer, returning filtered values.
930    pub fn process_buffer(&mut self, input: &[f32]) -> Vec<f32> {
931        input.iter().map(|&x| self.process(x)).collect()
932    }
933
934    /// Current average value.
935    pub fn value(&self) -> f32 {
936        if self.count == 0 { 0.0 } else { self.sum / self.count as f32 }
937    }
938
939    /// Reset state.
940    pub fn reset(&mut self) {
941        self.buffer.fill(0.0);
942        self.write_pos = 0;
943        self.sum = 0.0;
944        self.count = 0;
945    }
946}
947
948// ---------------------------------------------------------------------------
949// KalmanFilter1D — scalar Kalman filter
950// ---------------------------------------------------------------------------
951
952/// 1D scalar Kalman filter for sensor fusion and signal smoothing.
953///
954/// State: x̂ (estimate), P (estimate covariance)
955/// Model: x_k = x_{k-1} + process_noise
956///        y_k = x_k + measurement_noise
957#[derive(Debug, Clone)]
958pub struct KalmanFilter1D {
959    /// Estimated state.
960    pub x: f32,
961    /// Estimate error covariance.
962    pub p: f32,
963    /// Process noise covariance Q.
964    pub q: f32,
965    /// Measurement noise covariance R.
966    pub r: f32,
967}
968
969impl KalmanFilter1D {
970    /// Create a new filter.
971    /// * `initial_estimate` — initial state estimate
972    /// * `q` — process noise variance (larger = more responsive)
973    /// * `r` — measurement noise variance (larger = more smoothing)
974    pub fn new(initial_estimate: f32, q: f32, r: f32) -> Self {
975        Self { x: initial_estimate, p: 1.0, q, r }
976    }
977
978    /// Predict step (constant-velocity model here: x = x, P = P + Q).
979    pub fn predict(&mut self, _dt: f32) {
980        // Simple random-walk model: state unchanged, covariance grows
981        self.p += self.q;
982    }
983
984    /// Update with a new measurement.
985    pub fn update(&mut self, measurement: f32) {
986        // Kalman gain
987        let k = self.p / (self.p + self.r);
988        // Update estimate
989        self.x += k * (measurement - self.x);
990        // Update covariance
991        self.p *= 1.0 - k;
992    }
993
994    /// Predict then update in one step.
995    pub fn filter(&mut self, measurement: f32, dt: f32) -> f32 {
996        self.predict(dt);
997        self.update(measurement);
998        self.x
999    }
1000
1001    /// Current estimate.
1002    pub fn estimate(&self) -> f32 { self.x }
1003
1004    /// Filter a buffer of measurements.
1005    pub fn filter_buffer(&mut self, measurements: &[f32], dt: f32) -> Vec<f32> {
1006        measurements.iter().map(|&m| self.filter(m, dt)).collect()
1007    }
1008
1009    /// Reset to a new initial state.
1010    pub fn reset(&mut self, initial: f32) {
1011        self.x = initial;
1012        self.p = 1.0;
1013    }
1014}
1015
1016// ---------------------------------------------------------------------------
1017// PllFilter — Phase-Locked Loop
1018// ---------------------------------------------------------------------------
1019
1020/// Simple digital Phase-Locked Loop for pitch/tempo tracking.
1021///
1022/// Uses a second-order loop filter (PI controller).
1023#[derive(Debug, Clone)]
1024pub struct PllFilter {
1025    /// Loop natural frequency in Hz.
1026    pub natural_freq_hz: f32,
1027    /// Damping factor ζ.
1028    pub damping: f32,
1029    sample_rate: f32,
1030    /// Current phase estimate (radians).
1031    phase: f32,
1032    /// Current frequency estimate (radians/sample).
1033    freq: f32,
1034    // PI filter integrator state
1035    integrator: f32,
1036    // Loop filter coefficients
1037    kp: f32,
1038    ki: f32,
1039}
1040
1041impl PllFilter {
1042    /// Create a PLL.
1043    /// * `center_freq_hz` — initial center frequency
1044    /// * `natural_freq_hz` — loop bandwidth
1045    /// * `damping` — damping factor (0.707 = Butterworth)
1046    pub fn new(center_freq_hz: f32, natural_freq_hz: f32, damping: f32, sample_rate: f32) -> Self {
1047        let wn = 2.0 * PI * natural_freq_hz / sample_rate;
1048        let kp = 2.0 * damping * wn;
1049        let ki = wn * wn;
1050        Self {
1051            natural_freq_hz,
1052            damping,
1053            sample_rate,
1054            phase: 0.0,
1055            freq: 2.0 * PI * center_freq_hz / sample_rate,
1056            integrator: 2.0 * PI * center_freq_hz / sample_rate,
1057            kp,
1058            ki,
1059        }
1060    }
1061
1062    /// Process one sample. Input is the raw signal (or phase error signal).
1063    /// Returns the VCO output (cosine at locked frequency).
1064    pub fn process_sample(&mut self, input: f32) -> f32 {
1065        // Phase detector: multiply input by VCO quadrature output
1066        let vco_i = self.phase.cos();
1067        let vco_q = self.phase.sin();
1068        let _phase_error_unused = input * vco_q - 0.0 * vco_i; // simplified phase discriminator
1069        let phase_error = input * (-self.phase).sin(); // XOR-like discriminator
1070        // Loop filter (PI)
1071        self.integrator += self.ki * phase_error;
1072        self.freq = self.integrator + self.kp * phase_error;
1073        // VCO
1074        self.phase += self.freq;
1075        self.phase = Self::wrap_phase(self.phase);
1076        self.phase.cos()
1077    }
1078
1079    /// Process a buffer, returning VCO output.
1080    pub fn process_buffer(&mut self, input: &[f32]) -> Vec<f32> {
1081        input.iter().map(|&x| self.process_sample(x)).collect()
1082    }
1083
1084    /// Current estimated frequency in Hz.
1085    pub fn frequency_hz(&self) -> f32 {
1086        self.freq * self.sample_rate / (2.0 * PI)
1087    }
1088
1089    /// Current phase in radians.
1090    pub fn phase(&self) -> f32 { self.phase }
1091
1092    /// Reset the PLL state.
1093    pub fn reset(&mut self) {
1094        self.phase = 0.0;
1095        self.integrator = self.freq;
1096    }
1097
1098    /// Wrap phase to [-π, π].
1099    fn wrap_phase(p: f32) -> f32 {
1100        let mut p = p;
1101        while p > PI  { p -= 2.0 * PI; }
1102        while p < -PI { p += 2.0 * PI; }
1103        p
1104    }
1105}
1106
1107// ---------------------------------------------------------------------------
1108// Tests
1109// ---------------------------------------------------------------------------
1110
1111#[cfg(test)]
1112mod tests {
1113    use super::*;
1114    use crate::dsp::SignalGenerator;
1115
1116    fn sine_buf(freq_hz: f32, sr: f32, len: usize) -> Vec<f32> {
1117        (0..len).map(|i| (2.0 * PI * freq_hz * i as f32 / sr).sin()).collect()
1118    }
1119
1120    fn rms(buf: &[f32]) -> f32 {
1121        let sum: f32 = buf.iter().map(|&x| x * x).sum();
1122        (sum / buf.len() as f32).sqrt()
1123    }
1124
1125    // --- Biquad ---
1126
1127    #[test]
1128    fn test_biquad_identity() {
1129        let mut bq = Biquad::identity();
1130        let input = vec![1.0, 0.5, -0.3, 0.8];
1131        let mut buf = input.clone();
1132        bq.process(&mut buf);
1133        for (&a, &b) in input.iter().zip(buf.iter()) {
1134            assert!((a - b).abs() < 1e-6);
1135        }
1136    }
1137
1138    #[test]
1139    fn test_biquad_lowpass_attenuates_high_freq() {
1140        let sr = 44100.0;
1141        let mut lp = BiquadDesign::lowpass(500.0, 0.707, sr);
1142        let hi_freq = sine_buf(10000.0, sr, 4410);
1143        let mut buf = hi_freq.clone();
1144        lp.process(&mut buf);
1145        // After lowpass, high frequency should be greatly attenuated
1146        assert!(rms(&buf) < rms(&hi_freq) * 0.5);
1147    }
1148
1149    #[test]
1150    fn test_biquad_highpass_passes_high_freq() {
1151        let sr = 44100.0;
1152        let mut hp = BiquadDesign::highpass(1000.0, 0.707, sr);
1153        let hi_buf = sine_buf(10000.0, sr, 4410);
1154        let mut buf = hi_buf.clone();
1155        hp.process(&mut buf);
1156        // High frequency should pass mostly unchanged
1157        assert!(rms(&buf) > rms(&hi_buf) * 0.5);
1158    }
1159
1160    #[test]
1161    fn test_biquad_reset() {
1162        let sr = 44100.0;
1163        let mut lp = BiquadDesign::lowpass(1000.0, 0.707, sr);
1164        let mut buf = vec![1.0f32; 100];
1165        lp.process(&mut buf);
1166        lp.reset();
1167        assert_eq!(lp.z1, 0.0);
1168        assert_eq!(lp.z2, 0.0);
1169    }
1170
1171    #[test]
1172    fn test_biquad_notch_attenuates_center() {
1173        let sr = 44100.0;
1174        let center = 1000.0f32;
1175        let mut notch = BiquadDesign::notch(center, 10.0, sr);
1176        let buf_in = sine_buf(center, sr, 44100);
1177        let mut buf = buf_in.clone();
1178        // Warm up
1179        for _ in 0..1000 { notch.process_sample(0.0); }
1180        notch.reset();
1181        notch.process(&mut buf);
1182        // Notch should significantly reduce the tone
1183        assert!(rms(&buf) < rms(&buf_in) * 0.3);
1184    }
1185
1186    #[test]
1187    fn test_filter_chain() {
1188        let sr = 44100.0;
1189        let mut chain = FilterChain::new();
1190        chain.push(BiquadDesign::lowpass(1000.0, 0.707, sr));
1191        chain.push(BiquadDesign::lowpass(1000.0, 0.707, sr));
1192        let buf_in = sine_buf(10000.0, sr, 4410);
1193        let mut buf = buf_in.clone();
1194        chain.process(&mut buf);
1195        // Two cascaded LP should attenuate more than one
1196        let mut single = BiquadDesign::lowpass(1000.0, 0.707, sr);
1197        let mut buf2 = buf_in.clone();
1198        single.process(&mut buf2);
1199        assert!(rms(&buf) < rms(&buf2));
1200    }
1201
1202    #[test]
1203    fn test_butterworth_lowpass() {
1204        let sr = 44100.0;
1205        let mut filt = Butterworth::lowpass(4, 1000.0, sr);
1206        let buf_in = sine_buf(10000.0, sr, 4410);
1207        let mut buf = buf_in.clone();
1208        filt.process(&mut buf);
1209        assert!(rms(&buf) < rms(&buf_in) * 0.1);
1210    }
1211
1212    #[test]
1213    fn test_fir_lowpass_dc_gain() {
1214        // DC gain of a lowpass FIR should be 1
1215        let fir = FirDesign::lowpass_windowed(0.25, 63, WindowFunction::Hamming);
1216        let dc: Vec<f32> = vec![1.0; 512];
1217        let mut buf = dc.clone();
1218        let mut f = fir;
1219        f.process(&mut buf);
1220        // After transient, steady state should be near 1.0
1221        let steady = &buf[200..];
1222        let avg: f32 = steady.iter().sum::<f32>() / steady.len() as f32;
1223        assert!((avg - 1.0).abs() < 0.01, "avg={}", avg);
1224    }
1225
1226    #[test]
1227    fn test_fir_highpass_attenuates_dc() {
1228        let fir = FirDesign::highpass_windowed(0.25, 63, WindowFunction::Hann);
1229        let dc = vec![1.0f32; 512];
1230        let mut buf = dc.clone();
1231        let mut f = fir;
1232        f.process(&mut buf);
1233        let avg: f32 = buf[200..].iter().sum::<f32>() / buf[200..].len() as f32;
1234        assert!(avg.abs() < 0.05, "avg={}", avg);
1235    }
1236
1237    #[test]
1238    fn test_convolution_impulse() {
1239        // Convolving with an impulse should return the signal unchanged
1240        let sig = vec![1.0f32, 2.0, 3.0, 4.0];
1241        let kernel = vec![1.0f32, 0.0, 0.0];
1242        let out = Convolution::convolve(&sig, &kernel);
1243        assert_eq!(out[0], 1.0);
1244        assert_eq!(out[1], 2.0);
1245        assert_eq!(out[2], 3.0);
1246        assert_eq!(out[3], 4.0);
1247    }
1248
1249    #[test]
1250    fn test_convolution_matches_direct() {
1251        let sig: Vec<f32> = (0..20).map(|i| i as f32 * 0.1).collect();
1252        let kernel: Vec<f32> = vec![0.25, 0.5, 0.25];
1253        let fft_result = Convolution::convolve(&sig, &kernel);
1254        let direct_result = Convolution::convolve_direct(&sig, &kernel);
1255        assert_eq!(fft_result.len(), direct_result.len());
1256        for (a, b) in fft_result.iter().zip(direct_result.iter()) {
1257            assert!((a - b).abs() < 1e-4, "a={}, b={}", a, b);
1258        }
1259    }
1260
1261    #[test]
1262    fn test_ola_convolver_block_processing() {
1263        let kernel = vec![0.25f32, 0.5, 0.25];
1264        let block_size = 64;
1265        let mut ola = OlaConvolver::new(&kernel, block_size);
1266        let input = vec![1.0f32; block_size];
1267        let out = ola.process_block(&input);
1268        assert_eq!(out.len(), block_size);
1269    }
1270
1271    #[test]
1272    fn test_svf_lowpass() {
1273        let sr = 44100.0;
1274        let mut svf = SvfFilter::new(1000.0, 0.0, SvfMode::LowPass, sr);
1275        let hi = sine_buf(10000.0, sr, 4410);
1276        let mut buf = hi.clone();
1277        svf.process(&mut buf);
1278        assert!(rms(&buf) < rms(&hi) * 0.3);
1279    }
1280
1281    #[test]
1282    fn test_svf_highpass() {
1283        let sr = 44100.0;
1284        let mut svf = SvfFilter::new(1000.0, 0.0, SvfMode::HighPass, sr);
1285        let lo = sine_buf(100.0, sr, 4410);
1286        let mut buf = lo.clone();
1287        svf.process(&mut buf);
1288        // Should attenuate low frequency
1289        assert!(rms(&buf) < rms(&lo) * 0.5);
1290    }
1291
1292    #[test]
1293    fn test_comb_feedforward() {
1294        let mut comb = CombFilter::new(100, 0.5, CombMode::FeedForward);
1295        let impulse: Vec<f32> = {
1296            let mut v = vec![0.0f32; 200];
1297            v[0] = 1.0;
1298            v
1299        };
1300        let mut buf = impulse.clone();
1301        comb.process(&mut buf);
1302        // Should see an echo at sample 100
1303        assert!((buf[100] - 0.5).abs() < 1e-5);
1304    }
1305
1306    #[test]
1307    fn test_allpass_delay_unity_magnitude() {
1308        let mut ap = AllpassDelay::new(50, 0.5);
1309        let impulse: Vec<f32> = {
1310            let mut v = vec![0.0f32; 200];
1311            v[0] = 1.0;
1312            v
1313        };
1314        let mut buf = impulse.clone();
1315        ap.process(&mut buf);
1316        // Energy should be preserved
1317        let energy_in: f32 = impulse.iter().map(|&x| x * x).sum();
1318        let energy_out: f32 = buf.iter().map(|&x| x * x).sum();
1319        assert!((energy_in - energy_out).abs() < 0.01);
1320    }
1321
1322    #[test]
1323    fn test_moving_average_settling() {
1324        let mut ma = MovingAverage::new(8);
1325        for _ in 0..100 { ma.process(1.0); }
1326        assert!((ma.value() - 1.0).abs() < 1e-5);
1327    }
1328
1329    #[test]
1330    fn test_moving_average_step() {
1331        let mut ma = MovingAverage::new(4);
1332        // Feed 0s then 1s
1333        for _ in 0..4 { ma.process(0.0); }
1334        for i in 0..4 {
1335            let v = ma.process(1.0);
1336            assert!(v <= 1.0 && v >= 0.0, "i={} v={}", i, v);
1337        }
1338        assert!((ma.value() - 1.0).abs() < 1e-5);
1339    }
1340
1341    #[test]
1342    fn test_kalman_smoothing() {
1343        // Noisy constant signal — Kalman should converge to true value
1344        let mut kf = KalmanFilter1D::new(0.0, 0.001, 1.0);
1345        let dt = 1.0 / 44100.0;
1346        for _ in 0..1000 {
1347            kf.filter(1.0, dt);
1348        }
1349        assert!((kf.estimate() - 1.0).abs() < 0.05, "est={}", kf.estimate());
1350    }
1351
1352    #[test]
1353    fn test_pll_frequency_lock() {
1354        let sr = 44100.0;
1355        let target_hz = 440.0;
1356        let mut pll = PllFilter::new(target_hz, 5.0, 0.707, sr);
1357        let sig = sine_buf(target_hz, sr, 44100);
1358        // Run for a while to let the PLL lock
1359        for &s in &sig[..22050] { pll.process_sample(s); }
1360        let est_freq = pll.frequency_hz();
1361        // Should be in the right ballpark
1362        assert!(est_freq > 200.0 && est_freq < 1000.0, "est_freq={}", est_freq);
1363    }
1364
1365    #[test]
1366    fn test_biquad_peak_eq() {
1367        let sr = 44100.0;
1368        let mut peak = BiquadDesign::peak_eq(1000.0, 6.0, 1.0, sr);
1369        let buf_in = sine_buf(1000.0, sr, 4410);
1370        let mut buf = buf_in.clone();
1371        peak.process(&mut buf);
1372        // Peak EQ should boost at center frequency
1373        assert!(rms(&buf) > rms(&buf_in) * 1.3);
1374    }
1375
1376    #[test]
1377    fn test_chebyshev1_lowpass() {
1378        let sr = 44100.0;
1379        let mut ch = Chebyshev1::lowpass(4, 1000.0, 3.0, sr);
1380        let hi = sine_buf(10000.0, sr, 4410);
1381        let mut buf = hi.clone();
1382        ch.process(&mut buf);
1383        assert!(rms(&buf) < rms(&hi) * 0.1);
1384    }
1385
1386    #[test]
1387    fn test_bessel_lowpass_dc_gain() {
1388        let sr = 44100.0;
1389        let mut bessel = Bessel::lowpass(4, 2000.0, sr);
1390        let dc = vec![1.0f32; 4410];
1391        let mut buf = dc.clone();
1392        bessel.process(&mut buf);
1393        let avg: f32 = buf[1000..].iter().sum::<f32>() / buf[1000..].len() as f32;
1394        assert!((avg - 1.0).abs() < 0.1, "avg={}", avg);
1395    }
1396
1397    #[test]
1398    fn test_fir_bandpass() {
1399        let sr = 44100.0;
1400        let fir = FirDesign::bandpass_windowed(0.1, 0.3, 127, WindowFunction::Blackman);
1401        let lo = sine_buf(100.0, sr, 4410);
1402        let hi = sine_buf(10000.0, sr, 4410);
1403        let mid = sine_buf(3000.0, sr, 4410);
1404        let process = |f: &FirFilter, buf: &[f32]| -> f32 {
1405            let mut b = buf.to_vec();
1406            let mut ff = f.clone();
1407            ff.process(&mut b);
1408            rms(&b)
1409        };
1410        assert!(process(&fir, &mid) > process(&fir, &lo));
1411        assert!(process(&fir, &mid) > process(&fir, &hi));
1412    }
1413
1414    #[test]
1415    fn test_hilbert_fir_length() {
1416        let h = FirDesign::hilbert(63);
1417        assert_eq!(h.num_taps(), 63);
1418    }
1419}