1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
//! A power-of-two ring buffer delay line for audio DSP.
//!
//! [`DelayLine`] is a fixed-capacity circular buffer used internally by
//! reverb, chorus, flanger, and comb-filter algorithms. Samples are pushed
//! at the write head and tapped at arbitrary fractional offsets using linear
//! interpolation.
//!
//! # Example
//!
//! ```
//! use oximedia_audio::delay_line::DelayLine;
//!
//! let mut dl = DelayLine::new(1024);
//! for i in 0..16 {
//! dl.push(i as f32 * 0.1);
//! }
//! // Tap 4 samples ago
//! let val = dl.tap(4);
//! assert!((val - 1.1).abs() < 1e-5);
//! ```
#![allow(dead_code)]
/// A circular ring-buffer delay line.
///
/// The capacity is rounded up to the next power of two so that wrap-around
/// can be performed with a bitwise AND.
pub struct DelayLine {
buffer: Vec<f32>,
mask: usize,
write_pos: usize,
sample_rate: f32,
}
impl DelayLine {
/// Create a new delay line with at least `min_capacity` samples.
///
/// Actual capacity is rounded up to the next power of two.
#[must_use]
pub fn new(min_capacity: usize) -> Self {
let capacity = min_capacity.next_power_of_two().max(2);
Self {
buffer: vec![0.0; capacity],
mask: capacity - 1,
write_pos: 0,
sample_rate: 48_000.0,
}
}
/// Create a delay line sized to hold at least `max_delay_ms` milliseconds
/// at `sample_rate`.
#[must_use]
pub fn with_max_delay_ms(max_delay_ms: f32, sample_rate: f32) -> Self {
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
let samples = (max_delay_ms * 0.001 * sample_rate).ceil() as usize;
let mut dl = Self::new(samples + 1);
dl.sample_rate = sample_rate;
dl
}
/// Return the capacity of the delay line in samples.
#[must_use]
pub fn capacity(&self) -> usize {
self.buffer.len()
}
/// Return the maximum delay expressible with this buffer, in milliseconds.
#[must_use]
pub fn max_delay_ms(&self) -> f32 {
#[allow(clippy::cast_precision_loss)]
let cap = self.buffer.len() as f32;
cap / self.sample_rate * 1000.0
}
/// Push a single sample into the delay line, advancing the write head.
pub fn push(&mut self, sample: f32) {
self.buffer[self.write_pos] = sample;
self.write_pos = (self.write_pos + 1) & self.mask;
}
/// Read a sample `delay_samples` into the past (integer tap).
///
/// `delay_samples` is clamped to `[0, capacity - 1]`.
#[must_use]
pub fn tap(&self, delay_samples: usize) -> f32 {
let d = delay_samples.min(self.buffer.len() - 1);
let idx = self.write_pos.wrapping_sub(d).wrapping_sub(1) & self.mask;
self.buffer[idx]
}
/// Read a sample at a fractional delay using linear interpolation.
///
/// `delay_samples` can be any positive value up to `capacity - 1`.
#[must_use]
pub fn tap_frac(&self, delay_samples: f32) -> f32 {
let capped = delay_samples.clamp(0.0, (self.buffer.len() - 1) as f32);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let d_int = capped as usize;
let d_frac = capped - d_int as f32;
let s0 = self.tap(d_int);
let s1 = self.tap(d_int + 1);
s0 + d_frac * (s1 - s0)
}
/// Clear the delay line (fill with zeros) and reset the write head.
pub fn clear(&mut self) {
for s in self.buffer.iter_mut() {
*s = 0.0;
}
self.write_pos = 0;
}
/// Fill the delay line with `value` (useful for initialising with DC).
pub fn fill(&mut self, value: f32) {
for s in self.buffer.iter_mut() {
*s = value;
}
}
/// Return a read-only view of the internal ring buffer.
#[must_use]
pub fn as_slice(&self) -> &[f32] {
&self.buffer
}
/// Convert a time in milliseconds to a fractional number of samples.
#[must_use]
pub fn ms_to_samples(&self, ms: f32) -> f32 {
ms * 0.001 * self.sample_rate
}
}
/// A stereo delay line (two independent channels sharing the same parameters).
pub struct StereoDelayLine {
/// Left channel delay.
pub left: DelayLine,
/// Right channel delay.
pub right: DelayLine,
}
impl StereoDelayLine {
/// Create a stereo delay line sized for at least `min_capacity` samples.
#[must_use]
pub fn new(min_capacity: usize) -> Self {
Self {
left: DelayLine::new(min_capacity),
right: DelayLine::new(min_capacity),
}
}
/// Push a stereo sample pair.
pub fn push(&mut self, left: f32, right: f32) {
self.left.push(left);
self.right.push(right);
}
/// Tap both channels at the same integer delay.
#[must_use]
pub fn tap(&self, delay_samples: usize) -> (f32, f32) {
(self.left.tap(delay_samples), self.right.tap(delay_samples))
}
/// Tap both channels at the same fractional delay.
#[must_use]
pub fn tap_frac(&self, delay_samples: f32) -> (f32, f32) {
(
self.left.tap_frac(delay_samples),
self.right.tap_frac(delay_samples),
)
}
/// Clear both channels.
pub fn clear(&mut self) {
self.left.clear();
self.right.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_delay_line_capacity_power_of_two() {
let dl = DelayLine::new(100);
assert!(dl.capacity().is_power_of_two());
assert!(dl.capacity() >= 100);
}
#[test]
fn test_delay_line_tap_zero_on_empty() {
let dl = DelayLine::new(64);
assert_eq!(dl.tap(0), 0.0);
assert_eq!(dl.tap(10), 0.0);
}
#[test]
fn test_delay_line_push_and_tap() {
let mut dl = DelayLine::new(64);
dl.push(0.5);
// Immediately after push, tap(0) retrieves the last written sample
assert!((dl.tap(0) - 0.5).abs() < 1e-6);
}
#[test]
fn test_delay_line_integer_tap_history() {
let mut dl = DelayLine::new(64);
// Push samples 0.0, 0.1, 0.2, …, 1.5
for i in 0..=15 {
dl.push(i as f32 * 0.1);
}
// The last pushed sample (0.15=1.5) is at tap(0)
let v0 = dl.tap(0);
assert!((v0 - 1.5).abs() < 1e-5);
// One sample before (0.14=1.4) is at tap(1)
let v1 = dl.tap(1);
assert!((v1 - 1.4).abs() < 1e-5);
}
#[test]
fn test_delay_line_fractional_tap_midpoint() {
let mut dl = DelayLine::new(64);
dl.push(0.0);
dl.push(1.0);
// Fractional tap 0.5 between sample 0 (0.0) and sample 1 (1.0)
// Since tap(0)=1.0 and tap(1)=0.0, tap_frac(0.5) should be ~0.5
let v = dl.tap_frac(0.5);
assert!((v - 0.5).abs() < 1e-5);
}
#[test]
fn test_delay_line_clear() {
let mut dl = DelayLine::new(64);
dl.push(0.9);
dl.clear();
assert_eq!(dl.tap(0), 0.0);
assert_eq!(dl.write_pos, 0);
}
#[test]
fn test_delay_line_fill() {
let mut dl = DelayLine::new(16);
dl.fill(0.5);
for &v in dl.as_slice() {
assert!((v - 0.5).abs() < 1e-6);
}
}
#[test]
fn test_delay_line_max_delay_ms() {
let dl = DelayLine::with_max_delay_ms(10.0, 48_000.0);
assert!(dl.max_delay_ms() >= 10.0);
}
#[test]
fn test_delay_line_ms_to_samples() {
let dl = DelayLine::with_max_delay_ms(100.0, 48_000.0);
let samples = dl.ms_to_samples(1.0);
assert!((samples - 48.0).abs() < 1e-3);
}
#[test]
fn test_delay_line_wrap_around() {
// Use a tiny buffer to force wrap-around quickly
let mut dl = DelayLine::new(4);
for i in 0..8 {
dl.push(i as f32);
}
// After 8 pushes into a buffer of 4, the last 4 samples are 4,5,6,7
assert!((dl.tap(0) - 7.0).abs() < 1e-6);
assert!((dl.tap(3) - 4.0).abs() < 1e-6);
}
#[test]
fn test_delay_line_tap_clamp() {
let mut dl = DelayLine::new(4);
dl.push(1.0);
// Asking for a delay beyond capacity should not panic
let _ = dl.tap(1000);
}
#[test]
fn test_stereo_delay_line_push_tap() {
let mut sdl = StereoDelayLine::new(64);
sdl.push(0.3, -0.3);
let (l, r) = sdl.tap(0);
assert!((l - 0.3).abs() < 1e-6);
assert!((r - (-0.3)).abs() < 1e-6);
}
#[test]
fn test_stereo_delay_line_clear() {
let mut sdl = StereoDelayLine::new(64);
sdl.push(0.8, 0.6);
sdl.clear();
let (l, r) = sdl.tap(0);
assert_eq!(l, 0.0);
assert_eq!(r, 0.0);
}
#[test]
fn test_stereo_delay_line_frac_tap() {
let mut sdl = StereoDelayLine::new(64);
sdl.push(0.0, 0.0);
sdl.push(1.0, -1.0);
let (l, r) = sdl.tap_frac(0.5);
assert!((l - 0.5).abs() < 1e-5);
assert!((r - (-0.5)).abs() < 1e-5);
}
#[test]
fn test_delay_line_capacity_minimum() {
// Even if min_capacity is 0 or 1, we get at least 2
let dl = DelayLine::new(0);
assert!(dl.capacity() >= 2);
}
}