Skip to content

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

Proprietary software. All rights reserved.