Skip to main content

dsp_process/
adapters.rs

1use crate::{SplitInplace, SplitProcess};
2
3/// Adapt a scalar optional-input stage to chunk output mode.
4///
5/// The inner processor is called with `Some(x)` once and then `None` `N-1` times
6/// to synthesize one output chunk from one input sample.
7///
8/// This is convenient for polyphase interpolators and other stages whose natural
9/// scalar interface is `Option<X> -> Y`. Unlike [`crate::ChunkOut`], this
10/// preserves stream phase and still runs the recursive inner stage once per
11/// output sample.
12///
13/// See also [`Decimator`] for the inverse direction.
14///
15/// # Examples
16///
17/// ```rust
18/// use dsp_process::{FnSplitProcess, Interpolator, SplitProcess};
19///
20/// let proc = Interpolator(FnSplitProcess(|_: &mut (), x: Option<i32>| {
21///     x.unwrap_or_default()
22/// }));
23/// let mut state = ();
24/// assert_eq!(proc.process(&mut state, 7), [7, 0, 0]);
25/// ```
26#[derive(Clone, Debug, Default)]
27pub struct Interpolator<P>(pub P);
28impl<X: Copy, Y, C: SplitProcess<Option<X>, Y, S>, S, const N: usize> SplitProcess<X, [Y; N], S>
29    for Interpolator<C>
30{
31    fn process(&self, state: &mut S, x: X) -> [Y; N] {
32        core::array::from_fn(|i| self.0.process(state, (i == 0).then_some(x)))
33    }
34}
35impl<X: Copy, C, S> SplitInplace<X, S> for Interpolator<C> where Self: SplitProcess<X, X, S> {}
36
37/// Scalar downsampler with explicit tick phase.
38///
39/// The first input sample produces `Some(x)`, then `rate` input samples produce
40/// `None`, and the pattern repeats. This matches the phase convention used by
41/// [`crate::Decimator`] when wrapping a scalar `X -> Option<Y>` processor into a
42/// chunked one.
43///
44/// Use this when the stream is still scalar and phase must be tracked across
45/// time. It does not by itself turn a chunk `[X; N]` into one output `Y`; pair
46/// it with [`Decimator`] or [`TryDecimator`] for that.
47///
48/// Together with [`crate::Hold`], this forms the scalar optional-sample pair:
49/// `Downsample` removes samples by emitting `None`, while `Hold` fills those
50/// gaps again by repeating the last present sample.
51///
52/// Compare with:
53/// - [`crate::Rate`]: stateless chunk-slot conversion
54/// - [`Decimator`]: chunk adapter over a scalar `X -> Option<Y>` stage
55///
56/// State is the current countdown and should usually be initialized to `0`.
57///
58/// # Examples
59///
60/// ```rust
61/// use dsp_process::{Downsample, SplitProcess};
62///
63/// let ds = Downsample(2);
64/// let mut state = 0;
65/// assert_eq!(ds.process(&mut state, 10), Some(10));
66/// assert_eq!(ds.process(&mut state, 11), None);
67/// assert_eq!(ds.process(&mut state, 12), None);
68/// assert_eq!(ds.process(&mut state, 13), Some(13));
69/// ```
70#[derive(Clone, Copy, Debug, Default)]
71pub struct Downsample(pub u32);
72
73impl<X: Copy> SplitProcess<X, Option<X>, u32> for Downsample {
74    fn process(&self, state: &mut u32, x: X) -> Option<X> {
75        if let Some(index) = state.checked_sub(1) {
76            *state = index;
77            None
78        } else {
79            *state = self.0;
80            Some(x)
81        }
82    }
83}
84
85/// Zero-order hold over optional input samples.
86///
87/// `Some(x)` updates the held value, while `None` repeats the previous one.
88/// This is useful for interpolation pipelines and event-driven sample streams.
89///
90/// Together with [`Downsample`], this forms the scalar optional-sample pair:
91/// `Downsample` creates gaps by emitting `None`, while `Hold` turns those
92/// gaps back into a continuous stream by repeating the last present sample.
93///
94/// At the chunk level, [`Interpolator`] plays the analogous role for turning a
95/// scalar stream into chunk output.
96///
97/// # Examples
98///
99/// ```rust
100/// use dsp_process::{Hold, Process};
101///
102/// let mut hold = Hold(5);
103/// assert_eq!(hold.process(None), 5);
104/// assert_eq!(hold.process(Some(7)), 7);
105/// assert_eq!(hold.process(None), 7);
106/// ```
107#[derive(Debug, Copy, Clone, Default)]
108#[repr(transparent)]
109pub struct Hold<T>(pub T);
110
111impl<T: Copy> crate::Process<Option<T>, T> for Hold<T> {
112    fn process(&mut self, x: Option<T>) -> T {
113        if let Some(x) = x {
114            self.0 = x;
115        }
116        self.0
117    }
118}
119
120/// Adapt a scalar optional-output stage to chunk input mode.
121///
122/// Synchronizes to the inner tick by discarding samples after tick.
123/// Panics if tick does not match `N`.
124///
125/// This is the chunked counterpart to [`Interpolator`].
126///
127/// The inner processor must tick exactly once per input chunk. `Decimator`
128/// processes the whole chunk and panics if the contract is violated. Use
129/// [`TryDecimator`] when violating that contract should be reported instead of
130/// panicking.
131///
132/// Unlike [`crate::Rate`], this adapter still runs the
133/// inner processor on every sample in the chunk before choosing the output.
134/// That is the right semantics for recursive stages such as CIC decimators.
135///
136/// Conceptually, this is the chunk-level companion to [`crate::Downsample`]:
137/// `Downsample` gates a scalar stream into `Option<Y>`, while `Decimator`
138/// turns that exact-one-tick-per-chunk protocol into `[X; N] -> Y`.
139/// Unlike [`crate::ChunkIn`], this still executes the inner stage on every
140/// sample in the chunk and is therefore the right adapter for recursive
141/// decimators.
142///
143/// # Examples
144///
145/// ```rust
146/// use dsp_process::{Decimator, FnSplitProcess, SplitProcess};
147///
148/// let proc = Decimator(FnSplitProcess(|state: &mut bool, x: i32| {
149///     let y = if *state { Some(x) } else { None };
150///     *state = !*state;
151///     y
152/// }));
153///
154/// let mut tick = false;
155/// assert_eq!(proc.process(&mut tick, [1, 2]), 2);
156/// ```
157#[derive(Clone, Debug, Default)]
158pub struct Decimator<P>(pub P);
159impl<X: Copy, Y, C: SplitProcess<X, Option<Y>, S>, S, const N: usize> SplitProcess<[X; N], Y, S>
160    for Decimator<C>
161{
162    fn process(&self, state: &mut S, x: [X; N]) -> Y {
163        const { assert!(N > 0) }
164        TryDecimator(&self.0).process(state, x).unwrap()
165    }
166}
167impl<X: Copy, C, S> SplitInplace<X, S> for Decimator<C> where Self: SplitProcess<X, X, S> {}
168
169/// Error returned by [`TryDecimator`] when the inner decimator does not tick
170/// exactly once per input chunk.
171#[derive(Clone, Copy, Debug, Eq, PartialEq)]
172pub enum DecimatorError {
173    /// No output sample was produced for the chunk.
174    NoTick,
175    /// More than one output sample was produced for the chunk.
176    ExtraTick,
177}
178
179/// Checked variant of [`Decimator`].
180///
181/// This preserves the same chunked interface but reports contract violations
182/// instead of panicking.
183///
184/// # Examples
185///
186/// ```rust
187/// use dsp_process::{DecimatorError, FnSplitProcess, SplitProcess, TryDecimator};
188///
189/// let proc = TryDecimator(FnSplitProcess(|state: &mut bool, x: i32| {
190///     let y = if *state { Some(x) } else { None };
191///     *state = !*state;
192///     y
193/// }));
194///
195/// let mut tick = false;
196/// assert_eq!(proc.process(&mut tick, [1, 2]), Ok(2));
197///
198/// let never = TryDecimator(FnSplitProcess(|_: &mut (), _: i32| None::<i32>));
199/// let mut state = ();
200/// assert_eq!(
201///     never.process(&mut state, [1, 2]),
202///     Err(DecimatorError::NoTick)
203/// );
204/// ```
205#[derive(Clone, Debug, Default)]
206pub struct TryDecimator<P>(pub P);
207impl<X: Copy, Y, C: SplitProcess<X, Option<Y>, S>, S, const N: usize>
208    SplitProcess<[X; N], Result<Y, DecimatorError>, S> for TryDecimator<C>
209{
210    fn process(&self, state: &mut S, x: [X; N]) -> Result<Y, DecimatorError> {
211        const { assert!(N > 0) }
212        let mut y = None;
213        for x in x {
214            if let Some(next) = self.0.process(state, x)
215                && y.replace(next).is_some()
216            {
217                return Err(DecimatorError::ExtraTick);
218            }
219        }
220        y.ok_or(DecimatorError::NoTick)
221    }
222}
223
224/// Lift a processor through `Option` or `Result`.
225///
226/// This is useful when a processor should only run on present/valid samples
227/// while preserving outer framing or error signaling. It changes control-flow
228/// shape, not block layout.
229///
230/// # Examples
231///
232/// ```rust
233/// use dsp_process::{Map, Offset, SplitProcess};
234///
235/// let proc = Map(Offset(3));
236/// let mut state = ();
237/// assert_eq!(proc.process(&mut state, Some(4)), Some(7));
238/// assert_eq!(proc.process(&mut state, None::<i32>), None);
239/// ```
240#[derive(Clone, Debug, Default)]
241pub struct Map<P>(pub P);
242impl<X: Copy, Y, C: SplitProcess<X, Y, S>, S> SplitProcess<Option<X>, Option<Y>, S> for Map<C> {
243    fn process(&self, state: &mut S, x: Option<X>) -> Option<Y> {
244        x.map(|x| self.0.process(state, x))
245    }
246}
247impl<X: Copy, Y, C: SplitProcess<X, Y, S>, S, E: Copy> SplitProcess<Result<X, E>, Result<Y, E>, S>
248    for Map<C>
249{
250    fn process(&self, state: &mut S, x: Result<X, E>) -> Result<Y, E> {
251        x.map(|x| self.0.process(state, x))
252    }
253}
254impl<X: Copy, C: SplitInplace<X, S>, S> SplitInplace<X, S> for Map<C> where
255    Self: SplitProcess<X, X, S>
256{
257}
258
259/// Elementwise fixed-size chunk lifting.
260///
261/// Adapt a `X -> Y` processor into a `[X; N] -> [Y; N]` processor
262/// by flattening input and output.
263///
264/// This is the simplest array-lifting adapter and is often the right choice
265/// when a scalar stage should run elementwise over fixed-size chunks with no
266/// rate change and no frame semantics beyond flattening.
267///
268/// Prefer the more specific adapters when the inner stage consumes or produces
269/// grouped samples (`ChunkIn`, `ChunkOut`, `ChunkInOut`) or when stream phase is
270/// part of the semantics (`Interpolator`, `Decimator`).
271///
272/// # Examples
273///
274/// ```rust
275/// use dsp_process::{Chunk, Offset, Process, Split};
276///
277/// let mut p = Split::stateless(Chunk(Offset(3)));
278/// assert_eq!(p.process([1, 2, 3]), [4, 5, 6]);
279/// ```
280#[derive(Debug, Copy, Clone, Default)]
281pub struct Chunk<P>(pub P);
282impl<C: SplitProcess<X, Y, S>, S, X: Copy, Y, const N: usize> SplitProcess<[X; N], [Y; N], S>
283    for Chunk<C>
284{
285    fn process(&self, state: &mut S, x: [X; N]) -> [Y; N] {
286        x.map(|x| self.0.process(state, x))
287    }
288
289    fn block(&self, state: &mut S, x: &[[X; N]], y: &mut [[Y; N]]) {
290        self.0.block(state, x.as_flattened(), y.as_flattened_mut())
291    }
292}
293impl<C: SplitInplace<X, S>, S, X: Copy, const N: usize> SplitInplace<[X; N], S> for Chunk<C> {
294    fn inplace(&self, state: &mut S, xy: &mut [[X; N]]) {
295        self.0.inplace(state, xy.as_flattened_mut())
296    }
297}
298
299/// Fixed-ratio chunk adapter for grouped input.
300///
301/// Adapt a `[X; R] -> Y` processor to `[X; N=R*M]->[Y; M]` for any `M`
302/// by flattening and re-chunking input.
303///
304/// Use this when the inner stage consumes several input samples per output, such
305/// as a small decimating FIR kernel. This is a structural regrouping adapter:
306/// it does not track stream phase across calls.
307///
308/// See also [`ChunkOut`] and [`ChunkInOut`].
309///
310/// # Examples
311///
312/// ```rust
313/// use dsp_process::{ChunkIn, FnSplitProcess, Process, Split};
314///
315/// let mut p = Split::stateless(ChunkIn::<_, 2>(FnSplitProcess(
316///     |_: &mut (), [a, b]: [i32; 2]| a + b,
317/// )));
318/// assert_eq!(p.process([1, 2, 3, 4]), [3, 7]);
319/// ```
320#[derive(Debug, Copy, Clone, Default)]
321pub struct ChunkIn<P, const R: usize>(pub P);
322impl<C: SplitProcess<[X; R], Y, S>, S, X: Copy, Y, const N: usize, const R: usize, const M: usize>
323    SplitProcess<[X; N], [Y; M], S> for ChunkIn<C, R>
324{
325    fn process(&self, state: &mut S, x: [X; N]) -> [Y; M] {
326        const { assert!(R * M == N) }
327        let (x, []) = x.as_chunks() else {
328            unreachable!()
329        };
330        core::array::from_fn(|i| self.0.process(state, x[i]))
331    }
332
333    fn block(&self, state: &mut S, x: &[[X; N]], y: &mut [[Y; M]]) {
334        const { assert!(R * M == N) }
335        let (x, []) = x.as_flattened().as_chunks() else {
336            unreachable!()
337        };
338        self.0.block(state, x, y.as_flattened_mut())
339    }
340}
341impl<C: SplitInplace<[X; 1], S>, S, X: Copy, const N: usize> SplitInplace<[X; N], S>
342    for ChunkIn<C, 1>
343where
344    Self: SplitProcess<[X; N], [X; N], S>,
345{
346    fn inplace(&self, state: &mut S, xy: &mut [[X; N]]) {
347        let (xy, []) = xy.as_flattened_mut().as_chunks_mut() else {
348            unreachable!()
349        };
350        self.0.inplace(state, xy)
351    }
352}
353
354/// Fixed-ratio chunk adapter for grouped output.
355///
356/// Adapt a `X -> [Y; R]` processor to `[X; N]->[Y; M = R*N]` for any `N`
357/// by flattening and re-chunking output.
358///
359/// This is the natural adapter for small fixed-ratio interpolation kernels.
360/// Use [`ChunkOutPod`] when the output type is POD and the flattening cost
361/// matters. This is a structural regrouping adapter, not a phased stream
362/// interpolator; use [`Interpolator`] when the inner stage is naturally
363/// `Option<X> -> Y`.
364///
365/// # Examples
366///
367/// ```rust
368/// use dsp_process::{ChunkOut, FnSplitProcess, Process, Split};
369///
370/// let mut p = Split::stateless(ChunkOut::<_, 2>(FnSplitProcess(|_: &mut (), x: i32| {
371///     [x, -x]
372/// })));
373/// assert_eq!(p.process([2, 3]), [2, -2, 3, -3]);
374/// ```
375#[derive(Debug, Copy, Clone, Default)]
376pub struct ChunkOut<P, const R: usize>(pub P);
377impl<C, S, X: Copy, Y: Default + Copy, const N: usize, const R: usize, const M: usize>
378    SplitProcess<[X; N], [Y; M], S> for ChunkOut<C, R>
379where
380    C: SplitProcess<X, [Y; R], S>,
381{
382    fn process(&self, state: &mut S, x: [X; N]) -> [Y; M] {
383        const { assert!(R * N == M) }
384        // `poor-codegen-from-fn-iter-next`: if this changes, use a real conversion primitive.
385        let mut y = [Y::default(); M];
386        let (yy, []) = y.as_chunks_mut() else {
387            unreachable!()
388        };
389        for (x, y) in x.into_iter().zip(yy) {
390            *y = self.0.process(state, x);
391        }
392        y
393    }
394
395    fn block(&self, state: &mut S, x: &[[X; N]], y: &mut [[Y; M]]) {
396        const { assert!(R * N == M) }
397        let (y, []) = y.as_flattened_mut().as_chunks_mut() else {
398            unreachable!()
399        };
400        self.0.block(state, x.as_flattened(), y)
401    }
402}
403impl<C: SplitInplace<[X; 1], S>, S, X: Copy, const N: usize> SplitInplace<[X; N], S>
404    for ChunkOut<C, 1>
405where
406    Self: SplitProcess<[X; N], [X; N], S>,
407{
408    fn inplace(&self, state: &mut S, xy: &mut [[X; N]]) {
409        let (xy, []) = xy.as_flattened_mut().as_chunks_mut() else {
410            unreachable!()
411        };
412        self.0.inplace(state, xy)
413    }
414}
415
416/// POD-specialized [`ChunkOut`] variant.
417///
418/// This keeps the same semantics as [`ChunkOut`] but uses a bytemuck-backed
419/// representation cast to flatten `[[Y; R]; N]` into `[Y; R * N]` without the
420/// generic scratch-buffer path.
421///
422/// This is only available when `Y` is `bytemuck::Pod` and is mainly a codegen/cache
423/// choice, not a semantic one.
424///
425/// # Examples
426#[cfg_attr(
427    feature = "bytemuck",
428    doc = r##"/// ```rust
429/// use dsp_process::{ChunkOutPod, FnSplitProcess, Process, Split};
430///
431/// let mut p = Split::stateless(ChunkOutPod::<_, 2>(FnSplitProcess(|_: &mut (), x: i32| {
432///     [x, -x]
433/// })));
434/// assert_eq!(p.process([2, 3]), [2, -2, 3, -3]);
435/// ```"##
436)]
437#[derive(Debug, Copy, Clone, Default)]
438pub struct ChunkOutPod<P, const R: usize>(pub P);
439#[cfg(feature = "bytemuck")]
440impl<C, S, X: Copy, Y: bytemuck::Pod, const N: usize, const R: usize, const M: usize>
441    SplitProcess<[X; N], [Y; M], S> for ChunkOutPod<C, R>
442where
443    C: SplitProcess<X, [Y; R], S>,
444{
445    fn process(&self, state: &mut S, x: [X; N]) -> [Y; M] {
446        const { assert!(R * N == M) }
447        bytemuck::cast::<[[Y; R]; N], [Y; M]>(x.map(|x| self.0.process(state, x)))
448    }
449
450    fn block(&self, state: &mut S, x: &[[X; N]], y: &mut [[Y; M]]) {
451        const { assert!(R * N == M) }
452        let (y, []) = y.as_flattened_mut().as_chunks_mut() else {
453            unreachable!()
454        };
455        self.0.block(state, x.as_flattened(), y)
456    }
457}
458#[cfg(feature = "bytemuck")]
459impl<C: SplitInplace<[X; 1], S>, S, X: Copy, const N: usize> SplitInplace<[X; N], S>
460    for ChunkOutPod<C, 1>
461where
462    Self: SplitProcess<[X; N], [X; N], S>,
463{
464    fn inplace(&self, state: &mut S, xy: &mut [[X; N]]) {
465        let (xy, []) = xy.as_flattened_mut().as_chunks_mut() else {
466            unreachable!()
467        };
468        self.0.inplace(state, xy)
469    }
470}
471
472/// General fixed-ratio regrouping adapter for chunked input and output.
473///
474/// Adapt a `[X; Q] -> [Y; R]` processor to `[X; N = Q*I]->[Y; M = R*I]` for any `I`
475/// by flattening and re-chunking input and output.
476///
477/// This is the most general fixed-ratio chunk adapter in the crate. It requires
478/// the input and output to represent the same number of inner chunks. Reach for
479/// it when neither plain [`Chunk`], [`ChunkIn`], nor [`ChunkOut`] captures the
480/// actual grouping relation.
481///
482/// # Examples
483///
484/// ```rust
485/// use dsp_process::{ChunkInOut, FnSplitProcess, Process, Split};
486///
487/// let mut p = Split::stateless(ChunkInOut::<_, 2, 1>(FnSplitProcess(
488///     |_: &mut (), [a, b]: [i32; 2]| [a + b],
489/// )));
490/// assert_eq!(p.process([1, 2, 3, 4]), [3, 7]);
491/// ```
492#[derive(Debug, Copy, Clone, Default)]
493pub struct ChunkInOut<P, const Q: usize, const R: usize>(pub P);
494impl<
495    C,
496    S,
497    X: Copy,
498    Y: Default + Copy,
499    const Q: usize,
500    const N: usize,
501    const R: usize,
502    const M: usize,
503> SplitProcess<[X; N], [Y; M], S> for ChunkInOut<C, Q, R>
504where
505    C: SplitProcess<[X; Q], [Y; R], S>,
506{
507    fn process(&self, state: &mut S, x: [X; N]) -> [Y; M] {
508        const { assert!(Q > 0) }
509        const { assert!(R > 0) }
510        const { assert!(N.is_multiple_of(Q)) }
511        const { assert!(M.is_multiple_of(R)) }
512        const { assert!(N / Q == M / R) }
513        // `poor-codegen-from-fn-iter-next`: if this changes, use a real conversion primitive.
514        let mut y = [Y::default(); M];
515        let (yy, []) = y.as_chunks_mut() else {
516            unreachable!()
517        };
518        let (x, []) = x.as_chunks() else {
519            unreachable!()
520        };
521        for (x, y) in x.iter().zip(yy) {
522            *y = self.0.process(state, *x);
523        }
524        y
525    }
526
527    fn block(&self, state: &mut S, x: &[[X; N]], y: &mut [[Y; M]]) {
528        const { assert!(Q > 0) }
529        const { assert!(R > 0) }
530        const { assert!(N.is_multiple_of(Q)) }
531        const { assert!(M.is_multiple_of(R)) }
532        const { assert!(N / Q == M / R) }
533        let (x, []) = x.as_flattened().as_chunks() else {
534            unreachable!()
535        };
536        let (y, []) = y.as_flattened_mut().as_chunks_mut() else {
537            unreachable!()
538        };
539        self.0.block(state, x, y)
540    }
541}
542impl<C: SplitInplace<[X; 1], S>, S, X: Copy, const N: usize> SplitInplace<[X; N], S>
543    for ChunkInOut<C, 1, 1>
544where
545    Self: SplitProcess<[X; N], [X; N], S>,
546{
547    fn inplace(&self, state: &mut S, xy: &mut [[X; N]]) {
548        let (xy, []) = xy.as_flattened_mut().as_chunks_mut() else {
549            unreachable!()
550        };
551        self.0.inplace(state, xy)
552    }
553}