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        let channels = output_channels(block);
115        if self.lines.len() < channels {
116            let max_delay_samples = self.max_delay_samples();
117            self.lines
118                .resize_with(channels, || DelayLine::new(max_delay_samples));
119        }
120        let delay = self.delay_samples();
121        let frames = block.frames as usize;
122        for channel in 0..channels {
123            let line = &mut self.lines[channel];
124            for frame in 0..frames {
125                let input = input_sample(block, channel, frame);
126                let delayed = line.read(delay);
127                line.push(input + delayed * self.feedback);
128                block.out_audio[channel][frame] = input * self.dry + delayed * self.wet;
129            }
130        }
131    }
132
133    fn tail_frames(&self) -> u64 {
134        self.delay_samples().ceil() as u64
135    }
136}
137
138/// A fully wet fractional delay [`Processor`] wrapping [`DelayProcessor`].
139#[derive(Clone, Debug, PartialEq)]
140pub struct FractionalDelay {
141    inner: DelayProcessor,
142}
143
144impl FractionalDelay {
145    /// Creates a fractional delay from delay and maximum delay in milliseconds.
146    pub fn milliseconds(delay_ms: f32, max_delay_ms: f32) -> Self {
147        Self {
148            inner: DelayProcessor::milliseconds(delay_ms, max_delay_ms),
149        }
150    }
151}
152
153impl Processor for FractionalDelay {
154    fn prepare(&mut self, cfg: PrepareConfig) {
155        self.inner.prepare(cfg);
156    }
157
158    fn reset(&mut self) {
159        self.inner.reset();
160    }
161
162    fn process(&mut self, block: &mut ProcessBlock<'_>) {
163        self.inner.process(block);
164    }
165
166    fn tail_frames(&self) -> u64 {
167        self.inner.tail_frames()
168    }
169}
170
171/// A feedback comb filter [`Processor`] built on a delay line.
172#[derive(Clone, Debug, PartialEq)]
173pub struct CombFilter {
174    delay: DelayProcessor,
175}
176
177impl CombFilter {
178    /// Creates a comb filter with the given delay (ms) and feedback amount.
179    pub fn milliseconds(delay_ms: f32, feedback: f32) -> Self {
180        Self {
181            delay: DelayProcessor::milliseconds(delay_ms, delay_ms.max(1.0))
182                .with_feedback(feedback)
183                .with_mix(0.0, 1.0),
184        }
185    }
186}
187
188impl Processor for CombFilter {
189    fn prepare(&mut self, cfg: PrepareConfig) {
190        self.delay.prepare(cfg);
191    }
192
193    fn reset(&mut self) {
194        self.delay.reset();
195    }
196
197    fn process(&mut self, block: &mut ProcessBlock<'_>) {
198        self.delay.process(block);
199    }
200
201    fn tail_frames(&self) -> u64 {
202        self.delay.tail_frames()
203    }
204}
205
206/// A Schroeder all-pass filter [`Processor`] with per-channel delay lines.
207#[derive(Clone, Debug, PartialEq)]
208pub struct AllPassFilter {
209    delay_seconds: f32,
210    feedback: f32,
211    sample_rate_hz: f32,
212    lines: Vec<DelayLine>,
213}
214
215impl AllPassFilter {
216    /// Creates an all-pass filter with the given delay (ms) and feedback.
217    pub fn milliseconds(delay_ms: f32, feedback: f32) -> Self {
218        Self {
219            delay_seconds: (delay_ms / 1000.0).max(0.0),
220            feedback: feedback.clamp(-0.99, 0.99),
221            sample_rate_hz: 48_000.0,
222            lines: Vec::new(),
223        }
224    }
225
226    fn delay_samples(&self) -> f32 {
227        self.delay_seconds * self.sample_rate_hz
228    }
229}
230
231impl Processor for AllPassFilter {
232    fn prepare(&mut self, cfg: PrepareConfig) {
233        self.sample_rate_hz = cfg.sample_rate_hz as f32;
234        let samples = self.delay_samples().ceil() as usize;
235        prepare_channels(
236            &mut self.lines,
237            cfg.out_channels as usize,
238            DelayLine::new(samples),
239        );
240    }
241
242    fn reset(&mut self) {
243        for line in &mut self.lines {
244            line.reset();
245        }
246    }
247
248    fn process(&mut self, block: &mut ProcessBlock<'_>) {
249        let channels = output_channels(block);
250        if self.lines.len() < channels {
251            let delay_samples = self.delay_samples() as usize;
252            self.lines
253                .resize_with(channels, || DelayLine::new(delay_samples));
254        }
255        let delay = self.delay_samples();
256        let frames = block.frames as usize;
257        for channel in 0..channels {
258            let line = &mut self.lines[channel];
259            for frame in 0..frames {
260                let input = input_sample(block, channel, frame);
261                let delayed = line.read(delay);
262                let output = delayed - self.feedback * input;
263                line.push(input + self.feedback * output);
264                block.out_audio[channel][frame] = output;
265            }
266        }
267    }
268
269    fn tail_frames(&self) -> u64 {
270        self.delay_samples().ceil() as u64
271    }
272}