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