Skip to main content

tono_core/runtime/
ring.rs

1//! The wait-free SPSC transport: a lock-free sample ring joining a control
2//! thread ([`Pump`]) to the audio callback ([`Renderer`]).
3
4use std::sync::Arc;
5use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
6
7use super::engine::Engine;
8use super::source::AudioSource;
9
10/// slot is an `AtomicU32` (the sample's bits), so it is entirely safe — no
11/// `unsafe`, no locks. The [`Controller`] is the sole producer, the [`Renderer`]
12/// the sole consumer.
13pub(super) struct SampleRing {
14    buf: Vec<AtomicU32>,
15    /// Next slot the consumer reads.
16    head: AtomicUsize,
17    /// Next slot the producer writes.
18    tail: AtomicUsize,
19}
20
21impl SampleRing {
22    pub(super) fn new(capacity: usize) -> Self {
23        // One slot stays empty so full and empty are distinguishable.
24        let n = (capacity + 1).max(2);
25        SampleRing {
26            buf: (0..n).map(|_| AtomicU32::new(0)).collect(),
27            head: AtomicUsize::new(0),
28            tail: AtomicUsize::new(0),
29        }
30    }
31
32    fn cap(&self) -> usize {
33        self.buf.len()
34    }
35
36    pub(super) fn len(&self) -> usize {
37        let t = self.tail.load(Ordering::Acquire);
38        let h = self.head.load(Ordering::Acquire);
39        (t + self.cap() - h) % self.cap()
40    }
41
42    fn free(&self) -> usize {
43        self.cap() - 1 - self.len()
44    }
45
46    /// Push one sample (producer). Returns false if full.
47    pub(super) fn push(&self, sample: f32) -> bool {
48        let tail = self.tail.load(Ordering::Relaxed);
49        let next = (tail + 1) % self.cap();
50        if next == self.head.load(Ordering::Acquire) {
51            return false;
52        }
53        self.buf[tail].store(sample.to_bits(), Ordering::Relaxed);
54        self.tail.store(next, Ordering::Release);
55        true
56    }
57
58    /// Pop one sample (consumer), or `None` if empty.
59    pub(super) fn pop(&self) -> Option<f32> {
60        let head = self.head.load(Ordering::Relaxed);
61        if head == self.tail.load(Ordering::Acquire) {
62            return None;
63        }
64        let bits = self.buf[head].load(Ordering::Relaxed);
65        self.head.store((head + 1) % self.cap(), Ordering::Release);
66        Some(f32::from_bits(bits))
67    }
68}
69
70/// The control side of a split source: owns any [`AudioSource`] and produces
71/// audio into the shared ring. Lives on any non-audio thread — deref to call
72/// every method of the wrapped source (for an [`Engine`]: `play`, `set_gain`,
73/// `set_param`, …), then [`pump`](Self::pump) to keep the audio thread fed.
74///
75/// Generic over the source so the same seam drives a bare [`Engine`](super::Engine), a
76/// [`Mixer`](super::Mixer) of instruments + SFX, or any other [`AudioSource`]. [`Controller`]
77/// is the `Engine` specialization returned by [`Engine::split`].
78pub struct Pump<S: AudioSource> {
79    source: S,
80    ring: Arc<SampleRing>,
81    pump_buf: Vec<f32>,
82}
83
84impl<S: AudioSource> Pump<S> {
85    /// Mix up to `frames` frames and hand them to the audio thread. Returns the
86    /// number of frames actually pushed — fewer when the ring is full, which is
87    /// the backpressure signal to stop pumping until the next tick.
88    pub fn pump(&mut self, frames: usize) -> usize {
89        // Render only what the ring can take. `fill` advances every play head,
90        // so a frame rendered but not pushed would be audio lost forever — not
91        // deferred. As the sole producer, the space observed here can only grow
92        // before the pushes below.
93        let frames = frames.min(self.ring.free() / 2);
94        if frames == 0 {
95            return 0;
96        }
97        if self.pump_buf.len() < frames * 2 {
98            self.pump_buf.resize(frames * 2, 0.0);
99        }
100        let block = &mut self.pump_buf[..frames * 2];
101        self.source.fill(block);
102        for s in block.iter() {
103            self.ring.push(*s);
104        }
105        frames
106    }
107}
108
109impl<S: AudioSource> std::ops::Deref for Pump<S> {
110    type Target = S;
111    fn deref(&self) -> &S {
112        &self.source
113    }
114}
115
116impl<S: AudioSource> std::ops::DerefMut for Pump<S> {
117    fn deref_mut(&mut self) -> &mut S {
118        &mut self.source
119    }
120}
121
122/// The control side of a split [`Engine`] — see [`Pump`] and [`Engine::split`].
123pub type Controller = Pump<Engine>;
124
125/// Split any [`AudioSource`] into a [`Pump`] (control thread) and a [`Renderer`]
126/// (audio thread) joined by a wait-free ring `ring_frames` deep (floored at 1:
127/// a 0-deep ring can never hold a whole frame, so `pump` would push nothing,
128/// silently, forever). Pump the controller off the audio thread; the renderer
129/// drains it in the callback.
130pub fn spsc<S: AudioSource>(source: S, ring_frames: usize) -> (Pump<S>, Renderer) {
131    let ring = Arc::new(SampleRing::new(ring_frames.max(1) * 2));
132    (
133        Pump {
134            source,
135            ring: ring.clone(),
136            pump_buf: Vec::new(),
137        },
138        Renderer { ring },
139    )
140}
141
142/// The audio side of a split engine: drains the ring in the output callback.
143/// `Send`, alloc-free, lock-free — safe to hand to a cpal / AudioWorklet thread.
144/// On underrun it writes silence (the ring depth chosen at [`Engine::split`]
145/// trades latency against underrun safety).
146pub struct Renderer {
147    pub(super) ring: Arc<SampleRing>,
148}
149
150impl AudioSource for Renderer {
151    fn fill(&mut self, out: &mut [f32]) -> usize {
152        // Drain whole frames only: taking half a frame across an underrun
153        // would land every later left sample on a right slot — a permanent
154        // channel swap. As the sole consumer, an observed pair stays there.
155        for frame in out.chunks_mut(2) {
156            if frame.len() == 2 && self.ring.len() >= 2 {
157                frame[0] = self.ring.pop().unwrap_or(0.0);
158                frame[1] = self.ring.pop().unwrap_or(0.0);
159            } else {
160                frame.fill(0.0);
161            }
162        }
163        out.len() / 2
164    }
165}