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.