Skip to main content

dsp_process/
split.rs

1use core::array::{from_fn, repeat};
2
3use crate::{
4    ByLane, Chunk, Decimator, Inplace, Interpolator, Lanes, Major, Map, Minor, Parallel, PerFrame,
5    Process, SplitInplace, SplitProcess, TryDecimator,
6};
7
8//////////// SPLIT ////////////
9
10/// A stateful processor assembled from split configuration and state.
11///
12/// [`Split`] is the bridge between [`SplitProcess`] and [`Process`]: it stores
13/// the immutable configuration and mutable runtime state together so the pair can
14/// be passed around as a conventional stateful processor.
15///
16/// Reach for this when a split-state filter needs to be owned as one value, and
17/// use [`lanes()`](Self::lanes), [`minor()`](Self::minor), or
18/// [`major()`](Self::major) when changing how that owned processor is composed.
19///
20/// # Examples
21///
22/// ```rust
23/// use dsp_process::{Offset, Process, Split};
24///
25/// let mut p = Split::stateless(Offset(3));
26/// assert_eq!(p.process(5), 8);
27/// ```
28#[derive(Debug, Copy, Clone, Default)]
29pub struct Split<C, S> {
30    /// Processor configuration
31    pub config: C,
32    /// Processor state
33    pub state: S,
34}
35
36impl<X: Copy, Y, S, C: SplitProcess<X, Y, S>> Process<X, Y> for Split<C, S> {
37    fn process(&mut self, x: X) -> Y {
38        self.config.process(&mut self.state, x)
39    }
40
41    fn block(&mut self, x: &[X], y: &mut [Y]) {
42        self.config.block(&mut self.state, x, y)
43    }
44}
45
46impl<X: Copy, S, C: SplitInplace<X, S>> Inplace<X> for Split<C, S> {
47    fn inplace(&mut self, xy: &mut [X]) {
48        self.config.inplace(&mut self.state, xy);
49    }
50}
51
52impl<C, S> Split<C, S> {
53    /// Create a new [`Split`] from explicit configuration and state values.
54    #[must_use]
55    pub const fn new(config: C, state: S) -> Self {
56        Self { config, state }
57    }
58
59    /// Statically assert that this implements Process<X, Y>
60    pub const fn assert_process<X: Copy, Y>(&self)
61    where
62        Self: Process<X, Y>,
63    {
64    }
65}
66
67/// Marker for values that should live in the opposite half of a [`Split`].
68///
69/// To be used in `Split<Unsplit<P>, ()>` and `Split<(), Unsplit<P>>`
70/// to mark processors requiring no state and no configuration respectively.
71///
72/// Most users will not construct this directly and should prefer
73/// [`Split::stateless`] and [`Split::stateful`].
74#[derive(Debug, Copy, Clone, Default)]
75#[repr(transparent)]
76pub struct Unsplit<P>(pub P);
77
78impl<C> Split<C, ()> {
79    /// Create a [`Split`] with configuration only and unit state.
80    #[must_use]
81    pub fn stateless(config: C) -> Self {
82        Self::new(config, ())
83    }
84}
85
86impl<S> Split<(), Unsplit<S>> {
87    /// Create a [`Split`] with state only and unit configuration.
88    #[must_use]
89    pub fn stateful(state: S) -> Self {
90        Self::new((), Unsplit(state))
91    }
92}
93
94/// Unzip two splits into one
95impl<C0, C1, S0, S1> core::ops::Mul<Split<C1, S1>> for Split<C0, S0> {
96    type Output = Split<(C0, C1), (S0, S1)>;
97
98    fn mul(self, rhs: Split<C1, S1>) -> Self::Output {
99        Split::from((self, rhs))
100    }
101}
102
103/// Unzip two splits into one parallel
104impl<C0, C1, S0, S1> core::ops::Add<Split<C1, S1>> for Split<C0, S0> {
105    type Output = Split<Parallel<(C0, C1)>, (S0, S1)>;
106
107    fn add(self, rhs: Split<C1, S1>) -> Self::Output {
108        Split::from((self, rhs)).parallel()
109    }
110}
111
112/// Unzip two splits
113impl<C0, C1, S0, S1> From<(Split<C0, S0>, Split<C1, S1>)> for Split<(C0, C1), (S0, S1)> {
114    fn from(value: (Split<C0, S0>, Split<C1, S1>)) -> Self {
115        Split::new(
116            (value.0.config, value.1.config),
117            (value.0.state, value.1.state),
118        )
119    }
120}
121
122/// Unzip multiple splits
123impl<C, S, const N: usize> From<[Split<C, S>; N]> for Split<[C; N], [S; N]> {
124    fn from(splits: [Split<C, S>; N]) -> Self {
125        // Not efficient or nice, but this is usually not a hot path
126        let mut splits = splits.map(|s| (Some(s.config), Some(s.state)));
127        Self::new(
128            from_fn(|i| splits[i].0.take().unwrap()),
129            from_fn(|i| splits[i].1.take().unwrap()),
130        )
131    }
132}
133
134impl<C, S> Split<C, S> {
135    /// Convert to [`Minor`] composition.
136    ///
137    /// This keeps the same logical processor but requests sample-by-sample
138    /// `block()`/`inplace()` execution of the wrapped serial composition.
139    ///
140    /// Use this for small fine-grained stages, or when tuple composition must
141    /// cross an intermediate type and the downstream stage is not
142    /// [`SplitInplace`] for that intermediate. Avoid it when preserving
143    /// stage-major slice processing is important for cache behavior or SIMD.
144    #[must_use]
145    pub fn minor<U>(self) -> Split<Minor<C, U>, S> {
146        Split::new(Minor::new(self.config), self.state)
147    }
148
149    /// Convert to [`Major`] composition with an explicit intermediate buffer.
150    ///
151    /// Use this when preserving stage-major slice processing is more important
152    /// than avoiding an intermediate scratch buffer, especially for larger
153    /// stages or stages with meaningful `block()` specializations.
154    #[must_use]
155    pub fn major<U>(self) -> Split<Major<C, U>, S> {
156        Split::new(Major::new(self.config), self.state)
157    }
158
159    /// Convert to [`Parallel`] composition.
160    ///
161    /// This expresses structural branching: each input lane is routed to the
162    /// matching branch and outputs stay separate unless reduced explicitly.
163    #[must_use]
164    pub fn parallel(self) -> Split<Parallel<C>, S> {
165        Split::new(Parallel::new(self.config), self.state)
166    }
167
168    /// Map `Option` and `Result` around this processor.
169    ///
170    /// This lifts the processor through outer `Option`/`Result` control flow
171    /// while preserving the current state unchanged.
172    #[must_use]
173    pub fn map(self) -> Split<Map<C>, S> {
174        Split::new(Map(self.config), self.state)
175    }
176
177    /// Convert to elementwise fixed-size chunk processing.
178    ///
179    /// This is the basic array-lifting adapter. Use the more specific chunk or
180    /// rate adapters when samples must be regrouped rather than processed
181    /// elementwise.
182    #[must_use]
183    pub fn chunk(self) -> Split<Chunk<C>, S> {
184        Split::new(Chunk(self.config), self.state)
185    }
186
187    /// Convert a scalar optional-input stage into chunk output mode.
188    ///
189    /// This preserves stream phase across one input sample expanded into one
190    /// output chunk. Prefer this over structural chunk regrouping when the
191    /// inner stage is naturally `Option<X> -> Y`.
192    #[must_use]
193    pub fn interpolate(self) -> Split<Interpolator<C>, S> {
194        Split::new(Interpolator(self.config), self.state)
195    }
196
197    /// Convert a scalar optional-output stage into unchecked chunk input mode.
198    ///
199    /// This preserves stream phase across one input chunk collapsed into one
200    /// output sample. Prefer this over structural chunk regrouping when the
201    /// inner stage is naturally `X -> Option<Y>`.
202    #[must_use]
203    pub fn decimate(self) -> Split<Decimator<C>, S> {
204        Split::new(Decimator(self.config), self.state)
205    }
206
207    /// Convert a scalar optional-output stage into checked chunk input mode.
208    ///
209    /// This is the checked form of [`decimate()`](Self::decimate), returning an
210    /// error when the inner stage does not tick exactly once per input chunk.
211    #[must_use]
212    pub fn try_decimate(self) -> Split<TryDecimator<C>, S> {
213        Split::new(TryDecimator(self.config), self.state)
214    }
215
216    /// Treat each frame of a frame-major view as one chunk sample.
217    ///
218    /// This bridges chunk-style processors such as [`crate::Chunk`],
219    /// [`crate::ChunkIn`], [`crate::ChunkOut`], and [`crate::ChunkInOut`] into
220    /// the typed view API without changing the backing layout.
221    ///
222    /// ```rust
223    /// use dsp_process::{ChunkInOut, FnSplitProcess, Split, View, ViewMut};
224    ///
225    /// let mut p = Split::stateless(ChunkInOut::<_, 2, 1>(FnSplitProcess(
226    ///     |_: &mut (), [x0, x1]: [i32; 2]| [x0 + x1],
227    /// )))
228    /// .per_frame();
229    /// let x = View::from_frames(&[[1, 2], [3, 4]]);
230    /// let mut y = [[0; 1]; 2];
231    /// let yv = ViewMut::from_frames(&mut y);
232    /// p.process_frames(x, yv);
233    /// assert_eq!(y, [[3], [7]]);
234    /// ```
235    #[must_use]
236    pub fn per_frame(self) -> Split<PerFrame<C>, S> {
237        Split::new(PerFrame(self.config), self.state)
238    }
239
240    /// Duplicate the processor by cloning both configuration and current state.
241    ///
242    /// The current state is copied as-is. Use this only when duplicating the
243    /// existing state is intentional, for example when seeding several identical
244    /// branches from a known starting point.
245    #[must_use]
246    pub fn repeat<const N: usize>(self) -> Split<[C; N], [S; N]>
247    where
248        C: Clone,
249        S: Clone,
250    {
251        Split::new(repeat(self.config), repeat(self.state))
252    }
253
254    /// Share one configuration across multiple cloned states via [`Lanes`].
255    ///
256    /// This is usually preferable to [`repeat()`](Self::repeat) when the
257    /// configuration should be shared but each lane needs its own mutable
258    /// runtime state. For lane-major view processing, pair this with
259    /// [`crate::View`] using [`crate::LaneMajor`].
260    ///
261    /// ```rust
262    /// use dsp_process::{LaneMajor, Offset, Split, View, ViewMut, ViewProcess};
263    ///
264    /// let mut p = Split::stateless(Offset(3)).lanes::<2>();
265    /// let x = View::<_, LaneMajor, 2>::from_flat(&[1, 2, 3, 10, 20, 30], 3);
266    /// let mut y = [0; 6];
267    /// let yv = ViewMut::<_, LaneMajor, 2>::from_flat(&mut y, 3);
268    /// ViewProcess::process_view(&mut p, x, yv);
269    /// assert_eq!(y, [4, 5, 6, 13, 23, 33]);
270    /// ```
271    #[must_use]
272    pub fn lanes<const N: usize>(self) -> Split<Lanes<C>, [S; N]>
273    where
274        S: Clone,
275    {
276        Split::new(Lanes::new(self.config), repeat(self.state))
277    }
278
279    /// Convert to [`ByLane`] view semantics.
280    ///
281    /// Scalar `process()` is unchanged. Use this when parallel branches should
282    /// process lane-major views as long contiguous per-lane slices.
283    #[must_use]
284    pub fn by_lane(self) -> Split<ByLane<C>, S> {
285        Split::new(ByLane::new(self.config), self.state)
286    }
287}
288
289impl<C, S, U> Split<Minor<C, U>, S> {
290    /// Strip minor
291    #[must_use]
292    pub fn inter(self) -> Split<C, S> {
293        Split::new(self.config.into_inner(), self.state)
294    }
295}
296
297impl<C, S> Split<Parallel<C>, S> {
298    /// Convert to serial
299    #[must_use]
300    pub fn inter(self) -> Split<C, S> {
301        Split::new(self.config.into_inner(), self.state)
302    }
303}
304
305impl<C, S> Split<PerFrame<C>, S> {
306    /// Remove per-frame view adaptation.
307    #[must_use]
308    pub fn inter(self) -> Split<C, S> {
309        Split::new(self.config.0, self.state)
310    }
311}
312
313impl<C, S> Split<ByLane<C>, S> {
314    /// Convert to ordinary view semantics.
315    #[must_use]
316    pub fn inter(self) -> Split<C, S> {
317        Split::new(self.config.into_inner(), self.state)
318    }
319}
320
321impl<C, S, B> Split<Major<C, B>, S> {
322    /// Remove major intermediate buffering
323    #[must_use]
324    pub fn inter(self) -> Split<C, S> {
325        Split::new(self.config.into_inner(), self.state)
326    }
327}
328
329impl<C0, C1, S0, S1> Split<(C0, C1), (S0, S1)> {
330    /// Zip up a split
331    #[must_use]
332    pub fn zip(self) -> (Split<C0, S0>, Split<C1, S1>) {
333        (
334            Split::new(self.config.0, self.state.0),
335            Split::new(self.config.1, self.state.1),
336        )
337    }
338}
339
340impl<C, S, const N: usize> Split<[C; N], [S; N]> {
341    /// Zip up a split
342    #[must_use]
343    pub fn zip(self) -> [Split<C, S>; N] {
344        let mut it = self.config.into_iter().zip(self.state);
345        from_fn(|_| {
346            let (c, s) = it.next().unwrap();
347            Split::new(c, s)
348        })
349    }
350}
351
352/// Configuration-less filters
353impl<X: Copy, Y, P: Process<X, Y>> SplitProcess<X, Y, Unsplit<P>> for () {
354    fn process(&self, state: &mut Unsplit<P>, x: X) -> Y {
355        state.0.process(x)
356    }
357
358    fn block(&self, state: &mut Unsplit<P>, x: &[X], y: &mut [Y]) {
359        state.0.block(x, y)
360    }
361}
362
363impl<X: Copy, P: Inplace<X>> SplitInplace<X, Unsplit<P>> for () {
364    fn inplace(&self, state: &mut Unsplit<P>, xy: &mut [X]) {
365        state.0.inplace(xy)
366    }
367}