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}