Skip to content

Commit

Permalink
Merge pull request #28 from GoogleChrome/0.1.8-dev
Browse files Browse the repository at this point in the history
Release 0.1.8
  • Loading branch information
hoch authored Nov 7, 2016
2 parents 6662069 + 62dab22 commit cf3feae
Show file tree
Hide file tree
Showing 8 changed files with 276 additions and 12 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
});
```
Expand Down Expand Up @@ -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/)

Expand Down
2 changes: 1 addition & 1 deletion build/omnitone.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "omnitone",
"version": "0.1.7",
"version": "0.1.8",
"description": "Spatial Audio Decoder in Web Audio API",
"main": "src/main.js",
"keywords": [
Expand Down
15 changes: 8 additions & 7 deletions src/foa-phase-matched-filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,18 @@ function FOAPhaseMatchedFilter (context) {
this._input.connect(this._lpf);
this._lpf.connect(this._splitterLow);
this._splitterLow.connect(this._merger, 0, 0);
this._splitterLow.connect(this._merger, 0, 1);
this._splitterLow.connect(this._merger, 0, 2);
this._splitterLow.connect(this._merger, 0, 3);
this._splitterLow.connect(this._merger, 1, 1);
this._splitterLow.connect(this._merger, 2, 2);
this._splitterLow.connect(this._merger, 3, 3);

// 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);
this._gainHighZ.gain.setValueAtTime(-1 * GAIN_COEFFICIENTS[2], now);
this._gainHighX.gain.setValueAtTime(-1 * GAIN_COEFFICIENTS[3], now);

// Input/output Proxy.
this.input = this._input;
Expand Down
2 changes: 1 addition & 1 deletion src/version.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@
* Omnitone library version
* @type {String}
*/
module.exports = '0.1.7';
module.exports = '0.1.8';
1 change: 1 addition & 0 deletions test/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<script>mocha.setup('bdd')</script>
<script src="test-router.js"></script>
<script src="test-rotator.js"></script>
<script src="test-phasematchedfilter.js"></script>
<script>mocha.run();</script>
</body>
</html>
114 changes: 114 additions & 0 deletions test/test-phasematchedfilter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* 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 () {
// Test threshold. Approximately -140dBFS.
var TEST_THRESHOLD = 1.0e-7;

// Parameters for the FOA phase matched filter.
var crossoverFrequency = 690;
var hipassGainCompensation = [-1.4142, -0.8166, -0.8166, -0.8166];

var sampleRate = 48000;
var renderLength = 128;
var context;
var testAudioBus;

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


// Testing IIR filter implementation in the browser. (if applicable)
it('Testing the IIR filter implementation.', function (done) {
expect(typeof context.createIIRFilter).to.equal('function');

var filterCoefs = getDualBandFilterCoefs(
crossoverFrequency, sampleRate);

var expectedBus = new AudioBus(4, renderLength, sampleRate);
expectedBus.copyFrom(testAudioBus);
expectedBus.processIIRFilter(
filterCoefs.lowpassB, filterCoefs.lowpassA);

var source = context.createBufferSource();
source.buffer = testAudioBus.getAudioBuffer(context);
var filter = context.createIIRFilter(
filterCoefs.lowpassB, filterCoefs.lowpassA);

source.connect(filter);
filter.connect(context.destination);
source.start();

context.startRendering().then(function (renderedBuffer) {
var actualBus = new AudioBus(4, renderLength, sampleRate);
actualBus.copyFromAudioBuffer(renderedBuffer);
var result = expectedBus.compareWith(actualBus, 0.0);
expect(result).to.equal(true);

done();
});
}
);


// Create the expected result from the JS-version of dual band filter, and
// compare with the FOAPhaseMatchedFilter result.
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);
var result = hipassBus.compareWith(actualBus, TEST_THRESHOLD);
expect(result).to.equal(true);

done();
});
}
);


});
148 changes: 148 additions & 0 deletions test/test-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,151 @@ function isConstantValueOf(channelData, value) {

return Object.keys(mismatches).length === 0;
};


/**
* Generate the filter coefficients for the phase matched dual band filter.
* @param {NUmber} crossoverFrequency Filter crossover frequency.
* @param {NUmber} sampleRate Operating sample rate.
* @return {Object} Filter coefficients.
* { lowpassA, lowpassB, hipassA, hipassB }
* (where B is feedforward, A is feedback.)
*/
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));
}


/**
* A collection of Float32Array as AudioBus abstraction.
* @param {Number} numberOfChannels Number of channels.
* @param {Number} length Buffer length in samples.
* @param {Number} sampleRate Operating sample rate.
*/
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.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.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(channel
+ ' => [' + this._channelData[channel].subarray(begin, end) + ']');
}
}

0 comments on commit cf3feae

Please sign in to comment.