195 lines
7.9 KiB
JavaScript
195 lines
7.9 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2017 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.
|
|
*/
|
|
/**
|
|
* @file Spatially encodes input using weighted spherical harmonics.
|
|
* @author Andrew Allen <bitllama@google.com>
|
|
*/
|
|
'use strict';
|
|
// Internal dependencies.
|
|
import Tables from './tables.js';
|
|
import Utils from './utils.js';
|
|
/**
|
|
* @class Encoder
|
|
* @description Spatially encodes input using weighted spherical harmonics.
|
|
* @param {AudioContext} context
|
|
* Associated {@link
|
|
https://developer.mozilla.org/en-US/docs/Web/API/AudioContext AudioContext}.
|
|
* @param {Object} options
|
|
* @param {Number} options.ambisonicOrder
|
|
* Desired ambisonic order. Defaults to
|
|
* {@linkcode Utils.DEFAULT_AMBISONIC_ORDER DEFAULT_AMBISONIC_ORDER}.
|
|
* @param {Number} options.azimuth
|
|
* Azimuth (in degrees). Defaults to
|
|
* {@linkcode Utils.DEFAULT_AZIMUTH DEFAULT_AZIMUTH}.
|
|
* @param {Number} options.elevation
|
|
* Elevation (in degrees). Defaults to
|
|
* {@linkcode Utils.DEFAULT_ELEVATION DEFAULT_ELEVATION}.
|
|
* @param {Number} options.sourceWidth
|
|
* Source width (in degrees). Where 0 degrees is a point source and 360 degrees
|
|
* is an omnidirectional source. Defaults to
|
|
* {@linkcode Utils.DEFAULT_SOURCE_WIDTH DEFAULT_SOURCE_WIDTH}.
|
|
*/
|
|
class Encoder {
|
|
constructor(context, options) {
|
|
// Public variables.
|
|
/**
|
|
* Mono (1-channel) input {@link
|
|
* https://developer.mozilla.org/en-US/docs/Web/API/AudioNode AudioNode}.
|
|
* @member {AudioNode} input
|
|
* @memberof Encoder
|
|
* @instance
|
|
*/
|
|
/**
|
|
* Ambisonic (multichannel) output {@link
|
|
* https://developer.mozilla.org/en-US/docs/Web/API/AudioNode AudioNode}.
|
|
* @member {AudioNode} output
|
|
* @memberof Encoder
|
|
* @instance
|
|
*/
|
|
// Use defaults for undefined arguments.
|
|
if (options == undefined) {
|
|
options = {};
|
|
}
|
|
if (options.ambisonicOrder == undefined) {
|
|
options.ambisonicOrder = Utils.DEFAULT_AMBISONIC_ORDER;
|
|
}
|
|
if (options.azimuth == undefined) {
|
|
options.azimuth = Utils.DEFAULT_AZIMUTH;
|
|
}
|
|
if (options.elevation == undefined) {
|
|
options.elevation = Utils.DEFAULT_ELEVATION;
|
|
}
|
|
if (options.sourceWidth == undefined) {
|
|
options.sourceWidth = Utils.DEFAULT_SOURCE_WIDTH;
|
|
}
|
|
this._context = context;
|
|
// Create I/O nodes.
|
|
this.input = context.createGain();
|
|
this._channelGain = [];
|
|
this._merger = undefined;
|
|
this.output = context.createGain();
|
|
// Set initial order, angle and source width.
|
|
this.setAmbisonicOrder(options.ambisonicOrder);
|
|
this._azimuth = options.azimuth;
|
|
this._elevation = options.elevation;
|
|
this.setSourceWidth(options.sourceWidth);
|
|
}
|
|
/**
|
|
* Set the desired ambisonic order.
|
|
* @param {Number} ambisonicOrder Desired ambisonic order.
|
|
*/
|
|
setAmbisonicOrder(ambisonicOrder) {
|
|
this._ambisonicOrder = Encoder.validateAmbisonicOrder(ambisonicOrder);
|
|
this.input.disconnect();
|
|
for (let i = 0; i < this._channelGain.length; i++) {
|
|
this._channelGain[i].disconnect();
|
|
}
|
|
if (this._merger != undefined) {
|
|
this._merger.disconnect();
|
|
}
|
|
delete this._channelGain;
|
|
delete this._merger;
|
|
// Create audio graph.
|
|
let numChannels = (this._ambisonicOrder + 1) * (this._ambisonicOrder + 1);
|
|
this._merger = this._context.createChannelMerger(numChannels);
|
|
this._channelGain = new Array(numChannels);
|
|
for (let i = 0; i < numChannels; i++) {
|
|
this._channelGain[i] = this._context.createGain();
|
|
this.input.connect(this._channelGain[i]);
|
|
this._channelGain[i].connect(this._merger, 0, i);
|
|
}
|
|
this._merger.connect(this.output);
|
|
}
|
|
/**
|
|
* Set the direction of the encoded source signal.
|
|
* @param {Number} azimuth
|
|
* Azimuth (in degrees). Defaults to
|
|
* {@linkcode Utils.DEFAULT_AZIMUTH DEFAULT_AZIMUTH}.
|
|
* @param {Number} elevation
|
|
* Elevation (in degrees). Defaults to
|
|
* {@linkcode Utils.DEFAULT_ELEVATION DEFAULT_ELEVATION}.
|
|
*/
|
|
setDirection(azimuth, elevation) {
|
|
// Format input direction to nearest indices.
|
|
if (azimuth == undefined || isNaN(azimuth)) {
|
|
azimuth = Utils.DEFAULT_AZIMUTH;
|
|
}
|
|
if (elevation == undefined || isNaN(elevation)) {
|
|
elevation = Utils.DEFAULT_ELEVATION;
|
|
}
|
|
// Store the formatted input (for updating source width).
|
|
this._azimuth = azimuth;
|
|
this._elevation = elevation;
|
|
// Format direction for index lookups.
|
|
azimuth = Math.round(azimuth % 360);
|
|
if (azimuth < 0) {
|
|
azimuth += 360;
|
|
}
|
|
elevation = Math.round(Math.min(90, Math.max(-90, elevation))) + 90;
|
|
// Assign gains to each output.
|
|
this._channelGain[0].gain.value = Tables.MAX_RE_WEIGHTS[this._spreadIndex][0];
|
|
for (let i = 1; i <= this._ambisonicOrder; i++) {
|
|
let degreeWeight = Tables.MAX_RE_WEIGHTS[this._spreadIndex][i];
|
|
for (let j = -i; j <= i; j++) {
|
|
let acnChannel = (i * i) + i + j;
|
|
let elevationIndex = i * (i + 1) / 2 + Math.abs(j) - 1;
|
|
let val = Tables.SPHERICAL_HARMONICS[1][elevation][elevationIndex];
|
|
if (j != 0) {
|
|
let azimuthIndex = Tables.SPHERICAL_HARMONICS_MAX_ORDER + j - 1;
|
|
if (j < 0) {
|
|
azimuthIndex = Tables.SPHERICAL_HARMONICS_MAX_ORDER + j;
|
|
}
|
|
val *= Tables.SPHERICAL_HARMONICS[0][azimuth][azimuthIndex];
|
|
}
|
|
this._channelGain[acnChannel].gain.value = val * degreeWeight;
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Set the source width (in degrees). Where 0 degrees is a point source and 360
|
|
* degrees is an omnidirectional source.
|
|
* @param {Number} sourceWidth (in degrees).
|
|
*/
|
|
setSourceWidth(sourceWidth) {
|
|
// The MAX_RE_WEIGHTS is a 360 x (Tables.SPHERICAL_HARMONICS_MAX_ORDER+1)
|
|
// size table.
|
|
this._spreadIndex = Math.min(359, Math.max(0, Math.round(sourceWidth)));
|
|
this.setDirection(this._azimuth, this._elevation);
|
|
}
|
|
}
|
|
/**
|
|
* Validate the provided ambisonic order.
|
|
* @param {Number} ambisonicOrder Desired ambisonic order.
|
|
* @return {Number} Validated/adjusted ambisonic order.
|
|
* @private
|
|
*/
|
|
Encoder.validateAmbisonicOrder = ambisonicOrder => {
|
|
if (isNaN(ambisonicOrder) || ambisonicOrder == undefined) {
|
|
Utils.log('Error: Invalid ambisonic order', options.ambisonicOrder, '\nUsing ambisonicOrder=1 instead.');
|
|
ambisonicOrder = 1;
|
|
}
|
|
else if (ambisonicOrder < 1) {
|
|
Utils.log('Error: Unable to render ambisonic order', options.ambisonicOrder, '(Min order is 1)', '\nUsing min order instead.');
|
|
ambisonicOrder = 1;
|
|
}
|
|
else if (ambisonicOrder > Tables.SPHERICAL_HARMONICS_MAX_ORDER) {
|
|
Utils.log('Error: Unable to render ambisonic order', options.ambisonicOrder, '(Max order is', Tables.SPHERICAL_HARMONICS_MAX_ORDER, ')\nUsing max order instead.');
|
|
options.ambisonicOrder = Tables.SPHERICAL_HARMONICS_MAX_ORDER;
|
|
}
|
|
return ambisonicOrder;
|
|
};
|
|
export default Encoder;
|