Skip to content

Audio System

The @web-engine-dev/audio package provides a game-oriented audio engine built on the Web Audio API. It handles the concerns that game audio requires beyond basic playback: hierarchical bus mixing, 3D spatial positioning, voice management with polyphony limits, sound pooling, audio occlusion, and an adaptive music system with beat-synchronized transitions.

Architecture Overview

AudioManager (main controller)
├── AudioListener (player's ears in 3D space)
├── AudioBus hierarchy (mixing + effects)
│   ├── master
│   ├── sfx → master
│   ├── music → master
│   └── voice → master
├── AudioClip registry (loaded audio data)
├── AudioSource pool (active playback)
├── SoundPool (frequent sounds with LRU eviction)
├── VoiceManager (allocation + virtualization)
├── AudioNodePool (Web Audio node reuse)
├── AudioOcclusionSystem (line-of-sight occlusion)
└── MusicSystem (adaptive layered music)

Basic Setup

typescript
import { AudioManager } from '@web-engine-dev/audio';

const audioManager = new AudioManager();
await audioManager.initialize({
  masterVolume: 1.0,
  maxSources: 32,
  defaultDistanceModel: 'inverse',
  defaultPanningModel: 'HRTF',
  spatialAudioEnabled: true,
});

// Load audio clips
await audioManager.loadClip('explosion', '/sounds/explosion.mp3');
await audioManager.loadClip('footstep', '/sounds/footstep.wav');
await audioManager.loadClip('theme', '/music/theme.mp3');

// Play a sound
audioManager.play('explosion', { volume: 0.8, bus: 'sfx' });

// Update each frame (required for spatial audio, voice management, etc.)
function gameLoop(deltaTime: number) {
  audioManager.update(deltaTime);
}

Web Audio Context

Browsers require user interaction before audio can play. Call audioManager.resume() in response to a click or key press:

typescript
document.addEventListener('click', async () => {
  await audioManager.resume();
}, { once: true });

Audio Buses

Buses form a hierarchical mixing graph. Each bus can have its own volume, effects, and mute state. Audio routed to a child bus flows through its parent, enabling group-level control.

typescript
// Create bus hierarchy
const sfxBus = audioManager.createBus('sfx', 'master');
const musicBus = audioManager.createBus('music', 'master');
const ambientBus = audioManager.createBus('sfx-ambient', 'sfx');

// Control bus volume
sfxBus.setVolume(0.8);
musicBus.mute();    // Mute all music
musicBus.unmute();   // Restore

// Add effects to buses
sfxBus.addEffect({ type: 'reverb', roomSize: 0.3, wet: 0.2 });
sfxBus.addEffect({ type: 'compressor', threshold: -24, ratio: 4 });

Effect Types

EffectDescription
gainVolume control
lowpassLow-frequency filter
highpassHigh-frequency filter
bandpassBand filter
reverbRoom simulation
delayEcho effect
compressorDynamic range compression
distortionOverdrive
chorusModulation effect
flangerSweep effect
equalizerMulti-band EQ

Playback Options

Every play() call accepts a PlaybackOptions object:

typescript
audioManager.play('explosion', {
  loop: false,
  volume: 0.8,
  pitch: 1.0,
  startTime: 0,
  fadeInDuration: 0.5,
  priority: 'high',
  bus: 'sfx',
});

Priority levels: lowest, low, normal, high, highest, critical. When the voice pool is full, lower-priority sounds are stolen first.

Spatial Audio

The audio system supports full 3D spatial audio with distance attenuation and directional panning.

Listener

The listener represents the player's "ears" -- typically positioned at the camera:

typescript
audioManager.listener.setFromTransform(
  cameraPosition,   // Vec3Like: { x, y, z }
  cameraForward,    // Vec3Like: forward direction
  cameraUp          // Vec3Like: up direction
);

Playing Spatial Sounds

typescript
audioManager.playAtPosition('explosion', { x: 10, y: 0, z: 5 }, {
  distanceModel: 'inverse',
  panningModel: 'HRTF',
  refDistance: 1,
  maxDistance: 100,
  rolloffFactor: 1,
});

Distance Models

ModelFormulaBehavior
linearLinear falloff from refDistance to maxDistanceVolume drops linearly
inverserefDistance / (refDistance + rolloff * (dist - refDistance))Realistic falloff (default)
exponential(dist / refDistance) ^ -rolloffAggressive falloff

Panning Models

ModelDescription
equalpowerSimple stereo panning
HRTFHead-Related Transfer Function for realistic spatial positioning

Audio Occlusion

The AudioOcclusionSystem provides line-of-sight occlusion using simple geometric primitives. When a wall or object blocks the direct path between the listener and a sound source, the sound is attenuated and low-pass filtered.

typescript
import { AudioOcclusionSystem } from '@web-engine-dev/audio';

const occlusion = new AudioOcclusionSystem({
  maxOcclusion: 0.9,
  maxObstruction: 0.8,
});

// Add occluder shapes
occlusion.addOccluder({
  id: 'wall-1',
  shape: 'aabb',
  bounds: { minX: 0, minY: 0, minZ: 0, maxX: 10, maxY: 3, maxZ: 0.5 },
  occlusion: 0.7,
  obstruction: 0.3,
});

occlusion.addOccluder({
  id: 'pillar',
  shape: 'sphere',
  center: { x: 5, y: 1.5, z: 3 },
  radius: 0.5,
  occlusion: 0.5,
});

// Update each frame
occlusion.update(listenerPosition, sources);

// Query occlusion
const blocked = occlusion.isOccluded(listenerPos, sourcePos);

Occluder shapes: aabb (axis-aligned bounding box) and sphere.

Sound Pools

For frequently played sounds (footsteps, gunshots), sound pools pre-allocate voices and handle voice stealing automatically:

typescript
const footstepPool = audioManager.createSoundPool('footstep', 8);

// Pre-warm for reduced first-play latency
footstepPool.warmUp(4);

// Configure voice stealing
footstepPool.setStealingPolicy('combined');

// Play from pool
footstepPool.play({
  volume: 0.5,
  pitch: 0.9 + Math.random() * 0.2,  // Slight variation
  position: characterPosition,
  priority: 'normal',
});

// Pool statistics
const stats = footstepPool.getStats();
console.log(`Plays: ${stats.totalPlays}, Steals: ${stats.stealCount}`);

Voice Stealing Policies

PolicyBehavior
oldestSteal the oldest playing voice
newestSteal the most recently started voice
lowest-prioritySteal the lowest priority voice
least-audibleSteal based on distance and volume
combinedWeighted score of priority + audibility (default)

Voice Management

The VoiceManager handles global voice allocation and virtualization. When the maximum voice count is reached, less important sounds are virtualized (tracked but not rendered to audio hardware).

typescript
const voiceManager = audioManager.voiceManager;
voiceManager.setMaxVoices(64);
voiceManager.setStealingPolicy('combined');

// Per-bus voice limits
voiceManager.setBusVoiceLimit('sfx', 16);
voiceManager.setBusVoiceLimit('music', 4);

// Spatial culling -- automatically stop sounds beyond max distance
voiceManager.setSpatialCulling({
  enabled: true,
  cullMultiplier: 1.5,   // Cull at 1.5x maxDistance
  hysteresis: 0.1,       // Prevent thrashing at boundary
  updateInterval: 100,    // Check every 100ms
});

Voice states: real (playing through audio hardware), virtual (tracked but silent), stopped.

Adaptive Music System

The music system supports layered tracks with beat-synchronized transitions, enabling dynamic soundtracks that respond to game state.

Defining Tracks

typescript
const music = audioManager.getMusicSystem();

const combatTrack = {
  id: 'combat',
  name: 'Combat Theme',
  bpm: 140,
  beatsPerBar: 4,
  layers: [
    { name: 'drums', clip: drumsClip, volume: 1.0, enabled: true },
    { name: 'bass', clip: bassClip, volume: 0.8, enabled: true },
    { name: 'tension', clip: tensionClip, volume: 0.7, enabled: false },
  ],
  loopStart: 0,
  loopEnd: 32.0,
};

await music.loadTrack(combatTrack);
music.play('combat', { type: 'fade', duration: 2.0 });

Dynamic Layer Control

typescript
// Enable/disable layers based on game state
music.enableLayer('tension', { type: 'fade', duration: 0.5 });
music.setLayerVolume('drums', 0.9);
music.disableLayer('bass', { type: 'fade', duration: 1.0 });

Transitions

typescript
music.transitionTo('exploration', {
  type: 'onBar',       // Wait for the next bar boundary
  duration: 4.0,
});
TransitionDescription
immediateInstant switch
fadeFade out then fade in
crossfadeSimultaneous fade (overlap)
onBeatSync transition to beat boundary
onBarSync transition to bar boundary
onMarkerSync to a named marker in the track

Adaptive Triggers

Define game-state-driven music changes that respond automatically to parameter updates:

typescript
music.addTrigger({
  id: 'danger-high',
  parameter: 'danger',
  condition: 'greater',   // 'greater' | 'less' | 'equals' | 'between'
  value: 0.7,
  action: 'enableLayer',  // 'enableLayer' | 'disableLayer' | 'transition' | 'setVolume'
  target: 'tension',
});

// Update from game logic
music.setParameter('danger', calculateDangerLevel());

Audio Node Pool

The AudioNodePool reuses Web Audio GainNode and PannerNode instances to reduce garbage collection pressure:

typescript
const nodePool = audioManager.nodePool;

// Pool statistics
const stats = nodePool.getStats();
console.log(`Gain nodes: ${stats.gainNodes.inUse}/${stats.gainNodes.total}`);

Configuration Reference

typescript
interface AudioSystemConfig {
  masterVolume?: number;            // Default: 1.0
  maxSources?: number;              // Default: 32
  defaultDistanceModel?: DistanceModel; // Default: 'inverse'
  defaultPanningModel?: PanningModel;   // Default: 'HRTF'
  defaultRefDistance?: number;      // Default: 1
  defaultMaxDistance?: number;      // Default: 100
  defaultRolloff?: number;          // Default: 1
  spatialAudioEnabled?: boolean;    // Default: true
  dopplerEnabled?: boolean;         // Default: false
  reverbEnabled?: boolean;          // Default: true
}

Performance Tips

  • Sound pools -- Use createSoundPool() for frequently played sounds to avoid allocation overhead
  • Pool warm-up -- Call pool.warmUp() during loading screens to pre-allocate audio nodes
  • Bus hierarchy -- Group sounds by type for efficient volume and effects control
  • Spatial culling -- Enable automatic culling of distant sounds to save Web Audio resources
  • Node pooling -- The AudioNodePool reuses GainNode/PannerNode to reduce GC pressure
  • Clip preloading -- Load clips during loading screens, not during gameplay
  • Spatial updates -- Only update listener/source positions for moving objects

Cleanup

typescript
audioManager.stopAll();
audioManager.dispose();

Proprietary software. All rights reserved.