use super::support::{lerp, smooth_towards};
use crate::contracts::AudioProcessor;
use crate::core::{
AudioError, AudioFrame, AudioResult, AudioSpec, DelayEffectConfig, DelayEffectPreset,
};
use std::f32::consts::TAU;
#[allow(dead_code)]
pub struct DoublingDelayProcessor {
base_delay_ms: f32,
wet_mix: f32,
feedback: f32,
modulation_depth_ms: f32,
modulation_rate_hz: f32,
damping_hz: f32,
wet_high_cut_hz: f32,
wet_sibilance_hz: f32,
wet_sibilance_drive: f32,
wet_sibilance_suppression: f32,
body_low_hz: f32,
body_high_hz: f32,
body_mix: f32,
post_wet_cut_hz: f32,
output_high_cut_hz: f32,
ducking: f32,
silence_gate: f32,
silence_reset_ms: f32,
onset_ramp_ms: f32,
tail_release_suppression: f32,
sample_rate: f32,
buffer: Vec<f32>,
write_index: usize,
lfo_phase: f32,
lfo_increment: f32,
damping_alpha: f32,
wet_tone_alpha: f32,
wet_sibilance_alpha: f32,
body_low_alpha: f32,
body_high_alpha: f32,
post_wet_alpha: f32,
output_high_cut_alpha: f32,
tone_state: f32,
wet_tone_state: f32,
wet_sibilance_state: f32,
body_low_state: f32,
body_high_state: f32,
post_wet_state_a: f32,
post_wet_state_b: f32,
output_high_cut_state_a: f32,
output_high_cut_state_b: f32,
input_envelope: f32,
silence_run: usize,
silence_reset_samples: usize,
onset_ramp_samples: usize,
onset_ramp_progress: usize,
wet_armed: bool,
}
impl DoublingDelayProcessor {
fn soft_saturate(sample: f32) -> f32 {
if sample.abs() <= 0.72 {
sample
} else {
let sign = sample.signum();
let excess = sample.abs() - 0.72;
sign * (0.72 + 0.28 * (excess / 0.28).tanh())
}
}
#[allow(dead_code)]
pub fn new(delay_ms: f32, wet_mix: f32, feedback: f32) -> Self {
Self::with_character(
delay_ms, wet_mix, feedback, 0.0, 0.0, 12_000.0, 4_800.0, 1.0, 0.0, 12_000.0, 140.0,
420.0, 0.0, 12_000.0, 20_000.0, 0.0, 0.008, 120.0, 18.0, 0.0,
)
}
pub fn from_config(config: DelayEffectConfig) -> Self {
match config.preset {
DelayEffectPreset::VoiceDoubler => Self::voice_doubler(),
DelayEffectPreset::Thickener => Self::thickener(),
DelayEffectPreset::Slapback => Self::slapback(),
DelayEffectPreset::Bloom => Self::bloom(),
}
}
pub fn voice_doubler() -> Self {
Self::with_character(
8.8, 0.066, 0.0, 0.08, 0.03, 3_900.0, 3_000.0, 1.20, 0.44, 2_250.0, 200.0, 420.0, 0.12,
1_450.0, 4_600.0, 0.78, 0.010, 28.0, 7.0, 0.92,
)
}
pub fn thickener() -> Self {
Self::with_character(
22.0, 0.32, 0.14, 0.9, 0.18, 5_400.0, 4_400.0, 1.15, 0.14, 4_600.0, 150.0, 390.0, 0.16,
4_000.0, 7_500.0, 0.24, 0.008, 130.0, 16.0, 0.18,
)
}
pub fn slapback() -> Self {
Self::with_character(
86.0, 0.48, 0.28, 2.4, 0.22, 3_600.0, 4_800.0, 1.0, 0.08, 3_800.0, 140.0, 360.0, 0.08,
3_400.0, 8_500.0, 0.18, 0.007, 180.0, 10.0, 0.08,
)
}
pub fn bloom() -> Self {
Self::with_character(
58.0, 0.44, 0.42, 4.8, 0.27, 2_800.0, 4_600.0, 1.0, 0.10, 3_200.0, 130.0, 320.0, 0.12,
2_900.0, 7_200.0, 0.28, 0.007, 180.0, 12.0, 0.12,
)
}
fn with_character(
delay_ms: f32,
wet_mix: f32,
feedback: f32,
modulation_depth_ms: f32,
modulation_rate_hz: f32,
damping_hz: f32,
wet_sibilance_hz: f32,
wet_sibilance_drive: f32,
wet_sibilance_suppression: f32,
wet_high_cut_hz: f32,
body_low_hz: f32,
body_high_hz: f32,
body_mix: f32,
post_wet_cut_hz: f32,
output_high_cut_hz: f32,
ducking: f32,
silence_gate: f32,
silence_reset_ms: f32,
onset_ramp_ms: f32,
tail_release_suppression: f32,
) -> Self {
Self {
base_delay_ms: delay_ms.clamp(1.0, 180.0),
wet_mix: wet_mix.clamp(0.0, 0.75),
feedback: feedback.clamp(0.0, 0.82),
modulation_depth_ms: modulation_depth_ms.clamp(0.0, 12.0),
modulation_rate_hz: modulation_rate_hz.clamp(0.0, 6.0),
damping_hz: damping_hz.clamp(800.0, 16_000.0),
wet_sibilance_hz: wet_sibilance_hz.clamp(2_200.0, 9_000.0),
wet_sibilance_drive: wet_sibilance_drive.clamp(0.6, 2.5),
wet_sibilance_suppression: wet_sibilance_suppression.clamp(0.0, 0.85),
wet_high_cut_hz: wet_high_cut_hz.clamp(1_200.0, 16_000.0),
body_low_hz: body_low_hz.clamp(60.0, 600.0),
body_high_hz: body_high_hz.clamp(120.0, 1_200.0),
body_mix: body_mix.clamp(0.0, 0.45),
post_wet_cut_hz: post_wet_cut_hz.clamp(1_200.0, 16_000.0),
output_high_cut_hz: output_high_cut_hz.clamp(1_800.0, 20_000.0),
ducking: ducking.clamp(0.0, 0.85),
silence_gate: silence_gate.clamp(0.002, 0.04),
silence_reset_ms: silence_reset_ms.clamp(40.0, 500.0),
onset_ramp_ms: onset_ramp_ms.clamp(2.0, 60.0),
tail_release_suppression: tail_release_suppression.clamp(0.0, 0.9),
sample_rate: 0.0,
buffer: Vec::new(),
write_index: 0,
lfo_phase: 0.0,
lfo_increment: 0.0,
damping_alpha: 1.0,
wet_tone_alpha: 1.0,
wet_sibilance_alpha: 1.0,
body_low_alpha: 1.0,
body_high_alpha: 1.0,
post_wet_alpha: 1.0,
output_high_cut_alpha: 1.0,
tone_state: 0.0,
wet_tone_state: 0.0,
wet_sibilance_state: 0.0,
body_low_state: 0.0,
body_high_state: 0.0,
post_wet_state_a: 0.0,
post_wet_state_b: 0.0,
output_high_cut_state_a: 0.0,
output_high_cut_state_b: 0.0,
input_envelope: 0.0,
silence_run: 0,
silence_reset_samples: 0,
onset_ramp_samples: 0,
onset_ramp_progress: 0,
wet_armed: false,
}
}
fn delay_samples(&self) -> f32 {
(self.base_delay_ms * self.sample_rate) / 1_000.0
}
fn modulation_depth_samples(&self) -> f32 {
(self.modulation_depth_ms * self.sample_rate) / 1_000.0
}
fn read_interpolated(&self, delay_samples: f32) -> f32 {
let len = self.buffer.len();
let read_position = (self.write_index as f32 - delay_samples).rem_euclid(len.max(1) as f32);
let first_index = read_position.floor() as usize % len;
let second_index = (first_index + 1) % len;
let fraction = read_position - first_index as f32;
lerp(
self.buffer[first_index],
self.buffer[second_index],
fraction.clamp(0.0, 1.0),
)
}
fn lowpass_alpha(cutoff_hz: f32, sample_rate: f32) -> f32 {
(1.0 - (-TAU * cutoff_hz / sample_rate.max(1.0)).exp()).clamp(0.0, 1.0)
}
}
impl AudioProcessor for DoublingDelayProcessor {
fn name(&self) -> &'static str {
"doubling_delay"
}
fn prepare(&mut self, spec: AudioSpec) -> AudioResult<()> {
self.sample_rate = spec.sample_rate as f32;
let max_delay_samples =
(self.delay_samples() + self.modulation_depth_samples()).ceil() as usize + 4;
self.buffer = vec![0.0; max_delay_samples.max(4)];
self.write_index = 0;
self.lfo_phase = 0.0;
self.lfo_increment = if self.modulation_rate_hz <= f32::EPSILON {
0.0
} else {
TAU * self.modulation_rate_hz / self.sample_rate.max(1.0)
};
self.damping_alpha = Self::lowpass_alpha(self.damping_hz, self.sample_rate);
self.wet_sibilance_alpha = Self::lowpass_alpha(self.wet_sibilance_hz, self.sample_rate);
self.wet_tone_alpha = Self::lowpass_alpha(self.wet_high_cut_hz, self.sample_rate);
self.body_low_alpha = Self::lowpass_alpha(self.body_low_hz, self.sample_rate);
self.body_high_alpha = Self::lowpass_alpha(self.body_high_hz, self.sample_rate);
self.post_wet_alpha = Self::lowpass_alpha(self.post_wet_cut_hz, self.sample_rate);
self.output_high_cut_alpha = Self::lowpass_alpha(self.output_high_cut_hz, self.sample_rate);
self.silence_reset_samples =
((self.silence_reset_ms * self.sample_rate) / 1_000.0).round() as usize;
self.onset_ramp_samples =
((self.onset_ramp_ms * self.sample_rate) / 1_000.0).round() as usize;
self.tone_state = 0.0;
self.wet_tone_state = 0.0;
self.wet_sibilance_state = 0.0;
self.body_low_state = 0.0;
self.body_high_state = 0.0;
self.post_wet_state_a = 0.0;
self.post_wet_state_b = 0.0;
self.output_high_cut_state_a = 0.0;
self.output_high_cut_state_b = 0.0;
self.input_envelope = 0.0;
self.silence_run = 0;
self.onset_ramp_progress = 0;
self.wet_armed = false;
Ok(())
}
fn process(&mut self, frame: &mut AudioFrame) -> AudioResult<()> {
if frame.samples.is_empty() {
return Ok(());
}
if self.buffer.is_empty() {
return Err(AudioError::new("Doubling delay not prepared"));
}
if self.wet_mix <= f32::EPSILON {
return Ok(());
}
let base_delay = self.delay_samples();
let modulation_depth = self.modulation_depth_samples();
let dry_mix = (1.0 - self.wet_mix * 0.30).clamp(0.72, 1.0);
for sample in &mut frame.samples {
let input = *sample;
let abs_input = input.abs();
let envelope_smoothing = if abs_input > self.input_envelope {
0.24
} else {
0.03
};
self.input_envelope =
smooth_towards(self.input_envelope, abs_input, envelope_smoothing);
let effectively_silent =
abs_input <= self.silence_gate && self.input_envelope <= self.silence_gate * 1.8;
if effectively_silent {
self.silence_run = self.silence_run.saturating_add(1);
if self.silence_run >= self.silence_reset_samples && !self.wet_armed {
self.buffer.fill(0.0);
self.write_index = 0;
self.tone_state = 0.0;
self.wet_tone_state = 0.0;
self.wet_sibilance_state = 0.0;
self.body_low_state = 0.0;
self.body_high_state = 0.0;
self.post_wet_state_a = 0.0;
self.post_wet_state_b = 0.0;
self.output_high_cut_state_a = 0.0;
self.output_high_cut_state_b = 0.0;
self.onset_ramp_progress = 0;
self.wet_armed = true;
}
} else {
self.silence_run = 0;
}
let modulation = self.lfo_phase.sin() * modulation_depth;
self.lfo_phase = (self.lfo_phase + self.lfo_increment).rem_euclid(TAU);
let delayed = self.read_interpolated((base_delay + modulation).max(1.0));
self.tone_state += (delayed - self.tone_state) * self.damping_alpha;
let damped = self.tone_state;
self.wet_sibilance_state +=
(damped - self.wet_sibilance_state) * self.wet_sibilance_alpha;
let sibilance_band = (damped - self.wet_sibilance_state) * self.wet_sibilance_drive;
let sibilance_level = (sibilance_band.abs() - 0.010).max(0.0);
let sibilance_reduction =
1.0 - (sibilance_level * 14.0 * self.wet_sibilance_suppression).clamp(0.0, 0.72);
let deessed = damped * sibilance_reduction;
self.wet_tone_state += (deessed - self.wet_tone_state) * self.wet_tone_alpha;
self.body_low_state +=
(self.wet_tone_state - self.body_low_state) * self.body_low_alpha;
self.body_high_state +=
(self.wet_tone_state - self.body_high_state) * self.body_high_alpha;
let body_band = self.body_high_state - self.body_low_state;
let colored_wet = self.wet_tone_state + body_band * self.body_mix;
self.post_wet_state_a += (colored_wet - self.post_wet_state_a) * self.post_wet_alpha;
self.post_wet_state_b +=
(self.post_wet_state_a - self.post_wet_state_b) * self.post_wet_alpha;
let hard_cut_wet = self.post_wet_state_b;
let onset_gain = if self.wet_armed {
if self.onset_ramp_samples == 0 {
self.wet_armed = false;
1.0
} else {
let gain = (self.onset_ramp_progress as f32 / self.onset_ramp_samples as f32)
.clamp(0.0, 1.0);
self.onset_ramp_progress = self.onset_ramp_progress.saturating_add(1);
if self.onset_ramp_progress >= self.onset_ramp_samples {
self.wet_armed = false;
}
gain
}
} else {
1.0
};
let ducking_gain =
1.0 - self.ducking * ((self.input_envelope - 0.08) / 0.30).clamp(0.0, 1.0);
let tail_silence = ((self.silence_gate * 2.2 - self.input_envelope)
/ (self.silence_gate * 1.8))
.clamp(0.0, 1.0);
let tail_gain = 1.0 - self.tail_release_suppression * tail_silence;
let wet = Self::soft_saturate(
hard_cut_wet * self.wet_mix * ducking_gain * onset_gain * tail_gain,
);
let feedback_sample = (input + damped * self.feedback).clamp(-1.0, 1.0);
self.buffer[self.write_index] = feedback_sample;
self.write_index = (self.write_index + 1) % self.buffer.len();
let mixed = Self::soft_saturate(input * dry_mix + wet);
self.output_high_cut_state_a +=
(mixed - self.output_high_cut_state_a) * self.output_high_cut_alpha;
self.output_high_cut_state_b += (self.output_high_cut_state_a
- self.output_high_cut_state_b)
* self.output_high_cut_alpha;
*sample = self.output_high_cut_state_b.clamp(-1.0, 1.0);
}
Ok(())
}
fn reset(&mut self) {
self.buffer.fill(0.0);
self.write_index = 0;
self.lfo_phase = 0.0;
self.tone_state = 0.0;
self.wet_tone_state = 0.0;
self.wet_sibilance_state = 0.0;
self.body_low_state = 0.0;
self.body_high_state = 0.0;
self.post_wet_state_a = 0.0;
self.post_wet_state_b = 0.0;
self.output_high_cut_state_a = 0.0;
self.output_high_cut_state_b = 0.0;
self.input_envelope = 0.0;
self.silence_run = 0;
self.onset_ramp_progress = 0;
self.wet_armed = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{AudioSpec, DelayEffectConfig, DelayEffectPreset};
fn spec() -> AudioSpec {
AudioSpec {
sample_rate: 1_000,
channels: 1,
}
}
#[test]
fn effect_injects_delayed_tap_for_impulse() {
let mut processor = DoublingDelayProcessor::new(3.0, 0.5, 0.0);
processor.prepare(spec()).unwrap();
let mut frame = AudioFrame {
samples: vec![1.0, 0.0, 0.0, 0.0, 0.0],
spec: spec(),
};
processor.process(&mut frame).unwrap();
assert!(frame.samples[0] < 1.0);
assert_eq!(frame.samples[1], 0.0);
assert_eq!(frame.samples[2], 0.0);
assert!(frame.samples[3] > 0.35);
}
#[test]
fn feedback_creates_decay_on_following_echoes() {
let mut processor = DoublingDelayProcessor::new(2.0, 0.4, 0.5);
processor.prepare(spec()).unwrap();
let mut frame = AudioFrame {
samples: vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
spec: spec(),
};
processor.process(&mut frame).unwrap();
assert!(frame.samples[2] > 0.2);
assert!(frame.samples[4] > 0.08);
assert!(frame.samples[4] < frame.samples[2]);
}
#[test]
fn wet_mix_zero_preserves_signal() {
let mut processor = DoublingDelayProcessor::new(15.0, 0.0, 0.6);
processor.prepare(spec()).unwrap();
let original = vec![0.2, -0.1, 0.05, -0.3];
let mut frame = AudioFrame {
samples: original.clone(),
spec: spec(),
};
processor.process(&mut frame).unwrap();
assert_eq!(frame.samples, original);
}
#[test]
fn effect_output_is_clamped_under_hot_feedback() {
let mut processor = DoublingDelayProcessor::slapback();
processor.prepare(spec()).unwrap();
let mut frame = AudioFrame {
samples: vec![1.0; 32],
spec: spec(),
};
processor.process(&mut frame).unwrap();
assert!(frame.samples.iter().all(|sample| sample.abs() <= 1.0));
}
#[test]
fn preset_config_maps_to_distinct_characters() {
let mut doubler = DoublingDelayProcessor::from_config(DelayEffectConfig {
preset: DelayEffectPreset::VoiceDoubler,
});
let mut slapback = DoublingDelayProcessor::from_config(DelayEffectConfig {
preset: DelayEffectPreset::Slapback,
});
doubler.prepare(spec()).unwrap();
slapback.prepare(spec()).unwrap();
assert!(slapback.delay_samples() > doubler.delay_samples());
assert!(slapback.feedback > doubler.feedback);
assert!(doubler.wet_mix < slapback.wet_mix);
assert!(doubler.post_wet_cut_hz < slapback.post_wet_cut_hz);
}
#[test]
fn silence_clears_tail_before_next_onset() {
let mut processor = DoublingDelayProcessor::voice_doubler();
processor.prepare(spec()).unwrap();
let mut first = AudioFrame {
samples: vec![
1.0, 0.8, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
],
spec: spec(),
};
processor.process(&mut first).unwrap();
let mut silence = AudioFrame {
samples: vec![0.0; 140],
spec: spec(),
};
processor.process(&mut silence).unwrap();
let mut onset = AudioFrame {
samples: vec![0.9, 0.0, 0.0, 0.0, 0.0, 0.0],
spec: spec(),
};
processor.process(&mut onset).unwrap();
assert!(onset.samples[0] <= 0.9);
assert!(onset.samples[1].abs() < 0.05);
}
}