Skip to main content
Version: Next

Audio

Sliver’s SoundManager is a small wrapper around the Web Audio API (AudioContext).

You typically:

  1. create a single SoundManager for your game
  2. preload sounds (SFX + music) with loadSound
  3. “unlock” audio after a user interaction
  4. use playSound for one-shots and playSong for background music

Getting the SoundManager​

SoundManager is available through the GameContext:

const sound = game.getContext().getSoundManager();

Your Game instance receives a soundManager in its constructor options, so you usually create it once and pass it in:

import { Game, SoundManager } from "sliver-engine";

const soundManager = new SoundManager();

const game = new Game({
canvas,
scenes,
soundManager: soundManager,
});

Unlocking audio (required by browsers)​

Browsers block audio autoplay. Until the user clicks/taps/presses a key, the underlying AudioContext is typically “suspended”.

Call unlock() from a user interaction handler (recommended), once, as early as possible:

window.addEventListener("pointerdown", async () => {
await game.getContext().getSoundManager().unlock();
});

I recommend integrating this unlock into your game in the following way:

  • Your start scene has nothing but a button for start game
  • Clicking that button transitions to the next scene and unlocks the audio
class MyButton extends GameObject {
// {...}

@onClick((self) => {
self.getContext().getSoundManager().unlock();
})
override handleEvent(event: GameEvent) {
super.handleEvent(event);
}
}

// or

class MyButton extends GameObject {
// {...}

@onClick((self) => {
self.getContext().getSoundManager().playSound("button_click");
// If the first audio that's played comes from a user
// interaction, the unlock happens automatically
})
override handleEvent(event: GameEvent) {
super.handleEvent(event);
}
}

Why this matters:

  • unlock() connects the internal masterGain to the audio output and resumes the AudioContext.
  • playSound() / playSong() try to unlock automatically when needed, but they don’t await it, so your very first sound is more reliable if you unlock explicitly on input.

Preloading sounds​

Load audio files into an in-memory library (decoded into an AudioBuffer):

const sound = game.getContext().getSoundManager();

await sound.loadSound(
"jump",
new URL("/audio/jump.wav", window.location.href),
["sfx", "player"]
);

await sound.loadSound(
"bgm:forest",
new URL("/audio/forest-theme.ogg", window.location.href),
["music"]
);

Notes:

  • loadSound(name, url, tags?) fetches the asset and decodes it. If the fetch fails, it throws with the HTTP status.
  • tags are stored with the sound entry (useful for your own filtering/organization via getLoadedSounds()).
  • [TODO] tags will also be used to create a gain control for each tag in the future

Master volume​

SoundManager routes all sounds through a master gain node, so you can implement global volume/mute easily:

const sound = game.getContext().getSoundManager();

sound.setMasterGain(0.6); // clamps to >= 0
console.log(sound.getMasterGain()); // -> 0.6

Per-sound default volume​

Each loaded sound keeps a default volume (used when you don’t pass an explicit volume to playSound / playSong):

sound.setSoundVolume("jump", 0.3); // clamps to >= 0
console.log(sound.getSoundVolume("jump")); // -> 0.3

This is a good place to normalize loud assets so your call sites can stay simple.

Playing one-shot sound effects​

Use playSound(name, options) for SFX. It returns a small controller with a stop() method.

const sfx = sound.playSound("jump", {
volume: 0.8,
playbackRate: 1.1,
});

// ...later (optional)
sfx.stop();

Useful options​

  • loop: repeat until stopped (defaults to false)
  • playbackRate: speed/pitch (defaults to 1)
  • wait: delay before playing (seconds)
  • start: offset into the audio file (seconds)
  • end: stop at an absolute time in the file (seconds)
  • duration: how long to play (seconds). If present, it takes precedence over end.

Example: play only a short “blip” segment, after a delay:

sound.playSound("ui:beep", {
wait: 0.05,
start: 0.12,
duration: 0.08,
});

Why duration wins over end: it’s often easier to express “play 80ms from here” than to compute an absolute end time when you change start.

Playing and controlling background music (“songs”)​

Use playSong(name, options) for tracked background music.

Behavior:

  • If you request the same song that’s already playing, nothing changes and you get the existing controller back.
  • If you request a different song, the previous song is stopped and the new one starts.

By default, songs loop (loop defaults to true for playSong).

Fading between songs​

playSong supports fade transitions (in seconds) via fade, fadeIn, and fadeOut:

sound.playSong("bgm:forest", { volume: 0.5, fade: 0.6 });

// Later, cross-fade to another track
sound.playSong("bgm:cave", { volume: 0.5, fadeOut: 0.6, fadeIn: 0.8 });

To stop the current song:

sound.stopSong({ fadeOut: 0.5 });

Or use the controller returned by playSong:

const song = sound.playSong("bgm:forest");
song.stop({ fadeOut: 0.5 });

Introspection and debugging​

console.log(sound.getCurrentSongName()); // string | undefined
console.log(Object.keys(sound.getLoadedSounds())); // names you loaded