use crate::{AudioEffect, EffectError, Result};
use oxifft::Complex;
pub struct ConvolutionReverb {
ir_fft: Vec<Complex<f32>>,
#[allow(dead_code)]
ir_length: usize,
fft_size: usize,
input_buffer: Vec<f32>,
input_fft: Vec<Complex<f32>>,
output_buffer: Vec<f32>,
tail_buffer: Vec<f32>,
input_pos: usize,
output_pos: usize,
wet: f32,
dry: f32,
#[allow(dead_code)]
sample_rate: f32,
}
impl ConvolutionReverb {
pub fn new(impulse_response: &[f32], sample_rate: f32) -> Result<Self> {
if impulse_response.is_empty() {
return Err(EffectError::InvalidParameter(
"Impulse response cannot be empty".into(),
));
}
if impulse_response.len() > 100_000 {
return Err(EffectError::InvalidParameter(
"Impulse response too long (max 100k samples)".into(),
));
}
let ir_length = impulse_response.len();
let fft_size = (ir_length * 2).next_power_of_two();
let mut ir_padded: Vec<Complex<f32>> = impulse_response
.iter()
.map(|&x| Complex::new(x, 0.0))
.collect();
ir_padded.resize(fft_size, Complex::new(0.0, 0.0));
let ir_fft = oxifft::fft(&ir_padded);
Ok(Self {
ir_fft,
ir_length,
fft_size,
input_buffer: vec![0.0; fft_size],
input_fft: vec![Complex::new(0.0, 0.0); fft_size],
output_buffer: vec![0.0; fft_size],
tail_buffer: vec![0.0; fft_size],
input_pos: 0,
output_pos: 0,
wet: 0.5,
dry: 0.5,
sample_rate,
})
}
pub fn set_wet(&mut self, wet: f32) {
self.wet = wet.clamp(0.0, 1.0);
}
pub fn set_dry(&mut self, dry: f32) {
self.dry = dry.clamp(0.0, 1.0);
}
fn process_block(&mut self) {
for (i, &sample) in self.input_buffer.iter().enumerate() {
self.input_fft[i] = Complex::new(sample, 0.0);
}
let fft_result = oxifft::fft(&self.input_fft);
let result_fft_freq: Vec<Complex<f32>> = fft_result
.iter()
.zip(self.ir_fft.iter())
.map(|(&a, &b)| a * b)
.collect();
let result_fft = oxifft::ifft(&result_fft_freq);
#[allow(clippy::cast_precision_loss)]
let scale = 1.0 / self.fft_size as f32;
for (i, val) in result_fft.iter().enumerate().take(self.fft_size) {
self.output_buffer[i] = val.re * scale;
}
for i in 0..self.fft_size {
self.output_buffer[i] += self.tail_buffer[i];
}
for i in 0..self.fft_size / 2 {
self.tail_buffer[i] = self.output_buffer[self.fft_size / 2 + i];
}
for i in self.fft_size / 2..self.fft_size {
self.tail_buffer[i] = 0.0;
}
self.output_pos = 0;
}
}
impl AudioEffect for ConvolutionReverb {
fn process_sample(&mut self, input: f32) -> f32 {
self.input_buffer[self.input_pos] = input;
self.input_pos += 1;
if self.input_pos >= self.fft_size / 2 {
self.process_block();
self.input_pos = 0;
for i in self.fft_size / 2..self.fft_size {
self.input_buffer[i] = 0.0;
}
}
let wet_sample = if self.output_pos < self.output_buffer.len() {
self.output_buffer[self.output_pos]
} else {
0.0
};
self.output_pos += 1;
wet_sample * self.wet + input * self.dry
}
fn reset(&mut self) {
self.input_buffer.fill(0.0);
self.output_buffer.fill(0.0);
self.tail_buffer.fill(0.0);
self.input_fft.fill(Complex::new(0.0, 0.0));
self.input_pos = 0;
self.output_pos = 0;
}
fn latency_samples(&self) -> usize {
self.fft_size / 2
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_convolution_reverb_creation() {
let ir = vec![1.0, 0.5, 0.25, 0.125]; let reverb = ConvolutionReverb::new(&ir, 48000.0);
assert!(reverb.is_ok());
}
#[test]
fn test_convolution_reverb_empty_ir() {
let ir: Vec<f32> = vec![];
let result = ConvolutionReverb::new(&ir, 48000.0);
assert!(result.is_err());
}
#[test]
fn test_convolution_reverb_process() {
let ir = vec![1.0; 100]; let mut reverb = ConvolutionReverb::new(&ir, 48000.0).expect("test expectation failed");
let output = reverb.process_sample(1.0);
assert!(output.is_finite());
for _ in 0..1000 {
let out = reverb.process_sample(0.0);
assert!(out.is_finite());
}
}
#[test]
fn test_convolution_wet_dry() {
let ir = vec![0.5; 50];
let mut reverb = ConvolutionReverb::new(&ir, 48000.0).expect("test expectation failed");
reverb.set_wet(0.0);
reverb.set_dry(1.0);
for _ in 0..100 {
reverb.process_sample(1.0);
}
let output = reverb.process_sample(1.0);
assert!((output - 1.0).abs() < 0.5);
}
}