pub struct DirectConvolver {
ir_head: Vec<f32>,
history: Vec<f32>,
write_pos: usize,
}
impl DirectConvolver {
pub fn new(ir_head: &[f32]) -> Self {
let head_len = ir_head.len();
Self {
ir_head: ir_head.to_vec(),
history: vec![0.0f32; 2 * head_len.max(1)],
write_pos: 0,
}
}
#[inline]
pub fn head_len(&self) -> usize {
self.ir_head.len()
}
pub fn load_ir(&mut self, ir_head: &[f32]) {
let needed_history = 2 * ir_head.len().max(1);
if needed_history != self.history.len() {
self.history = vec![0.0f32; needed_history];
} else {
self.history.fill(0.0);
}
self.write_pos = 0;
self.ir_head.clear();
self.ir_head.extend_from_slice(ir_head);
}
#[inline]
pub fn reset(&mut self) {
self.history.fill(0.0);
self.write_pos = 0;
}
pub fn process(&mut self, input: &[f32], output: &mut [f32]) {
debug_assert_eq!(
input.len(),
output.len(),
"process: input and output must have the same length"
);
let head_len = self.ir_head.len();
if head_len == 0 {
output.fill(0.0);
return;
}
for (x, y) in input.iter().zip(output.iter_mut()) {
self.write_pos = if self.write_pos == 0 {
head_len - 1
} else {
self.write_pos - 1
};
self.history[self.write_pos] = *x;
self.history[self.write_pos + head_len] = *x;
let window = &self.history[self.write_pos..self.write_pos + head_len];
*y = dot(window, &self.ir_head);
}
}
}
#[inline]
fn dot(a: &[f32], b: &[f32]) -> f32 {
a.iter()
.zip(b.iter())
.fold(0.0f32, |acc, (x, h)| acc + x * h)
}
#[cfg(test)]
mod tests {
use super::*;
const EPSILON: f32 = 1e-6;
fn convolve_block(ir: &[f32], input: &[f32]) -> Vec<f32> {
let mut c = DirectConvolver::new(ir);
let mut out = vec![0.0f32; input.len()];
c.process(input, &mut out);
out
}
#[test]
fn unit_impulse_ir_is_identity() {
let ir = [1.0f32];
let input: Vec<f32> = (1..=8).map(|i| i as f32).collect();
let output = convolve_block(&ir, &input);
for (x, y) in input.iter().zip(output.iter()) {
assert!((x - y).abs() < EPSILON, "expected {x}, got {y}");
}
}
#[test]
fn two_tap_delay_ir() {
let ir = [0.0f32, 1.0];
let input = [1.0f32, 2.0, 3.0, 4.0];
let expected = [0.0f32, 1.0, 2.0, 3.0]; let output = convolve_block(&ir, &input);
for (e, y) in expected.iter().zip(output.iter()) {
assert!((e - y).abs() < EPSILON, "expected {e}, got {y}");
}
}
#[test]
fn output_zero_uses_input_zero() {
let ir = [1.0f32];
let input = [42.0f32, 0.0, 0.0];
let output = convolve_block(&ir, &input);
assert!((output[0] - 42.0).abs() < EPSILON);
}
#[test]
fn known_two_tap_convolution() {
let ir = [1.0f32, 2.0];
let input = [1.0f32, 2.0, 3.0, 4.0];
let expected = [1.0f32, 4.0, 7.0, 10.0];
let output = convolve_block(&ir, &input);
for (e, y) in expected.iter().zip(output.iter()) {
assert!((e - y).abs() < EPSILON, "expected {e}, got {y}");
}
}
#[test]
fn state_persists_across_blocks() {
let ir = [1.0f32, 1.0];
let mut conv = DirectConvolver::new(&ir);
let block = [1.0f32; 4];
let mut out1 = [0.0f32; 4];
conv.process(&block, &mut out1);
assert!((out1[0] - 1.0).abs() < EPSILON); assert!((out1[1] - 2.0).abs() < EPSILON); assert!((out1[2] - 2.0).abs() < EPSILON);
assert!((out1[3] - 2.0).abs() < EPSILON);
let mut out2 = [0.0f32; 4];
conv.process(&block, &mut out2);
assert!((out2[0] - 2.0).abs() < EPSILON);
assert!((out2[1] - 2.0).abs() < EPSILON);
}
#[test]
fn reset_clears_history() {
let ir = [1.0f32, 1.0];
let mut conv = DirectConvolver::new(&ir);
let mut scratch = [0.0f32; 4];
conv.process(&[1.0; 4], &mut scratch);
conv.reset();
let input = [3.0f32, 5.0, 7.0];
let mut after_reset = [0.0f32; 3];
conv.process(&input, &mut after_reset);
let expected = convolve_block(&ir, &input);
for (e, y) in expected.iter().zip(after_reset.iter()) {
assert!((e - y).abs() < EPSILON, "expected {e}, got {y}");
}
}
#[test]
fn empty_ir_produces_silence() {
let mut conv = DirectConvolver::new(&[]);
let input = [1.0f32, 2.0, 3.0];
let mut output = [0.0f32; 3];
conv.process(&input, &mut output);
for &y in &output {
assert_eq!(y, 0.0);
}
}
#[test]
fn three_tap_convolution() {
let ir = [1.0f32, 10.0, 100.0];
let input = [1.0f32, 2.0, 3.0, 4.0];
let expected = [1.0f32, 12.0, 123.0, 234.0];
let output = convolve_block(&ir, &input);
for (i, (e, y)) in expected.iter().zip(output.iter()).enumerate() {
assert!((e - y).abs() < EPSILON, "y[{i}]: expected {e}, got {y}");
}
}
#[test]
fn four_tap_convolution_across_wraps() {
let ir = [1.0f32, 2.0, 3.0, 4.0];
let input = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let expected = [1.0f32, 4.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0];
let output = convolve_block(&ir, &input);
for (i, (e, y)) in expected.iter().zip(output.iter()).enumerate() {
assert!((e - y).abs() < EPSILON, "y[{i}]: expected {e}, got {y}");
}
}
#[test]
fn eight_tap_single_sample_matches_block() {
let ir: Vec<f32> = (1..=8).map(|i| i as f32).collect(); let input: Vec<f32> = (1..=16).map(|i| i as f32).collect();
let mut whole = DirectConvolver::new(&ir);
let mut out_whole = vec![0.0f32; input.len()];
whole.process(&input, &mut out_whole);
let mut streamed = DirectConvolver::new(&ir);
let mut out_streamed = vec![0.0f32; input.len()];
for (i, &x) in input.iter().enumerate() {
streamed.process(&[x], &mut out_streamed[i..i + 1]);
}
for (i, (a, b)) in out_whole.iter().zip(out_streamed.iter()).enumerate() {
assert!(
(a - b).abs() < EPSILON,
"streamed y[{i}]={b} diverged from whole-block y[{i}]={a}"
);
}
}
#[test]
fn matches_naive_reference() {
let ir: Vec<f32> = [0.2, -0.5, 0.7, 0.1, -0.3, 0.9].to_vec();
let input: Vec<f32> = (0..32).map(|i| ((i as f32) * 0.37).sin()).collect();
let mut reference = vec![0.0f32; input.len()];
for (n, r) in reference.iter_mut().enumerate() {
for (k, &h) in ir.iter().enumerate() {
if n >= k {
*r += h * input[n - k];
}
}
}
let output = convolve_block(&ir, &input);
for (i, (r, y)) in reference.iter().zip(output.iter()).enumerate() {
assert!((r - y).abs() < EPSILON, "y[{i}]: expected {r}, got {y}");
}
}
#[test]
fn load_ir_swaps_coefficients() {
let mut conv = DirectConvolver::new(&[1.0f32]);
let mut scratch = [0.0f32; 4];
conv.process(&[9.0; 4], &mut scratch);
conv.load_ir(&[2.0f32]);
let mut out = [0.0f32; 2];
conv.process(&[1.0, 1.0], &mut out);
assert!((out[0] - 2.0).abs() < EPSILON);
assert!((out[1] - 2.0).abs() < EPSILON);
}
}