blob: 871c7643ece9db8cc0083f3c8a250ec8c59fec58 [file] [log] [blame]
/*
* 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);
}