Skip to main content

sim_lib_audio_dsp/
delay.rs

1use sim_lib_audio_graph_core::{PrepareConfig, ProcessBlock, Processor};
2
3use crate::common::{input_sample, prepare_channels, prepared_output_channels};
4
5/// A circular delay buffer with fractional, linearly interpolated reads.
6#[derive(Clone, Debug, PartialEq)]
7pub struct DelayLine {
8    buffer: Vec<f32>,
9    write: usize,
10}
11
12impl DelayLine {
13    /// Creates a delay line holding at least `max_delay_samples` samples.
14    pub fn new(max_delay_samples: usize) -> Self {
15        Self {
16            buffer: vec![0.0; max_delay_samples.max(2) + 2],
17            write: 0,
18        }
19    }
20
21    /// Clears the buffer and resets the write position.
22    pub fn reset(&mut self) {
23        self.buffer.fill(0.0);
24        self.write = 0;
25    }
26
27    #[cfg(all(test, not(debug_assertions)))]
28    pub(crate) fn allocated_capacity(&self) -> usize {
29        self.buffer.capacity()
30    }
31
32    /// Reads the sample `delay_samples` in the past, interpolating fractional
33    /// delays.
34    pub fn read(&self, delay_samples: f32) -> f32 {
35        let len = self.buffer.len();
36        let delay = delay_samples.clamp(0.0, (len - 2) as f32);
37        let read = (self.write as f32 - delay).rem_euclid(len as f32);
38        let i0 = read.floor() as usize % len;
39        let i1 = (i0 + 1) % len;
40        let frac = read - i0 as f32;
41        self.buffer[i0] * (1.0 - frac) + self.buffer[i1] * frac
42    }
43
44    /// Writes a sample at the current position and advances the write head.
45    pub fn push(&mut self, sample: f32) {
46        self.buffer[self.write] = sample;
47        self.write = (self.write + 1) % self.buffer.len();
48    }
49}
50
51/// A per-channel delay [`Processor`] with feedback and dry/wet mix.
52#[derive(Clone, Debug, PartialEq)]
53pub struct DelayProcessor {
54    delay_seconds: f32,
55    max_delay_seconds: f32,
56    feedback: f32,
57    wet: f32,
58    dry: f32,
59    sample_rate_hz: f32,
60    lines: Vec<DelayLine>,
61}
62
63impl DelayProcessor {
64    /// Creates a delay processor with the given delay and maximum delay, in
65    /// seconds, defaulting to a fully wet, feedback-free mix.
66    pub fn new(delay_seconds: f32, max_delay_seconds: f32) -> Self {
67        Self {
68            delay_seconds: delay_seconds.max(0.0),
69            max_delay_seconds: max_delay_seconds.max(delay_seconds).max(0.001),
70            feedback: 0.0,
71            wet: 1.0,
72            dry: 0.0,
73            sample_rate_hz: 48_000.0,
74            lines: Vec::new(),
75        }
76    }
77
78    /// Creates a delay processor from delay and maximum delay in milliseconds.
79    pub fn milliseconds(delay_ms: f32, max_delay_ms: f32) -> Self {
80        Self::new(delay_ms / 1000.0, max_delay_ms / 1000.0)
81    }
82
83    /// Returns the processor with explicit dry and wet mix levels.
84    pub fn with_mix(mut self, dry: f32, wet: f32) -> Self {
85        self.dry = dry;
86        self.wet = wet;
87        self
88    }
89
90    /// Returns the processor with feedback set, clamped to `-0.99..=0.99`.
91    pub fn with_feedback(mut self, feedback: f32) -> Self {
92        self.feedback = feedback.clamp(-0.99, 0.99);
93        self
94    }
95
96    fn delay_samples(&self) -> f32 {
97        self.delay_seconds * self.sample_rate_hz
98    }
99
100    fn max_delay_samples(&self) -> usize {
101        (self.max_delay_seconds * self.sample_rate_hz).ceil() as usize
102    }
103
104    #[cfg(all(test, not(debug_assertions)))]
105    pub(crate) fn realtime_state_snapshot(&self) -> Vec<usize> {
106        let mut snapshot = Vec::with_capacity(self.lines.len() + 1);
107        snapshot.push(self.lines.capacity());
108        snapshot.extend(self.lines.iter().map(DelayLine::allocated_capacity));
109        snapshot
110    }
111}
112
113impl Processor for DelayProcessor {
114    fn prepare(&mut self, cfg: PrepareConfig) {
115        self.sample_rate_hz = cfg.sample_rate_hz as f32;
116        let line = DelayLine::new(self.max_delay_samples());
117        prepare_channels(&mut self.lines, cfg.out_channels as usize, line);
118    }
119
120    fn reset(&mut self) {
121        for line in &mut self.lines {
122            line.reset();
123        }
124    }
125
126    fn process(&mut self, block: &mut ProcessBlock<'_>) {
127        let channels = prepared_output_channels(block, self.lines.len(), "DelayProcessor");
128        let delay = self.delay_samples();
129        let frames = block.frames as usize;
130        for channel in 0..channels {
131            let line = &mut self.lines[channel];
132            for frame in 0..frames {
133                let input = input_sample(block, channel, frame);
134                let delayed = if delay <= f32::EPSILON {
135                    input
136                } else {
137                    line.read(delay)
138                };
139                line.push(input + delayed * self.feedback);
140                block.out_audio[channel][frame] = input * self.dry + delayed * self.wet;
141            }
142        }
143    }
144
145    fn tail_frames(&self) -> u64 {
146        self.delay_samples().ceil() as u64
147    }
148}
149
150/// A fully wet fractional delay [`Processor`] wrapping [`DelayProcessor`].
151#[derive(Clone, Debug, PartialEq)]
152pub struct FractionalDelay {
153    inner: DelayProcessor,
154}
155
156impl FractionalDelay {
157    /// Creates a fractional delay from delay and maximum delay in milliseconds.
158    pub fn milliseconds(delay_ms: f32, max_delay_ms: f32) -> Self {
159        Self {
160            inner: DelayProcessor::milliseconds(delay_ms, max_delay_ms),
161        }
162    }
163
164    #[cfg(all(test, not(debug_assertions)))]
165    pub(crate) fn realtime_state_snapshot(&self) -> Vec<usize> {
166        self.inner.realtime_state_snapshot()
167    }
168}
169
170impl Processor for FractionalDelay {
171    fn prepare(&mut self, cfg: PrepareConfig) {
172        self.inner.prepare(cfg);
173    }
174
175    fn reset(&mut self) {
176        self.inner.reset();
177    }
178
179    fn process(&mut self, block: &mut ProcessBlock<'_>) {
180        self.inner.process(block);
181    }
182
183    fn tail_frames(&self) -> u64 {
184        self.inner.tail_frames()
185    }
186}
187
188/// A feedback comb filter [`Processor`] built on a delay line.
189#[derive(Clone, Debug, PartialEq)]
190pub struct CombFilter {
191    delay: DelayProcessor,
192}
193
194impl CombFilter {
195    /// Creates a comb filter with the given delay (ms) and feedback amount.
196    pub fn milliseconds(delay_ms: f32, feedback: f32) -> Self {
197        Self {
198            delay: DelayProcessor::milliseconds(delay_ms, delay_ms.max(1.0))
199                .with_feedback(feedback)
200                .with_mix(0.0, 1.0),
201        }
202    }
203
204    #[cfg(all(test, not(debug_assertions)))]
205    pub(crate) fn realtime_state_snapshot(&self) -> Vec<usize> {
206        self.delay.realtime_state_snapshot()
207    }
208}
209
210impl Processor for CombFilter {
211    fn prepare(&mut self, cfg: PrepareConfig) {
212        self.delay.prepare(cfg);
213    }
214
215    fn reset(&mut self) {
216        self.delay.reset();
217    }
218
219    fn process(&mut self, block: &mut ProcessBlock<'_>) {
220        self.delay.process(block);
221    }
222
223    fn tail_frames(&self) -> u64 {
224        self.delay.tail_frames()
225    }
226}
227
228/// A Schroeder all-pass filter [`Processor`] with per-channel delay lines.
229#[derive(Clone, Debug, PartialEq)]
230pub struct AllPassFilter {
231    delay_seconds: f32,
232    feedback: f32,
233    sample_rate_hz: f32,
234    lines: Vec<DelayLine>,
235}
236
237impl AllPassFilter {
238    /// Creates an all-pass filter with the given delay (ms) and feedback.
239    pub fn milliseconds(delay_ms: f32, feedback: f32) -> Self {
240        Self {
241            delay_seconds: (delay_ms / 1000.0).max(0.0),
242            feedback: feedback.clamp(-0.99, 0.99),
243            sample_rate_hz: 48_000.0,
244            lines: Vec::new(),
245        }
246    }
247
248    fn delay_samples(&self) -> f32 {
249        self.delay_seconds * self.sample_rate_hz
250    }
251
252    #[cfg(all(test, not(debug_assertions)))]
253    pub(crate) fn realtime_state_snapshot(&self) -> Vec<usize> {
254        let mut snapshot = Vec::with_capacity(self.lines.len() + 1);
255        snapshot.push(self.lines.capacity());
256        snapshot.extend(self.lines.iter().map(DelayLine::allocated_capacity));
257        snapshot
258    }
259}
260
261impl Processor for AllPassFilter {
262    fn prepare(&mut self, cfg: PrepareConfig) {
263        self.sample_rate_hz = cfg.sample_rate_hz as f32;
264        let samples = self.delay_samples().ceil() as usize;
265        prepare_channels(
266            &mut self.lines,
267            cfg.out_channels as usize,
268            DelayLine::new(samples),
269        );
270    }
271
272    fn reset(&mut self) {
273        for line in &mut self.lines {
274            line.reset();
275        }
276    }
277
278    fn process(&mut self, block: &mut ProcessBlock<'_>) {
279        let channels = prepared_output_channels(block, self.lines.len(), "AllPassFilter");
280        let delay = self.delay_samples();
281        let frames = block.frames as usize;
282        for channel in 0..channels {
283            let line = &mut self.lines[channel];
284            for frame in 0..frames {
285                let input = input_sample(block, channel, frame);
286                let delayed = line.read(delay);
287                let output = delayed - self.feedback * input;
288                line.push(input + self.feedback * output);
289                block.out_audio[channel][frame] = output;
290            }
291        }
292    }
293
294    fn tail_frames(&self) -> u64 {
295        self.delay_samples().ceil() as u64
296    }
297}