~2 min read

Audio

Phaser 4's Web Audio integration — music vs. SFX, the user-gesture unlock, mixing, and spatial sound.

Phaser 4 wraps the browser’s Web Audio API behind this.sound. You load audio through the standard loader and play it through a typed sound manager.

Loading

preload() {
	// Multiple formats — the browser picks the first it can decode.
	this.load.audio('music', ['assets/audio/theme.ogg', 'assets/audio/theme.mp3']);
	this.load.audio('hit', 'assets/audio/hit.ogg');
}

Always ship at least two formats. OGG covers Firefox and Chrome; MP3 or M4A is needed for Safari.

Playing

this.sound.play('hit');                                  // fire-and-forget SFX
this.sound.play('hit', { volume: 0.6, detune: -200 });   // tuned per-call

// For anything you need a handle to:
const music = this.sound.add('music', { loop: true, volume: 0.5 });
music.play();
music.setVolume(0.2);
music.stop();

this.sound.play(key, config) returns the sound but doesn’t keep a reference — use add + play whenever you need to control the sound after starting it (pause, fade, change volume).

The user-gesture unlock

Browsers refuse to start audio until the page has received a user gesture (click, key press, touch). Phaser detects this and unlocks the audio context on the first input — but any sound you try to play before that point will be silent.

Defensive patterns:

// Wait until unlocked before playing music.
if (this.sound.locked) {
	this.sound.once(Phaser.Sound.Events.UNLOCKED, () => music.play());
} else {
	music.play();
}

For most games this is a non-issue because the first interaction (a menu click, “press any key”) naturally precedes any audio. It’s a footgun when you try to play music in an auto-starting splash scene.

Music: looping and fading

const music = this.sound.add('music', { loop: true, volume: 0 });
music.play();
this.tweens.add({ targets: music, volume: 0.6, duration: 1500 });

// Cross-fade to a different track.
this.tweens.add({ targets: music, volume: 0, duration: 1000, onComplete: () => {
	music.stop();
	const next = this.sound.add('battle', { loop: true, volume: 0 });
	next.play();
	this.tweens.add({ targets: next, volume: 0.6, duration: 1000 });
}});

For seamless loops, your source file must be sample-accurate at the loop point. Vorbis (.ogg) is the most forgiving; MP3 historically adds silent padding.

Mixing

The sound manager has a master volume:

this.sound.volume = 0.8;       // 0..1, multiplied with every sound's volume
this.sound.mute = true;        // toggle, not absolute
this.sound.pauseAll();
this.sound.resumeAll();

For separate music/SFX buses, group sounds in your own helper and apply category multipliers — Phaser doesn’t ship built-in bus support.

Spatial sound

For 2D positional audio, use the Web Audio backend’s panning support:

const sfx = this.sound.add('engine', { loop: true });
sfx.play();
// Pan from -1 (full left) to 1 (full right) based on distance from camera center.
this.events.on('update', () => {
	const dx = (vehicle.x - this.cameras.main.midPoint.x) / (this.cameras.main.width / 2);
	sfx.setPan(Phaser.Math.Clamp(dx, -1, 1));
	sfx.setVolume(1 / (1 + Math.abs(dx) * 1.5));
});

For richer 3D-style behavior (HRTF, distance models), drop down to the raw Web Audio API — Phaser exposes the underlying AudioContext as this.sound.context.

Common pitfalls

Forgetting to remove sounds on scene transitions. Sounds keep playing across scenes unless you stop them. Stop your scene’s music in shutdown() or attach it to the scene’s lifecycle:

this.events.once('shutdown', () => music.stop());

Per-call config doesn’t persist. this.sound.play('hit', { volume: 0.3 }) plays this one at 30% volume — the cached config for the key is unchanged.

Decoding cost. Large audio files decode on first play, which can stutter. Pre-decode by playing the sound muted at game start, or load smaller files.