use core::ops::{Add, Mul, Neg};
use dsp_process::{SplitInplace, SplitProcess};
use num_traits::AsPrimitive;
#[cfg(not(feature = "std"))]
#[allow(unused_imports)]
use num_traits::float::Float as _;
use crate::Complex;
use super::DirectForm1;
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))]
pub struct Normal<C> {
pub b: [C; 3],
pub p: Complex<C>,
}
impl<
C: Copy + Mul<T, Output = A> + Neg<Output = C>,
A: Add<Output = A> + AsPrimitive<T>,
T: 'static + Copy + Add<Output = T>,
> SplitProcess<T, T, DirectForm1<T>> for Normal<C>
{
fn process(&self, state: &mut DirectForm1<T>, x0: T) -> T {
let [u, v] = state.y[0];
let y = (self.b[0] * x0 + self.b[1] * u + self.b[2] * v).as_();
let rotated: T = (self.p.re() * u + self.p.im() * v).as_();
state.y[0] = [rotated + x0, (self.p.re() * v + -self.p.im() * u).as_()];
y
}
}
impl<C, T: Copy> SplitInplace<T, DirectForm1<T>> for Normal<C> where
Self: SplitProcess<T, T, DirectForm1<T>>
{
}
impl<C: 'static + Copy> From<&[[f64; 3]; 2]> for Normal<C>
where
f64: AsPrimitive<C>,
{
fn from(ba: &[[f64; 3]; 2]) -> Self {
let a0 = ba[1][0].recip();
let [b0, b1, b2] = ba[0].map(|b| b * a0);
let re = -0.5 * ba[1][1] * a0;
let norm = ba[1][2] * a0;
let im2 = norm - re * re;
assert!(im2 > 0.0, "normal form requires non-real poles");
let im = im2.sqrt();
let c1 = b1 + 2.0 * re * b0;
let b = [b0, c1, (norm * b0 - re * c1 - b2) / im];
Self {
b: b.map(AsPrimitive::as_),
p: Complex([re.as_(), im.as_()]),
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::iir::Biquad;
#[test]
#[should_panic(expected = "normal form requires non-real poles")]
fn rejects_real_poles() {
let _ = Normal::<f64>::from(&[[1.0, 0.0, 0.0], [1.0, -1.0, 0.25]]);
}
#[test]
fn fixed_point_transfer() {
use dsp_fixedpoint::Q32;
let ba = [[1.0, 0.0, 0.0], [1.0, 0.0, 0.25]];
let normal = Normal::<Q32<30>>::from(&ba);
let direct = Biquad::<Q32<30>>::from(ba);
let mut ns = DirectForm1::default();
let mut ds = DirectForm1::default();
for i in 0..32 {
let x: i32 = if i == 0 { 1 << 20 } else { 0 };
let actual: i32 = normal.process(&mut ns, x);
let expected: i32 = direct.process(&mut ds, x);
assert!((actual - expected).abs() <= 1);
}
}
#[test]
fn fixed_point_step_with_headroom() {
use dsp_fixedpoint::Q32;
const AMPLITUDE: i32 = 1 << 24;
let ba = [[0.0002, 0.0, 0.0], [1.0, -1.98, 0.9802]];
let normal = Normal::<Q32<30>>::from(&ba);
let reference = Biquad::<f64>::from(ba);
let mut ns = DirectForm1::default();
let mut rs = DirectForm1::default();
for x in [AMPLITUDE, -AMPLITUDE, 0] {
for _ in 0..1024 {
let actual: i32 = normal.process(&mut ns, x);
let expected: f64 = reference.process(&mut rs, x as f64);
assert!((actual as f64 - expected).abs() < AMPLITUDE as f64 * 1e-5);
assert!(
ns.y[0]
.iter()
.all(|s| s.unsigned_abs() < 101 * AMPLITUDE as u32)
);
}
}
}
#[test]
fn transfer_matches_biquad() {
for ba in [
[[1.0, 0.0, 0.0], [1.0, 0.0, 0.25]],
[[0.2, -0.3, 0.4], [2.0, -1.2, 0.5]],
[[0.5, 0.2, -0.1], [1.0, 1.6, 0.81]],
] {
let normal = Normal::<f64>::from(&ba);
let direct = Biquad::<f64>::from(ba);
let mut ns = DirectForm1::default();
let mut ds = DirectForm1::default();
for i in 0..256 {
let x = if i == 0 { 1.0 } else { 0.0 };
let expected = direct.process(&mut ds, x);
let actual = normal.process(&mut ns, x);
assert!((actual - expected).abs() < 1e-12);
}
}
}
}