#![allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
use crate::{
utils::{AllPassFilter, DelayLine},
AudioEffect,
};
#[derive(Debug, Clone)]
pub struct SpringReverbConfig {
pub tension: f32,
pub damping: f32,
pub dispersion: f32,
pub diffusion: f32,
pub wet_mix: f32,
}
impl Default for SpringReverbConfig {
fn default() -> Self {
Self {
tension: 0.5,
damping: 0.4,
dispersion: 0.3,
diffusion: 0.6,
wet_mix: 0.4,
}
}
}
const DISP_STAGES: usize = 4;
const DIFF_STAGES: usize = 2;
struct SpringWaveguide {
fwd: DelayLine,
bwd: DelayLine,
dispersion: [AllPassFilter; DISP_STAGES],
loss_state: f32,
loss_coeff: f32,
feedback: f32,
}
impl SpringWaveguide {
fn new(delay_ms: f32, sample_rate: f32, damping: f32, dispersion: f32, feedback: f32) -> Self {
let delay_samp = ((delay_ms * sample_rate / 1000.0) as usize).max(4);
let base_coeff = (dispersion * 0.5).clamp(0.05, 0.49);
let disp_coeffs = [
base_coeff,
base_coeff * 1.1,
base_coeff * 1.2,
base_coeff * 1.3,
];
let dispersion = [
AllPassFilter::new(disp_coeffs[0].clamp(-0.999, 0.999)),
AllPassFilter::new(disp_coeffs[1].clamp(-0.999, 0.999)),
AllPassFilter::new(disp_coeffs[2].clamp(-0.999, 0.999)),
AllPassFilter::new(disp_coeffs[3].clamp(-0.999, 0.999)),
];
let loss_coeff = (damping * 0.3).clamp(0.0, 0.95);
Self {
fwd: DelayLine::new(delay_samp),
bwd: DelayLine::new(delay_samp),
dispersion,
loss_state: 0.0,
loss_coeff,
feedback,
}
}
#[inline]
fn process(&mut self, input: f32) -> f32 {
let bwd_out = self.bwd.read(self.bwd.max_delay());
let mut dispersed = bwd_out;
for ap in &mut self.dispersion {
dispersed = ap.process(dispersed);
}
self.loss_state = dispersed * (1.0 - self.loss_coeff) + self.loss_state * self.loss_coeff;
let fwd_in = input + self.loss_state * self.feedback;
self.fwd.write(fwd_in);
let fwd_out = self.fwd.read(self.fwd.max_delay());
self.bwd.write(fwd_out * self.feedback);
bwd_out
}
fn reset(&mut self) {
self.fwd.clear();
self.bwd.clear();
for ap in &mut self.dispersion {
ap.reset();
}
self.loss_state = 0.0;
}
}
pub struct SpringReverb {
spring1: SpringWaveguide,
spring2: SpringWaveguide,
helical: DelayLine,
helical_feedback: f32,
helical_state: f32,
diffuser: [AllPassFilter; DIFF_STAGES],
chirp_phase: f32,
chirp_rate: f32,
config: SpringReverbConfig,
#[allow(dead_code)]
sample_rate: f32,
}
impl SpringReverb {
#[must_use]
pub fn new(config: SpringReverbConfig, sample_rate: f32) -> Self {
let tension = config.tension.clamp(0.1, 1.0);
let damping = config.damping.clamp(0.0, 1.0);
let dispersion = config.dispersion.clamp(0.0, 1.0);
let diffusion = config.diffusion.clamp(0.0, 0.99);
let spring1_ms = 30.0 + tension * 20.0; let spring2_ms = spring1_ms * 1.06;
let helical_ms = 5.0 + tension * 3.0; let helical_samp = ((helical_ms * sample_rate / 1000.0) as usize).max(4);
let spring1 = SpringWaveguide::new(
spring1_ms,
sample_rate,
damping,
dispersion,
0.85 - damping * 0.2,
);
let spring2 = SpringWaveguide::new(
spring2_ms,
sample_rate,
damping,
dispersion,
0.82 - damping * 0.2,
);
let diffuser = [
AllPassFilter::new(diffusion * 0.6),
AllPassFilter::new(diffusion * 0.5),
];
let chirp_rate = 2.0 * std::f32::consts::TAU / sample_rate;
Self {
spring1,
spring2,
helical: DelayLine::new(helical_samp),
helical_feedback: 0.4 + tension * 0.15,
helical_state: 0.0,
diffuser,
chirp_phase: 0.0,
chirp_rate,
config,
sample_rate,
}
}
#[must_use]
pub fn vintage_tank(sample_rate: f32) -> Self {
Self::new(
SpringReverbConfig {
tension: 0.8,
damping: 0.35,
dispersion: 0.55,
diffusion: 0.7,
wet_mix: 0.45,
},
sample_rate,
)
}
#[must_use]
pub fn guitar_amp(sample_rate: f32) -> Self {
Self::new(
SpringReverbConfig {
tension: 0.45,
damping: 0.5,
dispersion: 0.35,
diffusion: 0.55,
wet_mix: 0.35,
},
sample_rate,
)
}
#[must_use]
pub fn large_tank(sample_rate: f32) -> Self {
Self::new(
SpringReverbConfig {
tension: 0.9,
damping: 0.25,
dispersion: 0.45,
diffusion: 0.75,
wet_mix: 0.5,
},
sample_rate,
)
}
pub fn set_tension(&mut self, tension: f32) {
self.config.tension = tension.clamp(0.1, 1.0);
}
pub fn set_damping(&mut self, damping: f32) {
self.config.damping = damping.clamp(0.0, 1.0);
}
pub fn set_wet_mix(&mut self, wet: f32) {
self.config.wet_mix = wet.clamp(0.0, 1.0);
}
#[must_use]
pub fn wet_mix(&self) -> f32 {
self.config.wet_mix
}
}
impl AudioEffect for SpringReverb {
fn process_sample(&mut self, input: f32) -> f32 {
let mut diffused = input;
for ap in &mut self.diffuser {
diffused = ap.process(diffused);
}
let s1_out = self.spring1.process(diffused * 0.6);
let s2_out = self.spring2.process(diffused * 0.5);
let helical_in = diffused * 0.15 + self.helical_state * self.helical_feedback;
let helical_delayed = self.helical.read(self.helical.max_delay());
self.helical.write(helical_in);
self.helical_state = helical_delayed;
let chirp_mod = 1.0 + 0.01 * self.chirp_phase.sin();
self.chirp_phase += self.chirp_rate;
if self.chirp_phase > std::f32::consts::TAU {
self.chirp_phase -= std::f32::consts::TAU;
}
let wet = (s1_out + s2_out + helical_delayed * 0.3) * (chirp_mod / 3.0);
wet * self.config.wet_mix + input * (1.0 - self.config.wet_mix)
}
fn reset(&mut self) {
self.spring1.reset();
self.spring2.reset();
self.helical.clear();
self.helical_state = 0.0;
for ap in &mut self.diffuser {
ap.reset();
}
self.chirp_phase = 0.0;
}
fn wet_mix(&self) -> f32 {
self.config.wet_mix
}
fn set_wet_mix(&mut self, wet: f32) {
self.config.wet_mix = wet.clamp(0.0, 1.0);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::AudioEffect;
fn make_noise(num_samples: usize) -> Vec<f32> {
let mut state = 12345_u32;
(0..num_samples)
.map(|_| {
state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
(state as f32 / u32::MAX as f32) * 2.0 - 1.0
})
.collect()
}
#[test]
fn test_spring_reverb_default_config() {
let _reverb = SpringReverb::new(SpringReverbConfig::default(), 48000.0);
}
#[test]
fn test_spring_reverb_output_finite() {
let mut reverb = SpringReverb::new(SpringReverbConfig::default(), 48000.0);
let noise = make_noise(2000);
for &s in &noise {
let out = reverb.process_sample(s);
assert!(out.is_finite(), "Output must remain finite, got: {out}");
}
}
#[test]
fn test_spring_reverb_silence_input_decays() {
let mut reverb = SpringReverb::new(SpringReverbConfig::default(), 48000.0);
for _ in 0..4800 {
reverb.process_sample(0.5);
}
let mut all_finite = true;
for _ in 0..24000 {
let out = reverb.process_sample(0.0);
if !out.is_finite() {
all_finite = false;
break;
}
}
assert!(
all_finite,
"Spring reverb output must remain finite during decay"
);
}
#[test]
fn test_spring_reverb_preset_vintage_tank() {
let mut reverb = SpringReverb::vintage_tank(48000.0);
let noise = make_noise(1024);
for &s in &noise {
let out = reverb.process_sample(s);
assert!(out.is_finite());
}
}
#[test]
fn test_spring_reverb_preset_guitar_amp() {
let mut reverb = SpringReverb::guitar_amp(48000.0);
let noise = make_noise(1024);
for &s in &noise {
let out = reverb.process_sample(s);
assert!(out.is_finite());
}
}
#[test]
fn test_spring_reverb_preset_large_tank() {
let mut reverb = SpringReverb::large_tank(48000.0);
let noise = make_noise(1024);
for &s in &noise {
let out = reverb.process_sample(s);
assert!(out.is_finite());
}
}
#[test]
fn test_spring_reverb_wet_dry_mix() {
let mut reverb = SpringReverb::new(SpringReverbConfig::default(), 48000.0);
assert!((reverb.wet_mix() - 0.4).abs() < 1e-6);
reverb.set_wet_mix(0.8);
assert!((reverb.wet_mix() - 0.8).abs() < 1e-6);
reverb.set_wet_mix(2.0);
assert!((reverb.wet_mix() - 1.0).abs() < 1e-6);
reverb.set_wet_mix(-1.0);
assert!((reverb.wet_mix() - 0.0).abs() < 1e-6);
}
#[test]
fn test_spring_reverb_reset() {
let mut reverb = SpringReverb::new(SpringReverbConfig::default(), 48000.0);
for _ in 0..1000 {
reverb.process_sample(0.9);
}
reverb.reset();
let out = reverb.process_sample(0.0);
assert!(
out.abs() < 1e-6,
"After reset with zero input, output should be zero: {out}"
);
}
#[test]
fn test_spring_reverb_adds_decay() {
let mut reverb = SpringReverb::new(
SpringReverbConfig {
wet_mix: 1.0,
..Default::default()
},
48000.0,
);
let _ = reverb.process_sample(1.0);
let mut energy = 0.0_f32;
for _ in 0..4800 {
let out = reverb.process_sample(0.0);
energy += out * out;
}
assert!(
energy > 0.0,
"Spring reverb should produce a decay tail: energy={energy}"
);
}
#[test]
fn test_spring_reverb_set_tension() {
let mut reverb = SpringReverb::new(SpringReverbConfig::default(), 48000.0);
reverb.set_tension(0.9);
assert!((reverb.config.tension - 0.9).abs() < 1e-6);
reverb.set_tension(5.0);
assert!((reverb.config.tension - 1.0).abs() < 1e-6);
reverb.set_tension(-1.0);
assert!((reverb.config.tension - 0.1).abs() < 1e-6);
}
}