use std::sync::Arc;
use rustfft::{Fft, FftPlanner, num_complex::Complex};
pub struct OfflineConvolver {
ir: Vec<f32>,
fft_size: usize,
ir_spectrum: Vec<Complex<f64>>,
fft: Arc<dyn Fft<f64>>,
ifft: Arc<dyn Fft<f64>>,
}
impl OfflineConvolver {
pub fn new(ir: &[f32]) -> Self {
let fft_size = (2 * ir.len().max(1)).next_power_of_two().max(2);
let mut planner = FftPlanner::<f64>::new();
let fft = planner.plan_fft_forward(fft_size);
let ifft = planner.plan_fft_inverse(fft_size);
let mut ir_spectrum = vec![Complex::default(); fft_size];
for (bin, &sample) in ir_spectrum.iter_mut().zip(ir.iter()) {
*bin = Complex::new(sample as f64, 0.0);
}
fft.process(&mut ir_spectrum);
Self {
ir: ir.to_vec(),
fft_size,
ir_spectrum,
fft,
ifft,
}
}
#[inline]
pub fn ir(&self) -> &[f32] {
&self.ir
}
#[inline]
pub fn ir_len(&self) -> usize {
self.ir.len()
}
#[inline]
pub fn fft_size(&self) -> usize {
self.fft_size
}
pub fn convolve(&self, input: &[f32]) -> Vec<f32> {
if self.ir.is_empty() || input.is_empty() {
return Vec::new();
}
let ir_len = self.ir.len();
let out_len = input.len() + ir_len - 1;
let mut output = vec![0.0f64; out_len];
let block_size = self.fft_size - ir_len + 1;
let mut buf = vec![Complex::<f64>::default(); self.fft_size];
let norm = 1.0f64 / self.fft_size as f64;
let mut pos = 0;
while pos < input.len() {
let end = (pos + block_size).min(input.len());
let chunk = &input[pos..end];
let chunk_len = chunk.len();
for (bin, &sample) in buf.iter_mut().zip(chunk.iter()) {
*bin = Complex::new(sample as f64, 0.0);
}
for bin in buf.iter_mut().skip(chunk_len) {
*bin = Complex::default();
}
self.fft.process(&mut buf);
for (bin, &h) in buf.iter_mut().zip(self.ir_spectrum.iter()) {
*bin *= h;
}
self.ifft.process(&mut buf);
let contrib_len = (chunk_len + ir_len - 1).min(self.fft_size);
for (i, bin) in buf.iter().take(contrib_len).enumerate() {
let idx = pos + i;
if idx < out_len {
output[idx] += bin.re * norm;
}
}
pos += block_size;
}
output.into_iter().map(|x| x as f32).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn naive(ir: &[f32], input: &[f32]) -> Vec<f64> {
if ir.is_empty() || input.is_empty() {
return Vec::new();
}
let out_len = input.len() + ir.len() - 1;
let mut out = vec![0.0f64; out_len];
for (idx, sample) in out.iter_mut().enumerate() {
for (tap_idx, &tap) in ir.iter().enumerate() {
if idx >= tap_idx && idx - tap_idx < input.len() {
*sample += tap as f64 * input[idx - tap_idx] as f64;
}
}
}
out
}
fn assert_close(got: &[f32], expected: &[f64], tol: f64) {
assert_eq!(got.len(), expected.len(), "length mismatch");
for (idx, (&value, &target)) in got.iter().zip(expected.iter()).enumerate() {
let err = (value as f64 - target).abs();
assert!(
err < tol,
"sample {idx}: expected {target}, got {value} (abs err {err} > tol {tol})"
);
}
}
#[test]
fn empty_ir_returns_empty() {
let conv = OfflineConvolver::new(&[]);
assert_eq!(conv.ir_len(), 0);
assert!(conv.convolve(&[1.0, 2.0, 3.0]).is_empty());
}
#[test]
fn empty_input_returns_empty() {
let conv = OfflineConvolver::new(&[1.0, 2.0, 3.0]);
assert!(conv.convolve(&[]).is_empty());
}
#[test]
fn fft_size_is_power_of_two_covering_ir() {
let conv = OfflineConvolver::new(&[1.0; 10]);
assert!(conv.fft_size() >= 20);
assert!(conv.fft_size().is_power_of_two());
}
#[test]
fn unit_impulse_ir_is_identity() {
let conv = OfflineConvolver::new(&[1.0]);
let input = [1.0f32, 2.0, 3.0, 4.0];
let out = conv.convolve(&input);
assert_eq!(out.len(), input.len());
for (idx, (&value, &target)) in out.iter().zip(input.iter()).enumerate() {
assert!(
(value - target).abs() < 1e-6,
"sample {idx}: expected {target}, got {value}"
);
}
}
#[test]
fn one_sample_delay_ir() {
let conv = OfflineConvolver::new(&[0.0, 1.0]);
let input = [1.0f32, 2.0, 3.0, 4.0];
let out = conv.convolve(&input);
let expected = [0.0f32, 1.0, 2.0, 3.0, 4.0];
assert_eq!(out.len(), expected.len());
for (idx, (&value, &target)) in out.iter().zip(expected.iter()).enumerate() {
assert!(
(value - target).abs() < 1e-6,
"sample {idx}: expected {target}, got {value}"
);
}
}
#[test]
fn known_two_tap_convolution() {
let conv = OfflineConvolver::new(&[1.0, 2.0]);
let input = [1.0f32, 2.0, 3.0, 4.0];
let out = conv.convolve(&input);
let expected = [1.0f32, 4.0, 7.0, 10.0, 8.0];
assert_eq!(out.len(), expected.len());
for (idx, (&value, &target)) in out.iter().zip(expected.iter()).enumerate() {
assert!(
(value - target).abs() < 1e-6,
"sample {idx}: expected {target}, got {value}"
);
}
}
#[test]
fn output_length_is_full_linear_convolution() {
let ir = [1.0f32, 2.0, 3.0];
let input = [0.5f32, 1.5, 2.5, 3.5, 4.5];
let conv = OfflineConvolver::new(&ir);
let out = conv.convolve(&input);
assert_eq!(out.len(), input.len() + ir.len() - 1);
}
#[test]
fn matches_naive_reference_short_input() {
let ir: Vec<f32> = (0..17).map(|i| ((i as f32) * 0.3).sin() * 0.5).collect();
let input: Vec<f32> = (0..64).map(|i| ((i as f32) * 0.17).sin()).collect();
let conv = OfflineConvolver::new(&ir);
let out = conv.convolve(&input);
let reference = naive(&ir, &input);
assert_close(&out, &reference, 1e-6);
}
#[test]
fn overlap_add_covers_multi_block_input() {
let ir = [1.0f32, -0.5, 0.25];
let conv = OfflineConvolver::new(&ir);
let input: Vec<f32> = (0..1000).map(|i| (i as f32 * 0.11).cos()).collect();
let out = conv.convolve(&input);
let reference = naive(&ir, &input);
assert_close(&out, &reference, 1e-5);
}
#[test]
fn partial_last_chunk_is_handled() {
let ir = [1.0f32, 0.5, 0.25, 0.125];
let conv = OfflineConvolver::new(&ir);
let input: Vec<f32> = (0..13).map(|i| (i + 1) as f32).collect();
let out = conv.convolve(&input);
let reference = naive(&ir, &input);
assert_close(&out, &reference, 1e-5);
}
#[test]
fn long_ir_matches_reference() {
let ir: Vec<f32> = (0..129)
.map(|i| ((i as f32) * 0.05).sin() * 0.1 - ((i as f32) * 0.02).cos() * 0.05)
.collect();
let input: Vec<f32> = (0..300).map(|i| ((i as f32) * 0.07).sin()).collect();
let conv = OfflineConvolver::new(&ir);
let out = conv.convolve(&input);
let reference = naive(&ir, &input);
assert_close(&out, &reference, 1e-5);
}
#[test]
fn ir_accessor_returns_original_coefficients() {
let src = [0.1f32, -0.2, 0.3, -0.4];
let conv = OfflineConvolver::new(&src);
assert_eq!(conv.ir(), &src);
assert_eq!(conv.ir_len(), src.len());
}
}