Files
SoulStream/crates/player/examples/focus_ack_probe.rs
T
KeelrLeek 6dcc62a2fd Phase 0.4: silence-ack primitive — pause_acked + desired-paused latch
Player::pause_acked() returns a generation-bound PauseAck applied on the
audio thread; a desired-paused latch makes an in-flight decoder come up
paused instead of auto-starting, closing the window where a pause lands
during decode setup. Proven live by examples/focus_ack_probe.rs:
ack-while-playing freezes position; ack-while-buffering never becomes
audible; resume clears the latch; plain pause() during buffering is
demonstrated to auto-start (pre-existing hazard, filed separately).

Transport ADR recorded (native), Phase 0 marked complete in the plan.
2026-07-22 18:17:37 -05:00

106 lines
4.2 KiB
Rust

//! Phase 0.4 — silence-acknowledgement proof (isolated diagnostic).
//!
//! Proves the audio-focus primitive the preview feature's take-audio-on-open
//! contract depends on:
//!
//! A. pause_acked while PLAYING → ack(sink_was_active), position freezes
//! B. pause_acked while BUFFERING → ack(latched), the in-flight decoder
//! comes up PAUSED — never audible.
//! (Plain pause() provably fails this:
//! case D demonstrates the old hazard.)
//! C. resume after ack → Playing again (latch cleared)
//! D. plain pause() while BUFFERING → track auto-starts anyway — the
//! documented hazard the latch closes.
//!
//! Run: cargo run -p player --example focus_ack_probe -- <mp3>
use std::sync::mpsc::RecvTimeoutError;
use std::time::{Duration, Instant};
use player::{Player, PlayerEvent};
fn collect(rx: &std::sync::mpsc::Receiver<PlayerEvent>, for_ms: u64) -> Vec<PlayerEvent> {
let end = Instant::now() + Duration::from_millis(for_ms);
let mut out = Vec::new();
loop {
let left = end.saturating_duration_since(Instant::now());
if left.is_zero() {
break;
}
match rx.recv_timeout(left) {
Ok(ev) => out.push(ev),
Err(RecvTimeoutError::Timeout) => break,
Err(RecvTimeoutError::Disconnected) => break,
}
}
out
}
fn saw_playing(evs: &[PlayerEvent]) -> bool {
evs.iter().any(|e| matches!(e, PlayerEvent::Playing))
}
fn last_pos(evs: &[PlayerEvent]) -> Option<f64> {
evs.iter().rev().find_map(|e| match e {
PlayerEvent::Position { secs, .. } => Some(*secs),
_ => None,
})
}
fn main() {
let file = std::env::args().nth(1).expect("argv[1]: an mp3 file");
// ---- A: ack while playing ------------------------------------------
let (p, rx) = Player::spawn();
p.play_local(&file, Some(30.0));
let evs = collect(&rx, 2500);
assert!(saw_playing(&evs), "A: track should start");
let ack = p.pause_acked().recv_timeout(Duration::from_millis(1000)).expect("A: ack");
let pos_at_ack = last_pos(&collect(&rx, 200)).unwrap_or(0.0);
let after = collect(&rx, 800);
let pos_later = last_pos(&after).unwrap_or(pos_at_ack);
let frozen = (pos_later - pos_at_ack).abs() < 0.075; // one 60ms tick of slack
println!(
"PROBE A ack-while-playing: sink_was_active={} latched={} gen={} frozen={}",
ack.sink_was_active, ack.latched, ack.generation, frozen
);
assert!(ack.sink_was_active && !ack.latched && frozen, "A failed");
// ---- C (on the same instance): resume clears the latch --------------
p.resume();
let evs = collect(&rx, 900);
println!("PROBE C resume-after-ack: playing={}", saw_playing(&evs));
assert!(saw_playing(&evs), "C failed");
p.stop();
drop(p);
let _ = collect(&rx, 400);
// ---- B: ack while buffering — the latch case ------------------------
let (p2, rx2) = Player::spawn();
p2.play_local(&file, Some(30.0));
// no waiting: the decoder thread is still setting up
let ack2 = p2.pause_acked().recv_timeout(Duration::from_millis(1000)).expect("B: ack");
let evs = collect(&rx2, 2500);
let became_audible = saw_playing(&evs);
let came_up_paused = evs.iter().any(|e| matches!(e, PlayerEvent::Paused));
println!(
"PROBE B ack-while-buffering: latched={} sink_was_active={} came_up_paused={} became_audible={}",
ack2.latched, ack2.sink_was_active, came_up_paused, became_audible
);
assert!(ack2.latched && !became_audible, "B failed: latch leaked audio");
p2.stop();
drop(p2);
let _ = collect(&rx2, 400);
// ---- D: the pre-existing hazard, demonstrated on plain pause() ------
let (p3, rx3) = Player::spawn();
p3.play_local(&file, Some(30.0));
p3.pause(); // fire-and-forget, sink not built yet → dropped on the floor
let evs = collect(&rx3, 2500);
let auto_started = saw_playing(&evs);
println!("PROBE D plain-pause-while-buffering: auto_started={auto_started} (the hazard the latch closes)");
p3.stop();
println!("ALL PROBES PASSED");
}