/*
* WebAudioSoundInstance
* Visit http://createjs.com/ for documentation, updates and examples.
*
*
* Copyright (c) 2012 gskinner.com, inc.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
/**
* @module SoundJS
*/
// namespace:
this.createjs = this.createjs || {};
/**
* WebAudioSoundInstance extends the base api of {{#crossLink "AbstractSoundInstance"}}{{/crossLink}} and is used by
* {{#crossLink "WebAudioPlugin"}}{{/crossLink}}.
*
* WebAudioSoundInstance exposes audioNodes for advanced users.
*
* @param {String} src The path to and file name of the sound.
* @param {Number} startTime Audio sprite property used to apply an offset, in milliseconds.
* @param {Number} duration Audio sprite property used to set the time the clip plays for, in milliseconds.
* @param {Object} playbackResource Any resource needed by plugin to support audio playback.
* @class WebAudioSoundInstance
* @extends AbstractSoundInstance
* @constructor
*/
(function () {
"use strict";
function WebAudioSoundInstance(src, startTime, duration, playbackResource) {
this.AbstractSoundInstance_constructor(src, startTime, duration, playbackResource);
// public properties
/**
* NOTE this is only intended for use by advanced users.
* <br />GainNode for controlling <code>WebAudioSoundInstance</code> volume. Connected to the {{#crossLink "WebAudioSoundInstance/destinationNode:property"}}{{/crossLink}}.
* @property gainNode
* @type {AudioGainNode}
* @since 0.4.0
*
*/
this.gainNode = s.context.createGain();
/**
* NOTE this is only intended for use by advanced users.
* <br />A panNode allowing left and right audio channel panning only. Connected to WebAudioSoundInstance {{#crossLink "WebAudioSoundInstance/gainNode:property"}}{{/crossLink}}.
* @property panNode
* @type {AudioPannerNode}
* @since 0.4.0
*/
this.panNode = s.context.createPanner();
this.panNode.panningModel = s._panningModel;
this.panNode.connect(this.gainNode);
/**
* NOTE this is only intended for use by advanced users.
* <br />sourceNode is the audio source. Connected to WebAudioSoundInstance {{#crossLink "WebAudioSoundInstance/panNode:property"}}{{/crossLink}}.
* @property sourceNode
* @type {AudioNode}
* @since 0.4.0
*
*/
this.sourceNode = null;
// private properties
/**
* Timeout that is created internally to handle sound playing to completion.
* Stored so we can remove it when stop, pause, or cleanup are called
* @property _soundCompleteTimeout
* @type {timeoutVariable}
* @default null
* @protected
* @since 0.4.0
*/
this._soundCompleteTimeout = null;
/**
* NOTE this is only intended for use by very advanced users.
* _sourceNodeNext is the audio source for the next loop, inserted in a look ahead approach to allow for smooth
* looping. Connected to {{#crossLink "WebAudioSoundInstance/gainNode:property"}}{{/crossLink}}.
* @property _sourceNodeNext
* @type {AudioNode}
* @default null
* @protected
* @since 0.4.1
*
*/
this._sourceNodeNext = null;
/**
* Time audio started playback, in seconds. Used to handle set position, get position, and resuming from paused.
* @property _playbackStartTime
* @type {Number}
* @default 0
* @protected
* @since 0.4.0
*/
this._playbackStartTime = 0;
// Proxies, make removing listeners easier.
this._endedHandler = createjs.proxy(this._handleSoundComplete, this);
};
var p = createjs.extend(WebAudioSoundInstance, createjs.AbstractSoundInstance);
var s = WebAudioSoundInstance;
// TODO: deprecated
// p.initialize = function() {}; // searchable for devs wondering where it is. REMOVED. See docs for details.
/**
* Note this is only intended for use by advanced users.
* <br />Audio context used to create nodes. This is and needs to be the same context used by {{#crossLink "WebAudioPlugin"}}{{/crossLink}}.
* @property context
* @type {AudioContext}
* @static
* @since 0.6.0
*/
s.context = null;
/**
* Note this is only intended for use by advanced users.
* <br /> Audio node from WebAudioPlugin that sequences to <code>context.destination</code>
* @property destinationNode
* @type {AudioNode}
* @static
* @since 0.6.0
*/
s.destinationNode = null;
/**
* Value to set panning model to equal power for WebAudioSoundInstance. Can be "equalpower" or 0 depending on browser implementation.
* @property _panningModel
* @type {Number / String}
* @protected
* @static
* @since 0.6.0
*/
s._panningModel = "equalpower";
// Public methods
p.destroy = function() {
this.AbstractSoundInstance_destroy();
this.panNode.disconnect(0);
this.panNode = null;
this.gainNode.disconnect(0);
this.gainNode = null;
};
p.toString = function () {
return "[WebAudioSoundInstance]";
};
// Private Methods
p._updatePan = function() {
this.panNode.setPosition(this._pan, 0, -0.5);
// z need to be -0.5 otherwise the sound only plays in left, right, or center
};
p._removeLooping = function(value) {
this._sourceNodeNext = this._cleanUpAudioNode(this._sourceNodeNext);
};
p._addLooping = function(value) {
if (this.playState != createjs.Sound.PLAY_SUCCEEDED) { return; }
this._sourceNodeNext = this._createAndPlayAudioNode(this._playbackStartTime, 0);
};
p._setDurationFromSource = function () {
this._duration = this.playbackResource.duration * 1000;
};
p._handleCleanUp = function () {
if (this.sourceNode && this.playState == createjs.Sound.PLAY_SUCCEEDED) {
this.sourceNode = this._cleanUpAudioNode(this.sourceNode);
this._sourceNodeNext = this._cleanUpAudioNode(this._sourceNodeNext);
}
if (this.gainNode.numberOfOutputs != 0) {this.gainNode.disconnect(0);}
// OJR there appears to be a bug that this doesn't always work in webkit (Chrome and Safari). According to the documentation, this should work.
clearTimeout(this._soundCompleteTimeout);
this._playbackStartTime = 0; // This is used by getPosition
};
/**
* Turn off and disconnect an audioNode, then set reference to null to release it for garbage collection
* @method _cleanUpAudioNode
* @param audioNode
* @return {audioNode}
* @protected
* @since 0.4.1
*/
p._cleanUpAudioNode = function(audioNode) {
if(audioNode) {
audioNode.stop(0);
audioNode.disconnect(0);
audioNode = null;
}
return audioNode;
};
p._handleSoundReady = function (event) {
this.gainNode.connect(s.destinationNode); // this line can cause a memory leak. Nodes need to be disconnected from the audioDestination or any sequence that leads to it.
var dur = this._duration * 0.001;
var pos = this._position * 0.001;
if (pos > dur) {pos = dur;}
this.sourceNode = this._createAndPlayAudioNode((s.context.currentTime - dur), pos);
this._playbackStartTime = this.sourceNode.startTime - pos;
this._soundCompleteTimeout = setTimeout(this._endedHandler, (dur - pos) * 1000);
if(this._loop != 0) {
this._sourceNodeNext = this._createAndPlayAudioNode(this._playbackStartTime, 0);
}
};
/**
* Creates an audio node using the current src and context, connects it to the gain node, and starts playback.
* @method _createAndPlayAudioNode
* @param {Number} startTime The time to add this to the web audio context, in seconds.
* @param {Number} offset The amount of time into the src audio to start playback, in seconds.
* @return {audioNode}
* @protected
* @since 0.4.1
*/
p._createAndPlayAudioNode = function(startTime, offset) {
var audioNode = s.context.createBufferSource();
audioNode.buffer = this.playbackResource;
audioNode.connect(this.panNode);
var dur = this._duration * 0.001;
audioNode.startTime = startTime + dur;
audioNode.start(audioNode.startTime, offset+(this._startTime*0.001), dur - offset);
return audioNode;
};
p._pause = function () {
this._position = (s.context.currentTime - this._playbackStartTime) * 1000; // * 1000 to give milliseconds, lets us restart at same point
this.sourceNode = this._cleanUpAudioNode(this.sourceNode);
this._sourceNodeNext = this._cleanUpAudioNode(this._sourceNodeNext);
if (this.gainNode.numberOfOutputs != 0) {this.gainNode.disconnect(0);}
clearTimeout(this._soundCompleteTimeout);
};
p._resume = function () {
this._handleSoundReady();
};
/*
p._handleStop = function () {
// web audio does not need to do anything extra
};
*/
p._updateVolume = function () {
var newVolume = this._muted ? 0 : this._volume;
if (newVolume != this.gainNode.gain.value) {
this.gainNode.gain.value = newVolume;
}
};
p._calculateCurrentPosition = function () {
return ((s.context.currentTime - this._playbackStartTime) * 1000); // pos in seconds * 1000 to give milliseconds
};
p._updatePosition = function () {
this.sourceNode = this._cleanUpAudioNode(this.sourceNode);
this._sourceNodeNext = this._cleanUpAudioNode(this._sourceNodeNext);
clearTimeout(this._soundCompleteTimeout);
if (!this._paused) {this._handleSoundReady();}
};
// OJR we are using a look ahead approach to ensure smooth looping.
// We add _sourceNodeNext to the audio context so that it starts playing even if this callback is delayed.
// This technique is described here: http://www.html5rocks.com/en/tutorials/audio/scheduling/
// NOTE the cost of this is that our audio loop may not always match the loop event timing precisely.
p._handleLoop = function () {
this._cleanUpAudioNode(this.sourceNode);
this.sourceNode = this._sourceNodeNext;
this._playbackStartTime = this.sourceNode.startTime;
this._sourceNodeNext = this._createAndPlayAudioNode(this._playbackStartTime, 0);
this._soundCompleteTimeout = setTimeout(this._endedHandler, this._duration);
};
p._updateDuration = function () {
if(this.playState == createjs.Sound.PLAY_SUCCEEDED) {
this._pause();
this._resume();
}
};
createjs.WebAudioSoundInstance = createjs.promote(WebAudioSoundInstance, "AbstractSoundInstance");
}());