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 <audio>
elements in JavaScript:
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 <audio>
element is that it doesn't load the specified source file automatically. Well, at least not in some browsers. But that's ok – with the preload
property or the load()
method we can tell the Browser to always preload our sound file (note that Mobile Safari ignores both).
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 <img>
elements with the same source file, that source file is only loaded once. The same should apply to all other resources.
But guess what? <audio>
is different! Firefox 4, Chrome 11, Safari 5 and IE9 load that sound multiple times from the server. Firefox 3.6 doesn't load the sound at all, because it ignores the preload
property. At least Opera 11 gets it right and only loads that file once. Thanks for keeping me sane Opera.
Second try. Let's create only one <audio>
element, use the load()
method to preload it and clone it 3 times:
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 );
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!