Skip to content

Audio

YAGE wraps @pixi/sound for audio playback. The @yagejs/audio package provides channel-based mixing, a SoundComponent for entity-bound audio, and a sound() asset factory for preloading.

import { AudioPlugin } from "@yagejs/audio";
engine.use(new AudioPlugin({
channels: {
sfx: { volume: 1.0 },
music: { volume: 0.7 },
},
}));

Use the sound() factory to create preloadable asset handles:

import { sound } from "@yagejs/audio";
const CoinSfx = sound("assets/coin.wav");
const BgMusic = sound("assets/music.mp3");

Add them to your scene’s preload array so they load before onEnter:

class GameScene extends Scene {
readonly preload = [CoinSfx, BgMusic];
}

Resolve the AudioManager from the DI container and call play:

import { AudioManagerKey } from "@yagejs/audio";
const audio = this.use(AudioManagerKey);
// Play a sound effect
audio.play(CoinSfx.path, { channel: "sfx" });
// Play background music (looping)
audio.play(BgMusic.path, { channel: "music", loop: true, volume: 0.5 });

Play options:

audio.play(alias, {
channel: "sfx", // which channel (default: "sfx")
volume: 1, // instance volume 0–1 (default: 1)
loop: false, // loop playback (default: false)
speed: 1, // playback rate (default: 1)
});

Additional play methods:

// Only play if not already playing via playOnce
audio.playOnce(BgMusic.path, { channel: "music", loop: true });
// Pick a random sound from an array
audio.playRandom(["step1.wav", "step2.wav", "step3.wav"], { channel: "sfx" });

play returns a SoundHandle for controlling playback:

const handle = audio.play(BgMusic.path, { channel: "music", loop: true });
handle.playing; // boolean
handle.volume = 0.5; // adjust volume
handle.speed = 1.5; // adjust speed
handle.paused = true; // pause/resume
handle.muted = true; // mute/unmute
handle.stop(); // stop playback

You can also stop via the manager:

audio.stop(handle);

Channels group sounds for independent volume control. The default channels are "sfx" and "music", but you can define any channels in the config.

// Volume control
audio.setChannelVolume("music", 0.3);
audio.getChannelVolume("music"); // 0.3
// Mute/unmute a channel
audio.muteChannel("sfx");
audio.unmuteChannel("sfx");
// Pause/resume a channel
audio.pauseChannel("music");
audio.resumeChannel("music");
// Stop all sounds in a channel
audio.stopChannel("sfx");
audio.muteAll();
audio.unmuteAll();
audio.stopAll();

Web browsers suspend the AudioContext when a page loads and require a user gesture (click, tap, or keypress) before any audio can play. @pixi/sound attaches a one-shot gesture listener that resumes the context on the first interaction, so calling audio.play(...) after a click just works.

What this means in practice: any audio you schedule on page load is silent until the user first interacts with the page. That is expected browser behavior, not a YAGE bug — the classic symptom is “my title music doesn’t play until I click somewhere.” To handle the gap cleanly, use onUnlock:

const audio = this.use(AudioManagerKey);
audio.onUnlock(() => {
audio.play("music/title", { channel: "music", loop: true });
});

onUnlock fires synchronously if the context is already running, or once on the next gesture that resumes it. It returns a disposer, so you can cancel a pending registration:

const dispose = audio.onUnlock(() => startMusic());
// …later
dispose();
// or: audio.offUnlock(startMusic);

Expose the pending state to the UI if you want a “tap to enable audio” hint:

if (!audio.isUnlocked()) {
// render a prompt; it's safe to dismiss on first click —
// @pixi/sound will have resumed the context by then
}

isUnlocked() reflects AudioContext.state === "running" and is never flipped by the mute/pause-on-blur flags below — it is strictly the browser-capability check.

Most web games go silent when the user backgrounds the tab. AudioManager suspends the underlying AudioContext when the window loses focus and resumes it on return:

engine.use(new AudioPlugin({
autoMuteOnBlur: true, // default: true
}));
// Or toggle at runtime:
audio.autoMuteOnBlur = false;

autoMuteOnBlur is delegated to @pixi/sound’s built-in WebAudioContext.autoPause, which listens for window.blur/focus events and pauses/resumes the entire AudioContext. Per-channel mutes and volumes are untouched. Toggling the flag while the window is currently unfocused takes effect immediately (the manager reconciles paused for you); otherwise the new policy applies on the next blur event.

Pausing scenes on tab blur is a scene-lifecycle concern, not an audio one — see SceneManager.autoPauseOnBlur.

registerSound(alias, buffer) registers a runtime-generated AudioBuffer under an alias, so it resolves and plays exactly like a preloaded sound — through the same AudioManager channels, mute, and blur auto-pause. This is the audio counterpart to the renderer’s registerTexture(key, texture), and it’s how a procedural-audio addon plugs a synthesized sound effect into the engine.

import { registerSound, AudioManagerKey } from "@yagejs/audio";
const buffer = synthesizeShot(); // any code that produces an AudioBuffer
registerSound("shoot", buffer);
const audio = this.use(AudioManagerKey);
audio.play("shoot");

Registered aliases are engine-global and live until you call unregisterSound(alias). Re-registering an alias replaces the entry. Registering an alias already used by a loaded sound asset throws, since letting a runtime buffer shadow a loaded asset would let that asset’s unload destroy the registered sound.

For save and load, snapshots store only the alias string — re-register the buffer under the same alias during boot, before restoring, the same way you registered it on first run.

import { unregisterSound } from "@yagejs/audio";
unregisterSound("shoot");

unregisterSound is a no-op for aliases it never registered. An AudioBuffer has no destroy step, so unlike unregisterTexture there’s no GPU resource to release.

SoundComponent binds a sound to an entity. The sound is automatically stopped when the entity is destroyed.

import { SoundComponent } from "@yagejs/audio";
entity.add(new SoundComponent({
alias: BgMusic.path,
channel: "music",
loop: true,
volume: 0.8,
playOnAdd: true, // start playing when the component is added
}));

Control playback from the component:

const sc = entity.get(SoundComponent);
const handle = sc.play();
sc.stop();
sc.handle; // current SoundHandle or null

This is useful for ambient sounds attached to world entities — the sound lifecycle is tied to the entity lifecycle.