Web Audio Best Practices for Games in Phaser 3

How to get rid of excess warnings and play nice with iOS

by on 7 minute read


If you are making a game with Phaser 3 then you'll have to deal with audio eventually.

Every game needs music and sound effects to show off their best self.

But Web Audio rules and best practices can be confusing for those new to working with audio on the web.

It is not quite as simple as just calling APIs to play audio files like most other platforms.

And once you get Web Audio figured out for the desktop browser… iOS comes along like the second stage of a final boss. 😨

But here's the good news: it can all be solved.

And this article will show you a system that works for both desktop and mobile browsers!

Web Audio Basics

You probably know the basics of Web Audio but we will go over some just in case.

Web Audio is usually started in a suspended state. The browser generally won't allow audio to play without some kind of user interaction indicating that the user wanted to play the game.

This is a good thing. It prevents overeager ads hidden away in a tab you've forgotten about from suddenly starting to play audio while you are on a video call.

Phaser automatically creates an AudioContext when your game starts even though no sounds will be played.

You've probably seen a warning like this:

The AudioContext was not allowed to start. It must be resumed (or created) after a user gesture on the page.

Phaser will automatically resume this context when the game is given focus from user interaction.

Then later, when the player clicks on another window or switches tabs, Phaser will automatically suspend the AudioContext. Upon regaining focus, Phaser will also automatically resume the context.

Things generally work fine at the most basic level. 👏

Detour into Playing Music Immediately

Almost all games on other platforms will start playing background music as soon as it can regardless of whether the player is ready or not.

But you cannot do this with a web game. There's no way around it and if you did find one then rest assured that the browser makers will release a fix to correct you. 😉

The best solution is to design your game so that it receives user input before it is important for audio to be playing.

Some problems should be fixed by design instead of code. This is one of those problems.

Avoiding Multiple AudioContext Warnings

It is normal and expected to see 1 AudioContext was not allowed to start. warning in the Browser Console.

Seeing more than one is also generally okay but it is possible to get rid of them. Some among us are made uncomfortable by too many warnings.

The reason for multiple AudioContext warnings is likely because the game is trying to play audio before the user has interacted with it.

You may have something like this in your Scene's create() method:

1
2
3
4
5
6
7
8
9
create()
{
	this.music =  this.sound.add('music', {
		volume: 0.2,
		loop: true
	})

	this.music.play()
}

Seems innocent enough and everything will likely work fine. The music will play as soon as the user interacts with your game.

But that is the cause for the extra warnings that keep you up at night. 😅

The fix is to listen for the Phaser.Sound.Events.UNLOCKED event before calling this.music.play().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
create()
{
	this.music =  this.sound.add('music', {
		volume: 0.2,
		loop: true
	})

	if (!this.sound.locked)
	{
		// already unlocked so play
		this.music.play()
	}
	else
	{
		// wait for 'unlocked' to fire and then play
		this.sound.once(Phaser.Sound.Events.UNLOCKED, () => {
			this.music.play()
		})
	}
}

Doing it this way should leave you with a nice and tidy Console. 👍

The Difference with iOS

On a desktop browser, clicking on a different window will stop your game's audio. Then clicking back will resume it.

You may find that this is not the case on iOS. 😭

You can tap on your game and music will start. Go to a different tab and music will stop. So far so good. Working like a desktop browser.

But when you go back to the tab with your game and tap it… nothing happens. The music does not resume.

You can log the state of the AudioContext and it will report: suspended. 🤔

If your game doesn't fill the entire browser window then you may notice that tapping on empty space outside of the game will resume audio. You can also tap on the address bar and then cancel or some other menu item and cancel to get the music to resume.

Seems like voodoo doesn't it?

This can be solved with a hack where you get a reference to the AudioContext and then manually resume it on a POINTER_DOWN event.

For a quick fix, this hack might be fine. But there is another approach that's cleaner and more user-friendly.

Pause when Lose Focus

One user-friendly approach is to stop the music and pause the game with a pause screen when the game loses focus.

You can use a modal or an entire Paused Scene that runs when focus is lost.

Detecting when focus is lost is where the challenge comes in.

The standard blur and focus events don't fire on iOS as they do on the desktop as we described above.

Instead, we can use the visibilitychange event with the blur event like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
create()
{
	// previous code...

	this.game.events.on(Phaser.Core.Events.BLUR, () => {
		this.handleLoseFocus()
	})

	document.addEventListener('visibilitychange', () => {
		if (!document.hidden)
		{
			return
		}

		this.handleLoseFocus()
	})

	// NOTE: event listener clean-up is omitted
}

handleLoseFocus()
{
	// TODO
}

This will let us know when our game loses focus on both desktop and mobile browsers.

One potential drawback to this approach is that you will have to manually control starting and stopping music when losing and gaining focus instead of allowing Phaser to handle it.

Phaser won't know when visibilitychange happens and will not be able to automatically resume audio.

You can call the undocumented onBlur() and onFocus() methods of the WebAudioSoundManager but we don't recommend using private APIs.

Instead, you can set pauseOnBlur to false like this:

1
2
3
4
5
6
create()
{
	this.sound.pauseOnBlur = false

	// other code...
}

This means Phaser will no longer try to pause audio on the blur event or resume audio on the focus event.

Instead, we will have to handle it manually like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
handleLoseFocus()
{
	// assuming a Paused scene that has a pause modal
	if (this.scene.isActive('paused'))
	{
		return
	}

	// pause music or stop all sounds
	this.music.pause()

	// Paused Scene will call the onResume callback when ready
	this.scene.run('paused', {
		onResume: () => {
			this.scene.stop('paused')

			// resume music
			this.music.resume()
		}
	})
}

The above example is assuming the existence of a Paused Scene that can be shown when the game loses focus.

The Scene would have a Resume button that calls the onResume callback on click. The callback is passed as data to the Scene.

The concept is to have something like this 👇

Next Steps

Now you have the basics of a system that can manage Web Audio properly across desktop and mobile!

We hope this saves you a bunch of time! 🤗

Be sure to sign up for our newsletter so you don't miss any future Phaser 3 game development tips and techniques!

Drop your email into the box below.

Don't miss any Phaser 3 game development content

Subscribers get exclusive tips & techniques not found on the blog!

Join our newsletter. It's free. We don't spam. Spamming is for jerks.

Phaser 3 web audio audio best practice

Want tips and techniques more suited for you?


You may also like...


Video Guides


Beginner Guides


Articles Recommended For You

State Pattern for Character Movement in Phaser 3

by on

Writing clean and well-organized code is something all game developers aspire to. We want code to be reusable and easy …

7 minute read

Memory Match in Modern Javascript with Phaser 3 - Part 6

by on

If you've got the basics of Phaser 3 in modern JavaScript down then it might be time to try making something a bit more …

8 minute read

Memory Match in Modern Javascript with Phaser 3 - Part 5

by on

If you've got the basics of Phaser 3 in modern JavaScript down then it might be time to try making something a bit more …

6 minute read

Memory Match in Modern Javascript with Phaser 3 - Part 4

by on

If you've got the basics of Phaser 3 in modern JavaScript down then it might be time to try making something a bit more …

7 minute read

Didn't find what you were looking for?


comments powered by Disqus