Source: framework/components/sound/slot.js

Object.assign(pc, function () {
    'use strict';

    // temporary object for creating
    // instances
    var instanceOptions = {
        volume: 0,
        pitch: 0,
        loop: false,
        startTime: 0,
        duration: 0,
        position: new pc.Vec3(),
        maxDistance: 0,
        refDistance: 0,
        rollOffFactor: 0,
        distanceModel: 0,
        onPlay: null,
        onPause: null,
        onResume: null,
        onStop: null,
        onEnd: null
    };

    /**
     * @constructor
     * @name pc.SoundSlot
     * @classdesc The SoundSlot controls playback of an audio asset.
     * @description Create a new SoundSlot
     * @param {pc.SoundComponent} component The Component that created this slot.
     * @param {String} name The name of the slot.
     * @param {Object} options Settings for the slot
     * @param {Number} [options.volume=1] The playback volume, between 0 and 1.
     * @param {Number} [options.pitch=1] The relative pitch, default of 1, plays at normal pitch.
     * @param {Boolean} [options.loop=false] If true the sound will restart when it reaches the end.
     * @param {Number} [options.startTime=0] The start time from which the sound will start playing.
     * @param {Number} [options.duration=null] The duration of the sound that the slot will play starting from startTime.
     * @param {Boolean} [options.overlap=false] If true then sounds played from slot will be played independently of each other. Otherwise the slot will first stop the current sound before starting the new one.
     * @param {Boolean} [options.autoPlay=false] If true the slot will start playing as soon as its audio asset is loaded.
     * @param {Number} [options.asset=null] The asset id of the audio asset that is going to be played by this slot.
     * @property {String} name The name of the slot
     * @property {String} asset The asset id
     * @property {Boolean} autoPlay If true the slot will begin playing as soon as it is loaded
     * @property {Number} volume The volume modifier to play the sound with. In range 0-1.
     * @property {Number} pitch The pitch modifier to play the sound with. Must be larger than 0.01
     * @property {Number} startTime The start time from which the sound will start playing.
     * @property {Number} duration The duration of the sound that the slot will play starting from startTime.
     * @property {Boolean} loop If true the slot will restart when it finishes playing
     * @property {Boolean} overlap If true then sounds played from slot will be played independently of each other. Otherwise the slot will first stop the current sound before starting the new one.
     * @property {Boolean} isLoaded Returns true if the asset of the slot is loaded.
     * @property {Boolean} isPlaying Returns true if the slot is currently playing.
     * @property {Boolean} isPaused Returns true if the slot is currently paused.
     * @property {Boolean} isStopped Returns true if the slot is currently stopped.
     * @property {pc.SoundInstance[]} instances An array that contains all the {@link pc.SoundInstance}s currently being played by the slot.
     */
    var SoundSlot = function (component, name, options) {
        options = options || {};
        this._component = component;
        this._assets = component.system.app.assets;
        this._manager = component.system.manager;
        this._name = name || 'Untitled';
        this._volume = options.volume !== undefined ? pc.math.clamp(Number(options.volume) || 0, 0, 1) : 1;
        this._pitch = options.pitch !== undefined ? Math.max(0.01, Number(options.pitch) || 0) : 1;
        this._loop = !!(options.loop !== undefined ? options.loop : false);
        this._duration = options.duration > 0 ? options.duration : null;
        this._startTime = Math.max(0, Number(options.startTime) || 0);
        this._overlap = !!(options.overlap);
        this._autoPlay = !!(options.autoPlay);
        this._firstNode = null;
        this._lastNode = null;

        this._asset = options.asset;
        if (this._asset instanceof pc.Asset) {
            this._asset = this._asset.id;
        }

        this._onInstancePlayHandler = this._onInstancePlay.bind(this);
        this._onInstancePauseHandler = this._onInstancePause.bind(this);
        this._onInstanceResumeHandler = this._onInstanceResume.bind(this);
        this._onInstanceStopHandler = this._onInstanceStop.bind(this);
        this._onInstanceEndHandler = this._onInstanceEnd.bind(this);

        this.instances = [];

        pc.events.attach(this);
    };

    Object.assign(SoundSlot.prototype, {
        /**
         * @function pc.SoundSlot#play
         * @description Plays a sound. If {@link pc.SoundSlot#overlap} is true the new sound
         * instance will be played independently of any other instances already playing.
         * Otherwise existing sound instances will stop before playing the new sound.
         * @returns {pc.SoundInstance} The new sound instance
         */
        play: function () {
            // stop if overlap is false
            if (!this.overlap && (this.isPlaying || this.isPaused)) {
                this.stop();
            }

            var instance = this._createInstance();
            this.instances.push(instance);

            // if not loaded then load first
            // and then set sound resource on the created instance
            if (!this.isLoaded) {
                var onLoad = function (sound) {
                    var playWhenLoaded = instance._playWhenLoaded;
                    instance.sound = sound;
                    if (playWhenLoaded) {
                        instance.play();
                    }
                };

                this.off('load', onLoad);
                this.once('load', onLoad);
                this.load();
            } else {
                instance.play();
            }

            return instance;
        },

        /**
         * @function
         * @name pc.SoundSlot#pause
         * @description Pauses all sound instances. To continue playback call {@link pc.SoundSlot#resume}.
         * @returns {Boolean} true if the sound instances paused successfully, false otherwise.
         */
        pause: function () {
            var paused = false;

            var instances = this.instances;
            for (var i = 0, len = instances.length; i < len; i++) {
                if (instances[i].pause()) {
                    paused = true;
                }
            }

            return paused;
        },

        /**
         * @function
         * @name pc.SoundSlot#resume
         * @description Resumes playback of all paused sound instances.
         * @returns {Boolean} True if any instances were resumed.
         */
        resume: function () {
            var resumed = false;
            var instances = this.instances;
            for (var i = 0, len = instances.length; i < len; i++) {
                if (instances[i].resume())
                    resumed = true;
            }

            return resumed;
        },

        /**
         * @function
         * @name pc.SoundSlot#stop
         * @description Stops playback of all sound instances.
         * @returns {Boolean} True if any instances were stopped.
         */
        stop: function () {
            var stopped = false;
            var instances = this.instances;
            for (var i = 0, len = instances.length; i < len; i++) {
                if (instances[i].stop())
                    stopped = true;
            }

            instances.length = 0;

            return stopped;
        },

        /**
         * @function
         * @name pc.SoundSlot#load
         * @description Loads the asset assigned to this slot.
         */
        load: function () {
            if (!this._hasAsset())
                return;

            var asset = this._assets.get(this._asset);
            if (!asset) {
                this._assets.off('add:' + this._asset, this._onAssetAdd, this);
                this._assets.once('add:' + this._asset, this._onAssetAdd, this);
                return;
            }

            asset.off('remove', this._onAssetRemoved, this);
            asset.on('remove', this._onAssetRemoved, this);

            if (!asset.resource) {
                asset.off('load', this._onAssetLoad, this);
                asset.once('load', this._onAssetLoad, this);

                this._assets.load(asset);

                return;
            }

            this.fire('load', asset.resource);
        },

        /**
         * @function
         * @name pc.SoundSlot#setExternalNodes
         * @description Connect external Web Audio API nodes. Any sound played by this slot will
         * automatically attach the specified nodes to the source that plays the sound. You need to pass
         * the first node of the node graph that you created externally and the last node of that graph. The first
         * node will be connected to the audio source and the last node will be connected to the destination of the AudioContext (e.g. speakers).
         * @param {AudioNode} firstNode The first node that will be connected to the audio source of sound instances.
         * @param {AudioNode} [lastNode] The last node that will be connected to the destination of the AudioContext.
         * If unspecified then the firstNode will be connected to the destination instead.
         * @example
         * var context = app.systems.sound.context;
         * var analyzer = context.createAnalyzer();
         * var distortion = context.createWaveShaper();
         * var filter = context.createBiquadFilter();
         * analyzer.connect(distortion);
         * distortion.connect(filter);
         * slot.setExternalNodes(analyzer, filter);
         */
        setExternalNodes: function (firstNode, lastNode) {
            if (!(firstNode)) {
                console.error('The firstNode must have a valid AudioNode');
                return;
            }

            if (!lastNode) {
                lastNode = firstNode;
            }

            this._firstNode = firstNode;
            this._lastNode = lastNode;

            // update instances if not overlapping
            if (!this._overlap) {
                var instances = this.instances;
                for (var i = 0, len = instances.length; i < len; i++) {
                    instances[i].setExternalNodes(firstNode, lastNode);
                }
            }
        },

        /**
         * @function
         * @name pc.SoundSlot#clearExternalNodes
         * @description Clears any external nodes set by {@link pc.SoundSlot#setExternalNodes}.
         */
        clearExternalNodes: function () {
            this._firstNode = null;
            this._lastNode = null;

            // update instances if not overlapping
            if (!this._overlap) {
                var instances = this.instances;
                for (var i = 0, len = instances.length; i < len; i++) {
                    instances[i].clearExternalNodes();
                }
            }
        },

        /**
         * @function
         * @name pc.SoundSlot#getExternalNodes
         * @description Gets an array that contains the two external nodes set by {@link pc.SoundSlot#setExternalNodes}.
         * @returns {AudioNode[]} An array of 2 elements that contains the first and last nodes set by {@link pc.SoundSlot#setExternalNodes}.
         */
        getExternalNodes: function () {
            return [this._firstNode, this._lastNode];
        },

        /**
         * @function
         * @private
         * @name pc.SoundSlot#_hasAsset
         * @returns {Boolean} Returns true if the slot has an asset assigned.
         */
        _hasAsset: function () {
            // != intentional
            return this._asset != null;
        },

        /**
         * @function
         * @private
         * @name pc.SoundSlot#_createInstance
         * @description Creates a new pc.SoundInstance with the properties of the slot.
         * @returns {pc.SoundInstance} The new instance
         */
        _createInstance: function () {
            var instance = null;

            var component = this._component;

            var sound = null;

            // get sound resource
            if (this._hasAsset()) {
                var asset = this._assets.get(this._asset);
                if (asset) {
                    sound = asset.resource;
                }
            }

            // initialize instance options
            var data = instanceOptions;
            data.volume = this._volume * component.volume;
            data.pitch = this._pitch * component.pitch;
            data.loop = this._loop;
            data.startTime = this._startTime;
            data.duration = this._duration;

            data.onPlay = this._onInstancePlayHandler;
            data.onPause = this._onInstancePauseHandler;
            data.onResume = this._onInstanceResumeHandler;
            data.onStop = this._onInstanceStopHandler;
            data.onEnd = this._onInstanceEndHandler;

            if (component.positional) {
                data.position.copy(component.entity.getPosition());
                data.maxDistance = component.maxDistance;
                data.refDistance = component.refDistance;
                data.rollOffFactor = component.rollOffFactor;
                data.distanceModel = component.distanceModel;

                instance = new pc.SoundInstance3d(this._manager, sound, data);
            } else {
                instance = new pc.SoundInstance(this._manager, sound, data);
            }

            // hook external audio nodes
            if (this._firstNode) {
                instance.setExternalNodes(this._firstNode, this._lastNode);
            }

            return instance;
        },

        _onInstancePlay: function (instance) {
            // propagate event to slot
            this.fire('play', instance);

            // propagate event to component
            this._component.fire('play', this, instance);
        },

        _onInstancePause: function (instance) {
            // propagate event to slot
            this.fire('pause', instance);

            // propagate event to component
            this._component.fire('pause', this, instance);
        },

        _onInstanceResume: function (instance) {
            // propagate event to slot
            this.fire('resume', instance);

            // propagate event to component
            this._component.fire('resume', this, instance);
        },

        _onInstanceStop: function (instance) {
            // propagate event to slot
            this.fire('stop', instance);

            // propagate event to component
            this._component.fire('stop', this, instance);
        },

        _onInstanceEnd: function (instance) {
            // remove instance that ended
            var idx = this.instances.indexOf(instance);
            if (idx !== -1) {
                this.instances.splice(idx, 1);
            }

            // propagate event to slot
            this.fire('end', instance);

            // propagate event to component
            this._component.fire('end', this, instance);
        },

        _onAssetAdd: function (asset) {
            this.load();
        },

        _onAssetLoad: function (asset) {
            this.load();
        },

        _onAssetRemoved: function (asset) {
            asset.off('remove', this._onAssetRemoved, this);
            this._assets.off('add:' + asset.id, this._onAssetAdd, this);
            this.stop();
        },

        updatePosition: function (position) {
            var instances = this.instances;
            for (var i = 0, len = instances.length; i < len; i++) {
                instances[i].position = position;
            }
        }
    });

    Object.defineProperty(SoundSlot.prototype, 'name', {
        get: function () {
            return this._name;
        },
        set: function (value) {
            this._name = value;
        }
    });

    Object.defineProperty(SoundSlot.prototype, 'volume', {
        get: function () {
            return this._volume;
        },
        set: function (value) {
            this._volume = pc.math.clamp(Number(value) || 0, 0, 1);

            // update instances if non overlapping
            if (!this._overlap) {
                var instances = this.instances;
                for (var i = 0, len = instances.length; i < len; i++) {
                    instances[i].volume = this._volume * this._component.volume;
                }
            }
        }
    });

    Object.defineProperty(SoundSlot.prototype, 'pitch', {
        get: function () {
            return this._pitch;
        },
        set: function (value) {
            this._pitch = Math.max(Number(value) || 0, 0.01);

            // update instances if non overlapping
            if (!this._overlap) {
                var instances = this.instances;
                for (var i = 0, len = instances.length; i < len; i++) {
                    instances[i].pitch = this.pitch * this._component.pitch;
                }
            }
        }
    });

    Object.defineProperty(SoundSlot.prototype, 'loop', {
        get: function () {
            return this._loop;
        },
        set: function (value) {
            this._loop = !!value;

            // update instances if non overlapping
            var instances = this.instances;
            for (var i = 0, len = instances.length; i < len; i++) {
                instances[i].loop = this._loop;
            }
        }
    });

    Object.defineProperty(SoundSlot.prototype, 'autoPlay', {
        get: function () {
            return this._autoPlay;
        },
        set: function (value) {
            this._autoPlay = !!value;
        }
    });

    Object.defineProperty(SoundSlot.prototype, 'overlap', {
        get: function () {
            return this._overlap;
        },
        set: function (value) {
            this._overlap = !!value;
        }
    });

    Object.defineProperty(SoundSlot.prototype, 'startTime', {
        get: function () {
            return this._startTime;
        },
        set: function (value) {
            this._startTime = Math.max(0, Number(value) || 0);

            // update instances if non overlapping
            if (!this._overlap) {
                var instances = this.instances;
                for (var i = 0, len = instances.length; i < len; i++) {
                    instances[i].startTime = this._startTime;
                }
            }
        }
    });

    Object.defineProperty(SoundSlot.prototype, 'duration', {
        get: function () {
            var assetDuration = 0;
            if (this._hasAsset()) {
                var asset = this._assets.get(this._asset);
                assetDuration = asset.resource ? asset.resource.duration : 0;
            }

            // != intentional
            if (this._duration != null) {
                return this._duration % (assetDuration || 1);
            }
            return assetDuration;
        },
        set: function (value) {
            this._duration = Math.max(0, Number(value) || 0) || null;

            // update instances if non overlapping
            if (!this._overlap) {
                var instances = this.instances;
                for (var i = 0, len = instances.length; i < len; i++) {
                    instances[i].duration = this._duration;
                }
            }
        }
    });

    Object.defineProperty(SoundSlot.prototype, 'asset', {
        get: function () {
            return this._asset;
        },
        set: function (value) {
            var old = this._asset;

            if (old) {
                this._assets.off('add:' + old, this._onAssetAdd, this);
                var oldAsset = this._assets.get(old);
                if (oldAsset) {
                    oldAsset.off('remove', this._onAssetRemoved, this);
                }
            }

            this._asset = value;
            if (this._asset instanceof pc.Asset) {
                this._asset = this._asset.id;
            }

            // load asset if component and entity are enabled
            if (this._hasAsset() && this._component.enabled && this._component.entity.enabled) {
                this.load();
            }
        }
    });

    Object.defineProperty(SoundSlot.prototype, 'isLoaded', {
        get: function () {
            if (this._hasAsset()) {
                var asset = this._assets.get(this._asset);
                if (asset) {
                    return !!asset.resource;
                }
            }

            return false;
        }
    });

    Object.defineProperty(SoundSlot.prototype, 'isPlaying', {
        get: function () {
            var instances = this.instances;
            for (var i = 0, len = instances.length; i < len; i++) {
                if (instances[i].isPlaying)
                    return true;
            }

            return false;
        }
    });

    Object.defineProperty(SoundSlot.prototype, 'isPaused', {
        get: function () {
            var instances = this.instances;
            var len = instances.length;
            if (len === 0)
                return false;

            for (var i = 0; i < len; i++) {
                if (!instances[i].isPaused)
                    return false;
            }

            return true;
        }
    });

    Object.defineProperty(SoundSlot.prototype, 'isStopped', {
        get: function () {
            var instances = this.instances;
            for (var i = 0, len = instances.length; i < len; i++) {
                if (!instances[i].isStopped)
                    return false;
            }

            return true;
        }
    });

    return {
        SoundSlot: SoundSlot
    };

}());


// Events Documentation

/**
 * @event
 * @name pc.SoundSlot#play
 * @description Fired when a sound instance starts playing
 * @param {pc.SoundInstance} instance The instance that started playing
 */

/**
 * @event
 * @name pc.SoundSlot#pause
 * @description Fired when a sound instance is paused.
 * @param {pc.SoundInstance} instance The instance that was paused created to play the sound
 */

/**
 * @event
 * @name pc.SoundSlot#resume
 * @description Fired when a sound instance is resumed..
 * @param {pc.SoundInstance} instance The instance that was resumed
 */

/**
 * @event
 * @name pc.SoundSlot#stop
 * @description Fired when a sound instance is stopped.
 * @param {pc.SoundInstance} instance The instance that was stopped
 */

/**
 * @event
 * @name pc.SoundSlot#load
 * @description Fired when the asset assigned to the slot is loaded
 * @param {pc.Sound} sound The sound resource that was loaded
 */