Skip to main content

dsp_process/
lib.rs

1#![no_std]
2#![doc = include_str!("../README.md")]
3//!
4//! ## API Parameterization Rules
5//!
6//! `dsp-process` uses a small set of parameterization rules throughout the API:
7//!
8//! - Use runtime fields for per-instance values that may reasonably vary without
9//!   changing the type, such as gains, offsets, clamps, held samples, and
10//!   downsample counts.
11//! - Use const generics for structural facts that define shape, topology, or
12//!   fixed ratios at compile time, such as lane counts, chunk widths,
13//!   input/output ratios, slot indices, and view layouts.
14//! - When a structural parameter also determines stored memory shape, encode it
15//!   in the field type itself, typically with arrays like `[T; N]`, rather than
16//!   duplicating it as both a runtime field and a const generic.
17//! - Use wrapper newtypes for semantic adaptation and composition order
18//!   (`Chunk`, `Decimator`, `Lanes`, `Minor`, `Major`, `ByLane`) rather
19//!   than for user-tunable configuration.
20//! - Use stream adapters with explicit state when phase matters across time
21//!   (`Downsample`, `Hold`, `Decimator`, `Interpolator`); use structural adapters
22//!   when only per-call shape changes (`Rate`, `ChunkIn`, `ChunkOut`,
23//!   `ChunkInOut`, view layout markers).
24//!
25//! In short: values live at runtime, shapes live in types, and composition
26//! semantics live in wrappers.
27
28mod process;
29pub use process::*;
30mod view;
31pub use view::*;
32mod basic;
33pub use basic::*;
34mod adapters;
35pub use adapters::*;
36mod split;
37pub use split::*;
38mod compose;
39pub use compose::*;
40
41/// Parallel filter pair
42///
43/// This can be viewed as digital lattice filter or butterfly filter or complementary allpass pair
44/// or polyphase interpolator.
45/// Candidates for the branches are allpasses like Wdf or Ldi, polyphase banks for resampling or Hilbert filters.
46///
47/// Potentially required scaling with 0.5 gain is to be performed ahead of the filter, within each branch, or (with headroom) afterwards.
48///
49/// This uses the default configuration-minor/sample-major implementation
50/// and may lead to suboptimal cashing and register thrashing for large branches.
51/// To avoid this, use `block()` and `inplace()` on a scratch buffer ([`Major`] input or output).
52///
53/// The corresponding state for this is `((Unsplit<Identity>, (S0, S1)), Unsplit<Add>)`.
54///
55/// # Examples
56///
57/// ```rust
58/// use dsp_process::{Add, Gain, Identity, Offset, Pair, Parallel, Process, Split, Unsplit};
59///
60/// let mut pair = Split::new(
61///     Pair::<_, _, i32>::new((((), Parallel::new((Offset(3), Gain(4)))), ())),
62///     ((Unsplit(Identity), Default::default()), Unsplit(Add)),
63/// );
64/// let y: i32 = pair.process(5);
65/// assert_eq!(y, 28);
66/// ```
67pub type Pair<C0, C1, X, I = (), J = ()> = Minor<((I, Parallel<(C0, C1)>), J), [X; 2]>;
68
69#[cfg(test)]
70mod test {
71    use super::*;
72    use dsp_fixedpoint::Q32;
73
74    #[test]
75    fn basic() {
76        assert_eq!(3, Identity.process(3));
77        assert_eq!([7, 1], Butterfly.process([4, 3]));
78        assert_eq!(
79            Split::stateless(Gain(Q32::<3>::from_bits(32))).process(9),
80            9 * 4
81        );
82        assert_eq!(Split::stateless(Offset(7)).process(9), 7 + 9);
83    }
84
85    #[test]
86    fn stateless() {
87        assert_eq!(Neg.process(9), -9);
88        assert_eq!(Split::stateful(Neg).process(9), -9);
89
90        let mut p = (Split::stateless(Offset(7)) * Split::stateless(Offset(1))).minor();
91        p.assert_process::<i8, _>();
92        assert_eq!(p.process(9), 7 + 1 + 9);
93    }
94
95    #[test]
96    fn stateful() {
97        let mut xy = [3, 0, 0];
98        let mut dly = Buffer::<[_; 2]>::default();
99        dly.inplace(&mut xy);
100        assert_eq!(xy, [0, 0, 3]);
101        let y: i32 = Split::stateful(dly).process(4);
102        assert_eq!(y, 0);
103    }
104
105    #[test]
106    fn pair() {
107        let g = Gain(Q32::<1>::from_bits(4));
108        let mut f = Split::new(
109            Pair::<_, _, _>::new((((), Parallel::new((Offset(3), g))), ())),
110            ((Unsplit(Identity), Default::default()), Unsplit(Add)),
111        );
112        let y: i32 = f.process(5);
113        assert_eq!(y, (5 + 3) + ((5 * 4) >> 1));
114
115        let y: [i32; 5] = f.lanes().process([5; _]);
116        assert_eq!(y, [(5 + 3) + ((5 * 4) >> 1); 5]);
117    }
118
119    #[test]
120    fn chunk_in_out() {
121        let mut p = Split::stateless(ChunkInOut(FnSplitProcess(
122            |_: &mut (), [x0, x1]: [i32; 2]| [x0 + x1],
123        )));
124        let y = p.process([1, 2, 3, 4]);
125        assert_eq!(y, [3, 7]);
126    }
127
128    #[cfg(feature = "bytemuck")]
129    #[test]
130    fn chunk_out_pod() {
131        let mut p = Split::stateless(ChunkOutPod(FnSplitProcess(|_: &mut (), x: i32| [x, -x])));
132        let y = p.process([2, 3]);
133        assert_eq!(y, [2, -2, 3, -3]);
134    }
135
136    #[test]
137    fn frame_major_view_fallback() {
138        let mut p = Split::stateless(Offset(3));
139        let x = View::from_frames(&[[1, 2], [3, 4]]);
140        let mut y = [[0; 2]; 2];
141        let yb = ViewMut::from_frames(&mut y);
142        p.process_view(x, yb);
143        assert_eq!(y, [[4, 5], [6, 7]]);
144    }
145
146    #[test]
147    fn lane_major_lanes() {
148        let mut p = Split::stateless(Offset(3)).lanes::<2>();
149        let x = View::from_flat(&[1, 2, 3, 10, 20, 30], 3);
150        let mut y = [0; 6];
151        let yb = ViewMut::from_flat(&mut y, 3);
152        p.process_view(x, yb);
153        assert_eq!(y, [4, 5, 6, 13, 23, 33]);
154    }
155
156    #[test]
157    fn lane_major_by_lane() {
158        let mut p = Split::new(ByLane::new([Offset(1), Offset(10)]), [(), ()]);
159        let x = View::from_flat(&[1, 2, 3, 10, 20, 30], 3);
160        let mut y = [0; 6];
161        let yb = ViewMut::from_flat(&mut y, 3);
162        p.process_view(x, yb);
163        assert_eq!(y, [2, 3, 4, 20, 30, 40]);
164    }
165
166    #[test]
167    fn framewise_chunk_bridge() {
168        let mut p = Split::stateless(ChunkInOut::<_, 2, 1>(FnSplitProcess(
169            |_: &mut (), [x0, x1]: [i32; 2]| [x0 + x1],
170        )))
171        .per_frame();
172        let x = View::from_frames(&[[1, 2], [3, 4]]);
173        let mut y = [[0; 1]; 2];
174        let yb = ViewMut::from_frames(&mut y);
175        p.process_frames(x, yb);
176        assert_eq!(y, [[3], [7]]);
177    }
178
179    #[test]
180    fn buffer_blocks() {
181        let mut dly = Buffer::<[_; 2]>::default();
182        let mut y = [0; 5];
183        dly.block(&[1, 2, 3, 4, 5], &mut y);
184        assert_eq!(y, [0, 0, 1, 2, 3]);
185        assert_eq!(dly.process([6, 7, 8]), [4, 5, 6]);
186
187        let mut chunk = Buffer::<[i32; 2]>::default();
188        let mut y = [None; 5];
189        chunk.block(&[1, 2, 3, 4, 5], &mut y);
190        assert_eq!(y, [None, Some([1, 2]), None, Some([3, 4]), None]);
191
192        let mut stream = Buffer::<[i32; 2]>::default();
193        let mut y = [0; 5];
194        stream.block(
195            &[Some([1, 2]), None, Some([3, 4]), None, Some([5, 6])],
196            &mut y,
197        );
198        assert_eq!(y, [1, 2, 3, 4, 5]);
199    }
200
201    #[test]
202    fn diffsum_block() {
203        let mut nyq = Nyquist([10, 20]);
204        let mut y = [0; 4];
205        nyq.block(&[1, 2, 3, 4], &mut y);
206        assert_eq!(y, [21, 12, 4, 6]);
207        let mut xy = [5, 6, 7];
208        nyq.inplace(&mut xy);
209        assert_eq!(xy, [8, 10, 12]);
210
211        let mut comb = Comb([10, 20]);
212        let mut y = [0; 4];
213        comb.block(&[1, 2, 3, 4], &mut y);
214        assert_eq!(y, [-19, -8, 2, 2]);
215        let mut xy = [5, 6, 7];
216        comb.inplace(&mut xy);
217        assert_eq!(xy, [2, 2, 2]);
218    }
219}