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}