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 /**
      8  * Gets a random color
      9  */
     10 function 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  */
     22 var 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 
     49 window.AudioChannel = AudioChannel;
     50 
     51 var numberOfCurve = 0;
     52 
     53 /**
     54  * Audio curve class
     55  */
     56 var 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 
     69 window.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  */
     76 var 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 
    357 window.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  */
    367 function 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  */
    403 FFT.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 
    479 function setCurveVisible(checkbox, id, channel) {
    480   drawContext.setVisible(checkbox, id, channel);
    481 }
    482 
    483 function removeCurve(id) {
    484   drawContext.remove(id);
    485 }
    486