slice-4 (dev-b): TapAudioPipe::barge_in_flush + advisory_tx + MockRealtimeBrain schedule #9

Merged
alee merged 9 commits from slice-4-dev-b-tap into main 2026-07-04 01:43:44 +00:00
2 changed files with 81 additions and 0 deletions
Showing only changes of commit 58919b8bf7 - Show all commits

View File

@@ -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,
}

View File

@@ -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);
}
}