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> {}