Coaxing Sound out of the Browser

Go here to see a demo, or first read through the annotated code:
In most cases, you can get reasonable sound out of a browser - even mobile browsers. However, it is a bit of a challenge and there are caveats, particularly with mobile Safari. This article will explain some things about playing sound in a browser and go over sample code showing two different approaches for loading and playing sound. The first approach uses the generic HTML5 Audio element. The second more capable approach uses a very good JavaScript library named Howler.js.
Before diving into some code, there are few concepts related to making sound with a browser that should be understood, so we'll review them briefly here. At the most basic level, the process of causing a browser to emit sound consists of two steps:
  1. Loading your sound resource(s). Already we arrive at a caveat - there are a few different sound resource file formats and not every browser will play them all. There are a couple of formats that will load and play on almost all browsers: wav and mp3 variants. For more information, follow the link at the bottom of this article to a more extensive Uberiquity Blog article on sound ("Coaxing Sounds out of the Browser").
  2. Playing your sound resource(s). Sadly, there are caveats with playing sounds in different browsers, even after you get past the hurdle of choosing a compatible file format. The issues are numerous, but there is little that cannot be reasonably overcome with just a standard HTML5 Audio coding approach - with the exception of some mobile Safari problems. More on this issue later on in this article and again there is additional information in that same article on sound on the Uberiquity Blog.
The most well-supported method of playing sounds in a browser utilizes the HTML5 Audio element for the both the loading and playing of sounds. You can hard code one or more audio elements with src attribute(s) into the structure of your page or you can dynamically create the audio elements with JavaScript, as is done by the Sounds object below. There is an alternative solution to the HTML5 Audio element - a newer evolving standard being pushed by Google called Web Audio. However, this newer, better technology does not yet enjoy broad support on browser platforms (it's coming though). Howler.js, which will be covered later in this article, supports the use of Web Audio and falls back to the standard HTML5 Audio element methodology if Web Audio is not supported.
The first example that we'll be looking at is a Sounds object that relies on plain HTML5 code and the Audio element. This sounds object is in its own module, contained within an IIFE (Immediately Invoked Function Expression), and is using the revealing module pattern to expose functionality. It is stored in a single global app variable, making it ready for use anywhere within the app with no pollution of the global name space. Take a look at the code below.
APPNS.Sounds = (function () {
  var sounds = {};
  var loadSoundsWasExecuted = false;
  var numberOfSoundsToBeLoaded = 5;
  var numberOfSoundsLoaded = 0;
  var onLoadedCallBack;
  var verbose = false;

  // this fires even if an attmempt is made to load a non-existent sound file
  var soundLoaded = function (soundName) {
    numberOfSoundsLoaded += 1;

    if (verbose === true) {
      console.log(soundName + " loaded.");
    }

    if (numberOfSoundsLoaded === numberOfSoundsToBeLoaded) {
      onLoadedCallBack();
    }
  };

  var loadSounds = function (onAllLoadedCallback) {
    if (loadSoundsWasExecuted === true) {
      return;
    }

    onLoadedCallBack = onAllLoadedCallback;

    sounds["Shore"] = document.createElement("audio");
	sounds["Shore"].setAttribute("src", "sounds/68064__skipjack2001__ocean.mp3");
    sounds["Shore"].addEventListener("canplaythrough", soundLoaded("Shore"), false);

    sounds["1"] = document.createElement("audio");
    sounds["1"].setAttribute("src", "sounds/197013__margo-heston__one-f.mp3");
    sounds["1"].addEventListener("canplaythrough", soundLoaded("1"), false);

    sounds["2"] = document.createElement("audio");
    sounds["2"].setAttribute("src", "sounds/197016__margo-heston__two-f.mp3");
    sounds["2"].addEventListener("canplaythrough", soundLoaded("2"), false);

    sounds["3"] = document.createElement("audio");
    sounds["3"].setAttribute("src", "sounds/197019__margo-heston__three-f.mp3");
    sounds["3"].addEventListener("canplaythrough", soundLoaded("3"), false);

    sounds["Ding"] = document.createElement("audio");
	sounds["Ding"].setAttribute("src", "sounds/dingmodded.mp3");
    sounds["Ding"].addEventListener("canplaythrough", soundLoaded("Ding"), false);

    loadSoundsWasExecuted = true;
  };

  var play = function (name) {
    try {
      sounds[name].play();
    } catch (e) {
      if (console !== undefined) {
        console.log("While trying to play sound " + name + ", an error was encountered: " + e.message);
      }
    }
  };

  var delayedPlay = function (name, delay) {
    setTimeout(function () {play("" + name);}, delay);
  };

  var getSoundNames = function () {
    var soundNames = [];
    for (var prop in sounds) {
      soundNames[soundNames.length] = prop;
    }
    return soundNames;
  };

  return {
           loadSounds: loadSounds,
           play: play,
           delayedPlay: delayedPlay,
           getSoundNames: getSoundNames
         };

}());
      
The gist of the code above is that five different sound files are loaded and methods are provided to play these sounds individually. Each sound is assigned to its own dynamically created Audio element, with the src attribute of that element referencing the actual file containing the sound. Another attribute not shown here, named preload, can be used in some browsers to control if and when sounds are preloaded into memory for fast access when play is requested. The default for the preload attribute is "auto", which is why the code above does not bother to specify the attribute - auto will be used as is. In reality, "auto" is pretty much the only useful attribute anyway - but whether specified or defaulted, it is only considered a request, and browsers may honor it or ignore it.
The code above also adds an event listener for the "canplaythrough" event, which is supposed to fire when enough of the sound is loaded so that it can play without halting for additional loading. Essentially, the canplaythrough event is being treated as a "loaded" event, and this is the proper way to do a check for loading. One caveat should be noted here - there is no error event available for HTML5 audio loading. In fact, if you specify a path to a file that does not exist, the canplaythrough event will still fire, but it will fire almost immediately because there is nothing to be loaded.
A reference to each audio element, and thus each sound, is stored in the sounds object as a property using the associative array syntax, with the property name being a short descriptive name for the sound. This name is the argument value that is passed to the play method, denoting which sound to play. To initiate the preloading of sounds for this Sounds object, the loadSounds method must be called. This method takes one argument, the name of a callback function that the Sounds object will call to inform the invoker of loadSounds that all sound loading has been completed.
The simple Sounds object defined by the above code works fairly well. It can even be used to play more than one sound at a time in most browsers. It does however suffer from some weaknesses. Perhaps the most glaring of these is the fact that this Sounds object expects to be successful at playing whatever sound files you specify - there is no way to specify an alternate file type and no code for falling back to a different sound format if the one first specified is not supported. It is possible to do this, but it is beyond the scope of this article. One of the reasons I left this out of the scope of the article is that the Howler.js library discussed below provides a solution for this problem. If you use Howler.js, you get a lot more functionality than you get with the "hand rolled" solution shown above. Nevertheless, I have used the solution above in multiple projects and it works fine as long as you pick a sound format that covers all of the browser platforms that you wish to support. Unfortunately, there is no universally supported sound format yet. The ones that come closest are mp3 (no Opera support) and wav (missing desktop IE support).
I've mentioned Howler.js repeatedly in this article - we are now going to leave the previous HTML5 Audio based Sounds object behind and discuss a new version of this object, "HSounds", that is based upon the Howler.js library. Before delving into the Howler.js code example below, it will be helpful to briefly cover some of the features of this sound library. I've already mentioned that Howler.js supports the use of Web Audio and falls back to the standard HTML5 Audio element methodology if Web Audio is not supported. Howler.js also provides a solution to the problem of spotty browser support for different formats, as it will let you specify multiple formats (i.e. multiple sound files) for the same sound. As if that weren't enough, Howler.js will also let you employ "sound sprites" (aka "audio sprites") which is especially helpful in mobile Safari.
The example HSound object listed below does in fact employ sound sprites, so it behooves us to look a little more closely at what a sound sprite is and why a developer might wish to use one. The term "sound sprite" or if you prefer, "audio sprite", is a derivative of the image sprite idea employed in game development. Image sprites are often packed together in single files known as sprite sheets. These sprite sheets consist of multiple embedded images, which are in turn used as textures to give game sprites their unique individual appearance. Placing sprites all in one sprite sheet results in better organization, better performance and better memory usage. Similarly, a sound sprite is an embedded sound in a single larger file containing other embedded sounds. You get some of the same benefits using this technique as are gained through the use of image sprites, but that's not the best reason to use them. the biggest reason that sound sprites are worth using is that they help surmount some difficulties experienced in playing sound in Apple's mobile Safari browser. Mobile Safari has the following issues with playing sounds (taken from a somewhat famous article by Remy Sharp on his blog - see link at bottom of page): Using a single sound sprite file with multiple embedded sounds helps provide a work-arounds for these issues. So, now that we have seen some reaons to employ sound/audio sprites, let's take a look at a solution that uses them: The HSound object, courtesy of Howler.js.
APPNS.HSounds = (function () {
  var loadSoundsWasExecuted = false;
  var hSound;

  var loadErrorHandler = function () {
    console.log("An error occurred.  Unable to load sound: cat.m4a");
  };

  var loadSounds = function (onLoadCallback) {
    if (loadSoundsWasExecuted === true) {
      return;
    }

    hSound = new Howl({
      urls: ["sounds/cat.m4a"],
      sprite: {
        meow1: [0, 1100],
        meow2: [1300, 1100],
        whine: [2700, 800],
        purr: [5000, 5000]
      },
      onload: onLoadCallback,

      // does not fire if a specifed sound file does not exist,
      // but onLoadCallback will not fire in this case
      onloaderror: loadErrorHandler
    });

    loadSoundsWasExecuted = true;
  };

  var play = function (name) {
    try {
      hSound.play(name);
    } catch (e) {
      if (console !== undefined) {
        console.log("While trying to play sound " + name + ", an error was encountered: " + e.message);
      }
    }
  };

  var delayedPlay = function (name, delay) {
    setTimeout(function () {play("" + name);}, delay);
  };

  var getSoundNames = function () {
    var soundNames = [];
    for (var prop in hSound.sprite()) {
      soundNames[soundNames.length] = prop;
    }
    return soundNames;
  };

  return {
           loadSounds: loadSounds,
           play: play,
           delayedPlay: delayedPlay,
           getSoundNames: getSoundNames
         };

}());
      
In this example, there are four sounds embedded in one file. This file is borrowed from a JSFiddle example created by Aaron Gloege (see link at bottom of page). Note that for each sound, a starting point with the file and duration a duration (both in milliseconds) are specified. When Howler.js plays one of these sounds, it seeks to the proper point in the file and plays it for the specified duration. It is also worth noting that each embedded sound is surrounded by a small amount of silence (generally a second or two) which allows for some leeway in the precision of the audio playing mechanism.
Despite the major internal differences, the API for the HSounds object is pretty much the same as the Sounds object API. So I'll not repeat a description of that. This article is drawing to a close, but I would be remiss if I did not emphasize that Howler.js can also play solo sounds, just like the Sounds object. However, as mentioned, the Howler.js capabilites are broader than the Sounds object in that multiple sound files (i.e. multiple formats) can be specified for the same sound. Additionally, there are many other features of Howler.js that are not provided by the example Sounds object. I highly recommend that you check out the Howler.js home page using the link below.

You can see this code in action here.

An expanded version of this article is available on the Uberiquity Blog, here.


Resources cited in this article:



Attributions (for sound files other than those used by Mr. Gloege):