feat(tap): TapAudioPipe::barge_in_flush + barge_drained_inflight (slice-4 §3.3)
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 <himself@adlee.work>
This commit is contained in:
@@ -25,6 +25,11 @@ pub struct TapMetrics {
|
|||||||
pub unknown_frames: AtomicU64,
|
pub unknown_frames: AtomicU64,
|
||||||
pub malformed_frames: AtomicU64,
|
pub malformed_frames: AtomicU64,
|
||||||
pub reconnect_attempts: 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 {
|
impl TapMetrics {
|
||||||
@@ -45,6 +50,7 @@ impl TapMetrics {
|
|||||||
unknown_frames: self.unknown_frames.load(Ordering::Relaxed),
|
unknown_frames: self.unknown_frames.load(Ordering::Relaxed),
|
||||||
malformed_frames: self.malformed_frames.load(Ordering::Relaxed),
|
malformed_frames: self.malformed_frames.load(Ordering::Relaxed),
|
||||||
reconnect_attempts: self.reconnect_attempts.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 unknown_frames: u64,
|
||||||
pub malformed_frames: u64,
|
pub malformed_frames: u64,
|
||||||
pub reconnect_attempts: 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");
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use rutster_media::AudioPipe;
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)] // test helper: 5-tuple of channel ends; not worth a struct.
|
#[allow(clippy::type_complexity)] // test helper: 5-tuple of channel ends; not worth a struct.
|
||||||
fn channels() -> (
|
fn channels() -> (
|
||||||
@@ -229,4 +254,53 @@ mod tests {
|
|||||||
// Now next_pcm_frame should return None (silence) — Disconnected path.
|
// Now next_pcm_frame should return None (silence) — Disconnected path.
|
||||||
assert!(pipe.next_pcm_frame().is_none());
|
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