feat: crossfade preview suite, loudness normalization, debug overlay
Preview modal audio, five rounds: - True track-change crossfade: scheduleSettle no longer silences the outgoing clip at t=0; preview_load's equal-power fade owns both ramps, gated on the incoming voice's first Playing event (audible_marks), with failure/overstay fallbacks. CROSSFADE_MS = 340 — locked to the reel's card-slide so audio and visuals land together. - Two-neighbour lease prefetch + a 15-min on-disk clip byte-cache (recent_clips): steps in BOTH directions crossfade in ~15 ms; revisits never re-download. - Adaptive settle (isolated steps fire immediately, bursts coalesce) and anticipatory auto-advance (step at clip end minus 400 ms) so auto-play crossfades over the tail instead of gapping. - Session resume memory: revisited tracks continue where they left off (decoder-level start offset via play_local_from/resume_at); finished tracks restart from the top. - Open/close crossfades with the MAIN player: fade-pause with the desired-paused latch landing at ramp end, fade-resume from the paused position; MAIN_FADE_GEN guards; focus handshake relaxation documented in focusBridge. Volume normalization (Settings > Playback, default on): - Dependency-free BS.1770/EBU R128 meter in the player crate (K-weighting, 400 ms gated blocks, incremental) + analyzer that reads the same growing file playback streams from — first plays are measured at download speed. - Gain layered under user volume; audible start GATED on a preliminary measurement (10 s of audio or 800 ms budget) so the level is right from the first sample and never changes mid-track; prelim boosts capped 3 dB, peak-aware clipping guard, persistent loudness-cache.json. - Gapless boundaries get their own sample-accurate gain via SetNextNormGainDb, fed by preload analysis. Debug overlay (Ctrl+Shift+D): process-tree CPU / PDH GPU / working-set + private-commit RAM / network rates / FPS / current-track download progress read from the backend's own StreamProgress / live normalization state / preview session phase. Polls only while visible. 131 JS + player + app tests green; every path verified live over CDP.
This commit is contained in:
1 parent
5e877c0b0a
commit
793f78370e
27 files changed
+2834
-178
No files matched your search
@@ -55,7 +55,10 @@ fn main() {
|
||||
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 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);
|
||||
@@ -79,7 +82,10 @@ fn main() {
|
||||
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 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));
|
||||
@@ -87,7 +93,10 @@ fn main() {
|
||||
"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");
|
||||
assert!(
|
||||
ack2.latched && !became_audible,
|
||||
"B failed: latch leaked audio"
|
||||
);
|
||||
p2.stop();
|
||||
drop(p2);
|
||||
let _ = collect(&rx2, 400);
|
||||
|
||||
@@ -65,7 +65,10 @@ fn main() {
|
||||
|
||||
drain("preview", &prev_rx, 2500, &mut log);
|
||||
let started = log.iter().any(|l| l.contains("Playing"));
|
||||
println!("PROBE preview-starts: {started} ({} ms to first events)", t0.elapsed().as_millis());
|
||||
println!(
|
||||
"PROBE preview-starts: {started} ({} ms to first events)",
|
||||
t0.elapsed().as_millis()
|
||||
);
|
||||
|
||||
prev.pause();
|
||||
drain("preview", &prev_rx, 600, &mut log);
|
||||
@@ -78,7 +81,9 @@ fn main() {
|
||||
if let Some((_main, main_rx, ref path)) = main_pair {
|
||||
let mut main_log = Vec::new();
|
||||
drain("main", &main_rx, 1200, &mut main_log);
|
||||
let main_alive = main_log.iter().any(|l| l.contains("Playing") || l.contains("Position"));
|
||||
let main_alive = main_log
|
||||
.iter()
|
||||
.any(|l| l.contains("Playing") || l.contains("Position"));
|
||||
println!("PROBE main-coexists: {main_alive} ({path})");
|
||||
// cross-talk check: no preview-channel event may appear on main's
|
||||
// channel or vice versa — separate receivers make this structural,
|
||||
|
||||
Reference in new issue
Block a user