Revisting HTML5 Audio

Sat, Sep 17, 2011

In my earlier HTML5 pong game I used the <audio> tag, but the browser support was mixed and it didn’t work consistently. In another game, breakout, I avoided the issue by using the SoundManager2 library to fall back to flash audio when necessary.

Since then, browser support has matured (a little) and I’ve learned that some of my original problems stem from re-using a single <audio> tag to play the same sound multiple, overlapping, times. So for snakes (coming soon - honest!) I switched back to pure HTML5 <audio> and it has been much more successful.

The key is knowing that for short sounds that need to repeat, and might overlap, you must use a pool of <audio> elements instead of trying to play the same one twice at the same time.

Introducing the AudioFX Javascript Library

Given that knowledge, I built a tiny audio-fx.js library that makes it easy to play either single instance music, or pooled instance sound fx:

var music = AudioFX('music', { formats: ['ogg','mp3'], autoplay: true, loop: true });
var fx    = AudioFX('zap',   { formats: ['ogg','mp3'], pool: 10 });

// ... later ...

if (player.shoots(alien))
  fx.play();

How the Library Works

NOTE: This article is about how the audio-fx library works, and assumes you have a basic understanding of the HTML5 <audio> element. If you need a refresher on HTML5 audio you can check out the following resources:

Browser Support

The library starts off with detecting browser support by creating an <audio> element and asking if it canPlayType():

var hasAudio = false, audio = document.createElement('audio'), audioSupported = function(type) { var s = audio.canPlayType(type); return (s === 'probably') || (s === 'maybe'); };
if (audio && audio.canPlayType) {
  hasAudio = {
    ogg: audioSupported('audio/ogg; codecs="vorbis"'),
    mp3: audioSupported('audio/mpeg;'),
    wav: audioSupported('audio/wav; codecs="1"'),
    loop: (typeof audio.loop === 'boolean') // some browsers (FF) dont support loop yet
  }
}

Creating an <AUDIO> Element

The library includes a private helper function to create a single <audio> element:

var create = function(src, options, onload) {

  var audio = document.createElement('audio');

  if (onload) {
    var ready = function() {
      audio.removeEventListener('canplay', ready, false);
      onload();
    }
    audio.addEventListener('canplay', ready, false);
  }

  if (options.loop && !hasAudio.loop)
    audio.addEventListener('ended', function() { audio.currentTime = 0; audio.play(); }, false);

  audio.volume = options.volume || 0.2;
  audio.loop   = options.loop;
  audio.src    = src;

  return audio;
}

Helper Functions

Next come 2 helper functions:

var choose = function(formats) {
  for(var n = 0 ; n < formats.length ; n++)
    if (hasAudio && hasAudio[formats[n]])
      return formats[n];
};

var find = function(audios) {
  var n, audio;
  for(n = 0 ; n < audios.length ; n++) {
    audio = audios[n];
    if (audio.paused || audio.ended)
      return audio;
  }
};

The AudioFX Wrapper

Finally, a wrapper class abstracts away the difference between a single <audio> and a pool of them. It also allows us to hide the raw HTML5 implementation and provide our own simplified API:

var afx = function(src, options, onload) {

  options = options || {};

  var formats = options.formats || [],
      format  = choose(formats),
      pool    = [];

  src = src + (format ? '.' + format : '');

  if (hasAudio) {
    for(var n = 0 ; n < (options.pool || 1) ; n++)
      pool.push(create(src, options, n == 0 ? onload : null));
  }
  else {
    onload();
  }

  return {

    audio: (pool.length == 1 ? pool[0] : pool),

    play: function() {
      var audio = find(pool);
      if (audio)
        audio.play();
    },

    stop: function() {
      var n, audio;
      for(n = 0 ; n < pool.length ; n++) {
        audio = pool[n];
        audio.pause();
        audio.currentTime = 0;
      }
    }
  };

};

Encapsulation

All of the previous code is hidden in a private javascript module that returns the private afx wrapper as the public AudioFX object with a few helpful attributes (version and supported) attached:

AudioFX = function() {

  // ... all of the above code

  afx.version   = '0.0.1';
  afx.supported = hasAudio;

  return afx;

}();

Example Usage

So using it is easy:

if (AudioFX.supported) {

  alert('using AudioFX library version ' + AudioFX.version);

  var music = AudioFX('music', { formats: ['ogg','mp3'], autoplay: true, loop: true });
  var fx    = AudioFX('zap',   { formats: ['ogg','mp3'], pool: 10 });

  // ... later ...

  if (player.shoots(alien))
    fx.play();
}

Caveats

While my HTML5 audio experiments for my (upcoming) snakes game have been fairly successful. Its really only because I am limiting my support to the main modern desktop browsers, IE9, Chrome13, FF5, Opera11.

Safari still has trouble playing short sounds (latency issues) so while the background music is supported the sound FX are not. And don’t get me started on mobile browser support!

So, lets be honest, HTML5 Audio is still not ready for prime time cross browser support, and going with the SoundManager2 library is still a good idea for commercial games. But for personal experiments like a snakes game (coming soon! dont stress me out man!) it might be good enough.

What about the buzz Library ?

After starting this project, I discovered the buzz library which also abstracts HTML5 audio functionality… and does it much more thoroughly than my audio-fx, but does not have support for creating an audio pool, which is very important for short, repeat, overlapping sounds.

… I should probably just be forking buzz and trying to add pooling support to it, but I only discovered that library late in the day, so might have to revisit this issue.

If you have any thoughts let me know !

You can find the audio-fx.js library here.

How to use HTML5 Audio:

Is HTML5 Audio Ready for Prime Time ?

Other Libraries: