Skip to main content

dsp_process/
process.rs

1//! Core traits for synchronous sample and block processing.
2
3/// Processing block
4///
5/// Single-input processing with state held in `self`.
6///
7/// This is the simplest trait in the crate: one new sample goes in, one output
8/// value comes out. Override [`block()`](Self::block) when a specialized loop can
9/// reuse scratch storage, reduce bounds checks, or better match the desired data
10/// layout.
11///
12/// [`SplitProcess`] is the corresponding trait when immutable configuration and
13/// mutable runtime state should be separated.
14///
15/// # Examples
16///
17/// ```rust
18/// use dsp_process::Process;
19///
20/// #[derive(Default)]
21/// struct Acc(i32);
22///
23/// impl Process<i32> for Acc {
24///     fn process(&mut self, x: i32) -> i32 {
25///         self.0 += x;
26///         self.0
27///     }
28/// }
29///
30/// let mut acc = Acc::default();
31/// assert_eq!(acc.process(2), 2);
32/// assert_eq!(acc.process(3), 5);
33/// ```
34pub trait Process<X: Copy, Y = X> {
35    /// Update the state with a new input and obtain an output
36    fn process(&mut self, x: X) -> Y;
37
38    /// Process a block of inputs into a block of outputs
39    ///
40    /// Input and output must be of the same size.
41    ///
42    /// For hot-path use this is treated as a caller precondition; the default
43    /// implementation only checks it in debug builds.
44    fn block(&mut self, x: &[X], y: &mut [Y]) {
45        debug_assert_eq!(x.len(), y.len());
46        for (x, y) in x.iter().zip(y) {
47            *y = self.process(*x);
48        }
49    }
50}
51
52/// Inplace processing
53///
54/// This is a convenience trait for processors where input and output element
55/// types are identical and the computation can be expressed as overwriting a
56/// mutable slice.
57///
58/// See also [`SplitInplace`] for the split configuration/state form.
59pub trait Inplace<X: Copy>: Process<X> {
60    /// Process an input block into the same data as output
61    fn inplace(&mut self, xy: &mut [X]) {
62        for xy in xy.iter_mut() {
63            *xy = self.process(*xy);
64        }
65    }
66}
67
68/// Processing with split state
69///
70/// Splitting configuration (the part of the filter that is unaffected
71/// by processing inputs, e.g. "coefficients"), from state (the part
72/// that is modified by processing) allows:
73///
74/// * Separating mutable from immutable state guarantees consistency
75///   (configuration can not change state and processing can
76///   not change configuration)
77/// * Reduces memory traffic when swapping configuration
78/// * Allows the same filter to be applied to multiple states
79///   (e.g. IQ data, multiple lanes) guaranteeing consistency,
80///   reducing memory usage, and improving caching.
81///
82/// This is the central abstraction used throughout `dsp-process`. A typical DSP
83/// filter coefficient set becomes `Self`, while delay lines, accumulators, and
84/// history buffers become the separate `state` argument.
85///
86/// Use this when one configuration should drive many runtime states, or when it
87/// is beneficial to keep mutable state small and move immutable data out of hot
88/// loops.
89///
90/// [`Process`] is often easier when state and configuration naturally live
91/// together, while [`crate::Split`] turns a `SplitProcess` back into a stateful
92/// [`Process`] value.
93///
94/// # Examples
95///
96/// ```rust
97/// use dsp_process::SplitProcess;
98///
99/// #[derive(Copy, Clone)]
100/// struct Gain(i32);
101///
102/// impl SplitProcess<i32> for Gain {
103///     fn process(&self, _: &mut (), x: i32) -> i32 {
104///         self.0 * x
105///     }
106/// }
107///
108/// let mut state = ();
109/// assert_eq!(Gain(4).process(&mut state, 3), 12);
110/// ```
111pub trait SplitProcess<X: Copy, Y = X, S: ?Sized = ()> {
112    /// Process an input into an output
113    ///
114    /// See also [`Process::process`]
115    fn process(&self, state: &mut S, x: X) -> Y;
116
117    /// Process a block of inputs
118    ///
119    /// See also [`Process::block`]
120    ///
121    /// Length matching is a caller precondition in release builds.
122    fn block(&self, state: &mut S, x: &[X], y: &mut [Y]) {
123        debug_assert_eq!(x.len(), y.len());
124        for (x, y) in x.iter().zip(y) {
125            *y = self.process(state, *x);
126        }
127    }
128}
129
130/// Inplace processing with a split state
131///
132/// This is the split-state companion to [`Inplace`]. Implement it when a
133/// `SplitProcess<X, X, S>` can update a buffer in place more efficiently than
134/// routing through a separate output slice.
135pub trait SplitInplace<X: Copy, S: ?Sized = ()>: SplitProcess<X, X, S> {
136    /// See also [`Inplace::inplace`]
137    fn inplace(&self, state: &mut S, xy: &mut [X]) {
138        for xy in xy.iter_mut() {
139            *xy = self.process(state, *xy);
140        }
141    }
142}
143
144//////////// BLANKET ////////////
145
146impl<X: Copy, Y, T: Process<X, Y>> Process<X, Y> for &mut T {
147    fn process(&mut self, x: X) -> Y {
148        T::process(self, x)
149    }
150
151    fn block(&mut self, x: &[X], y: &mut [Y]) {
152        T::block(self, x, y)
153    }
154}
155
156impl<X: Copy, T: Inplace<X>> Inplace<X> for &mut T {
157    fn inplace(&mut self, xy: &mut [X]) {
158        T::inplace(self, xy)
159    }
160}
161
162impl<X: Copy, Y, S: ?Sized, T: SplitProcess<X, Y, S>> SplitProcess<X, Y, S> for &T {
163    fn process(&self, state: &mut S, x: X) -> Y {
164        T::process(self, state, x)
165    }
166
167    fn block(&self, state: &mut S, x: &[X], y: &mut [Y]) {
168        T::block(self, state, x, y)
169    }
170}
171
172impl<X: Copy, S: ?Sized, T: SplitInplace<X, S>> SplitInplace<X, S> for &T {
173    fn inplace(&self, state: &mut S, xy: &mut [X]) {
174        T::inplace(self, state, xy)
175    }
176}
177
178impl<X: Copy, Y, S: ?Sized, T: SplitProcess<X, Y, S>> SplitProcess<X, Y, S> for &mut T {
179    fn process(&self, state: &mut S, x: X) -> Y {
180        T::process(self, state, x)
181    }
182
183    fn block(&self, state: &mut S, x: &[X], y: &mut [Y]) {
184        T::block(self, state, x, y)
185    }
186}
187
188impl<X: Copy, S: ?Sized, T: SplitInplace<X, S>> SplitInplace<X, S> for &mut T {
189    fn inplace(&self, state: &mut S, xy: &mut [X]) {
190        T::inplace(self, state, xy)
191    }
192}
193
194/// Wrap a `FnMut` into a `Process`/`Inplace`
195///
196/// This is useful for quick experiments, benchmarks, or adapters at the edge of
197/// a pipeline. For reusable DSP stages, prefer a named type once the closure
198/// starts carrying real semantics.
199///
200/// # Examples
201///
202/// ```rust
203/// use dsp_process::{FnProcess, Process};
204///
205/// let mut square = FnProcess(|x: i32| x * x);
206/// assert_eq!(square.process(7), 49);
207/// ```
208pub struct FnProcess<F>(pub F);
209
210impl<F: FnMut(X) -> Y, X: Copy, Y> Process<X, Y> for FnProcess<F> {
211    fn process(&mut self, x: X) -> Y {
212        (self.0)(x)
213    }
214}
215
216impl<F, X: Copy> Inplace<X> for FnProcess<F> where Self: Process<X> {}
217
218/// Wrap a `Fn` into a `SplitProcess`/`SplitInplace`
219///
220/// The closure receives both the mutable split state and the new input sample.
221/// This is a compact way to prototype split-state processors before promoting
222/// them to named types.
223///
224/// # Examples
225///
226/// ```rust
227/// use dsp_process::{FnSplitProcess, SplitProcess};
228///
229/// let proc = FnSplitProcess(|state: &mut i32, x: i32| {
230///     *state += x;
231///     *state
232/// });
233///
234/// let mut state = 0;
235/// assert_eq!(proc.process(&mut state, 2), 2);
236/// assert_eq!(proc.process(&mut state, 3), 5);
237/// ```
238pub struct FnSplitProcess<F>(pub F);
239
240impl<F: Fn(&mut S, X) -> Y, X: Copy, Y, S> SplitProcess<X, Y, S> for FnSplitProcess<F> {
241    fn process(&self, state: &mut S, x: X) -> Y {
242        (self.0)(state, x)
243    }
244}
245
246impl<F, X: Copy, S> SplitInplace<X, S> for FnSplitProcess<F> where Self: SplitProcess<X, X, S> {}