/* * Sound * 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. */ // namespace: this.createjs = this.createjs || {}; /** * The SoundJS library manages the playback of audio on the web. It works via plugins which abstract the actual audio * implementation, so playback is possible on any platform without specific knowledge of what mechanisms are necessary * to play sounds. * * To use SoundJS, use the public API on the {{#crossLink "Sound"}}{{/crossLink}} class. This API is for: * <ul> * <li>Installing audio playback Plugins</li> * <li>Registering (and preloading) sounds</li> * <li>Creating and playing sounds</li> * <li>Master volume, mute, and stop controls for all sounds at once</li> * </ul> * * <b>Controlling Sounds</b><br /> * Playing sounds creates {{#crossLink "AbstractSoundInstance"}}{{/crossLink}} instances, which can be controlled * individually. * <ul> * <li>Pause, resume, seek, and stop sounds</li> * <li>Control a sound's volume, mute, and pan</li> * <li>Listen for events on sound instances to get notified when they finish, loop, or fail</li> * </ul> * * <h4>Example</h4> * * createjs.Sound.alternateExtensions = ["mp3"]; * createjs.Sound.on("fileload", this.loadHandler, this); * createjs.Sound.registerSound("path/to/mySound.ogg", "sound"); * function loadHandler(event) { * // This is fired for each sound that is registered. * var instance = createjs.Sound.play("sound"); // play using id. Could also use full sourcepath or event.src. * instance.on("complete", this.handleComplete, this); * instance.volume = 0.5; * } * * <h4>Browser Support</h4> * Audio will work in browsers which support Web Audio (<a href="http://caniuse.com/audio-api" target="_blank">http://caniuse.com/audio-api</a>) * or HTMLAudioElement (<a href="http://caniuse.com/audio" target="_blank">http://caniuse.com/audio</a>). * A Flash fallback can be used for any browser that supports the Flash player, and the Cordova plugin can be used in * any webview that supports <a href="http://plugins.cordova.io/#/package/org.apache.cordova.media" target="_blank">Cordova.Media</a>. * IE8 and earlier are not supported, even with the Flash fallback. To support earlier browsers, you can use an older * version of SoundJS (version 0.5.2 and earlier). * * @module SoundJS * @main SoundJS */ (function () { "use strict"; /** * The Sound class is the public API for creating sounds, controlling the overall sound levels, and managing plugins. * All Sound APIs on this class are static. * * <b>Registering and Preloading</b><br /> * Before you can play a sound, it <b>must</b> be registered. You can do this with {{#crossLink "Sound/registerSound"}}{{/crossLink}}, * or register multiple sounds using {{#crossLink "Sound/registerSounds"}}{{/crossLink}}. If you don't register a * sound prior to attempting to play it using {{#crossLink "Sound/play"}}{{/crossLink}} or create it using {{#crossLink "Sound/createInstance"}}{{/crossLink}}, * the sound source will be automatically registered but playback will fail as the source will not be ready. If you use * <a href="http://preloadjs.com" target="_blank">PreloadJS</a>, registration is handled for you when the sound is * preloaded. It is recommended to preload sounds either internally using the register functions or externally using * PreloadJS so they are ready when you want to use them. * * <b>Playback</b><br /> * To play a sound once it's been registered and preloaded, use the {{#crossLink "Sound/play"}}{{/crossLink}} method. * This method returns a {{#crossLink "AbstractSoundInstance"}}{{/crossLink}} which can be paused, resumed, muted, etc. * Please see the {{#crossLink "AbstractSoundInstance"}}{{/crossLink}} documentation for more on the instance control APIs. * * <b>Plugins</b><br /> * By default, the {{#crossLink "WebAudioPlugin"}}{{/crossLink}} or the {{#crossLink "HTMLAudioPlugin"}}{{/crossLink}} * are used (when available), although developers can change plugin priority or add new plugins (such as the * provided {{#crossLink "FlashAudioPlugin"}}{{/crossLink}}). Please see the {{#crossLink "Sound"}}{{/crossLink}} API * methods for more on the playback and plugin APIs. To install plugins, or specify a different plugin order, see * {{#crossLink "Sound/installPlugins"}}{{/crossLink}}. * * <h4>Example</h4> * * createjs.FlashAudioPlugin.swfPath = "../src/soundjs/flashaudio"; * createjs.Sound.registerPlugins([createjs.WebAudioPlugin, createjs.FlashAudioPlugin]); * createjs.Sound.alternateExtensions = ["mp3"]; * createjs.Sound.on("fileload", this.loadHandler, this); * createjs.Sound.registerSound("path/to/mySound.ogg", "sound"); * function loadHandler(event) { * // This is fired for each sound that is registered. * var instance = createjs.Sound.play("sound"); // play using id. Could also use full source path or event.src. * instance.on("complete", this.handleComplete, this); * instance.volume = 0.5; * } * * The maximum number of concurrently playing instances of the same sound can be specified in the "data" argument * of {{#crossLink "Sound/registerSound"}}{{/crossLink}}. Note that if not specified, the active plugin will apply * a default limit. Currently HTMLAudioPlugin sets a default limit of 2, while WebAudioPlugin and FlashAudioPlugin set a * default limit of 100. * * createjs.Sound.registerSound("sound.mp3", "soundId", 4); * * Sound can be used as a plugin with PreloadJS to help preload audio properly. Audio preloaded with PreloadJS is * automatically registered with the Sound class. When audio is not preloaded, Sound will do an automatic internal * load. As a result, it may fail to play the first time play is called if the audio is not finished loading. Use * the {{#crossLink "Sound/fileload:event"}}{{/crossLink}} event to determine when a sound has finished internally * preloading. It is recommended that all audio is preloaded before it is played. * * var queue = new createjs.LoadQueue(); * queue.installPlugin(createjs.Sound); * * <b>Audio Sprites</b><br /> * SoundJS has added support for {{#crossLink "AudioSprite"}}{{/crossLink}}, available as of version 0.6.0. * For those unfamiliar with audio sprites, they are much like CSS sprites or sprite sheets: multiple audio assets * grouped into a single file. * * <h4>Example</h4> * * var assetsPath = "./assets/"; * var sounds = [{ * src:"MyAudioSprite.ogg", data: { * audioSprite: [ * {id:"sound1", startTime:0, duration:500}, * {id:"sound2", startTime:1000, duration:400}, * {id:"sound3", startTime:1700, duration: 1000} * ]} * } * ]; * createjs.Sound.alternateExtensions = ["mp3"]; * createjs.Sound.on("fileload", loadSound); * createjs.Sound.registerSounds(sounds, assetsPath); * // after load is complete * createjs.Sound.play("sound2"); * * <b>Mobile Playback</b><br /> * Devices running iOS require the WebAudio context to be "unlocked" by playing at least one sound inside of a user- * initiated event (such as touch/click). Earlier versions of SoundJS included a "MobileSafe" sample, but this is no * longer necessary as of SoundJS 0.6.2. * <ul> * <li> * In SoundJS 0.4.1 and above, you can either initialize plugins or use the {{#crossLink "WebAudioPlugin/playEmptySound"}}{{/crossLink}} * method in the call stack of a user input event to manually unlock the audio context. * </li> * <li> * In SoundJS 0.6.2 and above, SoundJS will automatically listen for the first document-level "mousedown" * and "touchend" event, and unlock WebAudio. This will continue to check these events until the WebAudio * context becomes "unlocked" (changes from "suspended" to "running") * </li> * <li> * Both the "mousedown" and "touchend" events can be used to unlock audio in iOS9+, the "touchstart" event * will work in iOS8 and below. The "touchend" event will only work in iOS9 when the gesture is interpreted * as a "click", so if the user long-presses the button, it will no longer work. * </li> * <li> * When using the <a href="http://www.createjs.com/docs/easeljs/classes/Touch.html">EaselJS Touch class</a>, * the "mousedown" event will not fire when a canvas is clicked, since MouseEvents are prevented, to ensure * only touch events fire. To get around this, you can either rely on "touchend", or: * <ol> * <li>Set the `allowDefault` property on the Touch class constructor to `true` (defaults to `false`).</li> * <li>Set the `preventSelection` property on the EaselJS `Stage` to `false`.</li> * </ol> * These settings may change how your application behaves, and are not recommended. * </li> * </ul> * * <b>Loading Alternate Paths and Extension-less Files</b><br /> * SoundJS supports loading alternate paths and extension-less files by passing an object instead of a string for * the `src` property, which is a hash using the format `{extension:"path", extension2:"path2"}`. These labels are * how SoundJS determines if the browser will support the sound. This also enables multiple formats to live in * different folders, or on CDNs, which often has completely different filenames for each file. * * Priority is determined by the property order (first property is tried first). This is supported by both internal loading * and loading with PreloadJS. * * <em>Note: an id is required for playback.</em> * * <h4>Example</h4> * * var sounds = {path:"./audioPath/", * manifest: [ * {id: "cool", src: {mp3:"mp3/awesome.mp3", ogg:"noExtensionOggFile"}} * ]}; * * createjs.Sound.alternateExtensions = ["mp3"]; * createjs.Sound.addEventListener("fileload", handleLoad); * createjs.Sound.registerSounds(sounds); * * <h3>Known Browser and OS issues</h3> * <b>IE 9 HTML Audio limitations</b><br /> * <ul><li>There is a delay in applying volume changes to tags that occurs once playback is started. So if you have * muted all sounds, they will all play during this delay until the mute applies internally. This happens regardless of * when or how you apply the volume change, as the tag seems to need to play to apply it.</li> * <li>MP3 encoding will not always work for audio tags, particularly in Internet Explorer. We've found default * encoding with 64kbps works.</li> * <li>Occasionally very short samples will get cut off.</li> * <li>There is a limit to how many audio tags you can load and play at once, which appears to be determined by * hardware and browser settings. See {{#crossLink "HTMLAudioPlugin.MAX_INSTANCES"}}{{/crossLink}} for a safe * estimate.</li></ul> * * <b>Firefox 25 Web Audio limitations</b> * <ul><li>mp3 audio files do not load properly on all windows machines, reported * <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=929969" target="_blank">here</a>. </br> * For this reason it is recommended to pass another FF supported type (ie ogg) first until this bug is resolved, if * possible.</li></ul> * <b>Safari limitations</b><br /> * <ul><li>Safari requires Quicktime to be installed for audio playback.</li></ul> * * <b>iOS 6 Web Audio limitations</b><br /> * <ul><li>Sound is initially locked, and must be unlocked via a user-initiated event. Please see the section on * Mobile Playback above.</li> * <li>A bug exists that will distort un-cached web audio when a video element is present in the DOM that has audio * at a different sampleRate.</li> * </ul> * * <b>Android HTML Audio limitations</b><br /> * <ul><li>We have no control over audio volume. Only the user can set volume on their device.</li> * <li>We can only play audio inside a user event (touch/click). This currently means you cannot loop sound or use * a delay.</li></ul> * * <b>Web Audio and PreloadJS</b><br /> * <ul><li>Web Audio must be loaded through XHR, therefore when used with PreloadJS, tag loading is not possible. * This means that tag loading can not be used to avoid cross domain issues.</li><ul> * * @class Sound * @static * @uses EventDispatcher */ function Sound() { throw "Sound cannot be instantiated"; } var s = Sound; // Static Properties /** * The interrupt value to interrupt any currently playing instance with the same source, if the maximum number of * instances of the sound are already playing. * @property INTERRUPT_ANY * @type {String} * @default any * @static */ s.INTERRUPT_ANY = "any"; /** * The interrupt value to interrupt the earliest currently playing instance with the same source that progressed the * least distance in the audio track, if the maximum number of instances of the sound are already playing. * @property INTERRUPT_EARLY * @type {String} * @default early * @static */ s.INTERRUPT_EARLY = "early"; /** * The interrupt value to interrupt the currently playing instance with the same source that progressed the most * distance in the audio track, if the maximum number of instances of the sound are already playing. * @property INTERRUPT_LATE * @type {String} * @default late * @static */ s.INTERRUPT_LATE = "late"; /** * The interrupt value to not interrupt any currently playing instances with the same source, if the maximum number of * instances of the sound are already playing. * @property INTERRUPT_NONE * @type {String} * @default none * @static */ s.INTERRUPT_NONE = "none"; /** * Defines the playState of an instance that is still initializing. * @property PLAY_INITED * @type {String} * @default playInited * @static */ s.PLAY_INITED = "playInited"; /** * Defines the playState of an instance that is currently playing or paused. * @property PLAY_SUCCEEDED * @type {String} * @default playSucceeded * @static */ s.PLAY_SUCCEEDED = "playSucceeded"; /** * Defines the playState of an instance that was interrupted by another instance. * @property PLAY_INTERRUPTED * @type {String} * @default playInterrupted * @static */ s.PLAY_INTERRUPTED = "playInterrupted"; /** * Defines the playState of an instance that completed playback. * @property PLAY_FINISHED * @type {String} * @default playFinished * @static */ s.PLAY_FINISHED = "playFinished"; /** * Defines the playState of an instance that failed to play. This is usually caused by a lack of available channels * when the interrupt mode was "INTERRUPT_NONE", the playback stalled, or the sound could not be found. * @property PLAY_FAILED * @type {String} * @default playFailed * @static */ s.PLAY_FAILED = "playFailed"; /** * A list of the default supported extensions that Sound will <i>try</i> to play. Plugins will check if the browser * can play these types, so modifying this list before a plugin is initialized will allow the plugins to try to * support additional media types. * * NOTE this does not currently work for {{#crossLink "FlashAudioPlugin"}}{{/crossLink}}. * * More details on file formats can be found at <a href="http://en.wikipedia.org/wiki/Audio_file_format" target="_blank">http://en.wikipedia.org/wiki/Audio_file_format</a>.<br /> * A very detailed list of file formats can be found at <a href="http://www.fileinfo.com/filetypes/audio" target="_blank">http://www.fileinfo.com/filetypes/audio</a>. * @property SUPPORTED_EXTENSIONS * @type {Array[String]} * @default ["mp3", "ogg", "opus", "mpeg", "wav", "m4a", "mp4", "aiff", "wma", "mid"] * @since 0.4.0 * @static */ s.SUPPORTED_EXTENSIONS = ["mp3", "ogg", "opus", "mpeg", "wav", "m4a", "mp4", "aiff", "wma", "mid"]; /** * Some extensions use another type of extension support to play (one of them is a codex). This allows you to map * that support so plugins can accurately determine if an extension is supported. Adding to this list can help * plugins determine more accurately if an extension is supported. * * A useful list of extensions for each format can be found at <a href="http://html5doctor.com/html5-audio-the-state-of-play/" target="_blank">http://html5doctor.com/html5-audio-the-state-of-play/</a>. * @property EXTENSION_MAP * @type {Object} * @since 0.4.0 * @default {m4a:"mp4"} * @static */ s.EXTENSION_MAP = { m4a:"mp4" }; /** * The RegExp pattern used to parse file URIs. This supports simple file names, as well as full domain URIs with * query strings. The resulting match is: protocol:$1 domain:$2 path:$3 file:$4 extension:$5 query:$6. * @property FILE_PATTERN * @type {RegExp} * @static * @private */ s.FILE_PATTERN = /^(?:(\w+:)\/{2}(\w+(?:\.\w+)*\/?))?([/.]*?(?:[^?]+)?\/)?((?:[^/?]+)\.(\w+))(?:\?(\S+)?)?$/; // Class Public properties /** * Determines the default behavior for interrupting other currently playing instances with the same source, if the * maximum number of instances of the sound are already playing. Currently the default is {{#crossLink "Sound/INTERRUPT_NONE:property"}}{{/crossLink}} * but this can be set and will change playback behavior accordingly. This is only used when {{#crossLink "Sound/play"}}{{/crossLink}} * is called without passing a value for interrupt. * @property defaultInterruptBehavior * @type {String} * @default Sound.INTERRUPT_NONE, or "none" * @static * @since 0.4.0 */ s.defaultInterruptBehavior = s.INTERRUPT_NONE; // OJR does s.INTERRUPT_ANY make more sense as default? Needs game dev testing to see which case makes more sense. /** * An array of extensions to attempt to use when loading sound, if the default is unsupported by the active plugin. * These are applied in order, so if you try to Load Thunder.ogg in a browser that does not support ogg, and your * extensions array is ["mp3", "m4a", "wav"] it will check mp3 support, then m4a, then wav. The audio files need * to exist in the same location, as only the extension is altered. * * Note that regardless of which file is loaded, you can call {{#crossLink "Sound/createInstance"}}{{/crossLink}} * and {{#crossLink "Sound/play"}}{{/crossLink}} using the same id or full source path passed for loading. * * <h4>Example</h4> * * var sounds = [ * {src:"myPath/mySound.ogg", id:"example"}, * ]; * createjs.Sound.alternateExtensions = ["mp3"]; // now if ogg is not supported, SoundJS will try asset0.mp3 * createjs.Sound.on("fileload", handleLoad); // call handleLoad when each sound loads * createjs.Sound.registerSounds(sounds, assetPath); * // ... * createjs.Sound.play("myPath/mySound.ogg"); // works regardless of what extension is supported. Note calling with ID is a better approach * * @property alternateExtensions * @type {Array} * @since 0.5.2 * @static */ s.alternateExtensions = []; /** * The currently active plugin. If this is null, then no plugin could be initialized. If no plugin was specified, * Sound attempts to apply the default plugins: {{#crossLink "WebAudioPlugin"}}{{/crossLink}}, followed by * {{#crossLink "HTMLAudioPlugin"}}{{/crossLink}}. * @property activePlugin * @type {Object} * @static */ s.activePlugin = null; // class getter / setter properties /** * Set the master volume of Sound. The master volume is multiplied against each sound's individual volume. For * example, if master volume is 0.5 and a sound's volume is 0.5, the resulting volume is 0.25. To set individual * sound volume, use AbstractSoundInstance {{#crossLink "AbstractSoundInstance/volume:property"}}{{/crossLink}} * instead. * * <h4>Example</h4> * * createjs.Sound.volume = 0.5; * * @property volume * @type {Number} * @default 1 * @static * @since 0.6.1 */ /** * The internal volume level. Use {{#crossLink "Sound/volume:property"}}{{/crossLink}} to adjust the master volume. * @property _masterVolume * @type {number} * @default 1 * @private */ s._masterVolume = 1; /** * Use the {{#crossLink "Sound/volume:property"}}{{/crossLink}} property instead. * @method _getMasterVolume * @private * @static * @return {Number} **/ s._getMasterVolume = function() { return this._masterVolume; }; /** * Use the {{#crossLink "Sound/volume:property"}}{{/crossLink}} property instead. * @method getMasterVolume * @deprecated */ // Sound.getMasterVolume is @deprecated. Remove for 1.1+ s.getVolume = createjs.deprecate(s._getMasterVolume, "Sound.getVolume"); /** * Use the {{#crossLink "Sound/volume:property"}}{{/crossLink}} property instead. * @method _setMasterVolume * @static * @private **/ s._setMasterVolume = function(value) { if (Number(value) == null) { return; } value = Math.max(0, Math.min(1, value)); s._masterVolume = value; if (!this.activePlugin || !this.activePlugin.setVolume || !this.activePlugin.setVolume(value)) { var instances = this._instances; for (var i = 0, l = instances.length; i < l; i++) { instances[i].setMasterVolume(value); } } }; /** * Use the {{#crossLink "Sound/volume:property"}}{{/crossLink}} property instead. * @method setVolume * @deprecated */ // Sound.setVolume is @deprecated. Remove for 1.1+ s.setVolume = createjs.deprecate(s._setMasterVolume, "Sound.setVolume"); /** * Mute/Unmute all audio. Note that muted audio still plays at 0 volume. This global mute value is maintained * separately and when set will override, but not change the mute property of individual instances. To mute an individual * instance, use AbstractSoundInstance {{#crossLink "AbstractSoundInstance/muted:property"}}{{/crossLink}} instead. * * <h4>Example</h4> * * createjs.Sound.muted = true; * * * @property muted * @type {Boolean} * @default false * @static * @since 0.6.1 */ s._masterMute = false; /** * Use the {{#crossLink "Sound/muted:property"}}{{/crossLink}} property instead. * @method _getMute * @returns {Boolean} * @static * @private */ s._getMute = function () { return this._masterMute; }; /** * Use the {{#crossLink "Sound/muted:property"}}{{/crossLink}} property instead. * @method getMute * @deprecated */ // Sound.getMute is @deprecated. Remove for 1.1+ s.getMute = createjs.deprecate(s._getMute, "Sound.getMute"); /** * Use the {{#crossLink "Sound/muted:property"}}{{/crossLink}} property instead. * @method _setMute * @param {Boolean} value The muted value * @static * @private */ s._setMute = function (value) { if (value == null) { return; } this._masterMute = value; if (!this.activePlugin || !this.activePlugin.setMute || !this.activePlugin.setMute(value)) { var instances = this._instances; for (var i = 0, l = instances.length; i < l; i++) { instances[i].setMasterMute(value); } } }; /** * Use the {{#crossLink "Sound/muted:property"}}{{/crossLink}} property instead. * @method setMute * @deprecated */ // Sound.setMute is @deprecated. Remove for 1.1+ s.setMute = createjs.deprecate(s._setMute, "Sound.setMute"); /** * Get the active plugins capabilities, which help determine if a plugin can be used in the current environment, * or if the plugin supports a specific feature. Capabilities include: * <ul> * <li><b>panning:</b> If the plugin can pan audio from left to right</li> * <li><b>volume;</b> If the plugin can control audio volume.</li> * <li><b>tracks:</b> The maximum number of audio tracks that can be played back at a time. This will be -1 * if there is no known limit.</li> * <br />An entry for each file type in {{#crossLink "Sound/SUPPORTED_EXTENSIONS:property"}}{{/crossLink}}: * <li><b>mp3:</b> If MP3 audio is supported.</li> * <li><b>ogg:</b> If OGG audio is supported.</li> * <li><b>wav:</b> If WAV audio is supported.</li> * <li><b>mpeg:</b> If MPEG audio is supported.</li> * <li><b>m4a:</b> If M4A audio is supported.</li> * <li><b>mp4:</b> If MP4 audio is supported.</li> * <li><b>aiff:</b> If aiff audio is supported.</li> * <li><b>wma:</b> If wma audio is supported.</li> * <li><b>mid:</b> If mid audio is supported.</li> * </ul> * * You can get a specific capability of the active plugin using standard object notation * * <h4>Example</h4> * * var mp3 = createjs.Sound.capabilities.mp3; * * Note this property is read only. * * @property capabilities * @type {Object} * @static * @readOnly * @since 0.6.1 */ /** * Use the {{#crossLink "Sound/capabilities:property"}}{{/crossLink}} property instead. * @returns {null} * @private */ s._getCapabilities = function() { if (s.activePlugin == null) { return null; } return s.activePlugin._capabilities; }; /** * Use the {{#crossLink "Sound/capabilities:property"}}{{/crossLink}} property instead. * @method getCapabilities * @deprecated */ // Sound.getCapabilities is @deprecated. Remove for 1.1+ s.getCapabilities = createjs.deprecate(s._getCapabilities, "Sound.getCapabilities"); Object.defineProperties(s, { volume: { get: s._getMasterVolume, set: s._setMasterVolume }, muted: { get: s._getMute, set: s._setMute }, capabilities: { get: s._getCapabilities } }); // Class Private properties /** * Determines if the plugins have been registered. If false, the first call to {{#crossLink "play"}}{{/crossLink}} will instantiate the default * plugins ({{#crossLink "WebAudioPlugin"}}{{/crossLink}}, followed by {{#crossLink "HTMLAudioPlugin"}}{{/crossLink}}). * If plugins have been registered, but none are applicable, then sound playback will fail. * @property _pluginsRegistered * @type {Boolean} * @default false * @static * @private */ s._pluginsRegistered = false; /** * Used internally to assign unique IDs to each AbstractSoundInstance. * @property _lastID * @type {Number} * @static * @private */ s._lastID = 0; /** * An array containing all currently playing instances. This allows Sound to control the volume, mute, and playback of * all instances when using static APIs like {{#crossLink "Sound/stop"}}{{/crossLink}} and {{#crossLink "Sound/volume:property"}}{{/crossLink}}. * When an instance has finished playback, it gets removed via the {{#crossLink "Sound/finishedPlaying"}}{{/crossLink}} * method. If the user replays an instance, it gets added back in via the {{#crossLink "Sound/_beginPlaying"}}{{/crossLink}} * method. * @property _instances * @type {Array} * @private * @static */ s._instances = []; /** * An object hash storing objects with sound sources, startTime, and duration via there corresponding ID. * @property _idHash * @type {Object} * @private * @static */ s._idHash = {}; /** * An object hash that stores preloading sound sources via the parsed source that is passed to the plugin. Contains the * source, id, and data that was passed in by the user. Parsed sources can contain multiple instances of source, id, * and data. * @property _preloadHash * @type {Object} * @private * @static */ s._preloadHash = {}; /** * An object hash storing {{#crossLink "PlayPropsConfig"}}{{/crossLink}} via the parsed source that is passed as defaultPlayProps in * {{#crossLink "Sound/registerSound"}}{{/crossLink}} and {{#crossLink "Sound/registerSounds"}}{{/crossLink}}. * @property _defaultPlayPropsHash * @type {Object} * @private * @static * @since 0.6.1 */ s._defaultPlayPropsHash = {}; // EventDispatcher methods: s.addEventListener = null; s.removeEventListener = null; s.removeAllEventListeners = null; s.dispatchEvent = null; s.hasEventListener = null; s._listeners = null; createjs.EventDispatcher.initialize(s); // inject EventDispatcher methods. // Events /** * This event is fired when a file finishes loading internally. This event is fired for each loaded sound, * so any handler methods should look up the <code>event.src</code> to handle a particular sound. * @event fileload * @param {Object} target The object that dispatched the event. * @param {String} type The event type. * @param {String} src The source of the sound that was loaded. * @param {String} [id] The id passed in when the sound was registered. If one was not provided, it will be null. * @param {Number|Object} [data] Any additional data associated with the item. If not provided, it will be undefined. * @since 0.4.1 */ /** * This event is fired when a file fails loading internally. This event is fired for each loaded sound, * so any handler methods should look up the <code>event.src</code> to handle a particular sound. * @event fileerror * @param {Object} target The object that dispatched the event. * @param {String} type The event type. * @param {String} src The source of the sound that was loaded. * @param {String} [id] The id passed in when the sound was registered. If one was not provided, it will be null. * @param {Number|Object} [data] Any additional data associated with the item. If not provided, it will be undefined. * @since 0.6.0 */ // Class Public Methods /** * Get the preload rules to allow Sound to be used as a plugin by <a href="http://preloadjs.com" target="_blank">PreloadJS</a>. * Any load calls that have the matching type or extension will fire the callback method, and use the resulting * object, which is potentially modified by Sound. This helps when determining the correct path, as well as * registering the audio instance(s) with Sound. This method should not be called, except by PreloadJS. * @method getPreloadHandlers * @return {Object} An object containing: * <ul><li>callback: A preload callback that is fired when a file is added to PreloadJS, which provides * Sound a mechanism to modify the load parameters, select the correct file format, register the sound, etc.</li> * <li>types: A list of file types that are supported by Sound (currently supports "sound").</li> * <li>extensions: A list of file extensions that are supported by Sound (see {{#crossLink "Sound/SUPPORTED_EXTENSIONS:property"}}{{/crossLink}}).</li></ul> * @static * @private */ s.getPreloadHandlers = function () { return { callback:createjs.proxy(s.initLoad, s), types:["sound"], extensions:s.SUPPORTED_EXTENSIONS }; }; /** * Used to dispatch fileload events from internal loading. * @method _handleLoadComplete * @param event A loader event. * @private * @static * @since 0.6.0 */ s._handleLoadComplete = function(event) { var src = event.target.getItem().src; if (!s._preloadHash[src]) {return;} for (var i = 0, l = s._preloadHash[src].length; i < l; i++) { var item = s._preloadHash[src][i]; s._preloadHash[src][i] = true; if (!s.hasEventListener("fileload")) { continue; } var event = new createjs.Event("fileload"); event.src = item.src; event.id = item.id; event.data = item.data; event.sprite = item.sprite; s.dispatchEvent(event); } }; /** * Used to dispatch error events from internal preloading. * @param event * @private * @since 0.6.0 * @static */ s._handleLoadError = function(event) { var src = event.target.getItem().src; if (!s._preloadHash[src]) {return;} for (var i = 0, l = s._preloadHash[src].length; i < l; i++) { var item = s._preloadHash[src][i]; s._preloadHash[src][i] = false; if (!s.hasEventListener("fileerror")) { continue; } var event = new createjs.Event("fileerror"); event.src = item.src; event.id = item.id; event.data = item.data; event.sprite = item.sprite; s.dispatchEvent(event); } }; /** * Used by {{#crossLink "Sound/registerPlugins"}}{{/crossLink}} to register a Sound plugin. * * @method _registerPlugin * @param {Object} plugin The plugin class to install. * @return {Boolean} Whether the plugin was successfully initialized. * @static * @private */ s._registerPlugin = function (plugin) { // Note: Each plugin is passed in as a class reference, but we store the activePlugin as an instance if (plugin.isSupported()) { s.activePlugin = new plugin(); return true; } return false; }; /** * Register a list of Sound plugins, in order of precedence. To register a single plugin, pass a single element in the array. * * <h4>Example</h4> * * createjs.FlashAudioPlugin.swfPath = "../src/soundjs/flashaudio/"; * createjs.Sound.registerPlugins([createjs.WebAudioPlugin, createjs.HTMLAudioPlugin, createjs.FlashAudioPlugin]); * * @method registerPlugins * @param {Array} plugins An array of plugins classes to install. * @return {Boolean} Whether a plugin was successfully initialized. * @static */ s.registerPlugins = function (plugins) { s._pluginsRegistered = true; for (var i = 0, l = plugins.length; i < l; i++) { if (s._registerPlugin(plugins[i])) { return true; } } return false; }; /** * Initialize the default plugins. This method is automatically called when any audio is played or registered before * the user has manually registered plugins, and enables Sound to work without manual plugin setup. Currently, the * default plugins are {{#crossLink "WebAudioPlugin"}}{{/crossLink}} followed by {{#crossLink "HTMLAudioPlugin"}}{{/crossLink}}. * * <h4>Example</h4> * * if (!createjs.initializeDefaultPlugins()) { return; } * * @method initializeDefaultPlugins * @returns {Boolean} True if a plugin was initialized, false otherwise. * @since 0.4.0 * @static */ s.initializeDefaultPlugins = function () { if (s.activePlugin != null) {return true;} if (s._pluginsRegistered) {return false;} if (s.registerPlugins([createjs.WebAudioPlugin, createjs.HTMLAudioPlugin])) {return true;} return false; }; /** * Determines if Sound has been initialized, and a plugin has been activated. * * <h4>Example</h4> * This example sets up a Flash fallback, but only if there is no plugin specified yet. * * if (!createjs.Sound.isReady()) { * createjs.FlashAudioPlugin.swfPath = "../src/soundjs/flashaudio/"; * createjs.Sound.registerPlugins([createjs.WebAudioPlugin, createjs.HTMLAudioPlugin, createjs.FlashAudioPlugin]); * } * * @method isReady * @return {Boolean} If Sound has initialized a plugin. * @static */ s.isReady = function () { return (s.activePlugin != null); }; /** * Process manifest items from <a href="http://preloadjs.com" target="_blank">PreloadJS</a>. This method is intended * for usage by a plugin, and not for direct interaction. * @method initLoad * @param {Object} src The object to load. * @return {Object|AbstractLoader} An instance of AbstractLoader. * @private * @static */ s.initLoad = function (loadItem) { if (loadItem.type == "video") { return true; } // Don't handle video. PreloadJS's plugin model is really aggressive. return s._registerSound(loadItem); }; /** * Internal method for loading sounds. This should not be called directly. * * @method _registerSound * @param {Object} src The object to load, containing src property and optionally containing id and data. * @return {Object} An object with the modified values that were passed in, which defines the sound. * Returns false if the source cannot be parsed or no plugins can be initialized. * Returns true if the source is already loaded. * @static * @private * @since 0.6.0 */ s._registerSound = function (loadItem) { if (!s.initializeDefaultPlugins()) {return false;} var details; if (loadItem.src instanceof Object) { details = s._parseSrc(loadItem.src); details.src = loadItem.path + details.src; } else { details = s._parsePath(loadItem.src); } if (details == null) {return false;} loadItem.src = details.src; loadItem.type = "sound"; var data = loadItem.data; var numChannels = null; if (data != null) { if (!isNaN(data.channels)) { numChannels = parseInt(data.channels); } else if (!isNaN(data)) { numChannels = parseInt(data); } if(data.audioSprite) { var sp; for(var i = data.audioSprite.length; i--; ) { sp = data.audioSprite[i]; s._idHash[sp.id] = {src: loadItem.src, startTime: parseInt(sp.startTime), duration: parseInt(sp.duration)}; if (sp.defaultPlayProps) { s._defaultPlayPropsHash[sp.id] = createjs.PlayPropsConfig.create(sp.defaultPlayProps); } } } } if (loadItem.id != null) {s._idHash[loadItem.id] = {src: loadItem.src}}; var loader = s.activePlugin.register(loadItem); SoundChannel.create(loadItem.src, numChannels); // return the number of instances to the user. This will also be returned in the load event. if (data == null || !isNaN(data)) { loadItem.data = numChannels || SoundChannel.maxPerChannel(); } else { loadItem.data.channels = numChannels || SoundChannel.maxPerChannel(); } if (loader.type) {loadItem.type = loader.type;} if (loadItem.defaultPlayProps) { s._defaultPlayPropsHash[loadItem.src] = createjs.PlayPropsConfig.create(loadItem.defaultPlayProps); } return loader; }; /** * Register an audio file for loading and future playback in Sound. This is automatically called when using * <a href="http://preloadjs.com" target="_blank">PreloadJS</a>. It is recommended to register all sounds that * need to be played back in order to properly prepare and preload them. Sound does internal preloading when required. * * <h4>Example</h4> * * createjs.Sound.alternateExtensions = ["mp3"]; * createjs.Sound.on("fileload", handleLoad); // add an event listener for when load is completed * createjs.Sound.registerSound("myAudioPath/mySound.ogg", "myID", 3); * createjs.Sound.registerSound({ogg:"path1/mySound.ogg", mp3:"path2/mySoundNoExtension"}, "myID", 3); * * * @method registerSound * @param {String | Object} src The source or an Object with a "src" property or an Object with multiple extension labeled src properties. * @param {String} [id] An id specified by the user to play the sound later. Note id is required for when src is multiple extension labeled src properties. * @param {Number | Object} [data] Data associated with the item. Sound uses the data parameter as the number of * channels for an audio instance, however a "channels" property can be appended to the data object if it is used * for other information. The audio channels will set a default based on plugin if no value is found. * Sound also uses the data property to hold an {{#crossLink "AudioSprite"}}{{/crossLink}} array of objects in the following format {id, startTime, duration}.<br/> * id used to play the sound later, in the same manner as a sound src with an id.<br/> * startTime is the initial offset to start playback and loop from, in milliseconds.<br/> * duration is the amount of time to play the clip for, in milliseconds.<br/> * This allows Sound to support audio sprites that are played back by id. * @param {string} basePath Set a path that will be prepended to src for loading. * @param {Object | PlayPropsConfig} defaultPlayProps Optional Playback properties that will be set as the defaults on any new AbstractSoundInstance. * See {{#crossLink "PlayPropsConfig"}}{{/crossLink}} for options. * @return {Object} An object with the modified values that were passed in, which defines the sound. * Returns false if the source cannot be parsed or no plugins can be initialized. * Returns true if the source is already loaded. * @static * @since 0.4.0 */ s.registerSound = function (src, id, data, basePath, defaultPlayProps) { var loadItem = {src: src, id: id, data:data, defaultPlayProps:defaultPlayProps}; if (src instanceof Object && src.src) { basePath = id; loadItem = src; } loadItem = createjs.LoadItem.create(loadItem); loadItem.path = basePath; if (basePath != null && !(loadItem.src instanceof Object)) {loadItem.src = basePath + loadItem.src;} var loader = s._registerSound(loadItem); if(!loader) {return false;} if (!s._preloadHash[loadItem.src]) { s._preloadHash[loadItem.src] = [];} s._preloadHash[loadItem.src].push(loadItem); if (s._preloadHash[loadItem.src].length == 1) { // OJR note this will disallow reloading a sound if loading fails or the source changes loader.on("complete", this._handleLoadComplete, this); loader.on("error", this._handleLoadError, this); s.activePlugin.preload(loader); } else { if (s._preloadHash[loadItem.src][0] == true) {return true;} } return loadItem; }; /** * Register an array of audio files for loading and future playback in Sound. It is recommended to register all * sounds that need to be played back in order to properly prepare and preload them. Sound does internal preloading * when required. * * <h4>Example</h4> * * var assetPath = "./myAudioPath/"; * var sounds = [ * {src:"asset0.ogg", id:"example"}, * {src:"asset1.ogg", id:"1", data:6}, * {src:"asset2.mp3", id:"works"} * {src:{mp3:"path1/asset3.mp3", ogg:"path2/asset3NoExtension"}, id:"better"} * ]; * createjs.Sound.alternateExtensions = ["mp3"]; // if the passed extension is not supported, try this extension * createjs.Sound.on("fileload", handleLoad); // call handleLoad when each sound loads * createjs.Sound.registerSounds(sounds, assetPath); * * @method registerSounds * @param {Array} sounds An array of objects to load. Objects are expected to be in the format needed for * {{#crossLink "Sound/registerSound"}}{{/crossLink}}: <code>{src:srcURI, id:ID, data:Data}</code> * with "id" and "data" being optional. * You can also pass an object with path and manifest properties, where path is a basePath and manifest is an array of objects to load. * Note id is required if src is an object with extension labeled src properties. * @param {string} basePath Set a path that will be prepended to each src when loading. When creating, playing, or removing * audio that was loaded with a basePath by src, the basePath must be included. * @return {Object} An array of objects with the modified values that were passed in, which defines each sound. * Like registerSound, it will return false for any values when the source cannot be parsed or if no plugins can be initialized. * Also, it will return true for any values when the source is already loaded. * @static * @since 0.6.0 */ s.registerSounds = function (sounds, basePath) { var returnValues = []; if (sounds.path) { if (!basePath) { basePath = sounds.path; } else { basePath = basePath + sounds.path; } sounds = sounds.manifest; // TODO document this feature } for (var i = 0, l = sounds.length; i < l; i++) { returnValues[i] = createjs.Sound.registerSound(sounds[i].src, sounds[i].id, sounds[i].data, basePath, sounds[i].defaultPlayProps); } return returnValues; }; /** * Remove a sound that has been registered with {{#crossLink "Sound/registerSound"}}{{/crossLink}} or * {{#crossLink "Sound/registerSounds"}}{{/crossLink}}. * <br />Note this will stop playback on active instances playing this sound before deleting them. * <br />Note if you passed in a basePath, you need to pass it or prepend it to the src here. * * <h4>Example</h4> * * createjs.Sound.removeSound("myID"); * createjs.Sound.removeSound("myAudioBasePath/mySound.ogg"); * createjs.Sound.removeSound("myPath/myOtherSound.mp3", "myBasePath/"); * createjs.Sound.removeSound({mp3:"musicNoExtension", ogg:"music.ogg"}, "myBasePath/"); * * @method removeSound * @param {String | Object} src The src or ID of the audio, or an Object with a "src" property, or an Object with multiple extension labeled src properties. * @param {string} basePath Set a path that will be prepended to each src when removing. * @return {Boolean} True if sound is successfully removed. * @static * @since 0.4.1 */ s.removeSound = function(src, basePath) { if (s.activePlugin == null) {return false;} if (src instanceof Object && src.src) {src = src.src;} var details; if (src instanceof Object) { details = s._parseSrc(src); } else { src = s._getSrcById(src).src; details = s._parsePath(src); } if (details == null) {return false;} src = details.src; if (basePath != null) {src = basePath + src;} for(var prop in s._idHash){ if(s._idHash[prop].src == src) { delete(s._idHash[prop]); } } // clear from SoundChannel, which also stops and deletes all instances SoundChannel.removeSrc(src); delete(s._preloadHash[src]); s.activePlugin.removeSound(src); return true; }; /** * Remove an array of audio files that have been registered with {{#crossLink "Sound/registerSound"}}{{/crossLink}} or * {{#crossLink "Sound/registerSounds"}}{{/crossLink}}. * <br />Note this will stop playback on active instances playing this audio before deleting them. * <br />Note if you passed in a basePath, you need to pass it or prepend it to the src here. * * <h4>Example</h4> * * assetPath = "./myPath/"; * var sounds = [ * {src:"asset0.ogg", id:"example"}, * {src:"asset1.ogg", id:"1", data:6}, * {src:"asset2.mp3", id:"works"} * ]; * createjs.Sound.removeSounds(sounds, assetPath); * * @method removeSounds * @param {Array} sounds An array of objects to remove. Objects are expected to be in the format needed for * {{#crossLink "Sound/removeSound"}}{{/crossLink}}: <code>{srcOrID:srcURIorID}</code>. * You can also pass an object with path and manifest properties, where path is a basePath and manifest is an array of objects to remove. * @param {string} basePath Set a path that will be prepended to each src when removing. * @return {Object} An array of Boolean values representing if the sounds with the same array index were * successfully removed. * @static * @since 0.4.1 */ s.removeSounds = function (sounds, basePath) { var returnValues = []; if (sounds.path) { if (!basePath) { basePath = sounds.path; } else { basePath = basePath + sounds.path; } sounds = sounds.manifest; } for (var i = 0, l = sounds.length; i < l; i++) { returnValues[i] = createjs.Sound.removeSound(sounds[i].src, basePath); } return returnValues; }; /** * Remove all sounds that have been registered with {{#crossLink "Sound/registerSound"}}{{/crossLink}} or * {{#crossLink "Sound/registerSounds"}}{{/crossLink}}. * <br />Note this will stop playback on all active sound instances before deleting them. * * <h4>Example</h4> * * createjs.Sound.removeAllSounds(); * * @method removeAllSounds * @static * @since 0.4.1 */ s.removeAllSounds = function() { s._idHash = {}; s._preloadHash = {}; SoundChannel.removeAll(); if (s.activePlugin) {s.activePlugin.removeAllSounds();} }; /** * Check if a source has been loaded by internal preloaders. This is necessary to ensure that sounds that are * not completed preloading will not kick off a new internal preload if they are played. * * <h4>Example</h4> * * var mySound = "assetPath/asset0.ogg"; * if(createjs.Sound.loadComplete(mySound) { * createjs.Sound.play(mySound); * } * * @method loadComplete * @param {String} src The src or id that is being loaded. * @return {Boolean} If the src is already loaded. * @since 0.4.0 * @static */ s.loadComplete = function (src) { if (!s.isReady()) { return false; } var details = s._parsePath(src); if (details) { src = s._getSrcById(details.src).src; } else { src = s._getSrcById(src).src; } if(s._preloadHash[src] == undefined) {return false;} return (s._preloadHash[src][0] == true); // src only loads once, so if it's true for the first it's true for all }; /** * Parse the path of a sound. Alternate extensions will be attempted in order if the * current extension is not supported * @method _parsePath * @param {String} value The path to an audio source. * @return {Object} A formatted object that can be registered with the {{#crossLink "Sound/activePlugin:property"}}{{/crossLink}} * and returned to a preloader like <a href="http://preloadjs.com" target="_blank">PreloadJS</a>. * @private * @static */ s._parsePath = function (value) { if (typeof(value) != "string") {value = value.toString();} var match = value.match(s.FILE_PATTERN); if (match == null) {return false;} var name = match[4]; var ext = match[5]; var c = s.capabilities; var i = 0; while (!c[ext]) { ext = s.alternateExtensions[i++]; if (i > s.alternateExtensions.length) { return null;} // no extensions are supported } value = value.replace("."+match[5], "."+ext); var ret = {name:name, src:value, extension:ext}; return ret; }; /** * Parse the path of a sound based on properties of src matching with supported extensions. * Returns false if none of the properties are supported * @method _parseSrc * @param {Object} value The paths to an audio source, indexed by extension type. * @return {Object} A formatted object that can be registered with the {{#crossLink "Sound/activePlugin:property"}}{{/crossLink}} * and returned to a preloader like <a href="http://preloadjs.com" target="_blank">PreloadJS</a>. * @private * @static */ s._parseSrc = function (value) { var ret = {name:undefined, src:undefined, extension:undefined}; var c = s.capabilities; for (var prop in value) { if(value.hasOwnProperty(prop) && c[prop]) { ret.src = value[prop]; ret.extension = prop; break; } } if (!ret.src) {return false;} // no matches var i = ret.src.lastIndexOf("/"); if (i != -1) { ret.name = ret.src.slice(i+1); } else { ret.name = ret.src; } return ret; }; /* --------------- Static API. --------------- */ /** * Play a sound and get a {{#crossLink "AbstractSoundInstance"}}{{/crossLink}} to control. If the sound fails to * play, an AbstractSoundInstance will still be returned, and have a playState of {{#crossLink "Sound/PLAY_FAILED:property"}}{{/crossLink}}. * Note that even on sounds with failed playback, you may still be able to call the {{#crossLink "AbstractSoundInstance/play"}}{{/crossLink}}, * method, since the failure could be due to lack of available channels. If the src does not have a supported * extension or if there is no available plugin, a default AbstractSoundInstance will still be returned, which will * not play any audio, but will not generate errors. * * <h4>Example</h4> * * createjs.Sound.on("fileload", handleLoad); * createjs.Sound.registerSound("myAudioPath/mySound.mp3", "myID", 3); * function handleLoad(event) { * createjs.Sound.play("myID"); * // store off AbstractSoundInstance for controlling * var myInstance = createjs.Sound.play("myID", {interrupt: createjs.Sound.INTERRUPT_ANY, loop:-1}); * } * * NOTE: To create an audio sprite that has not already been registered, both startTime and duration need to be set. * This is only when creating a new audio sprite, not when playing using the id of an already registered audio sprite. * * @method play * @param {String} src The src or ID of the audio. * @param {Object | PlayPropsConfig} props A PlayPropsConfig instance, or an object that contains the parameters to * play a sound. See the {{#crossLink "PlayPropsConfig"}}{{/crossLink}} for more info. * @return {AbstractSoundInstance} A {{#crossLink "AbstractSoundInstance"}}{{/crossLink}} that can be controlled * after it is created. * @static */ s.play = function (src, props) { var playProps = createjs.PlayPropsConfig.create(props); var instance = s.createInstance(src, playProps.startTime, playProps.duration); var ok = s._playInstance(instance, playProps); if (!ok) {instance._playFailed();} return instance; }; /** * Creates a {{#crossLink "AbstractSoundInstance"}}{{/crossLink}} using the passed in src. If the src does not have a * supported extension or if there is no available plugin, a default AbstractSoundInstance will be returned that can be * called safely but does nothing. * * <h4>Example</h4> * * var myInstance = null; * createjs.Sound.on("fileload", handleLoad); * createjs.Sound.registerSound("myAudioPath/mySound.mp3", "myID", 3); * function handleLoad(event) { * myInstance = createjs.Sound.createInstance("myID"); * // alternately we could call the following * myInstance = createjs.Sound.createInstance("myAudioPath/mySound.mp3"); * } * * NOTE to create an audio sprite that has not already been registered, both startTime and duration need to be set. * This is only when creating a new audio sprite, not when playing using the id of an already registered audio sprite. * * @method createInstance * @param {String} src The src or ID of the audio. * @param {Number} [startTime=null] To create an audio sprite (with duration), the initial offset to start playback and loop from, in milliseconds. * @param {Number} [duration=null] To create an audio sprite (with startTime), the amount of time to play the clip for, in milliseconds. * @return {AbstractSoundInstance} A {{#crossLink "AbstractSoundInstance"}}{{/crossLink}} that can be controlled after it is created. * Unsupported extensions will return the default AbstractSoundInstance. * @since 0.4.0 * @static */ s.createInstance = function (src, startTime, duration) { if (!s.initializeDefaultPlugins()) { return new createjs.DefaultSoundInstance(src, startTime, duration); } var defaultPlayProps = s._defaultPlayPropsHash[src]; // for audio sprites, which create and store defaults by id src = s._getSrcById(src); var details = s._parsePath(src.src); var instance = null; if (details != null && details.src != null) { SoundChannel.create(details.src); if (startTime == null) { startTime = src.startTime; } instance = s.activePlugin.create(details.src, startTime, duration || src.duration); defaultPlayProps = defaultPlayProps || s._defaultPlayPropsHash[details.src]; if (defaultPlayProps) { instance.applyPlayProps(defaultPlayProps); } } else { instance = new createjs.DefaultSoundInstance(src, startTime, duration); } instance.uniqueId = s._lastID++; return instance; }; /** * Stop all audio (global stop). Stopped audio is reset, and not paused. To play audio that has been stopped, * call AbstractSoundInstance {{#crossLink "AbstractSoundInstance/play"}}{{/crossLink}}. * * <h4>Example</h4> * * createjs.Sound.stop(); * * @method stop * @static */ s.stop = function () { var instances = this._instances; for (var i = instances.length; i--; ) { instances[i].stop(); // NOTE stop removes instance from this._instances } }; /** * Set the default playback properties for all new SoundInstances of the passed in src or ID. * See {{#crossLink "PlayPropsConfig"}}{{/crossLink}} for available properties. * * @method setDefaultPlayProps * @param {String} src The src or ID used to register the audio. * @param {Object | PlayPropsConfig} playProps The playback properties you would like to set. * @since 0.6.1 */ s.setDefaultPlayProps = function(src, playProps) { src = s._getSrcById(src); s._defaultPlayPropsHash[s._parsePath(src.src).src] = createjs.PlayPropsConfig.create(playProps); }; /** * Get the default playback properties for the passed in src or ID. These properties are applied to all * new SoundInstances. Returns null if default does not exist. * * @method getDefaultPlayProps * @param {String} src The src or ID used to register the audio. * @returns {PlayPropsConfig} returns an existing PlayPropsConfig or null if one does not exist * @since 0.6.1 */ s.getDefaultPlayProps = function(src) { src = s._getSrcById(src); return s._defaultPlayPropsHash[s._parsePath(src.src).src]; }; /* --------------- Internal methods --------------- */ /** * Play an instance. This is called by the static API, as well as from plugins. This allows the core class to * control delays. * @method _playInstance * @param {AbstractSoundInstance} instance The {{#crossLink "AbstractSoundInstance"}}{{/crossLink}} to start playing. * @param {PlayPropsConfig} playProps A PlayPropsConfig object. * @return {Boolean} If the sound can start playing. Sounds that fail immediately will return false. Sounds that * have a delay will return true, but may still fail to play. * @private * @static */ s._playInstance = function (instance, playProps) { var defaultPlayProps = s._defaultPlayPropsHash[instance.src] || {}; if (playProps.interrupt == null) {playProps.interrupt = defaultPlayProps.interrupt || s.defaultInterruptBehavior}; if (playProps.delay == null) {playProps.delay = defaultPlayProps.delay || 0;} if (playProps.offset == null) {playProps.offset = instance.position;} if (playProps.loop == null) {playProps.loop = instance.loop;} if (playProps.volume == null) {playProps.volume = instance.volume;} if (playProps.pan == null) {playProps.pan = instance.pan;} if (playProps.delay == 0) { var ok = s._beginPlaying(instance, playProps); if (!ok) {return false;} } else { //Note that we can't pass arguments to proxy OR setTimeout (IE only), so just wrap the function call. // OJR WebAudio may want to handle this differently, so it might make sense to move this functionality into the plugins in the future var delayTimeoutId = setTimeout(function () { s._beginPlaying(instance, playProps); }, playProps.delay); instance.delayTimeoutId = delayTimeoutId; } this._instances.push(instance); return true; }; /** * Begin playback. This is called immediately or after delay by {{#crossLink "Sound/playInstance"}}{{/crossLink}}. * @method _beginPlaying * @param {AbstractSoundInstance} instance A {{#crossLink "AbstractSoundInstance"}}{{/crossLink}} to begin playback. * @param {PlayPropsConfig} playProps A PlayPropsConfig object. * @return {Boolean} If the sound can start playing. If there are no available channels, or the instance fails to * start, this will return false. * @private * @static */ s._beginPlaying = function (instance, playProps) { if (!SoundChannel.add(instance, playProps.interrupt)) { return false; } var result = instance._beginPlaying(playProps); if (!result) { var index = createjs.indexOf(this._instances, instance); if (index > -1) {this._instances.splice(index, 1);} return false; } return true; }; /** * Get the source of a sound via the ID passed in with a register call. If no ID is found the value is returned * instead. * @method _getSrcById * @param {String} value The ID the sound was registered with. * @return {String} The source of the sound if it has been registered with this ID or the value that was passed in. * @private * @static */ s._getSrcById = function (value) { return s._idHash[value] || {src: value}; }; /** * A sound has completed playback, been interrupted, failed, or been stopped. This method removes the instance from * Sound management. It will be added again, if the sound re-plays. Note that this method is called from the * instances themselves. * @method _playFinished * @param {AbstractSoundInstance} instance The instance that finished playback. * @private * @static */ s._playFinished = function (instance) { SoundChannel.remove(instance); var index = createjs.indexOf(this._instances, instance); if (index > -1) {this._instances.splice(index, 1);} // OJR this will always be > -1, there is no way for an instance to exist without being added to this._instances }; createjs.Sound = Sound; /** * An internal class that manages the number of active {{#crossLink "AbstractSoundInstance"}}{{/crossLink}} instances for * each sound type. This method is only used internally by the {{#crossLink "Sound"}}{{/crossLink}} class. * * The number of sounds is artificially limited by Sound in order to prevent over-saturation of a * single sound, as well as to stay within hardware limitations, although the latter may disappear with better * browser support. * * When a sound is played, this class ensures that there is an available instance, or interrupts an appropriate * sound that is already playing. * #class SoundChannel * @param {String} src The source of the instances * @param {Number} [max=1] The number of instances allowed * @constructor * @protected */ function SoundChannel(src, max) { this.init(src, max); } /* ------------ Static API ------------ */ /** * A hash of channel instances indexed by source. * #property channels * @type {Object} * @static */ SoundChannel.channels = {}; /** * Create a sound channel. Note that if the sound channel already exists, this will fail. * #method create * @param {String} src The source for the channel * @param {Number} max The maximum amount this channel holds. The default is {{#crossLink "SoundChannel.maxDefault"}}{{/crossLink}}. * @return {Boolean} If the channels were created. * @static */ SoundChannel.create = function (src, max) { var channel = SoundChannel.get(src); if (channel == null) { SoundChannel.channels[src] = new SoundChannel(src, max); return true; } return false; }; /** * Delete a sound channel, stop and delete all related instances. Note that if the sound channel does not exist, this will fail. * #method remove * @param {String} src The source for the channel * @return {Boolean} If the channels were deleted. * @static */ SoundChannel.removeSrc = function (src) { var channel = SoundChannel.get(src); if (channel == null) {return false;} channel._removeAll(); // this stops and removes all active instances delete(SoundChannel.channels[src]); return true; }; /** * Delete all sound channels, stop and delete all related instances. * #method removeAll * @static */ SoundChannel.removeAll = function () { for(var channel in SoundChannel.channels) { SoundChannel.channels[channel]._removeAll(); // this stops and removes all active instances } SoundChannel.channels = {}; }; /** * Add an instance to a sound channel. * #method add * @param {AbstractSoundInstance} instance The instance to add to the channel * @param {String} interrupt The interrupt value to use. Please see the {{#crossLink "Sound/play"}}{{/crossLink}} * for details on interrupt modes. * @return {Boolean} The success of the method call. If the channel is full, it will return false. * @static */ SoundChannel.add = function (instance, interrupt) { var channel = SoundChannel.get(instance.src); if (channel == null) {return false;} return channel._add(instance, interrupt); }; /** * Remove an instance from the channel. * #method remove * @param {AbstractSoundInstance} instance The instance to remove from the channel * @return The success of the method call. If there is no channel, it will return false. * @static */ SoundChannel.remove = function (instance) { var channel = SoundChannel.get(instance.src); if (channel == null) {return false;} channel._remove(instance); return true; }; /** * Get the maximum number of sounds you can have in a channel. * #method maxPerChannel * @return {Number} The maximum number of sounds you can have in a channel. */ SoundChannel.maxPerChannel = function () { return p.maxDefault; }; /** * Get a channel instance by its src. * #method get * @param {String} src The src to use to look up the channel * @static */ SoundChannel.get = function (src) { return SoundChannel.channels[src]; }; var p = SoundChannel.prototype; p.constructor = SoundChannel; /** * The source of the channel. * #property src * @type {String} */ p.src = null; /** * The maximum number of instances in this channel. -1 indicates no limit * #property max * @type {Number} */ p.max = null; /** * The default value to set for max, if it isn't passed in. Also used if -1 is passed. * #property maxDefault * @type {Number} * @default 100 * @since 0.4.0 */ p.maxDefault = 100; /** * The current number of active instances. * #property length * @type {Number} */ p.length = 0; /** * Initialize the channel. * #method init * @param {String} src The source of the channel * @param {Number} max The maximum number of instances in the channel * @protected */ p.init = function (src, max) { this.src = src; this.max = max || this.maxDefault; if (this.max == -1) {this.max = this.maxDefault;} this._instances = []; }; /** * Get an instance by index. * #method get * @param {Number} index The index to return. * @return {AbstractSoundInstance} The AbstractSoundInstance at a specific instance. */ p._get = function (index) { return this._instances[index]; }; /** * Add a new instance to the channel. * #method add * @param {AbstractSoundInstance} instance The instance to add. * @return {Boolean} The success of the method call. If the channel is full, it will return false. */ p._add = function (instance, interrupt) { if (!this._getSlot(interrupt, instance)) {return false;} this._instances.push(instance); this.length++; return true; }; /** * Remove an instance from the channel, either when it has finished playing, or it has been interrupted. * #method remove * @param {AbstractSoundInstance} instance The instance to remove * @return {Boolean} The success of the remove call. If the instance is not found in this channel, it will * return false. */ p._remove = function (instance) { var index = createjs.indexOf(this._instances, instance); if (index == -1) {return false;} this._instances.splice(index, 1); this.length--; return true; }; /** * Stop playback and remove all instances from the channel. Usually in response to a delete call. * #method removeAll */ p._removeAll = function () { // Note that stop() removes the item from the list for (var i=this.length-1; i>=0; i--) { this._instances[i].stop(); } }; /** * Get an available slot depending on interrupt value and if slots are available. * #method getSlot * @param {String} interrupt The interrupt value to use. * @param {AbstractSoundInstance} instance The sound instance that will go in the channel if successful. * @return {Boolean} Determines if there is an available slot. Depending on the interrupt mode, if there are no slots, * an existing AbstractSoundInstance may be interrupted. If there are no slots, this method returns false. */ p._getSlot = function (interrupt, instance) { var target, replacement; if (interrupt != Sound.INTERRUPT_NONE) { // First replacement candidate replacement = this._get(0); if (replacement == null) { return true; } } for (var i = 0, l = this.max; i < l; i++) { target = this._get(i); // Available Space if (target == null) { return true; } // Audio is complete or not playing if (target.playState == Sound.PLAY_FINISHED || target.playState == Sound.PLAY_INTERRUPTED || target.playState == Sound.PLAY_FAILED) { replacement = target; break; } if (interrupt == Sound.INTERRUPT_NONE) { continue; } // Audio is a better candidate than the current target, according to playhead if ((interrupt == Sound.INTERRUPT_EARLY && target.position < replacement.position) || (interrupt == Sound.INTERRUPT_LATE && target.position > replacement.position)) { replacement = target; } } if (replacement != null) { replacement._interrupt(); this._remove(replacement); return true; } return false; }; p.toString = function () { return "[Sound SoundChannel]"; }; // do not add SoundChannel to namespace }());