use serde::{Deserialize, Serialize};
use tracing::debug;
use goonj::fdn::{Fdn, fdn_config_for_room};
use crate::error::{NaadError, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FdnReverb {
#[serde(skip)]
fdn: Option<Fdn>,
room_length: f32,
room_width: f32,
room_height: f32,
target_rt60: f32,
sample_rate: u32,
pub mix: f32,
}
impl FdnReverb {
pub fn new(
room_length: f32,
room_width: f32,
room_height: f32,
target_rt60: f32,
sample_rate: u32,
mix: f32,
) -> Result<Self> {
if room_length <= 0.0 || room_width <= 0.0 || room_height <= 0.0 {
return Err(NaadError::ComputationError {
message: "room dimensions must be positive".into(),
});
}
if target_rt60 <= 0.0 || !target_rt60.is_finite() {
return Err(NaadError::ComputationError {
message: "target_rt60 must be positive and finite".into(),
});
}
if sample_rate == 0 {
return Err(NaadError::ComputationError {
message: "sample_rate must be > 0".into(),
});
}
debug!(
room_length,
room_width, room_height, target_rt60, sample_rate, "FDN reverb created"
);
let config = fdn_config_for_room(
room_length,
room_width,
room_height,
target_rt60,
sample_rate,
);
let fdn = Fdn::new(&config);
Ok(Self {
fdn: Some(fdn),
room_length,
room_width,
room_height,
target_rt60,
sample_rate,
mix: mix.clamp(0.0, 1.0),
})
}
#[inline]
#[must_use]
pub fn process_sample(&mut self, input: f32) -> f32 {
if self.fdn.is_none() {
let config = fdn_config_for_room(
self.room_length,
self.room_width,
self.room_height,
self.target_rt60,
self.sample_rate,
);
self.fdn = Some(Fdn::new(&config));
}
let wet = if let Some(ref mut fdn) = self.fdn {
fdn.process_sample(input)
} else {
0.0
};
input * (1.0 - self.mix) + wet * self.mix
}
pub fn reset(&mut self) {
if let Some(ref mut fdn) = self.fdn {
fdn.reset();
}
}
#[must_use]
pub fn target_rt60(&self) -> f32 {
self.target_rt60
}
#[must_use]
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
}
const MATRIX_FDN_N: usize = 8;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MatrixFdn {
#[serde(skip, default = "default_buffers")]
buffers: [Vec<f32>; MATRIX_FDN_N],
#[serde(skip)]
positions: [usize; MATRIX_FDN_N],
delay_lengths: [usize; MATRIX_FDN_N],
#[serde(skip)]
damping_state: [f32; MATRIX_FDN_N],
damping_gain: [f32; MATRIX_FDN_N],
target_rt60: f32,
sample_rate: u32,
pub mix: f32,
}
fn default_buffers() -> [Vec<f32>; MATRIX_FDN_N] {
Default::default()
}
impl MatrixFdn {
pub fn new(target_rt60: f32, sample_rate: u32, mix: f32) -> Result<Self> {
if target_rt60 <= 0.0 || !target_rt60.is_finite() {
return Err(NaadError::ComputationError {
message: "target_rt60 must be positive and finite".into(),
});
}
if sample_rate == 0 {
return Err(NaadError::ComputationError {
message: "sample_rate must be > 0".into(),
});
}
let ms = [29.7f32, 37.1, 41.3, 43.7, 53.9, 59.1, 67.3, 73.7];
let mut delay_lengths = [0usize; MATRIX_FDN_N];
for (i, m) in ms.iter().enumerate() {
delay_lengths[i] = ((*m * 0.001) * sample_rate as f32).max(1.0) as usize;
}
let mut damping_gain = [0.0f32; MATRIX_FDN_N];
for i in 0..MATRIX_FDN_N {
let d = delay_lengths[i] as f32;
damping_gain[i] = 10f32.powf(-3.0 * d / (target_rt60 * sample_rate as f32));
}
let buffers: [Vec<f32>; MATRIX_FDN_N] =
std::array::from_fn(|i| vec![0.0; delay_lengths[i]]);
debug!(target_rt60, sample_rate, "MatrixFdn created");
Ok(Self {
buffers,
positions: [0; MATRIX_FDN_N],
delay_lengths,
damping_state: [0.0; MATRIX_FDN_N],
damping_gain,
target_rt60,
sample_rate,
mix: mix.clamp(0.0, 1.0),
})
}
fn ensure_buffers(&mut self) {
if self.buffers[0].len() != self.delay_lengths[0] {
for i in 0..MATRIX_FDN_N {
self.buffers[i] = vec![0.0; self.delay_lengths[i]];
self.positions[i] = 0;
self.damping_state[i] = 0.0;
}
}
}
#[inline]
#[must_use]
pub fn process_sample(&mut self, input: f32) -> f32 {
self.ensure_buffers();
let mut delayed = [0.0f32; MATRIX_FDN_N];
for ((slot, line), (pos, gain)) in delayed
.iter_mut()
.zip(self.buffers.iter())
.zip(self.positions.iter().zip(self.damping_gain.iter()))
{
let read_pos = (*pos + 1) % line.len();
let damped = line[read_pos] * gain;
*slot = crate::flush_denormal(damped);
}
self.damping_state.copy_from_slice(&delayed);
let wet = delayed.iter().sum::<f32>() / (MATRIX_FDN_N as f32).sqrt();
let h = hadamard8(delayed);
let scale = 1.0 / (MATRIX_FDN_N as f32).sqrt();
for ((line, pos), &fb) in self
.buffers
.iter_mut()
.zip(self.positions.iter_mut())
.zip(h.iter())
{
let v = input + fb * scale;
*pos = (*pos + 1) % line.len();
line[*pos] = crate::flush_denormal(v);
}
input * (1.0 - self.mix) + wet * self.mix
}
#[inline]
pub fn process_buffer(&mut self, buffer: &mut [f32]) {
for s in buffer.iter_mut() {
*s = self.process_sample(*s);
}
}
pub fn reset(&mut self) {
for i in 0..MATRIX_FDN_N {
for s in &mut self.buffers[i] {
*s = 0.0;
}
self.positions[i] = 0;
self.damping_state[i] = 0.0;
}
}
#[must_use]
pub fn target_rt60(&self) -> f32 {
self.target_rt60
}
#[must_use]
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
}
#[inline]
fn hadamard8(x: [f32; 8]) -> [f32; 8] {
[
x[0] + x[1] + x[2] + x[3] + x[4] + x[5] + x[6] + x[7],
x[0] - x[1] + x[2] - x[3] + x[4] - x[5] + x[6] - x[7],
x[0] + x[1] - x[2] - x[3] + x[4] + x[5] - x[6] - x[7],
x[0] - x[1] - x[2] + x[3] + x[4] - x[5] - x[6] + x[7],
x[0] + x[1] + x[2] + x[3] - x[4] - x[5] - x[6] - x[7],
x[0] - x[1] + x[2] - x[3] - x[4] + x[5] - x[6] + x[7],
x[0] + x[1] - x[2] - x[3] - x[4] - x[5] + x[6] + x[7],
x[0] - x[1] - x[2] + x[3] - x[4] + x[5] + x[6] - x[7],
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fdn_produces_reverb_tail() {
let mut fdn = FdnReverb::new(10.0, 8.0, 3.0, 1.0, 48000, 1.0).unwrap();
let _ = fdn.process_sample(1.0);
let mut has_tail = false;
for _ in 0..10000 {
let out = fdn.process_sample(0.0);
if out.abs() > 0.001 {
has_tail = true;
break;
}
}
assert!(has_tail, "FDN should produce a reverb tail");
}
#[test]
fn test_fdn_output_finite() {
let mut fdn = FdnReverb::new(10.0, 8.0, 3.0, 0.5, 48000, 1.0).unwrap();
let _ = fdn.process_sample(1.0);
for _ in 0..5000 {
let out = fdn.process_sample(0.0);
assert!(out.is_finite(), "output should be finite");
assert!(out.abs() < 100.0, "output should not blow up: {out}");
}
}
#[test]
fn test_fdn_invalid_params() {
assert!(FdnReverb::new(0.0, 8.0, 3.0, 1.0, 48000, 1.0).is_err());
assert!(FdnReverb::new(10.0, 8.0, 3.0, -1.0, 48000, 1.0).is_err());
assert!(FdnReverb::new(10.0, 8.0, 3.0, 1.0, 0, 1.0).is_err());
}
#[test]
fn test_serde_roundtrip() {
let fdn = FdnReverb::new(10.0, 8.0, 3.0, 1.5, 48000, 0.7).unwrap();
let json = serde_json::to_string(&fdn).unwrap();
let mut back: FdnReverb = serde_json::from_str(&json).unwrap();
assert!((fdn.mix - back.mix).abs() < f32::EPSILON);
assert!((fdn.target_rt60 - back.target_rt60).abs() < f32::EPSILON);
assert!(back.fdn.is_none());
let out = back.process_sample(1.0);
assert!(out.is_finite());
assert!(back.fdn.is_some(), "should reconstruct fdn on first use");
}
#[test]
fn test_hadamard8_orthogonality() {
let x = [1.0f32, 2.0, 3.0, 4.0, -5.0, -6.0, 7.0, 8.0];
let h = hadamard8(x);
let hh = hadamard8(h);
for (i, &v) in hh.iter().enumerate() {
let expected = 8.0 * x[i];
assert!(
(v - expected).abs() < 1e-3,
"round-trip[{i}] = {v}, expected {expected}"
);
}
}
#[test]
fn test_matrix_fdn_produces_reverb_tail() {
let mut fdn = MatrixFdn::new(1.0, 48000, 1.0).unwrap();
let _ = fdn.process_sample(1.0);
let mut has_tail = false;
for _ in 0..10_000 {
let out = fdn.process_sample(0.0);
if out.abs() > 0.001 {
has_tail = true;
break;
}
}
assert!(has_tail, "MatrixFdn should produce a reverb tail");
}
#[test]
fn test_matrix_fdn_decays() {
let sr = 48000;
let mut fdn = MatrixFdn::new(0.5, sr, 1.0).unwrap();
let _ = fdn.process_sample(1.0);
for _ in 0..(0.08 * sr as f32) as usize {
let _ = fdn.process_sample(0.0);
}
let mut early = 0.0f32;
for _ in 0..(0.05 * sr as f32) as usize {
let out = fdn.process_sample(0.0);
early += out * out;
}
for _ in 0..(0.4 * sr as f32) as usize {
let _ = fdn.process_sample(0.0);
}
let mut late = 0.0f32;
for _ in 0..(0.05 * sr as f32) as usize {
let out = fdn.process_sample(0.0);
late += out * out;
}
assert!(
early > late * 5.0,
"energy should decay across ~RT60: early={early}, late={late}"
);
}
#[test]
fn test_matrix_fdn_output_finite_under_white_noise() {
let mut fdn = MatrixFdn::new(2.0, 48000, 0.5).unwrap();
let mut state = 12345u32;
for _ in 0..20_000 {
let n = crate::dsp_util::xorshift32_signed_f32(&mut state);
let out = fdn.process_sample(n);
assert!(out.is_finite() && out.abs() < 100.0, "blow-up: {out}");
}
}
#[test]
fn test_matrix_fdn_invalid_params() {
assert!(MatrixFdn::new(0.0, 48000, 1.0).is_err());
assert!(MatrixFdn::new(-1.0, 48000, 1.0).is_err());
assert!(MatrixFdn::new(1.0, 0, 1.0).is_err());
}
#[test]
fn test_matrix_fdn_dry_passthrough() {
let mut fdn = MatrixFdn::new(1.0, 48000, 0.0).unwrap();
let out = fdn.process_sample(0.7);
assert!((out - 0.7).abs() < 1e-5);
}
#[test]
fn test_matrix_fdn_serde_roundtrip() {
let fdn = MatrixFdn::new(1.5, 48000, 0.6).unwrap();
let json = serde_json::to_string(&fdn).unwrap();
let mut back: MatrixFdn = serde_json::from_str(&json).unwrap();
assert!((fdn.mix - back.mix).abs() < f32::EPSILON);
assert!((fdn.target_rt60 - back.target_rt60).abs() < f32::EPSILON);
let out = back.process_sample(1.0);
assert!(out.is_finite());
assert_eq!(back.buffers[0].len(), back.delay_lengths[0]);
}
}