use super::*;
fn direct_convolve(ir: &[f32], input: &[f32]) -> Vec<f32> {
let out_len = input.len() + ir.len() - 1;
let mut output = vec![0.0f32; out_len];
for (n, out) in output.iter_mut().enumerate() {
let mut acc = 0.0f32;
for (m, &ir_val) in ir.iter().enumerate() {
let x_idx = n as isize - m as isize;
if x_idx >= 0 && x_idx < input.len() as isize {
acc += ir_val * input[x_idx as usize];
}
}
*out = acc;
}
output
}
fn compute_esr(reference: &[f32], computed: &[f32]) -> f64 {
assert_eq!(reference.len(), computed.len());
let mut ref_energy = 0.0f64;
let mut err_energy = 0.0f64;
for (r, c) in reference.iter().zip(computed.iter()) {
let diff = *r as f64 - *c as f64;
ref_energy += (*r as f64) * (*r as f64);
err_energy += diff * diff;
}
if ref_energy < 1e-30 {
return 0.0; }
err_energy / ref_energy
}
fn process_full_signal(engine: &mut ConvEngine, signal: &[f32]) -> Vec<f32> {
let b = engine.partition_size();
let mut output = Vec::with_capacity(signal.len());
let mut buf_in = vec![0.0f32; b];
let mut buf_out = vec![0.0f32; b];
let mut pos = 0;
while pos < signal.len() {
let chunk = (signal.len() - pos).min(b);
buf_in[..chunk].copy_from_slice(&signal[pos..pos + chunk]);
if chunk < b {
buf_in[chunk..].fill(0.0);
}
engine.process(&buf_in, &mut buf_out);
output.extend_from_slice(&buf_out[..chunk.min(b)]);
pos += chunk;
}
let flush_blocks = engine.num_partitions();
for _ in 0..flush_blocks {
buf_in.fill(0.0);
engine.process(&buf_in, &mut buf_out);
output.extend_from_slice(&buf_out[..b]);
}
output
}
fn synth_ir(len: usize, freq: f32, decay: f32, sample_rate: u32) -> Vec<f32> {
(0..len)
.map(|n| {
let t = n as f32 / sample_rate as f32;
(std::f32::consts::TAU * freq * t).sin() * (-decay * t).exp()
})
.collect()
}
#[test]
fn passthrough_on_empty_ir() {
let engine =
ConvEngine::new(&[], 64).expect("construction should succeed for test-sized buffers");
assert!(engine.is_passthrough());
assert_eq!(engine.num_partitions(), 0);
let input: Vec<f32> = (0..128).map(|i| (i as f32 * 0.01).sin()).collect();
let mut output = vec![0.0f32; 64];
let mut engine2 =
ConvEngine::new(&[], 64).expect("construction should succeed for test-sized buffers");
engine2.process(&input[..64], &mut output);
for i in 0..64 {
assert!((output[i] - input[i]).abs() < 1e-10);
}
}
#[test]
fn impulse_response_is_identity_with_latency() {
let mut ir = vec![0.0f32; 64];
ir[0] = 1.0;
let block_size = 32;
let mut engine = ConvEngine::new(&ir, block_size)
.expect("construction should succeed for test-sized buffers");
let signal: Vec<f32> = (0..256).map(|i| i as f32).collect();
let ref_full = direct_convolve(&ir, &signal);
let upols_out = process_full_signal(&mut engine, &signal);
let min_len = ref_full.len().min(upols_out.len());
let esr = compute_esr(&ref_full[..min_len], &upols_out[..min_len]);
assert!(
esr < 1e-5,
"ESR = {:.2e} exceeds 1e-5 for dirac delta IR",
esr
);
}
#[test]
fn latency_equals_partition_size() {
let ir = synth_ir(100, 440.0, 10.0, 48000);
for &bs in &[32, 64, 128, 256] {
let engine =
ConvEngine::new(&ir, bs).expect("construction should succeed for test-sized buffers");
assert_eq!(engine.latency_samples(), bs);
}
}
#[test]
fn esr_parity_short_ir() {
let ir = synth_ir(32, 800.0, 15.0, 48000);
let signal: Vec<f32> = (0..512)
.map(|i| {
let t = i as f32 / 48000.0;
(std::f32::consts::TAU * 220.0 * t).sin()
+ 0.5 * (std::f32::consts::TAU * 660.0 * t).sin()
})
.collect();
let ref_full = direct_convolve(&ir, &signal);
let block_size = 64;
let mut engine = ConvEngine::new(&ir, block_size)
.expect("construction should succeed for test-sized buffers");
let upols_out = process_full_signal(&mut engine, &signal);
let min_len = ref_full.len().min(upols_out.len());
let esr = compute_esr(&ref_full[..min_len], &upols_out[..min_len]);
assert!(
esr < 1e-5,
"ESR = {:.2e} exceeds 1e-5 threshold for short IR (block_size={})",
esr,
block_size
);
}
#[test]
fn esr_parity_medium_ir() {
let ir = synth_ir(200, 500.0, 8.0, 48000);
let signal: Vec<f32> = (0..1024)
.map(|i| {
let t = i as f32 / 48000.0;
(std::f32::consts::TAU * 180.0 * t).sin()
+ 0.3 * (std::f32::consts::TAU * 1200.0 * t).sin()
})
.collect();
let ref_full = direct_convolve(&ir, &signal);
let block_size = 64;
let mut engine = ConvEngine::new(&ir, block_size)
.expect("construction should succeed for test-sized buffers");
assert_eq!(engine.num_partitions(), 4);
let upols_out = process_full_signal(&mut engine, &signal);
let min_len = ref_full.len().min(upols_out.len());
let esr = compute_esr(&ref_full[..min_len], &upols_out[..min_len]);
assert!(
esr < 1e-5,
"ESR = {:.2e} exceeds 1e-5 threshold for medium IR (4 partitions)",
esr
);
}
#[test]
fn esr_parity_long_ir() {
let ir = synth_ir(1000, 300.0, 4.0, 48000);
let signal: Vec<f32> = (0..2048)
.map(|i| {
let t = i as f32 / 48000.0;
(std::f32::consts::TAU * 150.0 * t).sin()
})
.collect();
let ref_full = direct_convolve(&ir, &signal);
let block_size = 64;
let mut engine = ConvEngine::new(&ir, block_size)
.expect("construction should succeed for test-sized buffers");
assert!(engine.num_partitions() > 10);
let upols_out = process_full_signal(&mut engine, &signal);
let min_len = ref_full.len().min(upols_out.len());
let esr = compute_esr(&ref_full[..min_len], &upols_out[..min_len]);
assert!(
esr < 1e-5,
"ESR = {:.2e} exceeds 1e-5 threshold for long IR ({} partitions)",
esr,
engine.num_partitions()
);
}
#[test]
fn single_sample_ir() {
let ir = vec![0.75f32];
let signal: Vec<f32> = (0..256).map(|i| (i as f32 * 0.02).sin()).collect();
let mut engine =
ConvEngine::new(&ir, 32).expect("construction should succeed for test-sized buffers");
let output = process_full_signal(&mut engine, &signal);
let ref_full = direct_convolve(&ir, &signal);
let min_len = ref_full.len().min(output.len());
let esr = compute_esr(&ref_full[..min_len], &output[..min_len]);
assert!(esr < 1e-5, "ESR = {:.2e} for single-sample IR", esr);
}
#[test]
fn block_size_one() {
let ir = synth_ir(16, 1000.0, 20.0, 48000);
let signal: Vec<f32> = (0..128).map(|i| (i as f32 * 0.05).sin()).collect();
let mut engine =
ConvEngine::new(&ir, 1).expect("construction should succeed for test-sized buffers");
assert_eq!(engine.fft_size(), 2);
let output = process_full_signal(&mut engine, &signal);
let ref_full = direct_convolve(&ir, &signal);
let min_len = ref_full.len().min(output.len());
let esr = compute_esr(&ref_full[..min_len], &output[..min_len]);
assert!(esr < 1e-5, "ESR = {:.2e} for block_size=1", esr);
}
#[test]
fn non_power_of_two_block_size() {
let ir = synth_ir(80, 600.0, 12.0, 48000);
let signal: Vec<f32> = (0..300).map(|i| (i as f32 * 0.03).sin()).collect();
let mut engine =
ConvEngine::new(&ir, 75).expect("construction should succeed for test-sized buffers");
assert_eq!(engine.fft_size(), 256);
assert_eq!(engine.num_partitions(), 2);
let output = process_full_signal(&mut engine, &signal);
let ref_full = direct_convolve(&ir, &signal);
let min_len = ref_full.len().min(output.len());
let esr = compute_esr(&ref_full[..min_len], &output[..min_len]);
assert!(
esr < 1e-5,
"ESR = {:.2e} for non-power-of-2 block size 75 (fft_size=256)",
esr
);
}
#[test]
fn output_length_matches_reference() {
let ir = synth_ir(47, 440.0, 10.0, 48000);
let signal: Vec<f32> = (0..200).map(|i| (i as f32 * 0.01).sin()).collect();
let expected_len = signal.len() + ir.len() - 1;
let mut engine =
ConvEngine::new(&ir, 50).expect("construction should succeed for test-sized buffers");
let output = process_full_signal(&mut engine, &signal);
assert!(
output.len() >= expected_len,
"Output length {} < expected {}",
output.len(),
expected_len
);
}
#[test]
fn deterministic_output() {
let ir = synth_ir(60, 350.0, 8.0, 48000);
let signal: Vec<f32> = (0..300).map(|i| (i as f32 * 0.01).sin()).collect();
let mut engine1 =
ConvEngine::new(&ir, 64).expect("construction should succeed for test-sized buffers");
let mut engine2 =
ConvEngine::new(&ir, 64).expect("construction should succeed for test-sized buffers");
let out1 = process_full_signal(&mut engine1, &signal);
let out2 = process_full_signal(&mut engine2, &signal);
assert_eq!(out1.len(), out2.len());
for (a, b) in out1.iter().zip(out2.iter()) {
assert!((a - b).abs() < 1e-10, "Non-deterministic output");
}
}