UNPKG

transcord

Version:

A simple recording and transcription module.

319 lines (279 loc) 10.1 kB
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(); },