Audio
The @web-engine-dev/audio package provides a full game audio pipeline: sound effects, background music, 2D and 3D spatial audio, and a bus mixing hierarchy.
Setup
AudioManager is an OOP class, not an ECS resource. Create it once during initialization and call update(dt) each frame:
typescript
import { AudioManager } from '@web-engine-dev/audio';
const audioManager = new AudioManager();
await audioManager.initialize();
// In your game loop - must be called every frame
function tick(dt: number): void {
world.runSchedule(CoreSchedule.Update, dt);
audioManager.update(dt);
requestAnimationFrame(tick);
}Audio Buses (Mixing)
Set up bus hierarchy before playing any sounds:
Master
├── Music (0.6 volume)
├── SFX (1.0 volume)
│ ├── UI
│ └── World
└── Ambient (0.4 volume)typescript
// createBus(id, parentId?) returns a bus object
const masterBus = audioManager.createBus('master');
const musicBus = audioManager.createBus('music', 'master');
const sfxBus = audioManager.createBus('sfx', 'master');
const uiBus = audioManager.createBus('ui', 'sfx');
const worldBus = audioManager.createBus('world', 'sfx');
const ambientBus = audioManager.createBus('ambient', 'master');
// Set initial levels
musicBus.setVolume(0.6);
ambientBus.setVolume(0.4);
// Apply effects to a bus (reverb, lowpass, etc.)
worldBus.addEffect({ type: 'reverb', roomSize: 0.3, damping: 0.5, wet: 0.2 });At runtime (e.g. settings screen):
typescript
const music = audioManager.getBus('music');
music.setVolume(0.4);
const ambient = audioManager.getBus('ambient');
ambient.mute();
ambient.unmute();Playing Sound Effects
typescript
// One-shot sound: fire and forget
audioManager.play('sfx/explosion.wav');
// With options
audioManager.play('sfx/explosion.wav', {
volume: 0.8,
pitch: 0.9 + Math.random() * 0.2, // slight pitch randomization
bus: 'sfx',
});
// Looping ambient sound
const handle = audioManager.play('ambient/wind.ogg', {
loop: true,
volume: 0.3,
bus: 'ambient',
});
// Stop later
audioManager.stop(handle);Background Music
Music is played through the same AudioManager on the music bus:
typescript
// Start a looping music track
const musicHandle = audioManager.play('music/level-1-theme.ogg', {
loop: true,
volume: 0.6,
bus: 'music',
});
// Pause / resume
audioManager.pause(musicHandle);
audioManager.resume(musicHandle);
// Stop
audioManager.stop(musicHandle);Spatial (3D) Audio
Set the listener position each frame (usually the camera or player):
typescript
// Update listener from your camera transform
audioManager.listener.setFromTransform(cameraPosition, cameraForward, cameraUp);Play a sound at a world position:
typescript
audioManager.playAtPosition(
'ambient/waterfall.ogg',
{ x: 200, y: 0, z: 300 },
{
loop: true,
volume: 1.0,
bus: 'ambient',
rolloffModel: 'inverse', // 'inverse' | 'linear' | 'exponential'
refDistance: 100,
maxDistance: 400,
}
);
// Enemy growl emanating from entity position
const pos = world.get(enemyEntity, Position);
audioManager.playAtPosition('enemy/growl.ogg', pos, {
refDistance: 50,
maxDistance: 200,
bus: 'world',
});Decoupled Sound Events
Rather than calling audioManager directly from game logic, emit events for loose coupling:
typescript
import { defineEvent } from '@web-engine-dev/events';
import type { Vec3 } from '@web-engine-dev/math';
export const PlaySoundEvent = defineEvent<{
clip: string;
volume?: number;
pitch?: number;
position?: Vec3;
bus?: string;
}>('PlaySound');
// Fire from any system (no AudioManager import needed)
world.eventWriter(PlaySoundEvent).send({ clip: 'sfx/jump.wav', volume: 0.9 });
world.eventWriter(PlaySoundEvent).send({ clip: 'sfx/explosion.wav', position: blastPos });
// One dedicated system reads events and drives AudioManager
function AudioEventSystem(world: World): void {
for (const event of world.eventReader(PlaySoundEvent).read()) {
if (event.position) {
audioManager.playAtPosition(event.clip, event.position, {
volume: event.volume ?? 1,
bus: event.bus ?? 'sfx',
});
} else {
audioManager.play(event.clip, {
volume: event.volume ?? 1,
pitch: event.pitch ?? 1,
bus: event.bus ?? 'sfx',
});
}
}
}Volume Settings
Store user preferences externally and apply them to buses:
typescript
interface AudioPreferences {
masterVolume: number;
musicVolume: number;
sfxVolume: number;
ambientVolume: number;
}
function applyAudioPreferences(prefs: AudioPreferences): void {
audioManager.getBus('master').setVolume(prefs.masterVolume);
audioManager.getBus('music').setVolume(prefs.musicVolume);
audioManager.getBus('sfx').setVolume(prefs.sfxVolume);
audioManager.getBus('ambient').setVolume(prefs.ambientVolume);
}
// Load from localStorage
const saved = localStorage.getItem('audioPrefs');
if (saved) applyAudioPreferences(JSON.parse(saved));Next Steps
- UI & HUD, connect audio settings to an in-game menu
- Visual Effects, sync particle effects with audio triggers
- Animation, trigger sounds from animation state transitions