• 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
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