use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use ringbuf::traits::{Consumer, Observer, RingBuffer};
use ringbuf::HeapRb;
use crate::types::{AudioChunk, ChunkFlags};
type Shared = Arc<Mutex<HeapRb<AudioChunk>>>;
pub fn chunk_ring(capacity_chunks: usize) -> (ChunkProducer, ChunkConsumer) {
let cap = capacity_chunks.max(1);
let rb: Shared = Arc::new(Mutex::new(HeapRb::<AudioChunk>::new(cap)));
let dropped = Arc::new(AtomicU64::new(0));
(
ChunkProducer {
rb: rb.clone(),
dropped: dropped.clone(),
},
ChunkConsumer {
rb,
dropped,
observed_drops: 0,
},
)
}
pub struct ChunkProducer {
rb: Shared,
dropped: Arc<AtomicU64>,
}
impl ChunkProducer {
pub fn push(&mut self, chunk: AudioChunk) -> Option<u64> {
self.push_with_evicted(chunk).map(|(total, _)| total)
}
pub fn push_with_evicted(&mut self, mut chunk: AudioChunk) -> Option<(u64, AudioChunk)> {
let mut rb = self.rb.lock().unwrap_or_else(|e| e.into_inner());
let will_evict = rb.is_full();
if will_evict {
self.dropped.fetch_add(1, Ordering::Relaxed);
}
let total = self.dropped.load(Ordering::Relaxed);
chunk.dropped_before = u32::try_from(total).unwrap_or(u32::MAX);
let evicted = rb.push_overwrite(chunk);
drop(rb);
debug_assert_eq!(evicted.is_some(), will_evict);
evicted.map(|chunk| (total, chunk))
}
pub fn dropped_count(&self) -> u64 {
self.dropped.load(Ordering::Relaxed)
}
}
pub struct ChunkConsumer {
rb: Shared,
dropped: Arc<AtomicU64>,
observed_drops: u64,
}
impl ChunkConsumer {
pub fn try_pop(&mut self) -> Option<AudioChunk> {
let mut rb = self.rb.lock().unwrap_or_else(|e| e.into_inner());
let mut chunk = rb.try_pop()?;
let dropped = self.dropped.load(Ordering::Relaxed);
if dropped != self.observed_drops {
chunk.flags |= ChunkFlags::DISCONTINUITY;
self.observed_drops = dropped;
}
Some(chunk)
}
pub fn len(&self) -> usize {
self.rb
.lock()
.unwrap_or_else(|e| e.into_inner())
.occupied_len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn dropped_count(&self) -> u64 {
self.dropped.load(Ordering::Relaxed)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::ChunkFlags;
fn chunk(seq: u64) -> AudioChunk {
AudioChunk {
frame_index: seq * 960,
data: vec![0.0; 1920],
frames: 960,
pts_ns: seq as i64 * 20_000_000,
seq,
flags: ChunkFlags::empty(),
dropped_before: 0,
peak: 0.0,
rms: 0.0,
}
}
#[test]
fn fifo_order_when_not_full() {
let (mut p, mut c) = chunk_ring(4);
assert!(c.is_empty());
for s in 0..3 {
assert_eq!(p.push(chunk(s)), None);
}
assert_eq!(c.len(), 3);
assert_eq!(c.try_pop().unwrap().seq, 0);
assert_eq!(c.try_pop().unwrap().seq, 1);
assert_eq!(c.try_pop().unwrap().seq, 2);
assert!(c.try_pop().is_none());
}
#[test]
fn drop_oldest_when_full_and_counts() {
let (mut p, mut c) = chunk_ring(2);
assert_eq!(p.push(chunk(0)), None);
assert_eq!(p.push(chunk(1)), None);
assert_eq!(p.dropped_count(), 0);
let dropped_total = p.push(chunk(2));
assert_eq!(dropped_total, Some(1));
assert_eq!(p.dropped_count(), 1);
let dropped_total = p.push(chunk(3));
assert_eq!(dropped_total, Some(2));
assert_eq!(p.dropped_count(), 2);
let first = c.try_pop().unwrap();
assert_eq!(first.seq, 2);
assert_eq!(first.dropped_before, 1);
let second = c.try_pop().unwrap();
assert_eq!(second.seq, 3);
assert_eq!(second.dropped_before, 2);
assert!(c.try_pop().is_none());
}
#[test]
fn dropped_before_is_cumulative() {
let (mut p, mut c) = chunk_ring(1);
p.push(chunk(0)); p.push(chunk(1)); c.try_pop(); let r = p.push(chunk(2)); assert_eq!(r, None);
let got = c.try_pop().unwrap();
assert_eq!(got.seq, 2);
assert_eq!(got.dropped_before, 1);
assert_eq!(p.dropped_count(), 1);
}
#[test]
fn zero_capacity_is_clamped_to_one() {
let (mut p, mut c) = chunk_ring(0);
assert!(c.is_empty());
assert_eq!(p.push(chunk(0)), None); assert_eq!(c.len(), 1);
assert_eq!(p.push(chunk(1)), Some(1));
let got = c.try_pop().unwrap();
assert_eq!(got.seq, 1);
}
#[test]
fn len_tracks_occupancy_and_is_capped() {
let (mut p, mut c) = chunk_ring(3);
assert!(c.is_empty());
for s in 0..3 {
p.push(chunk(s));
}
assert_eq!(c.len(), 3);
assert!(!c.is_empty());
p.push(chunk(3));
p.push(chunk(4));
assert_eq!(c.len(), 3, "occupancy never exceeds capacity 3");
assert_eq!(c.try_pop().unwrap().seq, 2);
assert_eq!(c.try_pop().unwrap().seq, 3);
assert_eq!(c.try_pop().unwrap().seq, 4);
assert!(c.try_pop().is_none());
assert!(c.is_empty());
}
#[test]
fn empty_pop_is_none_and_dropped_is_shared() {
let (mut p, mut c) = chunk_ring(2);
assert!(c.try_pop().is_none());
p.push(chunk(0));
p.push(chunk(1));
p.push(chunk(2)); assert_eq!(p.dropped_count(), 1);
assert_eq!(c.dropped_count(), 1, "consumer sees the same dropped count");
}
#[test]
fn dropped_chunk_preserves_its_exact_frame_gap() {
let (mut producer, mut consumer) = chunk_ring(1);
producer.push(chunk(0));
let first = consumer.try_pop().unwrap();
producer.push(chunk(1));
producer.push(chunk(2));
let next = consumer.try_pop().unwrap();
assert_eq!(
next.frame_index - first.frame_index - first.frames as u64,
960
);
assert_eq!(next.dropped_before, 1);
}
#[test]
fn eviction_returns_actual_pcm_and_marks_oldest_survivor_once() {
let (mut producer, mut consumer) = chunk_ring(3);
let mut tail = chunk(0);
tail.frames = 13;
tail.data.truncate(26);
producer.push(tail.clone());
producer.push(chunk(1));
producer.push(chunk(2));
let (total, evicted) = producer.push_with_evicted(chunk(3)).unwrap();
assert_eq!(total, 1);
assert_eq!(evicted, tail);
let next = consumer.try_pop().unwrap();
assert_eq!(next.seq, 1);
assert_eq!(
next.dropped_before, 0,
"survivor was queued before eviction"
);
assert!(next.flags.contains(ChunkFlags::DISCONTINUITY));
assert!(!consumer
.try_pop()
.unwrap()
.flags
.contains(ChunkFlags::DISCONTINUITY));
assert!(!consumer
.try_pop()
.unwrap()
.flags
.contains(ChunkFlags::DISCONTINUITY));
}
}