use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FdnConfig {
pub num_delays: usize,
pub delay_lengths: Vec<u32>,
pub feedback_gains: Vec<f32>,
pub sample_rate: u32,
pub target_rt60: f32,
}
#[derive(Debug, Clone)]
pub struct Fdn {
delay_lines: Vec<Vec<f32>>,
write_positions: Vec<usize>,
feedback_gains: Vec<f32>,
scratch: Vec<f32>,
_sample_rate: u32,
}
impl Fdn {
#[must_use]
pub fn new(config: &FdnConfig) -> Self {
let n = config.num_delays;
let delay_lines: Vec<Vec<f32>> = config
.delay_lengths
.iter()
.take(n)
.map(|&len| vec![0.0; len.max(1) as usize])
.collect();
let feedback_gains = if config.feedback_gains.len() >= n {
config.feedback_gains[..n].to_vec()
} else {
config
.delay_lengths
.iter()
.take(n)
.map(|&len| {
let delay_time = len as f32 / config.sample_rate.max(1) as f32;
if config.target_rt60 > 0.0 {
10.0_f32.powf(-3.0 * delay_time / config.target_rt60)
} else {
0.0
}
})
.collect()
};
Self {
write_positions: vec![0; n],
scratch: vec![0.0; n],
delay_lines,
feedback_gains,
_sample_rate: config.sample_rate,
}
}
#[inline]
pub fn process_sample(&mut self, input: f32) -> f32 {
let n = self.delay_lines.len();
if n == 0 {
return input;
}
let mut output_sum = 0.0_f32;
for i in 0..n {
let len = self.delay_lines[i].len();
let read_pos = (self.write_positions[i] + 1) % len;
self.scratch[i] = self.delay_lines[i][read_pos];
output_sum += self.scratch[i];
}
let factor = 2.0 / n as f32;
let correction = factor * output_sum;
let input_per_line = input / n as f32;
for i in 0..n {
let fb = self.scratch[i] - correction;
let len = self.delay_lines[i].len();
self.delay_lines[i][self.write_positions[i]] =
input_per_line + fb * self.feedback_gains[i];
self.write_positions[i] = (self.write_positions[i] + 1) % len;
}
output_sum / (n as f32).sqrt()
}
#[must_use]
pub fn process_buffer(&mut self, input: &[f32]) -> Vec<f32> {
input.iter().map(|&s| self.process_sample(s)).collect()
}
pub fn reset(&mut self) {
for line in &mut self.delay_lines {
for s in line.iter_mut() {
*s = 0.0;
}
}
for pos in &mut self.write_positions {
*pos = 0;
}
}
}
#[must_use]
pub fn fdn_config_for_room(
length: f32,
width: f32,
height: f32,
rt60: f32,
sample_rate: u32,
) -> FdnConfig {
let c = crate::propagation::speed_of_sound(20.0);
let base_delays = [
length,
width,
height,
(length + width) * 0.5,
(length + height) * 0.5,
(width + height) * 0.5,
(length + width + height) / 3.0,
length.max(width).max(height),
];
let delay_lengths: Vec<u32> = base_delays
.iter()
.map(|&d| {
let samples = (2.0 * d / c * sample_rate as f32) as u32;
samples.max(1)
})
.collect();
FdnConfig {
num_delays: 8,
delay_lengths,
feedback_gains: vec![], sample_rate,
target_rt60: rt60,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fdn_produces_output() {
let config = fdn_config_for_room(10.0, 8.0, 3.0, 1.0, 48000);
let mut fdn = Fdn::new(&config);
let mut output = vec![0.0_f32; 4800];
output[0] = fdn.process_sample(1.0);
for s in output[1..].iter_mut() {
*s = fdn.process_sample(0.0);
}
let energy: f32 = output.iter().map(|&s| s * s).sum();
assert!(energy > 0.0, "FDN should produce reverb output");
}
#[test]
fn fdn_decays_over_time() {
let config = fdn_config_for_room(10.0, 8.0, 3.0, 0.5, 48000);
let mut fdn = Fdn::new(&config);
fdn.process_sample(1.0);
for _ in 0..4800 {
fdn.process_sample(0.0);
}
let early: f32 = (0..9600).map(|_| fdn.process_sample(0.0).powi(2)).sum();
let late: f32 = (0..9600).map(|_| fdn.process_sample(0.0).powi(2)).sum();
assert!(
early > late,
"early energy ({early}) should exceed late ({late})"
);
}
#[test]
fn fdn_reset_clears_state() {
let config = fdn_config_for_room(5.0, 4.0, 3.0, 1.0, 48000);
let mut fdn = Fdn::new(&config);
fdn.process_sample(1.0);
for _ in 0..100 {
fdn.process_sample(0.0);
}
fdn.reset();
let out = fdn.process_sample(0.0);
assert!(out.abs() < f32::EPSILON, "reset should clear all state");
}
#[test]
fn fdn_config_for_room_valid() {
let config = fdn_config_for_room(10.0, 8.0, 3.0, 1.0, 48000);
assert_eq!(config.num_delays, 8);
assert_eq!(config.delay_lengths.len(), 8);
for &d in &config.delay_lengths {
assert!(d > 0, "delay lengths should be positive");
}
}
#[test]
fn process_buffer_matches_sample_by_sample() {
let config = fdn_config_for_room(5.0, 4.0, 3.0, 0.8, 48000);
let mut fdn1 = Fdn::new(&config);
let mut fdn2 = Fdn::new(&config);
let input: Vec<f32> = (0..100).map(|i| if i == 0 { 1.0 } else { 0.0 }).collect();
let buf_out = fdn1.process_buffer(&input);
let sample_out: Vec<f32> = input.iter().map(|&s| fdn2.process_sample(s)).collect();
for (a, b) in buf_out.iter().zip(sample_out.iter()) {
assert!(
(a - b).abs() < f32::EPSILON,
"buffer and sample-by-sample should match"
);
}
}
}