Audio
The audio system is built on top of Three.js Audio and provides three layers:
- GlobalAudio — master volume control, shared
AudioListener - GlobalAudioManager — load, configure, and destroy
Audio/PositionalAudioinstances - AudioBackground — playlist-based background music with auto-advance, loop, and chaining
GlobalAudio is created automatically by World and exposed as world.audio. The manager and background player are built on top of it.
🔇 Browser autoplay policy
Browsers block audio playback until the user has interacted with the page (click, tap, key press). You must start playback from within a user gesture handler:
canvas.addEventListener("click", async() => {
await audioBackground.play("ambient.forest");
});NOTE
This is a browser restriction, not an engine limitation. See Chrome Autoplay Policy.
GlobalAudio
Master volume controller. Wraps a Three.js AudioListener and emits volumechange when the volume changes. Every sound created with the world listener goes through the listener gain, so the master volume applies to it without any extra wiring.
The listener, and the browser AudioContext behind it, is created the first time listener, threeAudioListener or volume is used, so a world that never plays sound opens no audio context.
type GlobalAudioEvents = {
volumechange: [volume: number];
};interface GlobalAudio {
readonly listener: THREE.AudioListener;
/** Same object as `listener`. */
readonly threeAudioListener: THREE.AudioListener;
/** Master volume, clamped to [0, 1]. */
volume: number;
}GlobalAudioManager
Loads audio files, configures volume/loop, and manages cleanup.
interface AudioLoadingOptions {
name?: string;
// default false
loop?: boolean;
// default 1
volume?: number;
}interface AudioManager {
// Async — fetch + decode from a URL at runtime
loadAudio(url: string, options?: AudioLoadingOptions): Promise<THREE.Audio>;
loadPositionalAudio(url: string, options?: AudioLoadingOptions): Promise<THREE.PositionalAudio>;
// Sync — construct from a buffer already prepared by the runtime
createAudio(buffer: AudioBuffer, options?: AudioLoadingOptions): THREE.Audio;
createPositionalAudio(buffer: AudioBuffer, options?: AudioLoadingOptions): THREE.PositionalAudio;
destroyAudio(audio: THREE.Audio | THREE.PositionalAudio): void;
}Construction
interface GlobalAudioManagerOptions {
/** @default new THREE.AudioListener() */
listener?: THREE.AudioListener;
/** @default THREE.AudioLoader */
loadBuffer?: (url: string) => Promise<AudioBuffer>;
}
GlobalAudioManager.fromWorld(world: World): GlobalAudioManager;fromWorld binds the manager to the world's AudioListener, so the world master volume applies to every sound it creates. A manager built with its own listener is independent from world.audio.volume. volume in AudioLoadingOptions is the per-sound gain.
The browser runtime registers AudioAssetLoader (exported with AUDIO_ASSET) by default.
Async loading (loadAudio / loadPositionalAudio)
Fetches and decodes a URL on demand. Useful for audio that is loaded dynamically at runtime (e.g. user-triggered sound effects loaded after the loading screen). Each URL is decoded once per manager; a failed load is retried on the next call.
const audio = await audioManager.loadAudio("sounds/click.mp3", { volume: 0.5 });
audio.play();Sync creation from pre-loaded buffers (createAudio / createPositionalAudio)
When buffers have been prepared through the runtime (e.g. via AudioLibrary), these methods construct a ready-to-play THREE.Audio or THREE.PositionalAudio synchronously — no await needed in lifecycle methods.
// In Behavior.start():
this.#shootAudio = audioManager.createAudio(sfx.get("shoot"), { volume: 0.8 });
this.#musicAudio = audioManager.createAudio(sfx.get("music"), { loop: true, volume: 0.5 });
this.actor.add(this.#shootAudio);For 3D-positioned sound, use createPositionalAudio and add the result to an Actor:
this.#footsteps = audioManager.createPositionalAudio(sfx.get("footstep"), { loop: true });
this.actor.add(this.#footsteps);