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/// This is the chunked counterpart to [`Interpolator`].
123///
124/// The inner processor must tick exactly once per input chunk. `Decimator`
125/// processes the whole chunk and panics if the contract is violated. Use
126/// [`TryDecimator`] when violating that contract should be reported instead of
127/// panicking.
128///
129/// Unlike [`crate::Rate`], this adapter still runs the
130/// inner processor on every sample in the chunk before choosing the output.
131/// That is the right semantics for recursive stages such as CIC decimators.
132///
133/// Conceptually, this is the chunk-level companion to [`crate::Downsample`]:
134/// `Downsample` gates a scalar stream into `Option<Y>`, while `Decimator`
135/// turns that exact-one-tick-per-chunk protocol into `[X; N] -> Y`.
136/// Unlike [`crate::ChunkIn`], this still executes the inner stage on every
137/// sample in the chunk and is therefore the right adapter for recursive
138/// decimators.
139///
140/// # Examples
141///
142/// ```rust
143/// use dsp_process::{Decimator, FnSplitProcess, SplitProcess};
144///
145/// let proc = Decimator(FnSplitProcess(|state: &mut bool, x: i32| {
146/// let y = if *state { Some(x) } else { None };
147/// *state = !*state;
148/// y
149/// }));
150///
151/// let mut tick = false;
152/// assert_eq!(proc.process(&mut tick, [1, 2]), 2);
153/// ```
154#[derive(Clone, Debug, Default)]
155pub struct Decimator<P>(pub P);
156impl<X: Copy, Y, C: SplitProcess<X, Option<Y>, S>, S, const N: usize> SplitProcess<[X; N], Y, S>
157 for Decimator<C>
158{
159 fn process(&self, state: &mut S, x: [X; N]) -> Y {
160 const { assert!(N > 0) }
161 TryDecimator(&self.0).process(state, x).unwrap()
162 }
163}
164impl<X: Copy, C, S> SplitInplace<X, S> for Decimator<C> where Self: SplitProcess<X, X, S> {}
165
166/// Error returned by [`TryDecimator`] when the inner decimator does not tick
167/// exactly once per input chunk.
168#[derive(Clone, Copy, Debug, Eq, PartialEq)]
169pub enum DecimatorError {
170 /// No output sample was produced for the chunk.
171 NoTick,
172 /// More than one output sample was produced for the chunk.
173 ExtraTick,
174}
175
176/// Checked variant of [`Decimator`].
177///
178/// Returns a tick error after processing the full chunk, preserving stream position.
179///
180/// # Examples
181///
182/// ```rust
183/// use dsp_process::{DecimatorError, FnSplitProcess, SplitProcess, TryDecimator};
184///
185/// let proc = TryDecimator(FnSplitProcess(|state: &mut bool, x: i32| {
186/// let y = if *state { Some(x) } else { None };
187/// *state = !*state;
188/// y
189/// }));
190///
191/// let mut tick = false;
192/// assert_eq!(proc.process(&mut tick, [1, 2]), Ok(2));
193///
194/// let never = TryDecimator(FnSplitProcess(|_: &mut (), _: i32| None::<i32>));
195/// let mut state = ();
196/// assert_eq!(
197/// never.process(&mut state, [1, 2]),
198/// Err(DecimatorError::NoTick)
199/// );
200/// ```
201#[derive(Clone, Debug, Default)]
202pub struct TryDecimator<P>(pub P);
203impl<X: Copy, Y, C: SplitProcess<X, Option<Y>, S>, S, const N: usize>
204 SplitProcess<[X; N], Result<Y, DecimatorError>, S> for TryDecimator<C>
205{
206 fn process(&self, state: &mut S, x: [X; N]) -> Result<Y, DecimatorError> {
207 const { assert!(N > 0) }
208 let mut y = None;
209 let mut extra = false;
210 for x in x {
211 if let Some(next) = self.0.process(state, x)
212 && y.replace(next).is_some()
213 {
214 extra = true;
215 }
216 }
217 if extra {
218 Err(DecimatorError::ExtraTick)
219 } else {
220 y.ok_or(DecimatorError::NoTick)
221 }
222 }
223}
224
225/// Lift a processor through `Option` or `Result`.
226///
227/// This is useful when a processor should only run on present/valid samples
228/// while preserving outer framing or error signaling. It changes control-flow
229/// shape, not block layout.
230///
231/// # Examples
232///
233/// ```rust
234/// use dsp_process::{Map, Offset, SplitProcess};
235///
236/// let proc = Map(Offset(3));
237/// let mut state = ();
238/// assert_eq!(proc.process(&mut state, Some(4)), Some(7));
239/// assert_eq!(proc.process(&mut state, None::<i32>), None);
240/// ```
241#[derive(Clone, Debug, Default)]
242pub struct Map<P>(pub P);
243impl<X: Copy, Y, C: SplitProcess<X, Y, S>, S> SplitProcess<Option<X>, Option<Y>, S> for Map<C> {
244 fn process(&self, state: &mut S, x: Option<X>) -> Option<Y> {
245 x.map(|x| self.0.process(state, x))
246 }
247}
248impl<X: Copy, Y, C: SplitProcess<X, Y, S>, S, E: Copy> SplitProcess<Result<X, E>, Result<Y, E>, S>
249 for Map<C>
250{
251 fn process(&self, state: &mut S, x: Result<X, E>) -> Result<Y, E> {
252 x.map(|x| self.0.process(state, x))
253 }
254}
255impl<X: Copy, C: SplitInplace<X, S>, S> SplitInplace<X, S> for Map<C> where
256 Self: SplitProcess<X, X, S>
257{
258}
259
260/// Elementwise fixed-size chunk lifting.
261///
262/// Adapt a `X -> Y` processor into a `[X; N] -> [Y; N]` processor
263/// by flattening input and output.
264///
265/// This is the simplest array-lifting adapter and is often the right choice
266/// when a scalar stage should run elementwise over fixed-size chunks with no
267/// rate change and no frame semantics beyond flattening.
268///
269/// Prefer the more specific adapters when the inner stage consumes or produces
270/// grouped samples (`ChunkIn`, `ChunkOut`, `ChunkInOut`) or when stream phase is
271/// part of the semantics (`Interpolator`, `Decimator`).
272///
273/// # Examples
274///
275/// ```rust
276/// use dsp_process::{Chunk, Offset, Process, Split};
277///
278/// let mut p = Split::stateless(Chunk(Offset(3)));
279/// assert_eq!(p.process([1, 2, 3]), [4, 5, 6]);
280/// ```
281#[derive(Debug, Copy, Clone, Default)]
282pub struct Chunk<P>(pub P);
283impl<C: SplitProcess<X, Y, S>, S, X: Copy, Y, const N: usize> SplitProcess<[X; N], [Y; N], S>
284 for Chunk<C>
285{
286 fn process(&self, state: &mut S, x: [X; N]) -> [Y; N] {
287 x.map(|x| self.0.process(state, x))
288 }
289
290 fn block(&self, state: &mut S, x: &[[X; N]], y: &mut [[Y; N]]) {
291 self.0.block(state, x.as_flattened(), y.as_flattened_mut())
292 }
293}
294impl<C: SplitInplace<X, S>, S, X: Copy, const N: usize> SplitInplace<[X; N], S> for Chunk<C> {
295 fn inplace(&self, state: &mut S, xy: &mut [[X; N]]) {
296 self.0.inplace(state, xy.as_flattened_mut())
297 }
298}
299
300/// Fixed-ratio chunk adapter for grouped input.
301///
302/// Adapt a `[X; R] -> Y` processor to `[X; N=R*M]->[Y; M]` for any `M`
303/// by flattening and re-chunking input.
304///
305/// Use this when the inner stage consumes several input samples per output, such
306/// as a small decimating FIR kernel. This is a structural regrouping adapter:
307/// it does not track stream phase across calls.
308///
309/// See also [`ChunkOut`] and [`ChunkInOut`].
310///
311/// # Examples
312///
313/// ```rust
314/// use dsp_process::{ChunkIn, FnSplitProcess, Process, Split};
315///
316/// let mut p = Split::stateless(ChunkIn::<_, 2>(FnSplitProcess(
317/// |_: &mut (), [a, b]: [i32; 2]| a + b,
318/// )));
319/// assert_eq!(p.process([1, 2, 3, 4]), [3, 7]);
320/// ```
321#[derive(Debug, Copy, Clone, Default)]
322pub struct ChunkIn<P, const R: usize>(pub P);
323impl<C: SplitProcess<[X; R], Y, S>, S, X: Copy, Y, const N: usize, const R: usize, const M: usize>
324 SplitProcess<[X; N], [Y; M], S> for ChunkIn<C, R>
325{
326 fn process(&self, state: &mut S, x: [X; N]) -> [Y; M] {
327 const { assert!(R * M == N) }
328 let (x, []) = x.as_chunks() else {
329 unreachable!()
330 };
331 core::array::from_fn(|i| self.0.process(state, x[i]))
332 }
333
334 fn block(&self, state: &mut S, x: &[[X; N]], y: &mut [[Y; M]]) {
335 const { assert!(R * M == N) }
336 let (x, []) = x.as_flattened().as_chunks() else {
337 unreachable!()
338 };
339 self.0.block(state, x, y.as_flattened_mut())
340 }
341}
342impl<C: SplitInplace<[X; 1], S>, S, X: Copy, const N: usize> SplitInplace<[X; N], S>
343 for ChunkIn<C, 1>
344where
345 Self: SplitProcess<[X; N], [X; N], S>,
346{
347 fn inplace(&self, state: &mut S, xy: &mut [[X; N]]) {
348 let (xy, []) = xy.as_flattened_mut().as_chunks_mut() else {
349 unreachable!()
350 };
351 self.0.inplace(state, xy)
352 }
353}
354
355/// Fixed-ratio chunk adapter for grouped output.
356///
357/// Adapt a `X -> [Y; R]` processor to `[X; N]->[Y; M = R*N]` for any `N`
358/// by flattening and re-chunking output.
359///
360/// This is the natural adapter for small fixed-ratio interpolation kernels.
361/// Use [`ChunkOutPod`] when the output type is POD and the flattening cost
362/// matters. This is a structural regrouping adapter, not a phased stream
363/// interpolator; use [`Interpolator`] when the inner stage is naturally
364/// `Option<X> -> Y`.
365///
366/// # Examples
367///
368/// ```rust
369/// use dsp_process::{ChunkOut, FnSplitProcess, Process, Split};
370///
371/// let mut p = Split::stateless(ChunkOut::<_, 2>(FnSplitProcess(|_: &mut (), x: i32| {
372/// [x, -x]
373/// })));
374/// assert_eq!(p.process([2, 3]), [2, -2, 3, -3]);
375/// ```
376#[derive(Debug, Copy, Clone, Default)]
377pub struct ChunkOut<P, const R: usize>(pub P);
378impl<C, S, X: Copy, Y: Default + Copy, const N: usize, const R: usize, const M: usize>
379 SplitProcess<[X; N], [Y; M], S> for ChunkOut<C, R>
380where
381 C: SplitProcess<X, [Y; R], S>,
382{
383 fn process(&self, state: &mut S, x: [X; N]) -> [Y; M] {
384 const { assert!(R * N == M) }
385 // `poor-codegen-from-fn-iter-next`: if this changes, use a real conversion primitive.
386 let mut y = [Y::default(); M];
387 let (yy, []) = y.as_chunks_mut() else {
388 unreachable!()
389 };
390 for (x, y) in x.into_iter().zip(yy) {
391 *y = self.0.process(state, x);
392 }
393 y
394 }
395
396 fn block(&self, state: &mut S, x: &[[X; N]], y: &mut [[Y; M]]) {
397 const { assert!(R * N == M) }
398 let (y, []) = y.as_flattened_mut().as_chunks_mut() else {
399 unreachable!()
400 };
401 self.0.block(state, x.as_flattened(), y)
402 }
403}
404impl<C: SplitInplace<[X; 1], S>, S, X: Copy, const N: usize> SplitInplace<[X; N], S>
405 for ChunkOut<C, 1>
406where
407 Self: SplitProcess<[X; N], [X; N], S>,
408{
409 fn inplace(&self, state: &mut S, xy: &mut [[X; N]]) {
410 let (xy, []) = xy.as_flattened_mut().as_chunks_mut() else {
411 unreachable!()
412 };
413 self.0.inplace(state, xy)
414 }
415}
416
417/// POD-specialized [`ChunkOut`] variant.
418///
419/// This keeps the same semantics as [`ChunkOut`] but uses a bytemuck-backed
420/// representation cast to flatten `[[Y; R]; N]` into `[Y; R * N]` without the
421/// generic scratch-buffer path.
422///
423/// This is only available when `Y` is `bytemuck::Pod` and is mainly a codegen/cache
424/// choice, not a semantic one.
425///
426/// # Examples
427#[cfg_attr(
428 feature = "bytemuck",
429 doc = r##"/// ```rust
430/// use dsp_process::{ChunkOutPod, FnSplitProcess, Process, Split};
431///
432/// let mut p = Split::stateless(ChunkOutPod::<_, 2>(FnSplitProcess(|_: &mut (), x: i32| {
433/// [x, -x]
434/// })));
435/// assert_eq!(p.process([2, 3]), [2, -2, 3, -3]);
436/// ```"##
437)]
438#[derive(Debug, Copy, Clone, Default)]
439pub struct ChunkOutPod<P, const R: usize>(pub P);
440#[cfg(feature = "bytemuck")]
441impl<C, S, X: Copy, Y: bytemuck::Pod, const N: usize, const R: usize, const M: usize>
442 SplitProcess<[X; N], [Y; M], S> for ChunkOutPod<C, R>
443where
444 C: SplitProcess<X, [Y; R], S>,
445{
446 fn process(&self, state: &mut S, x: [X; N]) -> [Y; M] {
447 const { assert!(R * N == M) }
448 bytemuck::cast::<[[Y; R]; N], [Y; M]>(x.map(|x| self.0.process(state, x)))
449 }
450
451 fn block(&self, state: &mut S, x: &[[X; N]], y: &mut [[Y; M]]) {
452 const { assert!(R * N == M) }
453 let (y, []) = y.as_flattened_mut().as_chunks_mut() else {
454 unreachable!()
455 };
456 self.0.block(state, x.as_flattened(), y)
457 }
458}
459#[cfg(feature = "bytemuck")]
460impl<C: SplitInplace<[X; 1], S>, S, X: Copy, const N: usize> SplitInplace<[X; N], S>
461 for ChunkOutPod<C, 1>
462where
463 Self: SplitProcess<[X; N], [X; N], S>,
464{
465 fn inplace(&self, state: &mut S, xy: &mut [[X; N]]) {
466 let (xy, []) = xy.as_flattened_mut().as_chunks_mut() else {
467 unreachable!()
468 };
469 self.0.inplace(state, xy)
470 }
471}
472
473/// General fixed-ratio regrouping adapter for chunked input and output.
474///
475/// Adapt a `[X; Q] -> [Y; R]` processor to `[X; N = Q*I]->[Y; M = R*I]` for any `I`
476/// by flattening and re-chunking input and output.
477///
478/// This is the most general fixed-ratio chunk adapter in the crate. It requires
479/// the input and output to represent the same number of inner chunks. Reach for
480/// it when neither plain [`Chunk`], [`ChunkIn`], nor [`ChunkOut`] captures the
481/// actual grouping relation.
482///
483/// # Examples
484///
485/// ```rust
486/// use dsp_process::{ChunkInOut, FnSplitProcess, Process, Split};
487///
488/// let mut p = Split::stateless(ChunkInOut::<_, 2, 1>(FnSplitProcess(
489/// |_: &mut (), [a, b]: [i32; 2]| [a + b],
490/// )));
491/// assert_eq!(p.process([1, 2, 3, 4]), [3, 7]);
492/// ```
493#[derive(Debug, Copy, Clone, Default)]
494pub struct ChunkInOut<P, const Q: usize, const R: usize>(pub P);
495impl<
496 C,
497 S,
498 X: Copy,
499 Y: Default + Copy,
500 const Q: usize,
501 const N: usize,
502 const R: usize,
503 const M: usize,
504> SplitProcess<[X; N], [Y; M], S> for ChunkInOut<C, Q, R>
505where
506 C: SplitProcess<[X; Q], [Y; R], S>,
507{
508 fn process(&self, state: &mut S, x: [X; N]) -> [Y; M] {
509 const { assert!(Q > 0) }
510 const { assert!(R > 0) }
511 const { assert!(N.is_multiple_of(Q)) }
512 const { assert!(M.is_multiple_of(R)) }
513 const { assert!(N / Q == M / R) }
514 // `poor-codegen-from-fn-iter-next`: if this changes, use a real conversion primitive.
515 let mut y = [Y::default(); M];
516 let (yy, []) = y.as_chunks_mut() else {
517 unreachable!()
518 };
519 let (x, []) = x.as_chunks() else {
520 unreachable!()
521 };
522 for (x, y) in x.iter().zip(yy) {
523 *y = self.0.process(state, *x);
524 }
525 y
526 }
527
528 fn block(&self, state: &mut S, x: &[[X; N]], y: &mut [[Y; M]]) {
529 const { assert!(Q > 0) }
530 const { assert!(R > 0) }
531 const { assert!(N.is_multiple_of(Q)) }
532 const { assert!(M.is_multiple_of(R)) }
533 const { assert!(N / Q == M / R) }
534 let (x, []) = x.as_flattened().as_chunks() else {
535 unreachable!()
536 };
537 let (y, []) = y.as_flattened_mut().as_chunks_mut() else {
538 unreachable!()
539 };
540 self.0.block(state, x, y)
541 }
542}
543impl<C: SplitInplace<[X; 1], S>, S, X: Copy, const N: usize> SplitInplace<[X; N], S>
544 for ChunkInOut<C, 1, 1>
545where
546 Self: SplitProcess<[X; N], [X; N], S>,
547{
548 fn inplace(&self, state: &mut S, xy: &mut [[X; N]]) {
549 let (xy, []) = xy.as_flattened_mut().as_chunks_mut() else {
550 unreachable!()
551 };
552 self.0.inplace(state, xy)
553 }
554}