Skip to main content

numeris/control/
biquad.rs

1use crate::traits::FloatScalar;
2
3/// A single second-order section (biquad) filter using Direct Form II Transposed.
4///
5/// Transfer function:
6/// ```text
7/// H(z) = (b0 + b1·z⁻¹ + b2·z⁻²) / (1 + a1·z⁻¹ + a2·z⁻²)
8/// ```
9///
10/// The denominator is stored normalized so `a[0] = 1`.
11#[derive(Debug, Clone, Copy)]
12pub struct Biquad<T> {
13    b: [T; 3],
14    a: [T; 3], // [1, a1, a2]
15    z: [T; 2], // DFII-T state
16}
17
18impl<T: FloatScalar> Biquad<T> {
19    /// Create a new biquad from numerator `b` and denominator `a` coefficients.
20    ///
21    /// Normalizes by `a[0]` so the stored `a[0]` is always 1.
22    ///
23    /// # Example
24    ///
25    /// ```
26    /// use numeris::control::Biquad;
27    ///
28    /// let bq = Biquad::new([1.0, 2.0, 1.0], [1.0, -0.5, 0.1]);
29    /// let (b, a) = bq.coefficients();
30    /// assert_eq!(a[0], 1.0);
31    /// ```
32    pub fn new(b: [T; 3], a: [T; 3]) -> Self {
33        let a0 = a[0];
34        Self {
35            b: [b[0] / a0, b[1] / a0, b[2] / a0],
36            a: [T::one(), a[1] / a0, a[2] / a0],
37            z: [T::zero(); 2],
38        }
39    }
40
41    /// Identity (passthrough) filter: output equals input.
42    pub fn passthrough() -> Self {
43        Self {
44            b: [T::one(), T::zero(), T::zero()],
45            a: [T::one(), T::zero(), T::zero()],
46            z: [T::zero(); 2],
47        }
48    }
49
50    /// Process a single input sample, returning the filtered output.
51    ///
52    /// Uses Direct Form II Transposed for numerical stability.
53    #[inline]
54    pub fn tick(&mut self, x: T) -> T {
55        let y = self.b[0] * x + self.z[0];
56        self.z[0] = self.b[1] * x - self.a[1] * y + self.z[1];
57        self.z[1] = self.b[2] * x - self.a[2] * y;
58        y
59    }
60
61    /// Reset internal state to zero.
62    pub fn reset(&mut self) {
63        self.z = [T::zero(); 2];
64    }
65
66    /// Process a slice of input samples into an output slice.
67    ///
68    /// # Panics
69    ///
70    /// Panics if `output.len() < input.len()`.
71    pub fn process(&mut self, input: &[T], output: &mut [T]) {
72        assert!(output.len() >= input.len());
73        for (i, &x) in input.iter().enumerate() {
74            output[i] = self.tick(x);
75        }
76    }
77
78    /// Process a slice of samples in-place.
79    pub fn process_inplace(&mut self, data: &mut [T]) {
80        for sample in data.iter_mut() {
81            *sample = self.tick(*sample);
82        }
83    }
84
85    /// Return the `(b, a)` coefficient arrays.
86    pub fn coefficients(&self) -> ([T; 3], [T; 3]) {
87        (self.b, self.a)
88    }
89}
90
91/// A cascade of `N` biquad (second-order) sections.
92///
93/// Filter order is `2*N` for all-second-order sections, or `2*N - 1` when the
94/// last section is a degenerate first-order biquad (`b2 = a2 = 0`).
95///
96/// # Example
97///
98/// ```
99/// use numeris::control::{butterworth_lowpass, BiquadCascade};
100///
101/// // 4th-order Butterworth → 2 biquad sections
102/// let mut lpf: BiquadCascade<f64, 2> = butterworth_lowpass(4, 1000.0, 8000.0).unwrap();
103/// let y = lpf.tick(1.0);
104/// ```
105#[derive(Debug, Clone, Copy)]
106pub struct BiquadCascade<T, const N: usize> {
107    pub sections: [Biquad<T>; N],
108}
109
110impl<T: FloatScalar, const N: usize> BiquadCascade<T, N> {
111    /// Process a single input sample through all sections in series.
112    #[inline]
113    pub fn tick(&mut self, x: T) -> T {
114        let mut y = x;
115        for section in self.sections.iter_mut() {
116            y = section.tick(y);
117        }
118        y
119    }
120
121    /// Reset all sections' internal state to zero.
122    pub fn reset(&mut self) {
123        for section in self.sections.iter_mut() {
124            section.reset();
125        }
126    }
127
128    /// Process a slice of input samples into an output slice.
129    ///
130    /// # Panics
131    ///
132    /// Panics if `output.len() < input.len()`.
133    pub fn process(&mut self, input: &[T], output: &mut [T]) {
134        assert!(output.len() >= input.len());
135        for (i, &x) in input.iter().enumerate() {
136            output[i] = self.tick(x);
137        }
138    }
139
140    /// Process a slice of samples in-place.
141    pub fn process_inplace(&mut self, data: &mut [T]) {
142        for sample in data.iter_mut() {
143            *sample = self.tick(*sample);
144        }
145    }
146
147    /// Actual filter order (detects degenerate first-order last section).
148    pub fn order(&self) -> usize {
149        if N == 0 {
150            return 0;
151        }
152        let last = &self.sections[N - 1];
153        let (b, a) = last.coefficients();
154        // Degenerate first-order: b2 == 0 and a2 == 0
155        if b[2] == T::zero() && a[2] == T::zero() {
156            2 * N - 1
157        } else {
158            2 * N
159        }
160    }
161}
162
163// ─────────────────────────────────────────────────────────────────────
164// Bilinear transform helpers (pub(super) — used by butterworth/chebyshev)
165// ─────────────────────────────────────────────────────────────────────
166
167/// Build a lowpass biquad from a conjugate analog pole pair `(σ ± jω)`.
168///
169/// `wa` is the pre-warped analog cutoff, `c = 2·fs`.
170pub(super) fn bilinear_lp_pair<T: FloatScalar>(sigma: T, omega: T, wa: T, c: T) -> Biquad<T> {
171    let two = T::one() + T::one();
172    let d = c * c - two * sigma * c + sigma * sigma + omega * omega;
173    let wa2 = wa * wa;
174    let b0 = wa2 / d;
175    let b1 = two * wa2 / d;
176    let b2 = b0;
177    let a1 = two * (sigma * sigma + omega * omega - c * c) / d;
178    let a2 = (c * c + two * sigma * c + sigma * sigma + omega * omega) / d;
179    Biquad::new([b0, b1, b2], [T::one(), a1, a2])
180}
181
182/// Build a highpass biquad from a conjugate analog pole pair `(σ ± jω)`.
183pub(super) fn bilinear_hp_pair<T: FloatScalar>(sigma: T, omega: T, _wa: T, c: T) -> Biquad<T> {
184    let two = T::one() + T::one();
185    let c2 = c * c;
186    let d = c2 - two * sigma * c + sigma * sigma + omega * omega;
187    let b0 = c2 / d;
188    let b1 = -(two * c2) / d;
189    let b2 = b0;
190    let a1 = two * (sigma * sigma + omega * omega - c2) / d;
191    let a2 = (c2 + two * sigma * c + sigma * sigma + omega * omega) / d;
192    Biquad::new([b0, b1, b2], [T::one(), a1, a2])
193}
194
195/// Build a lowpass biquad from a single real analog pole `σ` (odd-order case).
196pub(super) fn bilinear_lp_real<T: FloatScalar>(sigma: T, wa: T, c: T) -> Biquad<T> {
197    // sigma is negative for stable poles; wa > 0
198    // LP: b0 = wa/(c - sigma), b1 = b0, b2 = 0
199    //     a1 = (sigma + c) is wrong — let's derive carefully:
200    // H_a(s) = wa / (s - sigma)   [sigma < 0]
201    // Bilinear: s = c·(z-1)/(z+1)
202    // H(z) = wa / (c·(z-1)/(z+1) - sigma)
203    //       = wa·(z+1) / (c·(z-1) - sigma·(z+1))
204    //       = wa·(z+1) / ((c - sigma)z + (-c - sigma))
205    // Divide by (c - sigma):
206    //   b0 = wa/(c - sigma), b1 = wa/(c - sigma), b2 = 0
207    //   a0 = 1, a1 = (-c - sigma)/(c - sigma), a2 = 0
208    let denom = c - sigma;
209    let b0 = wa / denom;
210    Biquad::new([b0, b0, T::zero()], [T::one(), (-c - sigma) / denom, T::zero()])
211}
212
213/// Build a highpass biquad from a single real analog pole `σ` (odd-order case).
214pub(super) fn bilinear_hp_real<T: FloatScalar>(sigma: T, _wa: T, c: T) -> Biquad<T> {
215    // H_a(s) = s / (s - sigma)   [sigma < 0]
216    // Bilinear: s = c·(z-1)/(z+1)
217    // H(z) = c·(z-1)/(z+1) / (c·(z-1)/(z+1) - sigma)
218    //       = c·(z-1) / (c·(z-1) - sigma·(z+1))
219    //       = c·(z-1) / ((c-sigma)z + (-c-sigma))
220    // Divide by (c - sigma):
221    //   b0 = c/(c - sigma), b1 = -c/(c - sigma), b2 = 0
222    //   a0 = 1, a1 = (-c - sigma)/(c - sigma), a2 = 0
223    let denom = c - sigma;
224    let b0 = c / denom;
225    Biquad::new(
226        [b0, -b0, T::zero()],
227        [T::one(), (-c - sigma) / denom, T::zero()],
228    )
229}