/*
* 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;