Skip to main content

secs_dsp/
multirate.rs

1//! Experimental low-rate matrix convolution with shared rate converters.
2//!
3//! This causal anti-alias / interpolation cascade has a different response
4//! from the historical edge-tapered, cropped 48 kHz FIR materialization.
5//! Applications must opt into it and align other signal paths by the exposed
6//! rate-converter delay. It is not a transparent replacement for that FIR.
7
8use crate::{
9    DspError,
10    convolution::{FirRoute, MatrixConvolver},
11    fir::firwin,
12    invalid,
13};
14
15#[derive(Debug)]
16pub struct MultirateMatrixConvolver {
17    factor: usize,
18    block: usize,
19    prototype: Vec<f64>,
20    low: MatrixConvolver,
21    down_history: Vec<Vec<f64>>,
22    down_head: usize,
23    up_history: Vec<Vec<f64>>,
24    up_head: usize,
25    low_inputs: Vec<Vec<f64>>,
26    low_outputs: Vec<Vec<f64>>,
27    tail: usize,
28}
29
30impl MultirateMatrixConvolver {
31    /// Hamming-windowed `64 * factor + 1` tap prototype with cutoff at
32    /// 0.4 times the low sample rate. Route taps are at that low rate.
33    pub fn new(
34        inputs: usize,
35        outputs: usize,
36        low_block_size: usize,
37        factor: usize,
38        routes: &[FirRoute],
39    ) -> Result<Self, DspError> {
40        if factor == 0 || low_block_size == 0 || inputs == 0 || outputs == 0 {
41            return Err(invalid("multirate sizes must be positive"));
42        }
43        let length = factor
44            .checked_mul(64)
45            .and_then(|n| n.checked_add(1))
46            .ok_or_else(|| invalid("multirate prototype size overflow"))?;
47        low_block_size
48            .checked_mul(factor)
49            .ok_or_else(|| invalid("multirate block size overflow"))?;
50        length
51            .checked_mul(std::mem::size_of::<f64>())
52            .ok_or_else(|| invalid("multirate prototype size overflow"))?;
53        let mut prototype = firwin(length, 0.4 * (48000.0 / factor as f64), 48000.0)?;
54        // Window evaluation can differ in its last bits at mirrored indices.
55        // The new rate-converter contract uses exactly symmetric coefficients.
56        for k in 0..length / 2 {
57            let value = (prototype[k] + prototype[length - 1 - k]) * 0.5;
58            prototype[k] = value;
59            prototype[length - 1 - k] = value;
60        }
61        Self::with_prototype(inputs, outputs, low_block_size, factor, routes, &prototype)
62    }
63
64    /// Use an explicit odd, symmetric anti-alias prototype for both rate
65    /// converters. Its DC gain is used as supplied; interpolation gains by
66    /// `factor`. Symmetry makes the reported integer alignment delay valid.
67    pub fn with_prototype(
68        inputs: usize,
69        outputs: usize,
70        low_block_size: usize,
71        factor: usize,
72        routes: &[FirRoute],
73        prototype: &[f64],
74    ) -> Result<Self, DspError> {
75        if factor == 0 || low_block_size == 0 || inputs == 0 || outputs == 0 {
76            return Err(invalid("multirate sizes must be positive"));
77        }
78        if prototype.is_empty()
79            || prototype.len().is_multiple_of(2)
80            || prototype.iter().any(|v| !v.is_finite())
81            || prototype.iter().zip(prototype.iter().rev()).any(|(a, b)| {
82                (a - b).abs() > 8.0 * f64::EPSILON * a.abs().max(b.abs()).max(f64::MIN_POSITIVE)
83            })
84        {
85            return Err(invalid(
86                "multirate prototype must be finite, odd and symmetric",
87            ));
88        }
89        let block = low_block_size
90            .checked_mul(factor)
91            .ok_or_else(|| invalid("multirate block size overflow"))?;
92        let up_length = prototype.len().div_ceil(factor);
93        inputs
94            .checked_mul(prototype.len())
95            .and_then(|n| n.checked_mul(8))
96            .ok_or_else(|| invalid("multirate decimator size overflow"))?;
97        outputs
98            .checked_mul(up_length)
99            .and_then(|n| n.checked_mul(8))
100            .ok_or_else(|| invalid("multirate interpolator size overflow"))?;
101        let low = MatrixConvolver::new(inputs, outputs, low_block_size, routes)?;
102        let tail = low
103            .tail_samples()
104            .checked_mul(factor)
105            .and_then(|n| {
106                (prototype.len() - 1)
107                    .checked_mul(2)
108                    .and_then(|m| n.checked_add(m))
109            })
110            .ok_or_else(|| invalid("multirate tail size overflow"))?;
111        Ok(Self {
112            factor,
113            block,
114            prototype: prototype.to_vec(),
115            low,
116            down_history: vec![vec![0.0; prototype.len()]; inputs],
117            down_head: 0,
118            up_history: vec![vec![0.0; up_length]; outputs],
119            up_head: 0,
120            low_inputs: vec![vec![0.0; low_block_size]; inputs],
121            low_outputs: vec![vec![0.0; low_block_size]; outputs],
122            tail,
123        })
124    }
125
126    /// Process one high-rate block without allocation. Decimation computes
127    /// only retained samples; interpolation visits only nonzero polyphases.
128    pub fn process(
129        &mut self,
130        inputs: &[&[f64]],
131        outputs: &mut [&mut [f64]],
132    ) -> Result<(), DspError> {
133        self.process_buffers(inputs, outputs)
134    }
135
136    /// Process retained channel buffers without allocating slice-view arrays.
137    pub fn process_buffers<I: AsRef<[f64]>, O: AsMut<[f64]>>(
138        &mut self,
139        inputs: &[I],
140        outputs: &mut [O],
141    ) -> Result<(), DspError> {
142        if inputs.len() != self.input_count()
143            || outputs.len() != self.output_count()
144            || inputs.iter().any(|x| {
145                x.as_ref().len() != self.block || x.as_ref().iter().any(|v| !v.is_finite())
146            })
147            || outputs.iter_mut().any(|y| y.as_mut().len() != self.block)
148        {
149            return Err(invalid("invalid multirate input/output block"));
150        }
151        let length = self.prototype.len();
152        for t in 0..self.block {
153            for (channel, input) in inputs.iter().enumerate() {
154                self.down_history[channel][self.down_head] = input.as_ref()[t];
155            }
156            if t.is_multiple_of(self.factor) {
157                for (channel, history) in self.down_history.iter().enumerate() {
158                    let mut value = 0.0;
159                    for (k, h) in self.prototype.iter().enumerate() {
160                        value += h * history[(self.down_head + length - k) % length];
161                    }
162                    self.low_inputs[channel][t / self.factor] = value;
163                }
164            }
165            self.down_head = (self.down_head + 1) % length;
166        }
167        self.low
168            .process_buffers(&self.low_inputs, &mut self.low_outputs)?;
169        let up_length = self.up_history[0].len();
170        for t in 0..self.low.block_size() {
171            for (channel, history) in self.up_history.iter_mut().enumerate() {
172                history[self.up_head] = self.low_outputs[channel][t];
173            }
174            for phase in 0..self.factor {
175                for (channel, output) in outputs.iter_mut().enumerate() {
176                    let mut value = 0.0;
177                    for k in (phase..length).step_by(self.factor) {
178                        value += self.prototype[k]
179                            * self.up_history[channel]
180                                [(self.up_head + up_length - k / self.factor) % up_length];
181                    }
182                    output.as_mut()[t * self.factor + phase] = value * self.factor as f64;
183                }
184            }
185            self.up_head = (self.up_head + 1) % up_length;
186        }
187        Ok(())
188    }
189
190    pub fn reset(&mut self) {
191        self.low.reset();
192        self.down_head = 0;
193        self.up_head = 0;
194        for history in &mut self.down_history {
195            history.fill(0.0);
196        }
197        for history in &mut self.up_history {
198            history.fill(0.0);
199        }
200        for block in &mut self.low_inputs {
201            block.fill(0.0);
202        }
203        for block in &mut self.low_outputs {
204            block.fill(0.0);
205        }
206    }
207
208    pub fn block_size(&self) -> usize {
209        self.block
210    }
211    pub fn low_block_size(&self) -> usize {
212        self.low.block_size()
213    }
214    pub fn factor(&self) -> usize {
215        self.factor
216    }
217    pub fn input_count(&self) -> usize {
218        self.low.input_count()
219    }
220    pub fn output_count(&self) -> usize {
221        self.low.output_count()
222    }
223    /// Two linear-phase prototype delays, in high-rate samples.
224    pub fn latency_samples(&self) -> usize {
225        self.prototype.len() - 1
226    }
227    /// Conservative high-rate tail length, including both rate converters.
228    pub fn tail_samples(&self) -> usize {
229        self.tail
230    }
231    pub fn transforms_per_block(&self) -> (usize, usize) {
232        self.low.transforms_per_block()
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    fn direct(x: &[f64], h: &[f64], n: usize) -> Vec<f64> {
241        (0..n)
242            .map(|t| {
243                h.iter()
244                    .enumerate()
245                    .take(t + 1)
246                    .filter(|(k, _)| t - k < x.len())
247                    .map(|(k, h)| x[t - k] * h)
248                    .sum()
249            })
250            .collect()
251    }
252
253    #[test]
254    fn polyphase_stream_matches_explicit_filter_downsample_upsample_filter() {
255        let (factor, n) = (3, 504);
256        let prototype = [0.05, 0.1, 0.2, 0.3, 0.2, 0.1, 0.05];
257        let routes = vec![
258            FirRoute {
259                input: 0,
260                output: 0,
261                taps: vec![1.0, -0.3, 0.2],
262            },
263            FirRoute {
264                input: 1,
265                output: 0,
266                taps: vec![0.4; 19],
267            },
268            FirRoute {
269                input: 0,
270                output: 1,
271                taps: vec![0.5; 13],
272            },
273        ];
274        let inputs: Vec<Vec<_>> = (0..2)
275            .map(|i| {
276                (0..n)
277                    .map(|k| {
278                        if k < n / 2 {
279                            ((k * 7 + i) as f64).sin()
280                        } else {
281                            0.0
282                        }
283                    })
284                    .collect()
285            })
286            .collect();
287        let down: Vec<Vec<_>> = inputs
288            .iter()
289            .map(|x| {
290                direct(x, &prototype, n)
291                    .into_iter()
292                    .step_by(factor)
293                    .collect()
294            })
295            .collect();
296        let mut low = vec![vec![0.0; n / factor]; 2];
297        for route in &routes {
298            let filtered = direct(&down[route.input], &route.taps, n / factor);
299            for (a, b) in low[route.output].iter_mut().zip(filtered) {
300                *a += b;
301            }
302        }
303        let expected: Vec<_> = low
304            .iter()
305            .map(|x| {
306                let mut up = vec![0.0; n];
307                for (t, x) in x.iter().enumerate() {
308                    up[t * factor] = *x;
309                }
310                direct(&up, &prototype, n)
311                    .into_iter()
312                    .map(|y| y * factor as f64)
313                    .collect::<Vec<_>>()
314            })
315            .collect();
316        for block in [1, 7, 28] {
317            let mut filter =
318                MultirateMatrixConvolver::with_prototype(2, 2, block, factor, &routes, &prototype)
319                    .unwrap();
320            assert_eq!(filter.latency_samples(), 6);
321            for _ in 0..2 {
322                let mut got = vec![vec![0.0; n]; 2];
323                for offset in (0..n).step_by(filter.block_size()) {
324                    let end = offset + filter.block_size();
325                    let x: Vec<_> = inputs.iter().map(|x| &x[offset..end]).collect();
326                    let mut y: Vec<_> = got.iter_mut().map(|y| &mut y[offset..end]).collect();
327                    filter.process(&x, &mut y).unwrap();
328                }
329                for (a, b) in got.iter().flatten().zip(expected.iter().flatten()) {
330                    assert!((a - b).abs() < 2e-12);
331                }
332                filter.reset();
333            }
334        }
335    }
336
337    #[test]
338    fn default_rate_converter_preserves_dc_rejects_aliases_and_reports_delay() {
339        let route = FirRoute {
340            input: 0,
341            output: 0,
342            taps: vec![1.0],
343        };
344        let (factor, block) = (80, 32);
345        let mut filter = MultirateMatrixConvolver::new(1, 1, block, factor, &[route]).unwrap();
346        assert_eq!(filter.latency_samples(), 5120);
347        let n = filter.block_size();
348        let mut output = vec![0.0; n];
349        for _ in 0..8 {
350            filter
351                .process(&[&vec![1.0; n]], &mut [&mut output])
352                .unwrap();
353        }
354        let dc = output.iter().sum::<f64>() / n as f64;
355        assert!((dc - 1.0).abs() < 2e-4, "DC gain={dc}");
356        assert!(output.iter().all(|y| (y - 1.0).abs() < 1e-3));
357        filter.reset();
358        for b in 0..8 {
359            let input: Vec<_> = (0..n)
360                .map(|t| (2.0 * std::f64::consts::PI * 550.0 * (b * n + t) as f64 / 48000.0).sin())
361                .collect();
362            filter.process(&[&input], &mut [&mut output]).unwrap();
363        }
364        let rms = (output.iter().map(|y| y * y).sum::<f64>() / n as f64).sqrt();
365        assert!(rms < 1e-3, "550 Hz alias RMS={rms}");
366        filter.reset();
367        let mut impulse_response = Vec::new();
368        for b in 0..5 {
369            let mut input = vec![0.0; n];
370            if b == 0 {
371                input[0] = 1.0;
372            }
373            filter.process(&[&input], &mut [&mut output]).unwrap();
374            impulse_response.extend_from_slice(&output);
375        }
376        let peak = impulse_response
377            .iter()
378            .enumerate()
379            .max_by(|(_, a), (_, b)| a.abs().total_cmp(&b.abs()))
380            .unwrap()
381            .0;
382        assert_eq!(peak, filter.latency_samples());
383    }
384
385    #[test]
386    fn invalid_multirate_dimensions_and_prototypes_are_rejected() {
387        assert!(MultirateMatrixConvolver::new(1, 1, 8, 0, &[]).is_err());
388        assert!(MultirateMatrixConvolver::new(1, 1, usize::MAX, 80, &[]).is_err());
389        for prototype in [&[][..], &[1.0, 0.0], &[1.0, 2.0, 0.0], &[f64::NAN]] {
390            assert!(MultirateMatrixConvolver::with_prototype(1, 1, 8, 3, &[], prototype).is_err());
391        }
392        let mut filter = MultirateMatrixConvolver::with_prototype(1, 1, 8, 3, &[], &[1.0]).unwrap();
393        let mut output = [0.0; 24];
394        assert!(filter.process(&[&[0.0; 23]], &mut [&mut output]).is_err());
395        assert!(
396            filter
397                .process(&[&[f64::NAN; 24]], &mut [&mut output])
398                .is_err()
399        );
400    }
401}