Javascript  |  486行  |  13.84 KB

/*
 * Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

/**
 * Gets a random color
 */
function getRandomColor() {
  var letters = '0123456789ABCDEF'.split('');
  var color = '#';
  for (var i = 0; i < 6; i++) {
    color += letters[Math.floor(Math.random() * 16)];
  }
  return color;
}

/**
 * Audio channel class
 */
var AudioChannel = function(buffer) {
  this.init = function(buffer) {
    this.buffer = buffer;
    this.fftBuffer = this.toFFT(this.buffer);
    this.curveColor = getRandomColor();
    this.visible = true;
  }

  this.toFFT = function(buffer) {
    var k = Math.ceil(Math.log(buffer.length) / Math.LN2);
    var length = Math.pow(2, k);
    var tmpBuffer = new Float32Array(length);

    for (var i = 0; i < buffer.length; i++) {
      tmpBuffer[i] = buffer[i];
    }
    for (var i = buffer.length; i < length; i++) {
      tmpBuffer[i] = 0;
    }
    var fft = new FFT(length);
    fft.forward(tmpBuffer);
    return fft.spectrum;
  }

  this.init(buffer);
}

window.AudioChannel = AudioChannel;

var numberOfCurve = 0;

/**
 * Audio curve class
 */
var AudioCurve = function(buffers, filename, sampleRate) {
  this.init = function(buffers, filename) {
    this.filename = filename;
    this.id = numberOfCurve++;
    this.sampleRate = sampleRate;
    this.channel = [];
    for (var i = 0; i < buffers.length; i++) {
      this.channel.push(new AudioChannel(buffers[i]));
    }
  }
  this.init(buffers, filename);
}

window.AudioCurve = AudioCurve;

/**
 * Draw frequency response of curves on the canvas
 * @param {canvas} HTML canvas element to draw frequency response
 * @param {int} Nyquist frequency, in Hz
 */
var DrawCanvas = function(canvas, nyquist) {
  var HTML_TABLE_ROW_OFFSET = 2;
  var topMargin = 30;
  var leftMargin = 40;
  var downMargin = 10;
  var rightMargin = 30;
  var width = canvas.width - leftMargin - rightMargin;
  var height = canvas.height - topMargin - downMargin;
  var canvasContext = canvas.getContext('2d');
  var pixelsPerDb = height / 96.0;
  var noctaves = 10;
  var curveBuffer = [];

  findId = function(id) {
    for (var i = 0; i < curveBuffer.length; i++)
      if (curveBuffer[i].id == id)
        return i;
    return -1;
  }

  /**
   * Adds curve on the canvas
   * @param {AudioCurve} audio curve object
   */
  this.add = function(audioCurve) {
    curveBuffer.push(audioCurve);
    addTableList();
    this.drawCanvas();
  }

  /**
   * Removes curve from the canvas
   * @param {int} curve index
   */
  this.remove = function(id) {
    var index = findId(id);
    if (index != -1) {
      curveBuffer.splice(index, 1);
      removeTableList(index);
      this.drawCanvas();
    }
  }

  removeTableList = function(index) {
    var table = document.getElementById('curve_table');
    table.deleteRow(index + HTML_TABLE_ROW_OFFSET);
  }

  addTableList = function() {
    var table = document.getElementById('curve_table');
    var index = table.rows.length - HTML_TABLE_ROW_OFFSET;
    var curve_id = curveBuffer[index].id;
    var tr = table.insertRow(table.rows.length);
    var tdCheckbox = tr.insertCell(0);
    var tdFile = tr.insertCell(1);
    var tdLeft = tr.insertCell(2);
    var tdRight = tr.insertCell(3);
    var tdRemove = tr.insertCell(4);

    var checkbox = document.createElement('input');
    checkbox.setAttribute('type', 'checkbox');
    checkbox.checked = true;
    checkbox.onclick = function() {
      setCurveVisible(checkbox, curve_id, 'all');
    }
    tdCheckbox.appendChild(checkbox);
    tdFile.innerHTML = curveBuffer[index].filename;

    var checkLeft = document.createElement('input');
    checkLeft.setAttribute('type', 'checkbox');
    checkLeft.checked = true;
    checkLeft.onclick = function() {
      setCurveVisible(checkLeft, curve_id, 0);
    }
    tdLeft.bgColor = curveBuffer[index].channel[0].curveColor;
    tdLeft.appendChild(checkLeft);

    if (curveBuffer[index].channel.length > 1) {
      var checkRight = document.createElement('input');
      checkRight.setAttribute('type', 'checkbox');
      checkRight.checked = true;
      checkRight.onclick = function() {
        setCurveVisible(checkRight, curve_id, 1);
      }
      tdRight.bgColor = curveBuffer[index].channel[1].curveColor;
      tdRight.appendChild(checkRight);
    }

    var btnRemove = document.createElement('input');
    btnRemove.setAttribute('type', 'button');
    btnRemove.value = 'Remove';
    btnRemove.onclick = function() { removeCurve(curve_id); }
    tdRemove.appendChild(btnRemove);
  }

  /**
   * Sets visibility of curves
   * @param {boolean} visible or not
   * @param {int} curve index
   * @param {int,string} channel index.
   */
  this.setVisible = function(checkbox, id, channel) {
    var index = findId(id);
    if (channel == 'all') {
      for (var i = 0; i < curveBuffer[index].channel.length; i++) {
        curveBuffer[index].channel[i].visible = checkbox.checked;
      }
    } else if (channel == 0 || channel == 1) {
      curveBuffer[index].channel[channel].visible = checkbox.checked;
    }
    this.drawCanvas();
  }

  /**
   * Draws canvas background
   */
  this.drawBg = function() {
    var gridColor = 'rgb(200,200,200)';
    var textColor = 'rgb(238,221,130)';

    /* Draw the background */
    canvasContext.fillStyle = 'rgb(0, 0, 0)';
    canvasContext.fillRect(0, 0, canvas.width, canvas.height);

    /* Draw frequency scale. */
    canvasContext.beginPath();
    canvasContext.lineWidth = 1;
    canvasContext.strokeStyle = gridColor;

    for (var octave = 0; octave <= noctaves; octave++) {
      var x = octave * width / noctaves + leftMargin;

      canvasContext.moveTo(x, topMargin);
      canvasContext.lineTo(x, topMargin + height);
      canvasContext.stroke();

      var f = nyquist * Math.pow(2.0, octave - noctaves);
      canvasContext.textAlign = 'center';
      canvasContext.strokeText(f.toFixed(0) + 'Hz', x, 20);
    }

    /* Draw 0dB line. */
    canvasContext.beginPath();
    canvasContext.moveTo(leftMargin, topMargin + 0.5 * height);
    canvasContext.lineTo(leftMargin + width, topMargin + 0.5 * height);
    canvasContext.stroke();

    /* Draw decibel scale. */
    for (var db = -96.0; db <= 0; db += 12) {
      var y = topMargin + height - (db + 96) * pixelsPerDb;
      canvasContext.beginPath();
      canvasContext.setLineDash([1, 4]);
      canvasContext.moveTo(leftMargin, y);
      canvasContext.lineTo(leftMargin + width, y);
      canvasContext.stroke();
      canvasContext.setLineDash([]);
      canvasContext.strokeStyle = textColor;
      canvasContext.strokeText(db.toFixed(0) + 'dB', 20, y);
      canvasContext.strokeStyle = gridColor;
    }
  }

  /**
   * Draws a channel of a curve
   * @param {Float32Array} fft buffer of a channel
   * @param {string} curve color
   * @param {int} sample rate
   */
  this.drawCurve = function(buffer, curveColor, sampleRate) {
    canvasContext.beginPath();
    canvasContext.lineWidth = 1;
    canvasContext.strokeStyle = curveColor;
    canvasContext.moveTo(leftMargin, topMargin + height);

    for (var i = 0; i < buffer.length; ++i) {
      var f = i * sampleRate / 2 / nyquist / buffer.length;

      /* Convert to log frequency scale (octaves). */
      f = 1 + Math.log(f) / (noctaves * Math.LN2);
      if (f < 0) { continue; }
      /* Draw the magnitude */
      var x = f * width + leftMargin;
      var value = Math.max(20 * Math.log(buffer[i]) / Math.LN10, -96);
      var y = topMargin + height - ((value + 96) * pixelsPerDb);

      canvasContext.lineTo(x, y);
    }
    canvasContext.stroke();
  }

  /**
   * Draws all curves
   */
  this.drawCanvas = function() {
    this.drawBg();
    for (var i = 0; i < curveBuffer.length; i++) {
      for (var j = 0; j < curveBuffer[i].channel.length; j++) {
        if (curveBuffer[i].channel[j].visible) {
          this.drawCurve(curveBuffer[i].channel[j].fftBuffer,
                         curveBuffer[i].channel[j].curveColor,
                         curveBuffer[i].sampleRate);
        }
      }
    }
  }

  /**
   * Draws current buffer
   * @param {Float32Array} left channel buffer
   * @param {Float32Array} right channel buffer
   * @param {int} sample rate
   */
  this.drawInstantCurve = function(leftData, rightData, sampleRate) {
    this.drawBg();
    var fftLeft = new FFT(leftData.length);
    fftLeft.forward(leftData);
    var fftRight = new FFT(rightData.length);
    fftRight.forward(rightData);
    this.drawCurve(fftLeft.spectrum, "#FF0000", sampleRate);
    this.drawCurve(fftRight.spectrum, "#00FF00", sampleRate);
  }

  exportCurveByFreq = function(freqList) {
    function calcIndex(freq, length, sampleRate) {
      var idx = parseInt(freq * length * 2 / sampleRate);
      return Math.min(idx, length - 1);
    }
    /* header */
    channelName = ['L', 'R'];
    cvsString = 'freq';
    for (var i = 0; i < curveBuffer.length; i++) {
      for (var j = 0; j < curveBuffer[i].channel.length; j++) {
        cvsString += ',' + curveBuffer[i].filename + '_' + channelName[j];
      }
    }
    for (var i = 0; i < freqList.length; i++) {
      cvsString += '\n' + freqList[i];
      for (var j = 0; j < curveBuffer.length; j++) {
        var curve = curveBuffer[j];
        for (var k = 0; k < curve.channel.length; k++) {
          var fftBuffer = curve.channel[k].fftBuffer;
          var prevIdx = (i - 1 < 0) ? 0 :
              calcIndex(freqList[i - 1], fftBuffer.length, curve.sampleRate);
          var currIdx = calcIndex(
              freqList[i], fftBuffer.length, curve.sampleRate);

          var sum = 0;
          for (var l = prevIdx; l <= currIdx; l++) { // Get average
            var value = 20 * Math.log(fftBuffer[l]) / Math.LN10;
            sum += value;
          }
          cvsString += ',' + sum / (currIdx - prevIdx + 1);
        }
      }
    }
    return cvsString;
  }

  /**
   * Exports frequency response of curves into CSV format
   * @param {int} point number in octaves
   * @return {string} a string with CSV format
   */
  this.exportCurve = function(nInOctaves) {
    var freqList= [];
    for (var i = 0; i < noctaves; i++) {
      var fStart = nyquist * Math.pow(2.0, i - noctaves);
      var fEnd = nyquist * Math.pow(2.0, i + 1 - noctaves);
      var powerStart = Math.log(fStart) / Math.LN2;
      var powerEnd = Math.log(fEnd) / Math.LN2;
      for (var j = 0; j < nInOctaves; j++) {
        f = Math.pow(2,
            powerStart + j * (powerEnd - powerStart) / nInOctaves);
        freqList.push(f);
      }
    }
    freqList.push(nyquist);
    return exportCurveByFreq(freqList);
  }
}

window.DrawCanvas = DrawCanvas;

/**
 * FFT is a class for calculating the Discrete Fourier Transform of a signal
 * with the Fast Fourier Transform algorithm.
 *
 * @param {Number} bufferSize The size of the sample buffer to be computed.
 * Must be power of 2
 * @constructor
 */
function FFT(bufferSize) {
  this.bufferSize = bufferSize;
  this.spectrum   = new Float32Array(bufferSize/2);
  this.real       = new Float32Array(bufferSize);
  this.imag       = new Float32Array(bufferSize);

  this.reverseTable = new Uint32Array(bufferSize);
  this.sinTable = new Float32Array(bufferSize);
  this.cosTable = new Float32Array(bufferSize);

  var limit = 1;
  var bit = bufferSize >> 1;
  var i;

  while (limit < bufferSize) {
    for (i = 0; i < limit; i++) {
      this.reverseTable[i + limit] = this.reverseTable[i] + bit;
    }

    limit = limit << 1;
    bit = bit >> 1;
  }

  for (i = 0; i < bufferSize; i++) {
    this.sinTable[i] = Math.sin(-Math.PI/i);
    this.cosTable[i] = Math.cos(-Math.PI/i);
  }
}

/**
 * Performs a forward transform on the sample buffer.
 * Converts a time domain signal to frequency domain spectra.
 *
 * @param {Array} buffer The sample buffer. Buffer Length must be power of 2
 * @returns The frequency spectrum array
 */
FFT.prototype.forward = function(buffer) {
  var bufferSize      = this.bufferSize,
      cosTable        = this.cosTable,
      sinTable        = this.sinTable,
      reverseTable    = this.reverseTable,
      real            = this.real,
      imag            = this.imag,
      spectrum        = this.spectrum;

  var k = Math.floor(Math.log(bufferSize) / Math.LN2);

  if (Math.pow(2, k) !== bufferSize) {
    throw "Invalid buffer size, must be a power of 2.";
  }
  if (bufferSize !== buffer.length) {
    throw "Supplied buffer is not the same size as defined FFT. FFT Size: "
        + bufferSize + " Buffer Size: " + buffer.length;
  }

  var halfSize = 1,
      phaseShiftStepReal,
      phaseShiftStepImag,
      currentPhaseShiftReal,
      currentPhaseShiftImag,
      off,
      tr,
      ti,
      tmpReal,
      i;

  for (i = 0; i < bufferSize; i++) {
    real[i] = buffer[reverseTable[i]];
    imag[i] = 0;
  }

  while (halfSize < bufferSize) {
    phaseShiftStepReal = cosTable[halfSize];
    phaseShiftStepImag = sinTable[halfSize];

    currentPhaseShiftReal = 1.0;
    currentPhaseShiftImag = 0.0;

    for (var fftStep = 0; fftStep < halfSize; fftStep++) {
      i = fftStep;

      while (i < bufferSize) {
        off = i + halfSize;
        tr = (currentPhaseShiftReal * real[off]) -
            (currentPhaseShiftImag * imag[off]);
        ti = (currentPhaseShiftReal * imag[off]) +
            (currentPhaseShiftImag * real[off]);
        real[off] = real[i] - tr;
        imag[off] = imag[i] - ti;
        real[i] += tr;
        imag[i] += ti;

        i += halfSize << 1;
      }

      tmpReal = currentPhaseShiftReal;
      currentPhaseShiftReal = (tmpReal * phaseShiftStepReal) -
          (currentPhaseShiftImag * phaseShiftStepImag);
      currentPhaseShiftImag = (tmpReal * phaseShiftStepImag) +
          (currentPhaseShiftImag * phaseShiftStepReal);
    }

    halfSize = halfSize << 1;
  }

  i = bufferSize / 2;
  while(i--) {
    spectrum[i] = 2 * Math.sqrt(real[i] * real[i] + imag[i] * imag[i]) /
        bufferSize;
  }
};

function setCurveVisible(checkbox, id, channel) {
  drawContext.setVisible(checkbox, id, channel);
}

function removeCurve(id) {
  drawContext.remove(id);
}