Skip to content

Commit

Permalink
Merge pull request #26 from GoogleChrome/0.1.7-dev
Browse files Browse the repository at this point in the history
Update to 0.1.7.

- Edits README
- FOAPhaseMatchedFilter is now properly calculated based on context sample rate.
- FOARotator now supports 4x4 rotation matrix. (for Three.js)
- Adds a unit test for FOARotator.
  • Loading branch information
hoch authored Oct 25, 2016
2 parents 29a0467 + d498f79 commit 61c03fa
Show file tree
Hide file tree
Showing 15 changed files with 289 additions and 64 deletions.
47 changes: 34 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ Omnitone is a robust implementation of [FOA (first-order-ambisonic)](https://en.

The implementation of Omnitone is based on the [Google spatial media](https://github.com/google/spatial-media) specification. The input audio stream must be configured to ACN channel layout with SN3D normalization.


## Installation

Omnitone is designed to be used for the web-facing projects, so the installation via [Bower](https://bower.io/) or [NPM](https://www.npmjs.com/) is recommended. Alternatively, you can clone or download this repository and use the library script file as usual.

```bash
bower install omnitone
// Or
npm install omnitone
```
- [Installation](#installation)
- [Usage](#usage)
- [Advanced Usage](#advanced-usage)
+ [FOADecoder](#foadecoder)
+ [FOARouter](#foarouter)
+ [FOARotator](#foarotator)
+ [FOAPhaseMatchedFilter](#foaphasematchedfilter)
+ [FORVirtualSpeaker](#forvirtualspeaker)
- [Building](#building)
- [Audio Codec compatibility](#audio-codec-compatibility)
- [Related Resources](#related-resouces)


## How it works
Expand All @@ -25,6 +26,15 @@ Omnitone is a high-level library that abstracts various technical layers of the
</p>


## Installation

Omnitone is designed to be used for the web projects, so the installation via [NPM](https://www.npmjs.com/) is recommended. Alternatively, you can clone or download this repository and use the library script file as usual.

```bash
npm install omnitone
```


## Usage

The first step is to include the library file in an HTML document.
Expand Down Expand Up @@ -52,7 +62,7 @@ decoder.initialize().then(function () {
});
```

The decoder constructor accepts the context and the element as arguments. Omnitone uses [HRTFs](https://github.com/google/spatial-media/tree/master/support/hrtfs/cube) from Google spatial media repository, but you can use a custom set of HRTF files as well. The initialization of a decoder instance returns a promise which resolves when the resources (i.e. impulse responses) are fully loaded.
The decoder constructor accepts the context and the element as arguments. Omnitone uses [HRIRs](https://github.com/google/spatial-media/tree/master/support/hrtfs/cube) from Google spatial media repository, but you can use a custom set of HRIR files as well. The initialization of a decoder instance returns a promise which resolves when the resources (i.e. impulse responses) are fully loaded.

The rotation matrix (3x3, row-major) in the decoder can be updated inside of the graphics render loop. This operation rotates the entire sound field. The rotation matrix is commonly derived from the quaternion of the orientation sensor on the VR headset or the smartphone. Also Omnitone converts the coordinate system from the WebGL space to the audio space internally, so you need not to transform the matrix manually.

Expand All @@ -61,6 +71,13 @@ The rotation matrix (3x3, row-major) in the decoder can be updated inside of the
decoder.setRotationMatrix(rotationMatrix);
```

If you prefer to work with 4x4 rotation matrix (e.g. Three.js camera), you can use the following method instead.

```js
// Rotate the sound field based on a Three.js camera object.
decoder.setRotationMatrixFromCamera(camera.matrix);
```

Use `setMode` method to change the setting of the decoder. This is useful when the media source does not have spatially encoded (e.g. stereo or mono) or when you want to reduce the CPU usage or the power consumption by disabling the decoder.

```js
Expand Down Expand Up @@ -130,8 +147,12 @@ var rotator = Omnitone.createFOARotator(context);

```js
rotator.setRotationMatrix([1, 0, 0, 0, 1, 0, 0, 0, 1]); // 3x3 row-major matrix.
rotator.setRotationMatrix4([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]); // 4x4 row-major matrix.
```

* rotationMatrix (Array): 3x3 row-major matrix.
* rotationMatrix4 (Array): 4x4 row-major matrix.

### FOAPhaseMatchedFilter

`FOAPhaseMatchedFilter` is a pair of pass filters (LP/HP) with a crossover frequency to compensate the gain of high frequency contents without a phase difference.
Expand Down Expand Up @@ -171,7 +192,7 @@ Deactivating a virtual speaker can save CPU powers. Running multiple HRTF convol
Omnitone uses [WebPack](https://webpack.github.io/) to build the minified library and to manage dependencies.

```bash
npm run install # install dependencies.
npm install # install dependencies.
npm run build # build a non-minified library.
npm run watch # recompile whenever any source file changes.
npm run build-all # build a minified library and copy static resources.
Expand All @@ -180,7 +201,7 @@ npm run build-all # build a minified library and copy static resources.

## Audio Codec Compatibility

Omnitone is designed to run any browser that supports Web Audio API, however, it does not address the incompatibility issue around various media codec in the browsers. At the time of writing, the decoding of compressed multichannel audio via `<video>` or `<audio>` elements is not fully supported by the majority of mobile browsers.
Omnitone is designed to run any browser that supports Web Audio API, however, it does not address the incompatibility issue around various media codecs in the browsers. At the time of writing, the decoding of compressed multichannel audio via `<video>` or `<audio>` elements is not fully supported by the majority of mobile browsers.


## Related Resources
Expand Down
4 changes: 2 additions & 2 deletions 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.6",
"version": "0.1.7",
"description": "Spatial Audio Decoder in Web Audio API",
"main": "src/main.js",
"keywords": [
Expand Down
10 changes: 5 additions & 5 deletions src/audiobuffer-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function AudioBufferManager(context, audioFileData, resolve, reject, progress) {

// Check for duplicates filename and quit if it happens.
if (this._loadingTasks.hasOwnProperty(fileInfo.name)) {
Utils.LOG('Duplicated filename when loading: ' + fileInfo.name);
Utils.log('Duplicated filename when loading: ' + fileInfo.name);
return;
}

Expand All @@ -65,24 +65,24 @@ AudioBufferManager.prototype._loadAudioFile = function (fileInfo) {
if (xhr.status === 200) {
that._context.decodeAudioData(xhr.response,
function (buffer) {
// Utils.LOG('File loaded: ' + fileInfo.url);
// Utils.log('File loaded: ' + fileInfo.url);
that._done(fileInfo.name, buffer);
},
function (message) {
Utils.LOG('Decoding failure: '
Utils.log('Decoding failure: '
+ fileInfo.url + ' (' + message + ')');
that._done(fileInfo.name, null);
});
} else {
Utils.LOG('XHR Error: ' + fileInfo.url + ' (' + xhr.statusText
Utils.log('XHR Error: ' + fileInfo.url + ' (' + xhr.statusText
+ ')');
that._done(fileInfo.name, null);
}
};

// TODO: fetch local resources if XHR fails.
xhr.onerror = function (event) {
Utils.LOG('XHR Network failure: ' + fileInfo.url);
Utils.log('XHR Network failure: ' + fileInfo.url);
that._done(fileInfo.name, null);
};

Expand Down
26 changes: 20 additions & 6 deletions src/foa-decoder.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,20 +79,22 @@ function FOADecoder (context, videoElement, options) {
coef: FOASpeakerData[i].coef
});
}

this._tempMatrix4 = new Float32Array(16);
}

/**
* Initialize and load the resources for the decode.
* @return {Promise}
*/
FOADecoder.prototype.initialize = function () {
Utils.LOG('Version: ' + SystemVersion);
Utils.LOG('Initializing... (mode: ' + this._decodingMode + ')');
Utils.log('Version: ' + SystemVersion);
Utils.log('Initializing... (mode: ' + this._decodingMode + ')');

// Rerouting channels if necessary.
var channelMapString = this._channelMap.toString();
if (channelMapString !== CHANNEL_MAP.toString()) {
Utils.LOG('Remapping channels ([0,1,2,3] -> ['
Utils.log('Remapping channels ([0,1,2,3] -> ['
+ channelMapString + '])');
}

Expand All @@ -114,7 +116,7 @@ FOADecoder.prototype.initialize = function () {

// Get the linear amplitude from the post gain option, which is in decibel.
var postGainLinear = Math.pow(10, this._postGainDB/20);
Utils.LOG('Gain compensation: ' + postGainLinear + ' (' + this._postGainDB
Utils.log('Gain compensation: ' + postGainLinear + ' (' + this._postGainDB
+ 'dB)');

// This returns a promise so developers can use the decoder when it is ready.
Expand All @@ -136,7 +138,7 @@ FOADecoder.prototype.initialize = function () {
// Set the decoding mode.
me.setMode(me._decodingMode);
me._isDecoderReady = true;
Utils.LOG('HRTF IRs are loaded successfully. The decoder is ready.');
Utils.log('HRTF IRs are loaded successfully. The decoder is ready.');

resolve();
}, reject);
Expand All @@ -152,6 +154,18 @@ FOADecoder.prototype.setRotationMatrix = function (rotationMatrix) {
this._foaRotator.setRotationMatrix(rotationMatrix);
};


/**
* Update the rotation matrix from a Three.js camera object.
* @param {Object} cameraMatrix The Matrix4 obejct of Three.js the camera.
*/
FOADecoder.prototype.setRotationMatrixFromCamera = function (cameraMatrix) {
// Extract the inner array elements and inverse. (The actual view rotation is
// the opposite of the camera movement.)
Utils.invertMatrix4(this._tempMatrix4, cameraMatrix.elements);
this._foaRotator.setRotationMatrix4(this._tempMatrix4);
};

/**
* Set the decoding mode.
* @param {String} mode Decoding mode. When the mode is 'bypass'
Expand Down Expand Up @@ -193,7 +207,7 @@ FOADecoder.prototype.setMode = function (mode) {
break;
}

Utils.LOG('Decoding mode changed. (' + mode + ')');
Utils.log('Decoding mode changed. (' + mode + ')');
};

module.exports = FOADecoder;
45 changes: 27 additions & 18 deletions src/foa-phase-matched-filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,22 @@
var Utils = require('./utils.js');

// Static parameters.
var FREQUENCY = 700;
var COEFFICIENTS = [1.4142, 0.8166, 0.8166, 0.8166];
var CROSSOVER_FREQUENCY = 690;
var GAIN_COEFFICIENTS = [1.4142, 0.8166, 0.8166, 0.8166];

// Helper: generate the coefficients for dual band filter.
function generateDualBandCoefficients(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]
};
}

/**
* @class FOAPhaseMatchedFilter
Expand All @@ -39,23 +53,18 @@ function FOAPhaseMatchedFilter (context) {

this._input = this._context.createGain();

// TODO: calculate the freq/reso based on the context sample rate.
if (!this._context.createIIRFilter) {
Utils.LOG('IIR filter is missing. Using Biquad filter instead.');
Utils.log('IIR filter is missing. Using Biquad filter instead.');
this._lpf = this._context.createBiquadFilter();
this._hpf = this._context.createBiquadFilter();
this._lpf.frequency.value = FREQUENCY;
this._hpf.frequency.value = FREQUENCY;
this._lpf.frequency.value = CROSSOVER_FREQUENCY;
this._hpf.frequency.value = CROSSOVER_FREQUENCY;
this._hpf.type = 'highpass';
} else {
this._lpf = this._context.createIIRFilter(
[0.00058914319, 0.0011782864, 0.00058914319],
[1, -1.9029109, 0.90526748]
);
this._hpf = this._context.createIIRFilter(
[0.95204461, -1.9040892, 0.95204461],
[1, -1.9029109, 0.90526748]
);
var coef = generateDualBandCoefficients(
CROSSOVER_FREQUENCY, this._context.sampleRate);
this._lpf = this._context.createIIRFilter(coef.lowpassB, coef.lowpassA);
this._hpf = this._context.createIIRFilter(coef.hipassB, coef.hipassA);
}

this._splitterLow = this._context.createChannelSplitter(4);
Expand Down Expand Up @@ -87,10 +96,10 @@ 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 * COEFFICIENTS[0];
this._gainHighY.gain.value = -1 * COEFFICIENTS[1];
this._gainHighZ.gain.value = -1 * COEFFICIENTS[2];
this._gainHighX.gain.value = -1 * 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];

// Input/output Proxy.
this.input = this._input;
Expand Down
17 changes: 16 additions & 1 deletion src/foa-rotator.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ function FOARotator (context) {


/**
* Set 3x3 matrix for soundfield rotation.
* Set 3x3 matrix for soundfield rotation. (gl-matrix.js style)
* @param {Array} rotationMatrix A 3x3 matrix of soundfield rotation. The
* matrix is in the row-major representation.
*/
Expand All @@ -109,6 +109,21 @@ FOARotator.prototype.setRotationMatrix = function (rotationMatrix) {
this._m8.gain.value = rotationMatrix[8];
};

/**
* Set 4x4 matrix for soundfield rotation. (Three.js style)
* @param {Array} rotationMatrix4 A 4x4 matrix of soundfield rotation.
*/
FOARotator.prototype.setRotationMatrix4 = function (rotationMatrix4) {
this._m0.gain.value = rotationMatrix4[0];
this._m1.gain.value = rotationMatrix4[1];
this._m2.gain.value = rotationMatrix4[2];
this._m3.gain.value = rotationMatrix4[4];
this._m4.gain.value = rotationMatrix4[5];
this._m5.gain.value = rotationMatrix4[6];
this._m6.gain.value = rotationMatrix4[8];
this._m7.gain.value = rotationMatrix4[9];
this._m8.gain.value = rotationMatrix4[10];
};

/**
* Returns the current rotation matrix.
Expand Down
4 changes: 2 additions & 2 deletions src/foa-router.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
*/

var DEFAULT_CHANNEL_MAP = [0, 1, 2, 3];
var IOS_CHANNEL_MAP = [2, 0, 1, 3];
var FUMA_2_ACN_CHANNEL_MAP = [0, 3, 1, 2];
// var IOS_CHANNEL_MAP = [2, 0, 1, 3];
// var FUMA_2_ACN_CHANNEL_MAP = [0, 3, 1, 2];


/**
Expand Down
1 change: 0 additions & 1 deletion src/omnitone.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ var FOARotator = require('./foa-rotator.js');
var FOAPhaseMatchedFilter = require('./foa-phase-matched-filter.js');
var FOAVirtualSpeaker = require('./foa-virtual-speaker.js');
var FOADecoder = require('./foa-decoder.js');
var Utils = require('./utils.js');

/**
* Load audio buffers based on the speaker configuration map data.
Expand Down
Loading

0 comments on commit 61c03fa

Please sign in to comment.