Sounds
Standalone audio controller built on Howler.js. Manages two categories of audio — FX (one-shots) and Music (looping tracks) — with a browser user-interaction gate and automatic tab-visibility muting.
A ready-made singleton is exported alongside the class:
import { sounds, Sounds, type SoundSettings } from 'spine-layout';
A module-level pointerdown listener calls sounds.onUserInteraction() automatically on first user interaction, so you typically only need to call init() yourself.
Setup
constructor
new Sounds(settings?: Partial<SoundSettings>)
Creates a new instance with optional initial settings. The constructor immediately mutes global audio (Howler-level) and registers a visibilitychange listener for auto-mute on tab switch.
init
init(pixiManifest: AssetsManifest, settings?: Partial<SoundSettings>): void
Reads sound asset entries from the PixiJS manifest bundle named sounds (or ${prefix}/sounds when a prefix is configured), then unmutes audio if user interaction has already occurred.
sounds.init(manifest, {
prefix: 'game1',
musicVolume: 0.1,
fxVolume: 0.8,
soundsVolumes: { 'game1/coin': 0.5 },
});
User Interaction
onUserInteraction
onUserInteraction(): void
Unlocks audio playback. Called automatically by the module-level pointerdown listener on the sounds singleton. Call manually on custom interaction events if needed.
onActivation
onActivation(callback: () => void): void
Registers a callback that fires once when onUserInteraction() is called. Use to defer sound-dependent logic until playback is actually permitted.
sounds.onActivation(() => sounds.playMusic('bgm_main'));
activated
activated: boolean
true after onUserInteraction() has been called at least once.
Playback
playFX
playFX(fx?: string | string[], loop?: boolean): Promise<void>
Plays a sound effect. Pass an array to pick one entry at random each call. Uses the per-sound override from soundsVolumes if present, otherwise fxVolume. No-ops until init() has been called.
A name ending in _loop is played as a looping FX under its stripped name, so playFX('spin_loop')
and playFX('spin', true) are the same call and both are stopped by stopFX('spin'). Re-requesting
a looping FX that is still playing is a no-op rather than a second, overlapping instance — a looping
animation can fire its sound event every cycle without stacking.
sounds.playFX('coin');
sounds.playFX(['hit1', 'hit2', 'hit3']); // random pick
sounds.playFX('laser', true); // looping FX
sounds.playFX('laser_loop'); // the same looping FX
A name listed in soundVariants is a set of files under one ID: the caller keeps asking for the
ID and each play picks one of its files at random. What the manifest does not have is dropped
rather than picked, whether it was asked for by name or reached through a variant list.
sounds.init(manifest, { soundVariants: { stop: ['stop_1', 'stop_2', 'stop_3'] } });
sounds.playFX('stop'); // one of the three, a different one next time
stopFX
stopFX(fx: string): void
Stops a named FX sound (useful for looping FX started with playFX(..., true)). An ID with
soundVariants stops whichever of its files is playing, so the caller never needs to know which
one was picked.
setFXVolume
setFXVolume(fx: string, volume: number): void
Moves a currently playing FX to a given volume, still scaled by the player's fxLevel dial. A
no-op if the FX is not currently playing. An ID with soundVariants moves whichever of its
files is playing. Use with authoredFXVolume to ramp a loop's volume over time, e.g. a win
counter's loop getting louder the longer it runs.
sounds.playFX('counterLoop', true);
sounds.setFXVolume('counterLoop', 1); // full volume, still × fxLevel
authoredFXVolume
authoredFXVolume(fx: string): number
The volume an FX is authored at — soundsVolumes[fx] (or its variant group's), falling back to
fxVolume — before fxLevel is applied. The starting point for a setFXVolume ramp.
playMusic
playMusic(music: string): Promise<void>
Starts a looping music track. If a different track is already playing it is stopped first — but only once the new track has actually started, so requesting a track that is missing from the manifest leaves the current music playing. Only other music stops music: FX never do, whatever their name. Calling playMusic with the same track name that is already active is a no-op.
sounds.playMusic('bgm_main');
Volume & Mute
mute / unmute
mute(): void
unmute(): void
Global mute/unmute via Howler. unmute() is a no-op if muted is true in settings, or if user interaction has not yet occurred.
updateSettings
updateSettings(settings: Partial<SoundSettings>): void
Hot-updates any combination of settings. Immediately applies mute states and volumes to all active sounds.
sounds.updateSettings({
muted: false,
musicVolume: 0.2,
fxVolume: 0.6,
soundsVolumes: { 'game1/explosion': 1.0 },
});
SoundSettings
| Field | Type | Default | Description |
|---|---|---|---|
muted | boolean | false | Global mute (all audio) |
musicMuted | boolean | false | Mute music tracks only |
fxMuted | boolean | false | Mute FX tracks only |
musicVolume | number | 0.1 | Default music volume (0–1) |
fxVolume | number | 0.8 | Default FX volume (0–1) |
soundsVolumes | Record<string, number> | — | Per-sound FX volume overrides, keyed by the prefixed asset name (e.g. 'game1/coin'). An entry against a soundVariants ID covers every file in that set |
soundVariants | Record<string, string[]> | — | Sound IDs that stand for a set of files, one picked at random per play |
prefix | string | — | Prefix prepended to the manifest bundle name and all asset key lookups |
debug | boolean | false | Logs all operations to the console |
Behaviour Notes
- Tab visibility — automatically mutes when
document.hiddenbecomestrueand restores when the tab is visible again (respects themutedflag). - Asset resolution — sound names are resolved as
${prefix}/${soundName}against the manifest. The resolved URL becomesassets/${prefix}/${soundName}.oggfor string entries or the raw array for multi-source entries. - Singleton — the exported
soundsinstance is shared across the whole app. Create anew Sounds()instance only when you need isolated audio state (e.g. per-scene audio managers).