flexaudio_core/
chunk_ring.rs1use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::{Arc, Mutex};
15
16use ringbuf::traits::{Consumer, Observer, RingBuffer};
17use ringbuf::HeapRb;
18
19use crate::types::{AudioChunk, ChunkFlags};
20
21type Shared = Arc<Mutex<HeapRb<AudioChunk>>>;
22
23pub fn chunk_ring(capacity_chunks: usize) -> (ChunkProducer, ChunkConsumer) {
29 let cap = capacity_chunks.max(1);
30 let rb: Shared = Arc::new(Mutex::new(HeapRb::<AudioChunk>::new(cap)));
31 let dropped = Arc::new(AtomicU64::new(0));
32 (
33 ChunkProducer {
34 rb: rb.clone(),
35 dropped: dropped.clone(),
36 },
37 ChunkConsumer {
38 rb,
39 dropped,
40 observed_drops: 0,
41 },
42 )
43}
44
45pub struct ChunkProducer {
47 rb: Shared,
48 dropped: Arc<AtomicU64>,
49}
50
51impl ChunkProducer {
52 pub fn push(&mut self, chunk: AudioChunk) -> Option<u64> {
62 self.push_with_evicted(chunk).map(|(total, _)| total)
63 }
64
65 pub fn push_with_evicted(&mut self, mut chunk: AudioChunk) -> Option<(u64, AudioChunk)> {
69 let mut rb = self.rb.lock().unwrap_or_else(|e| e.into_inner());
72
73 let will_evict = rb.is_full();
75
76 if will_evict {
77 self.dropped.fetch_add(1, Ordering::Relaxed);
78 }
79 let total = self.dropped.load(Ordering::Relaxed);
81 chunk.dropped_before = u32::try_from(total).unwrap_or(u32::MAX);
82
83 let evicted = rb.push_overwrite(chunk);
84 drop(rb);
85
86 debug_assert_eq!(evicted.is_some(), will_evict);
87
88 evicted.map(|chunk| (total, chunk))
89 }
90
91 pub fn dropped_count(&self) -> u64 {
93 self.dropped.load(Ordering::Relaxed)
94 }
95}
96
97pub struct ChunkConsumer {
99 rb: Shared,
100 dropped: Arc<AtomicU64>,
101 observed_drops: u64,
102}
103
104impl ChunkConsumer {
105 pub fn try_pop(&mut self) -> Option<AudioChunk> {
109 let mut rb = self.rb.lock().unwrap_or_else(|e| e.into_inner());
111 let mut chunk = rb.try_pop()?;
112 let dropped = self.dropped.load(Ordering::Relaxed);
113 if dropped != self.observed_drops {
114 chunk.flags |= ChunkFlags::DISCONTINUITY;
117 self.observed_drops = dropped;
118 }
119 Some(chunk)
120 }
121
122 pub fn len(&self) -> usize {
124 self.rb
126 .lock()
127 .unwrap_or_else(|e| e.into_inner())
128 .occupied_len()
129 }
130
131 pub fn is_empty(&self) -> bool {
133 self.len() == 0
134 }
135
136 pub fn dropped_count(&self) -> u64 {
138 self.dropped.load(Ordering::Relaxed)
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::types::ChunkFlags;
146
147 fn chunk(seq: u64) -> AudioChunk {
148 AudioChunk {
149 frame_index: seq * 960,
150 data: vec![0.0; 1920],
151 frames: 960,
152 pts_ns: seq as i64 * 20_000_000,
153 seq,
154 flags: ChunkFlags::empty(),
155 dropped_before: 0,
156 peak: 0.0,
157 rms: 0.0,
158 }
159 }
160
161 #[test]
162 fn fifo_order_when_not_full() {
163 let (mut p, mut c) = chunk_ring(4);
164 assert!(c.is_empty());
165 for s in 0..3 {
166 assert_eq!(p.push(chunk(s)), None);
167 }
168 assert_eq!(c.len(), 3);
169 assert_eq!(c.try_pop().unwrap().seq, 0);
170 assert_eq!(c.try_pop().unwrap().seq, 1);
171 assert_eq!(c.try_pop().unwrap().seq, 2);
172 assert!(c.try_pop().is_none());
173 }
174
175 #[test]
176 fn drop_oldest_when_full_and_counts() {
177 let (mut p, mut c) = chunk_ring(2);
178 assert_eq!(p.push(chunk(0)), None);
180 assert_eq!(p.push(chunk(1)), None);
181 assert_eq!(p.dropped_count(), 0);
182
183 let dropped_total = p.push(chunk(2));
185 assert_eq!(dropped_total, Some(1));
186 assert_eq!(p.dropped_count(), 1);
187
188 let dropped_total = p.push(chunk(3));
190 assert_eq!(dropped_total, Some(2));
191 assert_eq!(p.dropped_count(), 2);
192
193 let first = c.try_pop().unwrap();
195 assert_eq!(first.seq, 2);
196 assert_eq!(first.dropped_before, 1);
198
199 let second = c.try_pop().unwrap();
200 assert_eq!(second.seq, 3);
201 assert_eq!(second.dropped_before, 2);
203
204 assert!(c.try_pop().is_none());
205 }
206
207 #[test]
208 fn dropped_before_is_cumulative() {
209 let (mut p, mut c) = chunk_ring(1);
210 p.push(chunk(0)); p.push(chunk(1)); c.try_pop(); let r = p.push(chunk(2)); assert_eq!(r, None);
216 let got = c.try_pop().unwrap();
217 assert_eq!(got.seq, 2);
218 assert_eq!(got.dropped_before, 1);
220 assert_eq!(p.dropped_count(), 1);
221 }
222
223 #[test]
225 fn zero_capacity_is_clamped_to_one() {
226 let (mut p, mut c) = chunk_ring(0);
227 assert!(c.is_empty());
228 assert_eq!(p.push(chunk(0)), None); assert_eq!(c.len(), 1);
230 assert_eq!(p.push(chunk(1)), Some(1));
232 let got = c.try_pop().unwrap();
233 assert_eq!(got.seq, 1);
234 }
235
236 #[test]
238 fn len_tracks_occupancy_and_is_capped() {
239 let (mut p, mut c) = chunk_ring(3);
240 assert!(c.is_empty());
241 for s in 0..3 {
242 p.push(chunk(s));
243 }
244 assert_eq!(c.len(), 3);
245 assert!(!c.is_empty());
246 p.push(chunk(3));
248 p.push(chunk(4));
249 assert_eq!(c.len(), 3, "occupancy never exceeds capacity 3");
250 assert_eq!(c.try_pop().unwrap().seq, 2);
252 assert_eq!(c.try_pop().unwrap().seq, 3);
253 assert_eq!(c.try_pop().unwrap().seq, 4);
254 assert!(c.try_pop().is_none());
255 assert!(c.is_empty());
256 }
257
258 #[test]
261 fn empty_pop_is_none_and_dropped_is_shared() {
262 let (mut p, mut c) = chunk_ring(2);
263 assert!(c.try_pop().is_none());
264 p.push(chunk(0));
266 p.push(chunk(1));
267 p.push(chunk(2)); assert_eq!(p.dropped_count(), 1);
269 assert_eq!(c.dropped_count(), 1, "consumer sees the same dropped count");
270 }
271 #[test]
272 fn dropped_chunk_preserves_its_exact_frame_gap() {
273 let (mut producer, mut consumer) = chunk_ring(1);
274 producer.push(chunk(0));
275 let first = consumer.try_pop().unwrap();
276 producer.push(chunk(1));
277 producer.push(chunk(2));
278 let next = consumer.try_pop().unwrap();
279 assert_eq!(
280 next.frame_index - first.frame_index - first.frames as u64,
281 960
282 );
283 assert_eq!(next.dropped_before, 1);
284 }
285
286 #[test]
287 fn eviction_returns_actual_pcm_and_marks_oldest_survivor_once() {
288 let (mut producer, mut consumer) = chunk_ring(3);
289 let mut tail = chunk(0);
290 tail.frames = 13;
291 tail.data.truncate(26);
292 producer.push(tail.clone());
293 producer.push(chunk(1));
294 producer.push(chunk(2));
295 let (total, evicted) = producer.push_with_evicted(chunk(3)).unwrap();
296 assert_eq!(total, 1);
297 assert_eq!(evicted, tail);
298 let next = consumer.try_pop().unwrap();
299 assert_eq!(next.seq, 1);
300 assert_eq!(
301 next.dropped_before, 0,
302 "survivor was queued before eviction"
303 );
304 assert!(next.flags.contains(ChunkFlags::DISCONTINUITY));
305 assert!(!consumer
306 .try_pop()
307 .unwrap()
308 .flags
309 .contains(ChunkFlags::DISCONTINUITY));
310 assert!(!consumer
311 .try_pop()
312 .unwrap()
313 .flags
314 .contains(ChunkFlags::DISCONTINUITY));
315 }
316}