/* * Copyright (c) 2014 The Chromium OS Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ var Recorder = function(source){ var bufferLen = 4096; var toneFreq = 1000, errorMargin = 0.05; var context = source.context; var sampleRate = context.sampleRate; var recBuffersL = [], recBuffersR = [], recLength = 0; this.node = (context.createScriptProcessor || context.createJavaScriptNode).call(context, bufferLen, 2, 2); var detectAppend = false, autoStop = false, recordCallback; var recording = false; var freqString; this.node.onaudioprocess = function(e) { if (!recording) return; var length = e.inputBuffer.getChannelData(0).length; var tmpLeft = new Float32Array(length); var tmpRight = new Float32Array(length); tmpLeft.set(e.inputBuffer.getChannelData(0), 0); tmpRight.set(e.inputBuffer.getChannelData(1), 0); recBuffersL.push(tmpLeft); recBuffersR.push(tmpRight); recLength += length; var stop = false; if (autoStop && detectTone(getFreqList(tmpLeft))) stop = true; if (recordCallback) { var tmpLeft = recBuffersL[recBuffersL.length - 1].subarray( -FFT_SIZE-1, -1); var tmpRight = recBuffersR[recBuffersR.length - 1].subarray( -FFT_SIZE-1, -1); recordCallback(tmpLeft, tmpRight, sampleRate, stop); } } /** * Starts recording * @param {function} callback function to get current buffer * @param {boolean} detect append tone or not * @param {boolean} auto stop when detecting append tone */ this.record = function(cb, detect, stop) { recordCallback = cb; detectAppend = detect; autoStop = stop; recording = true; } /** * Stops recording */ this.stop = function() { recording = false; recBuffersL = mergeBuffers(recBuffersL, recLength); recBuffersR = mergeBuffers(recBuffersR, recLength); if (detectAppend) { var freqList = getFreqList(recBuffersL); var index = getToneIndices(freqList); removeAppendTone(index[0], index[1]); exportFreqList(freqList); } } /** * Gets frequencies list * @param {Float32Array} buffer * @return {array} frequencies list */ getFreqList = function(buffer) { var prevPeak = 0; var valid = true; var freqList = []; for (i = 1; i < recLength; i++) { if (buffer[i] > 0.1 && buffer[i] >= buffer[i - 1] && buffer[i] >= buffer[i + 1]) { if (valid) { var freq = sampleRate / (i - prevPeak); freqList.push([freq, prevPeak, i]); prevPeak = i; valid = false; } } else if (buffer[i] < -0.1) { valid = true; } } return freqList; } /** * Checks average frequency is in allowed error margin * @param {float} average frequency * @return {boolean} checked result pass or fail */ checkFreq = function (average) { if (Math.abs(average - toneFreq) / toneFreq < errorMargin) return true; return false; } /** * Detects append tone while recording. * @param {array} frequencies list * @return {boolean} detected or not */ detectTone = function(freqList) { var passCriterion = 50; // Initialize function static variables if (typeof detectTone.startDetected == 'undefined') { detectTone.startDetected = false; detectTone.canStop = false; detectTone.accumulateTone = 0; } var windowSize = 10, windowSum = 0, i; var detected = false; for (i = 0; i < freqList.length && i < windowSize; i++) { windowSum += freqList[i][0]; } if (checkFreq(windowSum / Math.min(windowSize, freqList.length))) { detected = true; detectTone.accumulateTone++; } for (; i < freqList.length; i++) { windowSum = windowSum + freqList[i][0] - freqList[i - windowSize][0]; if (checkFreq(windowSum / windowSize)) { detected = true; detectTone.accumulateTone++; } } if (detected) { if (detectTone.accumulateTone > passCriterion) { if (!detectTone.startDetected) detectTone.startDetected = true; else if (detectTone.canStop) { detectTone.startDetected = false; detectTone.canStop = false; detectTone.accumulateTone = 0; return true; } } } else { detectTone.accumulateTone = 0; if (detectTone.startDetected) detectTone.canStop = true; } return false; } /** * Gets start and end indices from a frquencies list except append tone * @param {array} frequencies list * @return {array} start and end indices */ getToneIndices = function(freqList) { // find start and end indices var flag, j, k; var windowSize = 10, windowSum; var index = new Array(2); var scanRange = [[0, freqList.length, 1], [freqList.length - 1, -1, -1]]; if (freqList.length == 0) return index; for (i = 0; i < 2; i++) { flag = false; windowSum = 0; for (j = scanRange[i][0], k = 0; k < windowSize && j != scanRange[i][1]; j += scanRange[i][2], k++) { windowSum += freqList[j][0]; } for (; j != scanRange[i][1]; j += scanRange[i][2]) { windowSum = windowSum + freqList[j][0] - freqList[j - scanRange[i][2] * windowSize][0]; var avg = windowSum / windowSize; if (checkFreq(avg) && !flag) { flag = true; } if (!checkFreq(avg) && flag) { index[i] = freqList[j][1]; break; } } } return index; } /** * Removes append tone from recorded buffer * @param {int} start index * @param {int} end index */ removeAppendTone = function(start, end) { if (!isNaN(start) && !isNaN(end) && end > start) { recBuffersL = truncateBuffers(recBuffersL, recLength, start, end); recBuffersR = truncateBuffers(recBuffersR, recLength, start, end); recLength = end - start; } } /** * Exports frequency list for debugging purpose */ exportFreqList = function(freqList) { freqString = sampleRate + '\n'; for (var i = 0; i < freqList.length; i++) { freqString += freqList[i][0] + ' ' + freqList[i][1] + ' ' + freqList[i][2] + '\n'; } } this.getFreq = function() { return freqString; } /** * Clears recorded buffer */ this.clear = function() { recLength = 0; recBuffersL = []; recBuffersR = []; } /** * Gets recorded buffer */ this.getBuffer = function() { var buffers = []; buffers.push(recBuffersL); buffers.push(recBuffersR); return buffers; } /** * Exports WAV format file * @return {blob} audio file blob */ this.exportWAV = function(type) { type = type || 'audio/wav'; var interleaved = interleave(recBuffersL, recBuffersR); var dataview = encodeWAV(interleaved); var audioBlob = new Blob([dataview], { type: type }); return audioBlob; } /** * Truncates buffer from start index to end index * @param {Float32Array} audio buffer * @param {int} buffer length * @param {int} start index * @param {int} end index * @return {Float32Array} a truncated buffer */ truncateBuffers = function(recBuffers, recLength, startIdx, endIdx) { var buffer = new Float32Array(endIdx - startIdx); for (var i = startIdx, j = 0; i < endIdx; i++, j++) { buffer[j] = recBuffers[i]; } return buffer; } /** * Merges buffer into an array * @param {array} a list of Float32Array of audio buffer * @param {int} buffer length * @return {Float32Array} a merged buffer */ mergeBuffers = function(recBuffers, recLength) { var result = new Float32Array(recLength); var offset = 0; for (var i = 0; i < recBuffers.length; i++){ result.set(recBuffers[i], offset); offset += recBuffers[i].length; } return result; } /** * Interleaves left and right channel buffer * @param {Float32Array} left channel buffer * @param {Float32Array} right channel buffer * @return {Float32Array} an interleaved buffer */ interleave = function(inputL, inputR) { var length = inputL.length + inputR.length; var result = new Float32Array(length); var index = 0, inputIndex = 0; while (index < length){ result[index++] = inputL[inputIndex]; result[index++] = inputR[inputIndex]; inputIndex++; } return result; } floatTo16BitPCM = function(output, offset, input) { for (var i = 0; i < input.length; i++, offset+=2){ var s = Math.max(-1, Math.min(1, input[i])); output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); } } writeString = function(view, offset, string) { for (var i = 0; i < string.length; i++){ view.setUint8(offset + i, string.charCodeAt(i)); } } /** * Encodes audio buffer into WAV format raw data * @param {Float32Array} an interleaved buffer * @return {DataView} WAV format raw data */ encodeWAV = function(samples) { var buffer = new ArrayBuffer(44 + samples.length * 2); var view = new DataView(buffer); /* RIFF identifier */ writeString(view, 0, 'RIFF'); /* file length */ view.setUint32(4, 32 + samples.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, 2, true); /* sample rate */ view.setUint32(24, sampleRate, true); /* byte rate (sample rate * block align) */ view.setUint32(28, sampleRate * 4, true); /* block align (channel count * bytes per sample) */ view.setUint16(32, 4, true); /* bits per sample */ view.setUint16(34, 16, true); /* data chunk identifier */ writeString(view, 36, 'data'); /* data chunk length */ view.setUint32(40, samples.length * 2, true); floatTo16BitPCM(view, 44, samples); return view; } source.connect(this.node); this.node.connect(context.destination); }; window.Recorder = Recorder;