slice-4 (dev-b): TapAudioPipe::barge_in_flush + advisory_tx + MockRealtimeBrain schedule #9
@@ -25,6 +25,11 @@ pub struct TapMetrics {
|
||||
pub unknown_frames: AtomicU64,
|
||||
pub malformed_frames: AtomicU64,
|
||||
pub reconnect_attempts: AtomicU64,
|
||||
/// slice-4 §3.3: count of in-flight `audio_out` frames dropped from
|
||||
/// `rx_audio_out` during `barge_in_flush`. The drain makes the resume
|
||||
/// condition race-free — the first `audio_out` observed post-barge is
|
||||
/// provably post-barge.
|
||||
pub barge_drained_inflight: AtomicU64,
|
||||
}
|
||||
|
||||
impl TapMetrics {
|
||||
@@ -45,6 +50,7 @@ impl TapMetrics {
|
||||
unknown_frames: self.unknown_frames.load(Ordering::Relaxed),
|
||||
malformed_frames: self.malformed_frames.load(Ordering::Relaxed),
|
||||
reconnect_attempts: self.reconnect_attempts.load(Ordering::Relaxed),
|
||||
barge_drained_inflight: self.barge_drained_inflight.load(Ordering::Relaxed),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,4 +65,5 @@ pub struct MetricsSnapshot {
|
||||
pub unknown_frames: u64,
|
||||
pub malformed_frames: u64,
|
||||
pub reconnect_attempts: u64,
|
||||
pub barge_drained_inflight: u64,
|
||||
}
|
||||
|
||||
@@ -130,11 +130,36 @@ impl rutster_media::AudioPipe for TapAudioPipe {
|
||||
debug!(cleared, "playout ring flushed on brain disconnect");
|
||||
}
|
||||
}
|
||||
|
||||
/// slice-4 spec §3.3 — barge-in flush: clear the playout ring AND
|
||||
/// drain `rx_audio_out` of any frames queued before the barge. Without
|
||||
/// this drain, a stale brain frame in the mpsc would un-mute
|
||||
/// immediately on the next tick — defeating the "first fresh audio_out"
|
||||
/// resume condition. Hot-path: try_recv loop, bounded, no blocking.
|
||||
fn barge_in_flush(&mut self) {
|
||||
let cleared = self.playout_ring.len();
|
||||
self.playout_ring.clear();
|
||||
if cleared > 0 {
|
||||
debug!(cleared, "playout ring flushed on barge-in");
|
||||
}
|
||||
|
||||
let mut drained = 0usize;
|
||||
while self.rx_audio_out.try_recv().is_ok() {
|
||||
drained += 1;
|
||||
}
|
||||
if drained > 0 {
|
||||
self.metrics
|
||||
.barge_drained_inflight
|
||||
.fetch_add(drained as u64, Ordering::Relaxed);
|
||||
debug!(drained, "in-flight brain frames drained on barge-in");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rutster_media::AudioPipe;
|
||||
|
||||
#[allow(clippy::type_complexity)] // test helper: 5-tuple of channel ends; not worth a struct.
|
||||
fn channels() -> (
|
||||
@@ -229,4 +254,53 @@ mod tests {
|
||||
// Now next_pcm_frame should return None (silence) — Disconnected path.
|
||||
assert!(pipe.next_pcm_frame().is_none());
|
||||
}
|
||||
|
||||
/// slice-4 §3.3: barge_in_flush clears the playout ring AND drains the
|
||||
/// inbound `rx_audio_out` mpsc of any frames queued before the barge.
|
||||
/// Without draining the mpsc, a stale pre-barge frame would un-mute
|
||||
/// immediately on the next tick — defeating the "first fresh audio_out"
|
||||
/// resume condition.
|
||||
#[test]
|
||||
fn barge_in_flush_clears_ring_and_drains_rx_audio_out() {
|
||||
let (_tx_pcm_in, _rx_pcm_in, tx_audio_out, rx_audio_out, metrics) = channels();
|
||||
let mut pipe = TapAudioPipe::new(tx_audio_out.clone(), rx_audio_out, metrics.clone());
|
||||
|
||||
// Put some frames into the playout ring first (simulating steady-state
|
||||
// playout that already drained from the mpsc).
|
||||
for i in 0..2 {
|
||||
let mut f = PcmFrame::zeroed();
|
||||
f.samples[0] = i as i16;
|
||||
tx_audio_out.blocking_send(f).unwrap();
|
||||
}
|
||||
let _ = pipe.next_pcm_frame(); // drains mpsc into ring, pops first
|
||||
|
||||
// Now queue MORE frames into rx_audio_out that have NOT been pulled
|
||||
// into the ring yet — these are the "in-flight" stale frames that
|
||||
// must be drained during a barge-in to keep resume race-free.
|
||||
for i in 0..3 {
|
||||
let mut f = PcmFrame::zeroed();
|
||||
f.samples[0] = (10 + i) as i16;
|
||||
tx_audio_out.blocking_send(f).unwrap();
|
||||
}
|
||||
|
||||
// Barge-in: ring should clear + the 3 mpsc frames drain.
|
||||
pipe.barge_in_flush();
|
||||
|
||||
assert!(pipe.next_pcm_frame().is_none());
|
||||
assert_eq!(
|
||||
metrics.barge_drained_inflight.load(Ordering::Relaxed),
|
||||
3,
|
||||
"three in-flight mpsc frames should be drained on barge-in"
|
||||
);
|
||||
}
|
||||
|
||||
/// slice-4 §3.3: barge_in_flush when empty is a no-op and leaves the
|
||||
/// counter at zero.
|
||||
#[test]
|
||||
fn barge_in_flush_when_already_empty_is_noop() {
|
||||
let (_tx_pcm_in, _rx_pcm_in, _tx_audio_out, rx_audio_out, metrics) = channels();
|
||||
let mut pipe = TapAudioPipe::new(_tx_pcm_in, rx_audio_out, metrics.clone());
|
||||
pipe.barge_in_flush();
|
||||
assert_eq!(metrics.barge_drained_inflight.load(Ordering::Relaxed), 0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user