Skip to main content

rill_core/buffer/
tape.rs

1use crate::buffer::Buffer;
2use crate::math::Transcendental;
3use std::cell::UnsafeCell;
4use std::rc::Rc;
5
6/// Heap-allocated ring buffer for tape delay — single-threaded.
7///
8/// Unlike [`DelayLine`](super::DelayLine), `TapeLoop` does NOT use const generics
9/// for its capacity — the buffer is allocated on the heap at runtime.
10/// This allows arbitrarily large delay lines (millions of samples) without
11/// stack overflow.
12///
13/// # Thread safety
14///
15/// `TapeLoop` is **not** thread-safe. It uses plain `T`, not `AtomicCell`,
16/// because it is only accessed from the single signal thread.
17///
18/// # Example
19///
20/// ```rust
21/// use rill_core::buffer::TapeLoop;
22///
23/// let mut tape = TapeLoop::<f32>::new(96000).unwrap();
24/// tape.write(0.5);
25/// let sample = tape.read(100);
26/// ```
27#[derive(Debug)]
28pub struct TapeLoop<T> {
29    buffer: Box<[T]>,
30    capacity: usize,
31    write_pos: usize,
32}
33
34impl<T: Transcendental> TapeLoop<T> {
35    /// Allocate a new tape loop with the given capacity (in samples).
36    pub fn new(capacity: usize) -> Option<Self> {
37        if capacity == 0 {
38            return None;
39        }
40        let mut vec = Vec::with_capacity(capacity);
41        for _ in 0..capacity {
42            vec.push(T::ZERO);
43        }
44        Some(Self {
45            buffer: vec.into_boxed_slice(),
46            capacity,
47            write_pos: 0,
48        })
49    }
50
51    /// Maximum capacity in samples.
52    pub fn capacity(&self) -> usize {
53        self.capacity
54    }
55    /// Current write cursor position.
56    pub fn write_pos(&self) -> usize {
57        self.write_pos
58    }
59
60    /// Write a single sample and advance the write cursor.
61    #[inline(always)]
62    pub fn write(&mut self, sample: T) {
63        self.buffer[self.write_pos] = sample;
64        self.write_pos = (self.write_pos + 1) % self.capacity;
65    }
66
67    /// Read a sample at `delay` samples behind the write position.
68    #[inline(always)]
69    pub fn read(&self, delay: usize) -> T {
70        let d = delay.min(self.capacity - 1);
71        let read_pos = if self.write_pos > d {
72            self.write_pos - 1 - d
73        } else {
74            self.capacity + self.write_pos - 1 - d
75        };
76        self.buffer[read_pos]
77    }
78
79    /// Read with linear interpolation between samples.
80    #[inline(always)]
81    pub fn read_interpolated(&self, delay: f64) -> T {
82        let d = delay as usize;
83        let frac = T::from_f64(delay.fract());
84        let s1 = self.read(d);
85        let s2 = self.read(d + 1);
86        s1 + (s2 - s1) * frac
87    }
88
89    /// Write a full block of samples.
90    #[inline(always)]
91    pub fn write_block(&mut self, block: &[T]) {
92        let len = block.len().min(self.capacity);
93        for (i, &b) in block.iter().enumerate().take(len) {
94            self.buffer[(self.write_pos + i) % self.capacity] = b;
95        }
96        self.write_pos = (self.write_pos + len) % self.capacity;
97    }
98
99    /// Read a full block starting at `delay` samples behind write position.
100    #[inline(always)]
101    pub fn read_block(&self, delay: usize, output: &mut [T]) {
102        let len = output.len().min(self.capacity);
103        let d = delay.min(self.capacity - 1);
104        for (i, out) in output.iter_mut().enumerate().take(len) {
105            *out = self.read(d + len - 1 - i);
106        }
107    }
108
109    /// Fill the entire buffer with a constant value.
110    pub fn fill(&mut self, value: T) {
111        for slot in self.buffer.iter_mut() {
112            *slot = value;
113        }
114    }
115
116    /// Reset write position and zero the buffer.
117    pub fn clear(&mut self) {
118        for slot in self.buffer.iter_mut() {
119            *slot = T::ZERO;
120        }
121        self.write_pos = 0;
122    }
123}
124
125// ── Buffer trait impl ──────────────────────────────────────────────
126
127impl<T: Transcendental> Buffer<T> for TapeLoop<T> {
128    fn capacity(&self) -> usize {
129        self.capacity
130    }
131
132    fn len(&self) -> usize {
133        self.capacity
134    }
135
136    fn as_slice(&self) -> &[T] {
137        &self.buffer
138    }
139
140    fn as_mut_slice(&mut self) -> &mut [T] {
141        &mut self.buffer
142    }
143
144    fn fill(&mut self, value: T) {
145        for slot in self.buffer.iter_mut() {
146            *slot = value;
147        }
148    }
149
150    fn copy_from(&mut self, src: &[T]) {
151        let len = src.len().min(self.capacity);
152        self.buffer[..len].copy_from_slice(&src[..len]);
153    }
154
155    fn clear(&mut self) {
156        for slot in self.buffer.iter_mut() {
157            *slot = T::ZERO;
158        }
159        self.write_pos = 0;
160    }
161}
162
163// ── Capability-split shared handles ────────────────────────────────
164//
165// A `TapeLoop` is private shared state between one write head and any number
166// of read heads. It is orthogonal to signal propagation, so it is shared
167// directly between the participating nodes rather than routed through the
168// graph. Ownership is reference-counted; the two capabilities encode the
169// domain invariant "exactly one writer, many readers" in the type system.
170
171/// Shared, single-threaded cell holding a [`TapeLoop`].
172struct TapeCell<T> {
173    inner: Rc<UnsafeCell<TapeLoop<T>>>,
174}
175
176impl<T> Clone for TapeCell<T> {
177    fn clone(&self) -> Self {
178        Self {
179            inner: Rc::clone(&self.inner),
180        }
181    }
182}
183
184impl<T> TapeCell<T> {
185    fn new(tape: TapeLoop<T>) -> Self {
186        Self {
187            inner: Rc::new(UnsafeCell::new(tape)),
188        }
189    }
190}
191
192/// Unique write capability over a shared [`TapeLoop`].
193///
194/// Not `Clone` — at most one `TapeWriter` exists per tape (enforced by the
195/// resource registry). Held by the single write head.
196pub struct TapeWriter<T> {
197    cell: TapeCell<T>,
198}
199
200/// Shared read capability over a [`TapeLoop`]. `Clone` — one per read tap.
201///
202/// Exposes only read operations; readers cannot mutate the tape, so the
203/// single-writer invariant is checked at compile time.
204pub struct TapeReader<T> {
205    cell: TapeCell<T>,
206}
207
208impl<T> Clone for TapeReader<T> {
209    fn clone(&self) -> Self {
210        Self {
211            cell: self.cell.clone(),
212        }
213    }
214}
215
216/// Wrap a [`TapeLoop`] into a writer + reader capability pair sharing one cell.
217///
218/// The returned `TapeWriter` is unique; clone the `TapeReader` for additional
219/// read taps. The tape stays alive while any handle exists.
220pub fn tape_handles<T>(tape: TapeLoop<T>) -> (TapeWriter<T>, TapeReader<T>) {
221    let cell = TapeCell::new(tape);
222    (TapeWriter { cell: cell.clone() }, TapeReader { cell })
223}
224
225impl<T: Transcendental> TapeWriter<T> {
226    /// SAFETY: the graph is single-threaded; at most one `TapeWriter` exists
227    /// per cell (the registry hands the writer out once) and the writer is
228    /// never active at the same instant as any reader — nodes are processed
229    /// sequentially in topological order.
230    #[allow(unsafe_code)]
231    #[inline(always)]
232    fn tape_mut(&mut self) -> &mut TapeLoop<T> {
233        unsafe { &mut *self.cell.inner.get() }
234    }
235
236    /// Write a single sample and advance the write cursor.
237    #[inline(always)]
238    pub fn write(&mut self, sample: T) {
239        self.tape_mut().write(sample);
240    }
241
242    /// Write a full block of samples.
243    #[inline(always)]
244    pub fn write_block(&mut self, block: &[T]) {
245        self.tape_mut().write_block(block);
246    }
247
248    /// Fill the entire buffer with a constant value.
249    pub fn fill(&mut self, value: T) {
250        self.tape_mut().fill(value);
251    }
252
253    /// Reset write position and zero the buffer.
254    pub fn clear(&mut self) {
255        self.tape_mut().clear();
256    }
257
258    /// Maximum capacity in samples.
259    pub fn capacity(&self) -> usize {
260        // SAFETY: see `tape_mut`; a shared read for a scalar field is sound.
261        #[allow(unsafe_code)]
262        unsafe {
263            (*self.cell.inner.get()).capacity()
264        }
265    }
266}
267
268impl<T: Transcendental> TapeReader<T> {
269    /// SAFETY: the graph is single-threaded; no `&mut` to the tape is live
270    /// while a reader is active — nodes are processed sequentially in
271    /// topological order and readers hold shared refs only.
272    #[allow(unsafe_code)]
273    #[inline(always)]
274    fn tape(&self) -> &TapeLoop<T> {
275        unsafe { &*self.cell.inner.get() }
276    }
277
278    /// Read a sample at `delay` samples behind the write position.
279    #[inline(always)]
280    pub fn read(&self, delay: usize) -> T {
281        self.tape().read(delay)
282    }
283
284    /// Read with linear interpolation between samples.
285    #[inline(always)]
286    pub fn read_interpolated(&self, delay: f64) -> T {
287        self.tape().read_interpolated(delay)
288    }
289
290    /// Read a full block starting at `delay` samples behind write position.
291    #[inline(always)]
292    pub fn read_block(&self, delay: usize, output: &mut [T]) {
293        self.tape().read_block(delay, output);
294    }
295
296    /// Maximum capacity in samples.
297    pub fn capacity(&self) -> usize {
298        self.tape().capacity()
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305
306    #[test]
307    fn test_tape_basic_write_read() {
308        let mut tape = TapeLoop::<f32>::new(1024).unwrap();
309        tape.write(1.0);
310        tape.write(2.0);
311        tape.write(3.0);
312        assert_eq!(tape.read(0), 3.0);
313        assert_eq!(tape.read(1), 2.0);
314        assert_eq!(tape.read(2), 1.0);
315    }
316
317    #[test]
318    fn test_tape_wraparound() {
319        let mut tape = TapeLoop::<f32>::new(4).unwrap();
320        for i in 0..10 {
321            tape.write(i as f32);
322        }
323        assert_eq!(tape.read(0), 9.0);
324        assert_eq!(tape.read(1), 8.0);
325        assert_eq!(tape.read(2), 7.0);
326        assert_eq!(tape.read(3), 6.0);
327    }
328
329    #[test]
330    fn test_tape_block_ops() {
331        let mut tape = TapeLoop::<f32>::new(64).unwrap();
332        let block = [1.0f32; 64];
333        tape.write_block(&block);
334        let mut out = [0.0f32; 64];
335        tape.read_block(63, &mut out);
336        assert_eq!(out[0], 1.0);
337    }
338
339    #[test]
340    fn test_tape_large_capacity() {
341        let tape = TapeLoop::<f32>::new(1_000_000).unwrap();
342        assert_eq!(tape.capacity(), 1_000_000);
343    }
344
345    #[test]
346    fn test_tape_zero_capacity() {
347        assert!(TapeLoop::<f32>::new(0).is_none());
348    }
349
350    #[test]
351    fn test_read_interpolated() {
352        let mut tape = TapeLoop::<f32>::new(1024).unwrap();
353        tape.write(0.0);
354        tape.write(1.0);
355        let v = tape.read_interpolated(0.5);
356        assert!((v - 0.5).abs() < 0.01);
357    }
358}