Repository navigation
feat(audio)!: replace the Howler wrapper with a Web Audio sound mixer - #716
Open
stormmuller wants to merge 7 commits into
Open
stormmuller wants to merge 7 commits into
stormmuller wants to merge 7 commits into
Conversation
Implements Phase 1 of design/audio-mixer.md. Adds createSoundMixer with nestable buses (volume, mute), gesture unlocking and teardown; SoundAssetCache and createSoundAsset; entity-free playSound; and SoundEcsComponent with createSoundEcsSystem, which applies live changes, reports hasFinished and stops sounds whose entity or component is removed. Removes AudioEcsComponent, createAudioEcsSystem and the howler peer dependency, and migrates the space-shooter demo, the audio guides and the docs site's useGame teardown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GR7HgBEY8CatWd78XsJkag
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GR7HgBEY8CatWd78XsJkag
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GR7HgBEY8CatWd78XsJkag
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GR7HgBEY8CatWd78XsJkag
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GR7HgBEY8CatWd78XsJkag
Removes design/audio-mixer.md and marks the sound mixer finding as implemented in design/demo-findings.md, following #720's convention of deleting design documents once they ship. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GR7HgBEY8CatWd78XsJkag
…stances Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GR7HgBEY8CatWd78XsJkag
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements Phase 1 of the sound mixer design (
design/audio-mixer.md, tasks 1.1-1.11). The design doc is removed in this PR now that it has shipped, following the convention from #720, and its finding is marked implemented indesign/demo-findings.md.Forge's audio module was a thin wrapper around Howler. It had no buses, no volume or mute per group, no way to play a sound without an entity, and no report of when a sound ends. Removing an entity didn't stop its sound, and stopping the world unloaded shared
Howls. This PR replaces it with a mixer built directly on Web Audio.New API (
@forge-game-engine/forge/audio)createSoundMixer(context?): owns theAudioContextand amasterbus. It also provides:createBus(name, parent?)andgetBusstatesuspend/resumestop(), which stops every sound, closes the context and removes its listenerspointerup/touchend/click/keydown(Escape ignored) until the context reports'running'. It listens again if audio drops to suspended or interrupted without the game asking.MixerBus:volume(linear) andmuted. Changes ramp withsetTargetAtTime, so they don't click. Buses nest.SoundAsset,createSoundAsset({ sampleRate, channels }), andSoundAssetCache(anAssetCache). Concurrent loads of one URL share a single fetch and decode. HTTP and decode failures reject with an error naming the URL.playSound(bus, sound, { volume, rate, loop })returns aPlayingSoundwithvolume,isPlayingandstop(). Stopping fades out with a linear ramp before the source stops.SoundEcsComponent(addSoundComponent,soundId) andcreateSoundEcsSystem(). The system runs every tick and:volume,rate,loopandbuschanges live, and restarts whensoundchanges;hasFinishedon natural end (written only by the system;addSoundComponentdoesn't accept it);cleanup.Removed:
AudioEcsComponent,audioId,addAudioComponent,createAudioEcsSystem, andhowler/@types/howlerfrom bothpackage.jsons and lockfiles.Docs site
SoundEcsComponenton the music bus, and laser and explosion areplaySoundcalls on the sfx bus. The explosion's second entity with its guessed 6-second lifetime is gone, and so is theHowlcreated per gun system.useGameteardown:useGame/Demonow passcreateGameastopWithGame(resource)callback, so a demo's mixer is stopped after its game on unmount, including when it unmounts mid-creation. Every other demo is unchanged, since they ignore the argument.ecs/system.md(itscleanupexample was the old two-writer audio system),asset-loading/index.mdandui/controls.md.Other: README, AGENTS.md (audio line, peer deps,
test-helpers/convention, the e2e audio-activation gotcha) and thecreate-componentskill.Where this differs from the design draft
'running', for example after client-side navigation within the docs site. There, the literal §5.4 rule would drop every sound effect until the next click. The burst of stale sounds that DL-5 avoids can only happen while the context can't play.hasFinishedisn'treadonlyin the type, because the system has to write it without a cast.addSoundComponentdoesn't accept it, so game code can't set it.busto one from another mixer throws, because Web Audio can't connect nodes across contexts.hasFinished, instead of retrying every tick.visibilitychangesnippet).solution-reviewer verdict
REVISE, with six points, all adopted before implementation:
MixerBus.output. It would have exposed the graph to a second writer. The e2e test meters through an injectedAudioContextsubclass instead.useGame. It uses one callback signature instead ofGame | { game, mixer }.page.evaluateruns as a user gesture, and trace recording snapshots the page. So the spec turns tracing off and waits for a console signal from the scene before evaluating anything. With tracing on, the context started'running'.ecs/system.md, the skill,asset-loading/index.md,ui/controls.mdand AGENTS.md.Reviewer suggestion not taken in this PR: the space shooter's
KeyboardInputSourceis never stopped either. That's a separate fix.Testing
AudioContextinsrc/audio/test-helpers/, excluded from the build and coverage. They cover graph wiring, gains, gesture rule and re-arming,stop, the cache's shared loads and errors,createSoundAssetvalidation, and the component reconciliation (pause position, rate-change tracking, loop wrap, bus/sound/loop changes, finish, removal, mixer stop).audio-mixer.spec.ts):'suspended'and a one-shot is dropped.npm run buildfrom the root, thentypecheckandbuildindocumentation-site/./demos/space-shooterin Chromium: it renders, music starts, shooting plays a laser per volley, no page errors, and theAudioContextisclosedafter navigating to another page.All of the above re-run after each merge of
devinto this branch, most recently with the storage, states, physics, render-target and text changes.Related issue(s)
Implements
design/audio-mixer.md(finding 2 indesign/demo-findings.md). The Galactic Journey game repo uses the published package, so it can migrate once this is released.Verification checklist
npm run check-typespasses with 0 errorsnpm testpasses (2160 tests after the latest merge ofdev)npm run lintpasses with 0 errors (2 existing TODO warnings)npm run cspellpasses with 0 errorsnpm run check-exportspassesindex.ts/documentation-site/docs/docsis updatedChangelog
## [Unreleased]inCHANGELOG.md(#### Addedand#### Removed, with migration steps)🤖 Generated with Claude Code
https://claude.ai/code/session_01GR7HgBEY8CatWd78XsJkag