Skip to content

fix(sound): unlock WebKit audio so sound works on iOS (fixes #2530) - #3214

Open
woahwhattheheck wants to merge 1 commit into
FreezingMoon:masterfrom
woahwhattheheck:fix/2530-ios-audio-unlock
Open

fix(sound): unlock WebKit audio so sound works on iOS (fixes #2530)#3214
woahwhattheheck wants to merge 1 commit into
FreezingMoon:masterfrom
woahwhattheheck:fix/2530-ios-audio-unlock

Conversation

@woahwhattheheck

@woahwhattheheck woahwhattheheck commented Sep 6, 2026

Copy link
Copy Markdown

Fixes #2530

I don't own an iPhone either, so this is from reading the audio path rather than from a device. Three separate things in it are broken on WebKit, and every browser on iOS is WebKit, so they all apply to Chrome on iPhone as well as Safari.

1. The AudioContext is never resumed

SoundSys constructs its AudioContext in the constructor, while the page is still loading. WebKit hands back a context in the suspended state unless it was created inside a user gesture, and only leaves that state when something calls resume() from inside one. Nothing in the codebase ever called it, so every SFX, heartbeat and shout is silent for the whole session — the buffers decode and source.start(0) runs, but a suspended context produces no output.

2. AudioContext is read as a bare identifier

this.context = new (AudioContext || window.webkitAudioContext)();

envHasSound correctly checks 'AudioContext' in window || 'webkitAudioContext' in window, but the constructor then references AudioContext unqualified. On a Safari build that only ships the prefixed constructor there is no global binding by that name, so this throws ReferenceError instead of evaluating to undefined and falling through to the fallback — taking the whole sound system down on exactly the browsers the fallback exists for.

3. A blocked autoplay was never retried

MusicPlayer.run() caught the rejection, logged it and gave up. When setAudioMode('full') triggers playMusic() outside a gesture, WebKit rejects with NotAllowedError and the track then never starts.

What this changes

  • New src/sound/audio-context.ts with two small helpers: resolveAudioContextCtor() resolves the constructor by property lookup, and unlockAudioContextOnGesture() resumes a suspended context on the first pointerdown/touchend/keydown, then unhooks. If the resume is still refused the listeners stay armed for the next gesture.
  • soundsys.ts uses both, and derives envHasSound from the same resolution so the check and the construction can't disagree.
  • musicplayer.js retries once on the next gesture when playback is rejected with NotAllowedError. The pending retry is cancelled by stopMusic() and when a new track starts, so nothing can start playing after the user has muted or switched.

Behaviour on browsers that already worked is unchanged: a context that is already running gets no listeners attached, and resolveAudioContextCtor() returns the same unprefixed constructor it used before.

Scope — this does not by itself restore music on iOS

Every audio asset in the repo is .ogg (81 files; no mp3/m4a/aac). WebKit does not decode Ogg Vorbis, so even with the code fixed the music tracks and the buffered SFX still need a non-Ogg encoding to play on iOS. That's an asset/pipeline change rather than a code change and I've deliberately left it out of this PR. Please don't read this as "music works on iPhone now" — it removes the code-side blockers, and I'd be glad to do the asset side as a follow-up if you want to decide on a format and where the files should live.

Tests

src/__tests__/sound/audio-context.test.ts covers constructor resolution when only the prefixed global exists (the ReferenceError case), the null case, no listeners when already running, resume on first gesture then unhook, staying armed when the resume is refused, and the returned canceller.

Beyond that, whatever CI runs on this PR — I can't run the suite locally. Note the workflow will likely sit at action_required until a maintainer approves it, same as my other PR.

Not posting a wallet address here; happy to sort that out separately if this lands.

Safari and every browser on iOS create an AudioContext in the suspended
state unless it is constructed inside a user gesture, and only leave that
state when something calls resume() from inside one. SoundSys builds its
context while the page is loading and nothing ever resumed it, so effects
stayed silent for the whole session there.

SoundSys also read AudioContext as a bare identifier before falling back to
webkitAudioContext. On Safari builds that only ship the prefixed constructor
there is no such global binding, so that line threw a ReferenceError and took
the whole sound system down instead of using the fallback.

MusicPlayer logged the autoplay rejection and gave up, so a track blocked for
lack of a user gesture never started.

Resolve the constructor by property lookup, resume the context on the first
gesture, and retry blocked playback on the next interaction.

fixes FreezingMoon#2530
@vercel

vercel Bot commented Sep 6, 2026

Copy link
Copy Markdown

@woahwhattheheck is attempting to deploy a commit to the FreezingMoon Team on Vercel.

A member of the Team first needs to authorize it.

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.

no music on iPhone [bounty: 7 XTR]

1 participant