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({
masterVolume: 1, // default: 1
channels: {
sfx: { volume: 1.0 },
music: { volume: 0.7 },
},
}));

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

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

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

import { Scene } from "@yagejs/core";
class GameScene extends Scene {
readonly name = "game";
readonly preload = [CoinSfx, BgMusic];
}

Resolve the AudioManager from the DI container and call play:

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

Pass the handle sound() returned, or the alias string it registers — the handle’s path. The two forms can be mixed, and a sound added with registerSound has no handle, so it is always played by name.

If nothing is registered under the alias, play, playOnce, requestOnce, and playRandom throw and name it. That covers both a typo and playing a sound the scene never preloaded.

hasSound(ref) makes the same check and returns a boolean, so an alias built at runtime can fall back to a default:

function playStep(surface: string) {
const step = `sfx/step-${surface}.wav`;
audio.play(audio.hasSound(step) ? step : "sfx/step-default.wav");
}

It answers registration alone. Whether audio is audible is a different question: isUnlocked() covers the browser’s autoplay gate, and the mute calls below cover the rest.

Play options:

audio.play(CoinSfx, {
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, { channel: "music", loop: true });
// Own one request for playback shared by alias and channel
const finishCharge = () => console.log("charge finished");
const request = audio.requestOnce("charge", { onEnd: finishCharge });
request.release();
// Pick a random sound from an array
audio.playRandom(["step1.wav", "step2.wav", "step3.wav"], { channel: "sfx" });

playOnce holds one implicit owner for the shared playback. Repeated playOnce calls return the same SoundHandle without adding owners, so callers may ignore the handle. Each requestOnce call returns a separate SoundRequestHandle with active and an idempotent release(). Releasing one request does not stop the sound while another request or a playOnce owner remains.

When the sound ends on its own, every active request becomes inactive and its onEnd callback runs once. A released request receives no callback. Stopping the shared SoundHandle, its channel, or all audio clears every request without calling onEnd.

request.release({ fadeOut: seconds }) accepts SoundRequestReleaseOptions. fadeOut must be finite and non-negative; omitting it or passing zero stops immediately when the final owner releases. A positive fade requires an installed AudioPlugin. Other requests and playOnce owners keep the recording playing at its current volume. Only the final request’s fade applies.

The final request remains active through its fade, then becomes inactive when the sound stops or ends. A released request receives no onEnd callback, including if the recording ends during its fade. A new requestOnce or playOnce call during the fade starts a fresh recording. Channel controls still apply to both recordings.

play returns a SoundHandle for controlling playback:

const handle = audio.play(BgMusic, { channel: "music", loop: true });
handle.playing; // boolean
handle.channel; // readonly mixer channel
handle.volume = 0.5; // per-sound volume before the channel multiplier
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);

Fade a handle to a target volume with fadeTo. The method returns a Process, so you can cancel it or wait for it with toPromise():

import { easeInOutQuad } from "@yagejs/core";
const fade = handle.fadeTo(0, {
duration: 0.6,
easing: easeInOutQuad,
stopOnComplete: true,
});
await fade.toPromise();

The default easing is linear. stopOnComplete is available when the target volume is zero. Starting another fade on the same handle cancels and replaces the first fade.

Use crossfade to replace one playing sound with another:

import { sound } from "@yagejs/audio";
const StageMusic = sound("assets/stage.mp3");
const current = audio.play(BgMusic, { channel: "music", loop: true });
const next = audio.crossfade(current, StageMusic, {
duration: 1.2,
easing: easeInOutQuad,
channel: "music",
loop: true,
volume: 0.8,
});

The incoming sound starts at zero and fades to the requested volume, or 1 when volume is absent. The outgoing sound fades to zero and stops. The method returns the incoming SoundHandle immediately. If channel is absent, the incoming sound uses the outgoing handle’s channel.

SoundHandle.volume is the per-sound volume before master and channel multipliers. Changing either preserves the handle volume and the current point in an active fade.

Fades use the engine-global frame clock. They continue across scene changes and do not pause with an individual scene. They follow the global ProcessSystem.timeScale.

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

Use masterVolume for an overall Sound setting while keeping independent music and sound-effect levels:

audio.masterVolume = 0.5;
audio.setChannelVolume("music", 0.7);
audio.getChannelNames(); // ["sfx", "music"]

Playback volume is master volume × channel volume × sound volume. For example, master volume 0.5, channel volume 0.7 and sound volume 0.8 produce volume 0.28. Changing master volume immediately affects playing sounds and applies to future sounds, including channels created later. Channel and sound volumes, ongoing fades, and mute/pause state keep their own values.

Master volume defaults to 1. Set its initial value with AudioConfig.masterVolume or read and write audio.masterVolume at runtime. Values must be finite numbers from 0 to 1; invalid values throw before changing state. Master volume controls playback through this manager, including registered runtime sounds. It does not control external audio contexts or sounds played outside the manager.

getChannelNames() returns a fresh, read-only array of configured and subsequently created channel names in creation order. Silent channels remain in the list. Playback and channel volume, mute and pause controls create unknown channels automatically. Reading getChannelVolume(name) for an unknown channel returns 1 without creating it.

// 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:

import { AudioManagerKey } from "@yagejs/audio";
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 startMusic = () => {
audio.play("music/title", { channel: "music", loop: true });
};
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:

import { AudioManagerKey, AudioPlugin } from "@yagejs/audio";
engine.use(new AudioPlugin({
autoMuteOnBlur: true, // default: true
}));
// Or toggle at runtime:
const audio = this.use(AudioManagerKey);
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";
import { synthBuffer, synthPresets } from "@yagejs-addons/synth";
// Any code that produces an AudioBuffer; here, @yagejs-addons/synth
const buffer = synthBuffer(synthPresets.shoot());
registerSound("shoot", buffer);
const audio = this.use(AudioManagerKey);
audio.play("shoot");

You can call registerSound at module scope, before AudioPlugin installs: the alias is held and added to the audio library the plugin loads.

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.

If a save root stores the alias, re-register the buffer under the same alias during boot before loading that root. The AudioBuffer itself is runtime state and does not belong in a controlled save payload.

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";
import { BgMusic } from "./assets.js";
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.

Coding agents: fetch https://yage.dev/llms.txt first and prefer the Markdown references it links over these HTML pages. This page's Markdown counterpart is /llms/packages/audio.md.