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}