use crate::engine::partition::UniformPartitionEngine;
pub struct StereoConvolver {
left: UniformPartitionEngine,
right: UniformPartitionEngine,
}
impl StereoConvolver {
pub fn new(ir_left: &[f32], ir_right: &[f32], block_size: usize) -> Self {
Self {
left: UniformPartitionEngine::new(ir_left, block_size),
right: UniformPartitionEngine::new(ir_right, block_size),
}
}
pub fn new_mono_ir(ir: &[f32], block_size: usize) -> Self {
Self::new(ir, ir, block_size)
}
#[inline]
pub fn block_size(&self) -> usize {
self.left.block_size()
}
pub fn load_ir(&mut self, ir_left: &[f32], ir_right: &[f32]) {
self.left.load_ir(ir_left);
self.right.load_ir(ir_right);
}
pub fn reset(&mut self) {
self.left.reset();
self.right.reset();
}
pub fn process(&mut self, in_l: &[f32], in_r: &[f32], out_l: &mut [f32], out_r: &mut [f32]) {
debug_assert_eq!(in_l.len(), self.block_size(), "in_l length mismatch");
debug_assert_eq!(in_r.len(), self.block_size(), "in_r length mismatch");
debug_assert_eq!(out_l.len(), self.block_size(), "out_l length mismatch");
debug_assert_eq!(out_r.len(), self.block_size(), "out_r length mismatch");
out_l.fill(0.0);
out_r.fill(0.0);
self.left.process(in_l, out_l);
self.right.process(in_r, out_r);
}
}
pub struct TrueStereoConvolver {
ll: UniformPartitionEngine,
lr: UniformPartitionEngine,
rl: UniformPartitionEngine,
rr: UniformPartitionEngine,
}
impl TrueStereoConvolver {
pub fn new(
ir_ll: &[f32],
ir_lr: &[f32],
ir_rl: &[f32],
ir_rr: &[f32],
block_size: usize,
) -> Self {
Self {
ll: UniformPartitionEngine::new(ir_ll, block_size),
lr: UniformPartitionEngine::new(ir_lr, block_size),
rl: UniformPartitionEngine::new(ir_rl, block_size),
rr: UniformPartitionEngine::new(ir_rr, block_size),
}
}
#[inline]
pub fn block_size(&self) -> usize {
self.ll.block_size()
}
pub fn load_ir(&mut self, ir_ll: &[f32], ir_lr: &[f32], ir_rl: &[f32], ir_rr: &[f32]) {
self.ll.load_ir(ir_ll);
self.lr.load_ir(ir_lr);
self.rl.load_ir(ir_rl);
self.rr.load_ir(ir_rr);
}
pub fn reset(&mut self) {
self.ll.reset();
self.lr.reset();
self.rl.reset();
self.rr.reset();
}
pub fn process(&mut self, in_l: &[f32], in_r: &[f32], out_l: &mut [f32], out_r: &mut [f32]) {
debug_assert_eq!(in_l.len(), self.block_size(), "in_l length mismatch");
debug_assert_eq!(in_r.len(), self.block_size(), "in_r length mismatch");
debug_assert_eq!(out_l.len(), self.block_size(), "out_l length mismatch");
debug_assert_eq!(out_r.len(), self.block_size(), "out_r length mismatch");
out_l.fill(0.0);
out_r.fill(0.0);
self.ll.process(in_l, out_l); self.lr.process(in_l, out_r); self.rl.process(in_r, out_l); self.rr.process(in_r, out_r); }
}
#[cfg(test)]
mod tests {
use super::*;
const EPSILON: f32 = 1e-5;
const BLOCK: usize = 4;
fn unit_ir() -> Vec<f32> {
let mut ir = vec![0.0f32; BLOCK];
ir[0] = 1.0;
ir
}
fn assert_approx(label: &str, got: &[f32], expected: &[f32]) {
for (i, (&g, &e)) in got.iter().zip(expected.iter()).enumerate() {
assert!(
(g - e).abs() < EPSILON,
"{label}[{i}]: expected {e}, got {g}"
);
}
}
#[test]
fn stereo_mono_ir_is_identity() {
let mut conv = StereoConvolver::new_mono_ir(&unit_ir(), BLOCK);
let in_l = [1.0f32, 2.0, 3.0, 4.0];
let in_r = [5.0f32, 6.0, 7.0, 8.0];
let (mut out_l, mut out_r) = ([0.0f32; BLOCK], [0.0f32; BLOCK]);
conv.process(&in_l, &in_r, &mut out_l, &mut out_r);
assert_approx("out_l", &out_l, &in_l);
assert_approx("out_r", &out_r, &in_r);
}
#[test]
fn stereo_different_gains_per_channel() {
let ir_l: Vec<f32> = std::iter::once(2.0)
.chain(std::iter::repeat_n(0.0, 3))
.collect();
let ir_r: Vec<f32> = std::iter::once(3.0)
.chain(std::iter::repeat_n(0.0, 3))
.collect();
let mut conv = StereoConvolver::new(&ir_l, &ir_r, BLOCK);
let ones = [1.0f32; BLOCK];
let (mut out_l, mut out_r) = ([0.0f32; BLOCK], [0.0f32; BLOCK]);
conv.process(&ones, &ones, &mut out_l, &mut out_r);
assert_approx("out_l", &out_l, &[2.0f32; BLOCK]);
assert_approx("out_r", &out_r, &[3.0f32; BLOCK]);
}
#[test]
fn stereo_channels_do_not_cross_contaminate() {
let mut conv = StereoConvolver::new_mono_ir(&unit_ir(), BLOCK);
let in_l = [1.0f32, 2.0, 3.0, 4.0];
let in_r = [0.0f32; BLOCK];
let (mut out_l, mut out_r) = ([0.0f32; BLOCK], [0.0f32; BLOCK]);
conv.process(&in_l, &in_r, &mut out_l, &mut out_r);
assert_approx("out_r", &out_r, &[0.0f32; BLOCK]);
assert_approx("out_l", &out_l, &in_l);
}
#[test]
fn stereo_reset_clears_state() {
let ir = unit_ir();
let mut conv = StereoConvolver::new_mono_ir(&ir, BLOCK);
let noise = [9.0f32; BLOCK];
let (mut scratch_l, mut scratch_r) = ([0.0f32; BLOCK], [0.0f32; BLOCK]);
conv.process(&noise, &noise, &mut scratch_l, &mut scratch_r);
conv.reset();
let signal = [1.0f32, 2.0, 3.0, 4.0];
let (mut got_l, mut got_r) = ([0.0f32; BLOCK], [0.0f32; BLOCK]);
conv.process(&signal, &signal, &mut got_l, &mut got_r);
let mut fresh = StereoConvolver::new_mono_ir(&ir, BLOCK);
let (mut exp_l, mut exp_r) = ([0.0f32; BLOCK], [0.0f32; BLOCK]);
fresh.process(&signal, &signal, &mut exp_l, &mut exp_r);
assert_approx("after_reset_l", &got_l, &exp_l);
assert_approx("after_reset_r", &got_r, &exp_r);
}
#[test]
fn stereo_load_ir_replaces_coefficients() {
let mut conv = StereoConvolver::new_mono_ir(&unit_ir(), BLOCK);
let (mut s_l, mut s_r) = ([0.0f32; BLOCK], [0.0f32; BLOCK]);
conv.process(&[1.0f32; BLOCK], &[1.0f32; BLOCK], &mut s_l, &mut s_r);
let ir5: Vec<f32> = std::iter::once(5.0)
.chain(std::iter::repeat_n(0.0, 3))
.collect();
let ir7: Vec<f32> = std::iter::once(7.0)
.chain(std::iter::repeat_n(0.0, 3))
.collect();
conv.load_ir(&ir5, &ir7);
let (mut out_l, mut out_r) = ([0.0f32; BLOCK], [0.0f32; BLOCK]);
conv.process(&[1.0f32; BLOCK], &[1.0f32; BLOCK], &mut out_l, &mut out_r);
assert_approx("out_l", &out_l, &[5.0f32; BLOCK]);
assert_approx("out_r", &out_r, &[7.0f32; BLOCK]);
}
#[test]
fn stereo_state_persists_across_blocks() {
let ir = [0.0f32, 1.0, 0.0, 0.0];
let mut conv = StereoConvolver::new_mono_ir(&ir, BLOCK);
let b1 = [1.0f32, 0.0, 0.0, 0.0];
let (mut out_l, mut out_r) = ([0.0f32; BLOCK], [0.0f32; BLOCK]);
conv.process(&b1, &b1, &mut out_l, &mut out_r);
assert_approx("out_l", &out_l, &[0.0, 1.0, 0.0, 0.0]);
assert_approx("out_r", &out_r, &[0.0, 1.0, 0.0, 0.0]);
}
#[test]
fn true_stereo_identity_matrix() {
let unit = unit_ir();
let mut conv = TrueStereoConvolver::new(&unit, &[], &[], &unit, BLOCK);
let in_l = [1.0f32, 2.0, 3.0, 4.0];
let in_r = [5.0f32, 6.0, 7.0, 8.0];
let (mut out_l, mut out_r) = ([0.0f32; BLOCK], [0.0f32; BLOCK]);
conv.process(&in_l, &in_r, &mut out_l, &mut out_r);
assert_approx("out_l", &out_l, &in_l);
assert_approx("out_r", &out_r, &in_r);
}
#[test]
fn true_stereo_swap_matrix() {
let unit = unit_ir();
let mut conv = TrueStereoConvolver::new(&[], &unit, &unit, &[], BLOCK);
let in_l = [1.0f32, 2.0, 3.0, 4.0];
let in_r = [5.0f32, 6.0, 7.0, 8.0];
let (mut out_l, mut out_r) = ([0.0f32; BLOCK], [0.0f32; BLOCK]);
conv.process(&in_l, &in_r, &mut out_l, &mut out_r);
assert_approx("out_l", &out_l, &in_r);
assert_approx("out_r", &out_r, &in_l);
}
#[test]
fn true_stereo_all_four_paths_contribute() {
let make_ir = |gain: f32| -> Vec<f32> {
std::iter::once(gain)
.chain(std::iter::repeat_n(0.0, 3))
.collect()
};
let mut conv = TrueStereoConvolver::new(
&make_ir(1.0), &make_ir(2.0), &make_ir(3.0), &make_ir(4.0), BLOCK,
);
let impulse = [1.0f32, 0.0, 0.0, 0.0];
let (mut out_l, mut out_r) = ([0.0f32; BLOCK], [0.0f32; BLOCK]);
conv.process(&impulse, &impulse, &mut out_l, &mut out_r);
assert!((out_l[0] - 4.0).abs() < EPSILON, "out_l[0]={}", out_l[0]);
assert!((out_r[0] - 6.0).abs() < EPSILON, "out_r[0]={}", out_r[0]);
for i in 1..BLOCK {
assert!(out_l[i].abs() < EPSILON, "out_l[{i}]={}", out_l[i]);
assert!(out_r[i].abs() < EPSILON, "out_r[{i}]={}", out_r[i]);
}
}
#[test]
fn true_stereo_lr_path_does_not_affect_out_l() {
let unit = unit_ir();
let mut conv = TrueStereoConvolver::new(&[], &unit, &[], &[], BLOCK);
let in_l = [1.0f32, 2.0, 3.0, 4.0];
let (mut out_l, mut out_r) = ([0.0f32; BLOCK], [0.0f32; BLOCK]);
conv.process(&in_l, &[0.0f32; BLOCK], &mut out_l, &mut out_r);
assert_approx("out_l", &out_l, &[0.0f32; BLOCK]);
assert_approx("out_r", &out_r, &in_l);
}
#[test]
fn true_stereo_reset_clears_all_paths() {
let unit = unit_ir();
let mut conv = TrueStereoConvolver::new(&unit, &unit, &unit, &unit, BLOCK);
let noise = [9.0f32; BLOCK];
let (mut s_l, mut s_r) = ([0.0f32; BLOCK], [0.0f32; BLOCK]);
conv.process(&noise, &noise, &mut s_l, &mut s_r);
conv.reset();
let signal = [1.0f32, 0.0, 0.0, 0.0];
let (mut got_l, mut got_r) = ([0.0f32; BLOCK], [0.0f32; BLOCK]);
conv.process(&signal, &signal, &mut got_l, &mut got_r);
let mut fresh = TrueStereoConvolver::new(&unit, &unit, &unit, &unit, BLOCK);
let (mut exp_l, mut exp_r) = ([0.0f32; BLOCK], [0.0f32; BLOCK]);
fresh.process(&signal, &signal, &mut exp_l, &mut exp_r);
assert_approx("got_l", &got_l, &exp_l);
assert_approx("got_r", &got_r, &exp_r);
}
#[test]
fn true_stereo_load_ir_replaces_all_paths() {
let unit = unit_ir();
let mut conv = TrueStereoConvolver::new(&unit, &[], &[], &unit, BLOCK);
let (mut s_l, mut s_r) = ([0.0f32; BLOCK], [0.0f32; BLOCK]);
conv.process(&[1.0f32; BLOCK], &[1.0f32; BLOCK], &mut s_l, &mut s_r);
let ir2: Vec<f32> = std::iter::once(2.0)
.chain(std::iter::repeat_n(0.0, 3))
.collect();
conv.load_ir(&ir2, &ir2, &ir2, &ir2);
let ones = [1.0f32; BLOCK];
let (mut out_l, mut out_r) = ([0.0f32; BLOCK], [0.0f32; BLOCK]);
conv.process(&ones, &ones, &mut out_l, &mut out_r);
assert_approx("out_l", &out_l, &[4.0f32; BLOCK]);
assert_approx("out_r", &out_r, &[4.0f32; BLOCK]);
}
#[test]
fn true_stereo_asymmetric_partition_counts() {
let ir_ll = [1.0_f32, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5];
let ir_rl = [0.0_f32, 2.0, 0.0, 0.0]; let ir_rr: Vec<f32> = std::iter::once(3.0)
.chain(std::iter::repeat_n(0.0, 15))
.collect();
let mut conv = TrueStereoConvolver::new(&ir_ll, &[], &ir_rl, &ir_rr, BLOCK);
let in_l = [1.0_f32, 0.0, 0.0, 0.0];
let in_r = [1.0_f32, 0.0, 0.0, 0.0];
let (mut out_l, mut out_r) = ([0.0_f32; BLOCK], [0.0_f32; BLOCK]);
conv.process(&in_l, &in_r, &mut out_l, &mut out_r);
assert!((out_l[0] - 1.0).abs() < EPSILON, "out_l[0]={}", out_l[0]);
assert!((out_l[1] - 2.0).abs() < EPSILON, "out_l[1]={}", out_l[1]);
assert!((out_r[0] - 3.0).abs() < EPSILON, "out_r[0]={}", out_r[0]);
for (i, val) in out_r.iter().enumerate().skip(1) {
assert!(val.abs() < EPSILON, "out_r[{i}]={val}");
}
let (mut out_l2, mut out_r2) = ([0.0_f32; BLOCK], [0.0_f32; BLOCK]);
conv.process(&[0.0; BLOCK], &[0.0; BLOCK], &mut out_l2, &mut out_r2);
assert!(
(out_l2[3] - 0.5).abs() < EPSILON,
"ll tail expected 0.5 at block 2 sample 3, got {}",
out_l2[3]
);
for (i, val) in out_r2.iter().enumerate() {
assert!(val.abs() < EPSILON, "out_r2[{i}]={val} (expected 0)");
}
}
#[test]
fn true_stereo_rl_tail_persists_across_samples() {
let ir_rl = [0.0f32, 0.0, 0.0, 1.0];
let mut conv = TrueStereoConvolver::new(&[], &[], &ir_rl, &[], BLOCK);
let in_r = [1.0f32, 0.0, 0.0, 0.0];
let (mut out_l, mut out_r) = ([0.0f32; BLOCK], [0.0f32; BLOCK]);
conv.process(&[0.0f32; BLOCK], &in_r, &mut out_l, &mut out_r);
assert!((out_l[3] - 1.0).abs() < EPSILON, "out_l[3]={}", out_l[3]);
assert_approx("out_r", &out_r, &[0.0f32; BLOCK]);
}
}