Skip to main content

rill_core_dsp/
direct_conv.rs

1// rill-core-dsp/src/direct_conv.rs
2//! Direct (time-domain) convolution.
3//!
4//! Efficient for short impulse responses where FFT-based convolution
5//! overhead would dominate. For longer IRs, use `OverlapAddConvolver`
6//! or `PartitionedConvolver` from `rill-fft`.
7
8use rill_core::traits::algorithm::{Algorithm, AlgorithmCategory, AlgorithmMetadata};
9use rill_core::traits::ProcessResult;
10use rill_core::Transcendental;
11
12/// Direct time-domain convolver.
13///
14/// Uses a ring buffer for input history. All memory is stack-allocated
15/// via const generics — zero heap allocations in `process()`.
16///
17/// # Type parameters
18///
19/// - `T` — sample type (`f32` or `f64`)
20/// - `IR_LEN` — length of the impulse response
21/// - `BUF_SIZE` — processing block size
22pub struct DirectConvolver<T: Transcendental, const IR_LEN: usize, const BUF_SIZE: usize> {
23    ir: [T; IR_LEN],
24    delay_line: [T; IR_LEN],
25    write_head: usize,
26}
27
28impl<T: Transcendental, const IR_LEN: usize, const BUF_SIZE: usize>
29    DirectConvolver<T, IR_LEN, BUF_SIZE>
30{
31    /// Create a new direct convolver with a zero impulse response.
32    pub fn new() -> Self {
33        Self {
34            ir: [T::ZERO; IR_LEN],
35            delay_line: [T::ZERO; IR_LEN],
36            write_head: 0,
37        }
38    }
39
40    /// Set the impulse response.
41    ///
42    /// `ir` must have exactly `IR_LEN` elements. Extra elements are ignored;
43    /// if shorter, remaining taps are zeroed.
44    pub fn set_ir(&mut self, ir: &[T]) {
45        let len = ir.len().min(IR_LEN);
46        self.ir[..len].copy_from_slice(&ir[..len]);
47        for i in len..IR_LEN {
48            self.ir[i] = T::ZERO;
49        }
50    }
51
52    /// Returns the impulse response length.
53    pub fn ir_len(&self) -> usize {
54        IR_LEN
55    }
56}
57
58impl<T: Transcendental, const IR_LEN: usize, const BUF_SIZE: usize> Default
59    for DirectConvolver<T, IR_LEN, BUF_SIZE>
60{
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66impl<T: Transcendental, const IR_LEN: usize, const BUF_SIZE: usize> Algorithm<T>
67    for DirectConvolver<T, IR_LEN, BUF_SIZE>
68{
69    fn process(&mut self, input: Option<&[T]>, output: &mut [T]) -> ProcessResult<()> {
70        match input {
71            Some(samples) => {
72                for (out, &inp) in output.iter_mut().zip(samples.iter()) {
73                    self.delay_line[self.write_head] = inp;
74                    let mut acc = T::ZERO;
75                    for k in 0..IR_LEN {
76                        let idx = if self.write_head >= k {
77                            self.write_head - k
78                        } else {
79                            IR_LEN + self.write_head - k
80                        };
81                        acc += self.delay_line[idx] * self.ir[k];
82                    }
83                    *out = acc;
84                    self.write_head = (self.write_head + 1) % IR_LEN;
85                }
86                Ok(())
87            }
88            None => {
89                output.fill(T::ZERO);
90                Ok(())
91            }
92        }
93    }
94
95    fn reset(&mut self) {
96        self.delay_line.fill(T::ZERO);
97        self.write_head = 0;
98        self.ir.fill(T::ZERO);
99    }
100
101    fn metadata(&self) -> AlgorithmMetadata {
102        AlgorithmMetadata {
103            name: "DirectConvolver",
104            category: AlgorithmCategory::Effect,
105            description: "Direct time-domain convolution",
106            author: "Rill",
107            version: env!("CARGO_PKG_VERSION"),
108        }
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_unit_impulse_is_passthrough() {
118        let mut conv = DirectConvolver::<f32, 4, 8>::new();
119        conv.set_ir(&[1.0, 0.0, 0.0, 0.0]);
120
121        let input = [0.5f32, 0.3, -0.2, 0.8, 0.1, -0.5, 0.4, 0.0];
122        let mut output = [0.0f32; 8];
123        conv.process(Some(&input), &mut output).unwrap();
124
125        for (i, o) in input.iter().zip(output.iter()) {
126            assert!((i - o).abs() < 1e-6, "expected {i}, got {o}");
127        }
128    }
129
130    #[test]
131    fn test_delayed_impulse_is_delay() {
132        let mut conv = DirectConvolver::<f32, 4, 8>::new();
133        conv.set_ir(&[0.0, 0.0, 1.0, 0.0]);
134
135        let input = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
136        let mut output = [0.0f32; 8];
137        conv.process(Some(&input), &mut output).unwrap();
138
139        assert!((output[0] - 0.0).abs() < 1e-6);
140        assert!((output[1] - 0.0).abs() < 1e-6);
141        assert!((output[2] - 1.0).abs() < 1e-6);
142        assert!((output[3] - 2.0).abs() < 1e-6);
143        assert!((output[4] - 3.0).abs() < 1e-6);
144        assert!((output[5] - 4.0).abs() < 1e-6);
145        assert!((output[6] - 5.0).abs() < 1e-6);
146        assert!((output[7] - 6.0).abs() < 1e-6);
147    }
148
149    #[test]
150    fn test_identity_convolution() {
151        let mut conv = DirectConvolver::<f32, 3, 4>::new();
152        conv.set_ir(&[1.0, 0.0, 0.0]);
153
154        let input = [2.0f32, 3.0, 4.0, 5.0];
155        let mut output = [0.0f32; 4];
156        conv.process(Some(&input), &mut output).unwrap();
157
158        for (i, o) in input.iter().zip(output.iter()) {
159            assert!((i - o).abs() < 1e-6);
160        }
161    }
162
163    #[test]
164    fn test_averaging_ir() {
165        let mut conv = DirectConvolver::<f32, 2, 4>::new();
166        conv.set_ir(&[0.5, 0.5]);
167
168        let input = [1.0f32, 0.0, 0.0, 0.0];
169        let mut output = [0.0f32; 4];
170        conv.process(Some(&input), &mut output).unwrap();
171
172        assert!((output[0] - 0.5).abs() < 1e-6);
173        assert!((output[1] - 0.5).abs() < 1e-6);
174        assert!((output[2] - 0.0).abs() < 1e-6);
175    }
176
177    #[test]
178    fn test_no_input_zeroes_output() {
179        let mut conv = DirectConvolver::<f32, 4, 4>::new();
180        conv.set_ir(&[1.0, 0.5, 0.25, 0.125]);
181        let mut output = [1.0f32; 4];
182        conv.process(None, &mut output).unwrap();
183
184        for o in output.iter() {
185            assert!((o - 0.0).abs() < 1e-6);
186        }
187    }
188
189    #[test]
190    fn test_reset_clears_state() {
191        let mut conv = DirectConvolver::<f32, 4, 4>::new();
192        conv.set_ir(&[1.0, 1.0, 1.0, 1.0]);
193
194        let input = [1.0f32; 4];
195        let mut output = [0.0f32; 4];
196        conv.process(Some(&input), &mut output).unwrap();
197
198        conv.reset();
199        let input2 = [0.0f32; 4];
200        conv.process(Some(&input2), &mut output).unwrap();
201
202        for o in output.iter() {
203            assert!((o - 0.0).abs() < 1e-6);
204        }
205    }
206
207    #[test]
208    fn test_ir_shorter_than_ir_len_zeros_remainder() {
209        let mut conv = DirectConvolver::<f32, 4, 4>::new();
210        conv.set_ir(&[1.0, 2.0]);
211
212        let input = [1.0f32, 2.0, 3.0, 4.0];
213        let mut output = [0.0f32; 4];
214        conv.process(Some(&input), &mut output).unwrap();
215
216        let mut conv2 = DirectConvolver::<f32, 4, 4>::new();
217        conv2.set_ir(&[1.0, 2.0, 0.0, 0.0]);
218
219        let mut output2 = [0.0f32; 4];
220        conv2.process(Some(&input), &mut output2).unwrap();
221
222        for (o1, o2) in output.iter().zip(output2.iter()) {
223            assert!((o1 - o2).abs() < 1e-6);
224        }
225    }
226}