#![allow(clippy::cast_precision_loss)]
use crate::{utils::delay_line::DelayLine, AudioEffect, ReverbConfig};
const NUM_COMBS: usize = 8;
const NUM_ALLPASSES: usize = 4;
const COMB_DELAYS_L: [usize; NUM_COMBS] = [1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617];
const COMB_DELAYS_R: [usize; NUM_COMBS] = [
1116 + 29, 1188 + 37, 1277 + 43, 1356 + 53, 1422 + 59, 1491 + 67, 1557 + 71, 1617 + 79, ];
const ALLPASS_DELAYS_L: [usize; NUM_ALLPASSES] = [556, 441, 341, 225];
const ALLPASS_DELAYS_R: [usize; NUM_ALLPASSES] = [
556 + 31, 441 + 41, 341 + 47, 225 + 61, ];
#[derive(Debug, Clone)]
struct CombFilter {
delay_line: DelayLine,
delay: usize,
filterstore: f32,
feedback: f32,
damp1: f32,
damp2: f32,
}
impl CombFilter {
fn new(size: usize) -> Self {
let size = size.max(1);
Self {
delay_line: DelayLine::new(size + 1),
delay: size,
filterstore: 0.0,
feedback: 0.0,
damp1: 0.0,
damp2: 0.0,
}
}
#[inline]
fn process(&mut self, input: f32) -> f32 {
let output = self.delay_line.read(self.delay);
self.filterstore = output * self.damp2 + self.filterstore * self.damp1;
self.delay_line
.write(input + self.filterstore * self.feedback);
output
}
fn set_feedback(&mut self, val: f32) {
self.feedback = val;
}
fn set_damp(&mut self, val: f32) {
self.damp1 = val;
self.damp2 = 1.0 - val;
}
fn clear(&mut self) {
self.delay_line.clear();
self.filterstore = 0.0;
}
}
#[derive(Debug, Clone)]
struct AllPass {
delay_line: DelayLine,
delay: usize,
diffusion: f32,
}
impl AllPass {
fn new(size: usize, diffusion: f32) -> Self {
let size = size.max(1);
Self {
delay_line: DelayLine::new(size + 1),
delay: size,
diffusion,
}
}
#[inline]
fn process(&mut self, input: f32) -> f32 {
let bufout = self.delay_line.read(self.delay);
let output = -input + bufout;
self.delay_line.write(input + bufout * self.diffusion);
output
}
fn clear(&mut self) {
self.delay_line.clear();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StereoMode {
MonoToStereo,
TrueStereo,
}
pub struct Freeverb {
combs_l: Vec<CombFilter>,
allpasses_l: Vec<AllPass>,
combs_r: Vec<CombFilter>,
allpasses_r: Vec<AllPass>,
config: ReverbConfig,
room_size: f32,
damping: f32,
wet1: f32,
wet2: f32,
dry: f32,
predelay_buffer_l: DelayLine,
predelay_buffer_r: DelayLine,
predelay_samples: usize,
cross_feed: f32,
stereo_mode: StereoMode,
sample_rate: f32,
}
impl Freeverb {
const DIFFUSION_L: [f32; NUM_ALLPASSES] = [0.50, 0.50, 0.50, 0.50];
const DIFFUSION_R: [f32; NUM_ALLPASSES] = [0.45, 0.52, 0.48, 0.55];
#[must_use]
pub fn new(config: ReverbConfig, sample_rate: f32) -> Self {
Self::with_stereo_mode(config, sample_rate, StereoMode::TrueStereo)
}
#[must_use]
pub fn with_stereo_mode(
config: ReverbConfig,
sample_rate: f32,
stereo_mode: StereoMode,
) -> Self {
let scale_factor = sample_rate / 44100.0;
let combs_l: Vec<CombFilter> = COMB_DELAYS_L
.iter()
.map(|&delay| {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let scaled_delay = (delay as f32 * scale_factor) as usize;
CombFilter::new(scaled_delay.max(1))
})
.collect();
let combs_r: Vec<CombFilter> = COMB_DELAYS_R
.iter()
.map(|&delay| {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let scaled_delay = (delay as f32 * scale_factor) as usize;
CombFilter::new(scaled_delay.max(1))
})
.collect();
let allpasses_l: Vec<AllPass> = ALLPASS_DELAYS_L
.iter()
.zip(Self::DIFFUSION_L.iter())
.map(|(&delay, &diffusion)| {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let scaled_delay = (delay as f32 * scale_factor) as usize;
AllPass::new(scaled_delay.max(1), diffusion)
})
.collect();
let allpasses_r: Vec<AllPass> = ALLPASS_DELAYS_R
.iter()
.zip(Self::DIFFUSION_R.iter())
.map(|(&delay, &diffusion)| {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let scaled_delay = (delay as f32 * scale_factor) as usize;
AllPass::new(scaled_delay.max(1), diffusion)
})
.collect();
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let predelay_samples = ((config.predelay_ms * sample_rate) / 1000.0) as usize;
let predelay_size = predelay_samples.max(1);
let predelay_buffer_l = DelayLine::new(predelay_size + 1);
let predelay_buffer_r = DelayLine::new(predelay_size + 1);
let mut reverb = Self {
combs_l,
combs_r,
allpasses_l,
allpasses_r,
config,
room_size: 0.0,
damping: 0.0,
wet1: 0.0,
wet2: 0.0,
dry: 0.0,
predelay_buffer_l,
predelay_buffer_r,
predelay_samples,
cross_feed: 0.15, stereo_mode,
sample_rate,
};
reverb.update_parameters();
reverb
}
fn update_parameters(&mut self) {
const ROOM_OFFSET: f32 = 0.7;
const ROOM_SCALE: f32 = 0.28;
const DAMP_SCALE: f32 = 0.4;
self.room_size = self.config.room_size * ROOM_SCALE + ROOM_OFFSET;
self.damping = self.config.damping * DAMP_SCALE;
let wet = self.config.wet;
self.dry = self.config.dry;
let width = self.config.width;
self.wet1 = wet * (width / 2.0 + 0.5);
self.wet2 = wet * ((1.0 - width) / 2.0);
for comb in &mut self.combs_l {
comb.set_feedback(self.room_size);
comb.set_damp(self.damping);
}
for comb in &mut self.combs_r {
comb.set_feedback(self.room_size);
comb.set_damp(self.damping);
}
}
pub fn set_room_size(&mut self, room_size: f32) {
self.config.room_size = room_size.clamp(0.0, 1.0);
self.update_parameters();
}
pub fn set_damping(&mut self, damping: f32) {
self.config.damping = damping.clamp(0.0, 1.0);
self.update_parameters();
}
pub fn set_wet(&mut self, wet: f32) {
self.config.wet = wet.clamp(0.0, 1.0);
self.update_parameters();
}
pub fn set_dry(&mut self, dry: f32) {
self.config.dry = dry.clamp(0.0, 1.0);
self.update_parameters();
}
pub fn set_width(&mut self, width: f32) {
self.config.width = width.clamp(0.0, 1.0);
self.update_parameters();
}
pub fn set_cross_feed(&mut self, amount: f32) {
self.cross_feed = amount.clamp(0.0, 1.0);
}
#[must_use]
pub fn cross_feed(&self) -> f32 {
self.cross_feed
}
pub fn set_stereo_mode(&mut self, mode: StereoMode) {
self.stereo_mode = mode;
}
#[must_use]
pub fn stereo_mode(&self) -> StereoMode {
self.stereo_mode
}
fn process_sample_internal(&mut self, input_l: f32, input_r: f32) -> (f32, f32) {
let (delayed_l, delayed_r) = if self.predelay_samples > 0 {
let del_l = self.predelay_buffer_l.read(self.predelay_samples);
let del_r = self.predelay_buffer_r.read(self.predelay_samples);
match self.stereo_mode {
StereoMode::MonoToStereo => {
let mono = (input_l + input_r) * 0.5;
self.predelay_buffer_l.write(mono);
self.predelay_buffer_r.write(mono);
}
StereoMode::TrueStereo => {
let cf = self.cross_feed;
let mono = (input_l + input_r) * 0.5;
let direct_l = input_l * (1.0 - cf) + mono * cf;
let direct_r = input_r * (1.0 - cf) + mono * cf;
self.predelay_buffer_l.write(direct_l);
self.predelay_buffer_r.write(direct_r);
}
}
(del_l, del_r)
} else {
match self.stereo_mode {
StereoMode::MonoToStereo => {
let mono = (input_l + input_r) * 0.5;
(mono, mono)
}
StereoMode::TrueStereo => {
let cf = self.cross_feed;
let direct_l = input_l * (1.0 - cf) + input_r * cf;
let direct_r = input_r * (1.0 - cf) + input_l * cf;
(direct_l, direct_r)
}
}
};
let mut out_l = 0.0;
let mut out_r = 0.0;
for comb in &mut self.combs_l {
out_l += comb.process(delayed_l);
}
for comb in &mut self.combs_r {
out_r += comb.process(delayed_r);
}
for allpass in &mut self.allpasses_l {
out_l = allpass.process(out_l);
}
for allpass in &mut self.allpasses_r {
out_r = allpass.process(out_r);
}
let wet_l = out_l * self.wet1 + out_r * self.wet2;
let wet_r = out_r * self.wet1 + out_l * self.wet2;
let output_l = wet_l + input_l * self.dry;
let output_r = wet_r + input_r * self.dry;
(output_l, output_r)
}
}
impl AudioEffect for Freeverb {
const EFFECT_ID: &'static str = "freeverb";
fn process_sample(&mut self, input: f32) -> f32 {
let (left, _right) = self.process_sample_internal(input, input);
left
}
fn process_sample_stereo(&mut self, left: f32, right: f32) -> (f32, f32) {
self.process_sample_internal(left, right)
}
fn set_wet_dry(&mut self, wet: f32) {
let w = wet.clamp(0.0, 1.0);
self.set_wet(w);
self.set_dry(1.0 - w);
}
fn wet_dry(&self) -> f32 {
self.config.wet
}
fn reset(&mut self) {
for comb in &mut self.combs_l {
comb.clear();
}
for comb in &mut self.combs_r {
comb.clear();
}
for ap in &mut self.allpasses_l {
ap.clear();
}
for ap in &mut self.allpasses_r {
ap.clear();
}
self.predelay_buffer_l.clear();
self.predelay_buffer_r.clear();
}
fn set_sample_rate(&mut self, sample_rate: f32) {
self.sample_rate = sample_rate;
let mode = self.stereo_mode;
*self = Self::with_stereo_mode(self.config.clone(), sample_rate, mode);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_freeverb_creation() {
let config = ReverbConfig::default();
let reverb = Freeverb::new(config, 48000.0);
assert_eq!(reverb.combs_l.len(), NUM_COMBS);
assert_eq!(reverb.allpasses_l.len(), NUM_ALLPASSES);
}
#[test]
fn test_freeverb_process() {
let config = ReverbConfig::default();
let mut reverb = Freeverb::new(config, 48000.0);
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_freeverb_stereo() {
let config = ReverbConfig::default().with_width(1.0);
let mut reverb = Freeverb::new(config, 48000.0);
let (out_l, out_r) = reverb.process_sample_stereo(1.0, 0.0);
assert!(out_l != out_r);
}
#[test]
fn test_freeverb_parameters() {
let config = ReverbConfig::default();
let mut reverb = Freeverb::new(config, 48000.0);
reverb.set_room_size(0.9);
reverb.set_damping(0.3);
reverb.set_wet(0.5);
reverb.set_dry(0.5);
assert_eq!(reverb.config.room_size, 0.9);
assert_eq!(reverb.config.damping, 0.3);
}
#[test]
fn test_freeverb_reset() {
let config = ReverbConfig::default();
let mut reverb = Freeverb::new(config, 48000.0);
reverb.process_sample(1.0);
for _ in 0..100 {
reverb.process_sample(0.0);
}
reverb.reset();
let output = reverb.process_sample(0.0);
assert!(output.abs() < 0.001);
}
#[test]
fn test_true_stereo_decorrelation() {
let config = ReverbConfig::default().with_width(1.0);
let mut reverb = Freeverb::new(config, 48000.0);
assert_eq!(reverb.stereo_mode(), StereoMode::TrueStereo);
let (out_l, out_r) = reverb.process_sample_stereo(1.0, 1.0);
assert!(out_l.is_finite());
assert!(out_r.is_finite());
let mut diff_sum = 0.0f32;
for _ in 0..2000 {
let (l, r) = reverb.process_sample_stereo(0.0, 0.0);
diff_sum += (l - r).abs();
}
assert!(
diff_sum > 0.01,
"True stereo should produce decorrelated output, diff_sum={diff_sum}"
);
}
#[test]
fn test_mono_to_stereo_mode() {
let config = ReverbConfig::default().with_width(1.0);
let mut reverb = Freeverb::with_stereo_mode(config, 48000.0, StereoMode::MonoToStereo);
assert_eq!(reverb.stereo_mode(), StereoMode::MonoToStereo);
let (l, r) = reverb.process_sample_stereo(1.0, 1.0);
assert!(l.is_finite());
assert!(r.is_finite());
}
#[test]
fn test_cross_feed_zero_fully_independent() {
let config = ReverbConfig::default().with_width(1.0);
let mut reverb = Freeverb::new(config, 48000.0);
reverb.set_cross_feed(0.0);
let (l, _r) = reverb.process_sample_stereo(1.0, 0.0);
assert!(l.is_finite());
let mut r_energy = 0.0f32;
for _ in 0..500 {
let (_l, r) = reverb.process_sample_stereo(0.0, 0.0);
r_energy += r * r;
}
assert!(r_energy.is_finite());
}
#[test]
fn test_cross_feed_one_shared() {
let config = ReverbConfig::default()
.with_width(1.0)
.with_wet(0.5)
.with_room_size(0.8);
let mut reverb = Freeverb::new(config, 48000.0);
reverb.set_cross_feed(1.0);
let mut l_sum = 0.0f32;
let mut r_sum = 0.0f32;
for _ in 0..4000 {
let (l, r) = reverb.process_sample_stereo(0.5, 0.0);
l_sum += l.abs();
r_sum += r.abs();
}
assert!(
l_sum > 0.001,
"Left should have energy with cross-feed=1: {l_sum}"
);
assert!(
r_sum > 0.001,
"Right should have energy with cross-feed=1: {r_sum}"
);
}
#[test]
fn test_prime_offsets_differ() {
for i in 0..NUM_COMBS {
assert_ne!(
COMB_DELAYS_L[i], COMB_DELAYS_R[i],
"Comb delay {i} should differ between L and R"
);
}
for i in 0..NUM_ALLPASSES {
assert_ne!(
ALLPASS_DELAYS_L[i], ALLPASS_DELAYS_R[i],
"Allpass delay {i} should differ between L and R"
);
}
let offsets_comb: Vec<usize> = COMB_DELAYS_R
.iter()
.zip(COMB_DELAYS_L.iter())
.map(|(r, l)| r - l)
.collect();
for i in 0..offsets_comb.len() {
for j in (i + 1)..offsets_comb.len() {
assert_ne!(
offsets_comb[i], offsets_comb[j],
"Comb offsets should be unique primes"
);
}
}
}
#[test]
fn test_diffusion_coefficients_differ() {
for i in 0..NUM_ALLPASSES {
assert!(
(Freeverb::DIFFUSION_L[i] - Freeverb::DIFFUSION_R[i]).abs() > 1e-6 || i == 0, "Diffusion coefficients should generally differ between L and R"
);
}
}
#[test]
fn test_true_stereo_asymmetric_input() {
let config = ReverbConfig::default().with_width(1.0).with_room_size(0.8);
let mut reverb = Freeverb::new(config, 48000.0);
reverb.set_cross_feed(0.1);
reverb.process_sample_stereo(1.0, 0.0);
let mut l_energy = 0.0f32;
let mut r_energy = 0.0f32;
for _ in 0..4000 {
let (l, r) = reverb.process_sample_stereo(0.0, 0.0);
l_energy += l * l;
r_energy += r * r;
}
assert!(
l_energy > r_energy,
"Left should have more energy with left-only input: L={l_energy}, R={r_energy}"
);
assert!(
r_energy > 1e-6,
"Right should have some energy from cross-feed"
);
}
#[test]
fn test_energy_conservation() {
let config = ReverbConfig::default()
.with_room_size(0.5)
.with_wet(0.3)
.with_dry(0.7);
let mut reverb = Freeverb::new(config, 48000.0);
let mut input_energy = 0.0f32;
let mut output_energy = 0.0f32;
for i in 0..8000 {
#[allow(clippy::cast_precision_loss)]
let input = (i as f32 * 0.1).sin() * 0.5;
input_energy += input * input;
let (l, r) = reverb.process_sample_stereo(input, input);
output_energy += (l * l + r * r) * 0.5;
}
for _ in 0..48000 {
let (l, r) = reverb.process_sample_stereo(0.0, 0.0);
output_energy += (l * l + r * r) * 0.5;
}
assert!(output_energy.is_finite(), "Output energy should be finite");
assert!(
output_energy <= input_energy * 50.0,
"output energy {output_energy} exceeded input energy {input_energy} * 50 (reverb unstable)"
);
}
#[test]
fn test_set_stereo_mode() {
let config = ReverbConfig::default();
let mut reverb = Freeverb::new(config, 48000.0);
assert_eq!(reverb.stereo_mode(), StereoMode::TrueStereo);
reverb.set_stereo_mode(StereoMode::MonoToStereo);
assert_eq!(reverb.stereo_mode(), StereoMode::MonoToStereo);
}
#[test]
fn test_predelay_with_true_stereo() {
let config = ReverbConfig::default().with_predelay(20.0);
let mut reverb = Freeverb::new(config, 48000.0);
let (l, r) = reverb.process_sample_stereo(1.0, 1.0);
assert!(l.is_finite());
assert!(r.is_finite());
for _ in 0..2000 {
let (l, r) = reverb.process_sample_stereo(0.0, 0.0);
assert!(l.is_finite());
assert!(r.is_finite());
}
}
#[test]
fn delay_line_migration_output_bounded_and_decaying() {
let config = ReverbConfig::default()
.with_room_size(0.5)
.with_wet(0.8)
.with_dry(0.2);
let mut reverb = Freeverb::new(config, 48000.0);
reverb.process_sample(1.0);
let mut total_energy = 0.0_f32;
for i in 0..48_000usize {
let s = reverb.process_sample(0.0);
assert!(
s.is_finite(),
"freeverb sample {i} not finite after migration: {s}"
);
total_energy += s * s;
}
assert!(
total_energy > 1e-4,
"reverb tail should carry non-zero energy: {total_energy}"
);
assert!(
total_energy < 1.0e6,
"reverb tail energy is diverging (migration bug?): {total_energy}"
);
}
}