Update with fixes 15.10.2015

The known issues with the longer recordings length and the audio loopback have been fixed. All fixes have been committed to the master branch in GitHub. The live demo has been updated as well to the latest version. Thanks to all who commented bringing up the issues and to all who contributed. Special thanks to kehers, Nicholas, Wellington Soares, Muhammad Tahir, SimZal and Alex Zhukov.

 

With the continuous advancements of HTML 5, audio/video capture using only the browser has reached a turning point where it is possible to record but only on specific browsers.

In this article we will be focusing on audio capture and more specifically on capturing audio from the microphone and encoding it to MP3 using only JavaScript and HTML.

The Name of the Game is getUserMedia()

Using the getUserMedia() API, you can capture raw audio input from the microphone.

We will get to the how soon, but first off you will have to remember that this API is still in development and is not supported by all browsers and there is no standardized version yet. The best support can be found in Chrome, followed by Firefox. For a more detailed look on the history and development of the API you can check the html5rocks article. Also for a thorough reference guide you should check the Mozilla Developer Network article.

The Recorder.js Library and libmp3lame.js Library

The starting point for the whole process of recording mp3 is represented by Matt Diamond’s Recorder.js, a plugin developed in JavaScript for recording/exporting the output of Web Audio API nodes. This library is under MIT license.

Recorder.js implements the capture audio functionality and saves it in wav format using getUserMedia. The problem with wav is that the sound data is not compressed, therefore they take up a lot of space on the disk, just 1 minute of recording can take as much as 10 Megabytes.

The solution to this? Well let’s convert the wav file to mp3. Simply saving the wav file to disk and then converting it server side will not do. We will convert the recording, to mp3, in real time, in the browser.

So how do we do that? An mp3 JavaScript library exists that can do that. It has been ported directly from the most used mp3 encoder, LAME Mp3 Encoder. LAME is a high quality MPEG Audio Layer III (MP3) encoder licensed under the LGPL.

The library’s name is libmp3lame.js and a minified ready to use version can be downloaded from GitHub. The library is licensed under the LAME license terms.

So it looks like we got all the tools we need. Let’s take a look in detail on what we need to do next.

Putting It All Together

1. Making Recorder.js work on Firefox

I started off by modifying the Recorder.js project to my needs. The library in it’s default state works only in Chrome, so i modified that, so that it can capture audio in Firefox as well by changing the window.onload function from index.html like so:

window.onload = function init() {
try {
// webkit shim
window.AudioContext = window.AudioContext || window.webkitAudioContext;
navigator.getUserMedia = ( navigator.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia ||
navigator.msGetUserMedia);
window.URL = window.URL || window.webkitURL;

audio_context = new AudioContext;
__log('Audio context set up.');
__log('navigator.getUserMedia ' + (navigator.getUserMedia ? 'available.' : 'not present!'));
} catch (e) {
alert('No web audio support in this browser!');
}

navigator.getUserMedia({audio: true}, startUserMedia, function(e) {
__log('No live audio input: ' + e);
});
};

2. Changing Recorder.js to Record Mono Wav Files

The recorder.js (i’m talking here about the main javascript file and not the name of the library), expects by default an in input from two data channels, because the initial implementation produced stereo wav files. For our purpose we will need to change that to mono recording, otherwise abnormal mp3 recordings will be produced with low pitched sounds. Also there is no specific need that the recording should be stereo in the first place, by default a normal microphone records in mono.

The first change that has to be made for a mono recording is to the onaudioprocess event function:

this.node.onaudioprocess = function(e){
if (!recording) return;
worker.postMessage({
command: 'record',
buffer: [
e.inputBuffer.getChannelData(0),
//e.inputBuffer.getChannelData(1)
]
});
}

I’ve just commented out the inputBuffer capturing for the second channel.

Next in the actual javascript worker (recorderWorker.js), I’ve made several modifications.

First in the record function, we must only capture the inputBuffer for the first channel, because the seconds won’t exist after our previous modification

function record(inputBuffer){
recBuffersL.push(inputBuffer[0]);
//recBuffersR.push(inputBuffer[1]);
recLength += inputBuffer[0].length;
}

The second change comes to the exportWav method. Continuing the trend, we only need to process one audio channel.

function exportWAV(type){
var bufferL = mergeBuffers(recBuffersL, recLength);
//var bufferR = mergeBuffers(recBuffersR, recLength);
//var interleaved = interleave(bufferL, bufferR);
//var dataview = encodeWAV(interleaved);
var dataview = encodeWAV(bufferL);
var audioBlob = new Blob([dataview], { type: type });

this.postMessage(audioBlob);
}

And finally changes were made to the function that does all the encoding in order for the wav file to be produced (bit by bit). Here several lines have been replaced from stereo to their mono counterparts, exactly which ones are marked in the commented text in the source code:

function encodeWAV(samples){
var buffer = new ArrayBuffer(44 + samples.length * 2);
var view = new DataView(buffer);

/* RIFF identifier */
writeString(view, 0, 'RIFF');
/* file length */
view.setUint32(4, 32 + samples.length * 2, true);
/* RIFF type */
writeString(view, 8, 'WAVE');
/* format chunk identifier */
writeString(view, 12, 'fmt ');
/* format chunk length */
view.setUint32(16, 16, true);
/* sample format (raw) */
view.setUint16(20, 1, true);
/* channel count */
//view.setUint16(22, 2, true); /*STEREO*/
view.setUint16(22, 1, true); /*MONO*/
/* sample rate */
view.setUint32(24, sampleRate, true);
/* byte rate (sample rate * block align) */
//view.setUint32(28, sampleRate * 4, true); /*STEREO*/
view.setUint32(28, sampleRate * 2, true); /*MONO*/
/* block align (channel count * bytes per sample) */
//view.setUint16(32, 4, true); /*STEREO*/
view.setUint16(32, 2, true); /*MONO*/
/* bits per sample */
view.setUint16(34, 16, true);
/* data chunk identifier */
writeString(view, 36, 'data');
/* data chunk length */
view.setUint32(40, samples.length * 2, true);

floatTo16BitPCM(view, 44, samples);

return view;
}

With these changes the Recorder.js library will now produce mono wav files, exactly what we need.

3. Integrating libmp3lame.js With Recorder.js

For my project i used the compiled minified version of libmp3lame.js available in the github project.

The mono wav files produced in step 2 will be returned in blob format. From here we can start doing the real-time conversion from wav to mp3. This process starts in the worker.onmessage event handler function. Using a FileReader, i’ve read the blob as an array buffer: fileReader.readAsArrayBuffer(blob). 

worker.onmessage = function(e){
var blob = e.data;
//console.log("the blob " + blob + " " + blob.size + " " + blob.type);

var arrayBuffer;
var fileReader = new FileReader();

fileReader.onload = function(){
arrayBuffer = this.result;
var buffer = new Uint8Array(arrayBuffer),
data = parseWav(buffer);

console.log(data);
console.log("Converting to Mp3");
log.innerHTML += "n" + "Converting to Mp3";

encoderWorker.postMessage({ cmd: 'init', config:{
mode : 3,
channels:1,
samplerate: data.sampleRate,
bitrate: data.bitsPerSample
}});

encoderWorker.postMessage({ cmd: 'encode', buf: Uint8ArrayToFloat32Array(data.samples) });
encoderWorker.postMessage({ cmd: 'finish'});
encoderWorker.onmessage = function(e) {
if (e.data.cmd == 'data') {

console.log("Done converting to Mp3");
log.innerHTML += "n" + "Done converting to Mp3";

/*var audio = new Audio();
audio.src = 'data:audio/mp3;base64,'+encode64(e.data.buf);
audio.play();*/

//console.log ("The Mp3 data " + e.data.buf);

var mp3Blob = new Blob([new Uint8Array(e.data.buf)], {type: 'audio/mp3'});
uploadAudio(mp3Blob);

var url = 'data:audio/mp3;base64,'+encode64(e.data.buf);
var li = document.createElement('li');
var au = document.createElement('audio');
var hf = document.createElement('a');

au.controls = true;
au.src = url;
hf.href = url;
hf.download = 'audio_recording_' + new Date().getTime() + '.mp3';
hf.innerHTML = hf.download;
li.appendChild(au);
li.appendChild(hf);
recordingslist.appendChild(li);

}
};
};

fileReader.readAsArrayBuffer(blob);

currCallback(blob);
}

Next, the buffer is parsed by the parseWav function and the encoding process begins using encoderWorker. This worker is initialized by mp3Worker.js which is the javascript file that imports the minified version of libmp3lame.js. Here is where it all comes together, the final product of this worker being a Uint8Array of mp3 data.

Once the encoding is done a new blob object is created from the Uint8Array ready to be downloaded and listened to through a standard HTML audio control. The mp3 is also automatically saved to disk with the help of AJAX and a PHP data writing script.

4. Done

That’s it. We’ve achieved what we’ve set out for. We’ve created mp3 audio recordings directly from a browser using nothing more than JS and HTML.

A live demo is available here ( Chrome and Firefox only ).

The whole code is available to download on GitHub under the Recordmp3.js project (includes the modified Recorder.js).

The modified Recorder.js version  is also available separately as a fork of the original Record.js project here.

Known issues

The resulting mp3 recording will be longer by aproximetely 50%. So a 5 seconds recording will have a 10 seconds duration, with the last 5 seconds being empty. This may be caused by a buffer problem.