1#![deny(
21 clippy::unwrap_used,
22 clippy::expect_used,
23 clippy::indexing_slicing,
24 clippy::panic,
25 clippy::unreachable
26)]
27
28use std::cell::UnsafeCell;
29use std::sync::Arc;
30use std::sync::atomic::{AtomicUsize, Ordering};
31
32pub fn spsc(capacity_samples: usize, channels: u16) -> (SampleProducer, SampleConsumer) {
38 let capacity = capacity_samples.max(1).next_power_of_two();
39 let shared = Arc::new(RingShared {
40 buf: (0..capacity).map(|_| UnsafeCell::new(0.0)).collect(),
41 mask: capacity - 1,
42 head: PaddedAtomicUsize::new(0),
43 tail: PaddedAtomicUsize::new(0),
44 });
45 (
46 SampleProducer {
47 shared: Arc::clone(&shared),
48 channels,
49 },
50 SampleConsumer { shared },
51 )
52}
53
54#[repr(align(64))]
57struct PaddedAtomicUsize(AtomicUsize);
58
59impl PaddedAtomicUsize {
60 fn new(v: usize) -> Self {
61 Self(AtomicUsize::new(v))
62 }
63}
64
65struct RingShared {
66 buf: Box<[UnsafeCell<f32>]>,
67 mask: usize,
68 head: PaddedAtomicUsize,
70 tail: PaddedAtomicUsize,
72}
73
74unsafe impl Send for RingShared {}
78unsafe impl Sync for RingShared {}
79
80pub struct SampleProducer {
83 shared: Arc<RingShared>,
84 channels: u16,
85}
86
87impl SampleProducer {
88 #[allow(
94 clippy::indexing_slicing,
95 reason = "ring indices are masked (& mask) and `samples[..n]` is bounded by n = min(len, free); see the Safety notes"
96 )]
97 pub fn push_samples(&mut self, samples: &[f32]) -> usize {
98 debug_assert_eq!(samples.len() % self.channels as usize, 0);
99 let head = self.shared.head.0.load(Ordering::Relaxed);
100 let tail = self.shared.tail.0.load(Ordering::Acquire);
101 let free = self.shared.buf.len() - (head - tail);
102 let n = samples.len().min(free) / self.channels as usize * self.channels as usize;
105 for (i, &s) in samples[..n].iter().enumerate() {
106 let idx = (head + i) & self.shared.mask;
107 unsafe { *self.shared.buf[idx].get() = s };
110 }
111 self.shared.head.0.store(head + n, Ordering::Release);
112 n
113 }
114}
115
116pub struct SampleConsumer {
118 shared: Arc<RingShared>,
119}
120
121impl SampleConsumer {
122 pub fn available(&self) -> usize {
124 let head = self.shared.head.0.load(Ordering::Acquire);
125 let tail = self.shared.tail.0.load(Ordering::Relaxed);
126 head - tail
127 }
128
129 #[allow(
131 clippy::indexing_slicing,
132 reason = "ring indices are masked (& mask) and `out[..n]` is bounded by n = min(len, available); see the Safety notes"
133 )]
134 pub fn pop_samples(&mut self, out: &mut [f32]) -> usize {
135 let head = self.shared.head.0.load(Ordering::Acquire);
136 let tail = self.shared.tail.0.load(Ordering::Relaxed);
137 let n = out.len().min(head - tail);
138 for (i, slot) in out[..n].iter_mut().enumerate() {
139 let idx = (tail + i) & self.shared.mask;
140 *slot = unsafe { *self.shared.buf[idx].get() };
143 }
144 self.shared.tail.0.store(tail + n, Ordering::Release);
145 n
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 #![allow(clippy::indexing_slicing)]
154
155 use super::*;
156
157 #[test]
158 fn roundtrip_preserves_order() {
159 let (mut tx, mut rx) = spsc(8, 1);
160 assert_eq!(tx.push_samples(&[1.0, 2.0, 3.0]), 3);
161 let mut out = [0.0; 3];
162 assert_eq!(rx.pop_samples(&mut out), 3);
163 assert_eq!(out, [1.0, 2.0, 3.0]);
164 }
165
166 #[test]
167 fn wraparound_keeps_data_intact() {
168 let (mut tx, mut rx) = spsc(4, 1);
169 let mut out = [0.0; 4];
170 for round in 0..10 {
171 let base = round as f32 * 4.0;
172 assert_eq!(
173 tx.push_samples(&[base, base + 1.0, base + 2.0, base + 3.0]),
174 4
175 );
176 assert_eq!(rx.pop_samples(&mut out), 4);
177 assert_eq!(out, [base, base + 1.0, base + 2.0, base + 3.0]);
178 }
179 }
180
181 #[test]
182 fn full_ring_drops_excess_instead_of_blocking() {
183 let (mut tx, mut rx) = spsc(4, 1);
184 assert_eq!(tx.push_samples(&[1.0, 2.0, 3.0, 4.0]), 4);
185 assert_eq!(tx.push_samples(&[5.0]), 0);
186 let mut out = [0.0; 4];
187 assert_eq!(rx.pop_samples(&mut out), 4);
188 assert_eq!(out, [1.0, 2.0, 3.0, 4.0]);
189 assert_eq!(tx.push_samples(&[5.0]), 1);
190 assert_eq!(rx.pop_samples(&mut out[..1]), 1);
191 assert_eq!(out[0], 5.0);
192 }
193
194 #[test]
195 fn cross_thread_stream_arrives_in_order() {
196 let (mut tx, mut rx) = spsc(2048, 2);
197 let total: usize = 100_000;
198 let writer = std::thread::spawn(move || {
199 let mut sent = 0usize;
200 let mut chunk = [0.0f32; 64];
201 while sent < total {
202 let n = chunk.len().min(total - sent);
203 for (i, s) in chunk[..n].iter_mut().enumerate() {
204 *s = (sent + i) as f32;
205 }
206 sent += tx.push_samples(&chunk[..n]);
207 }
208 });
209 let mut expected = 0usize;
210 let mut buf = [0.0f32; 256];
211 while expected < total {
212 let n = rx.pop_samples(&mut buf);
213 for &s in &buf[..n] {
214 assert_eq!(s, expected as f32);
215 expected += 1;
216 }
217 }
218 writer.join().unwrap();
219 }
220}