1use 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 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 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 pub fn tail_samples(&self) -> usize {
214 self.tail
215 }
216 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}