Repository

js/Mobilizing/renderer/audio/Renderer.js

import Component from '../../core/Component';

//Used for singletonize this class instance (avoid mutilple rendrer)
let instance = null;

/**
* Renderer is a Web Audio API based renderer for playing sound.
*/
export default class Renderer extends Component {
    /**
    * @example
    *     //to do
    * @constructor
    * @extends Component
    * @param {Object} params Parameters object, given by the constructor.
    * @param {Boolean} [params.listenerAutoUpdateRotation=true] Set the update of the forward and up vector of this audio listener to be automatic 
    */
    constructor({
        listenerAutoUpdateRotation = true
    } = {}) {
        //if an instance already exists return it
        if (instance) {
            console.warn("an instance of audio Renderer already exists!");
            return instance;
        }

        super(...arguments);

        instance = this;

        this.listenerAutoUpdateRotation = listenerAutoUpdateRotation;
        this.audioContext = undefined;

        //Get the main audio context of Web Audio
        if (window.AudioContext !== undefined) {
            this.audioContext = new window.AudioContext();
        }
        else if (window.webkitAudioContext !== undefined) {
            this.audioContext = new window.webkitAudioContext();
        }

        //hack to reset the SR
        if (this.audioContext) {

            if (this.audioContext.sampleRate !== 44100) {
                const buffer = this.audioContext.createBuffer(1, 1, 44100);
                const dummy = this.audioContext.createBufferSource();
                dummy.buffer = buffer;
                dummy.connect(this.audioContext.destination);
                dummy.start(0);
                dummy.disconnect();
                this.audioContext.close();

                //Get the main audio context of Web Audio
                if (window.AudioContext !== undefined) {
                        this.audioContext = new window.AudioContext();
                }
                else if (window.webkitAudioContext !== undefined) {
                        this.audioContext = new window.webkitAudioContext();
                }
            }

            this.masterGain = this.audioContext.createGain();
            this.masterGain.connect(this.audioContext.destination);
        }

        this.listener = this.audioContext.listener;

        //adjust the strategy to what the browser supports
        if (this.listener.forwardX) {
            this.listener.forwardX.value = 0;
            this.listener.forwardY.value = 0;
            this.listener.forwardZ.value = -1;
            this.listener.upX.value = 0;
            this.listener.upY.value = 1;
            this.listener.upZ.value = 0;
        }
        else {
            this.listener.setOrientation(0, 0, -1, 0, 1, 0);
        }

        //keep an array of connected sources for easy duplication for offlineContext
        this.sources = [];
    }

    /**
     * Maintains an array of all the sources attached to this renderer
     * @param {Source} source 
     */
    registerNode(source) {
        this.sources.push(source);
    }

    /**
     * @return {Number} the current time of this audio renderer
     */
    getCurrentTime() {
        return this.audioContext.currentTime;
    }

    /**
     * Set this audioContext's gain volume, use a 1 sec linear ramp to avoid clicks
     * @param {Number} val 
     */
    setMasterGain(val) {
        let value = val;
        if (val === 0) {
            value = 0.001;
        }
        //this.masterGain.gain.value = val;
        this.masterGain.gain.linearRampToValueAtTime(value, this.getCurrentTime() + 1);
    }

    /**
    * Get this audioContext's gain volume
    * @param {Number} val 
    */
    getMasterGain() {
        return this.masterGain.gain.value;
    }

    /**
    * creates a webaudioAPI analyzer to extract frequencies from the audio source used in this renderer
    * @param {Number} fftSize this size of this fft in memory
    * @param {Number} smoothingTimeConstant
    */
    createAnalyzer(fftSize = 256, smoothingTimeConstant = 0.2) {
        /**
        * @property analyser
        * audio analyser
        */
        this.analyser = this.audioContext.createAnalyser();

        const _fftSize = fftSize;
        this.analyser.fftSize = _fftSize;
        this.analyser.smoothingTimeConstant = smoothingTimeConstant;

        const bufferSize = this.analyser.frequencyBinCount;

        /**
        * @property analyserArray
        * Uint8Array for accessing frequencies
        */
        this.analyserTimeArray = new Uint8Array(bufferSize);
        this.analyserFrequencyArray = new Uint8Array(bufferSize);

        this.analyserFloatTimeArray = new Float32Array(bufferSize);
        this.analyserFloatFrequencyArray = new Float32Array(bufferSize);
        //this.masterGain.connect(this.analyser);
    }

    beep(frequency) {
        const freq = (frequency) ? frequency : 440;
        const osc = this.audioContext.createOscillator();
        osc.connect(this.masterGain);
        osc.frequency.value = freq;
        osc.start(0);
        osc.stop(this.audioContext.currentTime + 0.2);
    }

    /**
     * Attach a Transform object, usually coming from a 3D graphical object, to this renderer listener (the "ears"). The listener position and orientation (rotation) will be automatically updated against the given Transform.
     * @param {Transform} transform
     */
    setListenerTransform(transform) {
        this.listener.transform = transform;
    }

    /**
     * Set the update of the forward and up vector of this audio listener to be automatic (computed from the given 3D object transform) or not (will stay at (0, 0, -1, 0, 1, 0))
     * @param {Boolean} value 
     */
    setListenerAutoUpdateRotation(value) {
        this.listenerAutoUpdateRotation = value;
    }

    /**
     * Set the position of the listner in 3D space ONLY when transform from a 3D object has not been attached to the listener (cf setListenerTransform())
     * @param {Object|Vector3} pos An object containing x,y,z coordinates in space (can be a Vector3 from the 3D renderer)
     */
    setListenerPosition(pos) {
        this.listener.setPosition(pos.x, pos.y, pos.z);
    }

    /**
     * Set the listener orientation through the given Mobilizing/Three.js quaternion. Directions (i.e forwardVector and upVector) are extracted by the Quaternion Class.
     * @param {Quaternion} quaternion Mobilizing/Three.js quaternion to use for this listener orientation
     */
    setListenerQuaternion(quaternion) {
        const directions = quaternion.getDirections();

        if (this.listener.forwardX) {
            this.listener.forwardX.value = directions.forwardVector.x;
            this.listener.forwardY.value = directions.forwardVector.y;
            this.listener.forwardZ.value = directions.forwardVector.z;
            this.listener.upX.value = directions.upVector.x;
            this.listener.upY.value = directions.upVector.y;
            this.listener.upZ.value = directions.upVector.z;
        }
        else {
            this.listener.setOrientation(
                directions.forwardVector.x,
                directions.forwardVector.y,
                directions.forwardVector.z,
                directions.upVector.x,
                directions.upVector.y,
                directions.upVector.z);
        }
    }

    update() {
        super.update();

        if (this.listener.transform) {
            const pos = this.listener.transform.getLocalPosition();
            this.listener.setPosition(pos.x, pos.y, pos.z);

            const quaternion = this.listener.transform.getLocalQuaternion();
            if (this.listenerAutoUpdateRotation) {
                this.setListenerQuaternion(quaternion);
            }
        }
    }
}