Audio Loom is a framework-agnostic audio management library built on the Web Audio API. It provides centralized control for organizing, playing, and managing audio assets in games and interactive applications.
Works seamlessly with Phaser, Three.js, React, Angular, and other JavaScript frameworks.
npm install @happy-pixels/audio-loom
# or
pnpm add @happy-pixels/audio-loom
import { AudioManager } from '@happy-pixels/audio-loom';
const audio = new AudioManager();
// Initialize on user interaction (required by browsers)
document.getElementById('startButton')!.onclick = async () => {
await audio.resumeAudioContext();
// Now audio is ready to play!
audio.playAudioTrack('click');
};
// Register sounds
audio.addAudioTrack('click', 'ui', '/sounds/click.wav');
audio.addAudioTrack('explosion', 'sfx', '/sounds/explosion.wav');
audio.addAudioTrack('music', 'music', '/music/theme.mp3');
// Preload for instant playback
await audio.preload(['click', 'explosion']);
// Play one-shot sound effects
audio.playAudioTrack('explosion');
// Play continuous background music
audio.playContinuous('music');
Organize sounds into logical groups for independent volume and mute control:
// Register tracks in different groups
audio.addAudioTrack('explosion', 'sfx', '/sounds/explosion.wav');
audio.addAudioTrack('footstep', 'sfx', '/sounds/footstep.wav');
audio.addAudioTrack('music', 'music', '/music/theme.mp3');
audio.addAudioTrack('rain', 'ambient', '/sounds/rain.mp3');
// Control groups independently
audio.setAudioVolume('sfx', 0.8); // SFX at 80%
audio.setAudioVolume('music', 0.5); // Music at 50%
audio.setAudioEnabled('ambient', false); // Mute ambient sounds
Control overall audio output:
audio.setMasterVolume(0.7); // Set master to 70%
const volume = audio.getMasterVolume();
Register multiple tracks under the same key for natural variation:
// Add multiple footstep sounds
audio.addAudioTrack('footstep', 'sfx', '/sounds/footstep1.wav');
audio.addAudioTrack('footstep', 'sfx', '/sounds/footstep2.wav');
audio.addAudioTrack('footstep', 'sfx', '/sounds/footstep3.wav');
// Each play selects randomly (shuffled, no repeats until all played)
audio.playAudioTrack('footstep');
Preload audio for instant, low-latency playback:
// Check loading status
const status = audio.getLoadStatus('explosion');
console.log(`Loaded ${status.loaded}/${status.total}`);
// Preload specific keys
await audio.preload(['explosion', 'gunshot', 'footstep']);
// Check if ready
if (audio.isLoaded('explosion')) {
audio.playAudioTrack('explosion');
}
Play sound effects that run to completion:
audio.playAudioTrack('explosion');
audio.playAudioTrack('gunshot');
// Pool limits prevent too many concurrent sounds
audio.setGroupPoolSize('sfx', 8); // Max 8 simultaneous SFX
Play looping audio with full control:
// Start background music
audio.playContinuous('music');
// Pause/resume
audio.pauseContinuous();
audio.resumeContinuous();
// Stop
audio.stopContinuous();
// Multiple channels for layered audio
audio.playContinuous('music', 'music-channel');
audio.playContinuous('rain', 'ambient-channel');
// Control channels independently
audio.pauseContinuous('ambient-channel');
audio.setPlaybackRate(0.8, 'music-channel');
Smooth volume transitions using Web Audio API:
// Fade in new music over 2 seconds
await audio.fadeIn('battle-music', 2000);
// Fade out over 1.5 seconds
await audio.fadeOut(1500);
// Cross-fade to new track (simultaneous fade out/in)
await audio.crossFade('victory-music', 2000);
Fine-grained control over continuous playback:
// Playback rate (speed/pitch)
audio.setPlaybackRate(1.5); // 1.5x speed
audio.setPlaybackRate(0.5); // Half speed
// Seeking
audio.seek(30); // Jump to 30 seconds
const currentTime = audio.getCurrentTime();
const duration = audio.getDuration();
// Get full playback info
const info = audio.getPlaybackInfo();
console.log(`${info.currentTime}/${info.duration}s at ${info.playbackRate}x`);
React to audio events using RxJS observables:
// Track starts
audio.onTrackStart$.subscribe(event => {
console.log(`Playing: ${event.key} on ${event.channelId}`);
});
// Track ends
audio.onTrackEnd$.subscribe(event => {
console.log(`Finished: ${event.key}`);
});
// Track loaded
audio.onLoadComplete$.subscribe(event => {
console.log(`Loaded: ${event.key}, duration: ${event.duration}s`);
});
// Errors
audio.onError$.subscribe(event => {
console.error(`Error: ${event.message}`, event.error);
});
Handle browser autoplay restrictions:
// Initialize on user interaction
button.onclick = async () => {
await audio.resumeAudioContext();
};
// Check if ready
if (audio.isAudioReady()) {
audio.playAudioTrack('click');
}
// Suspend when app is in background
document.addEventListener('visibilitychange', async () => {
if (document.hidden) {
await audio.suspendAudioContext();
} else {
await audio.resumeAudioContext();
}
});
Properly dispose of resources:
// Stop all and release resources
audio.destroy();
// React example
useEffect(() => {
const audio = new AudioManager();
return () => audio.destroy();
}, []);
Audio Loom provides a complete 3D spatial audio system using the Web Audio API's PannerNode and AudioListener.
Position your listener (usually the player/camera) in 3D space:
// Set listener position
audio.setListenerPosition({ x: 0, y: 1.7, z: 0 }); // Player at origin, ear height
// Set listener orientation (forward and up vectors)
audio.setListenerOrientation(
{ x: 0, y: 0, z: -1 }, // Looking forward (-Z)
{ x: 0, y: 1, z: 0 } // Y is up
);
// Convenience: use vec3 helper
import { vec3 } from '@happy-pixels/audio-loom';
audio.setListenerPosition(vec3(10, 0, -5));
Play positioned sound effects:
audio.addAudioTrack('explosion', 'sfx', '/sounds/explosion.wav');
await audio.preload(['explosion']);
// Play at position
const instanceId = audio.play3D('explosion', { x: 10, y: 0, z: -5 });
// With options
const id = audio.play3D('explosion', { x: 10, y: 0, z: -5 }, {
volume: 0.8,
spatialConfig: {
maxDistance: 100,
rolloffFactor: 1.5,
distanceModel: 'inverse'
}
});
// Update position for moving sounds
audio.updateSoundPosition(instanceId, { x: 12, y: 0, z: -3 });
Play positioned looping audio:
audio.addAudioTrack('fire', 'ambient', '/sounds/fire.wav');
// Play at position with channel ID
await audio.playContinuous3D('fire', { x: 5, y: 0, z: -10 }, 'campfire');
// Update position
audio.updateChannelPosition('campfire', { x: 6, y: 0, z: -10 });
// Check if channel is 3D
if (audio.is3DChannel('campfire')) {
const pos = audio.getChannelPosition('campfire');
}
Configure default spatial settings:
import { SPATIAL_PRESET_INDOOR, SPATIAL_PRESET_OUTDOOR } from '@happy-pixels/audio-loom';
// Set defaults for all 3D sounds
audio.setSpatialDefaults({
distanceModel: 'inverse', // 'linear', 'inverse', 'exponential'
panningModel: 'HRTF', // 'HRTF' for headphones, 'equalpower' for speakers
refDistance: 1,
maxDistance: 100,
rolloffFactor: 1
});
// Use presets
audio.setSpatialDefaults(SPATIAL_PRESET_INDOOR); // Smaller space
audio.setSpatialDefaults(SPATIAL_PRESET_OUTDOOR); // Larger space
Create cone-shaped sound sources (speakers, spotlights):
const id = audio.play3D('announcement', { x: 0, y: 2, z: 5 }, {
orientation: { x: 0, y: 0, z: -1 }, // Pointing forward
spatialConfig: {
cone: {
innerAngle: 60, // Full volume cone
outerAngle: 120, // Attenuated cone
outerGain: 0.2 // Volume outside outer cone
}
}
});
// Update orientation
audio.setSoundOrientation(id, { x: 1, y: 0, z: 0 }); // Point right
Lightweight left/right panning without full 3D:
// Play with stereo pan (-1 = left, 0 = center, 1 = right)
const id = audio.play2DPanned('footstep', { pan: -0.5, volume: 0.8 });
// Update pan position
audio.setPan(id, 0.3);
// Check pan value
const pan = audio.getSoundPan(id); // Returns -0.5
React when sounds cross distance thresholds (useful for LOD audio):
const id = audio.play3D('enemy', { x: 100, y: 0, z: 0 });
audio.registerDistanceCallback(id, {
thresholds: [10, 25, 50],
onThresholdCross: (instanceId, distance, threshold, direction) => {
console.log(`Sound ${instanceId} ${direction} ${threshold}m threshold`);
if (threshold === 10 && direction === 'entering') {
// Enemy is close! Play alert
}
},
checkInterval: 100 // ms between checks
});
// Unregister when done
audio.unregisterDistanceCallback(id);
Apply reverb and filtering effects to simulate acoustic environments.
The effects bus creates parallel wet/dry signal paths:
// Register impulse response files for reverb
audio.addImpulseResponse('hall', '/impulses/hall.wav');
audio.addImpulseResponse('cave', '/impulses/cave.wav');
// Preload impulse responses
await audio.preloadImpulses(['hall', 'cave']);
// Apply reverb
await audio.setEffectsReverb('hall');
// Set wet/dry mix (0 = dry, 1 = fully wet)
audio.setEffectsMix(0.3);
// Apply filters
audio.setEffectsLowPass(4000, 1); // Frequency, Q
audio.setEffectsHighPass(200, 1);
Use built-in presets or create custom environments:
import { ENVIRONMENT_PRESETS } from '@happy-pixels/audio-loom';
// Apply preset
await audio.setEnvironment('cave'); // Heavy reverb, muffled
await audio.setEnvironment('forest'); // Light reverb, natural
await audio.setEnvironment('underwater'); // Heavy low-pass
await audio.setEnvironment('indoor');
await audio.setEnvironment('none'); // Clear all effects
// Custom environment
await audio.setEnvironment({
reverb: 'hall', // Impulse response key
wetMix: 0.4,
lowPass: { frequency: 6000, Q: 1 },
highPass: { frequency: 80, Q: 0.7 }
});
Smoothly transition between environments:
// Transition over 2 seconds
await audio.transitionToEnvironment('cave', 2000);
// Transition to no effects
await audio.transitionToEnvironment('none', 1000);
Keep certain audio groups unaffected by effects:
// UI sounds bypass reverb (stay crisp)
audio.setGroupBypassEffects('ui', true);
// Dialog stays clear
audio.setGroupBypassEffects('dialog', true);
// Check bypass status
const bypassed = audio.getBypassedGroups(); // ['ui', 'dialog']
For best results with reverb:
Free impulse response sources:
| Method | Description |
|---|---|
initAudio() |
Initialize AudioContext |
resumeAudioContext() |
Resume suspended context |
suspendAudioContext() |
Suspend context |
isAudioReady() |
Check if context is running |
setMasterVolume(volume) |
Set master volume (0-1) |
getMasterVolume() |
Get master volume |
setAudioEnabled(group, enabled) |
Enable/disable group |
setAudioVolume(group, volume) |
Set group volume |
setGroupPoolSize(group, max) |
Set concurrent sound limit |
addAudioTrack(key, group, path) |
Register a track |
preload(keys) |
Preload tracks |
isLoaded(key) |
Check if loaded |
getLoadStatus(key) |
Get loading progress |
playAudioTrack(key) |
Play one-shot sound |
playContinuous(key, channel?) |
Start continuous playback |
stopContinuous(channel?) |
Stop continuous playback |
pauseContinuous(channel?) |
Pause playback |
resumeContinuous(channel?) |
Resume playback |
fadeIn(key, duration, channel?) |
Fade in new track |
fadeOut(duration, channel?) |
Fade out current track |
crossFade(key, duration, channel?) |
Cross-fade to new track |
setPlaybackRate(rate, channel?) |
Set playback speed |
seek(time, channel?) |
Seek to position |
getCurrentTime(channel?) |
Get current position |
getDuration(channel?) |
Get track duration |
getPlaybackInfo(channel?) |
Get full playback info |
getActiveChannels() |
Get all active channels |
getChannelInfo(channel?) |
Get channel info |
stopAllContinuous() |
Stop all channels |
pauseAllContinuous() |
Pause all channels |
resumeAllContinuous() |
Resume all channels |
destroy() |
Clean up resources |
| Method | Description |
|---|---|
setListenerPosition(position) |
Set listener position in 3D space |
getListenerPosition() |
Get current listener position |
setListenerOrientation(forward, up) |
Set listener direction |
getListenerOrientation() |
Get listener orientation vectors |
setSpatialDefaults(config) |
Set default spatial config |
getSpatialDefaults() |
Get current spatial defaults |
play3D(key, position, options?) |
Play 3D positioned sound |
playContinuous3D(key, position, channel?, options?) |
Play 3D looping sound |
updateSoundPosition(id, position) |
Update one-shot sound position |
updateChannelPosition(channel, position) |
Update channel position |
is3DSound(instanceId) |
Check if sound is 3D |
is3DChannel(channelId) |
Check if channel is 3D |
getSoundPosition(instanceId) |
Get sound position |
getChannelPosition(channelId) |
Get channel position |
setSoundOrientation(id, forward) |
Set directional sound orientation |
setChannelOrientation(channel, forward) |
Set channel orientation |
play2DPanned(key, options?) |
Play with stereo panning |
setPan(instanceId, pan) |
Update stereo pan (-1 to 1) |
getSoundPan(instanceId) |
Get current pan value |
registerDistanceCallback(id, config) |
Register distance threshold callback |
unregisterDistanceCallback(id) |
Remove distance callback |
| Method | Description |
|---|---|
addImpulseResponse(key, path) |
Register impulse response file |
preloadImpulses(keys) |
Preload impulse responses |
isImpulseLoaded(key) |
Check if impulse is loaded |
setEffectsReverb(key) |
Apply reverb (or null to disable) |
getActiveReverb() |
Get current reverb key |
setEffectsMix(wet) |
Set wet/dry mix (0-1) |
getEffectsMix() |
Get current wet/dry mix |
setEffectsLowPass(frequency, Q?) |
Set low-pass filter |
getEffectsLowPass() |
Get low-pass settings |
setEffectsHighPass(frequency, Q?) |
Set high-pass filter |
getEffectsHighPass() |
Get high-pass settings |
setEnvironment(preset|config) |
Apply environment preset or config |
getEnvironment() |
Get current environment |
transitionToEnvironment(preset, duration) |
Smooth environment transition |
setGroupBypassEffects(group, bypass) |
Bypass effects for group |
isGroupBypassingEffects(group) |
Check if group bypasses effects |
getBypassedGroups() |
Get list of bypassed groups |
getEffectsBusState() |
Get full effects bus state |
| Observable | Event Type | Description |
|---|---|---|
onTrackStart$ |
TrackStartEvent |
Track started playing |
onTrackEnd$ |
TrackEndEvent |
Track finished |
onLoadComplete$ |
LoadCompleteEvent |
Track metadata loaded |
onError$ |
AudioErrorEvent |
Error occurred |
onPositionUpdate$ |
PositionUpdateEvent |
3D sound position changed |
onOrientationUpdate$ |
OrientationUpdateEvent |
Sound orientation changed |
onDistanceThreshold$ |
DistanceThresholdEvent |
Distance threshold crossed |
Full API documentation is available at https://happy-pixels.github.io/audio-loom/
ISC