use crate::dsp::direct::DirectConvolver;
use crate::engine::partition::UniformPartitionEngine;
pub struct HybridConvolver {
direct: DirectConvolver,
tail: UniformPartitionEngine,
block_size: usize,
carry: Vec<f32>,
}
impl HybridConvolver {
pub fn new(ir: &[f32], block_size: usize) -> Self {
assert!(block_size > 0, "block_size must be > 0");
let head_len = ir.len().min(block_size);
let (head_ir, tail_ir) = ir.split_at(head_len);
Self {
direct: DirectConvolver::new(head_ir),
tail: UniformPartitionEngine::new(tail_ir, block_size),
block_size,
carry: vec![0.0f32; block_size],
}
}
#[inline]
pub fn block_size(&self) -> usize {
self.block_size
}
#[inline]
pub fn head_len(&self) -> usize {
self.direct.head_len()
}
#[inline]
pub fn num_partitions(&self) -> usize {
self.tail.num_partitions()
}
pub fn reset(&mut self) {
self.direct.reset();
self.tail.reset();
self.carry.fill(0.0);
}
pub fn load_ir(&mut self, ir: &[f32]) {
let head_len = ir.len().min(self.block_size);
let (head_ir, tail_ir) = ir.split_at(head_len);
self.direct.load_ir(head_ir);
self.tail.load_ir(tail_ir);
self.carry.fill(0.0);
}
pub fn process(&mut self, input: &[f32], output: &mut [f32]) {
debug_assert_eq!(
input.len(),
self.block_size,
"process: input length must equal block_size"
);
debug_assert_eq!(
output.len(),
self.block_size,
"process: output length must equal block_size"
);
self.direct.process(input, output);
for (out, &c) in output.iter_mut().zip(self.carry.iter()) {
*out += c;
}
self.carry.fill(0.0);
self.tail.process(input, &mut self.carry);
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPSILON: f32 = 1e-4;
fn naive(ir: &[f32], input: &[f32]) -> Vec<f32> {
let mut out = vec![0.0f32; input.len()];
for (idx, sample) in out.iter_mut().enumerate() {
for (tap_idx, &tap) in ir.iter().enumerate() {
if idx >= tap_idx {
*sample += tap * input[idx - tap_idx];
}
}
}
out
}
fn run_blocks(ir: &[f32], block_size: usize, blocks: &[&[f32]]) -> Vec<f32> {
let mut conv = HybridConvolver::new(ir, block_size);
let mut out = Vec::new();
for &block in blocks {
let mut buf = vec![0.0f32; block_size];
conv.process(block, &mut buf);
out.extend_from_slice(&buf);
}
out
}
fn assert_close(label: &str, got: &[f32], expected: &[f32], tol: f32) {
assert_eq!(got.len(), expected.len(), "{label}: length mismatch");
for (idx, (&value, &target)) in got.iter().zip(expected.iter()).enumerate() {
let err = (value - target).abs();
assert!(
err < tol,
"{label}[{idx}]: expected {target}, got {value} (abs err {err})"
);
}
}
#[test]
fn empty_ir_has_no_state() {
let conv = HybridConvolver::new(&[], 4);
assert_eq!(conv.head_len(), 0);
assert_eq!(conv.num_partitions(), 0);
assert_eq!(conv.block_size(), 4);
}
#[test]
fn short_ir_uses_only_direct_head() {
let conv = HybridConvolver::new(&[1.0f32, 0.5, 0.25], 8);
assert_eq!(conv.head_len(), 3);
assert_eq!(conv.num_partitions(), 0);
}
#[test]
fn ir_of_block_size_has_no_tail() {
let conv = HybridConvolver::new(&[1.0f32, 0.5, 0.25, 0.125], 4);
assert_eq!(conv.head_len(), 4);
assert_eq!(conv.num_partitions(), 0);
}
#[test]
fn long_ir_splits_head_and_tail() {
let conv = HybridConvolver::new(&[0.5f32; 12], 4);
assert_eq!(conv.head_len(), 4);
assert_eq!(conv.num_partitions(), 2);
}
#[test]
fn empty_ir_produces_silence() {
let mut conv = HybridConvolver::new(&[], 4);
let mut out = [99.0f32; 4]; conv.process(&[1.0f32, 2.0, 3.0, 4.0], &mut out);
assert_eq!(out, [0.0f32; 4]);
}
#[test]
fn unit_impulse_ir_is_identity() {
let block_size = 4;
let ir = [1.0f32];
let input = [1.0f32, 2.0, 3.0, 4.0];
let out = run_blocks(&ir, block_size, &[&input]);
assert_close("identity", &out, &input, EPSILON);
}
#[test]
fn head_only_matches_naive() {
let block_size = 8;
let ir = [1.0f32, 2.0, 3.0];
let input: Vec<f32> = (0..24).map(|i| (i as f32) * 0.1).collect();
let blocks: Vec<&[f32]> = input.chunks(block_size).collect();
let out = run_blocks(&ir, block_size, &blocks);
let reference = naive(&ir, &input);
assert_close("head-only", &out, &reference, EPSILON);
}
#[test]
fn head_plus_one_tail_partition_matches_naive() {
let block_size = 4;
let ir = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0];
let input: Vec<f32> = (0..12).map(|i| (i + 1) as f32).collect();
let blocks: Vec<&[f32]> = input.chunks(block_size).collect();
let out = run_blocks(&ir, block_size, &blocks);
let reference = naive(&ir, &input);
assert_close("head+1 tail", &out, &reference, EPSILON);
}
#[test]
fn head_plus_multi_partition_matches_naive() {
let block_size = 4;
let ir: Vec<f32> = (0..20).map(|i| ((i as f32) * 0.3).sin() * 0.5).collect();
let input: Vec<f32> = (0..40).map(|i| ((i as f32) * 0.17).sin()).collect();
let blocks: Vec<&[f32]> = input.chunks(block_size).collect();
let out = run_blocks(&ir, block_size, &blocks);
let reference = naive(&ir, &input);
assert_close("head+multi tail", &out, &reference, 1e-4);
}
#[test]
fn non_multiple_ir_length_matches_naive() {
let block_size = 4;
let ir: Vec<f32> = (1..=11).map(|i| i as f32 * 0.1).collect();
let input: Vec<f32> = (0..20).map(|i| (i as f32) * 0.05 + 0.1).collect();
let blocks: Vec<&[f32]> = input.chunks(block_size).collect();
let out = run_blocks(&ir, block_size, &blocks);
let reference = naive(&ir, &input);
assert_close("non-multiple", &out, &reference, EPSILON);
}
#[test]
fn zero_latency_at_sample_zero() {
let block_size = 4;
let ir = [1.0f32, 0.5, 0.25, 0.125, 0.0625, 0.03125];
let mut conv = HybridConvolver::new(&ir, block_size);
let input = [1.0f32, 0.0, 0.0, 0.0];
let mut out = [0.0f32; 4];
conv.process(&input, &mut out);
assert!(
(out[0] - 1.0).abs() < EPSILON,
"zero-latency: out[0]={} (expected 1.0)",
out[0]
);
}
#[test]
fn tail_tap_fires_on_next_block() {
let block_size = 4;
let ir = [0.0f32, 0.0, 0.0, 0.0, 1.0];
let mut conv = HybridConvolver::new(&ir, block_size);
let mut block0 = [0.0f32; 4];
conv.process(&[1.0f32, 0.0, 0.0, 0.0], &mut block0);
for (idx, &value) in block0.iter().enumerate() {
assert!(
value.abs() < EPSILON,
"block 0 out[{idx}]={value} (expected 0)"
);
}
let mut block1 = [0.0f32; 4];
conv.process(&[0.0f32; 4], &mut block1);
assert!(
(block1[0] - 1.0).abs() < EPSILON,
"block 1 out[0]={} (expected 1.0)",
block1[0]
);
for (idx, &value) in block1.iter().enumerate().skip(1) {
assert!(
value.abs() < EPSILON,
"block 1 out[{idx}]={value} (expected 0)"
);
}
}
#[test]
fn impulse_response_across_many_blocks() {
let block_size = 4;
let ir: Vec<f32> = (1..=10).map(|i| i as f32).collect();
let mut conv = HybridConvolver::new(&ir, block_size);
let mut got = Vec::new();
let blocks: [[f32; 4]; 4] = [[1.0, 0.0, 0.0, 0.0], [0.0; 4], [0.0; 4], [0.0; 4]];
for block in &blocks {
let mut buf = [0.0f32; 4];
conv.process(block, &mut buf);
got.extend_from_slice(&buf);
}
let mut expected = vec![0.0f32; 16];
for (i, &h) in ir.iter().enumerate() {
expected[i] = h;
}
assert_close("impulse response", &got, &expected, EPSILON);
}
#[test]
fn reset_gives_same_result_as_fresh() {
let block_size = 4;
let ir = [1.0f32, 0.5, 0.25, 0.125, 0.0625, 0.03125];
let mut conv = HybridConvolver::new(&ir, block_size);
let mut scratch = [0.0f32; 4];
conv.process(&[1.0f32; 4], &mut scratch);
conv.reset();
let input = [1.0f32, 2.0, 3.0, 4.0];
let mut after_reset = [0.0f32; 4];
conv.process(&input, &mut after_reset);
let mut fresh = HybridConvolver::new(&ir, block_size);
let mut expected = [0.0f32; 4];
fresh.process(&input, &mut expected);
assert_close("reset", &after_reset, &expected, EPSILON);
}
#[test]
fn load_ir_installs_new_coefficients() {
let block_size = 4;
let mut conv = HybridConvolver::new(&[1.0f32], block_size);
let mut scratch = [0.0f32; 4];
conv.process(&[1.0f32; 4], &mut scratch);
let new_ir = [2.0f32, 0.0, 0.0, 0.0, 0.0, 0.0, 3.0];
conv.load_ir(&new_ir);
assert_eq!(conv.head_len(), 4);
assert_eq!(conv.num_partitions(), 1);
let mut out = [0.0f32; 4];
conv.process(&[1.0f32, 0.0, 0.0, 0.0], &mut out);
assert!(
(out[0] - 2.0).abs() < EPSILON,
"post-load head fires wrong: out[0]={}",
out[0]
);
}
#[test]
fn process_overwrites_output() {
let block_size = 4;
let mut conv = HybridConvolver::new(&[1.0f32, 0.5], block_size);
let mut out = [99.0f32; 4]; conv.process(&[1.0f32; 4], &mut out);
for &value in &out {
assert!(value < 99.0, "sentinel not overwritten (sample = {value})");
}
}
}