assassin-bug/framework/resonator/vendor/resonance-es6/source.js

309 lines
12 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 Source model to spatialize an audio buffer.
* @author Andrew Allen <bitllama@google.com>
*/
'use strict';
// Internal dependencies.
import Directivity from './directivity.js';
import Attenuation from './attenuation.js';
import Encoder from './encoder.js';
import Utils from './utils.js';
/**
* Options for constructing a new Source.
* @typedef {Object} Source~SourceOptions
* @property {Float32Array} position
* The source's initial position (in meters), where origin is the center of
* the room. Defaults to {@linkcode Utils.DEFAULT_POSITION DEFAULT_POSITION}.
* @property {Float32Array} forward
* The source's initial forward vector. Defaults to
* {@linkcode Utils.DEFAULT_FORWARD DEFAULT_FORWARD}.
* @property {Float32Array} up
* The source's initial up vector. Defaults to
* {@linkcode Utils.DEFAULT_UP DEFAULT_UP}.
* @property {Number} minDistance
* Min. distance (in meters). Defaults to
* {@linkcode Utils.DEFAULT_MIN_DISTANCE DEFAULT_MIN_DISTANCE}.
* @property {Number} maxDistance
* Max. distance (in meters). Defaults to
* {@linkcode Utils.DEFAULT_MAX_DISTANCE DEFAULT_MAX_DISTANCE}.
* @property {string} rolloff
* Rolloff model to use, chosen from options in
* {@linkcode Utils.ATTENUATION_ROLLOFFS ATTENUATION_ROLLOFFS}. Defaults to
* {@linkcode Utils.DEFAULT_ATTENUATION_ROLLOFF DEFAULT_ATTENUATION_ROLLOFF}.
* @property {Number} gain Input gain (linear). Defaults to
* {@linkcode Utils.DEFAULT_SOURCE_GAIN DEFAULT_SOURCE_GAIN}.
* @property {Number} alpha Directivity alpha. Defaults to
* {@linkcode Utils.DEFAULT_DIRECTIVITY_ALPHA DEFAULT_DIRECTIVITY_ALPHA}.
* @property {Number} sharpness Directivity sharpness. Defaults to
* {@linkcode Utils.DEFAULT_DIRECTIVITY_SHARPNESS
* DEFAULT_DIRECTIVITY_SHARPNESS}.
* @property {Number} 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 Source
* @description Source model to spatialize an audio buffer.
* @param {ResonanceAudio} scene Associated {@link ResonanceAudio
* ResonanceAudio} instance.
* @param {Source~SourceOptions} options
* Options for constructing a new Source.
*/
class Source {
constructor(scene, options) {
// Public variables.
/**
* Mono (1-channel) input {@link
* https://developer.mozilla.org/en-US/docs/Web/API/AudioNode AudioNode}.
* @member {AudioNode} input
* @memberof Source
* @instance
*/
/**
*
*/
// Use defaults for undefined arguments.
if (options == undefined) {
options = {};
}
if (options.position == undefined) {
options.position = Utils.DEFAULT_POSITION.slice();
}
if (options.forward == undefined) {
options.forward = Utils.DEFAULT_FORWARD.slice();
}
if (options.up == undefined) {
options.up = Utils.DEFAULT_UP.slice();
}
if (options.minDistance == undefined) {
options.minDistance = Utils.DEFAULT_MIN_DISTANCE;
}
if (options.maxDistance == undefined) {
options.maxDistance = Utils.DEFAULT_MAX_DISTANCE;
}
if (options.rolloff == undefined) {
options.rolloff = Utils.DEFAULT_ROLLOFF;
}
if (options.gain == undefined) {
options.gain = Utils.DEFAULT_SOURCE_GAIN;
}
if (options.alpha == undefined) {
options.alpha = Utils.DEFAULT_DIRECTIVITY_ALPHA;
}
if (options.sharpness == undefined) {
options.sharpness = Utils.DEFAULT_DIRECTIVITY_SHARPNESS;
}
if (options.sourceWidth == undefined) {
options.sourceWidth = Utils.DEFAULT_SOURCE_WIDTH;
}
// Member variables.
this._scene = scene;
this._position = options.position;
this._forward = options.forward;
this._up = options.up;
this._dx = new Float32Array(3);
this._right = Utils.crossProduct(this._forward, this._up);
// Create audio nodes.
let context = scene._context;
this.input = context.createGain();
this._directivity = new Directivity(context, {
alpha: options.alpha,
sharpness: options.sharpness,
});
this._toEarly = context.createGain();
this._toLate = context.createGain();
this._attenuation = new Attenuation(context, {
minDistance: options.minDistance,
maxDistance: options.maxDistance,
rolloff: options.rolloff,
});
this._encoder = new Encoder(context, {
ambisonicOrder: scene._ambisonicOrder,
sourceWidth: options.sourceWidth,
});
// Connect nodes.
this.input.connect(this._toLate);
this._toLate.connect(scene._room.late.input);
this.input.connect(this._attenuation.input);
this._attenuation.output.connect(this._toEarly);
this._toEarly.connect(scene._room.early.input);
this._attenuation.output.connect(this._directivity.input);
this._directivity.output.connect(this._encoder.input);
this._encoder.output.connect(scene._listener.input);
// Assign initial conditions.
this.setPosition(options.position[0], options.position[1], options.position[2]);
this.input.gain.value = options.gain;
}
/**
* Set source's position (in meters), where origin is the center of
* the room.
* @param {Number} x
* @param {Number} y
* @param {Number} z
*/
setPosition(x, y, z) {
// Assign new position.
this._position[0] = x;
this._position[1] = y;
this._position[2] = z;
// Handle far-field effect.
let distance = this._scene._room.getDistanceOutsideRoom(this._position[0], this._position[1], this._position[2]);
let gain = _computeDistanceOutsideRoom(distance);
this._toLate.gain.value = gain;
this._toEarly.gain.value = gain;
this._update();
}
// Update the source when changing the listener's position.
_update() {
// Compute distance to listener.
for (let i = 0; i < 3; i++) {
this._dx[i] = this._position[i] - this._scene._listener.position[i];
}
let distance = Math.sqrt(this._dx[0] * this._dx[0] +
this._dx[1] * this._dx[1] + this._dx[2] * this._dx[2]);
if (distance > 0) {
// Normalize direction vector.
this._dx[0] /= distance;
this._dx[1] /= distance;
this._dx[2] /= distance;
}
// Compuete angle of direction vector.
let azimuth = Math.atan2(-this._dx[0], this._dx[2]) *
Utils.RADIANS_TO_DEGREES;
let elevation = Math.atan2(this._dx[1], Math.sqrt(this._dx[0] * this._dx[0] +
this._dx[2] * this._dx[2])) * Utils.RADIANS_TO_DEGREES;
// Set distance/directivity/direction values.
this._attenuation.setDistance(distance);
this._directivity.computeAngle(this._forward, this._dx);
this._encoder.setDirection(azimuth, elevation);
}
/**
* Set source's rolloff.
* @param {string} rolloff
* Rolloff model to use, chosen from options in
* {@linkcode Utils.ATTENUATION_ROLLOFFS ATTENUATION_ROLLOFFS}.
*/
setRolloff(rolloff) {
this._attenuation.setRolloff(rolloff);
}
/**
* Set source's minimum distance (in meters).
* @param {Number} minDistance
*/
setMinDistance(minDistance) {
this._attenuation.minDistance = minDistance;
}
/**
* Set source's maximum distance (in meters).
* @param {Number} maxDistance
*/
setMaxDistance(maxDistance) {
this._attenuation.maxDistance = maxDistance;
}
/**
* Set source's gain (linear).
* @param {Number} gain
*/
setGain(gain) {
this.input.gain.value = gain;
}
/**
* Set the source's orientation using forward and up vectors.
* @param {Number} forwardX
* @param {Number} forwardY
* @param {Number} forwardZ
* @param {Number} upX
* @param {Number} upY
* @param {Number} upZ
*/
setOrientation(forwardX, forwardY, forwardZ, upX, upY, upZ) {
this._forward[0] = forwardX;
this._forward[1] = forwardY;
this._forward[2] = forwardZ;
this._up[0] = upX;
this._up[1] = upY;
this._up[2] = upZ;
this._right = Utils.crossProduct(this._forward, this._up);
}
// TODO(bitllama): Make sure this works with Three.js as intended.
/**
* Set source's position and orientation using a
* Three.js modelViewMatrix object.
* @param {Float32Array} matrix4
* The Matrix4 representing the object position and rotation in world space.
*/
setFromMatrix(matrix4) {
this._right[0] = matrix4.elements[0];
this._right[1] = matrix4.elements[1];
this._right[2] = matrix4.elements[2];
this._up[0] = matrix4.elements[4];
this._up[1] = matrix4.elements[5];
this._up[2] = matrix4.elements[6];
this._forward[0] = matrix4.elements[8];
this._forward[1] = matrix4.elements[9];
this._forward[2] = matrix4.elements[10];
// Normalize to remove scaling.
this._right = Utils.normalizeVector(this._right);
this._up = Utils.normalizeVector(this._up);
this._forward = Utils.normalizeVector(this._forward);
// Update position.
this.setPosition(matrix4.elements[12], matrix4.elements[13], matrix4.elements[14]);
}
/**
* 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) {
this._encoder.setSourceWidth(sourceWidth);
this.setPosition(this._position[0], this._position[1], this._position[2]);
}
/**
* Set source's directivity pattern (defined by alpha), where 0 is an
* omnidirectional pattern, 1 is a bidirectional pattern, 0.5 is a cardiod
* pattern. The sharpness of the pattern is increased exponentially.
* @param {Number} alpha
* Determines directivity pattern (0 to 1).
* @param {Number} sharpness
* Determines the sharpness of the directivity pattern (1 to Inf).
*/
setDirectivityPattern(alpha, sharpness) {
this._directivity.setPattern(alpha, sharpness);
this.setPosition(this._position[0], this._position[1], this._position[2]);
}
}
/**
* Determine the distance a source is outside of a room. Attenuate gain going
* to the reflections and reverb when the source is outside of the room.
* @param {Number} distance Distance in meters.
* @return {Number} Gain (linear) of source.
* @private
*/
function _computeDistanceOutsideRoom(distance) {
// We apply a linear ramp from 1 to 0 as the source is up to 1m outside.
let gain = 1;
if (distance > Utils.EPSILON_FLOAT) {
gain = 1 - distance / Utils.SOURCE_MAX_OUTSIDE_ROOM_DISTANCE;
// Clamp gain between 0 and 1.
gain = Math.max(0, Math.min(1, gain));
}
return gain;
}
export default Source;