fix: defer the open crossfade so main no longer fades into a gap
Opening the preview faded the main track out at focus-acquire time, but the first preview clip only becomes audible after resolve/load/decode — so on the way IN the 340 ms main fade completed into silence before the preview arrived (a gap the user heard; the exit was already fine). The main fade is now deferred: focusBridge.requestSilence only arms the commitment and grants; coordinator.beginMainSilence performs the actual fade-pause, and the service fires it from afterChange on the opening session's first audible clip (or ITEM_FAILED). Main-cos-out then overlaps preview-sin-in — a true cross. resumeMain skips the fade-resume when the fade never fired (close before audible), and beginMainSilence re-checks the main is sounding before fading. The focus coordinator's acquisition logic and tests are untouched. Verified live: preview audible at 275 ms, main played until then, faded ~343 ms, paused at 618 ms and held frozen; close resumed from the exact position in 15 ms.
This commit is contained in:
1 parent
691fdcdc9a
commit
abc959ca2f
3 files changed
+88
-19
No files matched your search
@@ -64,6 +64,10 @@ export function createPreviewService({
|
||||
let settleTimer = null;
|
||||
let focusToken = null;
|
||||
let focusWaiting = false;
|
||||
// Deferred open crossfade: true from a granted open until the first
|
||||
// preview clip is audible (or definitively won't be), at which point the
|
||||
// main track's fade-out fires — synced to the preview's fade-in.
|
||||
let pendingOpenSilence = false;
|
||||
// Burst detection for the adaptive settle window.
|
||||
let lastNavAt = -Infinity;
|
||||
// One early auto-advance per generation, however many position events
|
||||
@@ -94,6 +98,14 @@ export function createPreviewService({
|
||||
if (generationMoved && state.phase === PHASE.RESOLVING_MEDIA) {
|
||||
scheduleSettle();
|
||||
}
|
||||
// Deferred open crossfade: fade the main track out the instant the
|
||||
// opening preview clip becomes audible (PLAYING) — so the ramps
|
||||
// overlap — or the moment it's clear it never will (ITEM_FAILED),
|
||||
// matching the old take-audio-on-open behaviour on failure.
|
||||
if (pendingOpenSilence && (state.phase === PHASE.PLAYING || state.phase === PHASE.ITEM_FAILED)) {
|
||||
pendingOpenSilence = false;
|
||||
focus.coordinator.beginMainSilence?.();
|
||||
}
|
||||
maybeScheduleEarlyAdvance();
|
||||
}
|
||||
|
||||
@@ -323,6 +335,10 @@ export function createPreviewService({
|
||||
state = createInitialState(sessionId);
|
||||
emit();
|
||||
const granted = await ensureFocus();
|
||||
// Arm the deferred main fade only on a granted open (a parked
|
||||
// session leaves the main track playing). It fires from
|
||||
// afterChange when the first clip is audible.
|
||||
pendingOpenSilence = granted;
|
||||
dispatch({ type: "MANIFEST_READY", sessionId, manifest, startAtManifestIndex });
|
||||
if (!granted && state.phase !== PHASE.EMPTY) {
|
||||
dispatch({ type: "SET_DESIRED_PLAYING", sessionId, value: false });
|
||||
@@ -411,6 +427,10 @@ export function createPreviewService({
|
||||
scheduler.clear(settleTimer);
|
||||
settleTimer = null;
|
||||
}
|
||||
// Closing before the opening clip ever sounded: leave the deferred
|
||||
// fade unfired — release() will find the main still playing and
|
||||
// simply disarm it (no fade-resume dip).
|
||||
pendingOpenSilence = false;
|
||||
dispatch({ type: "CLOSE", sessionId: state.sessionId });
|
||||
prefetched.clear();
|
||||
const fades = typeof transport.fadeOut === "function";
|
||||
|
||||
Reference in new issue
Block a user