PHOBOSLAB

Blog Home

Multiple Channels for HTML5 Audio

Now that I've calmed down a bit after my The State of HTML5 Audio article, let's see if can actually write something productive.

Here's the deal: for a typical game you may have a sound effect that you want to play very often. In Biolab Disaster the sound of your plasma gun is such an effect. This particular sound has a length of 0.6 seconds, but you sure can mash the shoot button quicker than 1.66 times per second – so to not interrupt the plasma sound each time you press the button, but instead play a new sound we need to have multiple sound “channels”.

There are a few different ways to create

var a1 = new Audio();
var a2 = document.createElement( 'audio' );
var a3 = a1.cloneNode( true );

(You could also use .innerHTML or document.write() if you're of the adventurous type.)

An important thing to know about the

So, the most logical way to create our sound channels I can think of is this (let's say we want to have four channels):

var channels = [];
for( var i = 0; i < 4; i++ ) {
    var a = new Audio( 'sound.ogg' );
    a.preload = 'auto';
    channels.push( a );
}

But hold on! Doesn't this result in the Browser requesting that sound file 4 times? Well, it really shouldn't – if I create 100 elements with the same source file, that source file is only loaded once. The same should apply to all other resources.

But guess what?

Second try. Let's create only one

var a = new Audio( 'sound.ogg' );
a.load()

// Add the audio element to the DOM, because otherwise a certain 
// retarded Browser from Redmond will refuse to properly clone it
document.body.appendChild( a );

var channels = [a];
for( var i = 0; i < 3; i++ ) {
    channels.push( a.cloneNode(true) );
}

Result? Firefox 3.6 and Opera 11 load the sound file only once. Firefox 4, Chrome 11, Safari 5 and IE9 still request it multiple times.

Ok, what if we wait till the sound file has been loaded completely and only then clone it? Pfft, fine:

a.addEventListener( 'canplaythrough', function(ev){
    for( var i = 0; i < 3; i++ ) {
        channels.push( a.cloneNode(true) );
    }
}, false );

(Boring test page)

With that, Firefox 3.6, Firefox 4, Opera 11, Chrome 11 and IE9 all load the sound file just once. Note however, that the canplaythrough event is not exactly the right place to clone the audio element. The audio file might not be loaded completely, but just enough to play the whole thing without interrupting, provided that the download rate doesn't change. It's a nice event to have for long sound files (music), but a bit pointless for short samples.

Why didn't I use the onload event? Well, the HTML5 Media Elements define a number of events, but onload is not one of them. There's also no “completed” event ore something like that.

Why didn't I use the progress event and check for e.loaded and e.total? Only Firefox supports those two properties. For other browsers there's the buffered object that specifies a number of “TimeRanges”.

In a quick test though, Chrome only fired the progress event once for my short sound file. And at the time it did that, the buffered object was still empty. If I wanted to check if the file has been loaded completely, I would need to set up an interval and poll for the loaded ranges. Pretty.

At this point I stopped for a moment and remembered why I'm doing this in the first place: Browsers, could you please properly cache audio files? Not you Opera; today I like you.

But there's more: Did you notice that Safari was missing in the list of browsers that only loaded the sound file once for the last example? Did you also notice how I always wrote the file was loaded “multiple times” instead of just saying “4 times” as in "once for each element"?

Here's the reason: for that simple HTML page with one external resource, Safari does not send 2 HTTP requests as one might expect, but 18:

"GET /html5audio/ HTTP/1.1" 200 506
"GET /html5audio/beep.mp3 HTTP/1.1" 206 2
"GET /html5audio/beep.mp3 HTTP/1.1" 206 16744
"GET /html5audio/beep.mp3 HTTP/1.1" 206 2
"GET /html5audio/beep.mp3 HTTP/1.1" 206 16744
"GET /html5audio/beep.mp3 HTTP/1.1" 206 2
"GET /html5audio/beep.mp3 HTTP/1.1" 206 2
"GET /html5audio/beep.mp3 HTTP/1.1" 304 -
"GET /html5audio/beep.mp3 HTTP/1.1" 206 16744
"GET /html5audio/beep.mp3 HTTP/1.1" 206 16744
"GET /html5audio/beep.mp3 HTTP/1.1" 206 2
"GET /html5audio/beep.mp3 HTTP/1.1" 304 -
"GET /html5audio/beep.mp3 HTTP/1.1" 304 -
"GET /html5audio/beep.mp3 HTTP/1.1" 206 16744
"GET /html5audio/beep.mp3 HTTP/1.1" 206 7068
"GET /html5audio/beep.mp3 HTTP/1.1" 206 15620
"GET /html5audio/beep.mp3 HTTP/1.1" 206 5620
"GET /html5audio/beep.mp3 HTTP/1.1" 206 14172

The last number on each line indicates the number of bytes sent. The beep.mp3 file is 17kb in size, but Safari only requested certain byte ranges of it. Now that's clever!

Friday, March 11th 2011
— Dominic Szablewski, @phoboslab