flaw/
lib.rs

1#![doc = include_str!("../README.md")]
2#![cfg_attr(not(feature = "std"), no_std)]
3pub mod fir;
4pub mod fractional_delay;
5pub mod iir;
6pub mod median;
7pub mod sos;
8
9use core::ops::{Index, IndexMut};
10use crunchy::unroll;
11pub use fir::SisoFirFilter;
12pub use fractional_delay::polynomial_fractional_delay;
13pub use iir::SisoIirFilter;
14pub use median::MedianFilter;
15pub use sos::SisoSosFilter;
16
17pub mod generated;
18pub use generated::butter::butter1::butter1;
19pub use generated::butter::butter2::butter2;
20pub use generated::butter::butter3::butter3;
21pub use generated::butter::butter4::butter4;
22pub use generated::butter::butter5::butter5;
23pub use generated::butter::butter6::butter6;
24
25use num_traits::{MulAdd, Num};
26
27/// A simple array with large memory alignment because it will be accessed
28/// often in a loop, with methods specialized for filter evaluation.
29#[derive(Clone, Copy, Debug)]
30#[repr(align(8))]
31pub struct AlignedArray<T, const N: usize>([T; N]);
32
33impl<T: Copy + Num, const N: usize> Default for AlignedArray<T, N> {
34    fn default() -> Self {
35        Self([T::zero(); N])
36    }
37}
38
39impl<T: Copy + Num + MulAdd<Output = T>, const N: usize> AlignedArray<T, N> {
40    /// Multiply-and-sum between this array and a target ring buffer.
41    ///
42    /// A starting value can be provided for the summation,
43    /// which can be helpful for fine-tuning floating-point error.
44    #[inline]
45    pub fn dot(&self, buf: &Ring<T, N>, start: T) -> T {
46        const { assert!(N < 129, "N > 128 not supported") }
47
48        // Multiply-and-sum loops could be turned into chained mul-add,
49        // but microcontrollers mostly don't have mul-add instructions
50        // as of the year 2025, so using mul_add here would cause severe
51        // performance regression.
52        let other = buf.buf();
53        let mut acc = start;
54
55        #[cfg(not(feature = "fma"))]
56        unroll! {
57            for i < 128 in 0..N {
58                acc = acc + self.0[i] * other[i];
59            }
60        }
61
62        #[cfg(feature = "fma")]
63        unroll! {
64            for i < 128 in 0..N {
65                acc = self.0[i].mul_add(other[i], acc);
66            }
67        }
68
69        acc
70    }
71}
72
73impl<T, const N: usize> Index<usize> for AlignedArray<T, N> {
74    type Output = T;
75    #[inline]
76    fn index(&self, idx: usize) -> &Self::Output {
77        &self.0[idx]
78    }
79}
80
81impl<T, const N: usize> IndexMut<usize> for AlignedArray<T, N> {
82    #[inline]
83    fn index_mut(&mut self, idx: usize) -> &mut Self::Output {
84        &mut self.0[idx]
85    }
86}
87
88/// Ring buffer.
89/// Most recent sample is stored last.
90#[derive(Clone, Copy, Default, Debug)]
91#[repr(transparent)]
92pub struct Ring<T: Num + Copy, const N: usize> {
93    buf: AlignedArray<T, N>,
94}
95
96impl<T: Num + Copy, const N: usize> Ring<T, N> {
97    /// Initialize with buffer populated with constant value
98    pub fn new(value: T) -> Self {
99        Self {
100            buf: AlignedArray([value; N]),
101        }
102    }
103
104    /// Replace the oldest value in the buffer with a new value.
105    /// Despite the unnecessary copies compared to a true ring buffer, this is
106    /// faster overall in filtering applications where one dot product is performed
107    /// per push, and the cost of splitting the dot product into two segments is large
108    /// compared to the savings from avoiding copies.
109    pub fn push(&mut self, value: T) {
110        unroll! {
111            for i < 128 in 0..(N-1) {
112                self.buf.0[i] = self.buf.0[i + 1];
113            }
114        }
115        self.buf.0[N - 1] = value;
116    }
117
118    /// The whole internal buffer, with no indication of the current index
119    fn buf(&self) -> &[T; N] {
120        &self.buf.0
121    }
122}
123
124/// `std` is required for tests, but is not a default feature.
125/// To allow the library to compile with default features,
126/// tests that require `std` are feature-gated.
127/// This test makes sure we do not skip the real tests.
128#[cfg(test)]
129#[cfg(not(feature = "std"))]
130mod test {
131    #[test]
132    fn require_std_for_tests() {
133        panic!("`std` feature is required for tests")
134    }
135}
136
137#[cfg(feature = "std")]
138#[cfg(test)]
139mod test {
140    /// Test initialization to a given input value
141    #[test]
142    fn test_initialize() {
143        let e = core::f64::consts::E as f32;
144        let mut f = super::butter2(0.2).unwrap();
145        f.set_steady_state(e);
146        assert!((e - f.update(e)).abs() / e < 1e-6);
147    }
148}