Source: media/sound/Sound.js

//------------------------------------------------------------------------------
// Constructor scope
//------------------------------------------------------------------------------

/**
 * Creates a new Sound object.
 *
 * @constructor
 * @package
 *
 * @param {rune.media.SoundChannel} channel The channel that "owns" the object.
 * @param {string} resource The name of the audio resource to be used.
 * @param {boolean} [unique=false] Whether the object should be unique or shared.
 *
 * @class
 * @classdesc
 * 
 * The Sound class represents a sound that can be played in an audio channel. 
 * The class offers an interface for playing a specific sound. It is the sound 
 * channel that does the actual playback.
 */
rune.media.Sound = function(channel, resource, unique) {

    //--------------------------------------------------------------------------
    // Private properties
    //--------------------------------------------------------------------------
    
    /**
     * Name of the resource file used as the audio source.
     *
     * @type {string}
     * @private
     */
    this.m_resource = resource;
    
    /**
     * The audio channel object that created the Sound object.
     *
     * @type {rune.media.SoundChannel}
     * @private
     */
    this.m_channel = channel;
    
    /**
     * Playback speed.
     *
     * @type {number}
     * @private
     */
    this.m_rate = 1.0;
    
    /**
     * Sound source.
     *
     * @type {MediaElementAudioSourceNode}
     * @private
     */
    this.m_source = null;
    
    /**
     * Stereo panner node.
     *
     * @type {StereoPannerNode}
     * @private
     */
    this.m_stereoPanner = null;
    
    /**
     * Whether the object is unique or shared.
     *
     * @type {boolean}
     * @private
     */
    this.m_unique = Boolean(unique);
    
    /**
     * Tween system used for volume management.
     *
     * @type {rune.tween.Tweens}
     * @private
     */
    this.m_tweens = null;

    //--------------------------------------------------------------------------
    // Constructor call
    //--------------------------------------------------------------------------

    /**
     * Invokes secondary class constructor.
     */
    this.m_construct();
};

//------------------------------------------------------------------------------
// Public prototype getter and setter methods
//------------------------------------------------------------------------------

/**
 * Refers to the audio channel that created the audio object and thus the 
 * object is bound to.
 *
 * @member {rune.media.SoundChannel} channel
 * @memberof rune.media.Sound
 * @instance
 * @readonly
 */
Object.defineProperty(rune.media.Sound.prototype, "channel", {
    /**
     * @this rune.media.Sound
     * @ignore
     */
    get : function() {
        return this.m_channel;
    }
});

/**
 * A boolean value which is true if the media contained in the element has 
 * finished playing.
 *
 * @member {boolean} ended
 * @memberof rune.media.Sound
 * @instance
 * @readonly
 */
Object.defineProperty(rune.media.Sound.prototype, "ended", {
    /**
     * @this rune.media.Sound
     * @ignore
     */
    get : function() {
        return this.m_source['mediaElement'].ended;
    }
});

/**
 * Whether the Sound object should start over when it reaches the end. This can 
 * for example be useful for background music.
 *
 * @member {boolean} loop
 * @memberof rune.media.Sound
 * @instance
 */
Object.defineProperty(rune.media.Sound.prototype, "loop", {
    /**
     * @this rune.media.Sound
     * @ignore
     */
    get : function() {
        return this.m_source['mediaElement'].loop;
    },
    
    /**
     * @this rune.media.Sound
     * @ignore
     */
    set : function(value) {
        this.m_source['mediaElement'].loop = value;
    }
});

/**
 * The name of the resource file used as the audio source for the Sound object.
 *
 * @member {string} name
 * @memberof rune.media.Sound
 * @instance
 * @readonly
 */
Object.defineProperty(rune.media.Sound.prototype, "name", {
    /**
     * @this rune.media.Sound
     * @ignore
     */
    get : function() {
        return this.m_resource;
    }
});

/**
 * Pan represented by a floating point number between -1 (left) and 1 (right). 
 * The default value is 0 and distributes the sound evenly between the left 
 * and right speakers.
 *
 * @member {number} pan
 * @memberof rune.media.Sound
 * @instance
 */
Object.defineProperty(rune.media.Sound.prototype, "pan", {
    /**
     * @this rune.media.Sound
     * @ignore
     */
    get : function() {
        return this.m_stereoPanner['pan'].value;
    },
    
    /**
     * @this rune.media.Sound
     * @ignore
     */
    set : function(value) {
        this.m_stereoPanner['pan'].value = rune.util.Math.clamp(value, -1.0, 1.0);
    }
});

/**
 * Whether the sound is paused (true) or not (false).
 *
 * @member {boolean} paused
 * @memberof rune.media.Sound
 * @instance
 * @readonly
 */
Object.defineProperty(rune.media.Sound.prototype, "paused", {
    /**
     * @this rune.media.Sound
     * @ignore
     */
    get : function() {
        return this.m_source['mediaElement'].paused;
    }
});

/**
 * The object's true playback speed, ie the speed relative to the playback 
 * speed of the audio channel to which the object is connected.
 *
 * @member {number} playbackRate
 * @memberof rune.media.Sound
 * @instance
 * @readonly
 */
Object.defineProperty(rune.media.Sound.prototype, "playbackRate", {
    /**
     * @this rune.media.Sound
     * @ignore
     */
    get : function() {
        return this.m_source['mediaElement'].playbackRate;
    }
});

/**
 * Whether or not the browser should adjust the pitch of the audio to 
 * compensate for changes to the playback rate.
 *
 * @member {boolean} preservesPitch
 * @memberof rune.media.Sound
 * @instance
 * @default true
 */
Object.defineProperty(rune.media.Sound.prototype, "preservesPitch", {
    /**
     * @this rune.media.Sound
     * @ignore
     */
    get : function() {
        return this.m_source['mediaElement'].preservesPitch;
    },
    
    /**
     * @this rune.media.Sound
     * @ignore
     */
    set : function(value) {
        this.m_source['mediaElement'].preservesPitch = value;
    }
});

/**
 * Sets the rate at which the media is being played back. This is used to 
 * implement user controls for fast forward, slow motion, and so forth. The 
 * normal playback rate is multiplied by this value to obtain the current 
 * rate, so a value of 1.0 indicates normal speed.
 *
 * @member {number} rate
 * @memberof rune.media.Sound
 * @instance
 * @default 1.0
 */
Object.defineProperty(rune.media.Sound.prototype, "rate", {
    /**
     * @this rune.media.Sound
     * @ignore
     */
    get : function() {
        return this.m_rate;
    },
    
    /**
     * @this rune.media.Sound
     * @ignore
     */
    set : function(value) {
        this.m_rate = value;
        //this.m_source['mediaElement'].playbackRate = this.m_rate * this.m_channel['rate'];
    }
});

/**
 * A double-precision floating-point value indicating the current playback 
 * time in seconds; if the media has not started to play and has not been 
 * seeked, this value is the media's initial playback time. Setting this value 
 * seeks the media to the new time. The time is specified relative to the 
 * media's timeline.
 *
 * @member {number} time
 * @memberof rune.media.Sound
 * @instance
 */
Object.defineProperty(rune.media.Sound.prototype, "time", {
    /**
     * @this rune.media.Sound
     * @ignore
     */
    get : function() {
        return this.m_source['mediaElement'].currentTime;
    },
    
    /**
     * @this rune.media.Sound
     * @ignore
     */
    set : function(value) {
        this.m_source['mediaElement'].currentTime = value;
    }
});

/**
 * Sound The object's sound volume. Volume is given as a floating point number 
 * between 0 (0%) and 1 (100%).
 *
 * @member {number} volume
 * @memberof rune.media.Sound
 * @instance
 */
Object.defineProperty(rune.media.Sound.prototype, "volume", {
    /**
     * @this rune.media.Sound
     * @ignore
     */
    get : function() {
        return this.m_source['mediaElement'].volume;
    },
    
    /**
     * @this rune.media.Sound
     * @ignore
     */
    set : function(value) {
        this.m_source['mediaElement'].volume = rune.util.Math.clamp(value, 0.0, 1.0);
    }
});

/**
 * Whether the Sound object is unique (true) or shared (false).
 *
 * @member {boolean} unique
 * @memberof rune.media.Sound
 * @instance
 * @readonly
 */
Object.defineProperty(rune.media.Sound.prototype, "unique", {
    /**
     * @this rune.media.Sound
     * @ignore
     */
    get : function() {
        return this.m_unique;
    }
});

//------------------------------------------------------------------------------
// Public prototype methods (API)
//------------------------------------------------------------------------------

/**
 * Adjust the volume of the audio channel to a certain level.
 *
 * @param {number} to Target volume.
 * @param {number} duration Fade duration.
 *
 * @returns {undefined}
 */
rune.media.Sound.prototype.fade = function(to, duration) {
    this.m_tweens.clear();
    this.m_tweens.create({
        target: this,
        transition: rune.tween.Sine.easeIn,
        duration: duration || 2500,
        args: {
            volume: to || 0
        },
    });
};

/**
 * Pause audio playback.
 *
 * @returns {undefined}
 */
rune.media.Sound.prototype.pause = function() {
    this.m_source['mediaElement'].pause();
};

/**
 * Start audio playback.
 *
 * @param {boolean} [restart=false] Restarts playback (if it is playing).
 *
 * @returns {undefined}
 */
rune.media.Sound.prototype.play = function(restart) {
    if (restart == true) {
        this.m_source['mediaElement'].currentTime = 0;
    }

    this.m_source['mediaElement'].play();
};

/**
 * Resumes current audio playback.
 *
 * @returns {undefined}
 */
rune.media.Sound.prototype.resume = function() {
    this.play(false);
};

/**
 * Stops current audio playback.
 *
 * @returns {undefined}
 */
rune.media.Sound.prototype.stop = function() {
    this.m_source['mediaElement'].pause();
    this.m_source['mediaElement'].currentTime = 0;
};

//------------------------------------------------------------------------------
// Public prototype methods (ENGINE)
//------------------------------------------------------------------------------

/**
 * Destroys the audio object and frees allocated memory. The method can be used 
 * to remove and destroy Sound objects, but the recommended way to remove 
 * (and destroy) a Sound object is via the remove method of the sound channel 
 * on which the object was created.
 *
 * @returns {undefined}
 */
rune.media.Sound.prototype.dispose = function() {
    this.m_disposeStereoPanner();
    this.m_disposeSource();
    this.m_disposeChannel();
};

//------------------------------------------------------------------------------
// Internal prototype methods
//------------------------------------------------------------------------------

/**
 * Connects the Sound object to an sound channel.
 *
 * @param {AudioNode} node Audio node to connect to.
 *
 * @returns {undefined}
 * @package
 * @ignore
 * @suppress {checkTypes}
 */
rune.media.Sound.prototype.connect = function(node) {
    this.m_stereoPanner.connect(node);
    this['rate'] = 1.0;
};

/**
 * Updates the Sound object.
 *
 * @param {number} step Current time step.
 *
 * @returns {undefined}
 * @package
 * @ignore
 */
rune.media.Sound.prototype.update = function(step) {
    this.m_updateRate(step);
    this.m_updateTweens(step);
};

//------------------------------------------------------------------------------
// Protected prototype methods
//------------------------------------------------------------------------------

/**
 * The class constructor.
 *
 * @returns {undefined}
 * @protected
 * @ignore
 */
rune.media.Sound.prototype.m_construct = function() {
    this.m_constructSource();
    this.m_constructStereoPanner();
    this.m_constructTweens();
};

//------------------------------------------------------------------------------
// Private prototype methods
//------------------------------------------------------------------------------

/**
 * Creates an audio source based on the resource used to represent the sound 
 * object.
 *
 * @returns {undefined}
 * @private
 */
rune.media.Sound.prototype.m_constructSource = function() {
    this.m_disposeSource();
    if (this.m_source == null && this.m_channel != null) {
        var resource = rune.system.Application['instance']['resources'].get(this.m_resource);
        this.m_source = this.m_channel['context'].createMediaElementSource(resource['data'].cloneNode());
    } else throw new Error();
};

/**
 * Creates the stereo panner node.
 *
 * @returns {undefined}
 * @private
 */
rune.media.Sound.prototype.m_constructStereoPanner = function() {
    this.m_disposeStereoPanner();
    if (this.m_stereoPanner == null && this.m_channel != null) {
        this.m_stereoPanner = this.m_channel['context'].createStereoPanner();
        this.m_source.connect(this.m_stereoPanner);
    } else throw new Error();
};

/**
 * Creates the Tween system.
 *
 * @returns {undefined}
 * @private
 */
rune.media.Sound.prototype.m_constructTweens = function() {
    this.m_disposeTweens();
    if (this.m_tweens == null) {
        this.m_tweens = new rune.tween.Tweens();
    } else throw new Error();
};

/**
 * Calculates and updates the playback speed (rate) based on the speed of the 
 * audio object and audio channel.
 *
 * @param {number} step Current time step.
 *
 * @returns {undefined}
 * @private
 */
rune.media.Sound.prototype.m_updateRate = function(step) {
    if (this.m_source && this.m_channel) {
        this.m_source['mediaElement'].playbackRate = this.m_rate * this.m_channel['rate'];
    }
};

/**
 * Updates the Tween system.
 *
 * @param {number} step Current time step.
 *
 * @returns {undefined}
 * @private
 */
rune.media.Sound.prototype.m_updateTweens = function(step) {
    if (this.m_tweens) {
        this.m_tweens.update(step);
    }
};

/**
 * Removes the Tween system.
 *
 * @returns {undefined}
 * @private
 */
rune.media.Sound.prototype.m_disposeTweens = function() {
    if (this.m_tweens instanceof rune.tween.Tweens) {
        this.m_tweens.dispose();
        this.m_tweens = null;
    }
};

/**
 * Disconnects the object from the audio channel.
 *
 * @returns {undefined}
 * @private
 */
rune.media.Sound.prototype.m_disposeChannel = function() {
    if (this.m_channel != null) {
        this.m_channel.remove(this, false);
        this.m_channel = null;   
    }
};

/**
 * Removes the stereo panner node.
 *
 * @returns {undefined}
 * @private
 */
rune.media.Sound.prototype.m_disposeStereoPanner = function() {
    if (this.m_stereoPanner instanceof StereoPannerNode) {
        this.m_stereoPanner.disconnect();
        this.m_stereoPanner = null;
    }
};

/**
 * Removes the audio source.
 *
 * @returns {undefined}
 * @private
 */
rune.media.Sound.prototype.m_disposeSource = function() {
    if (this.m_source != null) {
        this.stop();
        this.m_source.disconnect();
        this.m_source  = null;
    }
};