/* * 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); }