SoundJS v1.0.0 API Documentation : soundjs/webaudio/WebAudioPlugin.js

API Documentation for: 1.0.0
Show:

File:WebAudioPlugin.js

  1. /*
  2. * WebAudioPlugin
  3. * Visit http://createjs.com/ for documentation, updates and examples.
  4. *
  5. *
  6. * Copyright (c) 2012 gskinner.com, inc.
  7. *
  8. * Permission is hereby granted, free of charge, to any person
  9. * obtaining a copy of this software and associated documentation
  10. * files (the "Software"), to deal in the Software without
  11. * restriction, including without limitation the rights to use,
  12. * copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. * copies of the Software, and to permit persons to whom the
  14. * Software is furnished to do so, subject to the following
  15. * conditions:
  16. *
  17. * The above copyright notice and this permission notice shall be
  18. * included in all copies or substantial portions of the Software.
  19. *
  20. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  21. * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
  22. * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  23. * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
  24. * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  25. * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  26. * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
  27. * OTHER DEALINGS IN THE SOFTWARE.
  28. */
  29.  
  30. /**
  31. * @module SoundJS
  32. */
  33.  
  34. // namespace:
  35. this.createjs = this.createjs || {};
  36.  
  37. (function () {
  38.  
  39. "use strict";
  40.  
  41. /**
  42. * Play sounds using Web Audio in the browser. The WebAudioPlugin is currently the default plugin, and will be used
  43. * anywhere that it is supported. To change plugin priority, check out the Sound API
  44. * {{#crossLink "Sound/registerPlugins"}}{{/crossLink}} method.
  45.  
  46. * <h4>Known Browser and OS issues for Web Audio</h4>
  47. * <b>Firefox 25</b>
  48. * <li>
  49. * 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>.
  50. * <br />For this reason it is recommended to pass another FireFox-supported type (i.e. ogg) as the default
  51. * extension, until this bug is resolved
  52. * </li>
  53. *
  54. * <b>Webkit (Chrome and Safari)</b>
  55. * <li>
  56. * AudioNode.disconnect does not always seem to work. This can cause the file size to grow over time if you
  57. * are playing a lot of audio files.
  58. * </li>
  59. *
  60. * <b>iOS 6 limitations</b>
  61. * <ul>
  62. * <li>
  63. * Sound is initially muted and will only unmute through play being called inside a user initiated event
  64. * (touch/click). Please read the mobile playback notes in the the {{#crossLink "Sound"}}{{/crossLink}}
  65. * class for a full overview of the limitations, and how to get around them.
  66. * </li>
  67. * <li>
  68. * A bug exists that will distort un-cached audio when a video element is present in the DOM. You can avoid
  69. * this bug by ensuring the audio and video audio share the same sample rate.
  70. * </li>
  71. * </ul>
  72. * @class WebAudioPlugin
  73. * @extends AbstractPlugin
  74. * @constructor
  75. * @since 0.4.0
  76. */
  77. function WebAudioPlugin() {
  78. this.AbstractPlugin_constructor();
  79.  
  80.  
  81. // Private Properties
  82. /**
  83. * Value to set panning model to equal power for WebAudioSoundInstance. Can be "equalpower" or 0 depending on browser implementation.
  84. * @property _panningModel
  85. * @type {Number / String}
  86. * @protected
  87. */
  88. this._panningModel = s._panningModel;;
  89.  
  90. /**
  91. * The web audio context, which WebAudio uses to play audio. All nodes that interact with the WebAudioPlugin
  92. * need to be created within this context.
  93. * @property context
  94. * @type {AudioContext}
  95. */
  96. this.context = s.context;
  97.  
  98. /**
  99. * A DynamicsCompressorNode, which is used to improve sound quality and prevent audio distortion.
  100. * It is connected to <code>context.destination</code>.
  101. *
  102. * Can be accessed by advanced users through createjs.Sound.activePlugin.dynamicsCompressorNode.
  103. * @property dynamicsCompressorNode
  104. * @type {AudioNode}
  105. */
  106. this.dynamicsCompressorNode = this.context.createDynamicsCompressor();
  107. this.dynamicsCompressorNode.connect(this.context.destination);
  108.  
  109. /**
  110. * A GainNode for controlling master volume. It is connected to {{#crossLink "WebAudioPlugin/dynamicsCompressorNode:property"}}{{/crossLink}}.
  111. *
  112. * Can be accessed by advanced users through createjs.Sound.activePlugin.gainNode.
  113. * @property gainNode
  114. * @type {AudioGainNode}
  115. */
  116. this.gainNode = this.context.createGain();
  117. this.gainNode.connect(this.dynamicsCompressorNode);
  118. createjs.WebAudioSoundInstance.destinationNode = this.gainNode;
  119.  
  120. this._capabilities = s._capabilities;
  121.  
  122. this._loaderClass = createjs.WebAudioLoader;
  123. this._soundInstanceClass = createjs.WebAudioSoundInstance;
  124.  
  125. this._addPropsToClasses();
  126. }
  127. var p = createjs.extend(WebAudioPlugin, createjs.AbstractPlugin);
  128.  
  129. // Static Properties
  130. var s = WebAudioPlugin;
  131. /**
  132. * The capabilities of the plugin. This is generated via the {{#crossLink "WebAudioPlugin/_generateCapabilities:method"}}{{/crossLink}}
  133. * method and is used internally.
  134. * @property _capabilities
  135. * @type {Object}
  136. * @default null
  137. * @private
  138. * @static
  139. */
  140. s._capabilities = null;
  141.  
  142. /**
  143. * Value to set panning model to equal power for WebAudioSoundInstance. Can be "equalpower" or 0 depending on browser implementation.
  144. * @property _panningModel
  145. * @type {Number / String}
  146. * @private
  147. * @static
  148. */
  149. s._panningModel = "equalpower";
  150.  
  151. /**
  152. * The web audio context, which WebAudio uses to play audio. All nodes that interact with the WebAudioPlugin
  153. * need to be created within this context.
  154. *
  155. * Advanced users can set this to an existing context, but <b>must</b> do so before they call
  156. * {{#crossLink "Sound/registerPlugins"}}{{/crossLink}} or {{#crossLink "Sound/initializeDefaultPlugins"}}{{/crossLink}}.
  157. *
  158. * @property context
  159. * @type {AudioContext}
  160. * @static
  161. */
  162. s.context = null;
  163.  
  164. /**
  165. * The scratch buffer that will be assigned to the buffer property of a source node on close.
  166. * Works around an iOS Safari bug: https://github.com/CreateJS/SoundJS/issues/102
  167. *
  168. * Advanced users can set this to an existing source node, but <b>must</b> do so before they call
  169. * {{#crossLink "Sound/registerPlugins"}}{{/crossLink}} or {{#crossLink "Sound/initializeDefaultPlugins"}}{{/crossLink}}.
  170. *
  171. * @property _scratchBuffer
  172. * @type {AudioBuffer}
  173. * @private
  174. * @static
  175. */
  176. s._scratchBuffer = null;
  177.  
  178. /**
  179. * Indicated whether audio on iOS has been unlocked, which requires a touchend/mousedown event that plays an
  180. * empty sound.
  181. * @property _unlocked
  182. * @type {boolean}
  183. * @since 0.6.2
  184. * @private
  185. */
  186. s._unlocked = false;
  187.  
  188. /**
  189. * The default sample rate used when checking for iOS compatibility. See {{#crossLink "WebAudioPlugin/_createAudioContext"}}{{/crossLink}}.
  190. * @property DEFAULT_SAMPLE_REATE
  191. * @type {number}
  192. * @default 44100
  193. * @static
  194. */
  195. s.DEFAULT_SAMPLE_RATE = 44100;
  196.  
  197. // Static Public Methods
  198. /**
  199. * Determine if the plugin can be used in the current browser/OS.
  200. * @method isSupported
  201. * @return {Boolean} If the plugin can be initialized.
  202. * @static
  203. */
  204. s.isSupported = function () {
  205. // 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
  206. var isMobilePhoneGap = createjs.BrowserDetect.isIOS || createjs.BrowserDetect.isAndroid || createjs.BrowserDetect.isBlackberry;
  207. // OJR isMobile may be redundant with _isFileXHRSupported available. Consider removing.
  208. if (location.protocol == "file:" && !isMobilePhoneGap && !this._isFileXHRSupported()) { return false; } // Web Audio requires XHR, which is not usually available locally
  209. s._generateCapabilities();
  210. if (s.context == null) {return false;}
  211. return true;
  212. };
  213.  
  214. /**
  215. * Plays an empty sound in the web audio context. This is used to enable web audio on iOS devices, as they
  216. * require the first sound to be played inside of a user initiated event (touch/click). This is called when
  217. * {{#crossLink "WebAudioPlugin"}}{{/crossLink}} is initialized (by Sound {{#crossLink "Sound/initializeDefaultPlugins"}}{{/crossLink}}
  218. * for example).
  219. *
  220. * <h4>Example</h4>
  221. *
  222. * function handleTouch(event) {
  223. * createjs.WebAudioPlugin.playEmptySound();
  224. * }
  225. *
  226. * @method playEmptySound
  227. * @static
  228. * @since 0.4.1
  229. */
  230. s.playEmptySound = function() {
  231. if (s.context == null) {return;}
  232. var source = s.context.createBufferSource();
  233. source.buffer = s._scratchBuffer;
  234. source.connect(s.context.destination);
  235. source.start(0, 0, 0);
  236. };
  237.  
  238.  
  239. // Static Private Methods
  240. /**
  241. * Determine if XHR is supported, which is necessary for web audio.
  242. * @method _isFileXHRSupported
  243. * @return {Boolean} If XHR is supported.
  244. * @since 0.4.2
  245. * @private
  246. * @static
  247. */
  248. s._isFileXHRSupported = function() {
  249. // it's much easier to detect when something goes wrong, so let's start optimistically
  250. var supported = true;
  251.  
  252. var xhr = new XMLHttpRequest();
  253. try {
  254. xhr.open("GET", "WebAudioPluginTest.fail", false); // loading non-existant file triggers 404 only if it could load (synchronous call)
  255. } catch (error) {
  256. // catch errors in cases where the onerror is passed by
  257. supported = false;
  258. return supported;
  259. }
  260. xhr.onerror = function() { supported = false; }; // cause irrelevant
  261. // with security turned off, we can get empty success results, which is actually a failed read (status code 0?)
  262. xhr.onload = function() { supported = this.status == 404 || (this.status == 200 || (this.status == 0 && this.response != "")); };
  263. try {
  264. xhr.send();
  265. } catch (error) {
  266. // catch errors in cases where the onerror is passed by
  267. supported = false;
  268. }
  269.  
  270. return supported;
  271. };
  272.  
  273. /**
  274. * Determine the capabilities of the plugin. Used internally. Please see the Sound API {{#crossLink "Sound/capabilities:property"}}{{/crossLink}}
  275. * method for an overview of plugin capabilities.
  276. * @method _generateCapabilities
  277. * @static
  278. * @private
  279. */
  280. s._generateCapabilities = function () {
  281. if (s._capabilities != null) {return;}
  282. // Web Audio can be in any formats supported by the audio element, from http://www.w3.org/TR/webaudio/#AudioContext-section
  283. var t = document.createElement("audio");
  284. if (t.canPlayType == null) {return null;}
  285.  
  286. if (s.context == null) {
  287. s.context = s._createAudioContext();
  288. if (s.context == null) { return null; }
  289. }
  290. if (s._scratchBuffer == null) {
  291. s._scratchBuffer = s.context.createBuffer(1, 1, 22050);
  292. }
  293.  
  294. s._compatibilitySetUp();
  295.  
  296. // Listen for document level clicks to unlock WebAudio on iOS. See the _unlock method.
  297. if ("ontouchstart" in window && s.context.state != "running") {
  298. s._unlock(); // When played inside of a touch event, this will enable audio on iOS immediately.
  299. document.addEventListener("mousedown", s._unlock, true);
  300. document.addEventListener("touchstart", s._unlock, true);
  301. document.addEventListener("touchend", s._unlock, true);
  302. }
  303.  
  304. s._capabilities = {
  305. panning:true,
  306. volume:true,
  307. tracks:-1
  308. };
  309.  
  310. // determine which extensions our browser supports for this plugin by iterating through Sound.SUPPORTED_EXTENSIONS
  311. var supportedExtensions = createjs.Sound.SUPPORTED_EXTENSIONS;
  312. var extensionMap = createjs.Sound.EXTENSION_MAP;
  313. for (var i = 0, l = supportedExtensions.length; i < l; i++) {
  314. var ext = supportedExtensions[i];
  315. var playType = extensionMap[ext] || ext;
  316. s._capabilities[ext] = (t.canPlayType("audio/" + ext) != "no" && t.canPlayType("audio/" + ext) != "") || (t.canPlayType("audio/" + playType) != "no" && t.canPlayType("audio/" + playType) != "");
  317. } // OJR another way to do this might be canPlayType:"m4a", codex: mp4
  318.  
  319. // 0=no output, 1=mono, 2=stereo, 4=surround, 6=5.1 surround.
  320. // See http://www.w3.org/TR/webaudio/#AudioChannelSplitter for more details on channels.
  321. if (s.context.destination.numberOfChannels < 2) {
  322. s._capabilities.panning = false;
  323. }
  324. };
  325.  
  326. /**
  327. * Create an audio context for the sound.
  328. *
  329. * This method handles both vendor prefixes (specifically webkit support), as well as a case on iOS where
  330. * audio played with a different sample rate may play garbled when first started. The default sample rate is
  331. * 44,100, however it can be changed using the {{#crossLink "WebAudioPlugin/DEFAULT_SAMPLE_RATE:property"}}{{/crossLink}}.
  332. * @method _createAudioContext
  333. * @return {AudioContext | webkitAudioContext}
  334. * @private
  335. * @static
  336. * @since 1.0.0
  337. */
  338. s._createAudioContext = function() {
  339. // Slightly modified version of https://github.com/Jam3/ios-safe-audio-context
  340. // Resolves issues with first-run contexts playing garbled on iOS.
  341. var AudioCtor = (window.AudioContext || window.webkitAudioContext);
  342. if (AudioCtor == null) { return null; }
  343. var context = new AudioCtor();
  344.  
  345. // Check if hack is necessary. Only occurs in iOS6+ devices
  346. // and only when you first boot the iPhone, or play a audio/video
  347. // with a different sample rate
  348. if (/(iPhone|iPad)/i.test(navigator.userAgent)
  349. && context.sampleRate !== s.DEFAULT_SAMPLE_RATE) {
  350. var buffer = context.createBuffer(1, 1, s.DEFAULT_SAMPLE_RATE),
  351. dummy = context.createBufferSource();
  352. dummy.buffer = buffer;
  353. dummy.connect(context.destination);
  354. dummy.start(0);
  355. dummy.disconnect();
  356. context.close() // dispose old context
  357.  
  358. context = new AudioCtor();
  359. }
  360. return context;
  361. }
  362.  
  363. /**
  364. * Set up compatibility if only deprecated web audio calls are supported.
  365. * See http://www.w3.org/TR/webaudio/#DeprecationNotes
  366. * Needed so we can support new browsers that don't support deprecated calls (Firefox) as well as old browsers that
  367. * don't support new calls.
  368. *
  369. * @method _compatibilitySetUp
  370. * @static
  371. * @private
  372. * @since 0.4.2
  373. */
  374. s._compatibilitySetUp = function() {
  375. s._panningModel = "equalpower";
  376. //assume that if one new call is supported, they all are
  377. if (s.context.createGain) { return; }
  378.  
  379. // simple name change, functionality the same
  380. s.context.createGain = s.context.createGainNode;
  381.  
  382. // source node, add to prototype
  383. var audioNode = s.context.createBufferSource();
  384. audioNode.__proto__.start = audioNode.__proto__.noteGrainOn; // note that noteGrainOn requires all 3 parameters
  385. audioNode.__proto__.stop = audioNode.__proto__.noteOff;
  386.  
  387. // panningModel
  388. s._panningModel = 0;
  389. };
  390.  
  391. /**
  392. * Try to unlock audio on iOS. This is triggered from either WebAudio plugin setup (which will work if inside of
  393. * a `mousedown` or `touchend` event stack), or the first document touchend/mousedown event. If it fails (touchend
  394. * will fail if the user presses for too long, indicating a scroll event instead of a click event.
  395. *
  396. * Note that earlier versions of iOS supported `touchstart` for this, but iOS9 removed this functionality. Adding
  397. * a `touchstart` event to support older platforms may preclude a `mousedown` even from getting fired on iOS9, so we
  398. * stick with `mousedown` and `touchend`.
  399. * @method _unlock
  400. * @since 0.6.2
  401. * @private
  402. */
  403. s._unlock = function() {
  404. if (s._unlocked) { return; }
  405. s.playEmptySound();
  406. if (s.context.state == "running") {
  407. document.removeEventListener("mousedown", s._unlock, true);
  408. document.removeEventListener("touchend", s._unlock, true);
  409. document.removeEventListener("touchstart", s._unlock, true);
  410. s._unlocked = true;
  411. }
  412. };
  413.  
  414.  
  415. // Public Methods
  416. p.toString = function () {
  417. return "[WebAudioPlugin]";
  418. };
  419.  
  420.  
  421. // Private Methods
  422. /**
  423. * Set up needed properties on supported classes WebAudioSoundInstance and WebAudioLoader.
  424. * @method _addPropsToClasses
  425. * @static
  426. * @protected
  427. * @since 0.6.0
  428. */
  429. p._addPropsToClasses = function() {
  430. var c = this._soundInstanceClass;
  431. c.context = this.context;
  432. c._scratchBuffer = s._scratchBuffer;
  433. c.destinationNode = this.gainNode;
  434. c._panningModel = this._panningModel;
  435.  
  436. this._loaderClass.context = this.context;
  437. };
  438.  
  439.  
  440. /**
  441. * Set the gain value for master audio. Should not be called externally.
  442. * @method _updateVolume
  443. * @protected
  444. */
  445. p._updateVolume = function () {
  446. var newVolume = createjs.Sound._masterMute ? 0 : this._volume;
  447. if (newVolume != this.gainNode.gain.value) {
  448. this.gainNode.gain.value = newVolume;
  449. }
  450. };
  451.  
  452. createjs.WebAudioPlugin = createjs.promote(WebAudioPlugin, "AbstractPlugin");
  453. }());
  454.