A while ago a friend of mine told me “I am still
not clear on which scenarios make data URIs preferable over linked
files. “
In developing a Cross-Platform HTML5 Offline App I
had to include MP3 files in the offline cache. The problem I ran
into is that some browsers simply refuse to cache audio files and
fail silently, even if they’re explicitly defined in the
cache.manifest. Playing cached audio for offline use on mobile
devices was a challenge that was proved to be mission impossible. I
really had no idea what was causing this but I needed a solution. So
I decided to the adventage of the Web Audio API and base64 encode the
files and serve them up using a data URI.
HTML5 Web Audio API offers developers direct control
over the AudioBuffer and allows us to convert data formats on-the-fly
and feed them directly to the Web Audio API for playback. For
example, in my case I had to encode an MP3 file as a Base64 string
and than play it.
Playing the audio from Base64 string was relatively
straightforward:
<audio controls="controls" autobuffer="autobuffer" autoplay="autoplay"> <source src="data:audio/wav;base64,UklGRhw...dlIDguMAAA"/> </audio>
The only thing left was encoding an Audio File. You
can easily convert an MP3 file to a Base64 string using OpenSSL. But
what about converting MP3 files and testing the output directly in a
browser?
Here's a MP3 Base64 Encoder / Decoder widget. It
outputs a Base64-encoded string representation of your sound file.
After the string is processed it decodes the Base64 into a Uint8Array
typed array and stores it in arrayBuffer. Once this is done the
stored audio data is decoded using Web Audio decodeAudioData()
function. We can now test the audio by clicking play/stop buttons.
Full Demo and Source Code
JS Bin
Check out the source code repository for a complete example of the techniques discussed in this article.
<html> <head> <title>Web Audio API: Converting audio files from and to base64 string</title> </head> <body> <input accept="audio/*" type="file" /> <button disabled="" onclick="playSound()">Start</button> <button disabled="" onclick="stopSound()">Stop</button> <div> This will be the output of a base64 string representation of your sound file.<br /> <textarea cols="100" id="mp3String" rows="10"> </textarea> </div> <script> var context = new window.webkitAudioContext(); var source = null; var audioBuffer = null; // Converts an ArrayBuffer to base64, by converting to string // and then using window.btoa' to base64. var bufferToBase64 = function (buffer) { var bytes = new Uint8Array(buffer); var len = buffer.byteLength; var binary = ""; for (var i = 0; i < len; i++) { binary += String.fromCharCode(bytes[i]); } return window.btoa(binary); }; var base64ToBuffer = function (buffer) { var binary = window.atob(buffer); var buffer = new ArrayBuffer(binary.length); var bytes = new Uint8Array(buffer); for (var i = 0; i < buffer.byteLength; i++) { bytes[i] = binary.charCodeAt(i) & 0xFF; } return buffer; }; function stopSound() { if (source) { source.noteOff(0); } } function playSound() { // source is global so we can call .noteOff() later. source = context.createBufferSource(); source.buffer = audioBuffer; source.loop = false; source.connect(context.destination); source.noteOn(0); // Play immediately. } function initSound(arrayBuffer) { var base64String = bufferToBase64(arrayBuffer); var audioFromString = base64ToBuffer(base64String); document.getElementById("mp3String").value=base64String; context.decodeAudioData(audioFromString, function (buffer) { // audioBuffer is global to reuse the decoded audio later. audioBuffer = buffer; var buttons = document.querySelectorAll('button'); buttons[0].disabled = false; buttons[1].disabled = false; }, function (e) { console.log('Error decoding file', e); }); } // User selects file, read it as an ArrayBuffer and pass to the API. var fileInput = document.querySelector('input[type="file"]'); fileInput.addEventListener('change', function (e) { var reader = new FileReader(); reader.onload = function (e) { initSound(this.result); }; reader.readAsArrayBuffer(this.files[0]); }, false); // Load file from a URL as an ArrayBuffer. // Example: loading via xhr2: loadSoundFile('sounds/test.mp3'); function loadSoundFile(url) { var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.responseType = 'arraybuffer'; xhr.onload = function (e) { initSound(this.response); // this.response is an ArrayBuffer. }; xhr.send(); } </script> </body> </html>
This comment has been removed by the author.
ReplyDeletelol, the spam comments. DUDE, thank you so much for this post. Total game changer.
ReplyDeleteMy web app loads a full piano's worth of audio to each page, and I was struggling with having to make 88 separate http requests. Because of this post, those requests have now been reduced down to zero.
I owe ya!
Cheers,
Matt