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/** 8 * Gets a random color 9 */ 10function getRandomColor() { 11 var letters = '0123456789ABCDEF'.split(''); 12 var color = '#'; 13 for (var i = 0; i < 6; i++) { 14 color += letters[Math.floor(Math.random() * 16)]; 15 } 16 return color; 17} 18 19/** 20 * Audio channel class 21 */ 22var AudioChannel = function(buffer) { 23 this.init = function(buffer) { 24 this.buffer = buffer; 25 this.fftBuffer = this.toFFT(this.buffer); 26 this.curveColor = getRandomColor(); 27 this.visible = true; 28 } 29 30 this.toFFT = function(buffer) { 31 var k = Math.ceil(Math.log(buffer.length) / Math.LN2); 32 var length = Math.pow(2, k); 33 var tmpBuffer = new Float32Array(length); 34 35 for (var i = 0; i < buffer.length; i++) { 36 tmpBuffer[i] = buffer[i]; 37 } 38 for (var i = buffer.length; i < length; i++) { 39 tmpBuffer[i] = 0; 40 } 41 var fft = new FFT(length); 42 fft.forward(tmpBuffer); 43 return fft.spectrum; 44 } 45 46 this.init(buffer); 47} 48 49window.AudioChannel = AudioChannel; 50 51var numberOfCurve = 0; 52 53/** 54 * Audio curve class 55 */ 56var AudioCurve = function(buffers, filename, sampleRate) { 57 this.init = function(buffers, filename) { 58 this.filename = filename; 59 this.id = numberOfCurve++; 60 this.sampleRate = sampleRate; 61 this.channel = []; 62 for (var i = 0; i < buffers.length; i++) { 63 this.channel.push(new AudioChannel(buffers[i])); 64 } 65 } 66 this.init(buffers, filename); 67} 68 69window.AudioCurve = AudioCurve; 70 71/** 72 * Draw frequency response of curves on the canvas 73 * @param {canvas} HTML canvas element to draw frequency response 74 * @param {int} Nyquist frequency, in Hz 75 */ 76var DrawCanvas = function(canvas, nyquist) { 77 var HTML_TABLE_ROW_OFFSET = 2; 78 var topMargin = 30; 79 var leftMargin = 40; 80 var downMargin = 10; 81 var rightMargin = 30; 82 var width = canvas.width - leftMargin - rightMargin; 83 var height = canvas.height - topMargin - downMargin; 84 var canvasContext = canvas.getContext('2d'); 85 var pixelsPerDb = height / 96.0; 86 var noctaves = 10; 87 var curveBuffer = []; 88 89 findId = function(id) { 90 for (var i = 0; i < curveBuffer.length; i++) 91 if (curveBuffer[i].id == id) 92 return i; 93 return -1; 94 } 95 96 /** 97 * Adds curve on the canvas 98 * @param {AudioCurve} audio curve object 99 */ 100 this.add = function(audioCurve) { 101 curveBuffer.push(audioCurve); 102 addTableList(); 103 this.drawCanvas(); 104 } 105 106 /** 107 * Removes curve from the canvas 108 * @param {int} curve index 109 */ 110 this.remove = function(id) { 111 var index = findId(id); 112 if (index != -1) { 113 curveBuffer.splice(index, 1); 114 removeTableList(index); 115 this.drawCanvas(); 116 } 117 } 118 119 removeTableList = function(index) { 120 var table = document.getElementById('curve_table'); 121 table.deleteRow(index + HTML_TABLE_ROW_OFFSET); 122 } 123 124 addTableList = function() { 125 var table = document.getElementById('curve_table'); 126 var index = table.rows.length - HTML_TABLE_ROW_OFFSET; 127 var curve_id = curveBuffer[index].id; 128 var tr = table.insertRow(table.rows.length); 129 var tdCheckbox = tr.insertCell(0); 130 var tdFile = tr.insertCell(1); 131 var tdLeft = tr.insertCell(2); 132 var tdRight = tr.insertCell(3); 133 var tdRemove = tr.insertCell(4); 134 135 var checkbox = document.createElement('input'); 136 checkbox.setAttribute('type', 'checkbox'); 137 checkbox.checked = true; 138 checkbox.onclick = function() { 139 setCurveVisible(checkbox, curve_id, 'all'); 140 } 141 tdCheckbox.appendChild(checkbox); 142 tdFile.innerHTML = curveBuffer[index].filename; 143 144 var checkLeft = document.createElement('input'); 145 checkLeft.setAttribute('type', 'checkbox'); 146 checkLeft.checked = true; 147 checkLeft.onclick = function() { 148 setCurveVisible(checkLeft, curve_id, 0); 149 } 150 tdLeft.bgColor = curveBuffer[index].channel[0].curveColor; 151 tdLeft.appendChild(checkLeft); 152 153 if (curveBuffer[index].channel.length > 1) { 154 var checkRight = document.createElement('input'); 155 checkRight.setAttribute('type', 'checkbox'); 156 checkRight.checked = true; 157 checkRight.onclick = function() { 158 setCurveVisible(checkRight, curve_id, 1); 159 } 160 tdRight.bgColor = curveBuffer[index].channel[1].curveColor; 161 tdRight.appendChild(checkRight); 162 } 163 164 var btnRemove = document.createElement('input'); 165 btnRemove.setAttribute('type', 'button'); 166 btnRemove.value = 'Remove'; 167 btnRemove.onclick = function() { removeCurve(curve_id); } 168 tdRemove.appendChild(btnRemove); 169 } 170 171 /** 172 * Sets visibility of curves 173 * @param {boolean} visible or not 174 * @param {int} curve index 175 * @param {int,string} channel index. 176 */ 177 this.setVisible = function(checkbox, id, channel) { 178 var index = findId(id); 179 if (channel == 'all') { 180 for (var i = 0; i < curveBuffer[index].channel.length; i++) { 181 curveBuffer[index].channel[i].visible = checkbox.checked; 182 } 183 } else if (channel == 0 || channel == 1) { 184 curveBuffer[index].channel[channel].visible = checkbox.checked; 185 } 186 this.drawCanvas(); 187 } 188 189 /** 190 * Draws canvas background 191 */ 192 this.drawBg = function() { 193 var gridColor = 'rgb(200,200,200)'; 194 var textColor = 'rgb(238,221,130)'; 195 196 /* Draw the background */ 197 canvasContext.fillStyle = 'rgb(0, 0, 0)'; 198 canvasContext.fillRect(0, 0, canvas.width, canvas.height); 199 200 /* Draw frequency scale. */ 201 canvasContext.beginPath(); 202 canvasContext.lineWidth = 1; 203 canvasContext.strokeStyle = gridColor; 204 205 for (var octave = 0; octave <= noctaves; octave++) { 206 var x = octave * width / noctaves + leftMargin; 207 208 canvasContext.moveTo(x, topMargin); 209 canvasContext.lineTo(x, topMargin + height); 210 canvasContext.stroke(); 211 212 var f = nyquist * Math.pow(2.0, octave - noctaves); 213 canvasContext.textAlign = 'center'; 214 canvasContext.strokeText(f.toFixed(0) + 'Hz', x, 20); 215 } 216 217 /* Draw 0dB line. */ 218 canvasContext.beginPath(); 219 canvasContext.moveTo(leftMargin, topMargin + 0.5 * height); 220 canvasContext.lineTo(leftMargin + width, topMargin + 0.5 * height); 221 canvasContext.stroke(); 222 223 /* Draw decibel scale. */ 224 for (var db = -96.0; db <= 0; db += 12) { 225 var y = topMargin + height - (db + 96) * pixelsPerDb; 226 canvasContext.beginPath(); 227 canvasContext.setLineDash([1, 4]); 228 canvasContext.moveTo(leftMargin, y); 229 canvasContext.lineTo(leftMargin + width, y); 230 canvasContext.stroke(); 231 canvasContext.setLineDash([]); 232 canvasContext.strokeStyle = textColor; 233 canvasContext.strokeText(db.toFixed(0) + 'dB', 20, y); 234 canvasContext.strokeStyle = gridColor; 235 } 236 } 237 238 /** 239 * Draws a channel of a curve 240 * @param {Float32Array} fft buffer of a channel 241 * @param {string} curve color 242 * @param {int} sample rate 243 */ 244 this.drawCurve = function(buffer, curveColor, sampleRate) { 245 canvasContext.beginPath(); 246 canvasContext.lineWidth = 1; 247 canvasContext.strokeStyle = curveColor; 248 canvasContext.moveTo(leftMargin, topMargin + height); 249 250 for (var i = 0; i < buffer.length; ++i) { 251 var f = i * sampleRate / 2 / nyquist / buffer.length; 252 253 /* Convert to log frequency scale (octaves). */ 254 f = 1 + Math.log(f) / (noctaves * Math.LN2); 255 if (f < 0) { continue; } 256 /* Draw the magnitude */ 257 var x = f * width + leftMargin; 258 var value = Math.max(20 * Math.log(buffer[i]) / Math.LN10, -96); 259 var y = topMargin + height - ((value + 96) * pixelsPerDb); 260 261 canvasContext.lineTo(x, y); 262 } 263 canvasContext.stroke(); 264 } 265 266 /** 267 * Draws all curves 268 */ 269 this.drawCanvas = function() { 270 this.drawBg(); 271 for (var i = 0; i < curveBuffer.length; i++) { 272 for (var j = 0; j < curveBuffer[i].channel.length; j++) { 273 if (curveBuffer[i].channel[j].visible) { 274 this.drawCurve(curveBuffer[i].channel[j].fftBuffer, 275 curveBuffer[i].channel[j].curveColor, 276 curveBuffer[i].sampleRate); 277 } 278 } 279 } 280 } 281 282 /** 283 * Draws current buffer 284 * @param {Float32Array} left channel buffer 285 * @param {Float32Array} right channel buffer 286 * @param {int} sample rate 287 */ 288 this.drawInstantCurve = function(leftData, rightData, sampleRate) { 289 this.drawBg(); 290 var fftLeft = new FFT(leftData.length); 291 fftLeft.forward(leftData); 292 var fftRight = new FFT(rightData.length); 293 fftRight.forward(rightData); 294 this.drawCurve(fftLeft.spectrum, "#FF0000", sampleRate); 295 this.drawCurve(fftRight.spectrum, "#00FF00", sampleRate); 296 } 297 298 exportCurveByFreq = function(freqList) { 299 function calcIndex(freq, length, sampleRate) { 300 var idx = parseInt(freq * length * 2 / sampleRate); 301 return Math.min(idx, length - 1); 302 } 303 /* header */ 304 channelName = ['L', 'R']; 305 cvsString = 'freq'; 306 for (var i = 0; i < curveBuffer.length; i++) { 307 for (var j = 0; j < curveBuffer[i].channel.length; j++) { 308 cvsString += ',' + curveBuffer[i].filename + '_' + channelName[j]; 309 } 310 } 311 for (var i = 0; i < freqList.length; i++) { 312 cvsString += '\n' + freqList[i]; 313 for (var j = 0; j < curveBuffer.length; j++) { 314 var curve = curveBuffer[j]; 315 for (var k = 0; k < curve.channel.length; k++) { 316 var fftBuffer = curve.channel[k].fftBuffer; 317 var prevIdx = (i - 1 < 0) ? 0 : 318 calcIndex(freqList[i - 1], fftBuffer.length, curve.sampleRate); 319 var currIdx = calcIndex( 320 freqList[i], fftBuffer.length, curve.sampleRate); 321 322 var sum = 0; 323 for (var l = prevIdx; l <= currIdx; l++) { // Get average 324 var value = 20 * Math.log(fftBuffer[l]) / Math.LN10; 325 sum += value; 326 } 327 cvsString += ',' + sum / (currIdx - prevIdx + 1); 328 } 329 } 330 } 331 return cvsString; 332 } 333 334 /** 335 * Exports frequency response of curves into CSV format 336 * @param {int} point number in octaves 337 * @return {string} a string with CSV format 338 */ 339 this.exportCurve = function(nInOctaves) { 340 var freqList= []; 341 for (var i = 0; i < noctaves; i++) { 342 var fStart = nyquist * Math.pow(2.0, i - noctaves); 343 var fEnd = nyquist * Math.pow(2.0, i + 1 - noctaves); 344 var powerStart = Math.log(fStart) / Math.LN2; 345 var powerEnd = Math.log(fEnd) / Math.LN2; 346 for (var j = 0; j < nInOctaves; j++) { 347 f = Math.pow(2, 348 powerStart + j * (powerEnd - powerStart) / nInOctaves); 349 freqList.push(f); 350 } 351 } 352 freqList.push(nyquist); 353 return exportCurveByFreq(freqList); 354 } 355} 356 357window.DrawCanvas = DrawCanvas; 358 359/** 360 * FFT is a class for calculating the Discrete Fourier Transform of a signal 361 * with the Fast Fourier Transform algorithm. 362 * 363 * @param {Number} bufferSize The size of the sample buffer to be computed. 364 * Must be power of 2 365 * @constructor 366 */ 367function FFT(bufferSize) { 368 this.bufferSize = bufferSize; 369 this.spectrum = new Float32Array(bufferSize/2); 370 this.real = new Float32Array(bufferSize); 371 this.imag = new Float32Array(bufferSize); 372 373 this.reverseTable = new Uint32Array(bufferSize); 374 this.sinTable = new Float32Array(bufferSize); 375 this.cosTable = new Float32Array(bufferSize); 376 377 var limit = 1; 378 var bit = bufferSize >> 1; 379 var i; 380 381 while (limit < bufferSize) { 382 for (i = 0; i < limit; i++) { 383 this.reverseTable[i + limit] = this.reverseTable[i] + bit; 384 } 385 386 limit = limit << 1; 387 bit = bit >> 1; 388 } 389 390 for (i = 0; i < bufferSize; i++) { 391 this.sinTable[i] = Math.sin(-Math.PI/i); 392 this.cosTable[i] = Math.cos(-Math.PI/i); 393 } 394} 395 396/** 397 * Performs a forward transform on the sample buffer. 398 * Converts a time domain signal to frequency domain spectra. 399 * 400 * @param {Array} buffer The sample buffer. Buffer Length must be power of 2 401 * @returns The frequency spectrum array 402 */ 403FFT.prototype.forward = function(buffer) { 404 var bufferSize = this.bufferSize, 405 cosTable = this.cosTable, 406 sinTable = this.sinTable, 407 reverseTable = this.reverseTable, 408 real = this.real, 409 imag = this.imag, 410 spectrum = this.spectrum; 411 412 var k = Math.floor(Math.log(bufferSize) / Math.LN2); 413 414 if (Math.pow(2, k) !== bufferSize) { 415 throw "Invalid buffer size, must be a power of 2."; 416 } 417 if (bufferSize !== buffer.length) { 418 throw "Supplied buffer is not the same size as defined FFT. FFT Size: " 419 + bufferSize + " Buffer Size: " + buffer.length; 420 } 421 422 var halfSize = 1, 423 phaseShiftStepReal, 424 phaseShiftStepImag, 425 currentPhaseShiftReal, 426 currentPhaseShiftImag, 427 off, 428 tr, 429 ti, 430 tmpReal, 431 i; 432 433 for (i = 0; i < bufferSize; i++) { 434 real[i] = buffer[reverseTable[i]]; 435 imag[i] = 0; 436 } 437 438 while (halfSize < bufferSize) { 439 phaseShiftStepReal = cosTable[halfSize]; 440 phaseShiftStepImag = sinTable[halfSize]; 441 442 currentPhaseShiftReal = 1.0; 443 currentPhaseShiftImag = 0.0; 444 445 for (var fftStep = 0; fftStep < halfSize; fftStep++) { 446 i = fftStep; 447 448 while (i < bufferSize) { 449 off = i + halfSize; 450 tr = (currentPhaseShiftReal * real[off]) - 451 (currentPhaseShiftImag * imag[off]); 452 ti = (currentPhaseShiftReal * imag[off]) + 453 (currentPhaseShiftImag * real[off]); 454 real[off] = real[i] - tr; 455 imag[off] = imag[i] - ti; 456 real[i] += tr; 457 imag[i] += ti; 458 459 i += halfSize << 1; 460 } 461 462 tmpReal = currentPhaseShiftReal; 463 currentPhaseShiftReal = (tmpReal * phaseShiftStepReal) - 464 (currentPhaseShiftImag * phaseShiftStepImag); 465 currentPhaseShiftImag = (tmpReal * phaseShiftStepImag) + 466 (currentPhaseShiftImag * phaseShiftStepReal); 467 } 468 469 halfSize = halfSize << 1; 470 } 471 472 i = bufferSize / 2; 473 while(i--) { 474 spectrum[i] = 2 * Math.sqrt(real[i] * real[i] + imag[i] * imag[i]) / 475 bufferSize; 476 } 477}; 478 479function setCurveVisible(checkbox, id, channel) { 480 drawContext.setVisible(checkbox, id, channel); 481} 482 483function removeCurve(id) { 484 drawContext.remove(id); 485} 486