Skip to main content

rill_core_dsp/generators/
reader.rs

1//! Fractional-position buffer reader with interpolation.
2//!
3//! Provides [`InterpolatedReader`] for reading from heap-allocated sample
4//! buffers with linear or cubic interpolation, wrap/clamp boundary modes,
5//! and variable playback rate.
6
7use rill_core::interpolate::Interpolate;
8use rill_core::math::vector::scalar::ScalarVector4;
9use rill_core::math::vector::traits::Vector as VecTrait;
10use rill_core::Transcendental;
11
12fn len_remainder(pos: f64, len: f64) -> f64 {
13    let r = pos % len;
14    if r < 0.0 {
15        r + len
16    } else {
17        r
18    }
19}
20
21/// Heap-allocated buffer reader with fractional-position interpolation.
22///
23/// When `wrap` is true the position wraps modulo `len` (periodic / wavetable mode).
24/// When `wrap` is false the position clamps at buffer boundaries (sample mode).
25pub struct InterpolatedReader<T> {
26    buffer: Box<[T]>,
27    position: f64,
28    rate: f64,
29    cubic: bool,
30    wrap: bool,
31}
32
33impl<T: Transcendental + Copy> InterpolatedReader<T> {
34    /// Create a new reader from a `Vec<T>`.
35    ///
36    /// The reader starts at position 0 with unit rate and linear interpolation.
37    pub fn new(buffer: Vec<T>) -> Self {
38        Self {
39            buffer: buffer.into_boxed_slice(),
40            position: 0.0,
41            rate: 1.0,
42            cubic: false,
43            wrap: false,
44        }
45    }
46
47    /// Create a new reader from a pre-allocated `Box<[T]>`.
48    pub fn from_boxed(buffer: Box<[T]>) -> Self {
49        Self {
50            buffer,
51            position: 0.0,
52            rate: 1.0,
53            cubic: false,
54            wrap: false,
55        }
56    }
57
58    /// Return the number of samples in the buffer.
59    #[inline(always)]
60    pub fn len(&self) -> usize {
61        self.buffer.len()
62    }
63
64    /// Returns `true` if the buffer is empty.
65    #[inline(always)]
66    pub fn is_empty(&self) -> bool {
67        self.buffer.is_empty()
68    }
69
70    /// Current read position in samples (fractional).
71    #[inline(always)]
72    pub fn position(&self) -> f64 {
73        self.position
74    }
75
76    /// Set the read position (in samples).
77    #[inline(always)]
78    pub fn set_position(&mut self, pos: f64) {
79        self.position = pos;
80    }
81
82    /// Playback rate in samples per output sample (1.0 = normal speed).
83    #[inline(always)]
84    pub fn rate(&self) -> f64 {
85        self.rate
86    }
87
88    /// Set the playback rate.
89    #[inline(always)]
90    pub fn set_rate(&mut self, rate: f64) {
91        self.rate = rate;
92    }
93
94    /// Returns `true` if cubic Hermite interpolation is enabled.
95    #[inline(always)]
96    pub fn is_cubic(&self) -> bool {
97        self.cubic
98    }
99
100    /// Enable (`true`) or disable (`false`) cubic Hermite interpolation.
101    #[inline(always)]
102    pub fn set_cubic(&mut self, cubic: bool) {
103        self.cubic = cubic;
104    }
105
106    /// Returns `true` when the read position wraps at buffer boundaries.
107    #[inline(always)]
108    pub fn is_wrap(&self) -> bool {
109        self.wrap
110    }
111
112    /// Enable (`true`) or disable (`false`) wrap mode (periodic / wavetable).
113    #[inline(always)]
114    pub fn set_wrap(&mut self, wrap: bool) {
115        self.wrap = wrap;
116    }
117
118    /// Replace the internal buffer and reset the position to 0.
119    pub fn set_buffer(&mut self, buffer: Vec<T>) {
120        self.buffer = buffer.into_boxed_slice();
121        self.position = 0.0;
122    }
123
124    /// Return the internal buffer as an immutable slice.
125    #[inline(always)]
126    pub fn as_slice(&self) -> &[T] {
127        &self.buffer
128    }
129
130    /// Wrap-aware linear interpolation.
131    fn read_wrap_linear(&self, pos: f64) -> T {
132        let len = self.len();
133        let i0 = pos.floor() as usize % len;
134        let i1 = (i0 + 1) % len;
135        let frac = T::from_f64(pos.fract());
136        let a = self.buffer[i0];
137        let b = self.buffer[i1];
138        a + (b - a) * frac
139    }
140
141    /// Wrap-aware cubic Hermite interpolation (periodic boundary).
142    fn read_wrap_cubic(&self, pos: f64) -> T {
143        let len = self.len();
144        let i = pos.floor() as usize;
145        let i0 = (i + len - 1) % len;
146        let i1 = i % len;
147        let i2 = (i + 1) % len;
148        let i3 = (i + 2) % len;
149        let frac = T::from_f64(pos.fract());
150
151        let c0 = self.buffer[i1];
152        let c1 = (self.buffer[i2] - self.buffer[i0]) * T::from_f32(0.5);
153        let c2 = self.buffer[i0] * T::from_f32(-1.5)
154            + self.buffer[i1] * T::from_f32(2.0)
155            + self.buffer[i2] * T::from_f32(-0.5);
156        let c3 = self.buffer[i0] * T::from_f32(-0.5)
157            + self.buffer[i1] * T::from_f32(1.5)
158            + self.buffer[i2] * T::from_f32(-1.5)
159            + self.buffer[i3] * T::from_f32(0.5);
160
161        let f2 = frac * frac;
162        let f3 = f2 * frac;
163        c0 + c1 * frac + c2 * f2 + c3 * f3
164    }
165
166    /// Read a single sample at the current position without advancing.
167    #[inline(always)]
168    pub fn read_one(&self) -> T {
169        if self.is_empty() {
170            return T::ZERO;
171        }
172        let pos = if self.wrap {
173            len_remainder(self.position, self.len() as f64)
174        } else {
175            self.position
176        };
177        if self.cubic && self.len() >= 4 {
178            if self.wrap {
179                self.read_wrap_cubic(pos)
180            } else {
181                self.buffer.interpolate_cubic(pos)
182            }
183        } else if self.wrap {
184            self.read_wrap_linear(pos)
185        } else {
186            self.buffer.interpolate_linear(pos)
187        }
188    }
189
190    /// Advance position by `self.rate` (one sample).
191    #[inline(always)]
192    pub fn advance(&mut self) {
193        self.position += self.rate;
194    }
195
196    /// Read the next block of samples, advancing `self.position`.
197    pub fn render_block(&mut self, output: &mut [T]) {
198        if self.is_empty() {
199            for s in output.iter_mut() {
200                *s = T::ZERO;
201            }
202            return;
203        }
204        self.render_block_simd(output);
205    }
206
207    /// SIMD block render — processes in chunks of 4 with batched lerp math.
208    fn render_block_simd(&mut self, output: &mut [T]) {
209        let chunks = output.len() / 4;
210        let mut pos = self.position;
211        let rate = self.rate;
212
213        if self.wrap {
214            let len_f = self.len() as f64;
215            if self.cubic && self.len() >= 4 {
216                for chunk in 0..chunks {
217                    let offset = chunk * 4;
218                    let _p0 = pos;
219                    let _p1 = pos + rate;
220                    let _p2 = pos + rate * 2.0;
221                    let _p3 = pos + rate * 3.0;
222                    pos += rate * 4.0;
223
224                    // Cubic interpolation: 4 reads per sample (16 total per block)
225                    // Do per-sample for cubic to keep code manageable
226                    self.render_scalar_range(output, offset, 4, self.wrap, self.cubic);
227                    // Advance position back (render_scalar_range advances internally)
228                    // Actually, just use scalar fallback for cubic
229                }
230            } else {
231                for chunk in 0..chunks {
232                    let offset = chunk * 4;
233                    // Compute fractional positions (normalized to [0, len))
234                    let p0 = len_remainder(pos, len_f);
235                    let p1 = len_remainder(pos + rate, len_f);
236                    let p2 = len_remainder(pos + rate * 2.0, len_f);
237                    let p3 = len_remainder(pos + rate * 3.0, len_f);
238                    pos += rate * 4.0;
239
240                    let i0 = p0 as usize;
241                    let i1 = p1 as usize;
242                    let i2 = p2 as usize;
243                    let i3 = p3 as usize;
244
245                    let j0 = (i0 + 1) % self.len();
246                    let j1 = (i1 + 1) % self.len();
247                    let j2 = (i2 + 1) % self.len();
248                    let j3 = (i3 + 1) % self.len();
249
250                    let f0 = T::from_f64(p0.fract());
251                    let f1 = T::from_f64(p1.fract());
252                    let f2 = T::from_f64(p2.fract());
253                    let f3 = T::from_f64(p3.fract());
254
255                    let a0 = self.buffer[i0];
256                    let a1 = self.buffer[i1];
257                    let a2 = self.buffer[i2];
258                    let a3 = self.buffer[i3];
259                    let b0 = self.buffer[j0];
260                    let b1 = self.buffer[j1];
261                    let b2 = self.buffer[j2];
262                    let b3 = self.buffer[j3];
263
264                    let a_v = ScalarVector4::load(&[a0, a1, a2, a3]);
265                    let b_v = ScalarVector4::load(&[b0, b1, b2, b3]);
266                    let f_v = ScalarVector4::load(&[f0, f1, f2, f3]);
267
268                    // lerp: a + (b - a) * f
269                    let result = a_v.add(&b_v.sub(&a_v).mul(&f_v));
270                    result.store(&mut output[offset..offset + 4]);
271                }
272            }
273        } else if self.cubic {
274            // Cubic non-wrap: use scalar path (complex 4-sample gather not worth SIMD)
275            self.render_scalar_block(output);
276            return;
277        } else {
278            for chunk in 0..chunks {
279                let offset = chunk * 4;
280                let p0 = pos;
281                let p1 = pos + rate;
282                let p2 = pos + rate * 2.0;
283                let p3 = pos + rate * 3.0;
284                pos += rate * 4.0;
285
286                let last_idx = self.len() - 1;
287                let i0 = (p0 as usize).min(last_idx);
288                let i1 = (p1 as usize).min(last_idx);
289                let i2 = (p2 as usize).min(last_idx);
290                let i3 = (p3 as usize).min(last_idx);
291
292                let j0 = (i0 + 1).min(last_idx);
293                let j1 = (i1 + 1).min(last_idx);
294                let j2 = (i2 + 1).min(last_idx);
295                let j3 = (i3 + 1).min(last_idx);
296
297                let f0 = T::from_f64(p0.fract());
298                let f1 = T::from_f64(p1.fract());
299                let f2 = T::from_f64(p2.fract());
300                let f3 = T::from_f64(p3.fract());
301
302                let a_v = ScalarVector4::load(&[
303                    self.buffer[i0],
304                    self.buffer[i1],
305                    self.buffer[i2],
306                    self.buffer[i3],
307                ]);
308                let b_v = ScalarVector4::load(&[
309                    self.buffer[j0],
310                    self.buffer[j1],
311                    self.buffer[j2],
312                    self.buffer[j3],
313                ]);
314                let f_v = ScalarVector4::load(&[f0, f1, f2, f3]);
315
316                let result = a_v.add(&b_v.sub(&a_v).mul(&f_v));
317                result.store(&mut output[offset..offset + 4]);
318            }
319        }
320
321        self.position = pos;
322
323        // Scalar remainder
324        for out in output[chunks * 4..].iter_mut() {
325            *out = self.read_one();
326            self.advance();
327        }
328    }
329
330    /// Fallback scalar block render for complex cases (cubic non-wrap, etc.)
331    fn render_scalar_block(&mut self, output: &mut [T]) {
332        if self.is_empty() {
333            for s in output.iter_mut() {
334                *s = T::ZERO;
335            }
336            return;
337        }
338        if self.wrap {
339            let len_f = self.len() as f64;
340            for s in output.iter_mut() {
341                *s = self.read_wrap_linear(len_remainder(self.position, len_f));
342                self.position += self.rate;
343            }
344        } else {
345            for s in output.iter_mut() {
346                *s = self.buffer.interpolate_linear(self.position);
347                self.position += self.rate;
348            }
349        }
350    }
351
352    /// Render a scalar range (helper for cubic SIMD to handle 4-sample chunks).
353    #[allow(dead_code)]
354    fn render_scalar_range(
355        &mut self,
356        output: &mut [T],
357        start: usize,
358        count: usize,
359        wrap: bool,
360        _cubic: bool,
361    ) {
362        for out in output[start..start + count].iter_mut() {
363            *out = if wrap {
364                let len = self.len() as f64;
365                self.read_wrap_linear(len_remainder(self.position, len))
366            } else {
367                self.buffer.interpolate_linear(self.position)
368            };
369            self.position += self.rate;
370        }
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377
378    #[test]
379    fn test_basic_read() {
380        let buf = vec![0.0, 1.0, 2.0, 3.0, 4.0];
381        let mut reader = InterpolatedReader::new(buf);
382        let mut out = [0.0f64; 4];
383        reader.render_block(&mut out);
384        assert_eq!(out, [0.0, 1.0, 2.0, 3.0]);
385        assert!((reader.position() - 4.0).abs() < 1e-10);
386    }
387
388    #[test]
389    fn test_rate_half() {
390        let buf = vec![0.0, 2.0, 4.0, 6.0, 8.0];
391        let mut reader = InterpolatedReader::new(buf);
392        reader.set_rate(0.5);
393        let mut out = [0.0f64; 4];
394        reader.render_block(&mut out);
395        assert!((out[0] - 0.0).abs() < 1e-10);
396        assert!((out[1] - 1.0).abs() < 1e-10);
397        assert!((out[2] - 2.0).abs() < 1e-10);
398        assert!((out[3] - 3.0).abs() < 1e-10);
399        assert!((reader.position() - 2.0).abs() < 1e-10);
400    }
401
402    #[test]
403    fn test_empty_buffer() {
404        let buf: Vec<f64> = vec![];
405        let mut reader = InterpolatedReader::new(buf);
406        let mut out = [1.0f64; 4];
407        reader.render_block(&mut out);
408        assert_eq!(out, [0.0; 4]);
409    }
410
411    #[test]
412    fn test_set_buffer() {
413        let buf = vec![0.0, 0.0];
414        let mut reader = InterpolatedReader::new(buf);
415        reader.set_position(10.0);
416        reader.set_buffer(vec![10.0, 20.0, 30.0]);
417        assert_eq!(reader.position(), 0.0);
418        assert_eq!(reader.len(), 3);
419    }
420
421    #[test]
422    fn test_wrap_linear() {
423        let buf = vec![0.0, 1.0, 2.0, 3.0];
424        let mut reader = InterpolatedReader::new(buf);
425        reader.set_wrap(true);
426        reader.set_position(3.5);
427        let mut out = [0.0f64; 2];
428        reader.render_block(&mut out);
429        assert!(
430            (out[0] - 1.5).abs() < 1e-10,
431            "wrap 3.5 -> 1.5, got {}",
432            out[0]
433        );
434        assert!(
435            (out[1] - 0.5).abs() < 1e-10,
436            "wrap 4.5 -> 0.5, got {}",
437            out[1]
438        );
439    }
440
441    #[test]
442    fn test_wrap_cubic_at_boundary() {
443        let buf = vec![0.0, 1.0, 2.0, 3.0];
444        let mut reader = InterpolatedReader::new(buf);
445        reader.set_wrap(true);
446        reader.set_cubic(true);
447        reader.set_position(0.0);
448        let mut out = [0.0f64; 1];
449        reader.render_block(&mut out);
450        assert!(
451            (out[0] - 0.0).abs() < 1e-10,
452            "cubic wrap at 0 -> 0, got {}",
453            out[0]
454        );
455    }
456
457    #[test]
458    fn test_clamp_at_end() {
459        let buf = vec![10.0, 20.0];
460        let mut reader = InterpolatedReader::new(buf);
461        reader.set_position(5.0);
462        let mut out = [0.0f64; 3];
463        reader.render_block(&mut out);
464        assert_eq!(out, [20.0, 20.0, 20.0]);
465    }
466}