Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 5 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Coefficient sharing for multiple channels is implemented through [`dsp_process::
### Comparison

This is a rough feature comparison of several available `biquad` crates, with no claim for completeness, accuracy, or even fairness.
TL;DR: `idsp` is slower but offers more features.
TL;DR: `idsp` is as fast and offers more features.

| Feature\Crate | [`biquad-rs`](https://crates.io/crates/biquad) | [`fixed-filters`](https://crates.io/crates/fixed-filters) | `idsp::iir` |
|---|---|---|---|
Expand All @@ -68,16 +68,8 @@ TL;DR: `idsp` is slower but offers more features.
| PI²D² builder limits | ❌ | ❌ | ✅ |
| Support for fixed point `a1=-2` second order integrator | ❌ | ❌ | ✅ |

Three crates have been compared when processing 4x1M samples (4 channels) with a biquad lowpass.
Hardware was `thumbv7em-none-eabihf`, `cortex-m7`, code in ITCM, data in DTCM, caches enabled.

| Crate | Type, features | Cycles per sample |
|---|---|---|
| [`biquad-rs`](https://crates.io/crates/biquad) | `f32` | 11.4 |
| `idsp::iir` | `f32`, limits, offset | 15.5 |
| [`fixed-filters`](https://crates.io/crates/fixed-filters) | `i32`, limits | 20.3 |
| `idsp::iir` | `i32`, limits, offset | 23.5 |
| `idsp::iir` | `i32`, limits, offset, noise shaping | 30.0 |
The benchmarks and results comparing `idsp` and `biquad-rs` are in `tests/embedded`.
`idsp`'s biquad can process one `i32` sample every 8.5 cycles and one `f32` sample every 12 cycles on a cortex-m7.

## State variable, normal form, wave digital filter

Expand All @@ -92,8 +84,8 @@ bandpass, and notch filtering of a signal.

## FIR filters

[`hbf::HbfDec`], [`hbf::HbfInt`], [`hbf::HbfDec32`], [`hbf::HbfInt32`] etc:
Fast `f32` symmetric FIR filters, optimized half-band filters, half-band filter decimators and integators and cascades.
Type 1-4 linear phase FIR filters, [`hbf::HbfDec`], [`hbf::HbfInt`], [`hbf::HbfDec32`], [`hbf::HbfInt32`] etc:
Fast `f32` symmetric FIR filters, also optimized half-band filters, half-band filter decimators and integators and cascades.
These are used in [`stabilizer-stream`](https://github.com/quartiq/stabilizer-stream) for online PSD calculation for
arbitrarily low offset frequencies.

Expand Down
6 changes: 1 addition & 5 deletions dsp-process/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ Impls can be cascaded in (homogeneous) `[C; N]` arrays/`[C]` slices, and heterog
`(C0, C1)` tuples. They can be used as configuration-major or
configuration-minor (through [`Minor`]) or in [`Add`]s on complementary allpasses and polyphase banks.
Tuples, arrays, and Pairs, and Minor can be mixed and nested ad lib.
For a given filter configuration `C` and state `S` pair the trait is usually implemented
through [`Split<&'a C, &mut S>`] (created ad-hoc from by borrowing configuration and state)
or [`Split<C, S>`] (owned configuration and state).
Stateless filters should implement `Process for &Self` for composability through
[`Split<Unsplit<&Self>, ()>`].
Stateless filters should implement `SplitProcess<X, Y, ()> for Self` for composability.
Configuration-less filters or filters that include their configuration should implement
`Process for Self` and can be used in split configurations through [`Split<(), Unsplit<Self>>`].
60 changes: 30 additions & 30 deletions dsp-process/src/basic.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{Inplace, Process};
use crate::{Inplace, Process, SplitInplace, SplitProcess};

//////////// ELEMENTARY PROCESSORS ////////////

Expand All @@ -7,51 +7,51 @@ use crate::{Inplace, Process};
/// Fan in.
#[derive(Debug, Copy, Clone, Default)]
pub struct Add;
impl<X: Copy, Y: core::iter::Sum<X>, const N: usize> Process<[X; N], Y> for &Add {
impl<X: Copy, Y: core::iter::Sum<X>, const N: usize> Process<[X; N], Y> for Add {
fn process(&mut self, x: [X; N]) -> Y {
x.into_iter().sum()
}
}
impl<X0: Copy + core::ops::Add<X1, Output = Y>, X1: Copy, Y> Process<(X0, X1), Y> for &Add {
impl<X0: Copy + core::ops::Add<X1, Output = Y>, X1: Copy, Y> Process<(X0, X1), Y> for Add {
fn process(&mut self, x: (X0, X1)) -> Y {
x.0 + x.1
}
}
impl<X: Copy> Inplace<X> for &Add where Self: Process<X> {}
impl<X: Copy> Inplace<X> for Add where Self: Process<X> {}

/// Product
///
/// Fan in.
#[derive(Debug, Copy, Clone, Default)]
pub struct Mul;
impl<X: Copy, Y: core::iter::Product<X>, const N: usize> Process<[X; N], Y> for &Mul {
impl<X: Copy, Y: core::iter::Product<X>, const N: usize> Process<[X; N], Y> for Mul {
fn process(&mut self, x: [X; N]) -> Y {
x.into_iter().product()
}
}
impl<X0: Copy + core::ops::Mul<X1, Output = Y>, X1: Copy, Y> Process<(X0, X1), Y> for &Mul {
impl<X0: Copy + core::ops::Mul<X1, Output = Y>, X1: Copy, Y> Process<(X0, X1), Y> for Mul {
fn process(&mut self, x: (X0, X1)) -> Y {
x.0 * x.1
}
}
impl<X: Copy> Inplace<X> for &Mul where Self: Process<X> {}
impl<X: Copy> Inplace<X> for Mul where Self: Process<X> {}

/// Difference
///
/// Fan in.
#[derive(Debug, Copy, Clone, Default)]
pub struct Sub;
impl<X: Copy + core::ops::Sub<Output = Y>, Y> Process<[X; 2], Y> for &Sub {
impl<X: Copy + core::ops::Sub<Output = Y>, Y> Process<[X; 2], Y> for Sub {
fn process(&mut self, x: [X; 2]) -> Y {
x[0] - x[1]
}
}
impl<X0: Copy + core::ops::Sub<X1, Output = Y>, X1: Copy, Y> Process<(X0, X1), Y> for &Sub {
impl<X0: Copy + core::ops::Sub<X1, Output = Y>, X1: Copy, Y> Process<(X0, X1), Y> for Sub {
fn process(&mut self, x: (X0, X1)) -> Y {
x.0 - x.1
}
}
impl<X: Copy> Inplace<X> for &Sub where Self: Process<X> {}
impl<X: Copy> Inplace<X> for Sub where Self: Process<X> {}

/// Sum and difference
#[derive(Debug, Copy, Clone, Default)]
Expand All @@ -64,12 +64,12 @@ impl<X: Copy + core::ops::Add<Output = Y> + core::ops::Sub<Output = Y>, Y> Proce
}
}

impl<X: Copy> Inplace<X> for &Butterfly where Self: Process<X> {}
impl<X: Copy> Inplace<X> for Butterfly where Self: Process<X> {}

/// Identity using [`Copy`]
#[derive(Debug, Copy, Clone, Default)]
pub struct Identity;
impl<T: Copy> Process<T> for &Identity {
impl<T: Copy> Process<T> for Identity {
fn process(&mut self, x: T) -> T {
x
}
Expand All @@ -80,26 +80,26 @@ impl<T: Copy> Process<T> for &Identity {
}

/// NOP
impl<T: Copy> Inplace<T> for &Identity {
impl<T: Copy> Inplace<T> for Identity {
fn inplace(&mut self, _xy: &mut [T]) {}
}

/// Fan out
impl<X: Copy> Process<X, (X, X)> for &Identity {
impl<X: Copy> Process<X, (X, X)> for Identity {
fn process(&mut self, x: X) -> (X, X) {
(x, x)
}
}

/// Fan out
impl<X: Copy, const N: usize> Process<X, [X; N]> for &Identity {
impl<X: Copy, const N: usize> Process<X, [X; N]> for Identity {
fn process(&mut self, x: X) -> [X; N] {
core::array::repeat(x)
}
}

/// Flatten
impl<X: Copy> Process<[X; 1], X> for &Identity {
impl<X: Copy> Process<[X; 1], X> for Identity {
fn process(&mut self, x: [X; 1]) -> X {
x[0]
}
Expand All @@ -108,41 +108,41 @@ impl<X: Copy> Process<[X; 1], X> for &Identity {
/// Inversion using `Neg`.
#[derive(Debug, Copy, Clone, Default)]
pub struct Neg;
impl<T: Copy + core::ops::Neg<Output = T>> Process<T> for &Neg {
impl<T: Copy + core::ops::Neg<Output = T>> Process<T> for Neg {
fn process(&mut self, x: T) -> T {
x.neg()
}
}

impl<T: Copy> Inplace<T> for &Neg where Self: Process<T> {}
impl<T: Copy> Inplace<T> for Neg where Self: Process<T> {}

/// Addition of a constant
#[derive(Debug, Clone, Copy, Default)]
#[repr(transparent)]
pub struct Offset<T>(pub T);

/// Offset using `Add`
impl<X: Copy + core::ops::Add<T, Output = Y>, Y, T: Copy> Process<X, Y> for &Offset<T> {
fn process(&mut self, x: X) -> Y {
impl<X: Copy + core::ops::Add<T, Output = Y>, Y, T: Copy> SplitProcess<X, Y> for Offset<T> {
fn process(&self, _state: &mut (), x: X) -> Y {
x + self.0
}
}

impl<X: Copy, T> Inplace<X> for &Offset<T> where Self: Process<X> {}
impl<X: Copy, T> SplitInplace<X> for Offset<T> where Self: SplitProcess<X> {}

/// Multiply by constant
#[derive(Debug, Clone, Copy, Default)]
#[repr(transparent)]
pub struct Gain<T>(pub T);

/// Gain using `Mul`
impl<X: Copy + core::ops::Mul<T, Output = Y>, Y, T: Copy> Process<X, Y> for &Gain<T> {
fn process(&mut self, x: X) -> Y {
impl<X: Copy + core::ops::Mul<T, Output = Y>, Y, T: Copy> SplitProcess<X, Y> for Gain<T> {
fn process(&self, _state: &mut (), x: X) -> Y {
x * self.0
}
}

impl<X: Copy, T> Inplace<X> for &Gain<T> where Self: Process<X> {}
impl<X: Copy, T> SplitInplace<X> for Gain<T> where Self: SplitProcess<X> {}

/// Clamp between min and max using `Ord`
#[derive(Debug, Copy, Clone, Default)]
Expand All @@ -153,31 +153,31 @@ pub struct Clamp<T> {
pub max: T,
}

impl<T: Copy + Ord> Process<T> for &Clamp<T> {
fn process(&mut self, x: T) -> T {
impl<T: Copy + Ord> SplitProcess<T> for Clamp<T> {
fn process(&self, _state: &mut (), x: T) -> T {
x.clamp(self.min, self.max)
}
}

impl<T: Copy> Inplace<T> for &Clamp<T> where Self: Process<T> {}
impl<T: Copy> SplitInplace<T> for Clamp<T> where Self: SplitProcess<T> {}

/// Decimate or zero stuff
#[derive(Debug, Copy, Clone, Default)]
pub struct Rate;
impl<X: Copy, const N: usize> Process<[X; N], X> for &Rate {
impl<X: Copy, const N: usize> Process<[X; N], X> for Rate {
fn process(&mut self, x: [X; N]) -> X {
x[N - 1]
}
}

impl<X: Copy + Default, const N: usize> Process<X, [X; N]> for &Rate {
impl<X: Copy + Default, const N: usize> Process<X, [X; N]> for Rate {
fn process(&mut self, x: X) -> [X; N] {
let mut y = [X::default(); N];
y[0] = x;
y
}
}
impl<X: Copy> Inplace<X> for &Rate where Self: Process<X> {}
impl<X: Copy> Inplace<X> for Rate where Self: Process<X> {}

/// Buffer input or output, or fixed delay line
#[derive(Debug, Copy, Clone, Default)]
Expand Down
37 changes: 15 additions & 22 deletions dsp-process/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@ pub use compose::*;
/// and may lead to suboptimal cashing and register thrashing for large branches.
/// To avoid this, use `block()` and `inplace()` on a scratch buffer ([`Major`] input or output).
///
/// The corresponding state for this is `(((), (S0, S1)), ())`.
pub type Pair<C0, C1, X, I = Unsplit<&'static Identity>, J = Unsplit<&'static Add>> =
Minor<((I, Parallel<(C0, C1)>), J), [X; 2]>;
/// The corresponding state for this is `((Unsplit<Identity>, (S0, S1)), Unsplit<Add>)`.
pub type Pair<C0, C1, X, I = (), J = ()> = Minor<((I, Parallel<(C0, C1)>), J), [X; 2]>;

#[cfg(test)]
mod test {
Expand All @@ -35,19 +34,19 @@ mod test {

#[test]
fn basic() {
assert_eq!(3, (&Identity).process(3));
assert_eq!((&Gain(Q32::<3>::new(32))).process(9), 9 * 4);
assert_eq!((&Offset(7)).process(9), 7 + 9);
assert_eq!(3, Identity.process(3));
assert_eq!(Split::stateless(Gain(Q32::<3>::new(32))).process(9), 9 * 4);
assert_eq!(Split::stateless(Offset(7)).process(9), 7 + 9);
}

#[test]
fn stateless() {
assert_eq!(Unsplit(&Neg).process(&mut (), 9), -9);
assert_eq!(Split::stateless(&Neg).as_mut().process(9), -9);
assert_eq!(Neg.process(9), -9);
assert_eq!(Split::stateful(Neg).process(9), -9);

let mut p = (Split::stateless(&Offset(7)) * Split::stateless(&Offset(1))).minor();
p.as_mut().assert_process::<i8, _>();
assert_eq!(p.as_mut().process(9), 7 + 1 + 9);
let mut p = (Split::stateless(Offset(7)) * Split::stateless(Offset(1))).minor();
p.assert_process::<i8, _>();
assert_eq!(p.process(9), 7 + 1 + 9);
}

#[test]
Expand All @@ -56,27 +55,21 @@ mod test {
let mut dly = Buffer::<[_; 2]>::default();
dly.inplace(&mut xy);
assert_eq!(xy, [0, 0, 3]);
let y: i32 = Split::stateful(dly).as_mut().process(4);
let y: i32 = Split::stateful(dly).process(4);
assert_eq!(y, 0);
}

#[test]
fn pair() {
let g = Gain(Q32::<1>::new(4));
let mut f = Split::new(
Pair::<_, _, _>::new((
(
Unsplit(&Identity),
Parallel((Unsplit(&Offset(3)), Unsplit(&g))),
),
Unsplit(&Add),
)),
Default::default(),
Pair::<_, _, _>::new((((), Parallel((Offset(3), g))), ())),
((Unsplit(Identity), Default::default()), Unsplit(Add)),
);
let y: i32 = f.as_mut().process(5);
let y: i32 = f.process(5);
assert_eq!(y, (5 + 3) + ((5 * 4) >> 1));

let y: [i32; 5] = f.channels().as_mut().process([5; _]);
let y: [i32; 5] = f.channels().process([5; _]);
assert_eq!(y, [(5 + 3) + ((5 * 4) >> 1); 5]);
}
}
22 changes: 22 additions & 0 deletions dsp-process/src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,25 @@ impl<X: Copy, S: ?Sized, T: SplitInplace<X, S>> SplitInplace<X, S> for &mut T {
T::inplace(self, state, xy)
}
}

/// Wrap a `FnMut` into a `Process`/`Inplace`
pub struct FnProcess<F>(pub F);

impl<F: FnMut(X) -> Y, X: Copy, Y> Process<X, Y> for FnProcess<F> {
fn process(&mut self, x: X) -> Y {
(self.0)(x)
}
}

impl<F, X: Copy> Inplace<X> for FnProcess<F> where Self: Process<X> {}

/// Wrap a `Fn` into a `SplitProcess`/`SplitInplace`
pub struct FnSplitProcess<F>(pub F);

impl<F: Fn(&mut S, X) -> Y, X: Copy, Y, S> SplitProcess<X, Y, S> for FnSplitProcess<F> {
fn process(&self, state: &mut S, x: X) -> Y {
(self.0)(state, x)
}
}

impl<F, X: Copy, S> SplitInplace<X, S> for FnSplitProcess<F> where Self: SplitProcess<X, X, S> {}
Loading