Skip to main content

embedded_dsp/
filter_design.rs

1//! Filter design routines for calculating biquad IIR coefficients (Low-pass, High-pass, Band-pass, Notch, Peaking, All-pass, Butterworth).
2
3#[allow(unused_imports)]
4use crate::math::FloatMath;
5
6/// Computes Direct Form I Biquad coefficients `[b0, b1, b2, a1, a2]` for a Low-Pass Filter.
7///
8/// `cutoff_freq`: Cutoff frequency in Hz.
9/// `sample_rate`: Sampling rate in Hz.
10/// `q`: Quality factor (e.g. 0.7071 for Butterworth alignment).
11pub fn biquad_lowpass_coeffs(cutoff_freq: f32, sample_rate: f32, q: f32) -> [f32; 5] {
12    let w0 = 2.0 * core::f32::consts::PI * cutoff_freq / sample_rate;
13    let cos_w0 = w0.cos();
14    let sin_w0 = w0.sin();
15    let alpha = sin_w0 / (2.0 * q);
16
17    let a0 = 1.0 + alpha;
18    let b0 = (1.0 - cos_w0) / 2.0 / a0;
19    let b1 = (1.0 - cos_w0) / a0;
20    let b2 = (1.0 - cos_w0) / 2.0 / a0;
21    // In Direct Form I (out = b0*x + b1*x1 + b2*x2 + a1*y1 + a2*y2), sign of feedback terms is flipped:
22    let a1 = (2.0 * cos_w0) / a0;
23    let a2 = -(1.0 - alpha) / a0;
24
25    [b0, b1, b2, a1, a2]
26}
27
28/// Computes Direct Form I Biquad coefficients `[b0, b1, b2, a1, a2]` for a High-Pass Filter.
29pub fn biquad_highpass_coeffs(cutoff_freq: f32, sample_rate: f32, q: f32) -> [f32; 5] {
30    let w0 = 2.0 * core::f32::consts::PI * cutoff_freq / sample_rate;
31    let cos_w0 = w0.cos();
32    let sin_w0 = w0.sin();
33    let alpha = sin_w0 / (2.0 * q);
34
35    let a0 = 1.0 + alpha;
36    let b0 = (1.0 + cos_w0) / 2.0 / a0;
37    let b1 = -(1.0 + cos_w0) / a0;
38    let b2 = (1.0 + cos_w0) / 2.0 / a0;
39    let a1 = (2.0 * cos_w0) / a0;
40    let a2 = -(1.0 - alpha) / a0;
41
42    [b0, b1, b2, a1, a2]
43}
44
45/// Computes Direct Form I Biquad coefficients `[b0, b1, b2, a1, a2]` for a Band-Pass Filter (constant skirt gain).
46pub fn biquad_bandpass_coeffs(center_freq: f32, sample_rate: f32, q: f32) -> [f32; 5] {
47    let w0 = 2.0 * core::f32::consts::PI * center_freq / sample_rate;
48    let cos_w0 = w0.cos();
49    let sin_w0 = w0.sin();
50    let alpha = sin_w0 / (2.0 * q);
51
52    let a0 = 1.0 + alpha;
53    let b0 = alpha / a0;
54    let b1 = 0.0;
55    let b2 = -alpha / a0;
56    let a1 = (2.0 * cos_w0) / a0;
57    let a2 = -(1.0 - alpha) / a0;
58
59    [b0, b1, b2, a1, a2]
60}
61
62/// Computes Direct Form I Biquad coefficients `[b0, b1, b2, a1, a2]` for a Notch (Band-Stop) Filter.
63pub fn biquad_notch_coeffs(center_freq: f32, sample_rate: f32, q: f32) -> [f32; 5] {
64    let w0 = 2.0 * core::f32::consts::PI * center_freq / sample_rate;
65    let cos_w0 = w0.cos();
66    let sin_w0 = w0.sin();
67    let alpha = sin_w0 / (2.0 * q);
68
69    let a0 = 1.0 + alpha;
70    let b0 = 1.0 / a0;
71    let b1 = (-2.0 * cos_w0) / a0;
72    let b2 = 1.0 / a0;
73    let a1 = (2.0 * cos_w0) / a0;
74    let a2 = -(1.0 - alpha) / a0;
75
76    [b0, b1, b2, a1, a2]
77}
78
79/// Computes Direct Form I Biquad coefficients `[b0, b1, b2, a1, a2]` for a Peaking EQ Filter.
80pub fn biquad_peaking_coeffs(center_freq: f32, sample_rate: f32, q: f32, gain_db: f32) -> [f32; 5] {
81    let w0 = 2.0 * core::f32::consts::PI * center_freq / sample_rate;
82    let cos_w0 = w0.cos();
83    let sin_w0 = w0.sin();
84    let a = (10.0f32).powf(gain_db / 40.0);
85    let alpha = sin_w0 / (2.0 * q);
86
87    let a0 = 1.0 + alpha / a;
88    let b0 = (1.0 + alpha * a) / a0;
89    let b1 = (-2.0 * cos_w0) / a0;
90    let b2 = (1.0 - alpha * a) / a0;
91    let a1 = (2.0 * cos_w0) / a0;
92    let a2 = -(1.0 - alpha / a) / a0;
93
94    [b0, b1, b2, a1, a2]
95}
96
97/// Computes Direct Form I Biquad coefficients `[b0, b1, b2, a1, a2]` for an All-Pass Filter.
98pub fn biquad_allpass_coeffs(center_freq: f32, sample_rate: f32, q: f32) -> [f32; 5] {
99    let w0 = 2.0 * core::f32::consts::PI * center_freq / sample_rate;
100    let cos_w0 = w0.cos();
101    let sin_w0 = w0.sin();
102    let alpha = sin_w0 / (2.0 * q);
103
104    let a0 = 1.0 + alpha;
105    let b0 = (1.0 - alpha) / a0;
106    let b1 = (-2.0 * cos_w0) / a0;
107    let b2 = (1.0 + alpha) / a0;
108    let a1 = (2.0 * cos_w0) / a0;
109    let a2 = -(1.0 - alpha) / a0;
110
111    [b0, b1, b2, a1, a2]
112}
113
114/// Calculates multi-stage Butterworth Low-Pass filter biquad coefficients.
115/// `out_coeffs` must be a slice of size `5 * (order / 2)`.
116pub fn butterworth_lowpass_biquads(
117    cutoff_freq: f32,
118    sample_rate: f32,
119    order: usize,
120    out_coeffs: &mut [f32],
121) {
122    let num_stages = order / 2;
123    assert!(
124        out_coeffs.len() >= num_stages * 5,
125        "out_coeffs buffer too small"
126    );
127
128    for k in 0..num_stages {
129        let angle = core::f32::consts::PI * (2 * k + 1) as f32 / (2 * order) as f32;
130        let q = 1.0 / (2.0 * angle.sin());
131        let coeffs = biquad_lowpass_coeffs(cutoff_freq, sample_rate, q);
132        out_coeffs[k * 5..(k + 1) * 5].copy_from_slice(&coeffs);
133    }
134}
135
136// --- Chebyshev Recursive Filter Design (Steven W. Smith, Ch. 20) ---
137
138/// Computes one two-pole Direct Form I biquad stage `[b0, b1, b2, a1, a2]` of a Chebyshev
139/// recursive filter (Steven W. Smith, Ch. 20, Table 20-5), for pole-pair `pole_pair`
140/// (1-indexed, `1..=num_poles / 2`) of a `num_poles`-pole filter.
141///
142/// `cutoff_norm`: cutoff frequency as a fraction of the sample rate (`0.0..0.5`).
143/// `high_pass`: `false` for low-pass, `true` for high-pass.
144/// `ripple_percent`: passband ripple, `0.0..29.0` (`0.0` gives a maximally-flat/Butterworth
145/// response with no ripple).
146/// `num_poles`: total pole count for the filter this stage belongs to; must be even, `2..=20`.
147///
148/// The returned stage is not normalized for unity passband gain; use
149/// [`chebyshev_lowpass_biquads`] / [`chebyshev_highpass_biquads`] to design a complete,
150/// gain-normalized cascade.
151pub fn chebyshev_biquad_stage(
152    cutoff_norm: f32,
153    high_pass: bool,
154    ripple_percent: f32,
155    num_poles: u32,
156    pole_pair: u32,
157) -> [f32; 5] {
158    let pi = core::f32::consts::PI;
159    let np = num_poles as f32;
160    let p = pole_pair as f32;
161
162    // Pole location on the unit circle.
163    let angle = pi / (2.0 * np) + (p - 1.0) * pi / np;
164    let mut rp = -angle.cos();
165    let mut ip = angle.sin();
166
167    // Warp from a circle to an ellipse for a non-zero-ripple Chebyshev response.
168    if ripple_percent != 0.0 {
169        let es = ((100.0 / (100.0 - ripple_percent)).powf(2.0) - 1.0).sqrt();
170        let vx = (1.0 / np) * ((1.0 / es) + ((1.0 / (es * es)) + 1.0).sqrt()).ln();
171        let kx_raw = (1.0 / np) * ((1.0 / es) + ((1.0 / (es * es)) - 1.0).sqrt()).ln();
172        let kx = (kx_raw.exp() + (-kx_raw).exp()) / 2.0;
173        rp *= ((vx.exp() - (-vx).exp()) / 2.0) / kx;
174        ip *= ((vx.exp() + (-vx).exp()) / 2.0) / kx;
175    }
176
177    // s-domain to z-domain conversion.
178    let t = 2.0 * (0.5f32).tan();
179    let w = 2.0 * pi * cutoff_norm;
180    let m = rp * rp + ip * ip;
181    let d = 4.0 - 4.0 * rp * t + m * t * t;
182    let x0 = t * t / d;
183    let x1 = 2.0 * t * t / d;
184    let x2 = t * t / d;
185    let y1 = (8.0 - 2.0 * m * t * t) / d;
186    let y2 = (-4.0 - 4.0 * rp * t - m * t * t) / d;
187
188    // Low-pass-to-low-pass, or low-pass-to-high-pass, frequency transform.
189    let k = if high_pass {
190        -(w / 2.0 + 0.5).cos() / (w / 2.0 - 0.5).cos()
191    } else {
192        (0.5 - w / 2.0).sin() / (0.5 + w / 2.0).sin()
193    };
194
195    let d2 = 1.0 + y1 * k - y2 * k * k;
196    let b0 = (x0 - x1 * k + x2 * k * k) / d2;
197    let mut b1 = (-2.0 * x0 * k + x1 + x1 * k * k - 2.0 * x2 * k) / d2;
198    let b2 = (x0 * k * k - x1 * k + x2) / d2;
199    let mut a1 = (2.0 * k + y1 + y1 * k * k - 2.0 * y2 * k) / d2;
200    let a2 = (-(k * k) - y1 * k + y2) / d2;
201
202    if high_pass {
203        b1 = -b1;
204        a1 = -a1;
205    }
206
207    [b0, b1, b2, a1, a2]
208}
209
210/// Designs a complete, gain-normalized Chebyshev low-pass filter as a cascade of Direct Form I
211/// biquad stages (Steven W. Smith, Ch. 20). `out_coeffs` must be a slice of size
212/// `5 * (num_poles / 2)`. `num_poles` must be even, `2..=20`; `ripple_percent` in `0.0..29.0`.
213/// Larger pole counts amplify `f32` round-off error per the book's own guidance, and should be
214/// used with care (consider `f64` or splitting into explicit two-pole stages for high orders).
215pub fn chebyshev_lowpass_biquads(
216    cutoff_norm: f32,
217    ripple_percent: f32,
218    num_poles: u32,
219    out_coeffs: &mut [f32],
220) {
221    chebyshev_biquads(cutoff_norm, false, ripple_percent, num_poles, out_coeffs);
222}
223
224/// Designs a complete, gain-normalized Chebyshev high-pass filter as a cascade of Direct Form I
225/// biquad stages (Steven W. Smith, Ch. 20). See [`chebyshev_lowpass_biquads`] for parameters.
226pub fn chebyshev_highpass_biquads(
227    cutoff_norm: f32,
228    ripple_percent: f32,
229    num_poles: u32,
230    out_coeffs: &mut [f32],
231) {
232    chebyshev_biquads(cutoff_norm, true, ripple_percent, num_poles, out_coeffs);
233}
234
235fn chebyshev_biquads(
236    cutoff_norm: f32,
237    high_pass: bool,
238    ripple_percent: f32,
239    num_poles: u32,
240    out_coeffs: &mut [f32],
241) {
242    let num_stages = (num_poles / 2) as usize;
243    assert!(
244        out_coeffs.len() >= num_stages * 5,
245        "out_coeffs buffer too small"
246    );
247
248    // Overall passband gain is the product of each stage's gain at the reference frequency
249    // (DC for low-pass, Nyquist for high-pass); normalizing the cascade to unity gain there is
250    // equivalent to dividing any single stage's numerator by that product.
251    let mut total_gain = 1.0f32;
252    for k in 0..num_stages {
253        let stage = chebyshev_biquad_stage(
254            cutoff_norm,
255            high_pass,
256            ripple_percent,
257            num_poles,
258            (k + 1) as u32,
259        );
260        let [b0, b1, b2, a1, a2] = stage;
261        total_gain *= if high_pass {
262            (b0 - b1 + b2) / (1.0 + a1 - a2)
263        } else {
264            (b0 + b1 + b2) / (1.0 - a1 - a2)
265        };
266        out_coeffs[k * 5..(k + 1) * 5].copy_from_slice(&stage);
267    }
268
269    if total_gain != 0.0 {
270        let inv_gain = 1.0 / total_gain;
271        out_coeffs[0] *= inv_gain;
272        out_coeffs[1] *= inv_gain;
273        out_coeffs[2] *= inv_gain;
274    }
275}
276
277// --- Single-Pole Recursive Filter Design (Steven W. Smith, Ch. 19) ---
278
279/// Converts a normalized cutoff frequency (`0.0..0.5`, cycles/sample) to the sample-to-sample
280/// decay factor `x` used to design a single-pole recursive filter (Eq. 19-5).
281pub fn single_pole_decay_from_cutoff(cutoff_norm: f32) -> f32 {
282    (-2.0 * core::f32::consts::PI * cutoff_norm).exp()
283}
284
285/// Converts a time constant (in samples, the time to decay to `1/e` ≈ 36.8%) to the
286/// sample-to-sample decay factor `x` used to design a single-pole recursive filter (Eq. 19-4).
287pub fn single_pole_decay_from_time_constant(time_constant_samples: f32) -> f32 {
288    (-1.0 / time_constant_samples).exp()
289}
290
291/// Pre-warps continuous cutoff frequency `fc` for the bilinear transform at sampling rate `fs`.
292/// Returns pre-warped analog frequency $\omega_p = 2 f_s \tan(\pi f_c / f_s)$.
293pub fn prewarp_cutoff_f32(fc: f32, fs: f32) -> f32 {
294    let pi_fc_over_fs = core::f32::consts::PI * fc / fs;
295    2.0 * fs * pi_fc_over_fs.tan()
296}
297
298/// Converts a 2nd-order analog prototype filter section $H(s) = \frac{a_2 s^2 + a_1 s + a_0}{b_2 s^2 + b_1 s + b_0}$
299/// into discrete Direct Form I Biquad coefficients `[b0, b1, b2, a1, a2]` using the Bilinear Transform.
300pub fn bilinear_transform_biquad(
301    a0: f32,
302    a1: f32,
303    a2: f32,
304    b0: f32,
305    b1: f32,
306    b2: f32,
307    sample_rate: f32,
308) -> [f32; 5] {
309    let fs = sample_rate;
310    let fs2 = fs * fs;
311
312    let ad0 = 4.0 * a2 * fs2 + 2.0 * a1 * fs + a0;
313    let ad1 = 2.0 * a0 - 8.0 * a2 * fs2;
314    let ad2 = 4.0 * a2 * fs2 - 2.0 * a1 * fs + a0;
315
316    let bd0 = 4.0 * b2 * fs2 + 2.0 * b1 * fs + b0;
317    let bd1 = 2.0 * b0 - 8.0 * b2 * fs2;
318    let bd2 = 4.0 * b2 * fs2 - 2.0 * b1 * fs + b0;
319
320    let inv_bd0 = 1.0 / bd0;
321
322    let b_0 = ad0 * inv_bd0;
323    let b_1 = ad1 * inv_bd0;
324    let b_2 = ad2 * inv_bd0;
325    let a_1 = -bd1 * inv_bd0;
326    let a_2 = -bd2 * inv_bd0;
327    [b_0, b_1, b_2, a_1, a_2]
328}
329
330// --- Windowed-Sinc FIR Filter Design (Steven W. Smith, Ch. 16) ---
331
332use crate::types::Status;
333
334/// Computes a Low-Pass FIR filter kernel using the Blackman-Windowed Sinc method.
335///
336/// `fc_norm`: Cutoff frequency as a fraction of sampling rate ($0 < f_c < 0.5$).
337/// `out_taps`: Destination slice for filter coefficients. Length $M$ must be odd and $\ge 3$.
338pub fn fir_windowed_sinc_lowpass(fc_norm: f32, out_taps: &mut [f32]) -> Status {
339    let m = out_taps.len();
340    if m < 3 || m % 2 == 0 || fc_norm <= 0.0 || fc_norm >= 0.5 {
341        return Status::ArgumentError;
342    }
343
344    let half = (m - 1) as f32 / 2.0;
345    let two_pi_fc = 2.0 * core::f32::consts::PI * fc_norm;
346    let two_pi_over_m = 2.0 * core::f32::consts::PI / (m - 1) as f32;
347
348    let mut sum = 0.0f32;
349    for i in 0..m {
350        let d = (i as f32) - half;
351        let sinc = if d == 0.0 {
352            two_pi_fc
353        } else {
354            (two_pi_fc * d).sin() / d
355        };
356
357        // Blackman window
358        let w = 0.42 - 0.5 * (two_pi_over_m * i as f32).cos()
359            + 0.08 * (2.0 * two_pi_over_m * i as f32).cos();
360        let tap = sinc * w;
361        out_taps[i] = tap;
362        sum += tap;
363    }
364
365    // Normalize for 0 dB DC gain
366    if sum != 0.0 {
367        let inv_sum = 1.0 / sum;
368        for i in 0..m {
369            out_taps[i] *= inv_sum;
370        }
371    }
372
373    Status::Success
374}
375
376/// Computes a High-Pass FIR filter kernel using spectral inversion of the Windowed-Sinc Low-Pass.
377///
378/// `fc_norm`: Cutoff frequency as a fraction of sampling rate ($0 < f_c < 0.5$).
379/// `out_taps`: Destination slice for filter coefficients. Length $M$ must be odd and $\ge 3$.
380pub fn fir_windowed_sinc_highpass(fc_norm: f32, out_taps: &mut [f32]) -> Status {
381    let status = fir_windowed_sinc_lowpass(fc_norm, out_taps);
382    if status != Status::Success {
383        return status;
384    }
385
386    let m = out_taps.len();
387    let center = (m - 1) / 2;
388
389    // Spectral inversion: negate all taps and add 1.0 to center tap
390    for i in 0..m {
391        out_taps[i] = -out_taps[i];
392    }
393    out_taps[center] += 1.0;
394
395    Status::Success
396}
397
398/// Computes a Band-Pass FIR filter kernel using the difference of two Windowed-Sinc Low-Pass filters.
399pub fn fir_windowed_sinc_bandpass(
400    f_low_norm: f32,
401    f_high_norm: f32,
402    out_taps: &mut [f32],
403) -> Status {
404    let m = out_taps.len();
405    if m < 3 || m % 2 == 0 || f_low_norm <= 0.0 || f_high_norm >= 0.5 || f_low_norm >= f_high_norm {
406        return Status::ArgumentError;
407    }
408
409    let half = (m - 1) as f32 / 2.0;
410    let two_pi_flow = 2.0 * core::f32::consts::PI * f_low_norm;
411    let two_pi_fhigh = 2.0 * core::f32::consts::PI * f_high_norm;
412    let two_pi_over_m = 2.0 * core::f32::consts::PI / (m - 1) as f32;
413
414    for i in 0..m {
415        let d = (i as f32) - half;
416        let sinc_low = if d == 0.0 {
417            two_pi_flow
418        } else {
419            (two_pi_flow * d).sin() / d
420        };
421        let sinc_high = if d == 0.0 {
422            two_pi_fhigh
423        } else {
424            (two_pi_fhigh * d).sin() / d
425        };
426        let w = 0.42 - 0.5 * (two_pi_over_m * i as f32).cos()
427            + 0.08 * (2.0 * two_pi_over_m * i as f32).cos();
428        out_taps[i] = (sinc_high - sinc_low) * w;
429    }
430
431    // Normalize so center passband gain is 1.0
432    let f_center = (f_low_norm + f_high_norm) / 2.0;
433    let mut real_gain = 0.0f32;
434    let mut imag_gain = 0.0f32;
435    for i in 0..m {
436        let angle = 2.0 * core::f32::consts::PI * f_center * (i as f32);
437        real_gain += out_taps[i] * angle.cos();
438        imag_gain -= out_taps[i] * angle.sin();
439    }
440    let mag = (real_gain * real_gain + imag_gain * imag_gain).sqrt();
441    if mag > 1e-12 {
442        let inv_mag = 1.0 / mag;
443        for i in 0..m {
444            out_taps[i] *= inv_mag;
445        }
446    }
447
448    Status::Success
449}
450
451/// Computes a Band-Stop (Notch / Band-Reject) FIR filter kernel using spectral inversion of Band-Pass.
452pub fn fir_windowed_sinc_bandstop(
453    f_low_norm: f32,
454    f_high_norm: f32,
455    out_taps: &mut [f32],
456) -> Status {
457    let m = out_taps.len();
458    if m < 3 || m % 2 == 0 || f_low_norm <= 0.0 || f_high_norm >= 0.5 || f_low_norm >= f_high_norm {
459        return Status::ArgumentError;
460    }
461
462    let status = fir_windowed_sinc_bandpass(f_low_norm, f_high_norm, out_taps);
463    if status != Status::Success {
464        return status;
465    }
466
467    let center = (m - 1) / 2;
468    for i in 0..m {
469        out_taps[i] = -out_taps[i];
470    }
471    out_taps[center] += 1.0;
472
473    Status::Success
474}
475
476// --- Custom Filter Design via Frequency Sampling (Steven W. Smith, Ch. 17) ---
477
478/// Designs a custom FIR filter kernel matching an arbitrary desired frequency response, using
479/// the frequency-sampling method: build a Hermitian-symmetric spectrum from the desired
480/// positive-frequency samples, inverse FFT it into an aliased impulse response, circularly
481/// shift, truncate, and apply a Hamming window.
482///
483/// `desired_real` / `desired_imag`: the desired frequency response in rectangular form,
484/// sampled at `fft_len / 2 + 1` points evenly spaced from DC (`0`) to Nyquist (`0.5`). For a
485/// well-behaved real filter, `desired_imag[0]` and `desired_imag[fft_len / 2]` should be `0`
486/// (the DC and Nyquist bins have no conjugate partner to mirror against).
487/// `fft_len`: must be a power of 2, `>= out_taps.len()`, and `<= 512`; larger values better
488/// approximate the desired response at the cost of a longer intermediate FFT.
489/// `out_taps`: destination for the resulting FIR kernel; its length `M + 1` must be odd.
490///
491/// Requires the `transform` feature (enabled by `full`).
492#[cfg(feature = "transform")]
493pub fn fir_custom_frequency_sampling(
494    desired_real: &[f32],
495    desired_imag: &[f32],
496    fft_len: usize,
497    out_taps: &mut [f32],
498) -> Status {
499    let m = out_taps.len();
500    if m < 3 || m % 2 == 0 {
501        return Status::ArgumentError;
502    }
503    if fft_len < 2 || (fft_len & (fft_len - 1)) != 0 || fft_len > 512 || fft_len < m {
504        return Status::ArgumentError;
505    }
506    let half_spec = fft_len / 2 + 1;
507    if desired_real.len() < half_spec || desired_imag.len() < half_spec {
508        return Status::LengthError;
509    }
510
511    let mut c_data = [0.0f32; 1024];
512    for k in 0..half_spec {
513        c_data[2 * k] = desired_real[k];
514        c_data[2 * k + 1] = desired_imag[k];
515    }
516    // Hermitian symmetry: negative-frequency bins are the conjugate mirror of the positive
517    // ones, which guarantees a real (not complex) time-domain impulse response.
518    for k in half_spec..fft_len {
519        let mirror = fft_len - k;
520        c_data[2 * k] = desired_real[mirror];
521        c_data[2 * k + 1] = -desired_imag[mirror];
522    }
523
524    crate::transform::cfft_f32(&mut c_data[..2 * fft_len], fft_len, 1, 1);
525
526    // Circular shift right by M/2 so the (aliased, wrapped-around) impulse response is
527    // centered before truncation, then window it.
528    let half = m / 2;
529    let two_pi_over_m = 2.0 * core::f32::consts::PI / (m - 1) as f32;
530    for i in 0..m {
531        let src_idx = (i + fft_len - half) % fft_len;
532        let w = 0.54 - 0.46 * (two_pi_over_m * i as f32).cos();
533        out_taps[i] = c_data[2 * src_idx] * w;
534    }
535
536    Status::Success
537}
538
539// ─────────────────────────────────────────────────────────────────────────────
540// Filter Quantization and Scaling Pipeline (Design in Float, Deploy in Fixed)
541// ─────────────────────────────────────────────────────────────────────────────
542
543use crate::types::{q15, q31};
544
545/// Gain scaling strategy for biquad SOS fixed-point quantization.
546#[derive(Debug, Clone, Copy, PartialEq, Eq)]
547pub enum ScalingStrategy {
548    /// Strict peak-gain scaling: guarantees no overflow for any sinusoidal input.
549    LInfNorm,
550    /// Energy-based root-mean-square gain scaling.
551    L2Norm,
552    /// Preserves direct coefficient scale (post_shift handles dynamic range).
553    Direct,
554}
555
556/// Quantizes and scales floating-point biquad cascade coefficients into Q15.
557///
558/// Returns `Ok(post_shift)` on success, which should be passed directly to
559/// [`crate::filtering::BiquadCascadeInstanceQ15`].
560pub fn biquad_quantize_and_scale_q15(
561    sos_f32: &[f32],
562    out_q15: &mut [q15],
563    strategy: ScalingStrategy,
564) -> Result<u8, Status> {
565    if sos_f32.len() != out_q15.len() || sos_f32.is_empty() || sos_f32.len() % 5 != 0 {
566        return Err(Status::LengthError);
567    }
568
569    let num_stages = sos_f32.len() / 5;
570    let mut max_coeff_mag = 0.0f32;
571
572    let mut scaled_f32 = [0.0f32; 128];
573    if sos_f32.len() > scaled_f32.len() {
574        return Err(Status::ArgumentError);
575    }
576
577    for stage in 0..num_stages {
578        let idx = stage * 5;
579        let mut b0 = sos_f32[idx];
580        let mut b1 = sos_f32[idx + 1];
581        let mut b2 = sos_f32[idx + 2];
582        let a1 = sos_f32[idx + 3];
583        let a2 = sos_f32[idx + 4];
584
585        let scale_factor = match strategy {
586            ScalingStrategy::LInfNorm => {
587                let peak = crate::filter_analysis::biquad_peak_gain(&[b0, b1, b2, a1, a2], 64);
588                if peak > 1.0 { 1.0 / peak } else { 1.0 }
589            }
590            ScalingStrategy::L2Norm => {
591                let l2 = crate::filter_analysis::biquad_l2_norm(&[b0, b1, b2, a1, a2], 64);
592                if l2 > 1.0 { 1.0 / l2 } else { 1.0 }
593            }
594            ScalingStrategy::Direct => 1.0,
595        };
596
597        b0 *= scale_factor;
598        b1 *= scale_factor;
599        b2 *= scale_factor;
600
601        scaled_f32[idx] = b0;
602        scaled_f32[idx + 1] = b1;
603        scaled_f32[idx + 2] = b2;
604        scaled_f32[idx + 3] = a1;
605        scaled_f32[idx + 4] = a2;
606
607        for k in 0..5 {
608            let mag = scaled_f32[idx + k].abs();
609            if mag > max_coeff_mag {
610                max_coeff_mag = mag;
611            }
612        }
613    }
614
615    let mut post_shift = 0u8;
616    let mut limit = 0.9999f32;
617    while limit < max_coeff_mag && post_shift < 14 {
618        post_shift += 1;
619        limit *= 2.0;
620    }
621
622    let status = crate::support::biquad_coeffs_f32_to_q15(&scaled_f32[..sos_f32.len()], out_q15, post_shift);
623    if status != Status::Success {
624        return Err(status);
625    }
626
627    Ok(post_shift)
628}
629
630/// Quantizes and scales floating-point biquad cascade coefficients into Q31.
631///
632/// Returns `Ok(post_shift)` on success.
633pub fn biquad_quantize_and_scale_q31(
634    sos_f32: &[f32],
635    out_q31: &mut [q31],
636    strategy: ScalingStrategy,
637) -> Result<u8, Status> {
638    if sos_f32.len() != out_q31.len() || sos_f32.is_empty() || sos_f32.len() % 5 != 0 {
639        return Err(Status::LengthError);
640    }
641
642    let num_stages = sos_f32.len() / 5;
643    let mut max_coeff_mag = 0.0f32;
644
645    let mut scaled_f32 = [0.0f32; 128];
646    if sos_f32.len() > scaled_f32.len() {
647        return Err(Status::ArgumentError);
648    }
649
650    for stage in 0..num_stages {
651        let idx = stage * 5;
652        let mut b0 = sos_f32[idx];
653        let mut b1 = sos_f32[idx + 1];
654        let mut b2 = sos_f32[idx + 2];
655        let a1 = sos_f32[idx + 3];
656        let a2 = sos_f32[idx + 4];
657
658        let scale_factor = match strategy {
659            ScalingStrategy::LInfNorm => {
660                let peak = crate::filter_analysis::biquad_peak_gain(&[b0, b1, b2, a1, a2], 64);
661                if peak > 1.0 { 1.0 / peak } else { 1.0 }
662            }
663            ScalingStrategy::L2Norm => {
664                let l2 = crate::filter_analysis::biquad_l2_norm(&[b0, b1, b2, a1, a2], 64);
665                if l2 > 1.0 { 1.0 / l2 } else { 1.0 }
666            }
667            ScalingStrategy::Direct => 1.0,
668        };
669
670        b0 *= scale_factor;
671        b1 *= scale_factor;
672        b2 *= scale_factor;
673
674        scaled_f32[idx] = b0;
675        scaled_f32[idx + 1] = b1;
676        scaled_f32[idx + 2] = b2;
677        scaled_f32[idx + 3] = a1;
678        scaled_f32[idx + 4] = a2;
679
680        for k in 0..5 {
681            let mag = scaled_f32[idx + k].abs();
682            if mag > max_coeff_mag {
683                max_coeff_mag = mag;
684            }
685        }
686    }
687
688    let mut post_shift = 0u8;
689    let mut limit = 0.9999f32;
690    while limit < max_coeff_mag && post_shift < 14 {
691        post_shift += 1;
692        limit *= 2.0;
693    }
694
695    let status = crate::support::biquad_coeffs_f32_to_q31(&scaled_f32[..sos_f32.len()], out_q31, post_shift);
696    if status != Status::Success {
697        return Err(status);
698    }
699
700    Ok(post_shift)
701}
702
703/// Quantizes floating-point FIR filter taps into Q15 format.
704pub fn fir_quantize_q15(taps_f32: &[f32], out_q15: &mut [q15]) -> Result<(), Status> {
705    if taps_f32.len() != out_q15.len() || taps_f32.is_empty() {
706        return Err(Status::LengthError);
707    }
708    for i in 0..taps_f32.len() {
709        out_q15[i] = q15::saturating_from_num(taps_f32[i]);
710    }
711    Ok(())
712}