Skip to content

fix(audio): module-scope Howl cache survives provider remounts#358

Merged
mokn merged 1 commit intodevfrom
feat/audio-zones
Apr 13, 2026
Merged

fix(audio): module-scope Howl cache survives provider remounts#358
mokn merged 1 commit intodevfrom
feat/audio-zones

Conversation

@mokn
Copy link
Copy Markdown
Collaborator

@mokn mokn commented Apr 13, 2026

Summary

  • Hoist ambientHowls, battleHowls, activeTrack, and missingZoneWarned to module scope so they survive React provider remounts
  • Root cause: MUDProvider swaps component types when setupPromise resolves (MUDContext.ProviderMUDProviderInner), which unmounts the entire SoundProvider subtree. The old ref-based cache was destroyed mid-load, producing "Decoding audio data failed" on the first Howl and leaving the second mount silent despite Howler reporting playing
  • Strip the diagnostic console.logs shipped in chore(audio): diagnostic logging for silent ambient bug #357 now that the root cause is identified
  • Add __resetSoundForTests for the test suite to reset module state between runs

Why the old fix didn't work

PR #356 resumed the audio context synchronously inside toggleSound and registered unlock listeners unconditionally. That part was correct — the logs from #357 confirmed ctxState: 'running' on the second mount and Howl playing firing. But the Howl being "played" was a fresh instance from the second mount, racing against the first mount's unload cleanup. The shared Web Audio state ended up in a silent mode even though Howler's state machine said otherwise.

Module-scope cache means the second mount sees keyEq(activeTrack, desired) === true and early-returns without touching the live Howl at all.

Test plan

  • pnpm --filter client test src/contexts/SoundContext.test.tsx — 23/23 passing
  • pnpm --filter client build — clean
  • Hard refresh in Dark Cave on beta → ambient music plays within ~2s
  • Toggle sound off → on → ambient resumes
  • Start combat → fast crossfade to battle track
  • Back-to-back kills within 5s → no music whiplash
  • Kill + wait 6s → slow fade back to ambient
  • Mute mid-battle → unmute → battle track resumes
  • Zone 1 → Zone 2 mid-fight → battle track swaps

🤖 Generated with Claude Code

…unts

MUDProvider swaps its internal component type when setupPromise resolves
(MUDContext.Provider → MUDProviderInner), which unmounts the entire
SoundProvider subtree. The previous ref-based Howl cache was destroyed
mid-load, producing "Decoding audio data failed" on the first Howl and
leaving the second mount's Howl in a silent state even though Howler
reported playback.

Moving the cache, activeTrack, and missingZoneWarned set to module scope
means the second mount sees the Howl already playing and no-ops via the
keyEq early return. Also strips the diagnostic console.logs now that the
root cause is identified, and adds __resetSoundForTests so the test suite
can reset module state between runs.

All 23 SoundContext tests pass against the new implementation.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
blog Building Building Preview, Comment Apr 13, 2026 9:51pm
ud Building Building Preview, Comment Apr 13, 2026 9:51pm
ud-api Building Building Preview, Comment Apr 13, 2026 9:51pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ud-api-beta Ignored Ignored Preview Apr 13, 2026 9:51pm

Request Review

@mokn mokn merged commit 2b20c5e into dev Apr 13, 2026
2 of 6 checks passed
@mokn mokn deleted the feat/audio-zones branch April 13, 2026 21:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant