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:
KeelrLeek committed 2026-07-23 16:05:14 -05:00
1 parent 5e877c0b0a
commit 793f78370e
27 files changed
+2834 -178

No files matched your search

+12 -3
View File
@@ -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);