added recorder script tha also uploads the audio files
parent
103945c341
commit
031e5a23bf
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 icatcher.at
|
||||
|
||||
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:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
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.
|
@ -0,0 +1,102 @@
|
||||
# MP3RecorderJS #
|
||||
|
||||
Record MP3 (and WAV) files in the browser using JavaScript and HTML.
|
||||
|
||||
## General ##
|
||||
|
||||
### Why? ###
|
||||
|
||||
The whole project got kicked off by using [Recordmp3js](https://github.com/nusofthq/Recordmp3js) - discovered in [this wonderful article by Remus](http://nusofthq.com/blog/recording-mp3-using-only-html5-and-javascript-recordmp3-js/) -
|
||||
and discovering that my needs were not entirely met. I needed the possibility to have multiple recorders on one site. Also, since the original code was being altered to only reflect the MP3 changes and changing
|
||||
it from stereo to mono, I had the feeling that a lot of unused code has been left in there and I found it difficult to actually see what's going on.
|
||||
|
||||
### Fork? ###
|
||||
|
||||
My first idea was to fork the original project, but I soon discovered that I'm going more for a rewrite than a fork. Hence I ended up writing it anew in a different style.
|
||||
|
||||
## Requirements ##
|
||||
|
||||
* [jQuery (>= v1.11.1)](http://jquery.com/)
|
||||
* [libmp3lame.js](https://github.com/akrennmair/libmp3lame-js)
|
||||
* A browser that supports `navigator.getUserMedia`
|
||||
* [WC3 specification](http://dev.w3.org/2011/webrtc/editor/getusermedia.html)
|
||||
* [Supported Browsers](http://caniuse.com/#search=getUserMedia)
|
||||
|
||||
For easy use `jQuery` and `libmp3lame.js` are included in this project.
|
||||
|
||||
## Usage ##
|
||||
|
||||
### Creation ###
|
||||
|
||||
// create an audio context
|
||||
var audio_context = new AudioContext;
|
||||
|
||||
// tell the browser you want to get some audio user media
|
||||
navigator.getUserMedia({audio: true}, function(stream) {
|
||||
// create an MP3Recorder object supplying the audio context and the stream
|
||||
var recorderObject = new MP3Recorder(audio_context, stream);
|
||||
}, function(e) {
|
||||
// some error occured
|
||||
});
|
||||
|
||||
### Start recording ###
|
||||
|
||||
On a given `MP3Recorder` object you can simply call `start()` to start recording.
|
||||
|
||||
recorderObject.start();
|
||||
|
||||
### Stop recording ###
|
||||
|
||||
On a given `MP3Recorder` object you can simply call `stop()` to stop recording.
|
||||
|
||||
recorderObject.stop();
|
||||
|
||||
### Retrieving recorded data ###
|
||||
|
||||
On a given `MP3Recorder` object you can call 3 methods to get the recorded data, depending on which type you need.
|
||||
|
||||
#### As Blob data ####
|
||||
|
||||
recorderObject.exportBlob(function(blobData) {
|
||||
// blobData is a Blob object
|
||||
});
|
||||
|
||||
#### As WAV data ####
|
||||
|
||||
recorderObject.exportWAV(function(wavData) {
|
||||
// wavData is a base64 encoded Uint8Array
|
||||
});
|
||||
|
||||
#### As MP3 data ####
|
||||
|
||||
recorderObject.exportMP3(function(mp3Data) {
|
||||
// mp3Data is a base64 encoded Uint8Array
|
||||
});
|
||||
|
||||
### Logging ###
|
||||
|
||||
If you create the `MP3Recorder` object with a third parameter you can specify a container and a method to log to.
|
||||
|
||||
var recorderObject = new MP3Recorder(audio_context, stream,
|
||||
{ statusContainer: $('#status'), statusMethod: 'replace' }
|
||||
);
|
||||
|
||||
* `statusContainer` must be a jQuery object that responds to the [`text()`](http://api.jquery.com/text/) function.
|
||||
* `statusMethod` can be `'append'` to append the status text or anything else to replace it.
|
||||
|
||||
## Example ##
|
||||
|
||||
For a complete example, using multiple recorders on a page, see the `index.html` file.
|
||||
|
||||
## Known issues ##
|
||||
|
||||
As mentioned in [the article by Remus](http://nusofthq.com/blog/recording-mp3-using-only-html5-and-javascript-recordmp3-js/) the resulting mp3 recording will be longer by approximately 50%,
|
||||
which is an issue of the lame library that's being used.
|
||||
|
||||
A possible fix for this is mentioned [by Nicholas in the comment section](https://nusofthq.com/blog/recording-mp3-using-only-html5-and-javascript-recordmp3-js/#comment-674).
|
||||
|
||||
|
||||
## Disclaimer ##
|
||||
|
||||
For the purpose of this project, [libmp3lame.js](https://github.com/akrennmair/libmp3lame-js) was used which was not developed by me.
|
||||
Using LAME in your project may result in requiring a special patent license for your country. For more information see the [LAME project site](http://lame.sourceforge.net/links.php#Patents).
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,41 @@
|
||||
importScripts('libmp3lame.min.js');
|
||||
|
||||
var mp3codec;
|
||||
|
||||
this.addEventListener('message', function(e) {
|
||||
switch (e.data.cmd) {
|
||||
case 'init':
|
||||
if (!e.data.config) {
|
||||
e.data.config = { };
|
||||
}
|
||||
mp3codec = Lame.init();
|
||||
|
||||
Lame.set_mode(mp3codec, e.data.config.mode || Lame.JOINT_STEREO);
|
||||
Lame.set_num_channels(mp3codec, e.data.config.channels || 2);
|
||||
Lame.set_num_samples(mp3codec, e.data.config.samples || -1);
|
||||
Lame.set_in_samplerate(mp3codec, e.data.config.samplerate || 44100);
|
||||
Lame.set_out_samplerate(mp3codec, e.data.config.samplerate || 44100);
|
||||
Lame.set_bitrate(mp3codec, e.data.config.bitrate || 128);
|
||||
|
||||
Lame.init_params(mp3codec);
|
||||
// console.log('Version :', Lame.get_version() + ' / ',
|
||||
// 'Mode: '+Lame.get_mode(mp3codec) + ' / ',
|
||||
// 'Samples: '+Lame.get_num_samples(mp3codec) + ' / ',
|
||||
// 'Channels: '+Lame.get_num_channels(mp3codec) + ' / ',
|
||||
// 'Input Samplate: '+ Lame.get_in_samplerate(mp3codec) + ' / ',
|
||||
// 'Output Samplate: '+ Lame.get_in_samplerate(mp3codec) + ' / ',
|
||||
// 'Bitlate :' +Lame.get_bitrate(mp3codec) + ' / ');
|
||||
// 'VBR :' + Lame.get_VBR(mp3codec));
|
||||
break;
|
||||
case 'encode':
|
||||
var mp3data = Lame.encode_buffer_ieee_float(mp3codec, e.data.buf, e.data.buf);
|
||||
self.postMessage({cmd: 'data', buffer: mp3data.data});
|
||||
break;
|
||||
case 'finish':
|
||||
var mp3data = Lame.encode_flush(mp3codec);
|
||||
self.postMessage({cmd: 'end', buffer: mp3data.data});
|
||||
Lame.close(mp3codec);
|
||||
mp3codec = null;
|
||||
break;
|
||||
}
|
||||
});
|
@ -0,0 +1,157 @@
|
||||
(function(window){
|
||||
|
||||
var RECORDER_WORKER_PATH = 'js/recorderWorker.js';
|
||||
var ENCODER_WORKER_PATH = 'js/mp3Worker.js';
|
||||
|
||||
|
||||
var MP3Recorder = function(context, stream, cfg) {
|
||||
var config = cfg || { statusContainer: null, statusMethod: 'append' }
|
||||
|
||||
var bufferLen = 4096;
|
||||
var recording = false;
|
||||
|
||||
this.source = context.createMediaStreamSource(stream);
|
||||
this.node = (context.createScriptProcessor || context.createJavaScriptNode).call(context, bufferLen, 1, 1);
|
||||
|
||||
var recorderWorker = new Worker(RECORDER_WORKER_PATH);
|
||||
var encoderWorker = new Worker(ENCODER_WORKER_PATH);
|
||||
var exportCallback;
|
||||
|
||||
|
||||
// initialize the Recorder Worker
|
||||
recorderWorker.postMessage({ cmd: 'init', sampleRate: context.sampleRate });
|
||||
|
||||
// the recording loop
|
||||
this.node.onaudioprocess = function(e) {
|
||||
if(!recording) return;
|
||||
recorderWorker.postMessage({ cmd: 'record', buffer: e.inputBuffer.getChannelData(0) });
|
||||
}
|
||||
|
||||
|
||||
this.start = function() {
|
||||
recording = true;
|
||||
this.logStatus('recording...');
|
||||
}
|
||||
this.stop = function() {
|
||||
recording = false;
|
||||
this.logStatus('stopping...');
|
||||
}
|
||||
this.destroy = function() { recorderWorker.postMessage({ cmd: 'destroy' }); }
|
||||
|
||||
this.logStatus = function(status) {
|
||||
if(config.statusContainer) {
|
||||
if(config.statusMethod == 'append') {
|
||||
config.statusContainer.text(config.statusContainer.text + "\n" + status);
|
||||
} else {
|
||||
config.statusContainer.text(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.exportBlob = function(cb) {
|
||||
exportCallback = cb;
|
||||
if (!exportCallback) throw new Error('Callback not set');
|
||||
recorderWorker.postMessage({ cmd: 'exportBlob' });
|
||||
}
|
||||
|
||||
this.exportWAV = function(cb) {
|
||||
// export the blob from the worker
|
||||
this.exportBlob(function(blob) {
|
||||
var fileReader = new FileReader();
|
||||
|
||||
// read the blob as array buffer and convert it
|
||||
// to a base64 encoded WAV buffer
|
||||
fileReader.addEventListener("loadend", function() {
|
||||
var resultBuffer = new Uint8Array(this.result);
|
||||
cb(encode64(resultBuffer));
|
||||
});
|
||||
fileReader.readAsArrayBuffer(blob);
|
||||
});
|
||||
}
|
||||
|
||||
this.exportMP3 = function(cb) {
|
||||
this.logStatus('converting...');
|
||||
|
||||
// export the blob from the worker
|
||||
this.exportBlob(function(blob) {
|
||||
var fileReader = new FileReader();
|
||||
|
||||
fileReader.addEventListener("loadend", function() {
|
||||
var wavBuffer = new Uint8Array(this.result);
|
||||
var wavData = parseWav(wavBuffer);
|
||||
|
||||
encoderWorker.addEventListener('message', function(e) {
|
||||
if (e.data.cmd == 'data') {
|
||||
cb(encode64(e.data.buffer));
|
||||
}
|
||||
});
|
||||
|
||||
encoderWorker.postMessage({ cmd: 'init', config: { mode: 3, channels: 1, samplerate: wavData.sampleRate, bitrate: wavData.bitsPerSample } });
|
||||
encoderWorker.postMessage({ cmd: 'encode', buf: Uint8ArrayToFloat32Array(wavData.samples) });
|
||||
encoderWorker.postMessage({ cmd: 'finish' });
|
||||
});
|
||||
|
||||
fileReader.readAsArrayBuffer(blob);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// event listener for return values of the recorderWorker
|
||||
recorderWorker.addEventListener('message', function(e) {
|
||||
switch(e.data.from) {
|
||||
case 'exportBlob':
|
||||
exportCallback(e.data.blob);
|
||||
break;
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
// HELPER FUNCTIONS
|
||||
|
||||
function encode64(buffer) {
|
||||
var binary = '';
|
||||
var bytes = new Uint8Array(buffer);
|
||||
var len = bytes.byteLength;
|
||||
|
||||
for(var i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return window.btoa(binary);
|
||||
}
|
||||
|
||||
function parseWav(wav) {
|
||||
function readInt(i, bytes) {
|
||||
var ret = 0, shft = 0;
|
||||
|
||||
while(bytes) {
|
||||
ret += wav[i] << shft; shft += 8;
|
||||
i++; bytes--;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
if(readInt(20, 2) != 1) throw 'Invalid compression code, not PCM';
|
||||
if(readInt(22, 2) != 1) throw 'Invalid number of channels, not 1';
|
||||
|
||||
return { sampleRate: readInt(24, 4), bitsPerSample: readInt(34, 2), samples: wav.subarray(44) };
|
||||
}
|
||||
|
||||
function Uint8ArrayToFloat32Array(u8a){
|
||||
var f32Buffer = new Float32Array(u8a.length);
|
||||
for (var i = 0; i < u8a.length; i++) {
|
||||
var value = u8a[i<<1] + (u8a[(i<<1)+1]<<8);
|
||||
if (value >= 0x8000) value |= ~0x7FFF;
|
||||
f32Buffer[i] = value / 0x8000;
|
||||
}
|
||||
return f32Buffer;
|
||||
}
|
||||
|
||||
|
||||
this.source.connect(this.node);
|
||||
this.node.connect(context.destination); // this should not be necessary
|
||||
}
|
||||
|
||||
window.MP3Recorder = MP3Recorder;
|
||||
|
||||
})(window);
|
@ -0,0 +1,130 @@
|
||||
var recordingLength = 0,
|
||||
recordingBuffer = [],
|
||||
bits = 16,
|
||||
sampleRate = 0;
|
||||
|
||||
|
||||
this.addEventListener('message', function(e) {
|
||||
switch (e.data.cmd) {
|
||||
case 'init':
|
||||
init(e.data.sampleRate);
|
||||
break;
|
||||
case 'start':
|
||||
start();
|
||||
break;
|
||||
case 'stop':
|
||||
stop();
|
||||
break;
|
||||
case 'destroy':
|
||||
destroy();
|
||||
break;
|
||||
|
||||
case 'record':
|
||||
record(e.data.buffer);
|
||||
break;
|
||||
|
||||
case 'exportBlob':
|
||||
exportBlob();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
function init(sr) {
|
||||
sampleRate = sr;
|
||||
}
|
||||
|
||||
function start() {
|
||||
|
||||
}
|
||||
|
||||
function stop() {
|
||||
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
recordingLength = 0;
|
||||
recordingBuffer = [];
|
||||
sampleRate = 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function record(buffer) {
|
||||
recordingBuffer.push(buffer);
|
||||
recordingLength += buffer.length;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function exportBlob() {
|
||||
var audioBlob = new Blob([encodeWAV(mergeBuffer(recordingBuffer, recordingLength))]);
|
||||
this.postMessage({ from: 'exportBlob', blob: audioBlob });
|
||||
}
|
||||
|
||||
|
||||
|
||||
// HELPER FUNCTIONS
|
||||
|
||||
function mergeBuffer(buf, len){
|
||||
var result = new Float32Array(len);
|
||||
var offset = 0;
|
||||
for (var i = 0; i < buf.length; i++){
|
||||
result.set(buf[i], offset);
|
||||
offset += buf[i].length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function floatTo16BitPCM(output, offset, input){
|
||||
for (var i = 0; i < input.length; i++, offset+=2){
|
||||
var s = Math.max(-1, Math.min(1, input[i]));
|
||||
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
|
||||
}
|
||||
}
|
||||
|
||||
function writeString(view, offset, string){
|
||||
for (var i = 0; i < string.length; i++){
|
||||
view.setUint8(offset + i, string.charCodeAt(i));
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
// requires php5
|
||||
define('UPLOAD_DIR', '../uploads/');
|
||||
$img = $_POST['wavBase64'];
|
||||
$img = str_replace('data:audio/wav;base64,', '', $img);
|
||||
$img = str_replace(' ', '+', $img);
|
||||
$data = base64_decode($img);
|
||||
$file = UPLOAD_DIR . uniqid() . '.wav';
|
||||
$success = file_put_contents($file, $data);
|
||||
print $success ? $file : 'Unable to save the file.';
|
||||
|
||||
?>
|
@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<style type="text/css">
|
||||
body {font-family: "Old Standard TT"; font-size: 20px; line-height: 1.4; letter-spacing: 1px;}
|
||||
h2 {
|
||||
text-align: center;
|
||||
letter-spacing: 4px;
|
||||
font-size: 28px;}
|
||||
section {margin-bottom: 100px; margin-top: 50px;}
|
||||
table, th, td {vertical-align: top; border-collapse: separate; padding: 6px;}
|
||||
button {width: 100px;}
|
||||
.short-description{
|
||||
margin-left: 40%;
|
||||
margin-right: 40%;
|
||||
text-align: center;
|
||||
}
|
||||
a {
|
||||
text-decoration:none;
|
||||
}
|
||||
</style>
|
||||
<html>
|
||||
<head>
|
||||
<section>
|
||||
<h2 >Extracts</h2>
|
||||
<div class="short-description">
|
||||
<p>"Nusra considered their voices shameful, a form of nakedness" </p>
|
||||
<p>
|
||||
<a href="../index.html">Utterance</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<img src="texts/fresh1.jpg" width="35%"></img>
|
||||
<img src="texts/fresh2.jpg" width="35%"></img>
|
||||
<img src="texts/carson-extract.jpg" width="28%"></img>
|
||||
<img src="http://pzwiki.wdka.nl/mw-mediadesign/images/1/13/Extracts-reading.jpg" width="100%" style="margin-top: 150px;"></img>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
Binary file not shown.
Loading…
Reference in New Issue