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
}
}
#[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");
}
}