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.
AudioPlugin Setup
Section titled “AudioPlugin Setup”import { AudioPlugin } from "@yagejs/audio";
engine.use(new AudioPlugin({ masterVolume: 1, // default: 1 channels: { sfx: { volume: 1.0 }, music: { volume: 0.7 }, },}));Sound Assets
Section titled “Sound Assets”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];}Playing Sounds
Section titled “Playing Sounds”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 effectaudio.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 playOnceaudio.playOnce(BgMusic, { channel: "music", loop: true });
// Own one request for playback shared by alias and channelconst finishCharge = () => console.log("charge finished");const request = audio.requestOnce("charge", { onEnd: finishCharge });request.release();
// Pick a random sound from an arrayaudio.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.
Sound Handles
Section titled “Sound Handles”play returns a SoundHandle for controlling playback:
const handle = audio.play(BgMusic, { channel: "music", loop: true });
handle.playing; // booleanhandle.channel; // readonly mixer channelhandle.volume = 0.5; // per-sound volume before the channel multiplierhandle.speed = 1.5; // adjust speedhandle.paused = true; // pause/resumehandle.muted = true; // mute/unmutehandle.stop(); // stop playbackYou can also stop via the manager:
audio.stop(handle);Fades and Crossfades
Section titled “Fades and Crossfades”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
Section titled “Channels”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 controlaudio.setChannelVolume("music", 0.3);audio.getChannelVolume("music"); // 0.3
// Mute/unmute a channelaudio.muteChannel("sfx");audio.unmuteChannel("sfx");
// Pause/resume a channelaudio.pauseChannel("music");audio.resumeChannel("music");
// Stop all sounds in a channelaudio.stopChannel("sfx");Global Mute
Section titled “Global Mute”audio.muteAll();audio.unmuteAll();audio.stopAll();Audio Unlock
Section titled “Audio Unlock”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);// …laterdispose();// 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.
Tab Visibility
Section titled “Tab Visibility”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.
Runtime Sounds
Section titled “Runtime Sounds”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/synthconst 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
Section titled “SoundComponent”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 nullThis 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.