use serde::{Deserialize, Serialize};
use tracing::debug;
use crate::delay::DelayLine;
use crate::error::{self, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct DampedComb {
delay: DelayLine,
delay_samples: f32,
feedback: f32,
damp1: f32,
damp2: f32,
#[serde(skip)]
filter_state: f32,
}
impl DampedComb {
fn new(delay_samples: usize, feedback: f32, damping: f32) -> Self {
Self {
delay: DelayLine::new(delay_samples),
delay_samples: delay_samples as f32,
feedback,
damp1: damping,
damp2: 1.0 - damping,
filter_state: 0.0,
}
}
#[inline]
fn process(&mut self, input: f32) -> f32 {
let delayed = self.delay.read(self.delay_samples);
self.filter_state =
crate::flush_denormal(delayed * self.damp2 + self.filter_state * self.damp1);
self.delay.write(input + self.filter_state * self.feedback);
delayed
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ReverbAllpass {
delay: DelayLine,
delay_samples: f32,
feedback: f32,
}
impl ReverbAllpass {
fn new(delay_samples: usize, feedback: f32) -> Self {
Self {
delay: DelayLine::new(delay_samples),
delay_samples: delay_samples as f32,
feedback,
}
}
#[inline]
fn process(&mut self, input: f32) -> f32 {
let delayed = self.delay.read(self.delay_samples);
let output = -self.feedback * input + delayed;
self.delay
.write(input + self.feedback * crate::flush_denormal(output));
output
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Reverb {
pre_delay: DelayLine,
pre_delay_samples: f32,
combs_l: [DampedComb; 4],
combs_r: [DampedComb; 4],
allpasses_l: [ReverbAllpass; 2],
allpasses_r: [ReverbAllpass; 2],
pub mix: f32,
pub width: f32,
sample_rate: f32,
}
const COMB_LENGTHS: [usize; 4] = [1117, 1187, 1277, 1361];
const ALLPASS_LENGTHS: [usize; 2] = [557, 443];
const STEREO_OFFSET: usize = 23;
impl Reverb {
pub fn new(
decay: f32,
damping: f32,
pre_delay_ms: f32,
mix: f32,
sample_rate: f32,
) -> Result<Self> {
if let Some(e) = error::validate_sample_rate(sample_rate) {
return Err(e);
}
debug!(
decay,
damping, pre_delay_ms, mix, sample_rate, "reverb created"
);
let scale = sample_rate / 44100.0;
let feedback = decay.clamp(0.0, 0.99);
let damp = damping.clamp(0.0, 1.0);
let pre_samples = (pre_delay_ms * sample_rate / 1000.0).max(0.0);
let make_comb = |base: usize, offset: usize| -> DampedComb {
let len = ((base + offset) as f32 * scale) as usize;
DampedComb::new(len.max(1), feedback, damp)
};
let make_ap = |base: usize, offset: usize| -> ReverbAllpass {
let len = ((base + offset) as f32 * scale) as usize;
ReverbAllpass::new(len.max(1), 0.5)
};
Ok(Self {
pre_delay: DelayLine::new((pre_samples as usize).max(1)),
pre_delay_samples: pre_samples,
combs_l: [
make_comb(COMB_LENGTHS[0], 0),
make_comb(COMB_LENGTHS[1], 0),
make_comb(COMB_LENGTHS[2], 0),
make_comb(COMB_LENGTHS[3], 0),
],
combs_r: [
make_comb(COMB_LENGTHS[0], STEREO_OFFSET),
make_comb(COMB_LENGTHS[1], STEREO_OFFSET),
make_comb(COMB_LENGTHS[2], STEREO_OFFSET),
make_comb(COMB_LENGTHS[3], STEREO_OFFSET),
],
allpasses_l: [
make_ap(ALLPASS_LENGTHS[0], 0),
make_ap(ALLPASS_LENGTHS[1], 0),
],
allpasses_r: [
make_ap(ALLPASS_LENGTHS[0], STEREO_OFFSET),
make_ap(ALLPASS_LENGTHS[1], STEREO_OFFSET),
],
mix: mix.clamp(0.0, 1.0),
width: 1.0,
sample_rate,
})
}
#[inline]
#[must_use]
pub fn process_sample(&mut self, input: f32) -> (f32, f32) {
self.pre_delay.write(input);
let delayed = self.pre_delay.read(self.pre_delay_samples);
let mut wet_l = 0.0f32;
let mut wet_r = 0.0f32;
for comb in &mut self.combs_l {
wet_l += comb.process(delayed);
}
for comb in &mut self.combs_r {
wet_r += comb.process(delayed);
}
wet_l *= 0.25; wet_r *= 0.25;
for ap in &mut self.allpasses_l {
wet_l = ap.process(wet_l);
}
for ap in &mut self.allpasses_r {
wet_r = ap.process(wet_r);
}
let w = self.width;
let out_l = wet_l * (0.5 + 0.5 * w) + wet_r * (0.5 - 0.5 * w);
let out_r = wet_r * (0.5 + 0.5 * w) + wet_l * (0.5 - 0.5 * w);
let dry = 1.0 - self.mix;
(
input * dry + out_l * self.mix,
input * dry + out_r * self.mix,
)
}
pub fn process_buffer(&mut self, input: &[f32], left: &mut [f32], right: &mut [f32]) {
for (i, &s) in input.iter().enumerate() {
let (l, r) = self.process_sample(s);
if i < left.len() {
left[i] = l;
}
if i < right.len() {
right[i] = r;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_reverb_produces_output() {
let mut rev = Reverb::new(0.8, 0.3, 10.0, 1.0, 44100.0).unwrap();
let (l, r) = rev.process_sample(1.0);
assert!(l.is_finite());
assert!(r.is_finite());
let mut has_tail = false;
for _ in 0..10000 {
let (l, _r) = rev.process_sample(0.0);
if l.abs() > 0.001 {
has_tail = true;
break;
}
}
assert!(has_tail, "reverb should produce a tail after impulse");
}
#[test]
fn test_reverb_stereo_differs() {
let mut rev = Reverb::new(0.8, 0.3, 10.0, 1.0, 44100.0).unwrap();
rev.width = 1.0;
let _ = rev.process_sample(1.0);
let mut diff_found = false;
for _ in 0..5000 {
let (l, r) = rev.process_sample(0.0);
if (l - r).abs() > 0.001 {
diff_found = true;
break;
}
}
assert!(diff_found, "stereo reverb should have L/R differences");
}
#[test]
fn test_reverb_dry_passthrough() {
let mut rev = Reverb::new(0.5, 0.5, 0.0, 0.0, 44100.0).unwrap();
let (l, r) = rev.process_sample(0.7);
assert!((l - 0.7).abs() < 0.01, "mix=0 should pass dry signal");
assert!((r - 0.7).abs() < 0.01);
}
#[test]
fn test_reverb_no_blow_up() {
let mut rev = Reverb::new(0.99, 0.0, 0.0, 1.0, 44100.0).unwrap();
let _ = rev.process_sample(1.0);
for _ in 0..100_000 {
let (l, r) = rev.process_sample(0.0);
assert!(l.abs() < 10.0, "reverb should not blow up: {l}");
assert!(r.abs() < 10.0, "reverb should not blow up: {r}");
}
}
#[test]
fn test_serde_roundtrip() {
let rev = Reverb::new(0.7, 0.4, 15.0, 0.5, 44100.0).unwrap();
let json = serde_json::to_string(&rev).unwrap();
let back: Reverb = serde_json::from_str(&json).unwrap();
assert!((rev.mix - back.mix).abs() < f32::EPSILON);
}
}