Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added MP3 file support #79

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.swp
.DS-Store
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Recorder.js
A plugin for recording/exporting the output of Web Audio API nodes

### Quick Note
Using akrennmair/libmp3lame-js for mp3 support causes conflict in licensing (see the license section below). I'm not sure if this can be merged back to the original or not. I'm also not sure which license override which. Use at your own risk; or if you happen to be a lawyer, please explain this to me? Thanks!

## A plugin for recording/exporting the output of Web Audio API nodes

### Syntax
#### Constructor
Expand All @@ -14,10 +17,10 @@ Creates a recorder instance.
---------
#### Config

- **workerPath** - Path to recorder.js worker script. Defaults to 'js/recorderjs/recorderWorker.js'
- **workerPath** - Path to recorder.js worker script. Defaults to 'js/recorderjs/recorderWorker.js'. For the mp3 output format, you will need to use the 'recorderWorkerMP3.js'.
- **bufferLen** - The length of the buffer that the internal JavaScriptNode uses to capture the audio. Can be tweaked if experiencing performance issues. Defaults to 4096.
- **callback** - A default callback to be used with `exportWAV`.
- **type** - The type of the Blob generated by `exportWAV`. Defaults to 'audio/wav'.
- **callback** - A default callback to be used with `exportAudio`.
- **type** - The type of the Blob generated by `exportAudio`. Defaults to 'audio/wav'.

---------
#### Instance Methods
Expand All @@ -31,7 +34,7 @@ Pretty self-explanatory... **record** will begin capturing audio and **stop** wi

This will clear the recording.

rec.exportWAV([callback][, type])
rec.exportAudio([callback][, type])

This will generate a Blob object containing the recording in WAV format. The callback will be called with the Blob as its sole argument. If a callback is not specified, the default callback (as defined in the config) will be used. If no default has been set, an error will be thrown.

Expand Down Expand Up @@ -65,12 +68,9 @@ This will set the configuration for Recorder by passing in a config object.

This method will force a download using the new anchor link *download* attribute. Filename defaults to 'output.wav'.

## License (MIT)

Copyright © 2013 Matt Diamond

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
## License

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
Original mattdiamond/Recorderjs is licensed under MIT.
akrennmair/libmp3lame-js is licensed under the same license as LAME which is LGPL.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
I'm not sure how this will all works since I don't have a law degree. I wish it's just all WTFPL2.
106 changes: 106 additions & 0 deletions example_simple_exportmp3.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<!DOCTYPE html>

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Live input record and playback</title>
<style type='text/css'>
ul { list-style: none; }
#recordingslist audio { display: block; margin-bottom: 10px; }
</style>
</head>
<body>

<h1>Recorder.js simple MP3 export example</h1>

<p>Make sure you are using a recent version of Google Chrome.</p>
<p>Also before you enable microphone input either plug in headphones or turn the volume down if you want to avoid ear splitting feedback!</p>

<button onclick="startRecording(this);">record</button>
<button onclick="stopRecording(this);" disabled>stop</button>

<h2>Recordings</h2>
<ul id="recordingslist"></ul>

<h2>Log</h2>
<pre id="log"></pre>

<script>
function __log(e, data) {
log.innerHTML += "\n" + e + " " + (data || '');
}

var audio_context;
var recorder;

function startUserMedia(stream) {
var input = audio_context.createMediaStreamSource(stream);
__log('Media stream created.');

input.connect(audio_context.destination);
__log('Input connected to audio context destination.');

recorder = new Recorder(input, {workerPath: 'recorderWorkerMP3.js'});
__log('Recorder initialised.');
}

function startRecording(button) {
recorder && recorder.record();
button.disabled = true;
button.nextElementSibling.disabled = false;
__log('Recording...');
}

function stopRecording(button) {
recorder && recorder.stop();
button.disabled = true;
button.previousElementSibling.disabled = false;
__log('Stopped recording.');

// create WAV download link using audio data blob
createDownloadLink();

recorder.clear();
}

function createDownloadLink() {
recorder && recorder.exportAudio(function(blob) {
var url = URL.createObjectURL(blob);
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 = new Date().toISOString() + '.mp3';
hf.innerHTML = hf.download;
li.appendChild(au);
li.appendChild(hf);
recordingslist.appendChild(li);
});
}

window.onload = function init() {
try {
// webkit shim
window.AudioContext = window.AudioContext || window.webkitAudioContext;
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia;
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);
});
};
</script>

<script src="recorder.js"></script>
</body>
</html>
11 changes: 6 additions & 5 deletions recorder.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
(function(window){

var WORKER_PATH = 'recorderWorker.js';
var WORKER_WAV_PATH = 'recorderWorker.js';
var WORKER_MP3_PATH = 'recorderWorkerMP3.js';

var Recorder = function(source, cfg){
var config = cfg || {};
Expand All @@ -9,7 +10,7 @@
this.node = (this.context.createScriptProcessor ||
this.context.createJavaScriptNode).call(this.context,
bufferLen, 2, 2);
var worker = new Worker(config.workerPath || WORKER_PATH);
var worker = new Worker(config.workerPath || WORKER_WAV_PATH);
worker.postMessage({
command: 'init',
config: {
Expand Down Expand Up @@ -55,17 +56,17 @@
worker.postMessage({ command: 'getBuffer' })
}

this.exportWAV = function(cb, type){
this.exportAudio = function(cb, type) {
currCallback = cb || config.callback;
type = type || config.type || 'audio/wav';
if (!currCallback) throw new Error('Callback not set');
worker.postMessage({
command: 'exportWAV',
command: 'exportAudio',
type: type
});
}

worker.onmessage = function(e){
worker.onmessage = function(e) {
var blob = e.data;
currCallback(blob);
}
Expand Down
6 changes: 3 additions & 3 deletions recorderWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ this.onmessage = function(e){
case 'record':
record(e.data.buffer);
break;
case 'exportWAV':
exportWAV(e.data.type);
case 'exportAudio':
exportAudio(e.data.type);
break;
case 'getBuffer':
getBuffer();
Expand All @@ -33,7 +33,7 @@ function record(inputBuffer){
recLength += inputBuffer[0].length;
}

function exportWAV(type){
function exportAudio(type){
var bufferL = mergeBuffers(recBuffersL, recLength);
var bufferR = mergeBuffers(recBuffersR, recLength);
var interleaved = interleave(bufferL, bufferR);
Expand Down
92 changes: 92 additions & 0 deletions recorderWorkerMP3.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
importScripts('vendor/libmp3lame.js');

var mp3codec,
recBuffers = [],
recLength = 0;

this.onmessage = function(e) {
switch (e.data.command) {
case 'init':
init(e.data.config);
break;
case 'record':
record(e.data.buffer);
break;
case 'exportAudio':
exportAudio();
break;
case 'clear':
clear();
break;
}
};

// The initilization default sample rate of the input audio at 44k
// and scaled it down to 22k output. Not sure why this is necessary but is
// needed for the sound to come out right.
// The channels is set to mono and listenning to channel 1 which is the left channel.
// There is some problem with stereo channel support.
function init(config) {
if (!config) {
config = {};
}

mp3codec = Lame.init();

Lame.set_mode(mp3codec, config.mode || Lame.MONO);
Lame.set_num_channels(mp3codec, config.channels || 1);
Lame.set_out_samplerate(mp3codec, config.sampleRateOut || 22050);
Lame.set_in_samplerate(mp3codec, config.sampleRate || 44100);
Lame.set_bitrate(mp3codec, config.bitrate || 128);

Lame.init_params(mp3codec);
}

function record(buffer) {
var mp3data = Lame.encode_buffer_ieee_float(mp3codec, buffer[0], buffer[0]);
recBuffers.push(mp3data.data);
recLength += mp3data.data.length;
}

// Similar to the original exportWAV, it grabs the mp3 data from Lame encoder
// object and build an mp3 blob with it.
// When done, it posts the audio blob message to the worker.
function exportAudio() {
var mp3data = Lame.encode_flush(mp3codec);

recBuffers.push(mp3data.data);
recLength += mp3data.data.length;

var buffer = mergeBuffers(recBuffers, recLength);
var view = new Uint8Array(buffer);

var audioBlob = new Blob([view], {
type: "audio/mp3"
});

this.postMessage(audioBlob);
}

// Close the Lame encoder object stream, empty out the data in buffer and
// reset the buffer length stored.
function clear() {
Lame.close(mp3codec);
mp3codec = null;

recBuffers = [];
recLength = 0;
}

function mergeBuffers(recBuffers, recLength) {
//Float32Array
var result = new Float32Array(recLength);

var offset = 0;

for (var i = 0; i < recBuffers.length; i++) {
result.set(recBuffers[i], offset);
offset += recBuffers[i].length;
}

return result;
}
Loading