use crate::{
utils::{DelayLine, FractionalDelayLine, InterpolationMode, Lfo, LfoWaveform},
AudioEffect, ReverbConfig,
};
pub struct PlateReverb {
early_delays_l: Vec<DelayLine>,
early_delays_r: Vec<DelayLine>,
diffusion_l: Vec<FractionalDelayLine>,
diffusion_r: Vec<FractionalDelayLine>,
tank_delay_l: DelayLine,
tank_delay_r: DelayLine,
mod_lfo1: Lfo,
mod_lfo2: Lfo,
damp_l: f32,
damp_r: f32,
damping_coeff: f32,
predelay_buffer: Vec<f32>,
predelay_write_pos: usize,
predelay_samples: usize,
config: ReverbConfig,
#[allow(dead_code)]
sample_rate: f32,
}
impl PlateReverb {
#[must_use]
pub fn new(config: ReverbConfig, sample_rate: f32) -> Self {
let early_times_l = [13.0, 19.0, 29.0, 37.0];
let early_times_r = [17.0, 23.0, 31.0, 41.0];
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let early_delays_l: Vec<DelayLine> = early_times_l
.iter()
.map(|&ms| {
let samples = ((ms * sample_rate) / 1000.0) as usize;
DelayLine::new(samples.max(1))
})
.collect();
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let early_delays_r: Vec<DelayLine> = early_times_r
.iter()
.map(|&ms| {
let samples = ((ms * sample_rate) / 1000.0) as usize;
DelayLine::new(samples.max(1))
})
.collect();
let diffusion_times_l = [5.0, 7.0, 11.0, 13.0];
let diffusion_times_r = [6.0, 8.0, 12.0, 14.0];
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let diffusion_l: Vec<FractionalDelayLine> = diffusion_times_l
.iter()
.map(|&ms| {
let samples = ((ms * sample_rate) / 1000.0) as usize;
FractionalDelayLine::new(samples.max(1), InterpolationMode::Linear)
})
.collect();
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let diffusion_r: Vec<FractionalDelayLine> = diffusion_times_r
.iter()
.map(|&ms| {
let samples = ((ms * sample_rate) / 1000.0) as usize;
FractionalDelayLine::new(samples.max(1), InterpolationMode::Linear)
})
.collect();
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let tank_delay_l = DelayLine::new(((47.0 * sample_rate) / 1000.0) as usize);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let tank_delay_r = DelayLine::new(((53.0 * sample_rate) / 1000.0) as usize);
let mod_lfo1 = Lfo::new(0.3, sample_rate, LfoWaveform::Sine);
let mod_lfo2 = Lfo::new(0.37, sample_rate, LfoWaveform::Triangle);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let predelay_samples = ((config.predelay_ms * sample_rate) / 1000.0) as usize;
let predelay_buffer = vec![0.0; predelay_samples.max(1)];
let mut plate = Self {
early_delays_l,
early_delays_r,
diffusion_l,
diffusion_r,
tank_delay_l,
tank_delay_r,
mod_lfo1,
mod_lfo2,
damp_l: 0.0,
damp_r: 0.0,
damping_coeff: 0.0,
predelay_buffer,
predelay_write_pos: 0,
predelay_samples,
config,
sample_rate,
};
plate.update_parameters();
plate
}
fn update_parameters(&mut self) {
self.damping_coeff = self.config.damping * 0.5;
}
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);
}
pub fn set_dry(&mut self, dry: f32) {
self.config.dry = dry.clamp(0.0, 1.0);
}
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 delayed = self.predelay_buffer[self.predelay_write_pos];
self.predelay_buffer[self.predelay_write_pos] = (input_l + input_r) * 0.5;
self.predelay_write_pos = (self.predelay_write_pos + 1) % self.predelay_samples;
(delayed, delayed)
} else {
(input_l, input_r)
};
let mut early_l = 0.0;
let mut early_r = 0.0;
for delay in &mut self.early_delays_l {
early_l += delay.process(delayed_l, delay.max_delay()) * 0.25;
}
for delay in &mut self.early_delays_r {
early_r += delay.process(delayed_r, delay.max_delay()) * 0.25;
}
let mod1 = self.mod_lfo1.next_unipolar() * 2.0; let mod2 = self.mod_lfo2.next_unipolar() * 2.0;
let mut diffused_l = delayed_l + early_l;
let mut diffused_r = delayed_r + early_r;
for (i, ap) in self.diffusion_l.iter_mut().enumerate() {
let delay_time = ap.read(1.0) + if i % 2 == 0 { mod1 } else { mod2 };
let sample = ap.process(diffused_l, delay_time.max(1.0));
diffused_l = sample;
}
for (i, ap) in self.diffusion_r.iter_mut().enumerate() {
let delay_time = ap.read(1.0) + if i % 2 == 0 { mod2 } else { mod1 };
let sample = ap.process(diffused_r, delay_time.max(1.0));
diffused_r = sample;
}
let feedback_gain = 0.5 + self.config.room_size * 0.45;
let tank_l = self
.tank_delay_l
.process(diffused_l, self.tank_delay_l.max_delay());
let tank_r = self
.tank_delay_r
.process(diffused_r, self.tank_delay_r.max_delay());
self.damp_l = tank_l * (1.0 - self.damping_coeff) + self.damp_l * self.damping_coeff;
self.damp_r = tank_r * (1.0 - self.damping_coeff) + self.damp_r * self.damping_coeff;
let wet_l = self.damp_l * feedback_gain;
let wet_r = self.damp_r * feedback_gain;
let out_l = wet_l * self.config.wet + input_l * self.config.dry;
let out_r = wet_r * self.config.wet + input_r * self.config.dry;
(out_l, out_r)
}
}
impl AudioEffect for PlateReverb {
const EFFECT_ID: &'static str = "plate_reverb";
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 reset(&mut self) {
for delay in &mut self.early_delays_l {
delay.clear();
}
for delay in &mut self.early_delays_r {
delay.clear();
}
for ap in &mut self.diffusion_l {
ap.clear();
}
for ap in &mut self.diffusion_r {
ap.clear();
}
self.tank_delay_l.clear();
self.tank_delay_r.clear();
self.mod_lfo1.reset();
self.mod_lfo2.reset();
self.damp_l = 0.0;
self.damp_r = 0.0;
self.predelay_buffer.fill(0.0);
self.predelay_write_pos = 0;
}
fn set_sample_rate(&mut self, sample_rate: f32) {
*self = Self::new(self.config.clone(), sample_rate);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plate_reverb_creation() {
let config = ReverbConfig::default();
let reverb = PlateReverb::new(config, 48000.0);
assert_eq!(reverb.early_delays_l.len(), 4);
}
#[test]
fn test_plate_reverb_process() {
let config = ReverbConfig::default();
let mut reverb = PlateReverb::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_plate_reverb_stereo() {
let config = ReverbConfig::default();
let mut reverb = PlateReverb::new(config, 48000.0);
let (out_l, out_r) = reverb.process_sample_stereo(1.0, 0.0);
assert!(out_l != 0.0 || out_r != 0.0);
}
#[test]
fn test_plate_energy_conservation() {
let config = ReverbConfig::default()
.with_room_size(0.5)
.with_wet(0.3)
.with_dry(0.7);
let mut reverb = PlateReverb::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(),
"Plate reverb output energy must be finite"
);
assert!(
output_energy <= input_energy * 50.0,
"plate reverb energy {output_energy} exceeded input energy {input_energy} * 50 (unstable)"
);
}
}