summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorhenryhsu <henryhsu@chromium.org>2014-06-23 17:55:07 +0800
committerchrome-internal-fetch <chrome-internal-fetch@google.com>2014-06-24 20:08:58 +0000
commit176578e28066262d983ec67896be50d49cf86e0a (patch)
treef76394f46c04177293df927e94859bf4913192fe /scripts
parentfdf87598f83c8e428d90453b7634fa4a64de22e4 (diff)
downloadadhd-176578e28066262d983ec67896be50d49cf86e0a.tar.gz
Add partner microphone test website
BUG=chrome-os-partner:29863 TEST=manually test Change-Id: I95f6d724d20c11c97735d424759101b8fdf09097 Reviewed-on: https://chromium-review.googlesource.com/205210 Reviewed-by: Hsinyu Chao <hychao@chromium.org> Commit-Queue: Heng-ruey Hsu <henryhsu@google.com> Tested-by: Heng-ruey Hsu <henryhsu@google.com>
Diffstat (limited to 'scripts')
-rw-r--r--scripts/mic_testing/frontend/LICENSE27
-rw-r--r--scripts/mic_testing/frontend/analysis.js485
-rw-r--r--scripts/mic_testing/frontend/app.yaml30
-rw-r--r--scripts/mic_testing/frontend/audio.css107
-rw-r--r--scripts/mic_testing/frontend/audio.html184
-rw-r--r--scripts/mic_testing/frontend/audio.js363
-rw-r--r--scripts/mic_testing/frontend/recorder.js366
-rw-r--r--scripts/mic_testing/frontend/source.js222
8 files changed, 1784 insertions, 0 deletions
diff --git a/scripts/mic_testing/frontend/LICENSE b/scripts/mic_testing/frontend/LICENSE
new file mode 100644
index 00000000..0aa7fc93
--- /dev/null
+++ b/scripts/mic_testing/frontend/LICENSE
@@ -0,0 +1,27 @@
+// Copyright (c) 2006-2009 The Chromium OS Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
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);
+}
diff --git a/scripts/mic_testing/frontend/app.yaml b/scripts/mic_testing/frontend/app.yaml
new file mode 100644
index 00000000..723472e1
--- /dev/null
+++ b/scripts/mic_testing/frontend/app.yaml
@@ -0,0 +1,30 @@
+application: mic-testing
+version: 1
+runtime: python27
+api_version: 1
+threadsafe: true
+
+handlers:
+- url: /(.*\.css)
+ mime_type: text/css
+ static_files: \1
+ upload: (.*\.css)
+
+- url: /(.*\.html)
+ mime_type: text/html
+ static_files: \1
+ upload: (.*\.html)
+
+- url: /(.*\.js)
+ mime_type: text/javascript
+ static_files: \1
+ upload: (.*\.js)
+
+- url: /(LICENSE)
+ mime_type: text/plain
+ static_files: \1
+ upload: (LICENSE)
+
+- url: /
+ static_files: audio.html
+ upload: audio.html
diff --git a/scripts/mic_testing/frontend/audio.css b/scripts/mic_testing/frontend/audio.css
new file mode 100644
index 00000000..9bd2adc4
--- /dev/null
+++ b/scripts/mic_testing/frontend/audio.css
@@ -0,0 +1,107 @@
+/*
+ * 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.
+ */
+
+ul {
+ margin: 0;
+ padding: 0;
+}
+
+li {
+ list-style-type: none;
+}
+
+.tab-page {
+ display: table;
+ table-layout: fixed;
+ text-align: center;
+ width: 100%;
+}
+
+.tab-page li {
+ display: table-cell;
+ padding-right: 1px;
+ height: auto;
+ vertical-align: bottom;
+}
+
+.tab-page a {
+ font-size: x-large;
+ font-weight: bold;
+ display: block;
+ min-height: 100%;
+ padding: 4px 10px;
+ background-color: #FFFFFF;
+ color: black;
+ border-radius: 6px 6px 0 0;
+ border: 1px solid black;
+}
+
+.tab-page li.selected a {
+ font-size: x-large;
+ font-weight: bold;
+ display: block;
+ min-height: 100%;
+ padding: 4px 10px;
+ background-color: #FFFFFF;
+ color: black;
+ border-radius: 6px 6px 0 0;
+ border: 1px solid black;
+ border-bottom: none;
+}
+
+body {
+ text-align: center;
+}
+
+td {
+ text-align: center;
+ vertical-align: middle;
+}
+
+.btn-on-text {
+ display: none;
+}
+
+.btn-off-text {
+ display: none;
+}
+
+.btn-on .btn-on-text {
+ display: block;
+ font-size: large;
+}
+
+.btn-off .btn-off-text {
+ display: block;
+ font-size: large;
+}
+
+.tonegen-vol-cell input {
+ width: 20px;
+ height: 120px;
+ -webkit-appearance: slider-vertical;
+}
+
+.tonegen-vol-cell {
+ width: 15%;
+}
+
+.tonegen-main-cell {
+ width: 80%;
+ text-align: center;
+ vertical-align: middle;
+}
+
+.sweep_tone {
+ display: block;
+ text-align: center;
+ vertical-align: middle;
+}
+
+.canvas_detail {
+ vertical-align:top;
+ display: none;
+}
diff --git a/scripts/mic_testing/frontend/audio.html b/scripts/mic_testing/frontend/audio.html
new file mode 100644
index 00000000..2bd826a9
--- /dev/null
+++ b/scripts/mic_testing/frontend/audio.html
@@ -0,0 +1,184 @@
+<!-- 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. -->
+
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <title>Audio Quality Test</title>
+ <script type="text/javascript" src="audio.js"></script>
+ <script type="text/javascript" src="source.js"></script>
+ <script type="text/javascript" src="recorder.js"></script>
+ <script type="text/javascript" src="analysis.js"></script>
+ <link rel="stylesheet" type="text/css" href="audio.css">
+ </head>
+ <body>
+ <ul class="tab-page">
+ <li id="play_tab" onclick="switchTab('play_tab')" class="selected"><a>Play</a></li>
+ <li id="record_tab" onclick="switchTab('record_tab')"><a>Record</a></li>
+ </ul>
+ <div id="play_div">
+ <h1>Audio Source</h1>
+ <input type="radio" name="audio_source" value="sine"
+ onclick="setupSourceLayer('sine')"> Sine Tone
+ <input type="radio" name="audio_source" value="sweep"
+ onclick="setupSourceLayer('sweep')" checked> Sweep Tone
+ <input type="radio" name="audio_source" value="file"
+ onclick="setupSourceLayer('file')"> Load from file
+ <br>
+ <br>
+ <div id="source_layer" align="center">
+ <div id="source_tone">
+ <table style="width: 50%;">
+ <tr>
+ <td class="tonegen-main-cell">
+ <table style="width: 100%;">
+ <tr>
+ <td width="50%">Frequency: (20 Hz ~ SampleRate / 2)</td>
+ <td width="30%">Duration</td>
+ <td class="sweep_tone">Log</td>
+ </tr>
+ <tr>
+ <td>
+ <input type="text" id="freq_start" size=5 maxlength=5 value=1000> Hz
+ <div class="sweep_tone">
+ <input type="text" id="freq_end" size=5 maxlength=5 value=1000> Hz
+ </div>
+ </td>
+ <td>
+ <input type="text" id="tone_sec" size=5 maxlength=5 value=3> Seconds
+ </td>
+ <td class="sweep_tone">
+ <input type="checkbox" id="sweep_log" disabled>
+ </td>
+ </tr>
+ </table>
+ </td>
+ <td class="tonegen-vol-cell">
+ <input type="range" min="0" max="20" value="20" id="left_gain"
+ onchange="gainChanged();"/>
+ <input type="range" min="0" max="20" value="20" id="right_gain"
+ onchange="gainChanged();"/>
+ <br>
+ <div id="gain_label">
+ L(20) / R(20)
+ </div>
+ </td>
+ </tr>
+ </table>
+ </div>
+ <div id="source_file">
+ <input type="button" value="Local Audio File" onclick="loadAudioFile()"
+ style="font-size: large;">
+ <br><br>
+ <div style="display:none">
+ <input type=file id=audio_file onchange="changeAudioFile()">
+ </div>
+ </div>
+ </div>
+ <hr>
+ <input type="checkbox" id="append_tone" checked> Append 1K Hz start tone and end tone
+ <br><br>
+ <button id="play_audio" class="btn-off" onclick="playAudioFile()">
+ <div>
+ <span class="btn-off-text">Play</span>
+ <span class="btn-on-text">Stop Play</span>
+ </div>
+ </button>
+ </div>
+ <div id="record_div" style="display: none;">
+ <h1> Record Samples </h1>
+ <input type="radio" name="record_source" value="audio_source"
+ onclick="setupRecordSource('audio')">
+ Play Audio Source
+ <input type="radio" name="record_source" value="microphone" checked
+ onclick="setupRecordSource('microphone')">
+ Microphone
+ <br><br>
+ <input type="checkbox" id="detect_tone" checked> Detect 1K Hz start tone and end tone
+ <input type="checkbox" id="auto_stop" checked> Auto stop when detected 1K Hz end tone
+ <br><br>
+ <button id="record_btn" class="btn-off" onclick="recordButtonClicked()">
+ <div>
+ <span class="btn-off-text">Start Record</span>
+ <span class="btn-on-text">Stop Record</span>
+ </div>
+ </button>
+ <br><hr>
+ <table align="center">
+ <tr>
+ <td>
+ <div style="overflow:auto; max-height: 250px;">
+ <table id="record_list" align="center">
+ </table>
+ </div>
+ </td>
+ </tr>
+ </table>
+
+ </div>
+
+ <h1> Frequency Response </h1>
+ <table align="center">
+ <tr>
+ <td width=800>
+ <div id="curve_section">
+ <canvas id='fr_canvas' width=800 height=300>
+ </div>
+ </td>
+ <td width=400 height=300 class="canvas_detail">
+ <div id="curve_list" style="overflow:auto; max-height:300px;">
+ <table id="curve_table" width=100% height=100%>
+ <tr>
+ <td colspan=1>
+ <input type="button" value="Load File" onClick="loadButtonClicked();">
+ <div style="display:none">
+ <input type=file id=sample_file onchange="loadSampleFile()">
+ </div>
+ </td>
+ <td colspan=4>
+ <select id="noctaves">
+ <option value="3">1/3</option>
+ <option value="4">1/4</option>
+ <option value="5" selected>1/5</option>
+ <option value="6">1/6</option>
+ <option value="7">1/7</option>
+ <option value="8">1/8</option>
+ <option value="9">1/9</option>
+ <option value="10">1/10</option>
+ </select> Octaves
+ <a id='export_csv'>
+ <input type="button" value="Export CSV" onClick="exportCSV();">
+ </a>
+ </td>
+ </tr>
+ <tr>
+ <td>Show</td>
+ <td>File name</td>
+ <td width=50>Left</td>
+ <td width=50>Right</td>
+ <td>Remove</td>
+ </tr>
+ </table>
+ </div>
+ </td>
+ </tr>
+ </table>
+
+ <div id="debug" style="display:none;">
+ <h1>Debug</h1>
+ <a id='export_freq'>
+ <input type="button" value="Export Freq" onClick="exportFreq();">
+ </a>
+ <a id='export_buffer'>
+ <input type="button" value="Export Buffer">
+ </a>
+ </div>
+
+ <div id="log" style="display:none;">
+ <h1>Log</h1>
+ <pre id="log"></pre>
+ </div>
+
+ </body>
+</html>
diff --git a/scripts/mic_testing/frontend/audio.js b/scripts/mic_testing/frontend/audio.js
new file mode 100644
index 00000000..86974e73
--- /dev/null
+++ b/scripts/mic_testing/frontend/audio.js
@@ -0,0 +1,363 @@
+/*
+ * 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.
+ */
+
+var FFT_SIZE = 2048;
+
+var audioContext;
+var tonegen;
+var recorder;
+var drawContext;
+var audioPlay, audioBuffer;
+var audioSourceType = "sweep";
+var recordSourceType = "microphone";
+
+/**
+ * Switches Play/Record tab
+ * @param {string} tab name
+ */
+function switchTab(tabName) {
+ var canvas_detail = document.getElementsByClassName('canvas_detail');
+ switch (tabName) {
+ case 'play_tab':
+ document.getElementById('record_tab').setAttribute('class', '');
+ document.getElementById('record_div').style.display = 'none';
+ document.getElementById('play_div').style.display = 'block';
+ for (var i = 0; i < canvas_detail.length; i++) {
+ canvas_detail[i].style.display = "none";
+ }
+ drawContext.drawBg();
+ break;
+ case 'record_tab':
+ document.getElementById('play_tab').setAttribute('class', '');
+ document.getElementById('play_div').style.display = 'none';
+ document.getElementById('record_div').style.display = 'block';
+ for (var i = 0; i < canvas_detail.length; i++) {
+ canvas_detail[i].style.display = "block";
+ }
+ drawContext.drawCanvas();
+ break;
+ }
+ document.getElementById(tabName).setAttribute('class', 'selected');
+}
+
+function __log(e, data) {
+ log.innerHTML += "\n" + e + " " + (data || '');
+}
+
+function startUserMedia(stream) {
+ var input = audioContext.createMediaStreamSource(stream);
+ recorder = new Recorder(input);
+}
+
+window.onload = function init() {
+ setupSourceLayer(audioSourceType);
+ try {
+ // webkit shim
+ window.AudioContext = window.AudioContext || window.webkitAudioContext;
+ navigator.getUserMedia = navigator.getUserMedia ||
+ navigator.webkitGetUserMedia;
+ window.URL = window.URL || window.webkitURL;
+
+ audioContext = new AudioContext;
+ } catch (e) {
+ alert('No web audio support in this browser!');
+ }
+
+ navigator.getUserMedia({audio: true}, startUserMedia, function(e) {
+ alert('No live audio input: ' + e);
+ });
+
+ /* Initialize global objects */
+ tonegen = new ToneGen();
+ audioPlay = new AudioPlay();
+
+ var canvas = document.getElementById('fr_canvas');
+ drawContext = new DrawCanvas(canvas, audioContext.sampleRate / 2);
+ drawContext.drawBg();
+};
+
+/* For Play tab */
+
+/**
+ * Sets audio source layer
+ * @param {string} audio source type
+ */
+function setupSourceLayer(value) {
+ var sourceTone = document.getElementById('source_tone');
+ var sourceFile = document.getElementById('source_file');
+ var sweepTone = document.getElementsByClassName('sweep_tone');
+ audioSourceType = value;
+ switch (value) {
+ case 'sine':
+ for (var i = 0; i < sweepTone.length; i++) {
+ sweepTone[i].style.display = "none";
+ }
+ document.getElementById('freq_start').value = 1000;
+ document.getElementById('freq_end').value = 1000;
+ sourceTone.style.display = "block";
+ sourceFile.style.display = "none";
+ document.getElementById('play_audio').disabled = false;
+ break;
+ case 'sweep':
+ for (var i = 0; i < sweepTone.length; i++) {
+ sweepTone[i].style.display = "block";
+ }
+ document.getElementById('freq_start').value = 20;
+ document.getElementById('freq_end').value = 12000;
+ sourceTone.style.display = "block";
+ sourceFile.style.display = "none";
+ document.getElementById('play_audio').disabled = false;
+ break;
+ case 'file':
+ sourceTone.style.display = "none";
+ sourceFile.style.display = "block";
+ document.getElementById('play_audio').disabled = true;
+ break;
+ }
+}
+
+/**
+ * Sets left/right gain
+ */
+function gainChanged() {
+ var leftGain = document.getElementById('left_gain').value;
+ var rightGain = document.getElementById('right_gain').value;
+ var gainLabel = document.getElementById('gain_label');
+ gainLabel.innerHTML = 'L(' + leftGain + ') / R(' + rightGain + ')';
+}
+
+/**
+ * Checks sine tone generator parameters and sets audio buffer
+ */
+function toneValueCheckSet() {
+ var passed = true;
+ var freqStart = parseInt(document.getElementById('freq_start').value);
+ var freqEnd = parseInt(document.getElementById('freq_end').value);
+ var duration = parseFloat(document.getElementById('tone_sec').value);
+ var leftGain = parseInt(document.getElementById('left_gain').value);
+ var rightGain = parseInt(document.getElementById('right_gain').value);
+ var sweepLog = document.getElementById('sweep_log').checked;
+
+ function isNumber(value, msg) {
+ if (isNaN(value) || value <= 0) {
+ alert(msg);
+ passed = false;
+ }
+ }
+
+ if (audioSourceType == 'sine') {
+ freqEnd = freqStart;
+ }
+
+ isNumber(freqStart, "Start frequency should be a positive number.");
+ isNumber(freqEnd, "Stop frequency should be a positive number.");
+ isNumber(duration, "Duration should be a positive number.");
+ if (freqEnd > audioContext.sampleRate / 2) {
+ alert('Stop frequency is too large.');
+ passed = false;
+ }
+ if (freqStart < 20) {
+ alert('Start frequency is too small.');
+ passed = false;
+ }
+ if (passed) {
+ /* Passed value check and generate tone buffer */
+ tonegen.setFreq(freqStart, freqEnd, sweepLog);
+ tonegen.setDuration(duration);
+ tonegen.setGain(leftGain / 20, rightGain / 20);
+ tonegen.setSampleRate(audioContext.sampleRate);
+ tonegen.genBuffer();
+ var buffer = tonegen.getBuffer();
+ audioPlay.setBuffer(buffer, document.getElementById('append_tone').checked);
+ }
+ return passed;
+}
+
+function loadAudioFile() {
+ document.getElementById('audio_file').click();
+}
+
+/**
+ * Loads audio file from local drive
+ */
+function changeAudioFile() {
+ function loadAudioDone(filename, buffer) {
+ audioBuffer = buffer;
+ document.getElementById('play_audio').disabled = false;
+ }
+ var input = document.getElementById('audio_file');
+ document.getElementById('play_audio').disabled = true;
+ audioPlay.loadFile(input.files[0], loadAudioDone);
+ input.value = '';
+}
+
+/**
+ * Play audio according source type
+ */
+function playAudioFile() {
+ /**
+ * Callback function to draw frequency response of current buffer
+ */
+ function getInstantBuffer(leftData, rightData, sampleRate) {
+ drawContext.drawInstantCurve(leftData, rightData, sampleRate);
+ }
+
+ var btn = document.getElementById('play_audio');
+ var append = document.getElementById('append_tone').checked;
+ if (btn.className == 'btn-off') {
+ switch (audioSourceType) {
+ case 'sine':
+ case 'sweep':
+ if (toneValueCheckSet()) {
+ audioPlay.play(playAudioFile, getInstantBuffer);
+ btn.className = 'btn-on';
+ }
+ break;
+ case 'file':
+ audioPlay.setBuffer(audioBuffer, append);
+ audioPlay.play(playAudioFile, getInstantBuffer);
+ btn.className = 'btn-on';
+ break;
+ }
+ } else {
+ audioPlay.stop();
+ btn.className = 'btn-off';
+ drawContext.drawBg();
+ }
+}
+
+/* For Record tab */
+
+/**
+ * Sets record source type
+ * @param {string} record source type
+ */
+function setupRecordSource(value) {
+ recordSourceType = value;
+ var autoStop = document.getElementById('auto_stop');
+ if (value == 'audio') {
+ autoStop.disabled = true;
+ autoStop.checked = false;
+ } else {
+ autoStop.disabled = false;
+ autoStop.checked = true;
+ }
+}
+
+function loadButtonClicked() {
+ document.getElementById('sample_file').click();
+}
+
+/**
+ * Loads sample file to draw frequency response curve into canvas
+ */
+function loadSampleFile() {
+ /**
+ * Callback function when file loaded
+ * @param {string} file name
+ * @param {AudioBuffer} file buffer
+ */
+ function addFileToCanvas(filename, buffer) {
+ var newBuffer = [];
+ for (var i = 0; i < buffer.numberOfChannels; i++) {
+ newBuffer.push(buffer.getChannelData(i));
+ }
+ drawContext.add(new AudioCurve(newBuffer, filename, buffer.sampleRate));
+ }
+ var input = document.getElementById('sample_file');
+ audioPlay.loadFile(input.files[0], addFileToCanvas);
+ input.value = '';
+}
+
+/**
+ * Starts/Stops record function
+ */
+function recordButtonClicked() {
+ /**
+ * Callback function to draw frequency response of current recorded buffer
+ */
+ function getInstantBuffer(leftData, rightData, sampleRate, stop) {
+ drawContext.drawInstantCurve(leftData, rightData, sampleRate);
+ if (stop)
+ recordButtonClicked();
+ }
+
+ var btn = document.getElementById('record_btn');
+ if (btn.className == 'btn-off') {
+ var detect = document.getElementById('detect_tone').checked;
+ var autoStop = document.getElementById('auto_stop').checked;
+ var append = document.getElementById('append_tone').checked;
+ if (recordSourceType == 'audio') {
+ switch(audioSourceType) {
+ case 'sine':
+ case 'sweep':
+ if (toneValueCheckSet()) {
+ audioPlay.play(recordButtonClicked);
+ btn.className = 'btn-on';
+ }
+ break;
+ case 'file':
+ audioPlay.setBuffer(audioBuffer, append);
+ audioPlay.play(recordButtonClicked);
+ btn.className = 'btn-on';
+ break;
+ }
+ } else {
+ btn.className = 'btn-on';
+ }
+ recorder.record(getInstantBuffer, detect, autoStop);
+ } else {
+ recorder.stop();
+ if (recordSourceType == 'audio') {
+ audioPlay.stop();
+ }
+ // create WAV download link using audio data blob
+ var filename = new Date().toISOString() + '.wav';
+ buffer = recorder.getBuffer();
+ drawContext.add(new AudioCurve(buffer, filename, audioContext.sampleRate));
+ createDownloadLink(filename);
+ recorder.clear();
+ btn.className = 'btn-off';
+ }
+}
+
+/**
+ * Creates download link of recorded file
+ * @param {string} file name
+ */
+function createDownloadLink(filename) {
+ var blob = recorder.exportWAV();
+ var url = URL.createObjectURL(blob);
+ var table = document.getElementById('record_list');
+ var au = document.createElement('audio');
+ au.controls = true;
+ au.src = url;
+
+ var hf = document.createElement('a');
+ hf.href = url;
+ hf.download = filename;
+ hf.innerHTML = hf.download;
+
+ var tr = table.insertRow(table.rows.length);
+ var td_au = tr.insertCell(0);
+ var td_hf = tr.insertCell(1);
+ td_hf.style = "white-space: nowrap";
+ td_au.appendChild(au);
+ td_hf.appendChild(hf);
+}
+
+/**
+ * Exports frequency response CVS file of curves on the canvas
+ */
+function exportCSV() {
+ var hf = document.getElementById('export_csv');
+ var noctaves = document.getElementById('noctaves').value;
+ content = drawContext.exportCurve(noctaves);
+ var blob = new Blob([content], {type: 'application/octet-stream'});
+ var url = URL.createObjectURL(blob);
+ hf.href = url;
+ hf.download = 'audio.csv';
+}
diff --git a/scripts/mic_testing/frontend/recorder.js b/scripts/mic_testing/frontend/recorder.js
new file mode 100644
index 00000000..92f2766f
--- /dev/null
+++ b/scripts/mic_testing/frontend/recorder.js
@@ -0,0 +1,366 @@
+/*
+ * 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.
+ */
+
+var Recorder = function(source){
+ var bufferLen = 4096;
+ var toneFreq = 1000, errorMargin = 0.05;
+
+ var context = source.context;
+ var sampleRate = context.sampleRate;
+ var recBuffersL = [], recBuffersR = [], recLength = 0;
+ this.node = (context.createScriptProcessor ||
+ context.createJavaScriptNode).call(context, bufferLen, 2, 2);
+ var detectAppend = false, autoStop = false, recordCallback;
+ var recording = false;
+ var freqString;
+
+ this.node.onaudioprocess = function(e) {
+ if (!recording) return;
+
+ var length = e.inputBuffer.getChannelData(0).length;
+ var tmpLeft = new Float32Array(length);
+ var tmpRight = new Float32Array(length);
+ tmpLeft.set(e.inputBuffer.getChannelData(0), 0);
+ tmpRight.set(e.inputBuffer.getChannelData(1), 0);
+
+ recBuffersL.push(tmpLeft);
+ recBuffersR.push(tmpRight);
+ recLength += length;
+ var stop = false;
+
+ if (autoStop && detectTone(getFreqList(tmpLeft)))
+ stop = true;
+
+ if (recordCallback) {
+ var tmpLeft = recBuffersL[recBuffersL.length - 1].subarray(
+ -FFT_SIZE-1, -1);
+ var tmpRight = recBuffersR[recBuffersR.length - 1].subarray(
+ -FFT_SIZE-1, -1);
+ recordCallback(tmpLeft, tmpRight, sampleRate, stop);
+ }
+ }
+
+ /**
+ * Starts recording
+ * @param {function} callback function to get current buffer
+ * @param {boolean} detect append tone or not
+ * @param {boolean} auto stop when detecting append tone
+ */
+ this.record = function(cb, detect, stop) {
+ recordCallback = cb;
+ detectAppend = detect;
+ autoStop = stop;
+ recording = true;
+ }
+
+ /**
+ * Stops recording
+ */
+ this.stop = function() {
+ recording = false;
+ recBuffersL = mergeBuffers(recBuffersL, recLength);
+ recBuffersR = mergeBuffers(recBuffersR, recLength);
+ if (detectAppend) {
+ var freqList = getFreqList(recBuffersL);
+ var index = getToneIndices(freqList);
+ removeAppendTone(index[0], index[1]);
+ exportFreqList(freqList);
+ }
+ }
+
+ /**
+ * Gets frequencies list
+ * @param {Float32Array} buffer
+ * @return {array} frequencies list
+ */
+ getFreqList = function(buffer) {
+ var prevPeak = 0;
+ var valid = true;
+ var freqList = [];
+ for (i = 1; i < recLength; i++) {
+ if (buffer[i] > 0.1 &&
+ buffer[i] >= buffer[i - 1] && buffer[i] >= buffer[i + 1]) {
+ if (valid) {
+ var freq = sampleRate / (i - prevPeak);
+ freqList.push([freq, prevPeak, i]);
+ prevPeak = i;
+ valid = false;
+ }
+ } else if (buffer[i] < -0.1) {
+ valid = true;
+ }
+ }
+ return freqList;
+ }
+
+ /**
+ * Checks average frequency is in allowed error margin
+ * @param {float} average frequency
+ * @return {boolean} checked result pass or fail
+ */
+ checkFreq = function (average) {
+ if (Math.abs(average - toneFreq) / toneFreq < errorMargin)
+ return true;
+ return false;
+ }
+
+ /**
+ * Detects append tone while recording.
+ * @param {array} frequencies list
+ * @return {boolean} detected or not
+ */
+ detectTone = function(freqList) {
+ var passCriterion = 50;
+ // Initialize function static variables
+ if (typeof detectTone.startDetected == 'undefined') {
+ detectTone.startDetected = false;
+ detectTone.canStop = false;
+ detectTone.accumulateTone = 0;
+ }
+
+ var windowSize = 10, windowSum = 0, i;
+ var detected = false;
+ for (i = 0; i < freqList.length && i < windowSize; i++) {
+ windowSum += freqList[i][0];
+ }
+ if (checkFreq(windowSum / Math.min(windowSize, freqList.length))) {
+ detected = true;
+ detectTone.accumulateTone++;
+ }
+ for (; i < freqList.length; i++) {
+ windowSum = windowSum + freqList[i][0] - freqList[i - windowSize][0];
+ if (checkFreq(windowSum / windowSize)) {
+ detected = true;
+ detectTone.accumulateTone++;
+ }
+ }
+ if (detected) {
+ if (detectTone.accumulateTone > passCriterion) {
+ if (!detectTone.startDetected)
+ detectTone.startDetected = true;
+ else if (detectTone.canStop) {
+ detectTone.startDetected = false;
+ detectTone.canStop = false;
+ detectTone.accumulateTone = 0;
+ return true;
+ }
+ }
+ } else {
+ detectTone.accumulateTone = 0;
+ if (detectTone.startDetected)
+ detectTone.canStop = true;
+ }
+ return false;
+ }
+
+ /**
+ * Gets start and end indices from a frquencies list except append tone
+ * @param {array} frequencies list
+ * @return {array} start and end indices
+ */
+ getToneIndices = function(freqList) {
+ // find start and end indices
+ var flag, j, k;
+ var windowSize = 10, windowSum;
+ var index = new Array(2);
+ var scanRange = [[0, freqList.length, 1], [freqList.length - 1, -1, -1]];
+
+ if (freqList.length == 0) return index;
+
+ for (i = 0; i < 2; i++) {
+ flag = false;
+ windowSum = 0;
+ for (j = scanRange[i][0], k = 0; k < windowSize && j != scanRange[i][1];
+ j += scanRange[i][2], k++) {
+ windowSum += freqList[j][0];
+ }
+ for (; j != scanRange[i][1]; j += scanRange[i][2]) {
+ windowSum = windowSum + freqList[j][0] -
+ freqList[j - scanRange[i][2] * windowSize][0];
+ var avg = windowSum / windowSize;
+ if (checkFreq(avg) && !flag) {
+ flag = true;
+ }
+ if (!checkFreq(avg) && flag) {
+ index[i] = freqList[j][1];
+ break;
+ }
+ }
+ }
+ return index;
+ }
+
+ /**
+ * Removes append tone from recorded buffer
+ * @param {int} start index
+ * @param {int} end index
+ */
+ removeAppendTone = function(start, end) {
+ if (!isNaN(start) && !isNaN(end) && end > start) {
+ recBuffersL = truncateBuffers(recBuffersL, recLength, start, end);
+ recBuffersR = truncateBuffers(recBuffersR, recLength, start, end);
+ recLength = end - start;
+ }
+ }
+
+ /**
+ * Exports frequency list for debugging purpose
+ */
+ exportFreqList = function(freqList) {
+ freqString = sampleRate + '\n';
+ for (var i = 0; i < freqList.length; i++) {
+ freqString += freqList[i][0] + ' ' + freqList[i][1] + ' ' +
+ freqList[i][2] + '\n';
+ }
+ }
+
+ this.getFreq = function() {
+ return freqString;
+ }
+
+ /**
+ * Clears recorded buffer
+ */
+ this.clear = function() {
+ recLength = 0;
+ recBuffersL = [];
+ recBuffersR = [];
+ }
+
+ /**
+ * Gets recorded buffer
+ */
+ this.getBuffer = function() {
+ var buffers = [];
+ buffers.push(recBuffersL);
+ buffers.push(recBuffersR);
+ return buffers;
+ }
+
+ /**
+ * Exports WAV format file
+ * @return {blob} audio file blob
+ */
+ this.exportWAV = function(type) {
+ type = type || 'audio/wav';
+ var interleaved = interleave(recBuffersL, recBuffersR);
+ var dataview = encodeWAV(interleaved);
+ var audioBlob = new Blob([dataview], { type: type });
+ return audioBlob;
+ }
+
+ /**
+ * Truncates buffer from start index to end index
+ * @param {Float32Array} audio buffer
+ * @param {int} buffer length
+ * @param {int} start index
+ * @param {int} end index
+ * @return {Float32Array} a truncated buffer
+ */
+ truncateBuffers = function(recBuffers, recLength, startIdx, endIdx) {
+ var buffer = new Float32Array(endIdx - startIdx);
+ for (var i = startIdx, j = 0; i < endIdx; i++, j++) {
+ buffer[j] = recBuffers[i];
+ }
+ return buffer;
+ }
+
+ /**
+ * Merges buffer into an array
+ * @param {array} a list of Float32Array of audio buffer
+ * @param {int} buffer length
+ * @return {Float32Array} a merged buffer
+ */
+ mergeBuffers = function(recBuffers, recLength) {
+ var result = new Float32Array(recLength);
+ var offset = 0;
+ for (var i = 0; i < recBuffers.length; i++){
+ result.set(recBuffers[i], offset);
+ offset += recBuffers[i].length;
+ }
+ return result;
+ }
+
+ /**
+ * Interleaves left and right channel buffer
+ * @param {Float32Array} left channel buffer
+ * @param {Float32Array} right channel buffer
+ * @return {Float32Array} an interleaved buffer
+ */
+ interleave = function(inputL, inputR) {
+ var length = inputL.length + inputR.length;
+ var result = new Float32Array(length);
+
+ var index = 0,
+ inputIndex = 0;
+
+ while (index < length){
+ result[index++] = inputL[inputIndex];
+ result[index++] = inputR[inputIndex];
+ inputIndex++;
+ }
+ return result;
+ }
+
+ floatTo16BitPCM = function(output, offset, input) {
+ for (var i = 0; i < input.length; i++, offset+=2){
+ var s = Math.max(-1, Math.min(1, input[i]));
+ output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
+ }
+ }
+
+ writeString = function(view, offset, string) {
+ for (var i = 0; i < string.length; i++){
+ view.setUint8(offset + i, string.charCodeAt(i));
+ }
+ }
+
+ /**
+ * Encodes audio buffer into WAV format raw data
+ * @param {Float32Array} an interleaved buffer
+ * @return {DataView} WAV format raw data
+ */
+ encodeWAV = function(samples) {
+ var buffer = new ArrayBuffer(44 + samples.length * 2);
+ var view = new DataView(buffer);
+
+ /* RIFF identifier */
+ writeString(view, 0, 'RIFF');
+ /* file length */
+ view.setUint32(4, 32 + samples.length * 2, true);
+ /* RIFF type */
+ writeString(view, 8, 'WAVE');
+ /* format chunk identifier */
+ writeString(view, 12, 'fmt ');
+ /* format chunk length */
+ view.setUint32(16, 16, true);
+ /* sample format (raw) */
+ view.setUint16(20, 1, true);
+ /* channel count */
+ view.setUint16(22, 2, true);
+ /* sample rate */
+ view.setUint32(24, sampleRate, true);
+ /* byte rate (sample rate * block align) */
+ view.setUint32(28, sampleRate * 4, true);
+ /* block align (channel count * bytes per sample) */
+ view.setUint16(32, 4, true);
+ /* bits per sample */
+ view.setUint16(34, 16, true);
+ /* data chunk identifier */
+ writeString(view, 36, 'data');
+ /* data chunk length */
+ view.setUint32(40, samples.length * 2, true);
+
+ floatTo16BitPCM(view, 44, samples);
+
+ return view;
+ }
+
+ source.connect(this.node);
+ this.node.connect(context.destination);
+};
+
+window.Recorder = Recorder;
diff --git a/scripts/mic_testing/frontend/source.js b/scripts/mic_testing/frontend/source.js
new file mode 100644
index 00000000..0f21d120
--- /dev/null
+++ b/scripts/mic_testing/frontend/source.js
@@ -0,0 +1,222 @@
+/*
+ * 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.
+ */
+
+var ToneGen = function() {
+ /**
+ * Initializes tone generator.
+ */
+ this.init = function() {
+ this.audioContext = new AudioContext();
+ }
+
+ /**
+ * Sets sample rate
+ * @param {int} sample rate
+ */
+ this.setSampleRate = function(sampleRate) {
+ this.sampleRate = sampleRate;
+ }
+
+ /**
+ * Sets start/end frequencies and logarithmic sweep
+ * @param {int} start frequency
+ * @param {int} end frequency
+ * @param {boolean} logarithmic sweep or not
+ */
+ this.setFreq = function(freqStart, freqEnd, sweepLog) {
+ this.freqStart = freqStart;
+ this.freqEnd = freqEnd;
+ this.sweepLog = sweepLog;
+ }
+
+ /**
+ * Sets tone duration
+ * @param {float} duration in seconds
+ */
+ this.setDuration = function(duration) {
+ this.duration = parseFloat(duration);
+ }
+
+ /**
+ * Sets left and right gain value
+ * @param {float} left gain between 0 and 1
+ * @param {float} right gain between 0 and 1
+ */
+ this.setGain = function(leftGain, rightGain) {
+ this.leftGain = parseFloat(leftGain);
+ this.rightGain = parseFloat(rightGain);
+ }
+
+ /**
+ * Generates sine tone buffer
+ */
+ this.genBuffer = function() {
+ this.buffer = this.audioContext.createBuffer(2,
+ this.sampleRate * this.duration, this.sampleRate);
+ var leftChannel = this.buffer.getChannelData(0);
+ var rightChannel = this.buffer.getChannelData(1);
+ var f, phi;
+ var power_start = Math.log(this.freqStart) / Math.LN2;
+ var power_end = Math.log(this.freqEnd) / Math.LN2;
+ for (var i = 0; i < leftChannel.length; i++) {
+ f = this.freqStart + (this.freqEnd - this.freqStart) *
+ i / leftChannel.length / 2;
+ phi = f * 2 * Math.PI * i / this.sampleRate;
+ leftChannel[i] = this.leftGain * Math.sin(phi);
+ rightChannel[i] = this.rightGain * Math.sin(phi);
+ }
+ }
+
+ /**
+ * Returns generated sine tone buffer
+ * @return {AudioBuffer} audio buffer
+ */
+ this.getBuffer = function() {
+ return this.buffer;
+ }
+
+ /**
+ * Returns append buffer
+ * @return {AudioBuffer} append audio buffer
+ */
+ this.getAppendTone = function(sampleRate) {
+ var tone_freq = 1000, duration = 0.5;
+ this.setFreq(tone_freq, tone_freq, false);
+ this.setDuration(duration);
+ this.setGain(1, 1);
+ this.setSampleRate(sampleRate);
+ this.genBuffer();
+ return this.getBuffer();
+ }
+
+ this.init();
+}
+
+window.ToneGen = ToneGen;
+
+var AudioPlay = function() {
+ var playCallback = null;
+ var sampleRate;
+ var playing = false;
+
+ /**
+ * Initializes audio play object
+ */
+ this.init = function() {
+ this.audioContext = new AudioContext();
+ this.genChannel();
+ this.buffer = null;
+ sampleRate = this.audioContext.sampleRate;
+ }
+
+ /**
+ * Loads audio file
+ * @param {blob} audio file
+ * @param {function} callback function when file loaded
+ */
+ this.loadFile = function(file_blob, done_cb) {
+ if (file_blob) {
+ var audioContext = this.audioContext;
+ reader = new FileReader();
+ reader.onloadend = function(e) {
+ audioContext.decodeAudioData(e.target.result,
+ function(buffer) {
+ done_cb(file_blob.name, buffer);
+ });
+ };
+ reader.readAsArrayBuffer(file_blob);
+ }
+ }
+
+ /**
+ * Sets audio path
+ */
+ this.genChannel = function() {
+ this.node = (this.audioContext.createScriptProcessor ||
+ this.audioContext.createJavaScriptNode).call(
+ this.audioContext, 4096, 2, 2);
+ this.splitter = this.audioContext.createChannelSplitter(2);
+ this.merger = this.audioContext.createChannelMerger(2);
+ this.node.connect(this.splitter);
+ this.splitter.connect(this.merger, 0, 0);
+ this.splitter.connect(this.merger, 1, 1);
+ this.merger.connect(this.audioContext.destination);
+
+ this.node.onaudioprocess = function(e) {
+ for (var i = 0; i < e.inputBuffer.numberOfChannels; i++) {
+ e.outputBuffer.getChannelData(i).set(
+ e.inputBuffer.getChannelData(i), 0);
+ }
+ if (!playing) return;
+ if (playCallback) {
+ var tmpLeft = e.inputBuffer.getChannelData(0).subarray(
+ -FFT_SIZE-1, -1);
+ var tmpRight = e.inputBuffer.getChannelData(1).subarray(
+ -FFT_SIZE-1, -1);
+ playCallback(tmpLeft, tmpRight, sampleRate);
+ }
+ }
+ }
+
+ /**
+ * Plays audio
+ * @param {function} callback function when audio end
+ * @param {function} callback function to get current buffer
+ */
+ this.play = function(done_cb, play_cb) {
+ playCallback = play_cb;
+ this.source = this.audioContext.createBufferSource();
+ this.source.buffer = this.buffer;
+ this.source.onended = function(e) {
+ playing = false;
+ this.disconnect();
+ if (done_cb) {
+ done_cb();
+ }
+ }
+ this.source.connect(this.node);
+ this.source.start(0);
+ playing = true;
+ }
+
+ /**
+ * Stops audio
+ */
+ this.stop = function() {
+ playing = false;
+ this.source.stop();
+ this.source.disconnect();
+ }
+
+ /**
+ * Sets audio buffer
+ * @param {AudioBuffer} audio buffer
+ * @param {boolean} append tone or not
+ */
+ this.setBuffer = function(buffer, append) {
+ if (append) {
+ function copyBuffer(src, dest, offset) {
+ for (var i = 0; i < dest.numberOfChannels; i++) {
+ dest.getChannelData(i).set(src.getChannelData(i), offset);
+ }
+ }
+ var appendTone = tonegen.getAppendTone(buffer.sampleRate);
+ var bufferLength = appendTone.length * 2 + buffer.length;
+ var newBuffer = this.audioContext.createBuffer(buffer.numberOfChannels,
+ bufferLength, buffer.sampleRate);
+ copyBuffer(appendTone, newBuffer, 0);
+ copyBuffer(buffer, newBuffer, appendTone.length);
+ copyBuffer(appendTone, newBuffer, appendTone.length + buffer.length);
+ this.buffer = newBuffer;
+ } else {
+ this.buffer = buffer;
+ }
+ }
+
+ this.init();
+}
+
+window.AudioPlay = AudioPlay;