Home | History | Annotate | Download | only in frontend
      1 /*
      2  * Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
      3  * Use of this source code is governed by a BSD-style license that can be
      4  * found in the LICENSE file.
      5  */
      6 
      7 var Recorder = function(source){
      8   var bufferLen = 4096;
      9   var toneFreq = 1000, errorMargin = 0.05;
     10 
     11   var context = source.context;
     12   var sampleRate = context.sampleRate;
     13   var recBuffersL = [], recBuffersR = [], recLength = 0;
     14   this.node = (context.createScriptProcessor ||
     15               context.createJavaScriptNode).call(context, bufferLen, 2, 2);
     16   var detectAppend = false, autoStop = false, recordCallback;
     17   var recording = false;
     18   var freqString;
     19 
     20   this.node.onaudioprocess = function(e) {
     21     if (!recording) return;
     22 
     23     var length = e.inputBuffer.getChannelData(0).length;
     24     var tmpLeft = new Float32Array(length);
     25     var tmpRight = new Float32Array(length);
     26     tmpLeft.set(e.inputBuffer.getChannelData(0), 0);
     27     tmpRight.set(e.inputBuffer.getChannelData(1), 0);
     28 
     29     recBuffersL.push(tmpLeft);
     30     recBuffersR.push(tmpRight);
     31     recLength += length;
     32     var stop = false;
     33 
     34     if (autoStop && detectTone(getFreqList(tmpLeft)))
     35       stop = true;
     36 
     37     if (recordCallback) {
     38       var tmpLeft = recBuffersL[recBuffersL.length - 1].subarray(
     39           -FFT_SIZE-1, -1);
     40       var tmpRight = recBuffersR[recBuffersR.length - 1].subarray(
     41           -FFT_SIZE-1, -1);
     42       recordCallback(tmpLeft, tmpRight, sampleRate, stop);
     43     }
     44   }
     45 
     46   /**
     47    * Starts recording
     48    * @param {function} callback function to get current buffer
     49    * @param {boolean} detect append tone or not
     50    * @param {boolean} auto stop when detecting append tone
     51    */
     52   this.record = function(cb, detect, stop) {
     53     recordCallback = cb;
     54     detectAppend = detect;
     55     autoStop = stop;
     56     recording = true;
     57   }
     58 
     59   /**
     60    * Stops recording
     61    */
     62   this.stop = function() {
     63     recording = false;
     64     recBuffersL = mergeBuffers(recBuffersL, recLength);
     65     recBuffersR = mergeBuffers(recBuffersR, recLength);
     66     if (detectAppend) {
     67       var freqList = getFreqList(recBuffersL);
     68       var index = getToneIndices(freqList);
     69       removeAppendTone(index[0], index[1]);
     70       exportFreqList(freqList);
     71     }
     72   }
     73 
     74   /**
     75    * Gets frequencies list
     76    * @param {Float32Array} buffer
     77    * @return {array} frequencies list
     78    */
     79   getFreqList = function(buffer) {
     80     var prevPeak = 0;
     81     var valid = true;
     82     var freqList = [];
     83     for (i = 1; i < recLength; i++) {
     84       if (buffer[i] > 0.1 &&
     85           buffer[i] >= buffer[i - 1] && buffer[i] >= buffer[i + 1]) {
     86         if (valid) {
     87           var freq = sampleRate / (i - prevPeak);
     88           freqList.push([freq, prevPeak, i]);
     89           prevPeak = i;
     90           valid = false;
     91         }
     92       } else if (buffer[i] < -0.1) {
     93         valid = true;
     94       }
     95     }
     96     return freqList;
     97   }
     98 
     99   /**
    100    * Checks average frequency is in allowed error margin
    101    * @param {float} average frequency
    102    * @return {boolean} checked result pass or fail
    103    */
    104   checkFreq = function (average) {
    105     if (Math.abs(average - toneFreq) / toneFreq < errorMargin)
    106       return true;
    107     return false;
    108   }
    109 
    110   /**
    111    * Detects append tone while recording.
    112    * @param {array} frequencies list
    113    * @return {boolean} detected or not
    114    */
    115   detectTone = function(freqList) {
    116     var passCriterion = 50;
    117     // Initialize function static variables
    118     if (typeof detectTone.startDetected == 'undefined') {
    119       detectTone.startDetected = false;
    120       detectTone.canStop = false;
    121       detectTone.accumulateTone = 0;
    122     }
    123 
    124     var windowSize = 10, windowSum = 0, i;
    125     var detected = false;
    126     for (i = 0; i < freqList.length && i < windowSize; i++) {
    127       windowSum += freqList[i][0];
    128     }
    129     if (checkFreq(windowSum / Math.min(windowSize, freqList.length))) {
    130       detected = true;
    131       detectTone.accumulateTone++;
    132     }
    133     for (; i < freqList.length; i++) {
    134       windowSum = windowSum + freqList[i][0] - freqList[i - windowSize][0];
    135       if (checkFreq(windowSum / windowSize)) {
    136         detected = true;
    137         detectTone.accumulateTone++;
    138       }
    139     }
    140     if (detected) {
    141       if (detectTone.accumulateTone > passCriterion) {
    142         if (!detectTone.startDetected)
    143           detectTone.startDetected = true;
    144         else if (detectTone.canStop) {
    145           detectTone.startDetected = false;
    146           detectTone.canStop = false;
    147           detectTone.accumulateTone = 0;
    148           return true;
    149         }
    150       }
    151     } else {
    152       detectTone.accumulateTone = 0;
    153       if (detectTone.startDetected)
    154         detectTone.canStop = true;
    155     }
    156     return false;
    157   }
    158 
    159   /**
    160    * Gets start and end indices from a frquencies list except append tone
    161    * @param {array} frequencies list
    162    * @return {array} start and end indices
    163    */
    164   getToneIndices = function(freqList) {
    165     // find start and end indices
    166     var flag, j, k;
    167     var windowSize = 10, windowSum;
    168     var index = new Array(2);
    169     var scanRange = [[0, freqList.length, 1], [freqList.length - 1, -1, -1]];
    170 
    171     if (freqList.length == 0) return index;
    172 
    173     for (i = 0; i < 2; i++) {
    174       flag = false;
    175       windowSum = 0;
    176       for (j = scanRange[i][0], k = 0; k < windowSize && j != scanRange[i][1];
    177           j += scanRange[i][2], k++) {
    178         windowSum += freqList[j][0];
    179       }
    180       for (; j != scanRange[i][1]; j += scanRange[i][2]) {
    181         windowSum = windowSum + freqList[j][0] -
    182             freqList[j - scanRange[i][2] * windowSize][0];
    183         var avg = windowSum / windowSize;
    184         if (checkFreq(avg) && !flag) {
    185           flag = true;
    186         }
    187         if (!checkFreq(avg) && flag) {
    188           index[i] = freqList[j][1];
    189           break;
    190         }
    191       }
    192     }
    193     return index;
    194   }
    195 
    196   /**
    197    * Removes append tone from recorded buffer
    198    * @param {int} start index
    199    * @param {int} end index
    200    */
    201   removeAppendTone = function(start, end) {
    202     if (!isNaN(start) && !isNaN(end) && end > start) {
    203       recBuffersL = truncateBuffers(recBuffersL, recLength, start, end);
    204       recBuffersR = truncateBuffers(recBuffersR, recLength, start, end);
    205       recLength = end - start;
    206     }
    207   }
    208 
    209   /**
    210    * Exports frequency list for debugging purpose
    211    */
    212   exportFreqList = function(freqList) {
    213     freqString = sampleRate + '\n';
    214     for (var i = 0; i < freqList.length; i++) {
    215       freqString += freqList[i][0] + ' ' + freqList[i][1] + ' ' +
    216           freqList[i][2] + '\n';
    217     }
    218   }
    219 
    220   this.getFreq = function() {
    221     return freqString;
    222   }
    223 
    224   /**
    225    * Clears recorded buffer
    226    */
    227   this.clear = function() {
    228     recLength = 0;
    229     recBuffersL = [];
    230     recBuffersR = [];
    231   }
    232 
    233   /**
    234    * Gets recorded buffer
    235    */
    236   this.getBuffer = function() {
    237     var buffers = [];
    238     buffers.push(recBuffersL);
    239     buffers.push(recBuffersR);
    240     return buffers;
    241   }
    242 
    243   /**
    244    * Exports WAV format file
    245    * @return {blob} audio file blob
    246    */
    247   this.exportWAV = function(type) {
    248     type = type || 'audio/wav';
    249     var interleaved = interleave(recBuffersL, recBuffersR);
    250     var dataview = encodeWAV(interleaved);
    251     var audioBlob = new Blob([dataview], { type: type });
    252     return audioBlob;
    253   }
    254 
    255   /**
    256    * Truncates buffer from start index to end index
    257    * @param {Float32Array} audio buffer
    258    * @param {int} buffer length
    259    * @param {int} start index
    260    * @param {int} end index
    261    * @return {Float32Array} a truncated buffer
    262    */
    263   truncateBuffers = function(recBuffers, recLength, startIdx, endIdx) {
    264     var buffer = new Float32Array(endIdx - startIdx);
    265     for (var i = startIdx, j = 0; i < endIdx; i++, j++) {
    266       buffer[j] = recBuffers[i];
    267     }
    268     return buffer;
    269   }
    270 
    271   /**
    272    * Merges buffer into an array
    273    * @param {array} a list of Float32Array of audio buffer
    274    * @param {int} buffer length
    275    * @return {Float32Array} a merged buffer
    276    */
    277   mergeBuffers = function(recBuffers, recLength) {
    278     var result = new Float32Array(recLength);
    279     var offset = 0;
    280     for (var i = 0; i < recBuffers.length; i++){
    281       result.set(recBuffers[i], offset);
    282       offset += recBuffers[i].length;
    283     }
    284     return result;
    285   }
    286 
    287   /**
    288    * Interleaves left and right channel buffer
    289    * @param {Float32Array} left channel buffer
    290    * @param {Float32Array} right channel buffer
    291    * @return {Float32Array} an interleaved buffer
    292    */
    293   interleave = function(inputL, inputR) {
    294     var length = inputL.length + inputR.length;
    295     var result = new Float32Array(length);
    296 
    297     var index = 0,
    298       inputIndex = 0;
    299 
    300     while (index < length){
    301       result[index++] = inputL[inputIndex];
    302       result[index++] = inputR[inputIndex];
    303       inputIndex++;
    304     }
    305     return result;
    306   }
    307 
    308   floatTo16BitPCM = function(output, offset, input) {
    309     for (var i = 0; i < input.length; i++, offset+=2){
    310       var s = Math.max(-1, Math.min(1, input[i]));
    311       output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
    312     }
    313   }
    314 
    315   writeString = function(view, offset, string) {
    316     for (var i = 0; i < string.length; i++){
    317       view.setUint8(offset + i, string.charCodeAt(i));
    318     }
    319   }
    320 
    321   /**
    322    * Encodes audio buffer into WAV format raw data
    323    * @param {Float32Array} an interleaved buffer
    324    * @return {DataView} WAV format raw data
    325    */
    326   encodeWAV = function(samples) {
    327     var buffer = new ArrayBuffer(44 + samples.length * 2);
    328     var view = new DataView(buffer);
    329 
    330     /* RIFF identifier */
    331     writeString(view, 0, 'RIFF');
    332     /* file length */
    333     view.setUint32(4, 32 + samples.length * 2, true);
    334     /* RIFF type */
    335     writeString(view, 8, 'WAVE');
    336     /* format chunk identifier */
    337     writeString(view, 12, 'fmt ');
    338     /* format chunk length */
    339     view.setUint32(16, 16, true);
    340     /* sample format (raw) */
    341     view.setUint16(20, 1, true);
    342     /* channel count */
    343     view.setUint16(22, 2, true);
    344     /* sample rate */
    345     view.setUint32(24, sampleRate, true);
    346     /* byte rate (sample rate * block align) */
    347     view.setUint32(28, sampleRate * 4, true);
    348     /* block align (channel count * bytes per sample) */
    349     view.setUint16(32, 4, true);
    350     /* bits per sample */
    351     view.setUint16(34, 16, true);
    352     /* data chunk identifier */
    353     writeString(view, 36, 'data');
    354     /* data chunk length */
    355     view.setUint32(40, samples.length * 2, true);
    356 
    357     floatTo16BitPCM(view, 44, samples);
    358 
    359     return view;
    360   }
    361 
    362   source.connect(this.node);
    363   this.node.connect(context.destination);
    364 };
    365 
    366 window.Recorder = Recorder;
    367