use serde::{Deserialize, Serialize};
use crate::buffer::AudioBuffer;
#[must_use]
#[derive(Debug, Clone)]
pub struct DelayLine {
buffers: Vec<Vec<f32>>,
write_pos: usize,
delay_samples: usize,
max_delay_samples: usize,
feedback: f32,
mix: f32,
bypassed: bool,
}
impl DelayLine {
pub fn new(
delay_ms: f32,
max_delay_ms: f32,
feedback: f32,
mix: f32,
sample_rate: u32,
channels: u32,
) -> Self {
tracing::debug!(
delay_ms,
max_delay_ms,
feedback,
mix,
sample_rate,
channels,
"DelayLine::new"
);
let max_samples = ((max_delay_ms.max(delay_ms) / 1000.0) * sample_rate as f32) as usize;
let max_samples = max_samples.max(1);
let delay_samples = ((delay_ms / 1000.0) * sample_rate as f32) as usize;
let delay_samples = delay_samples.min(max_samples);
Self {
buffers: vec![vec![0.0; max_samples]; channels as usize],
write_pos: 0,
delay_samples,
max_delay_samples: max_samples,
feedback: feedback.clamp(0.0, 0.99),
mix: mix.clamp(0.0, 1.0),
bypassed: false,
}
}
#[inline]
pub fn process(&mut self, buf: &mut AudioBuffer) {
if self.bypassed {
return;
}
let ch = buf.channels as usize;
for frame in 0..buf.frames {
for c in 0..ch {
let idx = frame * ch + c;
buf.samples[idx] = self.process_sample(buf.samples[idx], c);
if !buf.samples[idx].is_finite() {
buf.samples[idx] = 0.0;
}
}
self.write_pos = (self.write_pos + 1) % self.max_delay_samples;
}
}
fn process_sample(&mut self, input: f32, channel: usize) -> f32 {
let buf = &mut self.buffers[channel];
let read_pos =
(self.write_pos + self.max_delay_samples - self.delay_samples) % self.max_delay_samples;
let delayed = buf[read_pos];
buf[self.write_pos] = input + delayed * self.feedback;
input * (1.0 - self.mix) + delayed * self.mix
}
fn read_interpolated(&self, channel: usize, delay_frac: f32) -> f32 {
let buf = &self.buffers[channel];
let delay_int = delay_frac as usize;
let frac = delay_frac - delay_int as f32;
let pos0 = (self.write_pos + self.max_delay_samples - delay_int) % self.max_delay_samples;
let pos1 =
(self.write_pos + self.max_delay_samples - delay_int - 1) % self.max_delay_samples;
buf[pos0] * (1.0 - frac) + buf[pos1] * frac
}
pub fn set_bypass(&mut self, bypassed: bool) {
self.bypassed = bypassed;
}
pub fn is_bypassed(&self) -> bool {
self.bypassed
}
pub fn latency_frames(&self) -> usize {
self.delay_samples
}
pub fn set_delay_ms(&mut self, delay_ms: f32, sample_rate: u32) {
let samples = ((delay_ms / 1000.0) * sample_rate as f32) as usize;
self.delay_samples = samples.min(self.max_delay_samples);
}
pub fn reset(&mut self) {
for buf in &mut self.buffers {
buf.fill(0.0);
}
self.write_pos = 0;
}
}
#[must_use]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(default)]
pub struct ModulatedDelayParams {
pub base_delay_ms: f32,
pub depth_ms: f32,
pub rate_hz: f32,
pub feedback: f32,
pub mix: f32,
}
impl Default for ModulatedDelayParams {
fn default() -> Self {
Self {
base_delay_ms: 7.0,
depth_ms: 3.0,
rate_hz: 0.5,
feedback: 0.3,
mix: 0.5,
}
}
}
impl ModulatedDelayParams {
pub fn validate(&self) -> Result<(), &'static str> {
if self.base_delay_ms < 0.0 {
return Err("base_delay_ms must be >= 0.0");
}
if self.depth_ms < 0.0 {
return Err("depth_ms must be >= 0.0");
}
if self.rate_hz < 0.0 {
return Err("rate_hz must be >= 0.0");
}
if !(0.0..=0.99).contains(&self.feedback) {
return Err("feedback must be 0.0–0.99");
}
if !(0.0..=1.0).contains(&self.mix) {
return Err("mix must be 0.0–1.0");
}
Ok(())
}
}
#[must_use]
#[derive(Debug, Clone)]
pub struct ModulatedDelay {
delay: DelayLine,
params: ModulatedDelayParams,
lfo_phase: f64,
sample_rate: u32,
bypassed: bool,
}
impl ModulatedDelay {
pub fn new(params: ModulatedDelayParams, sample_rate: u32, channels: u32) -> Self {
tracing::debug!(?params, sample_rate, channels, "ModulatedDelay::new");
let max_ms = params.base_delay_ms + params.depth_ms + 1.0;
Self {
delay: DelayLine::new(
params.base_delay_ms,
max_ms,
params.feedback,
params.mix,
sample_rate,
channels,
),
params,
lfo_phase: 0.0,
sample_rate,
bypassed: false,
}
}
#[inline]
pub fn process(&mut self, buf: &mut AudioBuffer) {
if self.bypassed {
return;
}
let ch = buf.channels as usize;
let phase_inc = self.params.rate_hz as f64 / self.sample_rate as f64;
for frame in 0..buf.frames {
let lfo = (2.0 * std::f64::consts::PI * self.lfo_phase).sin() as f32;
let delay_ms = self.params.base_delay_ms + self.params.depth_ms * lfo;
let delay_frac = (delay_ms / 1000.0) * self.sample_rate as f32;
let delay_frac = delay_frac.max(0.0);
for c in 0..ch {
let idx = frame * ch + c;
let input = buf.samples[idx];
let delayed = self.delay.read_interpolated(c, delay_frac);
self.delay.buffers[c][self.delay.write_pos] =
input + delayed * self.params.feedback;
buf.samples[idx] = input * (1.0 - self.params.mix) + delayed * self.params.mix;
if !buf.samples[idx].is_finite() {
buf.samples[idx] = 0.0;
}
}
self.delay.write_pos = (self.delay.write_pos + 1) % self.delay.max_delay_samples;
self.lfo_phase = (self.lfo_phase + phase_inc) % 1.0;
}
}
pub fn latency_frames(&self) -> usize {
((self.params.base_delay_ms / 1000.0) * self.sample_rate as f32) as usize
}
pub fn set_bypass(&mut self, bypassed: bool) {
self.bypassed = bypassed;
}
pub fn is_bypassed(&self) -> bool {
self.bypassed
}
pub fn set_params(&mut self, params: ModulatedDelayParams) {
self.delay.feedback = params.feedback.clamp(0.0, 0.99);
self.delay.mix = params.mix.clamp(0.0, 1.0);
self.params = params;
}
pub fn set_sample_rate(&mut self, sample_rate: u32) {
self.sample_rate = sample_rate;
let channels = self.delay.buffers.len() as u32;
self.delay = DelayLine::new(
self.params.base_delay_ms,
self.params.base_delay_ms + self.params.depth_ms + 1.0,
self.params.feedback,
self.params.mix,
sample_rate,
channels,
);
self.lfo_phase = 0.0;
}
pub fn reset(&mut self) {
self.delay.reset();
self.lfo_phase = 0.0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_mix_passthrough() {
let mut buf = AudioBuffer::from_interleaved(vec![1.0, 0.5, 0.25, 0.0], 1, 44100).unwrap();
let original = buf.samples.clone();
let mut delay = DelayLine::new(10.0, 10.0, 0.0, 0.0, 44100, 1);
delay.process(&mut buf);
assert_eq!(buf.samples, original);
}
#[test]
fn delay_shifts_signal() {
let frames = 16;
let mut samples = vec![0.0f32; frames];
samples[0] = 1.0; let mut buf = AudioBuffer::from_interleaved(samples, 1, 1000).unwrap();
let mut delay = DelayLine::new(1.0, 1.0, 0.0, 1.0, 1000, 1);
delay.process(&mut buf);
assert!(
buf.samples[0].abs() < f32::EPSILON,
"frame 0 should be dry=0"
);
assert!(
(buf.samples[1] - 1.0).abs() < f32::EPSILON,
"frame 1 should have delayed impulse"
);
}
#[test]
fn feedback_produces_echoes() {
let frames = 32;
let mut samples = vec![0.0f32; frames];
samples[0] = 1.0;
let mut buf = AudioBuffer::from_interleaved(samples, 1, 1000).unwrap();
let mut delay = DelayLine::new(2.0, 2.0, 0.5, 1.0, 1000, 1);
delay.process(&mut buf);
assert!(buf.samples[2].abs() > 0.9);
assert!(buf.samples[4].abs() > 0.4);
assert!(buf.samples[4].abs() < buf.samples[2].abs());
}
#[test]
fn reset_clears_buffers() {
let mut delay = DelayLine::new(10.0, 10.0, 0.5, 1.0, 44100, 2);
let mut buf = AudioBuffer::from_interleaved(vec![1.0; 200], 2, 44100).unwrap();
delay.process(&mut buf);
delay.reset();
assert!(delay.buffers[0].iter().all(|&s| s == 0.0));
assert!(delay.buffers[1].iter().all(|&s| s == 0.0));
}
#[test]
fn modulated_delay_produces_output() {
let params = ModulatedDelayParams::default();
let mut modulated = ModulatedDelay::new(params, 44100, 1);
let samples: Vec<f32> = (0..4096)
.map(|i| (2.0 * std::f32::consts::PI * 440.0 * i as f32 / 44100.0).sin())
.collect();
let mut buf = AudioBuffer::from_interleaved(samples, 1, 44100).unwrap();
let original_rms = buf.rms();
modulated.process(&mut buf);
assert!(buf.rms() > 0.0);
assert!((buf.rms() - original_rms).abs() > 0.001 || buf.peak() > 0.0);
}
#[test]
fn modulated_delay_reset() {
let mut modulated = ModulatedDelay::new(ModulatedDelayParams::default(), 44100, 1);
modulated.lfo_phase = 0.5;
modulated.reset();
assert!((modulated.lfo_phase).abs() < f64::EPSILON);
}
}