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