/** * @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 */ '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;