Skip to main content

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​

FieldTypeDefaultDescription
mutedbooleanfalseGlobal mute (all audio)
musicMutedbooleanfalseMute music tracks only
fxMutedbooleanfalseMute FX tracks only
musicVolumenumber0.1Default music volume (0–1)
fxVolumenumber0.8Default FX volume (0–1)
soundsVolumesRecord<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
soundVariantsRecord<string, string[]>—Sound IDs that stand for a set of files, one picked at random per play
prefixstring—Prefix prepended to the manifest bundle name and all asset key lookups
debugbooleanfalseLogs all operations to the console

Behaviour Notes​

  • Tab visibility — automatically mutes when document.hidden becomes true and restores when the tab is visible again (respects the muted flag).
  • Asset resolution — sound names are resolved as ${prefix}/${soundName} against the manifest. The resolved URL becomes assets/${prefix}/${soundName}.ogg for string entries or the raw array for multi-source entries.
  • Singleton — the exported sounds instance is shared across the whole app. Create a new Sounds() instance only when you need isolated audio state (e.g. per-scene audio managers).