diff options
author | henryhsu <henryhsu@chromium.org> | 2014-06-23 17:55:07 +0800 |
---|---|---|
committer | chrome-internal-fetch <chrome-internal-fetch@google.com> | 2014-06-24 20:08:58 +0000 |
commit | 176578e28066262d983ec67896be50d49cf86e0a (patch) | |
tree | f76394f46c04177293df927e94859bf4913192fe /scripts | |
parent | fdf87598f83c8e428d90453b7634fa4a64de22e4 (diff) | |
download | adhd-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/LICENSE | 27 | ||||
-rw-r--r-- | scripts/mic_testing/frontend/analysis.js | 485 | ||||
-rw-r--r-- | scripts/mic_testing/frontend/app.yaml | 30 | ||||
-rw-r--r-- | scripts/mic_testing/frontend/audio.css | 107 | ||||
-rw-r--r-- | scripts/mic_testing/frontend/audio.html | 184 | ||||
-rw-r--r-- | scripts/mic_testing/frontend/audio.js | 363 | ||||
-rw-r--r-- | scripts/mic_testing/frontend/recorder.js | 366 | ||||
-rw-r--r-- | scripts/mic_testing/frontend/source.js | 222 |
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; |