Javascript Game Foundations - Sound

Mon, Dec 9, 2013

Ten Essential Foundations of Javascript Game Development

  1. A Web Server and a Module Strategy
  2. Loading Assets
  3. The Game Loop
  4. Player Input
  5. Math
  6. DOM
  7. Rendering
  8. Sound
  9. State Management
  10. 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:

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:

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: