Skip to main content

embedded_dsp/
pipeline.rs

1//! Zero-allocation composable DSP pipeline and streaming abstraction.
2//!
3//! Provides the [`DspNode`] trait and zero-overhead pipeline combinators ([`Chain`], [`Gain`], [`Limiter`])
4//! for real-time sample-by-sample or block DMA stream processing.
5
6use crate::types::{q15, DspSample};
7
8/// A processing element in a real-time digital signal processing pipeline.
9pub trait DspNode<T: Copy> {
10    /// Process a single input sample and produce one output sample.
11    fn process_sample(&mut self, input: T) -> T;
12
13    /// Process a block of samples from `in_buf` into `out_buf`.
14    #[inline]
15    fn process_block(&mut self, in_buf: &[T], out_buf: &mut [T]) {
16        let len = in_buf.len().min(out_buf.len());
17        for i in 0..len {
18            out_buf[i] = self.process_sample(in_buf[i]);
19        }
20    }
21
22    /// Process a block of samples in place.
23    #[inline]
24    fn process_in_place(&mut self, buf: &mut [T]) {
25        for sample in buf.iter_mut() {
26            *sample = self.process_sample(*sample);
27        }
28    }
29
30    /// Chains this node with another processing node into a sequential pipeline.
31    #[inline]
32    fn then<Next>(self, next: Next) -> Chain<Self, Next>
33    where
34        Self: Sized,
35        Next: DspNode<T>,
36    {
37        Chain {
38            first: self,
39            second: next,
40        }
41    }
42}
43
44/// A sequential composition of two DSP nodes `A` and `B` with zero runtime overhead.
45#[derive(Debug, Clone, Copy, Default)]
46pub struct Chain<A, B> {
47    pub first: A,
48    pub second: B,
49}
50
51impl<T: Copy, A: DspNode<T>, B: DspNode<T>> DspNode<T> for Chain<A, B> {
52    #[inline(always)]
53    fn process_sample(&mut self, input: T) -> T {
54        let intermediate = self.first.process_sample(input);
55        self.second.process_sample(intermediate)
56    }
57}
58
59/// Linear gain scaling node.
60#[derive(Debug, Clone, Copy, Default)]
61pub struct Gain<T> {
62    pub gain: T,
63}
64
65impl<T> Gain<T> {
66    #[inline(always)]
67    pub const fn new(gain: T) -> Self {
68        Self { gain }
69    }
70}
71
72impl<T: DspSample> DspNode<T> for Gain<T> {
73    #[inline(always)]
74    fn process_sample(&mut self, input: T) -> T {
75        input.sat_mul(self.gain)
76    }
77}
78
79impl DspNode<i16> for Gain<i16> {
80    #[inline(always)]
81    fn process_sample(&mut self, input: i16) -> i16 {
82        crate::types::q15_mult(q15::from_bits(input), q15::from_bits(self.gain)).to_bits()
83    }
84}
85
86impl DspNode<i32> for Gain<i32> {
87    #[inline(always)]
88    fn process_sample(&mut self, input: i32) -> i32 {
89        let prod = (input as i64 * self.gain as i64) >> 31;
90        prod.clamp(i32::MIN as i64, i32::MAX as i64) as i32
91    }
92}
93
94/// Hard saturation limiter node clamping between `[min, max]`.
95#[derive(Debug, Clone, Copy, Default)]
96pub struct Limiter<T> {
97    pub min: T,
98    pub max: T,
99}
100
101impl<T> Limiter<T> {
102    #[inline(always)]
103    pub const fn new(min: T, max: T) -> Self {
104        Self { min, max }
105    }
106}
107
108impl<T: PartialOrd + Copy> DspNode<T> for Limiter<T> {
109    #[inline(always)]
110    fn process_sample(&mut self, input: T) -> T {
111        if input < self.min {
112            self.min
113        } else if input > self.max {
114            self.max
115        } else {
116            input
117        }
118    }
119}
120
121// ─────────────────────────────────────────────────────────────────────────────
122// Node Implementations for Built-in Filters and Controllers
123// ─────────────────────────────────────────────────────────────────────────────
124
125#[cfg(feature = "controller")]
126impl DspNode<f32> for crate::controller::PidInstanceF32 {
127    #[inline(always)]
128    fn process_sample(&mut self, input: f32) -> f32 {
129        self.process(input)
130    }
131}
132
133#[cfg(feature = "controller")]
134impl DspNode<q15> for crate::controller::PidInstanceQ15 {
135    #[inline(always)]
136    fn process_sample(&mut self, input: q15) -> q15 {
137        self.process(input)
138    }
139}
140
141#[cfg(feature = "filtering")]
142impl DspNode<f32> for crate::filtering::SinglePoleFilter {
143    #[inline(always)]
144    fn process_sample(&mut self, input: f32) -> f32 {
145        self.process(input)
146    }
147}
148
149#[cfg(feature = "filtering")]
150impl DspNode<q15> for crate::filtering::SinglePoleFilterQ15 {
151    #[inline(always)]
152    fn process_sample(&mut self, input: q15) -> q15 {
153        self.process(input)
154    }
155}
156
157#[cfg(feature = "filtering")]
158impl DspNode<q15> for crate::filtering::DcBlockerQ15 {
159    #[inline(always)]
160    fn process_sample(&mut self, input: q15) -> q15 {
161        self.process(input)
162    }
163}