lingua-recorder
Version:
A JavaScript library for easy and efficient voice recording tasks.
301 lines (249 loc) • 7.62 kB
JavaScript
'use strict';
/**
* 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 ];
/**
* @class AudioRecord
* Represents an audio recording and provides a handset of helper functions
* to exploit it in different contexts.
*/
class AudioRecord {
/**
* Creates a new AudioRecord instance.
*
* @param {Float32Array} [samples] The raw samples that will make up the record
* @param {Number} [sampleRate] Rate at which the samples added to this object should be played
*/
constructor( samples, sampleRate ) {
this.sampleRate = sampleRate;
this.samples = samples;
}
/**
* Change the sample rate.
*
* @param {Number} [value] new sample rate to set.
*/
setSampleRate( value ) {
this.sampleRate = value;
}
/**
* Get the sample rate in use.
*
* @return {Number} Current sample rate for the record.
*/
getSampleRate() {
return this.sampleRate;
}
/**
* Get the total number of samples in the record.
*
* @return {Number} Number of samples.
*/
getLength() {
return this.samples.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.
*/
getDuration() {
return this.samples.length / this.sampleRate;
}
/**
* Get all the raw samples that make up the record.
*
* @return {Float32Array} List of all samples.
*/
getSamples() {
return this.samples;
}
/**
* Trim the record, starting with the beginning of the record (the left side).
*
* @param {Number} [duration] duration (in seconds) to trim.
*/
lTrim( duration ) {
var nbSamplesToRemove = Math.round( duration * this.sampleRate );
if ( nbSamplesToRemove >= this.samples.length ) {
this.clear();
return;
}
this.samples = this.samples.subarray( nbSamplesToRemove );
}
/**
* Trim the record, starting with the end of the record (the right side).
*
* @param {Number} [duration] duration (in seconds) to trim.
*/
rTrim( duration ) {
var nbSamplesToRemove = Math.round( duration * this.sampleRate );
if ( nbSamplesToRemove >= this.samples.length ) {
this.clear();
return;
}
this.samples = this.samples.subarray( 0, this.samples.length - nbSamplesToRemove );
}
/**
* Clear the record.
*/
clear() {
this.samples = new Float32Array( 0 );
}
/**
* Play the record to the audio output (aka the user's loudspeaker).
*/
play() {
var audioContext = new window.AudioContext();
var buffer = audioContext.createBuffer( 1, this.samples.length, 48000 ); // sample rate
var channelData = buffer.getChannelData( 0 );
for ( let i = 0; i < this.samples.length; i++ ) {
channelData[i] = this.samples[ i ];
}
var source = audioContext.createBufferSource();
source.buffer = buffer;
source.connect( audioContext.destination );
source.start(0);
}
/**
* Get a WAV-encoded Blob version of the record.
*
* @return {Blob} WAV-encoded audio record.
* @alias getWAVE()
*/
getBlob() {
var buffer = new ArrayBuffer(44 + this.samples.length * 2),
view = new DataView(buffer);
/* RIFF identifier */
AudioRecord.writeString(view, 0, 'RIFF');
/* file length */
view.setUint32(4, 32 + this.samples.length * 2, true);
/* RIFF type */
AudioRecord.writeString(view, 8, 'WAVE');
/* format chunk identifier */
AudioRecord.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 */
AudioRecord.writeString(view, 36, 'data');
/* data chunk length */
view.setUint32(40, this.samples.length * 2, true);
for ( let i = 0; i < this.samples.length; i++ ){
/* Turn a 0->1 amplitude to 0->0x7FFF (highest number possible in a signed 16bits integer) */
let sample = parseInt( this.samples[i] * 0x7FFF );
/* Get rid of banned samples by incrementing it */
if ( BANNED_SAMPLES.indexOf( sample ) > -1 ) {
sample++;
}
/* Append the sample in the data chunk */
view.setInt16( 44 + i * 2, sample, true );
}
return new Blob( [view], {"type": "audio/wav"} );
}
/**
* @alias getBlob()
*/
getWAVE() {
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.
*/
getObjectURL() {
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.
*/
download( 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.
*/
getAudioElement() {
var audio = document.createElement( 'audio' ),
source = document.createElement( 'source' );
source.src = this.getObjectURL();
source.type = 'audio/wav';
audio.appendChild( source );
return audio;
}
/**
* Internal static helper function used in getBlob to write a complete string at once
* in a DataView object.
*
* @static
* @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.
* @private
*/
static writeString( dataview, offset, str ) {
for ( let i = 0; i < str.length; i++ ){
dataview.setUint8( offset + i, str.charCodeAt( i ) );
}
}
}
// UMD pattern to support different ways of loading the library
( function ( root, factory ) {
if ( typeof define === "function" && define.amd ) {
// AMD. Register as an anonymous module.
define( [], factory );
} else if ( typeof module === "object" && module.exports ) {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory();
} else {
// Browser globals (root is window)
if( ! root.LinguaRecorder ) root.LinguaRecorder = {}
root.LinguaRecorder.AudioRecord = factory().AudioRecord;
}
} ( typeof self !== "undefined" ? self : this, function () {
return { AudioRecord };
} ) );