Skip to main content

proteus_lib/dsp/effects/diffusion_reverb/
mod.rs

1//! Algorithmic reverb with smooth diffusion.
2//!
3//! This reverb follows a classic Schroeder-style layout:
4//! 1) A pre-delay to separate the direct sound from the reverb onset.
5//! 2) A set of parallel lowpass feedback comb filters to build decay.
6//! 3) A pair of series allpass filters to smooth the diffusion.
7//!
8//! The comb filters create the overall decay envelope while the allpass
9//! stages smear transients so the tail sounds dense instead of grainy.
10
11use log::info;
12use serde::{Deserialize, Serialize};
13
14use super::EffectContext;
15
16const DEFAULT_PRE_DELAY_MS: u64 = 12;
17const DEFAULT_ROOM_SIZE_MS: u64 = 48;
18const DEFAULT_DECAY: f32 = 0.72;
19const DEFAULT_DAMPING: f32 = 0.35;
20const DEFAULT_DIFFUSION: f32 = 0.72;
21const MAX_DECAY: f32 = 0.98;
22const MAX_DAMPING: f32 = 0.99;
23const MAX_DIFFUSION: f32 = 0.9;
24
25const COMB_TUNING_MULTIPLIERS: [f32; 4] = [1.0, 1.33, 1.58, 1.91];
26const ALLPASS_TUNING_MULTIPLIERS: [f32; 2] = [0.28, 0.52];
27
28/// Serialized configuration for the diffusion reverb.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30#[serde(default)]
31pub struct DiffusionReverbSettings {
32    /// Pre-delay time in milliseconds.
33    pub pre_delay_ms: u64,
34    /// Base delay time in milliseconds that scales the comb filters.
35    pub room_size_ms: u64,
36    /// Feedback amount for the comb filters. Higher values mean longer decay.
37    pub decay: f32,
38    /// Lowpass damping applied inside the comb feedback path.
39    pub damping: f32,
40    /// Feedback amount for the allpass diffusers. Higher values increase density.
41    pub diffusion: f32,
42}
43
44impl DiffusionReverbSettings {
45    /// Create diffusion reverb settings with basic validation.
46    ///
47    /// # Arguments
48    /// - `pre_delay_ms`: Pre-delay time in milliseconds.
49    /// - `room_size_ms`: Base delay for the comb filters in milliseconds.
50    /// - `decay`: Comb feedback gain in the range `[0.0, 1.0)`.
51    /// - `damping`: Lowpass damping factor in the range `[0.0, 1.0)`.
52    /// - `diffusion`: Allpass feedback gain in the range `[0.0, 1.0)`.
53    ///
54    /// # Returns
55    /// The validated settings.
56    pub fn new(
57        pre_delay_ms: u64,
58        room_size_ms: u64,
59        decay: f32,
60        damping: f32,
61        diffusion: f32,
62    ) -> Self {
63        Self {
64            pre_delay_ms: pre_delay_ms.clamp(0, u64::MAX),
65            room_size_ms: room_size_ms.clamp(0, u64::MAX),
66            decay: decay.clamp(0.0, MAX_DECAY),
67            damping: damping.clamp(0.0, MAX_DAMPING),
68            diffusion: diffusion.clamp(0.0, MAX_DIFFUSION),
69        }
70    }
71
72    fn decay(&self) -> f32 {
73        self.decay.clamp(0.0, MAX_DECAY)
74    }
75
76    fn damping(&self) -> f32 {
77        self.damping.clamp(0.0, MAX_DAMPING)
78    }
79
80    fn diffusion(&self) -> f32 {
81        self.diffusion.clamp(0.0, MAX_DIFFUSION)
82    }
83}
84
85impl Default for DiffusionReverbSettings {
86    fn default() -> Self {
87        Self {
88            pre_delay_ms: DEFAULT_PRE_DELAY_MS,
89            room_size_ms: DEFAULT_ROOM_SIZE_MS,
90            decay: DEFAULT_DECAY,
91            damping: DEFAULT_DAMPING,
92            diffusion: DEFAULT_DIFFUSION,
93        }
94    }
95}
96
97/// Diffusion reverb effect (pre-delay + combs + allpass diffusion).
98#[derive(Clone, Serialize, Deserialize)]
99#[serde(default)]
100pub struct DiffusionReverbEffect {
101    pub enabled: bool,
102    #[serde(alias = "dry_wet", alias = "wet_dry")]
103    pub mix: f32,
104    #[serde(flatten)]
105    pub settings: DiffusionReverbSettings,
106    #[serde(skip)]
107    state: Option<DiffusionReverbState>,
108}
109
110impl Default for DiffusionReverbEffect {
111    fn default() -> Self {
112        Self {
113            enabled: true,
114            mix: 0.0,
115            settings: DiffusionReverbSettings::default(),
116            state: None,
117        }
118    }
119}
120
121impl std::fmt::Debug for DiffusionReverbEffect {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        f.debug_struct("DiffusionReverbEffect")
124            .field("enabled", &self.enabled)
125            .field("mix", &self.mix)
126            .field("settings", &self.settings)
127            .finish()
128    }
129}
130
131impl DiffusionReverbEffect {
132    /// Create a new diffusion reverb effect.
133    ///
134    /// # Arguments
135    /// - `mix`: Wet/dry mix in the range `[0.0, 1.0]`.
136    ///
137    /// # Returns
138    /// The configured diffusion reverb effect.
139    pub fn new(mix: f32) -> Self {
140        Self {
141            mix: mix.clamp(0.0, 1.0),
142            ..Default::default()
143        }
144    }
145
146    /// Process interleaved samples through the diffusion reverb.
147    ///
148    /// # Arguments
149    /// - `samples`: Interleaved input samples.
150    /// - `context`: Environment details (sample rate, channels, etc.).
151    /// - `drain`: When true, flush buffered tail data.
152    ///
153    /// # Returns
154    /// Processed interleaved samples.
155    pub fn process(&mut self, samples: &[f32], context: &EffectContext, drain: bool) -> Vec<f32> {
156        self.ensure_state(context);
157        if !self.enabled || self.mix <= 0.0 {
158            return samples.to_vec();
159        }
160
161        let Some(state) = self.state.as_mut() else {
162            return samples.to_vec();
163        };
164
165        if samples.is_empty() {
166            if drain {
167                return state.drain_tail(
168                    self.settings.decay(),
169                    self.settings.damping(),
170                    self.settings.diffusion(),
171                );
172            }
173            return Vec::new();
174        }
175
176        let mix = self.mix.clamp(0.0, 1.0);
177        let mut output = Vec::with_capacity(samples.len());
178        state.process_samples(
179            samples,
180            mix,
181            self.settings.decay(),
182            self.settings.damping(),
183            self.settings.diffusion(),
184            &mut output,
185        );
186        output
187    }
188
189    /// Reset any internal state buffers.
190    ///
191    /// # Returns
192    /// Nothing.
193    pub fn reset_state(&mut self) {
194        if let Some(state) = self.state.as_mut() {
195            state.reset();
196        }
197        self.state = None;
198    }
199
200    /// Mutable access to the diffusion reverb settings.
201    pub fn settings_mut(&mut self) -> &mut DiffusionReverbSettings {
202        &mut self.settings
203    }
204
205    fn ensure_state(&mut self, context: &EffectContext) {
206        let pre_delay_samples = delay_samples(
207            context.sample_rate,
208            context.channels,
209            self.settings.pre_delay_ms,
210        );
211        let room_size_samples = delay_samples(
212            context.sample_rate,
213            context.channels,
214            self.settings.room_size_ms,
215        );
216        let tuning = Tuning::new(pre_delay_samples, room_size_samples);
217        let needs_reset = self
218            .state
219            .as_ref()
220            .map(|state| state.tuning != tuning)
221            .unwrap_or(true);
222        if needs_reset {
223            self.state = Some(DiffusionReverbState::new(tuning));
224        }
225    }
226}
227
228#[derive(Debug, Clone, Copy, PartialEq)]
229struct Tuning {
230    pre_delay_samples: usize,
231    comb_samples: [usize; 4],
232    allpass_samples: [usize; 2],
233    max_delay: usize,
234}
235
236impl Tuning {
237    fn new(pre_delay_samples: usize, room_size_samples: usize) -> Self {
238        let comb_samples = COMB_TUNING_MULTIPLIERS
239            .map(|multiplier| (room_size_samples as f32 * multiplier).round() as usize);
240        let allpass_samples = ALLPASS_TUNING_MULTIPLIERS
241            .map(|multiplier| (room_size_samples as f32 * multiplier).round() as usize);
242        let max_delay = comb_samples
243            .iter()
244            .copied()
245            .chain(allpass_samples.iter().copied())
246            .chain([pre_delay_samples])
247            .max()
248            .unwrap_or(1)
249            .max(1);
250        Self {
251            pre_delay_samples: pre_delay_samples.max(1),
252            comb_samples: comb_samples.map(|value| value.max(1)),
253            allpass_samples: allpass_samples.map(|value| value.max(1)),
254            max_delay,
255        }
256    }
257}
258
259#[derive(Clone)]
260struct DiffusionReverbState {
261    tuning: Tuning,
262    pre_delay: DelayLine,
263    combs: [CombFilter; 4],
264    allpass: [AllpassFilter; 2],
265}
266
267impl DiffusionReverbState {
268    fn new(tuning: Tuning) -> Self {
269        info!("Using Diffusion Reverb!");
270        Self {
271            tuning,
272            pre_delay: DelayLine::new(tuning.pre_delay_samples),
273            combs: [
274                CombFilter::new(tuning.comb_samples[0]),
275                CombFilter::new(tuning.comb_samples[1]),
276                CombFilter::new(tuning.comb_samples[2]),
277                CombFilter::new(tuning.comb_samples[3]),
278            ],
279            allpass: [
280                AllpassFilter::new(tuning.allpass_samples[0]),
281                AllpassFilter::new(tuning.allpass_samples[1]),
282            ],
283        }
284    }
285
286    fn reset(&mut self) {
287        self.pre_delay.reset();
288        for comb in &mut self.combs {
289            comb.reset();
290        }
291        for allpass in &mut self.allpass {
292            allpass.reset();
293        }
294    }
295
296    fn process_samples(
297        &mut self,
298        samples: &[f32],
299        mix: f32,
300        decay: f32,
301        damping: f32,
302        diffusion: f32,
303        out: &mut Vec<f32>,
304    ) {
305        for &sample in samples {
306            let delayed = self.pre_delay.process(sample);
307            let mut comb_sum = 0.0;
308            for comb in &mut self.combs {
309                comb_sum += comb.process(delayed, decay, damping);
310            }
311            let mut wet = comb_sum * 0.25;
312            for allpass in &mut self.allpass {
313                wet = allpass.process(wet, diffusion);
314            }
315            let output = sample * (1.0 - mix) + wet * mix;
316            out.push(output);
317        }
318    }
319
320    fn drain_tail(&mut self, decay: f32, damping: f32, diffusion: f32) -> Vec<f32> {
321        let tail_samples = self.tuning.max_delay.saturating_mul(4).max(1);
322        let mut out = Vec::with_capacity(tail_samples);
323        for _ in 0..tail_samples {
324            let delayed = self.pre_delay.process(0.0);
325            let mut comb_sum = 0.0;
326            for comb in &mut self.combs {
327                comb_sum += comb.process(delayed, decay, damping);
328            }
329            let mut wet = comb_sum * 0.25;
330            for allpass in &mut self.allpass {
331                wet = allpass.process(wet, diffusion);
332            }
333            out.push(wet);
334        }
335        out
336    }
337}
338
339#[derive(Clone)]
340struct DelayLine {
341    buffer: Vec<f32>,
342    index: usize,
343}
344
345impl DelayLine {
346    fn new(len: usize) -> Self {
347        Self {
348            buffer: vec![0.0; len.max(1)],
349            index: 0,
350        }
351    }
352
353    fn reset(&mut self) {
354        self.buffer.fill(0.0);
355        self.index = 0;
356    }
357
358    fn process(&mut self, input: f32) -> f32 {
359        let output = self.buffer[self.index];
360        self.buffer[self.index] = input;
361        self.index += 1;
362        if self.index >= self.buffer.len() {
363            self.index = 0;
364        }
365        output
366    }
367}
368
369#[derive(Clone)]
370struct CombFilter {
371    buffer: Vec<f32>,
372    index: usize,
373    lowpass: f32,
374}
375
376impl CombFilter {
377    fn new(len: usize) -> Self {
378        Self {
379            buffer: vec![0.0; len.max(1)],
380            index: 0,
381            lowpass: 0.0,
382        }
383    }
384
385    fn reset(&mut self) {
386        self.buffer.fill(0.0);
387        self.index = 0;
388        self.lowpass = 0.0;
389    }
390
391    fn process(&mut self, input: f32, feedback: f32, damping: f32) -> f32 {
392        let delayed = self.buffer[self.index];
393        self.lowpass = delayed * (1.0 - damping) + self.lowpass * damping;
394        let output = self.lowpass;
395        self.buffer[self.index] = input + output * feedback;
396        self.index += 1;
397        if self.index >= self.buffer.len() {
398            self.index = 0;
399        }
400        output
401    }
402}
403
404#[derive(Clone)]
405struct AllpassFilter {
406    buffer: Vec<f32>,
407    index: usize,
408}
409
410impl AllpassFilter {
411    fn new(len: usize) -> Self {
412        Self {
413            buffer: vec![0.0; len.max(1)],
414            index: 0,
415        }
416    }
417
418    fn reset(&mut self) {
419        self.buffer.fill(0.0);
420        self.index = 0;
421    }
422
423    fn process(&mut self, input: f32, feedback: f32) -> f32 {
424        let delayed = self.buffer[self.index];
425        let output = delayed - feedback * input;
426        self.buffer[self.index] = input + delayed * feedback;
427        self.index += 1;
428        if self.index >= self.buffer.len() {
429            self.index = 0;
430        }
431        output
432    }
433}
434
435fn delay_samples(sample_rate: u32, channels: usize, duration_ms: u64) -> usize {
436    if duration_ms == 0 {
437        return 0;
438    }
439    let ns = duration_ms.saturating_mul(1_000_000);
440    let samples = ns.saturating_mul(sample_rate as u64) / 1_000_000_000 * channels as u64;
441    samples as usize
442}