- /*
- * WebAudioPlugin
- * 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 || {};
-
- (function () {
-
- "use strict";
-
- /**
- * Play sounds using Web Audio in the browser. The WebAudioPlugin is currently the default plugin, and will be used
- * anywhere that it is supported. To change plugin priority, check out the Sound API
- * {{#crossLink "Sound/registerPlugins"}}{{/crossLink}} method.
-
- * <h4>Known Browser and OS issues for Web Audio</h4>
- * <b>Firefox 25</b>
- * <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 FireFox-supported type (i.e. ogg) as the default
- * extension, until this bug is resolved
- * </li>
- *
- * <b>Webkit (Chrome and Safari)</b>
- * <li>
- * AudioNode.disconnect does not always seem to work. This can cause the file size to grow over time if you
- * are playing a lot of audio files.
- * </li>
- *
- * <b>iOS 6 limitations</b>
- * <ul>
- * <li>
- * Sound is initially muted and will only unmute through play being called inside a user initiated event
- * (touch/click). Please read the mobile playback notes in the the {{#crossLink "Sound"}}{{/crossLink}}
- * class for a full overview of the limitations, and how to get around them.
- * </li>
- * <li>
- * A bug exists that will distort un-cached audio when a video element is present in the DOM. You can avoid
- * this bug by ensuring the audio and video audio share the same sample rate.
- * </li>
- * </ul>
- * @class WebAudioPlugin
- * @extends AbstractPlugin
- * @constructor
- * @since 0.4.0
- */
- function WebAudioPlugin() {
- this.AbstractPlugin_constructor();
-
-
- // Private Properties
- /**
- * 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
- */
- this._panningModel = s._panningModel;;
-
- /**
- * The web audio context, which WebAudio uses to play audio. All nodes that interact with the WebAudioPlugin
- * need to be created within this context.
- * @property context
- * @type {AudioContext}
- */
- this.context = s.context;
-
- /**
- * A DynamicsCompressorNode, which is used to improve sound quality and prevent audio distortion.
- * It is connected to <code>context.destination</code>.
- *
- * Can be accessed by advanced users through createjs.Sound.activePlugin.dynamicsCompressorNode.
- * @property dynamicsCompressorNode
- * @type {AudioNode}
- */
- this.dynamicsCompressorNode = this.context.createDynamicsCompressor();
- this.dynamicsCompressorNode.connect(this.context.destination);
-
- /**
- * A GainNode for controlling master volume. It is connected to {{#crossLink "WebAudioPlugin/dynamicsCompressorNode:property"}}{{/crossLink}}.
- *
- * Can be accessed by advanced users through createjs.Sound.activePlugin.gainNode.
- * @property gainNode
- * @type {AudioGainNode}
- */
- this.gainNode = this.context.createGain();
- this.gainNode.connect(this.dynamicsCompressorNode);
- createjs.WebAudioSoundInstance.destinationNode = this.gainNode;
-
- this._capabilities = s._capabilities;
-
- this._loaderClass = createjs.WebAudioLoader;
- this._soundInstanceClass = createjs.WebAudioSoundInstance;
-
- this._addPropsToClasses();
- }
- var p = createjs.extend(WebAudioPlugin, createjs.AbstractPlugin);
-
- // Static Properties
- var s = WebAudioPlugin;
- /**
- * The capabilities of the plugin. This is generated via the {{#crossLink "WebAudioPlugin/_generateCapabilities:method"}}{{/crossLink}}
- * method and is used internally.
- * @property _capabilities
- * @type {Object}
- * @default null
- * @private
- * @static
- */
- s._capabilities = 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}
- * @private
- * @static
- */
- s._panningModel = "equalpower";
-
- /**
- * The web audio context, which WebAudio uses to play audio. All nodes that interact with the WebAudioPlugin
- * need to be created within this context.
- *
- * Advanced users can set this to an existing context, but <b>must</b> do so before they call
- * {{#crossLink "Sound/registerPlugins"}}{{/crossLink}} or {{#crossLink "Sound/initializeDefaultPlugins"}}{{/crossLink}}.
- *
- * @property context
- * @type {AudioContext}
- * @static
- */
- s.context = null;
-
- /**
- * The scratch buffer that will be assigned to the buffer property of a source node on close.
- * Works around an iOS Safari bug: https://github.com/CreateJS/SoundJS/issues/102
- *
- * Advanced users can set this to an existing source node, but <b>must</b> do so before they call
- * {{#crossLink "Sound/registerPlugins"}}{{/crossLink}} or {{#crossLink "Sound/initializeDefaultPlugins"}}{{/crossLink}}.
- *
- * @property _scratchBuffer
- * @type {AudioBuffer}
- * @private
- * @static
- */
- s._scratchBuffer = null;
-
- /**
- * Indicated whether audio on iOS has been unlocked, which requires a touchend/mousedown event that plays an
- * empty sound.
- * @property _unlocked
- * @type {boolean}
- * @since 0.6.2
- * @private
- */
- s._unlocked = false;
-
- /**
- * The default sample rate used when checking for iOS compatibility. See {{#crossLink "WebAudioPlugin/_createAudioContext"}}{{/crossLink}}.
- * @property DEFAULT_SAMPLE_REATE
- * @type {number}
- * @default 44100
- * @static
- */
- s.DEFAULT_SAMPLE_RATE = 44100;
-
- // Static Public Methods
- /**
- * Determine if the plugin can be used in the current browser/OS.
- * @method isSupported
- * @return {Boolean} If the plugin can be initialized.
- * @static
- */
- s.isSupported = function () {
- // check if this is some kind of mobile device, Web Audio works with local protocol under PhoneGap and it is unlikely someone is trying to run a local file
- var isMobilePhoneGap = createjs.BrowserDetect.isIOS || createjs.BrowserDetect.isAndroid || createjs.BrowserDetect.isBlackberry;
- // OJR isMobile may be redundant with _isFileXHRSupported available. Consider removing.
- if (location.protocol == "file:" && !isMobilePhoneGap && !this._isFileXHRSupported()) { return false; } // Web Audio requires XHR, which is not usually available locally
- s._generateCapabilities();
- if (s.context == null) {return false;}
- return true;
- };
-
- /**
- * Plays an empty sound in the web audio context. This is used to enable web audio on iOS devices, as they
- * require the first sound to be played inside of a user initiated event (touch/click). This is called when
- * {{#crossLink "WebAudioPlugin"}}{{/crossLink}} is initialized (by Sound {{#crossLink "Sound/initializeDefaultPlugins"}}{{/crossLink}}
- * for example).
- *
- * <h4>Example</h4>
- *
- * function handleTouch(event) {
- * createjs.WebAudioPlugin.playEmptySound();
- * }
- *
- * @method playEmptySound
- * @static
- * @since 0.4.1
- */
- s.playEmptySound = function() {
- if (s.context == null) {return;}
- var source = s.context.createBufferSource();
- source.buffer = s._scratchBuffer;
- source.connect(s.context.destination);
- source.start(0, 0, 0);
- };
-
-
- // Static Private Methods
- /**
- * Determine if XHR is supported, which is necessary for web audio.
- * @method _isFileXHRSupported
- * @return {Boolean} If XHR is supported.
- * @since 0.4.2
- * @private
- * @static
- */
- s._isFileXHRSupported = function() {
- // it's much easier to detect when something goes wrong, so let's start optimistically
- var supported = true;
-
- var xhr = new XMLHttpRequest();
- try {
- xhr.open("GET", "WebAudioPluginTest.fail", false); // loading non-existant file triggers 404 only if it could load (synchronous call)
- } catch (error) {
- // catch errors in cases where the onerror is passed by
- supported = false;
- return supported;
- }
- xhr.onerror = function() { supported = false; }; // cause irrelevant
- // with security turned off, we can get empty success results, which is actually a failed read (status code 0?)
- xhr.onload = function() { supported = this.status == 404 || (this.status == 200 || (this.status == 0 && this.response != "")); };
- try {
- xhr.send();
- } catch (error) {
- // catch errors in cases where the onerror is passed by
- supported = false;
- }
-
- return supported;
- };
-
- /**
- * 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) {return;}
- // Web Audio can be in any formats supported by the audio element, from http://www.w3.org/TR/webaudio/#AudioContext-section
- var t = document.createElement("audio");
- if (t.canPlayType == null) {return null;}
-
- if (s.context == null) {
- s.context = s._createAudioContext();
- if (s.context == null) { return null; }
- }
- if (s._scratchBuffer == null) {
- s._scratchBuffer = s.context.createBuffer(1, 1, 22050);
- }
-
- s._compatibilitySetUp();
-
- // Listen for document level clicks to unlock WebAudio on iOS. See the _unlock method.
- if ("ontouchstart" in window && s.context.state != "running") {
- s._unlock(); // When played inside of a touch event, this will enable audio on iOS immediately.
- document.addEventListener("mousedown", s._unlock, true);
- document.addEventListener("touchstart", s._unlock, true);
- document.addEventListener("touchend", s._unlock, true);
- }
-
- s._capabilities = {
- panning:true,
- 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
-
- // 0=no output, 1=mono, 2=stereo, 4=surround, 6=5.1 surround.
- // See http://www.w3.org/TR/webaudio/#AudioChannelSplitter for more details on channels.
- if (s.context.destination.numberOfChannels < 2) {
- s._capabilities.panning = false;
- }
- };
-
- /**
- * Create an audio context for the sound.
- *
- * This method handles both vendor prefixes (specifically webkit support), as well as a case on iOS where
- * audio played with a different sample rate may play garbled when first started. The default sample rate is
- * 44,100, however it can be changed using the {{#crossLink "WebAudioPlugin/DEFAULT_SAMPLE_RATE:property"}}{{/crossLink}}.
- * @method _createAudioContext
- * @return {AudioContext | webkitAudioContext}
- * @private
- * @static
- * @since 1.0.0
- */
- s._createAudioContext = function() {
- // Slightly modified version of https://github.com/Jam3/ios-safe-audio-context
- // Resolves issues with first-run contexts playing garbled on iOS.
- var AudioCtor = (window.AudioContext || window.webkitAudioContext);
- if (AudioCtor == null) { return null; }
- var context = new AudioCtor();
-
- // Check if hack is necessary. Only occurs in iOS6+ devices
- // and only when you first boot the iPhone, or play a audio/video
- // with a different sample rate
- if (/(iPhone|iPad)/i.test(navigator.userAgent)
- && context.sampleRate !== s.DEFAULT_SAMPLE_RATE) {
- var buffer = context.createBuffer(1, 1, s.DEFAULT_SAMPLE_RATE),
- dummy = context.createBufferSource();
- dummy.buffer = buffer;
- dummy.connect(context.destination);
- dummy.start(0);
- dummy.disconnect();
- context.close() // dispose old context
-
- context = new AudioCtor();
- }
- return context;
- }
-
- /**
- * Set up compatibility if only deprecated web audio calls are supported.
- * See http://www.w3.org/TR/webaudio/#DeprecationNotes
- * Needed so we can support new browsers that don't support deprecated calls (Firefox) as well as old browsers that
- * don't support new calls.
- *
- * @method _compatibilitySetUp
- * @static
- * @private
- * @since 0.4.2
- */
- s._compatibilitySetUp = function() {
- s._panningModel = "equalpower";
- //assume that if one new call is supported, they all are
- if (s.context.createGain) { return; }
-
- // simple name change, functionality the same
- s.context.createGain = s.context.createGainNode;
-
- // source node, add to prototype
- var audioNode = s.context.createBufferSource();
- audioNode.__proto__.start = audioNode.__proto__.noteGrainOn; // note that noteGrainOn requires all 3 parameters
- audioNode.__proto__.stop = audioNode.__proto__.noteOff;
-
- // panningModel
- s._panningModel = 0;
- };
-
- /**
- * Try to unlock audio on iOS. This is triggered from either WebAudio plugin setup (which will work if inside of
- * a `mousedown` or `touchend` event stack), or the first document touchend/mousedown event. If it fails (touchend
- * will fail if the user presses for too long, indicating a scroll event instead of a click event.
- *
- * Note that earlier versions of iOS supported `touchstart` for this, but iOS9 removed this functionality. Adding
- * a `touchstart` event to support older platforms may preclude a `mousedown` even from getting fired on iOS9, so we
- * stick with `mousedown` and `touchend`.
- * @method _unlock
- * @since 0.6.2
- * @private
- */
- s._unlock = function() {
- if (s._unlocked) { return; }
- s.playEmptySound();
- if (s.context.state == "running") {
- document.removeEventListener("mousedown", s._unlock, true);
- document.removeEventListener("touchend", s._unlock, true);
- document.removeEventListener("touchstart", s._unlock, true);
- s._unlocked = true;
- }
- };
-
-
- // Public Methods
- p.toString = function () {
- return "[WebAudioPlugin]";
- };
-
-
- // Private Methods
- /**
- * Set up needed properties on supported classes WebAudioSoundInstance and WebAudioLoader.
- * @method _addPropsToClasses
- * @static
- * @protected
- * @since 0.6.0
- */
- p._addPropsToClasses = function() {
- var c = this._soundInstanceClass;
- c.context = this.context;
- c._scratchBuffer = s._scratchBuffer;
- c.destinationNode = this.gainNode;
- c._panningModel = this._panningModel;
-
- this._loaderClass.context = this.context;
- };
-
-
- /**
- * Set the gain value for master audio. Should not be called externally.
- * @method _updateVolume
- * @protected
- */
- p._updateVolume = function () {
- var newVolume = createjs.Sound._masterMute ? 0 : this._volume;
- if (newVolume != this.gainNode.gain.value) {
- this.gainNode.gain.value = newVolume;
- }
- };
-
- createjs.WebAudioPlugin = createjs.promote(WebAudioPlugin, "AbstractPlugin");
- }());
-
-