“Bubbles” canvas tutorial part 4: Getting noisy

HTML5 Bubbles 5

Now we’ve got graphics, animation and interactivity, what’s left? In this final part of my HTML5 javascript/canvas tutorial we finish off our little game by adding some sound effects – what good is popping bubbles without the satisfying sounds of destruction? Unfortunately, adding audio is far trickier than it has any rights to be, so hopefully this tutorial will save at least one person the troubles I had to go through.

If a bubble pops but nobody hears it…

In theory, javascript audio is trivial.

var sound = new Audio("sounds/fx.wav");
sound.play();

And indeed if you do that on a desktop it works precisely as advertised and if that was all you’re targeting you can stop right there and call it a day. On my first attempt I was very pleased with the fact that my first solution worked perfectly on Chrome, but when I loaded it up on my iPad to hand to my son to play with all I got were error messages, sounds that would play when they felt like it, and multi-second delays between touch and audio.

Initial research suggested that this was a known issue with iOS, and that sounds could only be played in direct response to user input. I tried rewriting so the touch function would return a boolean saying if a sound should be played and doing it from there, but still no improvement.

Eventually I found someone who said that Audio isn’t actually intended to be used in this way, in the same way that Image elements aren’t for drawing sprites in games. Back to the drawing board. The alternative is the Web Audio API, and specifically Boris Smus’ superb introductory guide.

Now we’re poppin’

There are two parts to this. First, we preload all the sounds – they need to come from the server to the client so javascript can deal with them. Then we need a system for playing them back.

Boris’ code has a single class that takes a list of files and returns a list of sounds than can be played. That’s almost what we need, but…

I have two sets of sounds. One group is single “pops” – three wave files for a bubble popping. The second group is for multiple “pops”, wave files that sound like several bubbles popping nearly at the same time. Therefore if you pop one bubble we choose a sound from one list, or if you pop multiple bubbles at once we choose from the second list.

Let’s see some code. As always we lay the groundwork first:

	function FileSet(id, files) {
		this.id = id;
		this.files = files;
	}

This simple class defines a group of filenames associated with an ID. With this in hand we amend our initialisation code as follows:

	pub.init = function() {
		//..cut out non-audio related stuff...

		var fileSets = [ 
			new FileSet("single", ["sounds/pop1.wav", "sounds/pop2.wav", "sounds/pop3.wav"] ),
			new FileSet("multiple", ["sounds/pops1.wav", "sounds/pops2.wav", "sounds/pops3.wav"] )
		];
		audioManager = new AudioManager(fileSets, preloadComplete());
	}

	function preloadComplete() {
		window.requestAnimationFrame(update);
	}

What we have now is a bit of code that will go away and load those two groups of sound files and call our preloadComplete() function when it’s done. If you’re loading a lot you’d want a progress bar or splash screen playing in the foreground while this happens but since my sounds total only 250KB I’m not really concerned with that here. Besides, when creating apps or web pages for toddlers loading screens mean delayed gratification, loss of interest and basically failure. Instant-on is where it’s at with toddlers.

For playback later on we have:

var sound = audioManager.getRandom("single"); //or "multiple", depending...
sound.start();

Again, there’s that AudioManager class. We should probably add that. It’s very similar to Boris’ sample code, but modified to handle loading several groups of files instead of just one. So how do we handle that? We give it a collection of FileSet objects, and it creates a collection of FxGroup objects that handle the actual loading. The FxGroup does the loading work we saw in Boris’ guide:

	function FxGroup(fileSet) {
		this.id = fileSet.id;
		this.audioList = [];
		this.audioContext = null;
		this.files = fileSet.files;
	}

	FxGroup.prototype.load = function(callback) {
		this.onComplete = callback;
		console.log("Loading FXGroup '" + this.id + "'");
		this.audioContext = new AudioContext();
		
		var fxLoader = this;
		
		this.bufferLoader = new BufferLoader(
			this.audioContext,
			this.files,
			function(bufferList) {
				console.log("FxGroup '" + fxLoader.id + "' loaded");
				fxLoader.audioList = bufferList;
				fxLoader.onComplete();
			}
		);
		this.bufferLoader.load();
	}

Each FxGroup is created with a single fileset, and then when we want to load it we call load with a callback. One very important note is shown on the highlighted lines. To pass a callback in this situation we need to deal with the confusing way javascript handles “this” when functions are passed around. I’d explain it but it would be hand-wavy and confusing. Suffice to say it works.

Our AudioManager class therefore looks like this:

	function AudioManager(fileSets, callback) {
		window.AudioContext = window.AudioContext || window.webkitAudioContext;
		this.remaining = fileSets.length;
		this.onComplete = callback;
		this.fxGroups = [];
		for(var index = 0; index < fileSets.length; index++) {
			var group = new FxGroup(fileSets[index]);
			this.fxGroups[fileSets[index].id] = group;
			this.fxGroups[fileSets[index].id].load(
				function() {
					this.remaining --;
					if (this.remaining == 0)
						this.onComplete();
				});
		}
	}

Each time it sets a new set of files loading it increments a counter, and when a set is loaded the counter decreases. Once the counter reaches zero again the ultimate callback function is invoked and the game starts.

All work and no play…

Now we can load sounds, now we need to play them. This means adding some new methods to our FxGroup and AudioManager classes. We don’t want need specific sounds, just a random sound from the appropriate group, to stop the noise getting quite so monotonous:

	AudioManager.prototype.getRandom = function(id) {
		return this.fxGroups[id].getRandom();
	}

	FxGroup.prototype.getRandom = function() {
		var index = Math.floor(Math.random() * this.audioList.length);
		var source = this.audioContext.createBufferSource();
		source.buffer = this.audioList[index];
		source.connect(this.audioContext.destination);
		return source;
	}

And then as seen above we just call start() on whatever gets returned from the AudioManager.

Sounds good

And that’s it. All it took was one amazingly useful piece of sample code that could be easily updated to meet our needs. Given how much truble I went through to get this far and solve the problem of audio playback, I sincerely hope that this tutorial saves someone else the effort.

Since this is the state of my game, there’s no “code so far” link this time. Instead, here’s the current version of the game itself: Gabi Bubbles.


Leave a Reply

Post Navigation