From 608ba49515ab0401c0572cbb0c12376485ace0ef Mon Sep 17 00:00:00 2001 From: Hongchan Choi Date: Mon, 7 Nov 2016 11:53:47 -0800 Subject: [PATCH 1/4] progress --- README.md | 4 +- src/foa-phase-matched-filter.js | 12 ++- test/index.html | 1 + test/test-phasematchedfilter.js | 77 ++++++++++++++++++ test/test-setup.js | 136 ++++++++++++++++++++++++++++++++ 5 files changed, 224 insertions(+), 6 deletions(-) create mode 100644 test/test-phasematchedfilter.js diff --git a/README.md b/README.md index 17db2fe..200ffc7 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ Omnitone also provides various building blocks for the first-order-ambisonic dec ```js var decoder = Omnitone.createFOADecoder(context, element, { HRTFSetUrl: 'YOUR_HRTF_SET_URL', - postGainDB: 30, + postGainDB: 0, channelMap: [0, 1, 2, 3] }); ``` @@ -217,7 +217,7 @@ Omnitone is designed to run any browser that supports Web Audio API, however, it ## Related Resources * [Google Spatial Media](https://github.com/google/spatial-media) -* [VRView](https://developers.google.com/vr/concepts/vrview/) +* [VR view](https://developers.google.com/vr/concepts/vrview/) * [Web Audio API](https://webaudio.github.io/web-audio-api/) * [WebVR](https://webvr.info/) diff --git a/src/foa-phase-matched-filter.js b/src/foa-phase-matched-filter.js index aba424d..5f6bed9 100644 --- a/src/foa-phase-matched-filter.js +++ b/src/foa-phase-matched-filter.js @@ -96,10 +96,14 @@ function FOAPhaseMatchedFilter (context) { // Apply gain correction to hi-passed pressure and velocity components: // Inverting sign is necessary as the low-passed and high-passed portion are // out-of-phase after the filtering. - this._gainHighW.gain.value = -1 * GAIN_COEFFICIENTS[0]; - this._gainHighY.gain.value = -1 * GAIN_COEFFICIENTS[1]; - this._gainHighZ.gain.value = -1 * GAIN_COEFFICIENTS[2]; - this._gainHighX.gain.value = -1 * GAIN_COEFFICIENTS[3]; + // this._gainHighW.gain.value = -1 * GAIN_COEFFICIENTS[0]; + // this._gainHighY.gain.value = -1 * GAIN_COEFFICIENTS[1]; + // this._gainHighZ.gain.value = -1 * GAIN_COEFFICIENTS[2]; + // this._gainHighX.gain.value = -1 * GAIN_COEFFICIENTS[3]; + this._gainHighW.gain.setValueAtTime(-GAIN_COEFFICIENTS[0], 0); + this._gainHighY.gain.setValueAtTime(-GAIN_COEFFICIENTS[1], 0); + this._gainHighZ.gain.setValueAtTime(-GAIN_COEFFICIENTS[2], 0); + this._gainHighX.gain.setValueAtTime(-GAIN_COEFFICIENTS[3], 0); // Input/output Proxy. this.input = this._input; diff --git a/test/index.html b/test/index.html index 55d1aaf..2ebb4fb 100644 --- a/test/index.html +++ b/test/index.html @@ -30,6 +30,7 @@ + diff --git a/test/test-phasematchedfilter.js b/test/test-phasematchedfilter.js new file mode 100644 index 0000000..a41513c --- /dev/null +++ b/test/test-phasematchedfilter.js @@ -0,0 +1,77 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +describe('FOAPhaseMatchedFilter', function () { + + var sampleRate = 48000; + var renderLength = 128; + var context; + var testAudioBus; + + // Parameters for the FOA phase matched filter. + var crossoverFrequency = 690; + var hipassGainCompensation = [-1.4142, -0.8166, -0.8166, -0.8166]; + + // A common task for router tests. Create an OAC for rendering. + beforeEach(function (done) { + context = new OfflineAudioContext(4, renderLength, sampleRate); + testAudioBus = new AudioBus(4, renderLength, sampleRate); + testAudioBus.fillChannelData([0.25, 0.5, 0.75, 1]); + done(); + }); + + + it('Simulate and verify the phase-matched filter implementation.', + function (done) { + + // Generate the expected filter result. + var filterCoefs = getDualBandFilterCoefs( + crossoverFrequency, sampleRate); + + var hipassBus = new AudioBus(4, renderLength, sampleRate); + hipassBus.copyFrom(testAudioBus); + hipassBus.processIIRFilter(filterCoefs.hipassB, filterCoefs.hipassA); + hipassBus.processGain(hipassGainCompensation); + + var lowpassBus = new AudioBus(4, renderLength, sampleRate); + lowpassBus.copyFrom(testAudioBus); + lowpassBus.processIIRFilter(filterCoefs.lowpassB, filterCoefs.lowpassA); + + hipassBus.sumFrom(lowpassBus); + + // Generate the actual filter result. + var source = context.createBufferSource(); + source.buffer = testAudioBus.getAudioBuffer(context); + + var dualbandFilter = Omnitone.createFOAPhaseMatchedFilter(context); + + source.connect(dualbandFilter.input); + dualbandFilter.output.connect(context.destination); + + source.start(); + + context.startRendering().then(function (renderedBuffer) { + var actualBus = new AudioBus(4, renderLength, sampleRate); + actualBus.copyFromAudioBuffer(renderedBuffer); + hipassBus.compareWith(actualBus, 0.01); + + done(); + }); + } + ); + + +}); diff --git a/test/test-setup.js b/test/test-setup.js index 81772bc..f7b232a 100644 --- a/test/test-setup.js +++ b/test/test-setup.js @@ -53,3 +53,139 @@ function isConstantValueOf(channelData, value) { return Object.keys(mismatches).length === 0; }; + + +/** + * @param {[type]} crossoverFrequency [description] + * @param {[type]} sampleRate [description] + * @return {[type]} [description] + */ +function getDualBandFilterCoefs(crossoverFrequency, sampleRate) { + var k = Math.tan(Math.PI * crossoverFrequency / sampleRate), + k2 = k * k, + denominator = k2 + 2 * k + 1; + + return { + lowpassA: [1, 2 * (k2 - 1) / denominator, (k2 - 2 * k + 1) / denominator], + lowpassB: [k2 / denominator, 2 * k2 / denominator, k2 / denominator], + hipassA: [1, 2 * (k2 - 1) / denominator, (k2 - 2 * k + 1) / denominator], + hipassB: [1 / denominator, -2 * 1 / denominator, 1 / denominator] + }; +} + +/** + * Kernel processor for IIR filter. (in-place processing) + * @param {Float32Array} channelData A channel data. + * @param {Float32Array} feedforward Feedforward coefficients. + * @param {Float32Array} feedback Feedback coefficients. + */ +function kernel_IIRFIlter (channelData, feedforward, feedback) { + var paddingSize = Math.max(feedforward.length, feedback.length); + var workSize = channelData.length + paddingSize; + var x = new Float32Array(workSize); + var y = new Float64Array(workSize); + + x.set(channelData, paddingSize); + + for (var index = paddingSize; index < workSize; ++index) { + var yn = 0; + for (k = 0; k < feedforward.length; ++k) + yn += feedforward[k] * x[index - k]; + for (k = 0; k < feedback.length; ++k) + yn -= feedback[k] * y[index - k]; + y[index] = yn; + } + + channelData.set(y.slice(paddingSize).map(Math.fround)); +} + + +/** + * [AudioBus description] + * @param {[type]} numberOfChannels [description] + * @param {[type]} length [description] + * @param {[type]} sampleRate [description] + */ +function AudioBus (numberOfChannels, length, sampleRate) { + this.numberOfChannels = numberOfChannels; + this.sampleRate = sampleRate; + this.length = length; + this.duration = this.length / this.sampleRate; + + this._channelData = []; + for (var i = 0; i < this.numberOfChannels; ++i) { + this._channelData[i] = new Float32Array(length); + } +} + +AudioBus.prototype.getChannelData = function (channel) { + return this._channelData[channel]; +}; + +AudioBus.prototype.getAudioBuffer = function (context) { + var audioBuffer = context.createBuffer( + this.numberOfChannels, this.length, this.sampleRate); + + for (var channel = 0; channel < this.numberOfChannels; ++channel) + audioBuffer.getChannelData(channel).set(this._channelData[channel]); + + return audioBuffer; +}; + +AudioBus.prototype.fillChannelData = function (samples) { + for (var channel = 0; channel < this.numberOfChannels; ++channel) + this._channelData[channel].fill(samples[channel]); +}; + +AudioBus.prototype.copyFrom = function (otherAudioBus) { + for (var channel = 0; channel < this.numberOfChannels; ++channel) + this._channelData[channel].set(otherAudioBus.getChannelData(channel)); +}; + +AudioBus.prototype.copyFromAudioBuffer = function (audioBuffer) { + for (var channel = 0; channel < this.numberOfChannels; ++channel) + this._channelData[channel].set(audioBuffer.getChannelData(channel)); +}; + +AudioBus.prototype.sumFrom = function (otherAudioBus) { + for (var channel = 0; channel < this.numberOfChannels; ++channel) { + var channelDataA = this._channelData[channel]; + var channelDataB = otherAudioBus.getChannelData(channel); + for (var i = 0; i < this.length; ++i) + channelDataA[i] += channelDataB[i]; + } +}; + +AudioBus.prototype.compareWith = function (otherAudioBus, threshold) { + for (var channel = 0; channel < this.numberOfChannels; ++channel) { + var channelDataA = this._channelData[channel]; + var channelDataB = otherAudioBus.getChannelData(channel); + + for (var i = 0; i < this.length; ++i) { + var absDiff = Math.abs(channelDataA[i] - channelDataB[i]); + if (absDiff > threshold) { + console.log('ERROR: at the index ' + i + ' (' + + absDiff + ' > ' + threshold + ').'); + } + } + } +}; + +AudioBus.prototype.processGain = function (coefficents) { + for (var channel = 0; channel < this.numberOfChannels; ++channel) { + var channelData = this._channelData[channel]; + for (var i = 0; i < this.length; ++i) + channelData[i] *= coefficents[channel]; + } +}; + +AudioBus.prototype.processIIRFilter = function (feedforward, feedback) { + for (var channel = 0; channel < this.numberOfChannels; ++channel) + kernel_IIRFIlter(this._channelData[channel], feedforward, feedback); +}; + +AudioBus.prototype.print = function () { + for (var channel = 0; channel < this.numberOfChannels; ++channel) { + console.log(this._channelData[channel]); + } +} From 28cac716c2644f68b6788f7e56ff010c4039777a Mon Sep 17 00:00:00 2001 From: Hongchan Choi Date: Mon, 7 Nov 2016 13:12:45 -0800 Subject: [PATCH 2/4] fixed filter issue --- build/omnitone.min.js | 2 +- package.json | 2 +- src/foa-phase-matched-filter.js | 15 +++++---- src/version.js | 2 +- test/test-phasematchedfilter.js | 49 +++++++++++++++++++++++---- test/test-setup.js | 60 ++++++++++++++++++++------------- 6 files changed, 90 insertions(+), 40 deletions(-) diff --git a/build/omnitone.min.js b/build/omnitone.min.js index 74f320b..272704e 100644 --- a/build/omnitone.min.js +++ b/build/omnitone.min.js @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -"use strict";i.Omnitone=e(1)},function(t,i,e){"use strict";var n={},s=e(2),o=e(4),a=e(5),h=e(6),c=e(7),r=e(8);n.loadAudioBuffers=function(t,i){return new Promise(function(e,n){new s(t,i,function(t){e(t)},n)})},n.createFOARouter=function(t,i){return new o(t,i)},n.createFOARotator=function(t){return new a(t)},n.createFOAPhaseMatchedFilter=function(t){return new h(t)},n.createFOAVirtualSpeaker=function(t,i){return new c(t,i)},n.createFOADecoder=function(t,i,e){return new r(t,i,e)},t.exports=n},function(t,i,e){"use strict";function n(t,i,e,n,o){this._context=t,this._buffers=new Map,this._loadingTasks={},this._resolve=e,this._reject=n,this._progress=o;for(var a=0;a ["+t+"])"),this._audioElementSource=this._context.createMediaElementSource(this._videoElement),this._foaRouter=new o(this._context,this._channelMap),this._foaRotator=new a(this._context),this._foaPhaseMatchedFilter=new h(this._context),this._audioElementSource.connect(this._foaRouter.input),this._foaRouter.output.connect(this._foaRotator.input),this._foaRotator.output.connect(this._foaPhaseMatchedFilter.input),this._foaVirtualSpeakers=[],this._bypass=this._context.createGain(),this._audioElementSource.connect(this._bypass);var i=Math.pow(10,this._postGainDB/20);_.log("Gain compensation: "+i+" ("+this._postGainDB+"dB)");var e=this;return new Promise(function(t,n){new s(e._context,e._speakerData,function(n){for(var s=0;s ["+t+"])"),this._audioElementSource=this._context.createMediaElementSource(this._videoElement),this._foaRouter=new o(this._context,this._channelMap),this._foaRotator=new a(this._context),this._foaPhaseMatchedFilter=new h(this._context),this._audioElementSource.connect(this._foaRouter.input),this._foaRouter.output.connect(this._foaRotator.input),this._foaRotator.output.connect(this._foaPhaseMatchedFilter.input),this._foaVirtualSpeakers=[],this._bypass=this._context.createGain(),this._audioElementSource.connect(this._bypass);var i=Math.pow(10,this._postGainDB/20);_.log("Gain compensation: "+i+" ("+this._postGainDB+"dB)");var e=this;return new Promise(function(t,n){new s(e._context,e._speakerData,function(n){for(var s=0;s threshold) { - console.log('ERROR: at the index ' + i + ' (' - + absDiff + ' > ' + threshold + ').'); - } - } - } -}; - AudioBus.prototype.processGain = function (coefficents) { for (var channel = 0; channel < this.numberOfChannels; ++channel) { var channelData = this._channelData[channel]; @@ -184,8 +172,32 @@ AudioBus.prototype.processIIRFilter = function (feedforward, feedback) { kernel_IIRFIlter(this._channelData[channel], feedforward, feedback); }; -AudioBus.prototype.print = function () { +AudioBus.prototype.compareWith = function (otherAudioBus, threshold) { + var passed = true; + + for (var channel = 0; channel < this.numberOfChannels; ++channel) { + var channelDataA = this._channelData[channel]; + var channelDataB = otherAudioBus.getChannelData(channel); + + for (var i = 0; i < this.length; ++i) { + var absDiff = Math.abs(channelDataA[i] - channelDataB[i]); + if (absDiff > threshold) { + console.log('ERROR: at the index ' + i + ' (' + absDiff + ' > ' + + threshold + ').'); + passed = false; + } + } + } + + return passed; +}; + +AudioBus.prototype.print = function (begin, end) { + begin = (begin || 0); + end = (end || this.length); + console.log('AudioBus: <' + begin + ' ~ ' + end + '>'); for (var channel = 0; channel < this.numberOfChannels; ++channel) { - console.log(this._channelData[channel]); + console.log(channel + + ' => [' + this._channelData[channel].subarray(begin, end) + ']'); } } From cc01a7cca067e81ffdc9dd40a752a61e74d2c2a1 Mon Sep 17 00:00:00 2001 From: Hongchan Choi Date: Mon, 7 Nov 2016 13:13:17 -0800 Subject: [PATCH 3/4] rebuild files for 0.1.8 --- build/omnitone.min.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/omnitone.min.js b/build/omnitone.min.js index 272704e..804025e 100644 --- a/build/omnitone.min.js +++ b/build/omnitone.min.js @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -"use strict";i.Omnitone=e(1)},function(t,i,e){"use strict";var n={},s=e(2),o=e(4),a=e(5),h=e(6),c=e(7),r=e(8);n.loadAudioBuffers=function(t,i){return new Promise(function(e,n){new s(t,i,function(t){e(t)},n)})},n.createFOARouter=function(t,i){return new o(t,i)},n.createFOARotator=function(t){return new a(t)},n.createFOAPhaseMatchedFilter=function(t){return new h(t)},n.createFOAVirtualSpeaker=function(t,i){return new c(t,i)},n.createFOADecoder=function(t,i,e){return new r(t,i,e)},t.exports=n},function(t,i,e){"use strict";function n(t,i,e,n,o){this._context=t,this._buffers=new Map,this._loadingTasks={},this._resolve=e,this._reject=n,this._progress=o;for(var a=0;a ["+t+"])"),this._audioElementSource=this._context.createMediaElementSource(this._videoElement),this._foaRouter=new o(this._context,this._channelMap),this._foaRotator=new a(this._context),this._foaPhaseMatchedFilter=new h(this._context),this._audioElementSource.connect(this._foaRouter.input),this._foaRouter.output.connect(this._foaRotator.input),this._foaRotator.output.connect(this._foaPhaseMatchedFilter.input),this._foaVirtualSpeakers=[],this._bypass=this._context.createGain(),this._audioElementSource.connect(this._bypass);var i=Math.pow(10,this._postGainDB/20);_.log("Gain compensation: "+i+" ("+this._postGainDB+"dB)");var e=this;return new Promise(function(t,n){new s(e._context,e._speakerData,function(n){for(var s=0;s ["+t+"])"),this._audioElementSource=this._context.createMediaElementSource(this._videoElement),this._foaRouter=new o(this._context,this._channelMap),this._foaRotator=new a(this._context),this._foaPhaseMatchedFilter=new h(this._context),this._audioElementSource.connect(this._foaRouter.input),this._foaRouter.output.connect(this._foaRotator.input),this._foaRotator.output.connect(this._foaPhaseMatchedFilter.input),this._foaVirtualSpeakers=[],this._bypass=this._context.createGain(),this._audioElementSource.connect(this._bypass);var i=Math.pow(10,this._postGainDB/20);_.log("Gain compensation: "+i+" ("+this._postGainDB+"dB)");var e=this;return new Promise(function(t,n){new s(e._context,e._speakerData,function(n){for(var s=0;s Date: Mon, 7 Nov 2016 13:31:48 -0800 Subject: [PATCH 4/4] remove unnecessary comments --- src/foa-phase-matched-filter.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/foa-phase-matched-filter.js b/src/foa-phase-matched-filter.js index 69d60c0..6d85f5c 100644 --- a/src/foa-phase-matched-filter.js +++ b/src/foa-phase-matched-filter.js @@ -96,10 +96,6 @@ function FOAPhaseMatchedFilter (context) { // Apply gain correction to hi-passed pressure and velocity components: // Inverting sign is necessary as the low-passed and high-passed portion are // out-of-phase after the filtering. - // this._gainHighW.gain.value = -1 * GAIN_COEFFICIENTS[0]; - // this._gainHighY.gain.value = -1 * GAIN_COEFFICIENTS[1]; - // this._gainHighZ.gain.value = -1 * GAIN_COEFFICIENTS[2]; - // this._gainHighX.gain.value = -1 * GAIN_COEFFICIENTS[3]; var now = this._context.currentTime; this._gainHighW.gain.setValueAtTime(-1 * GAIN_COEFFICIENTS[0], now); this._gainHighY.gain.setValueAtTime(-1 * GAIN_COEFFICIENTS[1], now);