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