301 lines
13 KiB
JavaScript
301 lines
13 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 Complete room model with early and late reflections.
|
||
|
* @author Andrew Allen <bitllama@google.com>
|
||
|
*/
|
||
|
'use strict';
|
||
|
// Internal dependencies.
|
||
|
import LateReflections from './late-reflections.js';
|
||
|
import EarlyReflections from './early-reflections.js';
|
||
|
import Utils from './utils.js';
|
||
|
/**
|
||
|
* Generate absorption coefficients from material names.
|
||
|
* @param {Object} materials
|
||
|
* @return {Object}
|
||
|
*/
|
||
|
function _getCoefficientsFromMaterials(materials) {
|
||
|
// Initialize coefficients to use defaults.
|
||
|
let coefficients = {};
|
||
|
for (let property in Utils.DEFAULT_ROOM_MATERIALS) {
|
||
|
if (Utils.DEFAULT_ROOM_MATERIALS.hasOwnProperty(property)) {
|
||
|
coefficients[property] = Utils.ROOM_MATERIAL_COEFFICIENTS[Utils.DEFAULT_ROOM_MATERIALS[property]];
|
||
|
}
|
||
|
}
|
||
|
// Sanitize materials.
|
||
|
if (materials == undefined) {
|
||
|
materials = {};
|
||
|
Object.assign(materials, Utils.DEFAULT_ROOM_MATERIALS);
|
||
|
}
|
||
|
// Assign coefficients using provided materials.
|
||
|
for (let property in Utils.DEFAULT_ROOM_MATERIALS) {
|
||
|
if (Utils.DEFAULT_ROOM_MATERIALS.hasOwnProperty(property) &&
|
||
|
materials.hasOwnProperty(property)) {
|
||
|
if (materials[property] in Utils.ROOM_MATERIAL_COEFFICIENTS) {
|
||
|
coefficients[property] =
|
||
|
Utils.ROOM_MATERIAL_COEFFICIENTS[materials[property]];
|
||
|
}
|
||
|
else {
|
||
|
Utils.log('Material \"' + materials[property] + '\" on wall \"' +
|
||
|
property + '\" not found. Using \"' +
|
||
|
Utils.DEFAULT_ROOM_MATERIALS[property] + '\".');
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
Utils.log('Wall \"' + property + '\" is not defined. Default used.');
|
||
|
}
|
||
|
}
|
||
|
return coefficients;
|
||
|
}
|
||
|
/**
|
||
|
* Sanitize coefficients.
|
||
|
* @param {Object} coefficients
|
||
|
* @return {Object}
|
||
|
*/
|
||
|
function _sanitizeCoefficients(coefficients) {
|
||
|
if (coefficients == undefined) {
|
||
|
coefficients = {};
|
||
|
}
|
||
|
for (let property in Utils.DEFAULT_ROOM_MATERIALS) {
|
||
|
if (!(coefficients.hasOwnProperty(property))) {
|
||
|
// If element is not present, use default coefficients.
|
||
|
coefficients[property] = Utils.ROOM_MATERIAL_COEFFICIENTS[Utils.DEFAULT_ROOM_MATERIALS[property]];
|
||
|
}
|
||
|
}
|
||
|
return coefficients;
|
||
|
}
|
||
|
/**
|
||
|
* Sanitize dimensions.
|
||
|
* @param {Utils~RoomDimensions} dimensions
|
||
|
* @return {Utils~RoomDimensions}
|
||
|
*/
|
||
|
function _sanitizeDimensions(dimensions) {
|
||
|
if (dimensions == undefined) {
|
||
|
dimensions = {};
|
||
|
}
|
||
|
for (let property in Utils.DEFAULT_ROOM_DIMENSIONS) {
|
||
|
if (!(dimensions.hasOwnProperty(property))) {
|
||
|
dimensions[property] = Utils.DEFAULT_ROOM_DIMENSIONS[property];
|
||
|
}
|
||
|
}
|
||
|
return dimensions;
|
||
|
}
|
||
|
/**
|
||
|
* Compute frequency-dependent reverb durations.
|
||
|
* @param {Utils~RoomDimensions} dimensions
|
||
|
* @param {Object} coefficients
|
||
|
* @param {Number} speedOfSound
|
||
|
* @return {Array}
|
||
|
*/
|
||
|
function _getDurationsFromProperties(dimensions, coefficients, speedOfSound) {
|
||
|
let durations = new Float32Array(Utils.NUMBER_REVERB_FREQUENCY_BANDS);
|
||
|
// Sanitize inputs.
|
||
|
dimensions = _sanitizeDimensions(dimensions);
|
||
|
coefficients = _sanitizeCoefficients(coefficients);
|
||
|
if (speedOfSound == undefined) {
|
||
|
speedOfSound = Utils.DEFAULT_SPEED_OF_SOUND;
|
||
|
}
|
||
|
// Acoustic constant.
|
||
|
let k = Utils.TWENTY_FOUR_LOG10 / speedOfSound;
|
||
|
// Compute volume, skip if room is not present.
|
||
|
let volume = dimensions.width * dimensions.height * dimensions.depth;
|
||
|
if (volume < Utils.ROOM_MIN_VOLUME) {
|
||
|
return durations;
|
||
|
}
|
||
|
// Room surface area.
|
||
|
let leftRightArea = dimensions.width * dimensions.height;
|
||
|
let floorCeilingArea = dimensions.width * dimensions.depth;
|
||
|
let frontBackArea = dimensions.depth * dimensions.height;
|
||
|
let totalArea = 2 * (leftRightArea + floorCeilingArea + frontBackArea);
|
||
|
for (let i = 0; i < Utils.NUMBER_REVERB_FREQUENCY_BANDS; i++) {
|
||
|
// Effective absorptive area.
|
||
|
let absorbtionArea = (coefficients.left[i] + coefficients.right[i]) * leftRightArea +
|
||
|
(coefficients.down[i] + coefficients.up[i]) * floorCeilingArea +
|
||
|
(coefficients.front[i] + coefficients.back[i]) * frontBackArea;
|
||
|
let meanAbsorbtionArea = absorbtionArea / totalArea;
|
||
|
// Compute reverberation using Eyring equation [1].
|
||
|
// [1] Beranek, Leo L. "Analysis of Sabine and Eyring equations and their
|
||
|
// application to concert hall audience and chair absorption." The
|
||
|
// Journal of the Acoustical Society of America, Vol. 120, No. 3.
|
||
|
// (2006), pp. 1399-1399.
|
||
|
durations[i] = Utils.ROOM_EYRING_CORRECTION_COEFFICIENT * k * volume /
|
||
|
(-totalArea * Math.log(1 - meanAbsorbtionArea) + 4 *
|
||
|
Utils.ROOM_AIR_ABSORPTION_COEFFICIENTS[i] * volume);
|
||
|
}
|
||
|
return durations;
|
||
|
}
|
||
|
/**
|
||
|
* Compute reflection coefficients from absorption coefficients.
|
||
|
* @param {Object} absorptionCoefficients
|
||
|
* @return {Object}
|
||
|
*/
|
||
|
function _computeReflectionCoefficients(absorptionCoefficients) {
|
||
|
let reflectionCoefficients = [];
|
||
|
for (let property in Utils.DEFAULT_REFLECTION_COEFFICIENTS) {
|
||
|
if (Utils.DEFAULT_REFLECTION_COEFFICIENTS
|
||
|
.hasOwnProperty(property)) {
|
||
|
// Compute average absorption coefficient (per wall).
|
||
|
reflectionCoefficients[property] = 0;
|
||
|
for (let j = 0; j < Utils.NUMBER_REFLECTION_AVERAGING_BANDS; j++) {
|
||
|
let bandIndex = j + Utils.ROOM_STARTING_AVERAGING_BAND;
|
||
|
reflectionCoefficients[property] +=
|
||
|
absorptionCoefficients[property][bandIndex];
|
||
|
}
|
||
|
reflectionCoefficients[property] /=
|
||
|
Utils.NUMBER_REFLECTION_AVERAGING_BANDS;
|
||
|
// Convert absorption coefficient to reflection coefficient.
|
||
|
reflectionCoefficients[property] =
|
||
|
Math.sqrt(1 - reflectionCoefficients[property]);
|
||
|
}
|
||
|
}
|
||
|
return reflectionCoefficients;
|
||
|
}
|
||
|
/**
|
||
|
* @class Room
|
||
|
* @description Model that manages early and late reflections using acoustic
|
||
|
* properties and listener position relative to a rectangular room.
|
||
|
* @param {AudioContext} context
|
||
|
* Associated {@link
|
||
|
https://developer.mozilla.org/en-US/docs/Web/API/AudioContext AudioContext}.
|
||
|
* @param {Object} options
|
||
|
* @param {Float32Array} options.listenerPosition
|
||
|
* The listener's initial position (in meters), where origin is the center of
|
||
|
* the room. Defaults to {@linkcode Utils.DEFAULT_POSITION DEFAULT_POSITION}.
|
||
|
* @param {Utils~RoomDimensions} options.dimensions Room dimensions (in meters). Defaults to
|
||
|
* {@linkcode Utils.DEFAULT_ROOM_DIMENSIONS DEFAULT_ROOM_DIMENSIONS}.
|
||
|
* @param {Utils~RoomMaterials} options.materials Named acoustic materials per wall.
|
||
|
* Defaults to {@linkcode Utils.DEFAULT_ROOM_MATERIALS DEFAULT_ROOM_MATERIALS}.
|
||
|
* @param {Number} options.speedOfSound
|
||
|
* (in meters/second). Defaults to
|
||
|
* {@linkcode Utils.DEFAULT_SPEED_OF_SOUND DEFAULT_SPEED_OF_SOUND}.
|
||
|
*/
|
||
|
class Room {
|
||
|
constructor(context, options) {
|
||
|
// Public variables.
|
||
|
/**
|
||
|
* EarlyReflections {@link EarlyReflections EarlyReflections} submodule.
|
||
|
* @member {AudioNode} early
|
||
|
* @memberof Room
|
||
|
* @instance
|
||
|
*/
|
||
|
/**
|
||
|
* LateReflections {@link LateReflections LateReflections} submodule.
|
||
|
* @member {AudioNode} late
|
||
|
* @memberof Room
|
||
|
* @instance
|
||
|
*/
|
||
|
/**
|
||
|
* Ambisonic (multichannel) output {@link
|
||
|
* https://developer.mozilla.org/en-US/docs/Web/API/AudioNode AudioNode}.
|
||
|
* @member {AudioNode} output
|
||
|
* @memberof Room
|
||
|
* @instance
|
||
|
*/
|
||
|
// Use defaults for undefined arguments.
|
||
|
if (options == undefined) {
|
||
|
options = {};
|
||
|
}
|
||
|
if (options.listenerPosition == undefined) {
|
||
|
options.listenerPosition = Utils.DEFAULT_POSITION.slice();
|
||
|
}
|
||
|
if (options.dimensions == undefined) {
|
||
|
options.dimensions = {};
|
||
|
Object.assign(options.dimensions, Utils.DEFAULT_ROOM_DIMENSIONS);
|
||
|
}
|
||
|
if (options.materials == undefined) {
|
||
|
options.materials = {};
|
||
|
Object.assign(options.materials, Utils.DEFAULT_ROOM_MATERIALS);
|
||
|
}
|
||
|
if (options.speedOfSound == undefined) {
|
||
|
options.speedOfSound = Utils.DEFAULT_SPEED_OF_SOUND;
|
||
|
}
|
||
|
// Sanitize room-properties-related arguments.
|
||
|
options.dimensions = _sanitizeDimensions(options.dimensions);
|
||
|
let absorptionCoefficients = _getCoefficientsFromMaterials(options.materials);
|
||
|
let reflectionCoefficients = _computeReflectionCoefficients(absorptionCoefficients);
|
||
|
let durations = _getDurationsFromProperties(options.dimensions, absorptionCoefficients, options.speedOfSound);
|
||
|
// Construct submodules for early and late reflections.
|
||
|
this.early = new EarlyReflections(context, {
|
||
|
dimensions: options.dimensions,
|
||
|
coefficients: reflectionCoefficients,
|
||
|
speedOfSound: options.speedOfSound,
|
||
|
listenerPosition: options.listenerPosition,
|
||
|
});
|
||
|
this.late = new LateReflections(context, {
|
||
|
durations: durations,
|
||
|
});
|
||
|
this.speedOfSound = options.speedOfSound;
|
||
|
// Construct auxillary audio nodes.
|
||
|
this.output = context.createGain();
|
||
|
this.early.output.connect(this.output);
|
||
|
this._merger = context.createChannelMerger(4);
|
||
|
this.late.output.connect(this._merger, 0, 0);
|
||
|
this._merger.connect(this.output);
|
||
|
}
|
||
|
/**
|
||
|
* Set the room's dimensions and wall materials.
|
||
|
* @param {Utils~RoomDimensions} dimensions Room dimensions (in meters). Defaults to
|
||
|
* {@linkcode Utils.DEFAULT_ROOM_DIMENSIONS DEFAULT_ROOM_DIMENSIONS}.
|
||
|
* @param {Utils~RoomMaterials} materials Named acoustic materials per wall. Defaults to
|
||
|
* {@linkcode Utils.DEFAULT_ROOM_MATERIALS DEFAULT_ROOM_MATERIALS}.
|
||
|
*/
|
||
|
setProperties(dimensions, materials) {
|
||
|
// Compute late response.
|
||
|
let absorptionCoefficients = _getCoefficientsFromMaterials(materials);
|
||
|
let durations = _getDurationsFromProperties(dimensions, absorptionCoefficients, this.speedOfSound);
|
||
|
this.late.setDurations(durations);
|
||
|
// Compute early response.
|
||
|
this.early.speedOfSound = this.speedOfSound;
|
||
|
let reflectionCoefficients = _computeReflectionCoefficients(absorptionCoefficients);
|
||
|
this.early.setRoomProperties(dimensions, reflectionCoefficients);
|
||
|
}
|
||
|
/**
|
||
|
* Set the listener's position (in meters), where origin is the center of
|
||
|
* the room.
|
||
|
* @param {Number} x
|
||
|
* @param {Number} y
|
||
|
* @param {Number} z
|
||
|
*/
|
||
|
setListenerPosition(x, y, z) {
|
||
|
this.early.speedOfSound = this.speedOfSound;
|
||
|
this.early.setListenerPosition(x, y, z);
|
||
|
// Disable room effects if the listener is outside the room boundaries.
|
||
|
let distance = this.getDistanceOutsideRoom(x, y, z);
|
||
|
let gain = 1;
|
||
|
if (distance > Utils.EPSILON_FLOAT) {
|
||
|
gain = 1 - distance / Utils.LISTENER_MAX_OUTSIDE_ROOM_DISTANCE;
|
||
|
// Clamp gain between 0 and 1.
|
||
|
gain = Math.max(0, Math.min(1, gain));
|
||
|
}
|
||
|
this.output.gain.value = gain;
|
||
|
}
|
||
|
/**
|
||
|
* Compute distance outside room of provided position (in meters).
|
||
|
* @param {Number} x
|
||
|
* @param {Number} y
|
||
|
* @param {Number} z
|
||
|
* @return {Number}
|
||
|
* Distance outside room (in meters). Returns 0 if inside room.
|
||
|
*/
|
||
|
getDistanceOutsideRoom(x, y, z) {
|
||
|
let dx = Math.max(0, -this.early._halfDimensions.width - x, x - this.early._halfDimensions.width);
|
||
|
let dy = Math.max(0, -this.early._halfDimensions.height - y, y - this.early._halfDimensions.height);
|
||
|
let dz = Math.max(0, -this.early._halfDimensions.depth - z, z - this.early._halfDimensions.depth);
|
||
|
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||
|
}
|
||
|
}
|
||
|
export default Room;
|