use crate::graph::{
GroupHint, ParamDescriptor, ParamFlags, ParamGroup, ParamUnit, ProcessContext, Processor,
ProcessorInfo, Sig,
};
pub struct PlateReverb {
combs_l: [CombFilter; 4],
combs_r: [CombFilter; 4],
allpass_l: [AllpassFilter; 2],
allpass_r: [AllpassFilter; 2],
room_size: f64,
damping: f64,
mix: f64,
}
const COMB_DELAYS: [usize; 4] = [1116, 1188, 1277, 1356];
const ALLPASS_DELAYS: [usize; 2] = [556, 225];
const STEREO_SPREAD: usize = 23;
const MAX_COMB_DELAY: usize = COMB_DELAYS[3] + STEREO_SPREAD + 1;
const MAX_ALLPASS_DELAY: usize = ALLPASS_DELAYS[0] + STEREO_SPREAD + 1;
fn comb_delay_for(base: usize, spread: usize, room_size: f64) -> usize {
let d = (base as f64 * (0.5 + room_size * 0.5)) as usize + spread;
d.min(MAX_COMB_DELAY - 1).max(1)
}
impl PlateReverb {
pub fn new(room_size: f64, damping: f64, mix: f64) -> Self {
let rs = room_size.clamp(0.0, 1.0);
let dp = damping.clamp(0.0, 1.0);
let fb = rs * 0.85 + 0.1;
let make_comb = |base: usize, spread: usize| {
CombFilter::new(comb_delay_for(base, spread, rs), MAX_COMB_DELAY, fb, dp)
};
let make_allpass =
|base: usize, spread: usize| AllpassFilter::new(base + spread, MAX_ALLPASS_DELAY, 0.5);
Self {
combs_l: [
make_comb(COMB_DELAYS[0], 0),
make_comb(COMB_DELAYS[1], 0),
make_comb(COMB_DELAYS[2], 0),
make_comb(COMB_DELAYS[3], 0),
],
combs_r: [
make_comb(COMB_DELAYS[0], STEREO_SPREAD),
make_comb(COMB_DELAYS[1], STEREO_SPREAD),
make_comb(COMB_DELAYS[2], STEREO_SPREAD),
make_comb(COMB_DELAYS[3], STEREO_SPREAD),
],
allpass_l: [
make_allpass(ALLPASS_DELAYS[0], 0),
make_allpass(ALLPASS_DELAYS[1], 0),
],
allpass_r: [
make_allpass(ALLPASS_DELAYS[0], STEREO_SPREAD),
make_allpass(ALLPASS_DELAYS[1], STEREO_SPREAD),
],
room_size: rs,
damping: dp,
mix: mix.clamp(0.0, 1.0),
}
}
fn update_comb_params(&mut self) {
let fb = self.room_size * 0.85 + 0.1;
for (i, comb) in self.combs_l.iter_mut().enumerate() {
comb.feedback = fb;
comb.damp = self.damping;
comb.delay = comb_delay_for(COMB_DELAYS[i], 0, self.room_size);
}
for (i, comb) in self.combs_r.iter_mut().enumerate() {
comb.feedback = fb;
comb.damp = self.damping;
comb.delay = comb_delay_for(COMB_DELAYS[i], STEREO_SPREAD, self.room_size);
}
}
}
impl Processor for PlateReverb {
fn info(&self) -> ProcessorInfo {
ProcessorInfo {
name: "reverb",
sig: Sig::STEREO,
description: "Plate-style reverb with damping control",
}
}
fn process(&mut self, ctx: &mut ProcessContext) {
let wet = self.mix as f32;
let dry = 1.0 - wet;
for i in 0..ctx.frames {
let in_l = ctx.inputs[0][i];
let in_r = ctx.inputs[1][i];
let mono_in = (in_l + in_r) * 0.5;
let mut wet_l = 0.0f32;
let mut wet_r = 0.0f32;
for comb in &mut self.combs_l {
wet_l += comb.tick(mono_in);
}
for comb in &mut self.combs_r {
wet_r += comb.tick(mono_in);
}
wet_l *= 0.25;
wet_r *= 0.25;
for ap in &mut self.allpass_l {
wet_l = ap.tick(wet_l);
}
for ap in &mut self.allpass_r {
wet_r = ap.tick(wet_r);
}
let out_l = in_l * dry + wet_l * wet;
let out_r = in_r * dry + wet_r * wet;
ctx.outputs[0][i] = if out_l.is_finite() { out_l } else { 0.0 };
ctx.outputs[1][i] = if out_r.is_finite() { out_r } else { 0.0 };
}
}
fn reset(&mut self) {
for c in &mut self.combs_l {
c.reset();
}
for c in &mut self.combs_r {
c.reset();
}
for a in &mut self.allpass_l {
a.reset();
}
for a in &mut self.allpass_r {
a.reset();
}
}
fn params(&self) -> Vec<ParamDescriptor> {
vec![
ParamDescriptor {
id: 0,
name: "Size",
min: 0.0,
max: 1.0,
default: 0.5,
unit: ParamUnit::Linear,
flags: ParamFlags::NONE,
step: 0.05,
group: Some(0),
help: "",
},
ParamDescriptor {
id: 1,
name: "Damping",
min: 0.0,
max: 1.0,
default: 0.5,
unit: ParamUnit::Linear,
flags: ParamFlags::NONE,
step: 0.05,
group: Some(0),
help: "",
},
ParamDescriptor {
id: 2,
name: "Mix",
min: 0.0,
max: 1.0,
default: 0.2,
unit: ParamUnit::Percent,
flags: ParamFlags::NONE,
step: 0.05,
group: Some(0),
help: "",
},
]
}
fn param_groups(&self) -> Vec<ParamGroup> {
vec![ParamGroup {
id: 0,
name: "Reverb",
hint: GroupHint::TimeBased,
}]
}
fn get_param(&self, id: u32) -> f64 {
match id {
0 => self.room_size,
1 => self.damping,
2 => self.mix,
_ => 0.0,
}
}
fn set_param(&mut self, id: u32, value: f64) {
match id {
0 => {
self.room_size = value.clamp(0.0, 1.0);
self.update_comb_params();
}
1 => {
self.damping = value.clamp(0.0, 1.0);
self.update_comb_params();
}
2 => self.mix = value.clamp(0.0, 1.0),
_ => {}
}
}
}
struct CombFilter {
buf: Vec<f32>,
pos: usize,
delay: usize,
feedback: f64,
damp: f64,
damp_state: f32,
}
impl CombFilter {
fn new(delay: usize, max_delay: usize, feedback: f64, damp: f64) -> Self {
let size = max_delay.max(delay).max(1).next_power_of_two();
Self {
buf: vec![0.0; size],
pos: 0,
delay: delay.min(size - 1).max(1),
feedback,
damp,
damp_state: 0.0,
}
}
fn tick(&mut self, input: f32) -> f32 {
let mask = self.buf.len() - 1;
let d = self.delay.min(mask);
let read = (self.pos + self.buf.len() - d) & mask;
let output = self.buf[read];
let damp = self.damp as f32;
self.damp_state = output * (1.0 - damp) + self.damp_state * damp;
let fb_sample = input + self.damp_state * self.feedback as f32;
self.buf[self.pos] = if fb_sample.is_finite() {
fb_sample
} else {
0.0
};
self.pos = (self.pos + 1) & mask;
output
}
fn reset(&mut self) {
self.buf.fill(0.0);
self.damp_state = 0.0;
self.pos = 0;
}
}
struct AllpassFilter {
buf: Vec<f32>,
pos: usize,
delay: usize,
feedback: f64,
}
impl AllpassFilter {
fn new(delay: usize, max_delay: usize, feedback: f64) -> Self {
let size = max_delay.max(delay).max(1).next_power_of_two();
Self {
buf: vec![0.0; size],
pos: 0,
delay: delay.min(size - 1).max(1),
feedback,
}
}
fn tick(&mut self, input: f32) -> f32 {
let mask = self.buf.len() - 1;
let d = self.delay.min(mask);
let read = (self.pos + self.buf.len() - d) & mask;
let buffered = self.buf[read];
let fb = self.feedback as f32;
let output = buffered - input;
let fb_sample = input + buffered * fb;
self.buf[self.pos] = if fb_sample.is_finite() {
fb_sample
} else {
0.0
};
self.pos = (self.pos + 1) & mask;
output
}
fn reset(&mut self) {
self.buf.fill(0.0);
self.pos = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reverb_produces_tail() {
let mut reverb = PlateReverb::new(0.5, 0.5, 1.0);
let mut impulse = vec![0.0f32; 4096];
impulse[0] = 1.0;
let out_l = vec![0.0f32; 4096];
let out_r = vec![0.0f32; 4096];
let inputs: Vec<&[f32]> = vec![&impulse, &impulse];
let mut outputs = vec![out_l, out_r];
let mut ctx = ProcessContext {
inputs: &inputs,
outputs: &mut outputs,
frames: 4096,
sample_rate: 44100.0,
events: &[],
};
reverb.process(&mut ctx);
let tail_energy: f32 = outputs[0][2000..].iter().map(|s| s * s).sum();
assert!(tail_energy > 1e-6, "reverb should produce a decaying tail");
}
#[test]
fn reverb_params_round_trip() {
let mut reverb = PlateReverb::new(0.5, 0.5, 0.2);
assert!((reverb.get_param(0) - 0.5).abs() < 1e-6);
reverb.set_param(0, 0.8);
assert!((reverb.get_param(0) - 0.8).abs() < 1e-6);
}
}