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