| name | phaser-audio |
| description | This skill should be used when the user asks to "add sound", "play music", "audio not working", "add background music", "sound effects", "mute button", "audio sprite", "game audio", "play sound effect", or "music won't play". |
| version | 0.5.0 |
Phaser 4 Audio
Phaser 4 wraps the Web Audio API for all audio playback. Know when each audio backend applies and follow the loading/playback patterns below to avoid the most common audio bugs.
Web Audio vs HTML5 Audio
Phaser uses Web Audio by default on every browser that supports it (all modern browsers). Web Audio runs audio processing on a dedicated thread, supports spatial audio, effects chains, precise scheduling, and is never subject to the single-track limit that plagues HTML5 Audio.
HTML5 Audio is the automatic fallback when AudioContext is unavailable — typically older Android WebViews and some edge-case browser configurations. You rarely target it intentionally. If you must force it:
const config: Phaser.Types.Core.GameConfig = {
audio: {
disableWebAudio: true,
},
};
Never force HTML5 Audio unless you have a specific compatibility requirement. Its limitations — single concurrent track, no detune/rate, no spatial positioning — will constrain your design.
Loading Audio: Always Provide mp3 AND ogg
Browsers do not agree on a single audio codec. Safari and iOS require mp3. Firefox prefers ogg/vorbis. Chrome accepts both. Provide both formats and let Phaser pick the one the browser supports:
preload(): void {
this.load.audio('music-main', ['assets/audio/main.mp3', 'assets/audio/main.ogg']);
this.load.audio('sfx-jump', ['assets/audio/jump.mp3', 'assets/audio/jump.ogg']);
this.load.audio('sfx-coin', ['assets/audio/coin.mp3', 'assets/audio/coin.ogg']);
this.load.audio('sfx-hurt', ['assets/audio/hurt.mp3', 'assets/audio/hurt.ogg']);
}
Never load a single format. A game that works perfectly on your Chrome dev machine will be completely silent on Safari/iOS if you only ship .ogg.
Adding and Playing Sounds
this.sound.add() — Full Control
Use add() when you need a reference to control the sound (pause, resume, seek, adjust volume, listen for events):
create(): void {
const music = this.sound.add('music-main', {
loop: true,
volume: 0.6,
});
music.play();
this.music = music;
}
this.sound.play() — One-Shot Fire-and-Forget
Use the manager's play() for sounds you fire once and never need to reference again. Phaser manages the lifetime automatically:
this.sound.play('sfx-coin', { volume: 0.9 });
this.sound.play('sfx-jump', { volume: 1.0, rate: 1.1 });
Rule of thumb: Background music → add(). One-shot SFX → sound.play().
SoundConfig Options
Pass a SoundConfig object as the second argument to add() or as the second argument to play():
const config: Phaser.Types.Sound.SoundConfig = {
loop: false,
volume: 1,
rate: 1,
detune: 0,
seek: 0,
delay: 0,
};
Background Music
Background music loops continuously, needs volume control, and must be stopped on scene shutdown.
this.load.audio('music-game', ['assets/audio/game.mp3', 'assets/audio/game.ogg']);
this.load.audio('music-menu', ['assets/audio/menu.mp3', 'assets/audio/menu.ogg']);
this.bgMusic = this.sound.add('music-game', { loop: true, volume: 0.5 });
this.bgMusic.play();
this.bgMusic.setVolume(0.3);
this.bgMusic.pause();
this.bgMusic.resume();
this.bgMusic.stop();
SFX Pattern: One-Shot Sounds
For sound effects that fire and forget — coin pickups, gunshots, UI clicks — use this.sound.play() directly. Do not store a reference:
this.sound.play('sfx-coin', { volume: 0.9 });
this.sound.play('sfx-jump', { volume: 1.0, rate: 1.05 });
this.sound.play('sfx-death', { volume: 0.8, detune: -200 });
Phaser creates an internal instance, plays it to completion, then destroys it. Zero cleanup required.
Sound Pooling for Rapid SFX
If a sound fires many times per second (gunshots, footsteps, rapid UI feedback), a single instance causes audible cutoff — each new play() call restarts the same sound from the beginning.
Pre-create a pool of instances and round-robin through them:
create(): void {
this.gunshotPool = [];
for (let i = 0; i < 5; i++) {
this.gunshotPool.push(this.sound.add('sfx-gunshot', { volume: 0.8 }));
}
this.poolIndex = 0;
}
private fireGunshot(): void {
const snd = this.gunshotPool[this.poolIndex];
if (snd.isPlaying) snd.stop();
snd.play();
this.poolIndex = (this.poolIndex + 1) % this.gunshotPool.length;
}
Pool size guideline: match the maximum overlapping instances you expect. For footsteps, 3–4 is usually sufficient.
Audio Sprites
Audio sprites pack multiple short sounds into a single audio file with a JSON marker file. This reduces HTTP requests and is ideal for mobile where audio loading is slow.
Loading
this.load.audioSprite(
'sfx-pack',
'assets/audio/sfx-pack.json',
['assets/audio/sfx-pack.mp3', 'assets/audio/sfx-pack.ogg']
);
Playing
this.sound.playAudioSprite('sfx-pack', 'coin');
this.sound.playAudioSprite('sfx-pack', 'jump', { volume: 0.8 });
this.sound.playAudioSprite('sfx-pack', 'hurt', { rate: 1.2 });
JSON Format
{
"resources": ["sfx-pack.mp3", "sfx-pack.ogg"],
"spritemap": {
"coin": { "start": 0.0, "end": 0.4, "loop": false },
"jump": { "start": 0.5, "end": 0.85, "loop": false },
"hurt": { "start": 1.0, "end": 1.6, "loop": false },
"music": { "start": 2.0, "end": 34.0, "loop": true }
}
}
See references/audio-api.md for the full AudioSprite JSON schema.
Volume Management
this.sound.volume = 0.5;
const vol = this.sound.volume;
music.setVolume(0.4);
const soundVol = music.volume;
this.sound.mute = true;
this.sound.mute = false;
const isMuted = this.sound.mute;
Mute Button Pattern
create(): void {
const muteBtn = this.add.image(750, 30, 'btn-mute').setInteractive();
muteBtn.on('pointerdown', () => {
this.sound.mute = !this.sound.mute;
muteBtn.setTexture(this.sound.mute ? 'btn-unmute' : 'btn-mute');
});
}
Mobile Audio Unlock
Mobile browsers and some desktop browsers block audio playback until the user interacts with the page. This is enforced at the browser level — there is no workaround.
Phaser handles this automatically. It listens for the first pointerdown or keydown event and resumes the AudioContext at that moment. All sounds queued before that point will begin playing immediately after unlock.
Check if audio is locked:
if (this.sound.locked) {
}
this.sound.on(Phaser.Sound.Events.UNLOCKED, () => {
this.bgMusic.play();
});
Best practice for audio-critical games: Show a full-screen "Tap to Start" overlay. When the player taps it, dismiss it. Phaser's internal unlock fires at the same time, so audio starts on the next play() call.
create(): void {
this.bgMusic = this.sound.add('music-main', { loop: true, volume: 0.6 });
if (this.sound.locked) {
const overlay = this.add.rectangle(400, 300, 800, 600, 0x000000, 0.7)
.setInteractive();
const label = this.add.text(400, 300, 'TAP TO START', {
fontSize: '32px', color: '#ffffff',
}).setOrigin(0.5);
this.sound.once(Phaser.Sound.Events.UNLOCKED, () => {
overlay.destroy();
label.destroy();
this.bgMusic.play();
});
} else {
this.bgMusic.play();
}
}
AudioContext Suspension Recovery
Mobile browsers (especially iOS Safari) and some desktop browsers suspend the AudioContext when the tab loses focus, the device sleeps, or the PWA is backgrounded. Unlike the initial autoplay lock, resumption is NOT automatic — Phaser does not restore a suspended context on tab re-focus.
Symptoms: Music stops abruptly when the user switches tabs and returns. No error in console. this.sound.locked is false (context was unlocked previously) but audio is still silent.
Fix — resume the context on visibility change:
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
const mgr = this.sound as Phaser.Sound.WebAudioSoundManager;
if (mgr.context?.state === 'suspended') {
mgr.context.resume();
}
}
});
Alternative — use Phaser's built-in focus event:
this.game.events.on(Phaser.Core.Events.FOCUS, () => {
const mgr = this.sound as Phaser.Sound.WebAudioSoundManager;
if (mgr.context?.state === 'suspended') {
mgr.context.resume();
}
});
Check mgr.context?.state before calling resume() — calling it when already 'running' is a no-op, but calling it when 'closed' throws.
AudioContext states:
| State | Meaning |
|---|
'running' | Audio playing normally |
'suspended' | Paused (tab hidden, device sleep) — call resume() |
'closed' | Permanently closed — create a new game instance |
Crossfading Music Between Scenes
Abrupt music cuts sound amateurish. Fade out the old track in the outgoing scene, fade in the new track in the incoming scene.
shutdown(): void {
if (this.bgMusic?.isPlaying) {
this.tweens.add({
targets: this.bgMusic,
volume: 0,
duration: 500,
onComplete: () => this.bgMusic.stop(),
});
}
}
create(): void {
this.bgMusic = this.sound.add('music-new', { loop: true, volume: 0 });
this.bgMusic.play();
this.tweens.add({
targets: this.bgMusic,
volume: 0.6,
duration: 800,
ease: 'Linear',
});
}
Note: this.tweens can tween any numeric property on any object, including sound.volume. No special audio tween API is needed.
Stopping Sounds on Scene Shutdown
Always clean up audio when a scene shuts down. Otherwise sounds from a previous scene continue playing indefinitely.
this.sound.stopAll();
this.bgMusic.stop();
shutdown(): void {
this.bgMusic?.stop();
}
Use stopAll() only at the top-level game exit or between completely unrelated game states. For scene transitions, stop only the specific sounds the current scene owns.
Additional Resources
Reference Files
references/audio-api.md — Complete SoundManager, BaseSound, and WebAudioSound API reference, all events, SoundConfig fields, AudioSprite JSON schema