use rustfft::num_complex::Complex;
use crate::dsp::FftHandler;
use crate::engine::fdl::{FreqDomainDelayLine, SplitSpectrum};
pub struct UniformPartitionEngine {
block_size: usize,
ir_spectra: Vec<SplitSpectrum>,
fdl: FreqDomainDelayLine,
input_buf: Vec<f32>,
acc_buf: Vec<Complex<f32>>,
acc_split: SplitSpectrum,
out_buf: Vec<f32>,
fft: FftHandler,
}
impl UniformPartitionEngine {
pub fn new(ir_tail: &[f32], block_size: usize) -> Self {
assert!(block_size > 0, "block_size must be > 0");
let fft_size = 2 * block_size;
let spectrum_size = block_size + 1;
let num_partitions = if ir_tail.is_empty() {
0
} else {
ir_tail.len().div_ceil(block_size)
};
let mut fft = FftHandler::new(fft_size);
let ir_spectra: Vec<SplitSpectrum> = (0..num_partitions)
.map(|k| {
let start = k * block_size;
let end = (start + block_size).min(ir_tail.len());
let mut padded = vec![0.0f32; fft_size];
padded[..end - start].copy_from_slice(&ir_tail[start..end]);
let mut spectrum = vec![Complex::default(); spectrum_size];
fft.forward(&padded, &mut spectrum);
SplitSpectrum::from_complex(&spectrum)
})
.collect();
let fdl = FreqDomainDelayLine::new(num_partitions.max(1), spectrum_size);
Self {
block_size,
ir_spectra,
fdl,
input_buf: vec![0.0f32; fft_size],
acc_buf: vec![Complex::default(); spectrum_size],
acc_split: SplitSpectrum::zeros(spectrum_size),
out_buf: vec![0.0f32; fft_size],
fft,
}
}
#[inline]
pub fn num_partitions(&self) -> usize {
self.ir_spectra.len()
}
#[inline]
pub fn block_size(&self) -> usize {
self.block_size
}
pub fn reset(&mut self) {
self.input_buf.fill(0.0);
self.fdl.reset();
self.acc_buf.fill(Complex::default());
self.acc_split.fill_zero();
self.out_buf.fill(0.0);
}
pub fn load_ir(&mut self, ir_tail: &[f32]) {
*self = Self::new(ir_tail, self.block_size);
}
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"
);
if self.ir_spectra.is_empty() {
return;
}
let n = self.block_size;
self.input_buf.copy_within(n.., 0);
self.input_buf[n..].copy_from_slice(input);
self.fft.forward(&self.input_buf, &mut self.acc_buf);
self.fdl.push(&self.acc_buf);
self.acc_split.fill_zero();
self.fdl.accumulate(&self.ir_spectra, &mut self.acc_split);
self.acc_split.write_interleaved(&mut self.acc_buf);
self.fft.inverse(&mut self.acc_buf, &mut self.out_buf);
for (y, &x) in output.iter_mut().zip(self.out_buf[n..].iter()) {
*y += x;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPSILON: f32 = 1e-5;
fn run_blocks(ir_tail: &[f32], block_size: usize, blocks: &[&[f32]]) -> Vec<f32> {
let mut engine = UniformPartitionEngine::new(ir_tail, block_size);
let mut out = Vec::new();
for &block in blocks {
let mut buf = vec![0.0f32; block_size];
engine.process(block, &mut buf);
out.extend_from_slice(&buf);
}
out
}
#[test]
fn empty_ir_has_zero_partitions() {
let e = UniformPartitionEngine::new(&[], 4);
assert_eq!(e.num_partitions(), 0);
}
#[test]
fn partition_count_rounds_up() {
let e = UniformPartitionEngine::new(&[1.0f32; 7], 4);
assert_eq!(e.num_partitions(), 2);
}
#[test]
fn empty_ir_tail_leaves_output_unchanged() {
let mut engine = UniformPartitionEngine::new(&[], 4);
let input = [1.0f32, 2.0, 3.0, 4.0];
let mut output = [99.0f32; 4]; engine.process(&input, &mut output);
assert_eq!(output, [99.0f32; 4]);
}
#[test]
fn unit_impulse_tail_is_identity() {
let block_size = 4;
let ir = [1.0f32, 0.0, 0.0, 0.0];
let input = [1.0f32, 2.0, 3.0, 4.0];
let out = run_blocks(&ir, block_size, &[&input]);
for (x, y) in input.iter().zip(out.iter()) {
assert!((x - y).abs() < EPSILON, "expected {x}, got {y}");
}
}
#[test]
fn one_sample_delay_tail() {
let block_size = 4;
let ir = [0.0f32, 1.0, 0.0, 0.0];
let input = [1.0f32, 2.0, 3.0, 4.0];
let expected = [0.0f32, 1.0, 2.0, 3.0];
let out = run_blocks(&ir, block_size, &[&input]);
for (e, y) in expected.iter().zip(out.iter()) {
assert!((e - y).abs() < EPSILON, "expected {e}, got {y}");
}
}
#[test]
fn multi_partition_spans_two_blocks() {
let block_size = 2;
let ir = [1.0f32, 0.0, 0.0, 1.0];
let b1 = [1.0f32, 0.0];
let b2 = [0.0f32, 0.0];
let out = run_blocks(&ir, block_size, &[&b1, &b2]);
let expected = [1.0f32, 0.0, 0.0, 1.0];
for (e, y) in expected.iter().zip(out.iter()) {
assert!((e - y).abs() < EPSILON, "expected {e}, got {y}");
}
}
#[test]
fn non_multiple_ir_length_is_zero_padded() {
let block_size = 2;
let ir = [1.0f32, 2.0, 3.0, 4.0, 5.0];
let blocks: Vec<&[f32]> = vec![&[1.0, 0.0], &[0.0, 0.0], &[0.0, 0.0]];
let out = run_blocks(&ir, block_size, &blocks);
let expected = [1.0f32, 2.0, 3.0, 4.0, 5.0, 0.0];
for (i, (e, y)) in expected.iter().zip(out.iter()).enumerate() {
assert!((e - y).abs() < EPSILON, "y[{i}]: expected {e}, got {y}");
}
}
#[test]
fn single_tap_in_last_partition() {
let block_size = 4;
let ir = [1.0f32, 10.0, 100.0, 1000.0, 10000.0];
let blocks: Vec<&[f32]> = vec![
&[1.0, 0.0, 0.0, 0.0],
&[0.0, 0.0, 0.0, 0.0],
&[0.0, 0.0, 0.0, 0.0],
];
let out = run_blocks(&ir, block_size, &blocks);
let expected = [
1.0f32, 10.0, 100.0, 1000.0, 10000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ];
let peak = ir.iter().fold(0.0f32, |m, &x| m.max(x.abs()));
let tol = peak * 1e-4;
for (i, (e, y)) in expected.iter().zip(out.iter()).enumerate() {
let err = (e - y).abs();
assert!(err < tol, "y[{i}]: expected {e}, got {y} (abs err {err})");
}
}
#[test]
fn known_two_tap_convolution() {
let block_size = 4;
let ir = [1.0f32, 2.0, 0.0, 0.0];
let input = [1.0f32, 2.0, 3.0, 4.0];
let expected = [1.0f32, 4.0, 7.0, 10.0];
let out = run_blocks(&ir, block_size, &[&input]);
for (e, y) in expected.iter().zip(out.iter()) {
assert!((e - y).abs() < EPSILON, "expected {e}, got {y}");
}
}
#[test]
fn state_persists_across_blocks() {
let block_size = 4;
let ir = [1.0f32, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0];
let b1 = [1.0f32, 0.0, 0.0, 0.0];
let b2 = [0.0f32; 4];
let out = run_blocks(&ir, block_size, &[&b1, &b2]);
assert!((out[0] - 1.0).abs() < EPSILON, "y[0]={}", out[0]);
assert!((out[7] - 1.0).abs() < EPSILON, "y[7]={}", out[7]);
for (i, val) in out.iter().enumerate().take(7).skip(1) {
assert!(val.abs() < EPSILON, "y[{i}]={val}");
}
}
#[test]
fn reset_gives_same_result_as_fresh_engine() {
let block_size = 4;
let ir = [1.0f32, 0.5, 0.0, 0.0];
let mut engine = UniformPartitionEngine::new(&ir, block_size);
let mut scratch = vec![0.0f32; block_size];
engine.process(&[1.0f32; 4], &mut scratch);
engine.reset();
let input = [1.0f32, 2.0, 3.0, 4.0];
let mut after_reset = vec![0.0f32; block_size];
engine.process(&input, &mut after_reset);
let fresh = run_blocks(&ir, block_size, &[&input]);
for (e, y) in fresh.iter().zip(after_reset.iter()) {
assert!((e - y).abs() < EPSILON, "expected {e}, got {y}");
}
}
#[test]
fn load_ir_installs_new_coefficients() {
let block_size = 4;
let mut engine = UniformPartitionEngine::new(&[1.0f32, 0.0, 0.0, 0.0], block_size);
let mut scratch = vec![0.0f32; block_size];
engine.process(&[1.0f32; 4], &mut scratch);
engine.load_ir(&[3.0f32, 0.0, 0.0, 0.0]);
let mut out = vec![0.0f32; block_size];
engine.process(&[1.0f32; 4], &mut out);
for &y in &out {
assert!((y - 3.0).abs() < EPSILON, "expected 3.0, got {y}");
}
}
#[test]
fn process_accumulates_into_output() {
let block_size = 4;
let ir = [1.0f32, 0.0, 0.0, 0.0];
let mut engine = UniformPartitionEngine::new(&ir, block_size);
let mut output = [10.0f32; 4];
engine.process(&[1.0f32; 4], &mut output);
for &y in &output {
assert!((y - 11.0).abs() < EPSILON, "expected 11.0, got {y}");
}
}
}