lingua-recorder
Version:
A JavaScript library for easy and efficient voice recording tasks.
335 lines (278 loc) • 8.79 kB
JavaScript
'use strict';
/**
* AudioRecord
*
* @constructor
* @param {number} [sampleRate] Rate at witch the samples added to this object should be played
*/
var AudioRecord = function( sampleRate ) {
this.sampleRate = sampleRate;
this.sampleBlocs = [];
this.length = 0;
};
/**
* Some MIME-type analyzer are just checking if the UTF-8 decoded file
* contains the strings "<?php" or "<\x00?\x00". So by banning 4 samples
* ("?\x00" ; "\x00?" ; "ph" ; "hp"), we get rid of all problems
* see https://phabricator.wikimedia.org/T212584
*/
const BANNED_SAMPLES = [ 0x003F, 0x3F00, 0x6870, 0x7068 ];
/**
* Add some raw samples to the record.
*
* @param {Float32Array} [samples] samples to append to the record
* @param {number} [rollingDuration] if set, last number of seconds of the record to keep after adding the new samples
* @return {number} the new total number of samples stored.
*/
AudioRecord.prototype.push = function( samples, rollingDuration ) {
this.length += samples.length;
this.sampleBlocs.push( samples );
if ( rollingDuration !== undefined ) {
var duration = this.getDuration();
if ( duration > rollingDuration ) {
this.ltrim( duration - rollingDuration );
}
}
return this.length;
};
/**
* Change the sample rate.
*
* @param {number} [value] new sample rate to set.
*/
AudioRecord.prototype.setSampleRate = function( value ) {
this.sampleRate = value;
};
/**
* Get the sample rate in use.
*
* @return {number} Current sample rate for the record.
*/
AudioRecord.prototype.getSampleRate = function() {
return this.sampleRate;
};
/**
* Get the total number of samples in the record.
*
* @return {number} number of samples.
*/
AudioRecord.prototype.getLength = function() {
return this.length;
};
/**
* Get the duration of the record.
*
* This is based on the number of samples and the declared sample rate.
*
* @return {number} duration (in seconds) of the record.
*/
AudioRecord.prototype.getDuration = function() {
return this.length / this.sampleRate;
};
/**
* Get all the raw samples that make up the record.
*
* @return {Float32Array} list of all samples.
*/
AudioRecord.prototype.getSamples = function() {
var flattened = new Float32Array( this.length + 575 ),
nbBlocs = this.sampleBlocs.length,
offset = 0;
for ( var i = 0; i < nbBlocs; ++i ) {
flattened.set( this.sampleBlocs[ i ], offset );
offset += this.sampleBlocs[ i ].length
}
return flattened;
};
/**
* Trim the record, starting with the beginning of the record (the left side).
*
* @param {number} [duration] duration (in seconds) to trim
*/
AudioRecord.prototype.ltrim = function( duration ) {
var nbSamplesToRemove = Math.round( duration * this.sampleRate );
if ( nbSamplesToRemove >= this.length ) {
this.sampleBlocs = [];
return;
}
this.length -= nbSamplesToRemove;
while ( nbSamplesToRemove > 0 && nbSamplesToRemove >= this.sampleBlocs[ 0 ].length ) {
nbSamplesToRemove -= this.sampleBlocs[ 0 ].length;
this.sampleBlocs.shift();
}
if ( nbSamplesToRemove > 0 ) {
this.sampleBlocs[ 0 ] = this.sampleBlocs[ 0 ].subarray( 0, this.sampleBlocs[ 0 ].length - nbSamplesToRemove );
}
};
/**
* Trim the record, starting with the end of the record (the right side).
*
* @param {number} [duration] duration (in seconds) to trim
*/
AudioRecord.prototype.rtrim = function( duration ) {
var nbSamplesToRemove = Math.round( duration * this.sampleRate );
if ( nbSamplesToRemove >= this.length ) {
this.sampleBlocs = [];
return;
}
this.length -= nbSamplesToRemove;
while ( nbSamplesToRemove > 0 && nbSamplesToRemove >= this.sampleBlocs[ this.sampleBlocs.length - 1 ].length ) {
nbSamplesToRemove -= this.sampleBlocs[ this.sampleBlocs.length - 1 ].length;
this.sampleBlocs.pop();
}
if ( nbSamplesToRemove > 0 ) {
var lastIndex = this.sampleBlocs.length - 1;
this.sampleBlocs[ lastIndex ] = this.sampleBlocs[ lastIndex ].subarray( nbSamplesToRemove );
}
};
/**
* Clear the record.
*/
AudioRecord.prototype.clear = function() {
this.length = 0;
this.sampleBlocs = [];
};
/**
* Play the record to the audio output (aka the user's loudspeaker)
*/
AudioRecord.prototype.play = function() {
var audioContext = new (window.AudioContext || window.webkitAudioContext)();
var buffer = audioContext.createBuffer(1, this.length, 48000); //samplerate
var channelData = buffer.getChannelData(0);
var nbBlocs = this.sampleBlocs.length
for ( var i = 0, t = 0; i < nbBlocs; i++ ) {
var nbSamples = this.sampleBlocs[ i ].length;
for ( var j = 0; j < nbSamples; j++ ) {
channelData[t++] = this.sampleBlocs[ i ][ j ];
}
}
var source = audioContext.createBufferSource();
source.buffer = buffer;
source.connect( audioContext.destination );
// Include deprecated noteOn to support old versions of Chrome
if ( source.start === undefined ) {
source.start = source.noteOn;
}
source.start(0);
};
/**
* Get a WAV-encoded Blob version of the record.
*
* @return {Blob} WAV-encoded audio record.
* @alias getWAVE()
*/
AudioRecord.prototype.getBlob = function() {
var sample,
buffer = new ArrayBuffer(44 + this.length * 2),
view = new DataView(buffer),
samples = this.getSamples();
/* RIFF identifier */
writeString(view, 0, 'RIFF');
/* file length */
view.setUint32(4, 32 + this.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, 1, true);
/* sample rate */
view.setUint32(24, this.sampleRate, true);
/* byte rate (sample rate * block align) */
view.setUint32(28, this.sampleRate * 2, true);
/* block align (channel count * bytes per sample) */
view.setUint16(32, 2, true);
/* bits per sample */
view.setUint16(34, 16, true);
/* data chunk identifier */
writeString(view, 36, 'data');
/* data chunk length */
view.setUint32(40, this.length * 2, true);
for (var i = 0; i < this.length; i++){
/* Turn a 0->1 amplitude to 0->0x7FFF (highest number possible in a signed 16bits integer) */
sample = parseInt( samples[i] * 0x7FFF );
/* Get rid of banned samples by incrementing it */
if ( BANNED_SAMPLES.indexOf( sample ) > -1 ) {
sample++;
}
/* Append the sample in the data chunck */
view.setInt16(44 + i * 2, sample, true);
}
return new Blob( [view], {"type": "audio/wav"} );
};
/**
* @alias getBlob()
*/
AudioRecord.prototype.getWAVE = function() {
return this.getBlob();
};
/**
* Generate an object URL representing the WAV-encoded record.
*
* For performance reasons, you should unload the objectURL once you're
* done with it, see
* https://developer.mozilla.org/en-US/docs/Web/API/URL/revokeObjectURL
*
* @return {DOMString} URL representing the record.
*/
AudioRecord.prototype.getObjectURL = function () {
// To support chrome 22 (window.URL was added in chrome 23)
if ( window.URL === undefined ) {
window.URL = window.webkitURL;
}
return window.URL.createObjectURL( this.getBlob() );
};
/**
* Start the download process of the record as if it where a normal file.
*
* @param {String} [fileName='record.wav'] name of the file that will be downloaded
*/
AudioRecord.prototype.download = function ( fileName ) {
var a = document.createElement( 'a' ),
url = this.getObjectURL();
fileName = fileName || 'record.wav';
if ( fileName.toLowerCase().indexOf( '.wav', fileName.length - 4 ) === -1 ) {
fileName += '.wav';
}
a.style.display = "none";
a.href = url;
a.download = fileName;
document.body.appendChild( a );
a.click();
// It seems that old browser take time to take into account the click
// So we delay the deletion of the URL to let them enough time to start the download
setTimeout( function() {
document.body.removeChild( a );
window.URL.revokeObjectURL( url );
}, 1000 );
};
/**
* Generate an HTML5 <audio> element containing the WAV-encoded record.
*
* @return {HTMLElement} audio element containing the record.
*/
AudioRecord.prototype.getAudioElement = function () {
var audio = document.createElement( 'audio' ),
source = document.createElement( 'source' );
source.src = this.getObjectURL();
source.type = 'audio/wav';
audio.appendChild( source );
return audio;
};
/**
* Internal helper function used in getBlob to write a complete string at once
* in a DataView object.
*
* @param {DataView} [dataview] DataView in which to write.
* @param {number} [offset] Offset at which writing should start.
* @param {String} [str] String to write in the DataView.
*/
function writeString( dataview, offset, str ) {
for ( var i = 0; i < str.length; i++ ){
dataview.setUint8( offset + i, str.charCodeAt( i ) );
}
};