idsp/iir/biquad.rs
1//! Biquad IIR
2
3use crate::Clamp;
4use core::ops::{Add, Div, Mul};
5use dsp_fixedpoint::Q;
6use dsp_process::{Process, SplitInplace, SplitProcess};
7
8#[cfg(not(feature = "std"))]
9#[allow(unused_imports)]
10use num_traits::float::FloatCore as _;
11use num_traits::{AsPrimitive, Float, clamp};
12
13/// Biquad IIR (second order section)
14///
15/// A biquadratic IIR filter supports up to two zeros and two poles in the transfer function.
16///
17/// The Biquad performs the following operation to compute a new output sample `y0` from a new
18/// input sample `x0` given its configuration and previous samples:
19///
20/// `y0 = b0*x0 + b1*x1 + b2*x2 + a1*y1 + a2*y2`
21///
22/// This implementation here saves storage and improves caching opportunities by decoupling
23/// filter configuration (coefficients, limits and offset) from filter state
24/// and thus supports both (a) sharing a single filter between multiple states ("channels") and (b)
25/// rapid switching of filters (tuning, transfer) for a given state without copying either
26/// state of configuration.
27///
28/// # Filter architecture
29///
30/// Direct Form 1 (DF1) and Direct Form 2 transposed (DF2T) are the only IIR filter
31/// structures with an (effective in the case of TDF2) single summing junction
32/// this allows clamping of the output before feedback.
33///
34/// DF1 allows atomic coefficient change because only inputs and outputs are stored.
35/// The summing junction pipelining of TDF2 would require incremental
36/// coefficient changes and is thus less amenable to online tuning.
37///
38/// DF2T needs less state storage (2 instead of 4). This is in addition to the coefficient
39/// storage (5 plus 2 limits plus 1 offset)
40///
41/// DF2T is less efficient and less accurate for fixed-point architectures as quantization
42/// happens at each intermediate summing junction in addition to the output quantization. This is
43/// especially true for common `i64 + i32 * i32 -> i64` MACC architectures.
44/// One could use wide state storage for fixed point DF2T but that would negate the storage
45/// and processing advantages.
46///
47/// # Coefficients
48///
49/// `ba: [T; 5] = [b0, b1, b2, a1, a2]` is the coefficients type.
50/// To represent the IIR coefficients, this contains the feed-forward
51/// coefficients `b0, b1, b2` followed by the feed-back coefficients
52/// `a1, a2`, all five normalized such that `a0 = 1`.
53///
54/// The summing junction of the [`BiquadClamp`] filter also receives an offset `u`
55/// and applies clamping such that `min <= y <= max`.
56///
57/// See [`crate::iir::coefficients::Filter`] and [`crate::iir::pid::Builder`] for ways to generate coefficients.
58///
59/// # Fixed point
60///
61/// Coefficient scaling for fixed point (i.e. integer) processing relies on [`dsp_fixedpoint::Q`].
62///
63/// Choose the number of fractional bits to meet coefficient range (e.g. potentially `a1 = 2`
64/// for a double integrator) and guard bits.
65///
66/// # PID controller
67///
68/// The IIR coefficients can be mapped to other transfer function
69/// representations, for example PID controllers as described in
70/// <https://hackmd.io/IACbwcOTSt6Adj3_F9bKuw> and
71/// <https://arxiv.org/abs/1508.06319>.
72///
73/// Using a Biquad as a template for a PID controller achieves several important properties:
74///
75/// * Its transfer function is universal in the sense that any biquadratic
76/// transfer function can be implemented (high-passes, gain limits, second
77/// order integrators with inherent anti-windup, notches etc) without code
78/// changes preserving all features.
79/// * It inherits a universal implementation of "integrator anti-windup", also
80/// and especially in the presence of set-point changes and in the presence
81/// of proportional or derivative gain without any back-off that would reduce
82/// steady-state output range.
83/// * It has universal derivative-kick (undesired, unlimited, and un-physical
84/// amplification of set-point changes by the derivative term) avoidance.
85/// * An offset at the input of an IIR filter (a.k.a. "set-point") is
86/// equivalent to an offset at the summing junction (in output units).
87/// They are related by the overall (DC feed-forward) gain of the filter.
88/// * It stores only previous outputs and inputs. These have direct and
89/// invariant interpretation (independent of coefficients and offset).
90/// Therefore it can trivially implement bump-less transfer between any
91/// coefficients/offset sets.
92/// * Cascading multiple IIR filters allows stable and robust
93/// implementation of transfer functions beyond biquadratic terms.
94#[derive(Clone, Debug, Default, PartialEq, PartialOrd, serde::Serialize, serde::Deserialize)]
95pub struct Biquad<C> {
96 /// Coefficients
97 ///
98 /// `[b0, b1, b2, a1, a2]`
99 ///
100 /// Such that
101 /// `y0 = (b0*x0 + b1*x1 + b2*x2 + a1*y1 + a2*y2)/(1 << F)`
102 /// where `x0, x1, x2` are current, delayed, and doubly delayed inputs and
103 /// `y0, y1, y2` are current, delayed, and doubly delayed outputs.
104 ///
105 /// Note the a1, a2 sign. The transfer function is:
106 /// `H(z) = (b0 + b1*z^-1 + b2*z^-2)/((1 << F) - a2*z^-1 - a2*z^-2)`
107 pub ba: [C; 5],
108}
109
110/// Second-order-section with offset and clamp
111#[derive(Clone, Debug, PartialEq, PartialOrd, serde::Serialize, serde::Deserialize)]
112pub struct BiquadClamp<C, T = C> {
113 /// Coefficients
114 pub coeff: Biquad<C>,
115
116 /// Summing junction offset
117 ///
118 /// ```
119 /// # use idsp::iir::*;
120 /// # use dsp_process::SplitProcess;
121 /// let mut i = BiquadClamp::<f32>::default();
122 /// i.u = 5.0;
123 /// assert_eq!(i.process(&mut DirectForm1::default(), 0.0), 5.0);
124 /// ```
125 pub u: T,
126
127 /// Summing junction lower limit
128 ///
129 /// ```
130 /// # use idsp::iir::*;
131 /// # use dsp_process::SplitProcess;
132 /// let mut i = BiquadClamp::<f32>::default();
133 /// i.min = 5.0;
134 /// assert_eq!(i.process(&mut DirectForm1::default(), 0.0), 5.0);
135 /// ```
136 pub min: T,
137
138 /// Summing junction upper limit
139 ///
140 /// ```
141 /// # use idsp::iir::*;
142 /// # use dsp_process::SplitProcess;
143 /// let mut i = BiquadClamp::<f32>::default();
144 /// i.max = -5.0;
145 /// assert_eq!(i.process(&mut DirectForm1::default(), 0.0), -5.0);
146 /// ```
147 pub max: T,
148}
149
150impl<C, T: Clamp> Default for BiquadClamp<C, T>
151where
152 Biquad<C>: Default,
153{
154 fn default() -> Self {
155 Self {
156 coeff: Biquad::default(),
157 u: T::ZERO,
158 min: T::MIN,
159 max: T::MAX,
160 }
161 }
162}
163
164impl<C: Clamp + Copy> Biquad<C> {
165 /// A unity gain filter
166 ///
167 /// ```
168 /// # use idsp::iir::*;
169 /// # use dsp_process::SplitProcess;
170 /// let x0 = 3.0;
171 /// let y0 = Biquad::<f32>::IDENTITY.process(&mut DirectForm1::default(), x0);
172 /// assert_eq!(y0, x0);
173 /// ```
174 pub const IDENTITY: Self = Self::proportional(C::ONE);
175
176 /// A filter with the given proportional gain at all frequencies
177 ///
178 /// ```
179 /// # use idsp::iir::*;
180 /// # use dsp_process::SplitProcess;
181 /// let x0 = 3.0;
182 /// let y0 = Biquad::<f32>::proportional(2.0).process(&mut DirectForm1::default(), x0);
183 /// assert_eq!(y0, 2.0 * x0);
184 /// ```
185 pub const fn proportional(k: C) -> Self {
186 Self {
187 ba: [k, C::ZERO, C::ZERO, C::ZERO, C::ZERO],
188 }
189 }
190 /// A "hold" filter that ingests input and maintains output
191 ///
192 /// ```
193 /// # use idsp::iir::*;
194 /// # use dsp_process::SplitProcess;
195 /// let mut state = DirectForm1::<f32>::default();
196 /// state.set_y(2.0);
197 /// let x0 = 7.0;
198 /// let y0 = Biquad::<f32>::HOLD.process(&mut state, x0);
199 /// assert_eq!(y0, 2.0);
200 /// ```
201 pub const HOLD: Self = Self {
202 ba: [C::ZERO, C::ZERO, C::ZERO, C::ONE, C::ZERO],
203 };
204}
205
206impl<C: Copy + Add<Output = C>> Biquad<C> {
207 /// DC forward gain fro input to summing junction
208 ///
209 /// ```
210 /// # use idsp::iir::*;
211 /// assert_eq!(Biquad::proportional(3.0).forward_gain(), 3.0);
212 /// ```
213 pub fn forward_gain(&self) -> C {
214 self.ba[0] + self.ba[1] + self.ba[2]
215 }
216}
217
218impl<C: Copy + Add<Output = C>, T: Copy + Div<C, Output = T> + Mul<C, Output = T>>
219 BiquadClamp<C, T>
220{
221 /// Summing junction offset referred to input
222 ///
223 /// ```
224 /// # use idsp::iir::*;
225 /// let mut i = BiquadClamp::from(Biquad::proportional(3.0));
226 /// i.u = 6.0;
227 /// assert_eq!(i.input_offset(), 2.0);
228 /// ```
229 pub fn input_offset(&self) -> T {
230 self.u / self.coeff.forward_gain()
231 }
232
233 /// Summing junction offset referred to input
234 ///
235 /// ```
236 /// # use idsp::iir::*;
237 /// let mut i = BiquadClamp::from(Biquad::proportional(3.0));
238 /// i.set_input_offset(2.0);
239 /// assert_eq!(i.u, 6.0);
240 /// ```
241 pub fn set_input_offset(&mut self, i: T) {
242 self.u = i * self.coeff.forward_gain();
243 }
244}
245
246#[derive(Clone, Debug)]
247/// Direct Form biquad/SOS state
248pub struct DirectForm<T, const N: usize = 1, const M: usize = 2> {
249 /// Input delay line
250 ///
251 /// `[x0, x1]`
252 pub x: [T; M],
253 /// Intermediate and output delay lines
254 ///
255 /// `[[y0, y1]]`
256 pub y: [[T; M]; N],
257}
258
259impl<T, const N: usize, const M: usize> Default for DirectForm<T, N, M>
260where
261 [T; M]: Default,
262 [[T; M]; N]: Default,
263{
264 fn default() -> Self {
265 Self {
266 x: Default::default(),
267 y: Default::default(),
268 }
269 }
270}
271
272impl<T: Copy, const N: usize> DirectForm<T, N> {
273 /// Get latest input
274 pub fn x0(&self) -> T {
275 self.x[0]
276 }
277
278 /// Get current output
279 pub fn y0(&self) -> T {
280 self.y.last().unwrap_or(&self.x)[0]
281 }
282
283 /// Set current and last output of the last stage.
284 ///
285 /// This is a NOP for `N=0`.
286 pub fn set_y(&mut self, y: T) {
287 if let Some(sy) = self.y.last_mut() {
288 *sy = [y, y];
289 }
290 }
291}
292
293/// N Poles at DC, N zeros at Nyquist
294impl<T: Copy + Add<Output = T>, const N: usize> Process<T> for DirectForm<T, N, 1> {
295 fn process(&mut self, x: T) -> T {
296 let (y0, y) = self.y.iter_mut().fold((x, &mut self.x), |(x0, x), y| {
297 let y0 = y[0] + x0 + x[0];
298 x[0] = x0;
299 (y0, y)
300 });
301 y[0] = y0;
302 y0
303 }
304}
305
306/// Direct form 1
307pub type DirectForm1<T, const M: usize = 2> = DirectForm<T, 1, M>;
308
309/// A cascade of `Biquad`s
310#[derive(Default, Clone, Debug, serde::Serialize, serde::Deserialize)]
311pub struct Cascade<C>(pub C);
312
313/// ```
314/// # use dsp_process::SplitProcess;
315/// # use idsp::iir::*;
316/// let mut state = DirectForm1 {
317/// x: [0.0, 1.0],
318/// y: [[2.0, 3.0]],
319/// };
320/// let x0 = 4.0;
321/// let y0 = Biquad::<f32>::IDENTITY.process(&mut state, x0);
322/// assert_eq!(y0, x0);
323/// assert_eq!(state.x, [x0, 0.0]);
324/// assert_eq!(state.y[0], [y0, 2.0]);
325/// ```
326impl<
327 const N: usize,
328 T: 'static + Copy,
329 C: Copy + Mul<T, Output = A>,
330 A: Add<Output = A> + AsPrimitive<T>,
331> SplitProcess<T, T, DirectForm<T, N>> for Cascade<[Biquad<C>; N]>
332{
333 fn process(&self, state: &mut DirectForm<T, N>, x0: T) -> T {
334 let (y0, y) =
335 self.0
336 .iter()
337 .zip(state.y.iter_mut())
338 .fold((x0, &mut state.x), |(x0, x), (c, y)| {
339 let y0 = (c.ba[0] * x0
340 + c.ba[1] * x[0]
341 + c.ba[2] * x[1]
342 + c.ba[3] * y[0]
343 + c.ba[4] * y[1])
344 .as_();
345 *x = [x0, x[0]];
346 (y0, y)
347 });
348 *y = [y0, y[0]];
349 y0
350 }
351}
352
353impl<
354 T: 'static + Copy + Add<Output = T> + PartialOrd,
355 C: Copy + Mul<T, Output = A>,
356 A: Add<Output = A> + AsPrimitive<T>,
357> SplitProcess<T, T, DirectForm1<T>> for Biquad<C>
358{
359 fn process(&self, state: &mut DirectForm1<T>, x0: T) -> T {
360 let y0 = (self.ba[0] * x0
361 + self.ba[1] * state.x[0]
362 + self.ba[2] * state.x[1]
363 + self.ba[3] * state.y[0][0]
364 + self.ba[4] * state.y[0][1])
365 .as_();
366 state.x = [x0, state.x[0]];
367 state.y[0] = [y0, state.y[0][0]];
368 y0
369 }
370}
371
372/// ```
373/// use dsp_process::SplitProcess;
374/// use idsp::iir::*;
375/// let biquad = BiquadClamp::<f32, f32>::from(Biquad::IDENTITY);
376/// let mut state = DirectForm2Transposed::default();
377/// let x = 3.0f32;
378/// let y = biquad.process(&mut state, x);
379/// assert_eq!(x, y);
380/// ```
381impl<T: Copy + Add<Output = T> + PartialOrd, C> SplitProcess<T, T, DirectForm1<T>>
382 for BiquadClamp<C, T>
383where
384 Biquad<C>: SplitProcess<T, T, DirectForm1<T>>,
385{
386 fn process(&self, state: &mut DirectForm1<T>, x0: T) -> T {
387 let y0 = clamp(self.coeff.process(state, x0) + self.u, self.min, self.max);
388 state.y[0][0] = y0; // overwrite
389 y0
390 }
391}
392
393/// Direct form 2 transposed SOS state
394pub type DirectForm2Transposed<T, const M: usize = 2> = DirectForm<T, 0, M>;
395
396/// ```
397/// use dsp_process::SplitProcess;
398/// use idsp::iir::*;
399/// let biquad = Biquad::<f32>::IDENTITY;
400/// let mut state = DirectForm2Transposed::default();
401/// let x = 3.0f32;
402/// let y = biquad.process(&mut state, x);
403/// assert_eq!(x, y);
404/// ```
405impl<T: Copy + Mul<Output = T> + Add<Output = T>> SplitProcess<T, T, DirectForm2Transposed<T>>
406 for Biquad<T>
407{
408 fn process(&self, state: &mut DirectForm2Transposed<T>, x0: T) -> T {
409 let ba = &self.ba;
410 let y0 = state.x[0] + ba[0] * x0;
411 state.x[0] = state.x[1] + ba[1] * x0 + ba[3] * y0;
412 state.x[1] = ba[2] * x0 + ba[4] * y0;
413 y0
414 }
415}
416
417impl<T: Copy + Add<Output = T> + Mul<Output = T> + PartialOrd>
418 SplitProcess<T, T, DirectForm2Transposed<T>> for BiquadClamp<T, T>
419{
420 fn process(&self, state: &mut DirectForm2Transposed<T>, x0: T) -> T {
421 let ba = &self.coeff.ba;
422 let y0 = clamp(state.x[0] + ba[0] * x0 + self.u, self.min, self.max);
423 state.x[0] = state.x[1] + ba[1] * x0 + ba[3] * y0;
424 state.x[1] = ba[2] * x0 + ba[4] * y0;
425 y0
426 }
427}
428
429/// SOS state with wide Y
430#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
431pub struct DirectForm1Wide {
432 /// X state
433 ///
434 /// `[x1, x2]`
435 pub x: [i32; 2],
436 /// Y state
437 ///
438 /// `[y1, y2]`
439 pub y: [i64; 2],
440}
441
442impl<const F: i8> SplitProcess<i32, i32, DirectForm1Wide> for Biquad<Q<i32, i64, F>> {
443 fn process(&self, state: &mut DirectForm1Wide, x0: i32) -> i32 {
444 const {
445 assert!(F >= 0 && F < 32);
446 }
447 let mut acc = (self.ba[0] * x0 + self.ba[1] * state.x[0] + self.ba[2] * state.x[1]).inner;
448 state.x = [x0, state.x[0]];
449 acc += (state.y[0] as u32 as i64 * self.ba[3].inner as i64) >> 32;
450 acc += (state.y[0] >> 32) as i32 as i64 * self.ba[3].inner as i64;
451 acc += (state.y[1] as u32 as i64 * self.ba[4].inner as i64) >> 32;
452 acc += (state.y[1] >> 32) as i32 as i64 * self.ba[4].inner as i64;
453 acc <<= 32 - F;
454 state.y = [acc, state.y[0]];
455 (acc >> 32) as _
456 }
457}
458
459impl<const F: i8> SplitProcess<i32, i32, DirectForm1Wide> for BiquadClamp<Q<i32, i64, F>, i32> {
460 fn process(&self, state: &mut DirectForm1Wide, x0: i32) -> i32 {
461 let y0 = clamp(self.coeff.process(state, x0) + self.u, self.min, self.max);
462 state.y[0] = ((y0 as i64) << 32) | state.y[0] as u32 as i64; // overwrite
463 y0
464 }
465}
466
467/// SOS state with first order error feedback
468#[derive(Clone, Debug, Default)]
469pub struct DirectForm1Dither {
470 /// X,Y state
471 ///
472 /// `{ x: [x0, x1], y: [[y0, y1]] }`
473 pub xy: DirectForm1<i32>,
474 /// Error feedback
475 pub e: u32,
476}
477
478/// ```
479/// # use dsp_process::SplitProcess;
480/// # use dsp_fixedpoint::Q32;
481/// # use idsp::iir::*;
482/// let mut state = DirectForm1Dither {
483/// xy: DirectForm {
484/// x: [1, 2],
485/// y: [[3, 4]],
486/// },
487/// e: 5,
488/// };
489/// let x0 = 6;
490/// let y0 = Biquad::<Q32<30>>::IDENTITY.process(&mut state, x0);
491/// assert_eq!(y0, x0);
492/// assert_eq!(state.xy.x, [x0, 1]);
493/// assert_eq!(state.xy.y, [[y0, 3]]);
494/// assert_eq!(state.e, 5);
495/// ```
496impl<const F: i8> SplitProcess<i32, i32, DirectForm1Dither> for Biquad<Q<i32, i64, F>> {
497 fn process(&self, state: &mut DirectForm1Dither, x0: i32) -> i32 {
498 const {
499 assert!(F >= 0 && F < 32);
500 }
501 let mut acc = state.e as i64
502 + (self.ba[0] * x0
503 + self.ba[1] * state.xy.x[0]
504 + self.ba[2] * state.xy.x[1]
505 + self.ba[3] * state.xy.y[0][0]
506 + self.ba[4] * state.xy.y[0][1])
507 .inner;
508 acc <<= 32 - F;
509 state.e = (acc as u32) >> (32 - F);
510 let y0 = (acc >> 32) as _;
511 state.xy.x = [x0, state.xy.x[0]];
512 state.xy.y[0] = [y0, state.xy.y[0][0]];
513 y0
514 }
515}
516
517impl<const F: i8> SplitProcess<i32, i32, DirectForm1Dither> for BiquadClamp<Q<i32, i64, F>, i32> {
518 fn process(&self, state: &mut DirectForm1Dither, x0: i32) -> i32 {
519 let y0 = clamp(self.coeff.process(state, x0) + self.u, self.min, self.max);
520 state.xy.y[0][0] = y0; // overwrite
521 y0
522 }
523}
524
525impl<C, T: Copy, S> SplitInplace<T, S> for Biquad<C> where Self: SplitProcess<T, T, S> {}
526impl<C, T: Copy, S> SplitInplace<T, S> for BiquadClamp<C, T> where Self: SplitProcess<T, T, S> {}
527impl<C, T: Copy, S> SplitInplace<T, S> for Cascade<C> where Self: SplitProcess<T, T, S> {}
528
529/// `[[b0, b1, b2], [a0, a1, a2]]` coefficients with the literature sign of a1/a2
530macro_rules! impl_from_float {
531 ($ty:ident) => {
532 impl<C> From<[[$ty; 3]; 2]> for Biquad<C>
533 where
534 [$ty; 5]: Into<Biquad<C>>,
535 {
536 fn from(ba: [[$ty; 3]; 2]) -> Self {
537 let a0 = 1.0 / ba[1][0];
538 [
539 ba[0][0] * a0,
540 ba[0][1] * a0,
541 ba[0][2] * a0,
542 -ba[1][1] * a0,
543 -ba[1][2] * a0,
544 ]
545 .into()
546 }
547 }
548 };
549}
550impl_from_float!(f32);
551impl_from_float!(f64);
552
553/// Normalized and sign-flipped coefficients
554/// `[b0, b1, b2, a1, a2]`
555impl<C: Copy + 'static, T: AsPrimitive<C>> From<[T; 5]> for Biquad<C> {
556 fn from(ba: [T; 5]) -> Self {
557 Self {
558 ba: ba.map(AsPrimitive::as_),
559 }
560 }
561}
562
563impl<C, T, F: Into<Biquad<C>>> From<F> for BiquadClamp<C, T>
564where
565 Self: Default,
566{
567 fn from(coeff: F) -> Self {
568 Self {
569 coeff: coeff.into(),
570 ..Default::default()
571 }
572 }
573}
574
575/// A pair of roots
576#[derive(Debug, Clone)]
577pub enum Pair<T> {
578 /// Two real roots
579 Real([T; 2]),
580 /// A pair of complex conjugate roots `X+-jY`
581 Complex([T; 2]),
582}
583
584impl<T: Float> Pair<T> {
585 /// Convert to real polynomial coefficients
586 pub fn coeff(self) -> [T; 2] {
587 match self {
588 Self::Real([x, y]) => [x + y, x * y],
589 Self::Complex([x, y]) => [x + x, x * x + y * y],
590 }
591 }
592}
593
594impl<C> Biquad<C> {
595 /// Convert from zeros pair, poles pair and gain
596 pub fn from_zpk<T: Float>(zeros: Pair<T>, poles: Pair<T>, gain: T) -> Self
597 where
598 Self: From<[T; 5]>,
599 {
600 let b = zeros.coeff().map(|b| gain * b);
601 let a = poles.coeff();
602 [gain, -b[0], b[1], a[0], -a[1]].into()
603 }
604}
605
606#[cfg(test)]
607mod test {
608 #![allow(dead_code)]
609 use super::*;
610 use dsp_fixedpoint::Q32;
611 use dsp_process::SplitInplace;
612 // No manual tuning needed here.
613 // Compiler knows best how and when:
614 // unroll loops
615 // cache on stack
616 // handle alignment
617 // register allocate variables
618 // manage pipeline and insn issue
619
620 // cargo asm idsp::iir::biquad::test::pnm -p idsp --rust --target thumbv7em-none-eabihf --lib --target-cpu cortex-m7 --color --mca -M=-iterations=1 -M=-timeline -M=-skip-unsupported-instructions=lack-sched | less -R
621
622 pub fn pnm(
623 config: &Cascade<[Biquad<Q32<29>>; 4]>,
624 state: &mut DirectForm<i32, 4>,
625 xy0: &mut [i32; 1 << 3],
626 ) {
627 config.inplace(state, xy0);
628 }
629
630 // ~20 cycles/sample/sos on skylake, >200 MS/s
631 #[test]
632 #[ignore]
633 fn sos_insn() {
634 let cfg = Cascade(
635 [
636 [[1., 3., 5.], [19., -9., 9.]],
637 [[3., 3., 5.], [21., -11., 11.]],
638 [[1., 3., 5.], [55., -17., 17.]],
639 [[1., 8., 5.], [77., -7., 7.]],
640 ]
641 .map(|c| Biquad::from(c)),
642 );
643 let mut state = Default::default();
644 let mut x = [977371917; 1 << 7];
645 for _ in 0..1 << 20 {
646 x[9] = x[63];
647 let (x, []) = x.as_chunks_mut() else {
648 unreachable!()
649 };
650 for x in x {
651 pnm(&cfg, &mut state, x);
652 }
653 }
654 }
655}