Source: framework/components/sound/system.js

Object.assign(pc, function () {
    var _schema = [
        'enabled',
        'volume',
        'pitch',
        'positional',
        'refDistance',
        'maxDistance',
        'rollOffFactor',
        'distanceModel',
        'slots'
    ];

    /**
     * @constructor
     * @name pc.SoundComponentSystem
     * @classdesc Manages creation of {@link pc.SoundComponent}s.
     * @description Create a SoundComponentSystem
     * @param {pc.Application} app The Application
     * @param {pc.SoundManager} manager The sound manager
     * @property {Number} volume Sets / gets the volume for the entire Sound system. All sounds will have their volume
     * multiplied by this value. Valid between [0, 1].
     * @property {AudioContext} context Gets the AudioContext currently used by the sound manager. Requires Web Audio API support.
     * @property {pc.SoundManager} manager Gets / sets the sound manager
     * @extends pc.ComponentSystem
     */
    var SoundComponentSystem = function (app, manager) {
        pc.ComponentSystem.call(this, app);

        this.id = "sound";
        this.description = "Allows an Entity to play sounds";

        this.ComponentType = pc.SoundComponent;
        this.DataType = pc.SoundComponentData;

        this.schema = _schema;

        this.manager = manager;

        pc.ComponentSystem.bind('update', this.onUpdate, this);

        this.on('beforeremove', this.onBeforeRemove, this);
    };
    SoundComponentSystem.prototype = Object.create(pc.ComponentSystem.prototype);
    SoundComponentSystem.prototype.constructor = SoundComponentSystem;

    pc.Component._buildAccessors(pc.SoundComponent.prototype, _schema);

    Object.assign(SoundComponentSystem.prototype, {
        initializeComponentData: function (component, data, properties) {
            properties = ['volume', 'pitch', 'positional', 'refDistance', 'maxDistance', 'rollOffFactor', 'distanceModel', 'slots', 'enabled'];
            pc.ComponentSystem.prototype.initializeComponentData.call(this, component, data, properties);
        },

        cloneComponent: function (entity, clone) {
            var key;
            var oldData = entity.sound.data;
            var newData = {};

            // copy old data to new data
            for (key in oldData) {
                if (oldData.hasOwnProperty(key)) {
                    newData[key] = oldData[key];
                }
            }

            // convert 'slots' back to
            // simple option objects
            newData.slots = {};

            for (key in oldData.slots) {
                var oldSlot = oldData.slots[key];
                if (oldSlot instanceof pc.SoundSlot) {
                    newData.slots[key] = {
                        name: oldSlot.name,
                        volume: oldSlot.volume,
                        pitch: oldSlot.pitch,
                        loop: oldSlot.loop,
                        duration: oldSlot.duration,
                        startTime: oldSlot.startTime,
                        overlap: oldSlot.overlap,
                        autoPlay: oldSlot.autoPlay,
                        asset: oldSlot.asset
                    };
                } else {
                    newData.slots[key] = oldSlot;
                }
            }

            // reset playingBeforeDisable
            newData.playingBeforeDisable = {};

            // add component with new data
            return this.addComponent(clone, newData);
        },

        onUpdate: function (dt) {
            var store = this.store;

            for (var id in store) {
                if (store.hasOwnProperty(id)) {
                    var item = store[id];
                    var entity = item.entity;
                    var componentData = item.data;

                    // Update slot position if this is a 3d sound
                    if (componentData.enabled && entity.enabled && componentData.positional) {
                        var position = entity.getPosition();
                        var slots = componentData.slots;
                        for (var key in slots) {
                            slots[key].updatePosition(position);
                        }
                    }
                }
            }
        },

        onBeforeRemove: function (entity, component) {
            var slots = component.slots;
            // stop non overlapping sounds
            for (var key in slots) {
                if (!slots[key].overlap) {
                    slots[key].stop();
                }
            }

            component.onRemove();
        }
    });

    Object.defineProperty(SoundComponentSystem.prototype, 'volume', {
        get: function () {
            return this.manager.volume;
        },
        set: function (volume) {
            this.manager.volume = volume;
        }
    });

    Object.defineProperty(SoundComponentSystem.prototype, 'context', {
        get: function () {
            if (!pc.SoundManager.hasAudioContext()) {
                console.warn('WARNING: Audio context is not supported on this browser');
                return null;
            }

            return this.manager.context;
        }
    });

    return {
        SoundComponentSystem: SoundComponentSystem
    };
}());