From 97d2ddd573180ada336bea0f91966838a2d5f6da Mon Sep 17 00:00:00 2001 From: BenStullsBets Date: Fri, 26 Jun 2026 13:42:12 -0700 Subject: [PATCH] fix(audio): client soundtrack fallback when /api/ring lacks audio (stale server) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of 'no sound', found via the live readout: the operator's /api/ring returned the ring WITHOUT the per-scale audio field (a server process predating the audio code) → soundtrackUrl() was null → the element never got a src (readyState 0, no error). The native test player proved the file itself plays. Fix: client falls back to a scale-id→soundtrack map (mirrors build_pool_manifest.SCALE_AUDIO) so audio works without a server restart. Verified: stale-ring sim now plays; audio-first-then- video leaves both on. +2 E2E regression tests (7 total). Chromium + WebKit clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- simulator/static/app.js | 19 +++++++++++++++++-- tests/test_e2e_audio.py | 20 ++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/simulator/static/app.js b/simulator/static/app.js index 92bee4f..a0a5096 100644 --- a/simulator/static/app.js +++ b/simulator/static/app.js @@ -960,10 +960,25 @@ function fadeVolume(el, to, ms, done) { }, 30); } -// The current altitude's soundtrack url (the ring carries each scale's `audio`). +// 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 +// fixed, committed facts, so this keeps audio working without a server restart. +const SCALE_AUDIO_FALLBACK = { + cosmos: "cosmos/pillars.loop.mp3", + orbit: "orbit/spaceamb.loop.mp3", + coast: "coast/waves.loop.mp3", + reef: "reef/soundscape.loop.mp3", + abyss: "abyss/whale.loop.mp3", +}; + +// The current altitude's soundtrack url: the ring's `audio` field, else the +// scale-id fallback above. function soundtrackUrl() { const s = ring && ring.scales[ringIndex]; - return s && s.audio ? "/media/audio/" + s.audio : null; + if (!s) return null; + const a = s.audio || SCALE_AUDIO_FALLBACK[s.id]; + return a ? "/media/audio/" + a : null; } // Load `url` into the single element and fade it in. Call this SYNCHRONOUSLY from diff --git a/tests/test_e2e_audio.py b/tests/test_e2e_audio.py index 1fd86de..2111e89 100644 --- a/tests/test_e2e_audio.py +++ b/tests/test_e2e_audio.py @@ -101,6 +101,26 @@ def test_audio_before_video_stays_audio_only(page): page.wait_for_selector("#black:not(.hidden)") # still blanked +def test_audio_then_video_leaves_both_on(page): + _toggle(page, "audio") # audio first + page.wait_for_function("!document.getElementById('aud').paused") + _toggle(page, "visual") # then video + page.wait_for_selector("#black.hidden", state="attached") # video showing + assert page.is_checked("#audio") is True + assert page.evaluate("!document.getElementById('aud').paused") is True # audio still plays + + +def test_soundtrack_fallback_when_ring_lacks_audio(page): + # simulate a stale server whose /api/ring omits the per-scale `audio` field; + # the client falls back to the scale-id soundtrack map so audio still plays + page.evaluate("ring.scales.forEach(s => { delete s.audio; })") + _toggle(page, "audio") + page.wait_for_function( + "(() => { const a = document.getElementById('aud');" + " return a.src.includes('/media/audio/cosmos/') && !a.paused; })()" + ) + + def test_video_off_blanks_after_being_on(page): _toggle(page, "visual") # on page.wait_for_selector("#black.hidden", state="attached")