transcord
Version:
A simple recording and transcription module.
475 lines (398 loc) • 23.4 kB
JavaScript
(function (factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as anonymous module.
define(['jquery'], factory);
} else if (typeof exports === 'object') {
// Node / CommonJS
factory(require('jquery'));
} else {
// Browser globals.
factory(jQuery);
}
})(function ($) {
'use strict';
var AudioContext = window.AudioContext || window.webkitAudioContext;
if (!navigator.getUserMedia){
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
}
// Constants
var NAMESPACE = 'transcord';
// Events
var EVENT_INIT = 'init.' + NAMESPACE;
var EVENT_START = 'start.' + NAMESPACE;
var EVENT_STOP = 'stop.' + NAMESPACE;
var EVENT_BLOB = 'blob.' + NAMESPACE;
function toArray(obj, offset) {
var args = [];
args.push(offset);
return args.slice.apply(obj, args);
}
function Transcord(element, options) {
this.$element = $(element);
this.options = $.extend({}, Transcord.DEFAULTS, $.isPlainObject(options) && options);
this.recognizing = false;
this.stream = null;
this.speechRecognitionIsSupported = false;
this.context = null;
this.recordingLength = 0;
this.leftchannel = [];
this.rightchannel = [];
this.sampleRate = 2048;
this.transcriptBuffer = '';
this.blobsURL = [];
this.blobs = [];
this.init();
}
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();
},
bind : function () {
let $el = this.$element;
if ($.isFunction(this.options.onInit)) {
$el.on(EVENT_INIT, this.options.onInit);
}
if ($.isFunction(this.options.onStart)) {
$el.on(EVENT_START, this.options.onStart);
}
if ($.isFunction(this.options.onStop)) {
$el.on(EVENT_STOP, this.options.onStop);
}
if ($.isFunction(this.options.onStop)) {
$el.on(EVENT_BLOB, this.options.onBlob);
}
}
};
Transcord.DEFAULTS = {
// Record audio
record : true,
// Transcript speech to text
transcript : true,
// Transcription language
transcriptLanguage : 'en-US',
// Transcription returns interim results
transcriptInterimResults : true,
// Define if the old text must be kept when updating transcription
isTranscriptPersistent : true,
// Set of words that can be recognized
transcriptgrammar : undefined,
// Things to do on initialization
onInit : null,
// Things to do when record/transcript starts
onStart : null,
// Things to do when record/transcript stops
onStop : null,
// Things to do when a record is ready
onBlob : null,
// Number of record to be displayed at the same time.
recordNumber : 1,
// Show mic icon with events binded.
showMicIconNode : true,
// Idle mic img.
imgMicIdle : ' data:image/gif;base64,R0lGODdhMgAyAPUAAJqampubm5ycnJ2dnZ6enp+fn6CgoKSkpKWlpaampqurq62tra+vr7KysrS0tLW1tbe3t7i4uLm5ucTExMXFxcbGxsfHx8zMzM3Nzc7Ozs/Pz9jY2NnZ2dzc3N3d3d7e3t/f3+Pj4+bm5ujo6Orq6uzs7PLy8vPz8/T09PX19fb29vf39/r6+vv7+/z8/P39/f7+/v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C0ltYWdlTWFnaWNrDWdhbW1hPTAuNDU0NTUALAAAAAAyADIAAAa0wJhwSCwaj8ikcslsOp/QqHRKrVqv2Kx2y+16v+CweExGjkaustkAaHPUxVB7DgjBhw16u3EXFvRtfTGAgX2EAYKHiYSLgI16j3RlHyBDAYxCIB8pXiKFMQqAC5Z1X20kQhmAGpmfXQsAE3h0EjCpAKNfHKdDIBUVla2lYAyucQe4tmEJbi1FFgLGxHMQFNYEeypqGGyrynAeEnsdLILCm+bn6errH83r8PHy8/T19vf4+VZBADs=',
// Recording mic img
imgMicRecording : 'data:image/gif;base64,R0lGODlhMgAyAPcJAPwERP6mvP/o7v/l7P/m7P/+/vwfWPwPTP/4+vwvZPw8bv1Nev1Wgf2AoP6Srv6ctf6guP6huf6lvP/19/wzZ/6xxf65y//C0v////sDQ/wIR//6+/1ljPwlXfw/cP1Xgf6btP67zfw0aPw6bP/r8PwgWfw7bf/v8/1FdP1Ec/wHRv6twv/h6f6Trv6Ur/62yf1hiv13mvwFRf1uk/wST/1dh/6uw/12mf6Nqv6QrP/c5f/q7/1Yg/6WsfwGRv1QfPwVUf6swv69zv/G1f1zl//W4f16nP/7/PwTT//a4/6ft/6XsfwMSvwZVP63yv66zP/Q3P/09/wsYv/2+P1Rff6et9s2YN7e3txOc/1Dc/6KqP/L2P/M2f/Y4v/u8v1GdfxCcvwOTP1skvwoX9nZ2aCgoP1chubEzf/S3vTx8v/8/dnCx826v6WUmK6Vm66VnLOUnLSUnLmrr/2Bocy5vqSUmJ+XmZ+XmqyPl9za2rGMlvS1xf64ytjBxt3b26yQl/b29vf39+Pj4/Pz8/7+/vr6+vLy8ujo6Pz8/P/x9PwuY5+VmP/5+vwkXPw+b9xdfv1Ledd6k/1oj/61yKWlpaampq+vr7KysrS0tLm5ubW1tczMzPT09PX19djY2Nzc3N3d3a2trebm5s3NzZ2Vl56Zmt2tuv/B0fwXUv/T3vwaVJ+SlfgJR+QmV9Y7Y56Ulp2UlvoFRKx/i9wzXv1ylp2YmeggVOYkVpuampqamvoGRf1tktw7ZOghVLZ6ipuZmrdtgMJeeP/V4KORlrZtgKqBjJ+dnZqZmfkGRctNbsxLbMVYdPwtYsNgeZyXmPcKR8RYdKx+iq57iLNzhLJ0hLF2hctPb8pQb/ATTPETTJ+TluMpWeAtW6WKkdk3Ydo3YdU9ZPYKR/MPSu4WTu8WTvoERNY9ZaSLka58ibdsgPIQSv1Jd/wJSP6ovvwWUv/k6/1ji/6yxvw9bv/w9PwmXf1vlP1Idv6Ipv15m6OLkbVugZuYmcJdd8NbdsVXc5uZmSH/C05FVFNDQVBFMi4wAwEAAAAh/wtJbWFnZU1hZ2ljaw1nYW1tYT0wLjQ1NDU1ACH/C0ltYWdlTWFnaWNrDWdhbW1hPTAuNDU0NTUAIf8LSW1hZ2VNYWdpY2sNZ2FtbWE9MC40NTQ1NQAh/wtJbWFnZU1hZ2ljaw1nYW1tYT0wLjQ1NDU1ACH/C0ltYWdlTWFnaWNrDWdhbW1hPTAuNDU0NTUAIf8LSW1hZ2VNYWdpY2sNZ2FtbWE9MC40NTQ1NQAh/wtJbWFnZU1hZ2ljaw1nYW1tYT0wLjQ1NDU1ACH/C0ltYWdlTWFnaWNrDWdhbW1hPTAuNDU0NTUAIf8LSW1hZ2VNYWdpY2sNZ2FtbWE9MC40NTQ1NQAh/wtJbWFnZU1hZ2ljaw1nYW1tYT0wLjQ1NDU1ACH/C0ltYWdlTWFnaWNrDWdhbW1hPTAuNDU0NTUAIf8LSW1hZ2VNYWdpY2sNZ2FtbWE9MC40NTQ1NQAh/wtJbWFnZU1hZ2ljaw1nYW1tYT0wLjQ1NDU1ACH/C0ltYWdlTWFnaWNrDWdhbW1hPTAuNDU0NTUAIf8LSW1hZ2VNYWdpY2sNZ2FtbWE9MC40NTQ1NQAh/wtJbWFnZU1hZ2ljaw1nYW1tYT0wLjQ1NDU1ACH/C0ltYWdlTWFnaWNrDWdhbW1hPTAuNDU0NTUAIf8LSW1hZ2VNYWdpY2sNZ2FtbWE9MC40NTQ1NQAh/wtJbWFnZU1hZ2ljaw1nYW1tYT0wLjQ1NDU1ACH/C0ltYWdlTWFnaWNrDWdhbW1hPTAuNDU0NTUAIf8LSW1hZ2VNYWdpY2sNZ2FtbWE9MC40NTQ1NQAh/wtJbWFnZU1hZ2ljaw1nYW1tYT0wLjQ1NDU1ACH/C0ltYWdlTWFnaWNrDWdhbW1hPTAuNDU0NTUAIf8LSW1hZ2VNYWdpY2sNZ2FtbWE9MC40NTQ1NQAh/wtJbWFnZU1hZ2ljaw1nYW1tYT0wLjQ1NDU1ACH/C0ltYWdlTWFnaWNrDWdhbW1hPTAuNDU0NTUAIf8LSW1hZ2VNYWdpY2sNZ2FtbWE9MC40NTQ1NQAh/wtJbWFnZU1hZ2ljaw1nYW1tYT0wLjQ1NDU1ACH/C0ltYWdlTWFnaWNrDWdhbW1hPTAuNDU0NTUAIf8LSW1hZ2VNYWdpY2sNZ2FtbWE9MC40NTQ1NQAh/wtJbWFnZU1hZ2ljaw1nYW1tYT0wLjQ1NDU1ACH/C0ltYWdlTWFnaWNrDWdhbW1hPTAuNDU0NTUAIf8LSW1hZ2VNYWdpY2sNZ2FtbWE9MC40NTQ1NQAh/wtJbWFnZU1hZ2ljaw1nYW1tYT0wLjQ1NDU1ACH/C0ltYWdlTWFnaWNr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'
};
Transcord.setDefaults = function (options) {
$.extend(Transcord.DEFAULTS, options);
};
// Register as jQuery plugin
$.fn.transcord = function (option) {
var args = toArray(arguments, 1);
var result;
this.each(function () {
var $this = $(this);
var data = $this.data(NAMESPACE);
var options;
var fn;
if (!data) {
if (/destroy/.test(option)) {
return;
}
options = $.extend({}, $this.data(), $.isPlainObject(option) && option);
$this.data(NAMESPACE, (data = new Transcord(this, options)));
}
if (typeof option === 'string' && $.isFunction(fn = data[option])) {
result = fn.apply(data, args);
}
});
return typeof result === 'undefined' ? this : result;
};
$.fn.transcord.Constructor = Transcord;
$.fn.transcord.setDefaults = Transcord.setDefaults;
});