/* * 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); this._updatePan(); /** * 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; /** * 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 />The scratch buffer that will be assigned to the buffer property of a source node on close. * This is and should be the same scratch buffer referenced by {{#crossLink "WebAudioPlugin"}}{{/crossLink}}. * @property _scratchBuffer * @type {AudioBufferSourceNode} * @static */ s._scratchBuffer = 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); // necessary to prevent leak on iOS Safari 7-9. will throw in almost all other // browser implementations. if ( createjs.BrowserDetect.isIOS ) { try { audioNode.buffer = s._scratchBuffer; } catch(e) {} } 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, pos = Math.min(Math.max(0, this._position) * 0.001, 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"); }());