diff options
Diffstat (limited to 'scripts/mic_testing/frontend/analysis.js')
-rw-r--r-- | scripts/mic_testing/frontend/analysis.js | 485 |
1 files changed, 485 insertions, 0 deletions
diff --git a/scripts/mic_testing/frontend/analysis.js b/scripts/mic_testing/frontend/analysis.js new file mode 100644 index 00000000..871c7643 --- /dev/null +++ b/scripts/mic_testing/frontend/analysis.js @@ -0,0 +1,485 @@ +/* + * 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); +} |