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}