arcade2d
Class

AudioEngine

audio/audio-engine.ts:73

Game-tier root of the audio graph: owns the Web Audio AudioContext, the master and per-category gain buses, and the seam used by AssetLibrary to decode AudioAssets and by the audio components (AudioSource, Music) to construct playback voices.

AudioEngine is auto-attached to every Game and reached through Game.audio. It lives at the game tier, not the world tier, because the audio context outlives any individual world: ramping a volume slider in the pause menu, or letting a music track continue across a world transition, both depend on a single long-lived context.

Graph layout

     [BufferSource] → [Gain] → [Panner] ─┐                  ┌─→ master ─→ destination
                                         ├─→ music gain ────┤
     [BufferSource] → [Gain] → [Panner] ─┘                  │
     [BufferSource] → [Gain] → [Panner] ─┐                  │
                                         ├─→ sfx gain ──────┘
     [BufferSource] → [Gain] → [Panner] ─┘

Per-instance gain and panner nodes are managed by AudioInstance; this class owns the three category buses and the connection from each bus up to context.destination. Adjusting AudioEngine.masterVolume, AudioEngine.musicVolume, or AudioEngine.sfxVolume ramps the corresponding GainNode instantly and affects every instance routed through it.

Headless mode

If the runtime has no AudioContext constructor — node, jsdom, an audio: { disabled: true } opt-out — the engine enters headless mode. In headless mode, AudioEngine.available returns false, the gain nodes are null, and every AudioInstance this engine produces is an inert no-op. This is the same model the Mouse and Keyboard samplers use to keep tests and SSR builds compiling and running without the page-scoped browser globals their production paths need.

Autoplay policy

Browsers suspend a freshly-created AudioContext until the page has received a user gesture. AudioEngine.resume is a thin wrapper over context.resume() for callers that want to hook the first click / key press explicitly; the engine does not silently resume the context on its own, because doing so on every play() would mask legitimate suspension cases (a user-paused tab).

Constructors

#
constructor(host: Game, options: AudioEngineOptions): AudioEngine

Parameters

Properties

#
enabled: boolean

Per-component gate on the three update hooks (onPreUpdate, onUpdate, onPostUpdate). When explicitly false, the engine skips all three for this component during the host's tick — useful for temporarily pausing behaviour (e.g. a freeze powerup) without removing the component and losing its internal state.

Does not gate onAdded or onDestroy; those always fire so a host can never end up with a half-attached component.

readonly #
host: Game

The host this component is attached to. Stored read-only; subclasses access it as this.host directly, or through the tier-appropriate aliases (this.world, this.game) on the per-tier abstract bases.

Accessors

readonly #
available: boolean

Whether audio is actually available in this runtime — true when a Web Audio context exists, false in headless mode. Branch on this if you want to skip work (e.g. don't even load audio assets) when the environment can't play them anyway.

readonly #
game: Game

The Game this component is attached to — identical to AbstractComponent.host at this tier, exposed under the game name so subclass code reads the same on every tier.

#
masterVolume: number

Master volume from 0 to 1. The single knob at the bottom of the audio graph; every category bus and every instance routes through it. Setting this in headless mode silently retains the value but has no audible effect.

#
musicVolume: number

Music-bus volume from 0 to 1. Multiplied with the master gain at playback time; affects every Music instance and any other playback routed through AudioCategory.Music.

readonly #
raw: null | AudioContext

Direct access to the underlying Web Audio AudioContext, or null in headless mode.

Use with care. raw is an intentional escape hatch for cases the arcade2d API doesn't cover — custom nodes (filters, reverbs, analysers), advanced scheduling against context.currentTime, anything we haven't decided how to model yet. Code that touches raw is coupled to the Web Audio API and may break when arcade2d swaps the audio backend.

None of those will be treated as breaking changes to arcade2d's own surface. Prefer the typed methods on this component; reach for raw only when no equivalent exists, and isolate the access behind your own helper so the coupling is in one place.

#
sfxVolume: number

SFX-bus volume from 0 to 1. Multiplied with the master gain at playback time; affects every AudioSource voice and any other playback routed through AudioCategory.Sfx.

Methods

#
createInstance(buffer: AudioBuffer, category: AudioCategory, options: AudioInstanceOptions): AudioInstance

Constructs a fresh AudioInstance routed through the requested AudioCategory. The instance is created in the idle state — call AudioInstance.play to start playback.

Parameters

Returns

AudioInstance

A fresh AudioInstance.

#
decodeAudioData(bytes: ArrayBuffer): Promise<AudioBuffer>

Decodes an ArrayBuffer of encoded audio bytes into a Web Audio AudioBuffer using the engine's context. This is the seam AssetLibrary uses to decode AudioAssets after fetching the raw bytes — exposed so tests and advanced callers can decode bytes they obtained through other means (e.g. a recorder, a custom loader).

Parameters

  • bytes ArrayBuffer

Returns

Promise<AudioBuffer>

A promise resolving to the decoded buffer.

Throws

EngineError with code ErrorCode.AUDIO_UNAVAILABLE when the engine is in headless mode.

#
loadAudioBuffer(path: string): Promise<AudioBuffer>

Convenience wrapper around fetch + AudioEngine.decodeAudioData: loads the bytes at path and returns the decoded buffer.

This is the path AssetLibrary takes for audio asset loads. Most game code uses AssetLibrary.load (or a bundle) and never reaches for this directly; reach for it when you need a one-off decode outside the asset library's lifecycle model.

Parameters

  • path string

Returns

Promise<AudioBuffer>

The decoded AudioBuffer.

Throws

EngineError with code ErrorCode.AUDIO_UNAVAILABLE when the engine is in headless mode.

Any error fetch or decodeAudioData produces. Callers that want the engine-error wrapping go through AssetLibrary.load.

#
onAdded(_deps: Record): void

Lifecycle hook that is called when the component is added to the host object. Should not be called directly.

Parameters

  • _deps Record

Returns

void
#
onDestroy(): void

Closes the underlying AudioContext and drops the engine's references. Idempotent; safe to call multiple times. Invoked automatically when the Game tears down.

Returns

void
#
onUpdate(_update: WorldUpdate, _deps: Record): void

Lifecycle hook for the main update phase. Called once per world tick, after every component's onPreUpdate and before any onPostUpdate.

Parameters

Returns

void
#
resume(): Promise<void>

Resumes the underlying AudioContext if browser autoplay policy suspended it. Returns a promise that resolves once the context is running (or immediately if it never needed resuming, or if the engine is headless).

Most games hook this to their first user input — a "Click to start" splash, the first key press, the pause-menu close button. Doing it on every play() is wasteful and masks legitimate suspension states.

Returns

Promise<void>
#
suspend(): Promise<void>

Suspends the underlying AudioContext. Use this when the game loses focus or the player pauses — every running source freezes in place and resumes audibly seamless on the next AudioEngine.resume. No-op in headless mode.

Returns

Promise<void>
ESC