UNPKG

soundjs

Version:

A JavaScript library that provides a simple API, and powerful features to make working with audio a breeze. Easily ties in audio file loading to PreloadJS.

571 lines (481 loc) 18.6 kB
/*! * SoundJS * Visit http://createjs.com/ for documentation, updates and examples. * * Copyright (c) 2010 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. */ //############################################################################## // CordovaAudioLoader.js //############################################################################## this.createjs = this.createjs || {}; (function () { "use strict"; /** * Loader provides a mechanism to preload Cordova audio content via PreloadJS or internally. Instances are returned to * the preloader, and the load method is called when the asset needs to be requested. * Currently files are assumed to be local and no loading actually takes place. This class exists to more easily support * the existing architecture. * * @class CordovaAudioLoader * @param {String} loadItem The item to be loaded * @extends XHRRequest * @protected */ function Loader(loadItem) { this.AbstractLoader_constructor(loadItem, true, createjs.Types.SOUND); /** * A Media object used to determine if src exists and to get duration * @property _media * @type {Media} * @protected */ this._media = null; /** * A time counter that triggers timeout if loading takes too long * @property _loadTime * @type {number} * @protected */ this._loadTime = 0; /** * The frequency to fire the loading timer until duration can be retrieved * @property _TIMER_FREQUENCY * @type {number} * @protected */ this._TIMER_FREQUENCY = 100; }; var p = createjs.extend(Loader, createjs.AbstractLoader); // public methods p.load = function() { this._media = new Media(this._item.src, null, createjs.proxy(this._mediaErrorHandler,this)); this._media.seekTo(0); // needed to get duration this._getMediaDuration(); }; p.toString = function () { return "[CordovaAudioLoader]"; }; // private methods /** * Fires if audio cannot seek, indicating that src does not exist. * @method _mediaErrorHandler * @param error * @protected */ p._mediaErrorHandler = function(error) { this._media.release(); this._sendError(); }; /** * will attempt to get duration of audio until successful or time passes this._item.loadTimeout * @method _getMediaDuration * @protected */ p._getMediaDuration = function() { this._result = this._media.getDuration() * 1000; if (this._result < 0) { this._loadTime += this._TIMER_FREQUENCY; if (this._loadTime > this._item.loadTimeout) { this.handleEvent({type:"timeout"}); } else { setTimeout(createjs.proxy(this._getMediaDuration, this), this._TIMER_FREQUENCY); } } else { this._media.release(); this._sendComplete(); } }; createjs.CordovaAudioLoader = createjs.promote(Loader, "AbstractLoader"); }()); //############################################################################## // CordovaAudioSoundInstance.js //############################################################################## this.createjs = this.createjs || {}; (function () { "use strict"; /** * CordovaAudioSoundInstance extends the base api of {{#crossLink "AbstractSoundInstance"}}{{/crossLink}} and is used by * {{#crossLink "CordovaAudioPlugin"}}{{/crossLink}}. * * @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 CordovaAudioSoundInstance * @extends AbstractSoundInstance * @constructor */ function CordovaAudioSoundInstance(src, startTime, duration, playbackResource) { this.AbstractSoundInstance_constructor(src, startTime, duration, playbackResource); // Public Properties /** * Sets the playAudioWhenScreenIsLocked property for play calls on iOS devices. * @property playWhenScreenLocked * @type {boolean} */ this.playWhenScreenLocked = null; // Private Properties /** * Used to approximate the playback position by storing the number of milliseconds elapsed since * 1 January 1970 00:00:00 UTC when playing * Note that if js clock is out of sync with Media playback, this will become increasingly inaccurate. * @property _playStartTime * @type {Number} * @protected */ this._playStartTime = null; /** * A TimeOut used to trigger the end and possible loop of audio sprites. * @property _audioSpriteTimeout * @type {null} * @protected */ this._audioSpriteTimeout = null; /** * Boolean value that indicates if we are using an audioSprite * @property _audioSprite * @type {boolean} * @protected */ this._audioSprite = false; // Proxies, make removing listeners easier. this._audioSpriteEndHandler = createjs.proxy(this._handleAudioSpriteComplete, this); this._mediaPlayFinishedHandler = createjs.proxy(this._handleSoundComplete, this); this._mediaErrorHandler = createjs.proxy(this._handleMediaError, this); this._mediaProgressHandler = createjs.proxy(this._handleMediaProgress, this); this._playbackResource = new Media(src, this._mediaPlayFinishedHandler, this._mediaErrorHandler, this._mediaProgressHandler); if (duration) { this._audioSprite = true; } else { this._setDurationFromSource(); } } var p = createjs.extend(CordovaAudioSoundInstance, createjs.AbstractSoundInstance); // Public Methods /** * Called by {{#crossLink "Sound"}}{{/crossLink}} when plugin does not handle master volume. * undoc'd because it is not meant to be used outside of Sound * #method setMasterVolume * @param value */ p.setMasterVolume = function (value) { this._updateVolume(); }; /** * Called by {{#crossLink "Sound"}}{{/crossLink}} when plugin does not handle master mute. * undoc'd because it is not meant to be used outside of Sound * #method setMasterMute * @param value */ p.setMasterMute = function (isMuted) { this._updateVolume(); }; p.destroy = function() { // pause and release the playback resource, then call parent function this._playbackResource.pause(); this._playbackResource.release(); this.AbstractSoundInstance_destroy(); }; /** * Maps to <a href="http://plugins.cordova.io/#/package/org.apache.cordova.media" target="_blank">Media.getCurrentPosition</a>, * which is curiously asynchronus and requires a callback. * @method getCurrentPosition * @param {Method} mediaSuccess The callback that is passed the current position in seconds. * @param {Method} [mediaError=null] (Optional) The callback to execute if an error occurs. */ p.getCurrentPosition = function (mediaSuccess, mediaError) { this._playbackResource.getCurrentPosition(mediaSuccess, mediaError); }; p.toString = function () { return "[CordovaAudioSoundInstance]"; }; //Private Methods /** * media object has failed and likely will never work * @method _handleMediaError * @param error * @private */ p._handleMediaError = function(error) { clearTimeout(this.delayTimeoutId); // clear timeout that plays delayed sound this.playState = createjs.Sound.PLAY_FAILED; this._sendEvent("failed"); }; p._handleMediaProgress = function(state) { // do nothing }; p._handleAudioSpriteComplete = function() { this._playbackResource.pause(); this._handleSoundComplete(); }; /* don't need these for current looping approach p._removeLooping = function() { }; p._addLooping = function() { }; */ p._handleCleanUp = function () { clearTimeout(this._audioSpriteTimeout); // OJR cannot use .stop as it prevents .seekTo from working // todo consider media.release }; p._handleSoundReady = function (event) { this._playbackResource.seekTo(this._startTime + this._position); if (this._audioSprite) { this._audioSpriteTimeout = setTimeout(this._audioSpriteEndHandler, this._duration - this._position) } this._playbackResource.play({playAudioWhenScreenIsLocked: this.playWhenScreenLocked}); this._playStartTime = Date.now(); }; p._pause = function () { clearTimeout(this._audioSpriteTimeout); this._playbackResource.pause(); if (this._playStartTime) { this._position = Date.now() - this._playStartTime; this._playStartTime = null; } this._playbackResource.getCurrentPosition(createjs.proxy(this._updatePausePos, this)); }; /** * Synchronizes the best guess position with the actual current position. * @method _updatePausePos * @param {Number} pos The current position in seconds * @private */ p._updatePausePos = function (pos) { this._position = pos * 1000 - this._startTime; if(this._playStartTime) { this._playStartTime = Date.now(); } }; p._resume = function () { if (this._audioSprite) { this._audioSpriteTimeout = setTimeout(this._audioSpriteEndHandler, this._duration - this._position) } this._playbackResource.play({playAudioWhenScreenIsLocked: this.playWhenScreenLocked}); this._playStartTime = Date.now(); }; p._handleStop = function() { clearTimeout(this._audioSpriteTimeout); this._playbackResource.pause(); // cannot use .stop because it prevents .seekTo from working this._playbackResource.seekTo(this._startTime); if (this._playStartTime) { this._position = 0; this._playStartTime = null; } }; p._updateVolume = function () { var newVolume = (this._muted || createjs.Sound._masterMute) ? 0 : this._volume * createjs.Sound._masterVolume; this._playbackResource.setVolume(newVolume); }; p._calculateCurrentPosition = function() { // return best guess position. // Note if Media and js clock are out of sync, this value will become increasingly inaccurate over time if (this._playStartTime) { this._position = Date.now() - this._playStartTime + this._position; this._playStartTime = Date.now(); } return this._position; }; p._updatePosition = function() { this._playbackResource.seekTo(this._startTime + this._position); this._playStartTime = Date.now(); if (this._audioSprite) { clearTimeout(this._audioSpriteTimeout); this._audioSpriteTimeout = setTimeout(this._audioSpriteEndHandler, this._duration - this._position) } }; p._handleLoop = function (event) { this._handleSoundReady(); }; p._updateStartTime = function () { this._audioSprite = true; if(this.playState == createjs.Sound.PLAY_SUCCEEDED) { // do nothing } }; p._updateDuration = function () { this._audioSprite if(this.playState == createjs.Sound.PLAY_SUCCEEDED) { clearTimeout(this._audioSpriteTimeout); this._audioSpriteTimeout = setTimeout(this._audioSpriteEndHandler, this._duration - this.position) } }; p._setDurationFromSource = function () { this._duration = createjs.Sound.activePlugin.getSrcDuration(this.src); // TODO find a better way to do this that does not break flow }; createjs.CordovaAudioSoundInstance = createjs.promote(CordovaAudioSoundInstance, "AbstractSoundInstance"); }()); //############################################################################## // CordovaAudioPlugin.js //############################################################################## this.createjs = this.createjs || {}; (function () { "use strict"; /** * Play sounds using Cordova Media plugin, which will work with a Cordova app and tools that utilize Cordova such as PhoneGap or Ionic. * This plugin is not used by default, and must be registered manually in {{#crossLink "Sound"}}{{/crossLink}} * using the {{#crossLink "Sound/registerPlugins"}}{{/crossLink}} method. * This plugin is recommended when building a Cordova based app, but is not required. * * <b>NOTE the <a href="http://plugins.cordova.io/#/package/org.apache.cordova.media" target="_blank">Cordova Media plugin</a> is required</b> * * cordova plugin add org.apache.cordova.media * * <h4>Known Issues</h4> * <b>Audio Position</b> * <ul>Audio position is calculated asynchronusly by Media. The SoundJS solution to this problem is two-fold: * <li>Provide {{#crossLink "CordovaAudioSoundInstance/getCurrentPosition"}}{{/crossLink}} that maps directly to media.getCurrentPosition.</li> * <li>Provide a best guess position based on elapsed time since playback started, which is synchronized with actual position when the audio is paused or stopped. * Testing showed this to be fairly reliable within 200ms.</li></ul> * <b>Cordova Media Docs</b> * <ul><li>See the <a href="http://plugins.cordova.io/#/package/org.apache.cordova.media" target="_blank">Cordova Media Docs</a> for various known OS issues.</li></ul> * <br /> * * @class CordovaAudioPlugin * @extends AbstractPlugin * @constructor */ function CordovaAudioPlugin() { this.AbstractPlugin_constructor(); this._capabilities = s._capabilities; this._loaderClass = createjs.CordovaAudioLoader; this._soundInstanceClass = createjs.CordovaAudioSoundInstance; this._srcDurationHash = {}; } var p = createjs.extend(CordovaAudioPlugin, createjs.AbstractPlugin); var s = CordovaAudioPlugin; // Static Properties /** * Sets a default playAudioWhenScreenIsLocked property for play calls on iOS devices. * Individual SoundInstances can alter the default with {{#crossLink "CordovaAudioSoundInstance/playWhenScreenLocked"}}{{/crossLink}}. * @property playWhenScreenLocked * @type {boolean} * @static */ s.playWhenScreenLocked = false; /** * The capabilities of the plugin. This is generated via the {{#crossLink "CordovaAudioPlugin/_generateCapabilities"}}{{/crossLink}} * method. Please see the Sound {{#crossLink "Sound/capabilities:property"}}{{/crossLink}} method for an overview of all * of the available properties. * @property _capabilities * @type {Object} * @private * @static */ s._capabilities = null; // Static Methods /** * Determine if the plugin can be used in the current browser/OS. Note that HTML audio is available in most modern * browsers, but is disabled in iOS because of its limitations. * @method isSupported * @return {Boolean} If the plugin can be initialized. * @static */ s.isSupported = function () { s._generateCapabilities(); return (s._capabilities != null); }; /** * Determine the capabilities of the plugin. Used internally. Please see the Sound API {{#crossLink "Sound/capabilities:property"}}{{/crossLink}} * method for an overview of plugin capabilities. * @method _generateCapabilities * @static * @private */ s._generateCapabilities = function () { if (s._capabilities != null || !(window.cordova || window.PhoneGap || window.phonegap) || !window.Media) {return;} // OJR my best guess is that Cordova will have the same limits on playback that the audio tag has, but this could be wrong var t = document.createElement("audio"); if (t.canPlayType == null) {return null;} s._capabilities = { panning:false, volume:true, tracks:-1 }; // determine which extensions our browser supports for this plugin by iterating through Sound.SUPPORTED_EXTENSIONS var supportedExtensions = createjs.Sound.SUPPORTED_EXTENSIONS; var extensionMap = createjs.Sound.EXTENSION_MAP; for (var i = 0, l = supportedExtensions.length; i < l; i++) { var ext = supportedExtensions[i]; var playType = extensionMap[ext] || ext; s._capabilities[ext] = (t.canPlayType("audio/" + ext) != "no" && t.canPlayType("audio/" + ext) != "") || (t.canPlayType("audio/" + playType) != "no" && t.canPlayType("audio/" + playType) != ""); } // OJR another way to do this might be canPlayType:"m4a", codex: mp4 }; // public methods p.create = function (src, startTime, duration) { var si = this.AbstractPlugin_create(src, startTime, duration); si.playWhenScreenLocked = this.playWhenScreenLocked; return si; }; p.toString = function () { return "[CordovaAudioPlugin]"; }; // plugin does not support these p.setVolume = p.getVolume = p.setMute = null; /** * Get the duration for a src. Intended for internal use by CordovaAudioSoundInstance. * @method getSrcDuration * @param src * @returns {Number} The duration of the src or null if it does not exist */ p.getSrcDuration = function(src) { return this._srcDurationHash[src]; }; // Private Methods p._handlePreloadComplete = function (event) { var src = event.target.getItem().src; this._srcDurationHash[src] = event.result; this._audioSources[src] = event.result; //this.AbstractPlugin__handlePreloadComplete(event); // we don't want to do the rest of this }; p.removeSound = function (src) { delete(this._srcDurationHash[src]); this.AbstractPlugin_removeSound(src); }; createjs.CordovaAudioPlugin = createjs.promote(CordovaAudioPlugin, "AbstractPlugin"); }()); //############################################################################## // version_cordovaplugin.js //############################################################################## this.createjs = this.createjs || {}; (function () { var s = createjs.CordovaAudioPlugin = createjs.CordovaAudioPlugin || {}; /** * The version string for this release. * @for CordovaAudioPlugin * @property version * @type String * @static **/ s.version = /*=version*/"1.0.0"; // injected by build process /** * The build date for this release in UTC format. * @for CordovaAudioPlugin * @property buildDate * @type String * @static **/ s.buildDate = /*=date*/"Thu, 14 Sep 2017 19:47:47 GMT"; // injected by build process })();