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