Skip to main content

secs_dsp/
convolution.rs

1//! Streaming matrix FIR convolution with shared input transforms.
2//!
3//! Every FIR is partitioned into `block_size` samples. One forward transform
4//! per input supplies all its routes, and one inverse transform per output
5//! combines every route before overlap-add. Processing reuses all buffers.
6
7use crate::{Complex64 as C, DspError, invalid};
8use realfft::{ComplexToReal, RealFftPlanner, RealToComplex};
9use std::sync::Arc;
10
11#[derive(Debug, Clone, PartialEq)]
12pub struct FirRoute {
13    pub input: usize,
14    pub output: usize,
15    pub taps: Vec<f64>,
16}
17
18struct PartitionedRoute {
19    input: usize,
20    output: usize,
21    partitions: Vec<Vec<C>>,
22}
23
24pub struct MatrixConvolver {
25    inputs: usize,
26    outputs: usize,
27    block: usize,
28    partitions: usize,
29    tail: usize,
30    head: usize,
31    routes: Vec<PartitionedRoute>,
32    history: Vec<Vec<Vec<C>>>,
33    overlap: Vec<Vec<f64>>,
34    forward: Arc<dyn RealToComplex<f64>>,
35    inverse: Arc<dyn ComplexToReal<f64>>,
36    time: Vec<f64>,
37    spectrum: Vec<C>,
38    scratch: Vec<C>,
39}
40
41impl std::fmt::Debug for MatrixConvolver {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        f.debug_struct("MatrixConvolver")
44            .field("inputs", &self.inputs)
45            .field("outputs", &self.outputs)
46            .field("block_size", &self.block)
47            .field("partitions", &self.partitions)
48            .field("routes", &self.routes.len())
49            .field("tail_samples", &self.tail)
50            .finish_non_exhaustive()
51    }
52}
53
54impl MatrixConvolver {
55    pub fn new(
56        inputs: usize,
57        outputs: usize,
58        block_size: usize,
59        routes: &[FirRoute],
60    ) -> Result<Self, DspError> {
61        let nfft = block_size
62            .checked_mul(2)
63            .ok_or_else(|| invalid("convolution block size overflow"))?;
64        if inputs == 0 || outputs == 0 || block_size == 0 {
65            return Err(invalid(
66                "convolution channel and block counts must be positive",
67            ));
68        }
69        if routes.iter().any(|r| {
70            r.input >= inputs
71                || r.output >= outputs
72                || r.taps.is_empty()
73                || r.taps.iter().any(|x| !x.is_finite())
74        }) {
75            return Err(invalid("invalid convolution route or coefficients"));
76        }
77        let longest = routes.iter().map(|r| r.taps.len()).max().unwrap_or(1);
78        let partitions = longest.div_ceil(block_size);
79        inputs
80            .checked_mul(partitions)
81            .and_then(|v| v.checked_mul(block_size + 1))
82            .and_then(|v| v.checked_mul(std::mem::size_of::<C>()))
83            .ok_or_else(|| invalid("convolution history size overflow"))?;
84        outputs
85            .checked_mul(block_size)
86            .and_then(|v| v.checked_mul(std::mem::size_of::<f64>()))
87            .ok_or_else(|| invalid("convolution output size overflow"))?;
88        let mut planner = RealFftPlanner::<f64>::new();
89        let forward = planner.plan_fft_forward(nfft);
90        let inverse = planner.plan_fft_inverse(nfft);
91        let mut time = forward.make_input_vec();
92        let mut scratch =
93            vec![C::default(); forward.get_scratch_len().max(inverse.get_scratch_len())];
94        let mut prepared = Vec::with_capacity(routes.len());
95        for route in routes {
96            let mut spectra = Vec::with_capacity(route.taps.len().div_ceil(block_size));
97            for taps in route.taps.chunks(block_size) {
98                time.fill(0.0);
99                time[..taps.len()].copy_from_slice(taps);
100                let mut spectrum = forward.make_output_vec();
101                forward
102                    .process_with_scratch(&mut time, &mut spectrum, &mut scratch)
103                    .map_err(|e| invalid(&format!("convolution forward transform: {e}")))?;
104                spectra.push(spectrum);
105            }
106            prepared.push(PartitionedRoute {
107                input: route.input,
108                output: route.output,
109                partitions: spectra,
110            });
111        }
112        Ok(Self {
113            inputs,
114            outputs,
115            block: block_size,
116            partitions,
117            tail: longest - 1,
118            head: 0,
119            routes: prepared,
120            history: vec![vec![vec![C::default(); block_size + 1]; partitions]; inputs],
121            overlap: vec![vec![0.0; block_size]; outputs],
122            spectrum: forward.make_output_vec(),
123            forward,
124            inverse,
125            time,
126            scratch,
127        })
128    }
129
130    /// Process exactly one block. Output channels are overwritten, including
131    /// outputs with no routes. Invalid dimensions leave streaming state intact.
132    pub fn process(
133        &mut self,
134        inputs: &[&[f64]],
135        outputs: &mut [&mut [f64]],
136    ) -> Result<(), DspError> {
137        self.process_buffers(inputs, outputs)
138    }
139
140    /// Buffer-owning counterpart to [`Self::process`], so an adapter can pass
141    /// retained `Vec<f64>` channel buffers without allocating a view vector.
142    pub fn process_buffers<I: AsRef<[f64]>, O: AsMut<[f64]>>(
143        &mut self,
144        inputs: &[I],
145        outputs: &mut [O],
146    ) -> Result<(), DspError> {
147        if inputs.len() != self.inputs
148            || outputs.len() != self.outputs
149            || inputs.iter().any(|x| {
150                x.as_ref().len() != self.block || x.as_ref().iter().any(|v| !v.is_finite())
151            })
152            || outputs.iter_mut().any(|y| y.as_mut().len() != self.block)
153        {
154            return Err(invalid("invalid convolution input/output block"));
155        }
156        for (input, history) in inputs.iter().zip(&mut self.history) {
157            self.time[..self.block].copy_from_slice(input.as_ref());
158            self.time[self.block..].fill(0.0);
159            self.forward
160                .process_with_scratch(&mut self.time, &mut history[self.head], &mut self.scratch)
161                .map_err(|e| invalid(&format!("convolution forward transform: {e}")))?;
162        }
163        for (output, destination) in outputs.iter_mut().enumerate() {
164            self.spectrum.fill(C::default());
165            for route in self.routes.iter().filter(|r| r.output == output) {
166                for (partition, filter) in route.partitions.iter().enumerate() {
167                    let slot = (self.head + self.partitions - partition) % self.partitions;
168                    let input = &self.history[route.input][slot];
169                    for ((sum, x), h) in self.spectrum.iter_mut().zip(input).zip(filter) {
170                        *sum += *x * *h;
171                    }
172                }
173            }
174            self.spectrum[0].im = 0.0;
175            self.spectrum[self.block].im = 0.0;
176            self.inverse
177                .process_with_scratch(&mut self.spectrum, &mut self.time, &mut self.scratch)
178                .map_err(|e| invalid(&format!("convolution inverse transform: {e}")))?;
179            let normalization = (2 * self.block) as f64;
180            for (k, y) in destination.as_mut().iter_mut().enumerate() {
181                *y = self.time[k] / normalization + self.overlap[output][k];
182                self.overlap[output][k] = self.time[self.block + k] / normalization;
183            }
184        }
185        self.head = (self.head + 1) % self.partitions;
186        Ok(())
187    }
188
189    pub fn reset(&mut self) {
190        self.head = 0;
191        for channel in &mut self.history {
192            for partition in channel {
193                partition.fill(C::default());
194            }
195        }
196        for channel in &mut self.overlap {
197            channel.fill(0.0);
198        }
199        self.time.fill(0.0);
200        self.spectrum.fill(C::default());
201    }
202
203    pub fn block_size(&self) -> usize {
204        self.block
205    }
206    pub fn input_count(&self) -> usize {
207        self.inputs
208    }
209    pub fn output_count(&self) -> usize {
210        self.outputs
211    }
212    /// Samples to flush after the final input sample.
213    pub fn tail_samples(&self) -> usize {
214        self.tail
215    }
216    /// Forward and inverse transform counts, independent of the route count.
217    pub fn transforms_per_block(&self) -> (usize, usize) {
218        (self.inputs, self.outputs)
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn shared_matrix_partitions_match_direct_across_blocks_and_reset() {
228        let routes = vec![
229            FirRoute {
230                input: 0,
231                output: 0,
232                taps: vec![0.7, -0.2, 0.1],
233            },
234            FirRoute {
235                input: 0,
236                output: 1,
237                taps: (0..31).map(|k| (k as f64).cos() / 31.0).collect(),
238            },
239            FirRoute {
240                input: 1,
241                output: 0,
242                taps: (0..19).map(|k| (k as f64).sin() / 19.0).collect(),
243            },
244            FirRoute {
245                input: 1,
246                output: 0,
247                taps: vec![0.03; 13],
248            },
249        ];
250        for block in [1, 7, 16, 32] {
251            let n = block * 8;
252            let inputs: Vec<Vec<_>> = (0..2)
253                .map(|i| {
254                    (0..n)
255                        .map(|k| {
256                            if k < n / 2 {
257                                ((k * 17 + i) as f64).sin()
258                            } else {
259                                0.0
260                            }
261                        })
262                        .collect()
263                })
264                .collect();
265            let mut expected = vec![vec![0.0; n]; 3];
266            for route in &routes {
267                for k in 0..n {
268                    for (j, h) in route.taps.iter().enumerate().take(k + 1) {
269                        expected[route.output][k] += inputs[route.input][k - j] * h;
270                    }
271                }
272            }
273            let mut convolver = MatrixConvolver::new(2, 3, block, &routes).unwrap();
274            assert_eq!(convolver.transforms_per_block(), (2, 3));
275            assert_eq!(convolver.tail_samples(), 30);
276            for _ in 0..2 {
277                let mut got = vec![vec![0.0; n]; 3];
278                for offset in (0..n).step_by(block) {
279                    let x: Vec<_> = inputs.iter().map(|x| &x[offset..offset + block]).collect();
280                    let mut y: Vec<_> = got
281                        .iter_mut()
282                        .map(|y| &mut y[offset..offset + block])
283                        .collect();
284                    convolver.process(&x, &mut y).unwrap();
285                }
286                for (a, b) in got.iter().flatten().zip(expected.iter().flatten()) {
287                    assert!((a - b).abs() < 2e-12, "block={block}: {a} vs {b}");
288                }
289                convolver.reset();
290            }
291        }
292    }
293
294    #[test]
295    fn convolution_rejects_invalid_data_without_consuming_a_block() {
296        let route = FirRoute {
297            input: 0,
298            output: 0,
299            taps: vec![1.0, 0.5],
300        };
301        assert!(MatrixConvolver::new(0, 1, 8, std::slice::from_ref(&route)).is_err());
302        assert!(MatrixConvolver::new(1, 1, 0, std::slice::from_ref(&route)).is_err());
303        assert!(MatrixConvolver::new(1, 1, usize::MAX, std::slice::from_ref(&route)).is_err());
304        assert!(
305            MatrixConvolver::new(
306                1,
307                1,
308                8,
309                &[FirRoute {
310                    taps: vec![f64::NAN],
311                    ..route.clone()
312                }]
313            )
314            .is_err()
315        );
316        let mut convolver = MatrixConvolver::new(1, 1, 8, &[route]).unwrap();
317        let mut output = [0.0; 8];
318        assert!(convolver.process(&[&[1.0; 7]], &mut [&mut output]).is_err());
319        assert!(
320            convolver
321                .process(&[&[f64::NAN; 8]], &mut [&mut output])
322                .is_err()
323        );
324        convolver
325            .process(
326                &[&[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]],
327                &mut [&mut output],
328            )
329            .unwrap();
330        assert!((output[0] - 1.0).abs() < 1e-12 && (output[1] - 0.5).abs() < 1e-12);
331    }
332}