Skip to main content

flexaudio_core/
raw_ring.rs

1//! SPSC, real-time-safe ring for raw interleaved f32 device frames, backed by rtrb.
2//!
3//! The producer (real-time callback) pushes slices without blocking. When full, it increments
4//! the overflow counter ([`AtomicU64`]) and drops the excess, so the real-time thread never blocks.
5//! The consumer pops data on the capture thread.
6
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::Arc;
9
10use rtrb::{Consumer, Producer, RingBuffer};
11
12/// Create a raw-frame ring. `capacity_samples` is the capacity in f32 samples.
13///
14/// Pass the returned producer to the real-time callback thread and the consumer to the capture
15/// thread (SPSC). Both share the `overflow` counter, which tracks dropped samples.
16pub fn raw_ring(capacity_samples: usize) -> (RawProducer, RawConsumer) {
17    let cap = capacity_samples.max(1);
18    let (prod, cons) = RingBuffer::<f32>::new(cap);
19    let overflow = Arc::new(AtomicU64::new(0));
20    (
21        RawProducer {
22            inner: prod,
23            overflow: overflow.clone(),
24        },
25        RawConsumer {
26            inner: cons,
27            overflow,
28        },
29    )
30}
31
32/// Handle for the real-time callback side. Only performs nonblocking pushes.
33pub struct RawProducer {
34    inner: Producer<f32>,
35    overflow: Arc<AtomicU64>,
36}
37
38impl RawProducer {
39    /// Push an interleaved sample slice without blocking.
40    ///
41    /// Writes as many samples as fit, drops the rest, and adds them to the overflow counter.
42    /// Returns the number of samples written. Never blocks.
43    pub fn push_slice(&mut self, samples: &[f32]) -> usize {
44        if samples.is_empty() {
45            return 0;
46        }
47        let free = self.inner.slots();
48        let writable = free.min(samples.len());
49
50        if writable > 0 {
51            // Write the whole chunk without allocating, using write_chunk_uninit.
52            if let Ok(mut chunk) = self.inner.write_chunk_uninit(writable) {
53                let (a, b) = chunk.as_mut_slices();
54                let (head, tail) = samples.split_at(a.len().min(samples.len()));
55                for (dst, &src) in a.iter_mut().zip(head.iter()) {
56                    dst.write(src);
57                }
58                let tail = &tail[..b.len().min(tail.len())];
59                for (dst, &src) in b.iter_mut().zip(tail.iter()) {
60                    dst.write(src);
61                }
62                // SAFETY: initialized exactly `writable` MaybeUninit values.
63                unsafe { chunk.commit_all() };
64            }
65        }
66
67        let dropped = samples.len() - writable;
68        if dropped > 0 {
69            self.overflow.fetch_add(dropped as u64, Ordering::Relaxed);
70        }
71        writable
72    }
73
74    /// Total number of samples dropped so far.
75    pub fn overflow_count(&self) -> u64 {
76        self.overflow.load(Ordering::Relaxed)
77    }
78}
79
80/// Handle for the capture thread. Pops samples.
81pub struct RawConsumer {
82    inner: Consumer<f32>,
83    overflow: Arc<AtomicU64>,
84}
85
86impl RawConsumer {
87    /// Pop up to `dst.len()` available samples into `dst`. Returns the number popped.
88    pub fn pop_slice(&mut self, dst: &mut [f32]) -> usize {
89        let avail = self.inner.slots();
90        let n = avail.min(dst.len());
91        if n == 0 {
92            return 0;
93        }
94        if let Ok(chunk) = self.inner.read_chunk(n) {
95            let (a, b) = chunk.as_slices();
96            let (alen, blen) = (a.len(), b.len());
97            dst[..alen].copy_from_slice(a);
98            dst[alen..alen + blen].copy_from_slice(b);
99            chunk.commit_all();
100            alen + blen
101        } else {
102            0
103        }
104    }
105
106    /// Pop one sample, or return `None` if empty.
107    pub fn pop(&mut self) -> Option<f32> {
108        self.inner.pop().ok()
109    }
110
111    /// Number of samples available to pop.
112    pub fn available(&self) -> usize {
113        self.inner.slots()
114    }
115
116    /// Total number of samples dropped by the producer so far.
117    pub fn overflow_count(&self) -> u64 {
118        self.overflow.load(Ordering::Relaxed)
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn push_and_pop_roundtrip() {
128        let (mut p, mut c) = raw_ring(16);
129        let n = p.push_slice(&[1.0, 2.0, 3.0, 4.0]);
130        assert_eq!(n, 4);
131        let mut out = [0.0f32; 4];
132        let got = c.pop_slice(&mut out);
133        assert_eq!(got, 4);
134        assert_eq!(out, [1.0, 2.0, 3.0, 4.0]);
135        assert_eq!(p.overflow_count(), 0);
136    }
137
138    #[test]
139    fn overflow_counts_dropped_and_never_blocks() {
140        let (mut p, mut c) = raw_ring(4);
141        // Push 6 samples into a capacity of 4: write 4 and drop 2.
142        let written = p.push_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
143        assert_eq!(written, 4);
144        assert_eq!(p.overflow_count(), 2);
145        assert_eq!(c.overflow_count(), 2);
146
147        let mut out = [0.0f32; 8];
148        let got = c.pop_slice(&mut out);
149        assert_eq!(got, 4);
150        assert_eq!(&out[..4], &[1.0, 2.0, 3.0, 4.0]);
151    }
152
153    #[test]
154    fn wraps_around() {
155        let (mut p, mut c) = raw_ring(4);
156        p.push_slice(&[1.0, 2.0, 3.0]);
157        let mut out = [0.0f32; 2];
158        c.pop_slice(&mut out); // Consume 2 and advance the read index.
159        assert_eq!(out, [1.0, 2.0]);
160        // The remaining 1 plus 3 new samples fills the ring. Verify wrapped writes.
161        let w = p.push_slice(&[4.0, 5.0, 6.0]);
162        assert_eq!(w, 3);
163        let mut out2 = [0.0f32; 4];
164        let got = c.pop_slice(&mut out2);
165        assert_eq!(got, 4);
166        assert_eq!(out2, [3.0, 4.0, 5.0, 6.0]);
167    }
168
169    /// Pushing an empty slice returns 0 and does not increment overflow (early return path).
170    #[test]
171    fn push_empty_is_noop() {
172        let (mut p, _c) = raw_ring(4);
173        assert_eq!(p.push_slice(&[]), 0);
174        assert_eq!(p.overflow_count(), 0);
175    }
176
177    /// Pushing to a full ring drops everything (`writable=0`) and increments overflow by that
178    /// number of samples. This also verifies that the real-time path does not block.
179    #[test]
180    fn full_ring_drops_entire_push() {
181        let (mut p, _c) = raw_ring(4);
182        assert_eq!(p.push_slice(&[1.0, 2.0, 3.0, 4.0]), 4); // Fill the ring.
183                                                            // No room remains; drop all 5 samples.
184        let w = p.push_slice(&[5.0; 5]);
185        assert_eq!(w, 0, "full ring cannot accept any samples");
186        assert_eq!(
187            p.overflow_count(),
188            5,
189            "all 5 samples are counted as overflow"
190        );
191    }
192
193    /// `pop_slice` pops only the available samples even if `dst` is larger (prevents off-by-one).
194    /// Verify a `dst` larger than the available data and that popping an empty ring returns 0.
195    #[test]
196    fn pop_slice_respects_available_and_dst_len() {
197        let (mut p, mut c) = raw_ring(8);
198        p.push_slice(&[1.0, 2.0, 3.0]);
199        assert_eq!(c.available(), 3);
200        // Pop only the 3 available samples, even though dst is larger.
201        let mut big = [0.0f32; 16];
202        assert_eq!(c.pop_slice(&mut big), 3);
203        assert_eq!(&big[..3], &[1.0, 2.0, 3.0]);
204        // The ring is empty, so the next pop returns 0.
205        assert_eq!(c.available(), 0);
206        assert_eq!(c.pop_slice(&mut big), 0);
207    }
208
209    /// Repeated drops keep incrementing the u64 overflow counter beyond u32::MAX without
210    /// saturating (`overflow` uses `AtomicU64`, separate from `dropped_before`'s u32 saturation).
211    #[test]
212    fn overflow_counter_exceeds_u32_max() {
213        let (mut p, _c) = raw_ring(1);
214        // Fill the ring with one sample; all subsequent samples will be dropped.
215        assert_eq!(p.push_slice(&[0.0]), 1);
216        // Drop more than u32::MAX samples. A single large slice can add many at once.
217        let big = vec![0.0f32; 1000];
218        let over_u32 = u64::from(u32::MAX) + 2_000;
219        let mut total_dropped = 0u64;
220        while total_dropped < over_u32 {
221            let w = p.push_slice(&big);
222            assert_eq!(w, 0, "full ring cannot accept any samples");
223            total_dropped += big.len() as u64;
224        }
225        assert!(
226            p.overflow_count() > u64::from(u32::MAX),
227            "overflow accumulates beyond u32::MAX: {}",
228            p.overflow_count()
229        );
230    }
231
232    /// A single `pop()` returns samples one at a time in FIFO order, then `None` when empty.
233    #[test]
234    fn single_pop_is_fifo_then_none() {
235        let (mut p, mut c) = raw_ring(4);
236        p.push_slice(&[10.0, 20.0]);
237        assert_eq!(c.pop(), Some(10.0));
238        assert_eq!(c.pop(), Some(20.0));
239        assert_eq!(c.pop(), None);
240    }
241
242    /// A capacity of 0 is clamped to at least 1 with `max(1)`, and push/pop work without panicking.
243    #[test]
244    fn zero_capacity_is_clamped_to_one() {
245        let (mut p, mut c) = raw_ring(0);
246        assert_eq!(
247            p.push_slice(&[7.0, 8.0]),
248            1,
249            "capacity is clamped to 1 and accepts one sample"
250        );
251        assert_eq!(p.overflow_count(), 1);
252        assert_eq!(c.pop(), Some(7.0));
253    }
254
255    /// The producer and consumer share the overflow counter (the same `Arc`).
256    #[test]
257    fn overflow_count_is_shared_between_ends() {
258        let (mut p, c) = raw_ring(2);
259        p.push_slice(&[1.0, 2.0, 3.0, 4.0]); // Write 2 and drop 2.
260        assert_eq!(p.overflow_count(), 2);
261        assert_eq!(
262            c.overflow_count(),
263            2,
264            "consumer sees the same overflow count"
265        );
266    }
267}