Javascript Game Foundations - Sound
Mon, Dec 9, 2013Ten Essential Foundations of Javascript Game Development
- A Web Server and a Module Strategy
- Loading Assets
- The Game Loop
- Player Input
- Math
- DOM
- Rendering
- Sound
- State Management
- Juiciness
Sound
In the early stages of making a new game, the sound requirements are easily forgotten, and I’m particularly guilt of this. But sound is an important component of a video game, whether it’s the ambient background music, dynamic music tracks, or interactive sound effects, it all plays a critical role in the game players experience.
To play music, or a sound effect we use the HTML5 <audio>
element:
- HTML5 Doctor - Native Audio in the Browser and HTML5 Audio state of play
- Mozilla - Using HTML5 Audio and Video and The Audio element
- W3C Spec - The Audio element
- Opera - Everything you need to know about HTML5 Video and Audio
- IEBlog - Unlocking the power of HTML5 Audio
- Article - Native Audio in the browser
- Great chapter on AUDIO in the HTML5 Canvas book
- Can I Use - Audio element
If our game has serious interactive sound requirements (e.g. we need to filter or synthesize audio at run-time instead of just playing back pre-recorded files), then we can use the more advanced HTML5 Web Audio API:
- HTML5 Rocks - Getting started with Web Audio API
- Creative JS - Web Audio API - getting started
- Mozilla - Web Audio API
- W3C - Web Audio API Spec
- Can I Use - Web Audio API
The remainder of this article will focus on the former HTML5 Audio approach.
Browser Support
We can detect if the browser supports <audio>
by verifying it responds to the canPlayType
method:
function hasAudio() {
var audio = document.createElement('audio');
return audio && audio.canPlayType;
}
The canPlayType
method is a bit quirky, but we can use it to further detect which formats are
supported:
function hasAudio() {
var audio = document.createElement('audio');
if (audio && audio.canPlayType) {
var ogg = audio.canPlayType('audio/ogg; codecs="vorbis"'),
mp3 = audio.canPlayType('audio/mpeg;'),
wav = audio.canPlayType('audio/wav; codecs="1"');
return {
ogg: (ogg === 'probably') || (ogg === 'maybe'),
mp3: (mp3 === 'probably') || (mp3 === 'maybe'),
wav: (wav === 'probably') || (wav === 'maybe')
};
}
return false;
}
Playing an <AUDIO>
Element
We can create a single <audio>
element in the same way we might create an <img>
element.
function createAudio(src, options) {
var audio = document.createElement('audio');
audio.volume = options.volume || 0.5;
audio.loop = options.loop;
audio.src = src;
return audio;
}
var zap = createAudio('sounds/zap.mp3');
Once created, we can play it on demand:
zap.play();
Waiting to Play
The <audio>
element downloads its src
asynchronously, so if we try to play()
it
immediately after creation then we have a race condition. If we’re lucky it will have finished
downloading and play, if it hasn’t finished loading then, at best, it will do nothing, at
worst it might throw a javascript error.
The solution is very similar to waiting for the onload
event when loading an <img>
tag. The
<audio>
element has a canplay
event that needs to fire before trying to play it:
function createAudio(src, options, canplay) {
var audio = document.createElement('audio');
audio.addEventListener('canplay', canplay, false);
audio.volume = options.volume || 0.5;
audio.loop = options.loop;
audio.src = src;
return audio;
}
var zap = createAudio('sounds/zap.mp3', { volume: 1.0 }, function() {
// ready for zap.play()
});
Looking back at an earlier article, Loading Assets,
we can see that we quietly skipped over how to load audio elements, but the principal is the
same, we would add a loadSounds
helper that loads multiple audio elements and performs a callback
only when all of them have completed.
function loadSounds(names, callback) {
var n,name,
result = {},
count = names.length,
canplay = function() { if (--count == 0) callback(result); };
for(n = 0 ; n < names.length ; n++) {
name = names[n];
result[name] = document.createElement('audio');
result[name].addEventListener('canplay', canplay, false);
result[name].src = "sounds/" + name + ".mp3";
}
}
var SOUNDS = ['zap', 'pow', 'boom'];
function run(sounds) {
// game loop goes here, during which we can...
sounds.zap.play();
sounds.pow.play();
sounds.boom.play();
}
loadSounds(SOUNDS, run);
It’s a fairly easy next step to merge both the loadImages
and loadSounds
methods into a single
loadResources
function that makes a single callback only when all images and all sounds are
ready.
Pooling Audio Elements
If we need to play the same sound effect multiple, overlapping, times (e.g gunshots or explosions)
then we need multiple <audio>
elements for that src
. Instead of creating a single element, we
should create a pool of elements and provide a helper method to play()
the next available
element from the pool.
The AudioFX Javascript Library
To wrap both the simple, and the pooled use cases of the <audio>
element, I created a small
javascript library called AudioFx, you can read more about it here:
Other Libraries
In addition, you can find a number of other 3rd party libraries that help wrap the HTML5 <audio>
element, including: