• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
7var 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
366window.Recorder = Recorder;
367