const FDN_DELAYS_44K: [usize; 8] = [1559, 1877, 2053, 2381, 2713, 3067, 3413, 3761];
const LFO_DEPTHS: [f32; 8] = [3.5, 2.8, 4.1, 3.2, 5.0, 2.5, 3.8, 4.4];
const LFO_RATES: [f32; 8] = [0.31, 0.47, 0.61, 0.79, 0.97, 1.13, 1.31, 1.51];
struct FdnChannel {
buf: Vec<f32>,
write_pos: usize,
lp_state: f32,
lfo_phase: f32,
lfo_rate: f32, lfo_depth: f32, }
impl FdnChannel {
fn new(len: usize, lfo_depth: f32, lfo_rate_hz: f32, sample_rate: f32) -> Self {
use std::f32::consts::TAU;
let buf_len = len + (lfo_depth.ceil() as usize) + 4;
Self {
buf: vec![0.0; buf_len.max(16)],
write_pos: 0,
lp_state: 0.0,
lfo_phase: 0.0,
lfo_rate: TAU * lfo_rate_hz / sample_rate,
lfo_depth,
}
}
fn write(&mut self, val: f32) {
self.buf[self.write_pos] = val;
self.write_pos = (self.write_pos + 1) % self.buf.len();
}
fn read_modulated(&mut self, base_delay: usize) -> f32 {
self.lfo_phase = (self.lfo_phase + self.lfo_rate).rem_euclid(std::f32::consts::TAU);
let mod_offset = self.lfo_phase.sin() * self.lfo_depth;
let delay_f = (base_delay as f32 - mod_offset).max(1.0);
let len = self.buf.len();
let d0 = delay_f as usize;
let frac = delay_f - d0 as f32;
let i0 = (self.write_pos + len - d0.min(len - 1)) % len;
let i1 = (self.write_pos + len - (d0 + 1).min(len - 1)) % len;
self.buf[i0] * (1.0 - frac) + self.buf[i1] * frac
}
fn apply_damping(&mut self, val: f32, damp: f32) -> f32 {
self.lp_state = (1.0 - damp) * val + damp * self.lp_state;
if !self.lp_state.is_finite() {
self.lp_state = 0.0;
}
self.lp_state
}
}
fn hadamard8(v: &mut [f32; 8]) {
for i in (0..8).step_by(2) {
let (a, b) = (v[i], v[i + 1]);
v[i] = a + b;
v[i + 1] = a - b;
}
for base in [0usize, 4] {
for j in 0..2 {
let (a, b) = (v[base + j], v[base + j + 2]);
v[base + j] = a + b;
v[base + j + 2] = a - b;
}
}
for j in 0..4 {
let (a, b) = (v[j], v[j + 4]);
v[j] = a + b;
v[j + 4] = a - b;
}
let norm = (8.0f32).sqrt().recip();
for x in v.iter_mut() {
*x *= norm;
}
}
pub struct FdnReverb {
channels: Vec<FdnChannel>,
base_delays: Vec<usize>,
pre_delay_buf: Vec<(f32, f32)>,
pre_delay_pos: usize,
pre_delay_len: usize,
pub feedback: f32,
pub damping: f32,
pub wet: f32,
}
impl FdnReverb {
pub fn new(sample_rate: f32) -> Self {
let scale = sample_rate / 44100.0;
let base_delays: Vec<usize> = FDN_DELAYS_44K
.iter()
.map(|&d| (d as f32 * scale) as usize)
.collect();
let channels = (0..8)
.map(|i| FdnChannel::new(base_delays[i], LFO_DEPTHS[i], LFO_RATES[i], sample_rate))
.collect();
let pre_delay_len = ((10.0 * 0.001 * sample_rate) as usize).max(1);
Self {
channels,
base_delays,
pre_delay_buf: vec![(0.0, 0.0); pre_delay_len],
pre_delay_pos: 0,
pre_delay_len,
feedback: 0.88,
damping: 0.25,
wet: 0.4,
}
}
pub fn process(&mut self, input_l: f32, input_r: f32) -> (f32, f32) {
let input_l = if input_l.is_finite() { input_l } else { 0.0 };
let input_r = if input_r.is_finite() { input_r } else { 0.0 };
self.pre_delay_buf[self.pre_delay_pos] = (input_l, input_r);
let read_pos = (self.pre_delay_pos + 1) % self.pre_delay_len;
let (pd_l, pd_r) = self.pre_delay_buf[read_pos];
self.pre_delay_pos = (self.pre_delay_pos + 1) % self.pre_delay_len;
let mut state = [0.0f32; 8];
for (i, ch) in self.channels.iter_mut().enumerate() {
let raw = ch.read_modulated(self.base_delays[i]);
state[i] = ch.apply_damping(raw, self.damping);
}
hadamard8(&mut state);
for (i, ch) in self.channels.iter_mut().enumerate() {
let excitation = if i % 2 == 0 { pd_l } else { pd_r };
let write_val = (state[i] * self.feedback + excitation * 0.12).clamp(-10.0, 10.0);
ch.write(write_val);
}
let mut out_l = 0.0f32;
let mut out_r = 0.0f32;
for (i, s) in state.iter().enumerate() {
if i % 2 == 0 {
out_l += s;
} else {
out_r += s;
}
}
out_l *= 0.25;
out_r *= 0.25;
if !out_l.is_finite() || !out_r.is_finite() {
for ch in &mut self.channels {
ch.buf.iter_mut().for_each(|x| *x = 0.0);
ch.lp_state = 0.0;
}
return (input_l * (1.0 - self.wet), input_r * (1.0 - self.wet));
}
let dry = 1.0 - self.wet;
(
input_l * dry + out_l * self.wet,
input_r * dry + out_r * self.wet,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
const SR: f32 = 44100.0;
#[test]
fn test_fdn_output_finite() {
let mut rv = FdnReverb::new(SR);
for i in 0..2000 {
let x = (i as f32 * 0.05).sin();
let (l, r) = rv.process(x, x);
assert!(l.is_finite(), "FDN L output non-finite at {}", i);
assert!(r.is_finite(), "FDN R output non-finite at {}", i);
}
}
#[test]
fn test_fdn_nan_input_safe() {
let mut rv = FdnReverb::new(SR);
let (l, r) = rv.process(f32::NAN, f32::NAN);
assert!(l.is_finite(), "NaN input should produce finite output");
assert!(r.is_finite(), "NaN input should produce finite output");
}
#[test]
fn test_fdn_produces_tail_after_impulse() {
let mut rv = FdnReverb::new(SR);
rv.wet = 1.0;
rv.process(1.0, 1.0);
let mut max_tail = 0.0f32;
for _ in 0..4410 {
let (l, _) = rv.process(0.0, 0.0);
max_tail = max_tail.max(l.abs());
}
assert!(max_tail > 0.0, "FDN reverb should produce a tail after impulse");
}
#[test]
fn test_fdn_wet_zero_passes_through() {
let mut rv = FdnReverb::new(SR);
rv.wet = 0.0;
let pre_delay_len = (10.0_f32 * 0.001 * SR) as usize;
for _ in 0..pre_delay_len {
rv.process(0.5, -0.3);
}
let (l, r) = rv.process(0.5, -0.3);
assert!(l.is_finite());
assert!(r.is_finite());
}
#[test]
fn test_fdn_tail_decays_over_time() {
let mut rv = FdnReverb::new(SR);
rv.wet = 1.0;
rv.feedback = 0.80; rv.process(1.0, 1.0);
for _ in 0..4000 {
rv.process(0.0, 0.0);
}
let window = 4410usize; let mut energy_near = 0.0f32;
for _ in 0..window {
let (l, _) = rv.process(0.0, 0.0);
energy_near += l * l;
}
for _ in 0..(SR as usize * 2) {
rv.process(0.0, 0.0);
}
let mut energy_far = 0.0f32;
for _ in 0..window {
let (l, _) = rv.process(0.0, 0.0);
energy_far += l * l;
}
assert!(
energy_far < energy_near,
"Reverb tail should decay: near={}, far={}",
energy_near,
energy_far
);
}
#[test]
fn test_fdn_stereo_injection_uses_both_channels() {
let mut rv_l = FdnReverb::new(SR);
rv_l.wet = 1.0;
rv_l.process(1.0, 0.0);
let mut rv_r = FdnReverb::new(SR);
rv_r.wet = 1.0;
rv_r.process(0.0, 1.0);
let mut diff = 0.0f32;
for _ in 0..4410 {
let (ll, _) = rv_l.process(0.0, 0.0);
let (_, rr) = rv_r.process(0.0, 0.0);
diff += (ll - rr).abs();
}
assert!(
diff > 0.01,
"L and R impulses should produce different reverb tails (diff={})",
diff
);
}
#[test]
fn test_hadamard8_energy_preserving() {
let input = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let energy_in: f32 = input.iter().map(|x| x * x).sum();
let mut v = input;
hadamard8(&mut v);
let energy_out: f32 = v.iter().map(|x| x * x).sum();
assert!(
(energy_in - energy_out).abs() < 1e-3,
"Hadamard8 should preserve energy: in={}, out={}",
energy_in,
energy_out
);
}
}