From 58919b8bf7dd3bdeea7deeb9bcf966c545d68efc Mon Sep 17 00:00:00 2001 From: "Aaron D. Lee" Date: Fri, 3 Jul 2026 00:01:38 -0400 Subject: [PATCH] =?UTF-8?q?feat(tap):=20TapAudioPipe::barge=5Fin=5Fflush?= =?UTF-8?q?=20+=20barge=5Fdrained=5Finflight=20(slice-4=20=C2=A73.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The kill-now path on the seam object: clears the playout ring AND drains rx_audio_out of pre-barge in-flight brain frames. The drain is what makes the resume condition race-free — the first audio_out post-barge is provably post-barge. Signed-off-by: Aaron D. Lee --- crates/rutster-tap/src/metrics.rs | 7 +++ crates/rutster-tap/src/tap_audio_pipe.rs | 74 ++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/crates/rutster-tap/src/metrics.rs b/crates/rutster-tap/src/metrics.rs index b0a1e73..dc7cbca 100644 --- a/crates/rutster-tap/src/metrics.rs +++ b/crates/rutster-tap/src/metrics.rs @@ -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, } diff --git a/crates/rutster-tap/src/tap_audio_pipe.rs b/crates/rutster-tap/src/tap_audio_pipe.rs index 91e2bd9..98b5955 100644 --- a/crates/rutster-tap/src/tap_audio_pipe.rs +++ b/crates/rutster-tap/src/tap_audio_pipe.rs @@ -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); + } }