pub struct Fdn {
num_lines: usize,
delay_lines: Vec<Vec<f32>>,
delay_lengths: Vec<usize>,
write_positions: Vec<usize>,
feedback_matrix: Vec<f32>,
feedback_gain: f32,
absorption_state: Vec<f32>,
absorption_coeff: Vec<f32>,
input_gains: Vec<f32>,
output_gains: Vec<[f32; 2]>,
scratch: Vec<f32>,
}
impl Fdn {
pub fn new(num_lines: usize, sample_rate: u32) -> Self {
let num_lines = num_lines.next_power_of_two().max(2);
let base_delays = prime_delays(num_lines, sample_rate);
let delay_lines: Vec<Vec<f32>> = base_delays.iter().map(|&len| vec![0.0; len]).collect();
let feedback_matrix = hadamard_flat(num_lines);
let scale = 1.0 / (num_lines as f32).sqrt();
let input_gains = vec![scale; num_lines];
let output_gains: Vec<[f32; 2]> = (0..num_lines)
.map(|i| {
let angle = std::f32::consts::PI * i as f32 / num_lines as f32;
[angle.cos(), angle.sin()]
})
.collect();
Self {
num_lines,
delay_lines,
delay_lengths: base_delays,
write_positions: vec![0; num_lines],
feedback_matrix,
feedback_gain: 0.7,
absorption_state: vec![0.0; num_lines],
absorption_coeff: vec![0.3; num_lines],
input_gains,
output_gains,
scratch: vec![0.0; num_lines],
}
}
pub fn set_room_params(&mut self, rt60: f32, damping: f32, size: f32, sample_rate: u32) {
let base_delays = prime_delays(self.num_lines, sample_rate);
for (i, &base) in base_delays.iter().enumerate() {
let new_len = ((base as f32 * size) as usize).max(1);
if new_len != self.delay_lengths[i] {
self.delay_lengths[i] = new_len;
self.delay_lines[i].resize(new_len, 0.0);
self.write_positions[i] %= new_len;
}
}
let avg_delay = self.delay_lengths.iter().sum::<usize>() as f32 / self.num_lines as f32;
let rt60_samples = rt60 * sample_rate as f32;
self.feedback_gain = if rt60_samples > 0.0 {
10.0_f32
.powf(-3.0 * avg_delay / rt60_samples)
.clamp(0.0, 0.999)
} else {
0.0
};
let damp = damping.clamp(0.0, 1.0);
for coeff in &mut self.absorption_coeff {
*coeff = damp * 0.6 + 0.1; }
}
#[inline]
pub fn process_stereo(&mut self, left: f32, right: f32) -> (f32, f32) {
let n = self.num_lines;
let mono_in = (left + right) * 0.5;
for i in 0..n {
let pos = self.write_positions[i];
self.scratch[i] = self.delay_lines[i][pos];
}
for i in 0..n {
let mut feedback_sum = 0.0;
for j in 0..n {
feedback_sum += self.feedback_matrix[i * n + j] * self.scratch[j];
}
let input = mono_in * self.input_gains[i];
let fb = feedback_sum * self.feedback_gain;
let alpha = self.absorption_coeff[i];
self.absorption_state[i] = alpha * self.absorption_state[i] + (1.0 - alpha) * fb;
let pos = self.write_positions[i];
self.delay_lines[i][pos] = input + self.absorption_state[i];
self.write_positions[i] = (pos + 1) % self.delay_lengths[i];
}
let mut out_l = 0.0;
let mut out_r = 0.0;
for i in 0..n {
out_l += self.scratch[i] * self.output_gains[i][0];
out_r += self.scratch[i] * self.output_gains[i][1];
}
(out_l.clamp(-4.0, 4.0), out_r.clamp(-4.0, 4.0))
}
pub fn reset(&mut self) {
for dl in &mut self.delay_lines {
dl.fill(0.0);
}
self.write_positions.fill(0);
self.absorption_state.fill(0.0);
}
}
fn hadamard_flat(n: usize) -> Vec<f32> {
let mut h = vec![0.0f32; n * n];
h[0] = 1.0;
let mut size = 1;
while size < n {
for i in 0..size {
for j in 0..size {
let val = h[i * n + j];
h[i * n + (j + size)] = val; h[(i + size) * n + j] = val; h[(i + size) * n + (j + size)] = -val; }
}
size *= 2;
}
let scale = 1.0 / (n as f32).sqrt();
for v in &mut h {
*v *= scale;
}
h
}
fn prime_delays(n: usize, sample_rate: u32) -> Vec<usize> {
let min_samples = (0.020 * sample_rate as f32) as usize;
let max_samples = (0.080 * sample_rate as f32) as usize;
let primes: Vec<usize> = (min_samples..=max_samples)
.filter(|&n| is_prime(n))
.collect();
if primes.len() >= n {
let step = primes.len() / n;
(0..n)
.map(|i| primes[(i * step).min(primes.len() - 1)])
.collect()
} else {
let base = (min_samples + max_samples) / 2;
(0..n).map(|i| next_prime(base + i * 7)).collect()
}
}
fn is_prime(n: usize) -> bool {
if n < 2 {
return false;
}
if n < 4 {
return true;
}
if n.is_multiple_of(2) || n.is_multiple_of(3) {
return false;
}
let mut i = 5;
while i * i <= n {
if n.is_multiple_of(i) || n.is_multiple_of(i + 2) {
return false;
}
i += 6;
}
true
}
fn next_prime(n: usize) -> usize {
let mut p = n;
while !is_prime(p) {
p += 1;
}
p
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hadamard_orthogonal() {
let n = 4;
let h = hadamard_flat(n);
for i in 0..n {
for j in 0..n {
let mut dot = 0.0;
for k in 0..n {
dot += h[i * n + k] * h[j * n + k];
}
let expected = if i == j { 1.0 } else { 0.0 };
assert!(
(dot - expected).abs() < 1e-5,
"H*H^T[{i}][{j}] = {dot}, expected {expected}"
);
}
}
}
#[test]
fn test_fdn_energy_decay() {
let mut fdn = Fdn::new(8, 48000);
fdn.set_room_params(1.0, 0.3, 1.0, 48000);
let (l, r) = fdn.process_stereo(1.0, 1.0);
let _ = (l, r);
let mut energy = 0.0f32;
for _ in 0..48000 {
let (l, r) = fdn.process_stereo(0.0, 0.0);
energy += l * l + r * r;
}
assert!(energy > 0.01, "FDN produced no energy: {energy}");
assert!(energy.is_finite(), "FDN energy is not finite");
}
#[test]
fn test_fdn_stability() {
let mut fdn = Fdn::new(8, 48000);
fdn.set_room_params(3.0, 0.5, 1.5, 48000);
for _ in 0..96000 {
let (l, r) = fdn.process_stereo(0.01, 0.01);
assert!(
l.is_finite() && r.is_finite(),
"Non-finite output: {l}, {r}"
);
assert!(l.abs() < 5.0 && r.abs() < 5.0, "Output too large: {l}, {r}");
}
}
#[test]
fn test_fdn_reset() {
let mut fdn = Fdn::new(4, 48000);
fdn.process_stereo(1.0, 1.0);
fdn.reset();
let (l, r) = fdn.process_stereo(0.0, 0.0);
assert!(
l.abs() < 1e-10 && r.abs() < 1e-10,
"State not cleared: {l}, {r}"
);
}
#[test]
fn test_prime_delays() {
let delays = prime_delays(8, 48000);
assert_eq!(delays.len(), 8);
for &d in &delays {
assert!(is_prime(d), "{d} is not prime");
}
for &d in &delays {
assert!((960..=3840).contains(&d), "Delay {d} out of range");
}
}
}