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:
BenStullsBets
2026-06-27 17:07:52 -07:00
parent e862fb498b
commit 4b1d3afe7b
2 changed files with 33 additions and 1 deletions
+17
View File
@@ -55,6 +55,23 @@ test("two audio elements exist for crossfade", async ({ page }) => {
expect(n).toBe(2);
});
test("a new volume fade supersedes a stale one (audio not silenced after landing)", async ({ page }) => {
await boot(page);
// Reproduce the race: a fade-to-0 in flight, then the same element is brought back
// up (as happens when a faded idle element becomes the newly-landed active scale).
// The stale fade must NOT drag it back to 0.
const vol = await page.evaluate(async () => {
const a = document.querySelector("#aud") as HTMLAudioElement;
a.volume = 1;
(window as any).__hefFade(a, 0, 600); // start a slow fade to 0
await new Promise((r) => setTimeout(r, 120)); // let it ramp partway down
(window as any).__hefFade(a, 0.8, 90); // new fade up — must cancel the old one
await new Promise((r) => setTimeout(r, 350)); // well past both fades
return a.volume;
});
expect(vol).toBeGreaterThan(0.7); // ended near 0.8, not dragged to 0
});
test("audio is a 0-10 level dial, defaulting to 0", async ({ page }) => {
await boot(page);
const ctl = await page.evaluate(() => {