fix(sim): cancel in-flight volume fades so audio isn't silenced after landing
Operator: audio turned off sometimes after landing on a new altitude. Root cause: fadeVolume started a setInterval ramp that was never cancelled. When restAudio faded the idle element to 0 and — within FADE_MS — that same element became the newly-landed ACTIVE scale (assignElements reuses an element per soundtrack url), the stale fade-to-0 kept running and dragged the now-active element back to silence. Fix: cancelFade() supersedes any in-flight fade when a new fade starts (fadeVolume) or a direct volume is set (ensurePlaying). E2E proves a new fade supersedes a stale one (fails without the fix). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+16
-1
@@ -1076,19 +1076,33 @@ aud.addEventListener("error", updateAudioStatus);
|
||||
audB.addEventListener("error", updateAudioStatus);
|
||||
setInterval(updateAudioStatus, 400);
|
||||
|
||||
// Cancel any in-flight fade on an element. A fade is a setInterval ramping volume;
|
||||
// if a stale one keeps running after a new fade or a direct volume set, it drags the
|
||||
// element back toward its old target (e.g. a fade-to-0 on what becomes the ACTIVE
|
||||
// element silences it — the "audio off after landing" bug).
|
||||
function cancelFade(el) {
|
||||
if (el._fadeIv) { clearInterval(el._fadeIv); el._fadeIv = null; }
|
||||
}
|
||||
|
||||
function fadeVolume(el, to, ms, done) {
|
||||
// setInterval (not requestAnimationFrame): rAF throttles/pauses when the tab is
|
||||
// backgrounded, which would stall the gain ramp; a timer fires regardless.
|
||||
cancelFade(el); // supersede any fade already running on this element
|
||||
const from = el.volume, steps = Math.max(1, Math.round(ms / 30));
|
||||
let i = 0;
|
||||
const iv = setInterval(() => {
|
||||
i += 1;
|
||||
const k = Math.min(1, i / steps);
|
||||
el.volume = Math.min(1, Math.max(0, from + (to - from) * k));
|
||||
if (k >= 1) { clearInterval(iv); if (done) done(); }
|
||||
if (k >= 1) { clearInterval(iv); el._fadeIv = null; if (done) done(); }
|
||||
}, 30);
|
||||
el._fadeIv = iv;
|
||||
}
|
||||
|
||||
// Diagnostic seam (sibling of window.__hefState): expose the fade so E2E can verify a
|
||||
// new fade supersedes a stale one rather than racing it.
|
||||
if (typeof window !== "undefined") window.__hefFade = fadeVolume;
|
||||
|
||||
// Fallback per-scale soundtrack paths, used when the ring from /api/ring lacks the
|
||||
// `audio` field (e.g. a server process started before the audio field existed).
|
||||
// Mirrors simulator/build_pool_manifest.py SCALE_AUDIO — the 5 scales + files are
|
||||
@@ -1128,6 +1142,7 @@ function ensurePlaying(el, url, vol) {
|
||||
if (pr) pr.then(() => { audLastErr = ""; updateAudioStatus(); })
|
||||
.catch((e) => { audLastErr = "play BLOCKED: " + ((e && e.name) || e); updateAudioStatus(); });
|
||||
}
|
||||
cancelFade(el); // a direct volume set supersedes any in-flight fade on this element
|
||||
el.volume = HEFScrub.clamp01(vol);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user