transcord
Version:
A simple recording and transcription module.
319 lines (279 loc) • 10.1 kB
JavaScript
Transcord.prototype = {
constructor : Transcord,
init: function () {
// Bind events;
this.bind();
this.$element.trigger(EVENT_INIT);
// Ask for user media.
var _this = this;
if (this.options.record && navigator.getUserMedia){
navigator.getUserMedia({ audio:true }, function (stream) {
_this.stream = stream;
}, function () {
console.log ('No audio device detected');
return;
});
}else {
console.error('getUserMedia not available');
return;
}
// Display the mic Icon.
if ($.type(this.options.showMicIconNode) === 'boolean' && this.options.showMicIconNode){
this.displayMic(this.$element.parent());
}else if (this.options.showMicIconNode instanceof jQuery){
this.displayMic(this.options.showMicIconNode);
}
if (this.options.transcript && 'webkitSpeechRecognition' in window) {
this.speechRecognitionIsSupported = true;
this.recognition = new webkitSpeechRecognition(); // That is the object that will manage our whole recognition process.
this.recognition.continuous = true; // Suitable for dictation.
this.recognition.interimResults = this.options.transcriptInterimResults; // If we want to start receiving results even if they are not final.
this.recognition.lang = this.options.transcriptLanguage;
this.recognition.maxAlternatives = 1; // Since from our experience, the highest result is really the best...
if (this.options.grammar){
this.recognition.grammar = this.options.transcriptgrammar;
}
// Results of the recognition
this.recognition.onresult = function (event) {
// Check if everything is OK, quit if not.
if (typeof(event.results) === 'undefined') {
_this.recognition.stop();
console.error('Results are undefined, stopping recognition');
return;
}
for (var i = event.resultIndex; i < event.results.length; ++i) {
if (event.results[i].isFinal) { // Final results
if (_this.options.isTranscriptPersistent){
_this.transcriptBuffer+= event.results[i][0].transcript;
}
_this.$element.text(_this.transcriptBuffer || event.results[i][0].transcript);
} else { // Not final
_this.$element.text(_this.transcriptBuffer + event.results[i][0].transcript) ;
}
}
};
} else {
console.log('Speech recognition is not supported by this navigator');
}
},
startRecording : function () {
this.$element.trigger(EVENT_START);
this.$micImg.trigger(EVENT_START);
if (this.options.transcript && this.speechRecognitionIsSupported){
this.recognition.start();
}
if (!this.options.record) {
return;
}
// creates the audio context
this.context = new AudioContext();
// retrieve the current sample rate to be used for WAV packaging
this.sampleRate = this.context.sampleRate;
// creates a gain node
let volume = this.context.createGain();
// creates an audio node from the microphone incoming stream
let audioInput = this.context.createMediaStreamSource(this.stream);
// connect the stream to the gain node
audioInput.connect(volume);
var bufferSize = 2048;
let recorder = this.context.createScriptProcessor(bufferSize, 2, 2);
var _this = this;
this.recognizing = true;
recorder.onaudioprocess = function (e) {
if (!_this.recognizing){
return;
}
var left = e.inputBuffer.getChannelData (0);
var right = e.inputBuffer.getChannelData (1);
// we clone the samples
_this.leftchannel.push (new Float32Array (left));
_this.rightchannel.push (new Float32Array (right));
_this.recordingLength += this.bufferSize;
};
// we connect the recorder
volume.connect (recorder);
recorder.connect (this.context.destination);
},
stopRecording : function () {
this.$element.trigger(EVENT_STOP);
this.$micImg.trigger(EVENT_STOP);
this.recognizing = false;
if (this.options.transcript && this.speechRecognitionIsSupported){
this.recognition.stop();
}
if (this.options.record) {
let blob = this.writeWaw();
this.blobs.push(blob);
this.$element.trigger(EVENT_BLOB, [blob]);
this.context.close();
return blob;
}
return null;
},
writeWaw : function () {
function mergeBuffers(channelBuffer, recordingLength) {
var result = new Float32Array(recordingLength);
var offset = 0;
var lng = channelBuffer.length;
for (var i = 0; i < lng; i++){
let buffer = channelBuffer[i];
result.set(buffer, offset);
offset += buffer.length;
}
return result;
}
function interleave(leftChannel, rightChannel) {
var length = leftChannel.length + rightChannel.length;
var result = new Float32Array(length);
var inputIndex = 0;
for (var index = 0; index < length;){
result[index++] = leftChannel[inputIndex];
result[index++] = rightChannel[inputIndex];
inputIndex++;
}
return result;
}
function writeUTFBytes(view, offset, string) {
var lng = string.length;
for (var i = 0; i < lng; i++){
view.setUint8(offset + i, string.charCodeAt(i));
}
}
// we flat the left and right channels down
var leftBuffer = mergeBuffers (this.leftchannel, this.recordingLength);
var rightBuffer = mergeBuffers (this.rightchannel, this.recordingLength);
// we interleave both channels together
var interleaved = interleave (leftBuffer, rightBuffer);
// create the buffer and view to create the .WAV file
var buffer = new ArrayBuffer(44 + interleaved.length * 2);
var view = new DataView(buffer);
// write the WAV container, check spec at: https://ccrma.stanford.edu/courses/422/projects/WaveFormat/
// RIFF chunk descriptor
writeUTFBytes(view, 0, 'RIFF');
view.setUint32(4, 44 + interleaved.length * 2, true);
writeUTFBytes(view, 8, 'WAVE');
// FMT sub-chunk
writeUTFBytes(view, 12, 'fmt ');
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
// stereo (2 channels)
view.setUint16(22, 2, true);
view.setUint32(24, this.sampleRate, true);
view.setUint32(28, this.sampleRate * 4, true);
view.setUint16(32, 4, true);
view.setUint16(34, 16, true);
// data sub-chunk
writeUTFBytes(view, 36, 'data');
view.setUint32(40, interleaved.length * 2, true);
// write the PCM samples
var lng = interleaved.length;
var index = 44;
var volume = 1;
for (var i = 0; i < lng; i++){
view.setInt16(index, interleaved[i] * (0x7FFF * volume), true);
index += 2;
}
// our final binary blob that we can hand off
var blob = new Blob ([view], { type : 'audio/wav' });
this.leftchannel.length = 0;
this.rightchannel.length = 0;
this.recordingLength = 0;
return blob;
},
toogleRecord : function () {
if (this.recognizing){
return this.stopRecording();
}else {
this.startRecording();
}
},
displayBlob : function (blob,$node) {
if (!blob || !(blob instanceof Blob)){
return;
}
// If the number of blob will be over the number of blob allowed, destroy the first recorded Blob.
if (this.blobsURL.length === this.options.recordNumber){
this.destroyBlob(0);
}
let url = window.URL.createObjectURL(blob);
this.blobsURL.push(url);
// The audio tag will be displayed at the chosen node,
let $nodeToAppend = $node || this.$element.parent();
$nodeToAppend.append('<audio controls src="' + url + '"></audio>');
},
destroyBlob : function (index) {
var _this = this;
function destroy (url) {
// Remove object with animation
$('audio[src="' + url + '"]').hide('slow', function () { $('audio[src="' + url + '"]').remove(); });
window.URL.revokeObjectURL(url);
let index = _this.blobsURL.indexOf(url);
if (index !== -1){
_this.blobsURL.splice(index, 1);
_this.blobs.splice(index, 1);
}
}
if (typeof index === 'string' && parseInt(index, 10) !== 'number'){
var corresponding = {
first : 0,
last : (this.blobsURL.length - 1),
all : Object.keys(this.blobsURL)
};
if (!corresponding[index]){
console.error('Invalid index : "' + index + '"');
return;
}else {
index = corresponding[index];
}
}
if (index.constructor === Array){
let length = index.length;
var values = [];
for (let i = 0; i < length && !isNaN(parseInt(index[i], 10)); i++){
values.push(this.blobsURL[i]);
}
length = values.length;
for (let i = 0; i < length; i++){
destroy(values[i]);
}
}else if (typeof parseInt(index, 10) === 'number'){
destroy(this.blobsURL[index]);
}
},
getBlobs : function () {
// Returns copy of blobs array
return this.blobs.slice();
},
trigger: function (type, data) {
var e = $.Event(type, data);
this.$element.trigger(e);
return e;
},
displayMic : function ($node) {
let _this = this;
this.$micImg = $('<img class=\'transcord_mic\' src=\'' + this.options.imgMicIdle + '\'>')
.on(EVENT_START, function () {
$(this).attr('src', _this.options.imgMicRecording);
})
.on(EVENT_STOP, function () {
$(this).attr('src', _this.options.imgMicIdle);
})
.on('click', function () {
_this.toogleRecord();
})
.appendTo($node);
},
downloadBlob : function (blob, filename) {
let url = window.URL.createObjectURL(blob);
// Simulate a link.
let $mah = $('<a/>')
.hide()
.attr('href', url)
.attr('download', (filename || 'new record'))
.appendTo($('body'));
// Click on link
$mah[0].click();
// Clear things up.
window.URL.revokeObjectURL(url);
$mah.remove();
},