Sunday 16 June 2013

MP3 to base64 encoder and decoder


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>