├── LICENSE.md ├── README.md ├── CHANGELOG.md ├── howler.min.js └── howler.js /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 James Simpson and GoldFire Studios, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![howler.js](http://goldfirestudios.com/proj/howlerjs/howlerjs_logo.png "howler.js") 2 | 3 | ## Description 4 | [**howler.js**](http://howlerjs.com) is an audio library for the modern web. It defaults to [Web Audio API](https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html) and falls back to [HTML5 Audio](http://www.whatwg.org/specs/web-apps/current-work/#the-audio-element). 5 | 6 | More documentation, examples and demos can be found at **[howlerjs.com](http://howlerjs.com)**. 7 | 8 | ### Features 9 | * Defaults to Web Audio API 10 | * Falls back to HTML5 Audio 11 | * Supports multiple file formats to support all browsers 12 | * Automatic caching for Web Audio API 13 | * Implements cache pool for HTML5 Audio 14 | * Per-sound and global mute/unmute and volume control 15 | * Playback of multiple sounds at the same time 16 | * Easy sound sprite definition and playback 17 | * Fade in/out sounds 18 | * Supports Web Audio 3D sound positioning 19 | * Methods can be chained 20 | * Uses no outside libraries, just pure Javascript 21 | * Lightweight, 9kb filesize (3kb gzipped) 22 | 23 | ### Browser Compatibility 24 | Tested in the following browsers/versions: 25 | * Google Chrome 4.0+ 26 | * Internet Explorer 9.0+ 27 | * Firefox 3.5+ 28 | * Safari 4.0+ 29 | * Mobile Safari 6.0+ (after user input) 30 | * Opera 10.5+ 31 | 32 | ## Documentation 33 | 34 | ### Examples 35 | 36 | ##### Most basic, play an MP3: 37 | ```javascript 38 | var sound = new Howl({ 39 | urls: ['sound.mp3'] 40 | }).play(); 41 | ``` 42 | 43 | ##### More playback options: 44 | ```javascript 45 | var sound = new Howl({ 46 | urls: ['sound.mp3', 'sound.ogg', 'sound.wav'], 47 | autoplay: true, 48 | loop: true, 49 | volume: 0.5, 50 | onend: function() { 51 | console.log('Finished!'); 52 | } 53 | }); 54 | ``` 55 | 56 | ##### Define and play a sound sprite: 57 | ```javascript 58 | var sound = new Howl({ 59 | urls: ['sounds.mp3', 'sounds.ogg'], 60 | sprite: { 61 | blast: [0, 1000], 62 | laser: [2000, 3000], 63 | winner: [4000, 7500] 64 | } 65 | }); 66 | 67 | // shoot the laser! 68 | sound.play('laser'); 69 | ``` 70 | 71 | ### Properties 72 | * **autoplay**: `Boolean` *(`false` by default)* Set to `true` to automatically start playback when sound is loaded. 73 | * **buffer**: `Boolean` *(`false` by default)* Set to `true` to force HTML5 Audio. This should be used for large audio files so that you don't have to wait for the full file to be downloaded and decoded before playing. 74 | * **format**: `String` *(`null` by default)* howler.js automatically detects your file format from the URL, but you may also specify a format in situations where URL extraction won't work. 75 | * **loop**: `Boolean` *(`false` by default)* Set to `true` to automatically loop the sound forever. 76 | * **sprite**: `Object` *(`{}` by default)* Define a sound sprite for the sound. The offset and duration are defined in milliseconds. A third (optional) parameter is available to set a sprite as looping. 77 | ``` 78 | Example: 79 | { 80 | key: [offset, duration, (loop)] 81 | } 82 | ``` 83 | * **volume**: `Number` *(`1.0` by default)* The volume of the specific track, from `0.0` to `1.0`. 84 | * **urls**: `Array` *(`[]` by default)* The source URLs to the track(s) to be loaded for the sound. These should be in order of preference, howler.js will automatically load the first one that is compatible with the current browser. If your files have no extensions, you will need to explicitly specify the format using the `format` property. 85 | * **onend**: `Function` *(`function(){}` by default)* Fire when the sound finishes playing (if it is looping, it'll fire at the end of each loop). 86 | * **onload**: `Function` *(`function(){}` by default)* Fires when the sound is loaded. 87 | * **onloaderror**: `Function` *(`function(){}` by default)* Fires when the sound is unable to load. 88 | * **onpause**: `Function` *(`function(){}` by default)* Fires when the sound has been paused. 89 | * **onplay**: `Function` *(`function(){}` by default)* Fires when the sound begins playing. 90 | 91 | ### Methods 92 | * **play**: Begins playback of sound. Will continue from previous point if sound has been previously paused. 93 | * *sprite*: `String` (optional) Plays from the defined sprite key. 94 | * *callback*: `Function` (optional) Fires when playback begins and returns the `soundId`, which is the unique identifier for this specific playback instance. 95 | * **pause**: Pauses playback of sound, saving the `pos` of playback. 96 | * *id*: `Number` (optional) The play instance ID. 97 | * **stop**: Stops playback of sound, resetting `pos` to `0`. 98 | * *id*: `Number` (optional) The play instance ID. 99 | * **mute**: Mutes the sound, but doesn't pause the playback. 100 | * *id*: `Number` (optional) The play instance ID. 101 | * **unmute**: Unmutes the sound. 102 | * *id*: `Number` (optional) The play instance ID. 103 | * **fade**: Fade a currently playing sound between two volumes. 104 | * *from*: `Number` Volume to fade from (`0.0` to `1.0`). 105 | * *to*: `Number` Volume to fade to (`0.0` to `1.0`). 106 | * *duration*: `Number` Time in milliseconds to fade. 107 | * *callback*: `Function` (optional) Fires when fade is complete. 108 | * *id*: `Number` (optional) The play instance ID. 109 | * [DEPRECATED] **fadeIn**: Fade in the current sound. 110 | * *to*: `Number` Volume to fade to (`0.0` to `1.0`). 111 | * *duration*: `Number` Time in milliseconds to fade. 112 | * *callback*: `Function` (optional) Fires when fade is complete. 113 | * [DEPRECATED] **fadeOut**: Fade out the current sound and pause when finished. 114 | * *to*: `Number` Volume to fade to (`0.0` to `1.0`). 115 | * *duration*: `Number` Time in milliseconds to fade. 116 | * *callback*: `Function` (optional) Fires when fade is complete. 117 | * *id*: `Number` (optional) The play instance ID. 118 | * **loop**: Get/set whether to loop the sound. 119 | * *loop*: `Boolean` (optional) To loop or not to loop, that is the question. 120 | * **pos**: Get/set the position of playback. 121 | * *position*: `Number` (optional) The position to move current playback to. 122 | * *id*: `Number` (optional) The play instance ID. 123 | * **pos3d**: Get/set the 3D position of the audio source. The most common usage is to set the `x` position to affect the left/right ear panning. Setting the value higher than `1.0` will begin to decrease the volume of the sound as it moves further away. **This only works with Web Audio API.** 124 | * *x*: `Number` The x-position of the sound. 125 | * *y*: `Number` The y-position of the sound. 126 | * *z*: `Number` The z-position of the sound. 127 | * *id*: `Number` (optional) The play instance ID. 128 | * **sprite**: Get/set sound sprite definition. 129 | * *sprite*: `Object` (optional) See above for sound sprite definition. 130 | * **volume**: Get/set volume of this sound. 131 | * *volume*: `Number` (optional) Volume from `0.0` to `1.0`. 132 | * *id*: `Number` (optional) The play instance ID. 133 | * **urls**: Get/set the URLs to be pulled from to play in this source. 134 | * *urls*: `Array` (optional) Changes the source files for this `Howl` object. 135 | * **on**: Call/set custom events. Multiple events can be added by calling this multiple times. 136 | * *event*: `String` Name of event to fire/set. 137 | * *function*: `Function` (optional) Define function to fire on event. 138 | * **off**: Remove custom events that you've set. 139 | * *event*: `String` Name of event. 140 | * *function*: `Function` (optional) The listener to remove. 141 | * **unload**: Unload and destroy a Howl object. This will immediately stop all play instances attached to this sound and remove it from the cache. 142 | 143 | ### Global Methods 144 | The following methods are used to modify all sounds globally, and are called from the `Howler` object. 145 | 146 | * **mute**: Mutes all sounds. 147 | * **unmute**: Unmutes all sounds and restores them to their previous volume. 148 | * **volume**: Get/set the global volume for all sounds. 149 | * *volume*: `Number` (optional) Volume from `0.0` to `1.0`. 150 | 151 | ## License 152 | 153 | Copyright (c) 2013 James Simpson and GoldFire Studios, Inc. 154 | 155 | Released under the MIT License. 156 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.1.17 (February 5, 2014) 2 | - `FIXED`: Fix another bug in Chrome that would throw an error when pausing/stopping when a source is already stopped. 3 | - `ADDED`: CommonJS support for things like Browserify (thanks Michal Kuklis). 4 | - `ADDED`: Support for playback mp4 files. 5 | - `ADDED`: Expose the `noAudio` variable to the global `Howler` object. 6 | - `FIXED`: Fix a rounding error that was causing HTML5 Audio to cut off early on some environments. 7 | - `FIXED`: The `onend` callback now correctly fires when changing the pos of a sound after it has started playing and when it is using HTML5 Audio. 8 | 9 | ## 1.1.16 (January 8, 2014) 10 | - `FIXED`: Prevent InvalidStateError when unloading a sound that has already been stopped. 11 | - `FIXED`: Fix bug in unload method that prevented the first sound from being unloaded. 12 | 13 | ## 1.1.15 (December 28, 2013) 14 | - `FIXED`: Fix bug that prevented master volume from being set to 0. 15 | - `FIXED`: Fix bug that prevented initial volume from being set to 0. 16 | - `FIXED`: Update the README to accurately show `autoplay` as defaulting to `false`. 17 | - `FIXED`: Call `loaderror` when decodeAudioData fails. 18 | - `FIXED`: Fix bug in setting position on an active playing WebAudio node through 'pos(position, id)' (thanks Arjun Mehta). 19 | - `FIXED`: Fix an issue with looping after resuming playback when in WebAudio playback (thanks anzev). 20 | 21 | ## 1.1.14 (October 18, 2013) 22 | - `FIXED`: Critical bug fix that was breaking support on some browsers and some codecs. 23 | 24 | ## 1.1.13 (October 17, 2013) 25 | - `FIXED`: Code cleanup by removing redundant `canPlay` object (thanks Fabien). 26 | - `FIXED`: File extensions are now detected correctly if there is a query string with dots in the filename (thanks theshock). 27 | - `FIXED`: Fire `onloaderror` if a bad filename is passed with the `urls` property. 28 | 29 | ## 1.1.12 (September 12, 2013) 30 | - `UPDATED`: Changed AMD definition to anonymous module and define it as global always (thanks Fabien). 31 | - `ADDED`: Added the `rate` property to `Howl` object creation, allowing you to specify the playback rate. This only works when using Web Audio (thanks Qqwy). 32 | - `FIXED`: Prevent some instances of IE9 from throwing "Not Implemented" error (thanks Tero Tilus). 33 | 34 | ## 1.1.11 (July 28, 2013) 35 | - `FIXED`: Fix bug caused by trying to disconnect audio node when using HTML5 Audio. 36 | - `FIXED`: Correctly return the sound's position when it is paused. 37 | - `FIXED`: Fix another bug that caused looping sounds to not always correctly resume after a pause. 38 | 39 | ## 1.1.10 (July 26, 2013) 40 | - `ADDED`: New `unload` method to destroy a Howl object. This will stop all associated sounds instantly and remove the sound from the cache. 41 | - `FIXED`: When using Web Audio, loop from the correct position after pausing the sound halfway through. 42 | - `FIXED`: Always return a number when getting a sound's position with the `pos` method, and always return the reference to the sound when setting a sound that hasn't loaded. 43 | 44 | ## 1.1.9 (July 11, 2013) 45 | - `FIXED`: Fixed issue where calling the `volume` method before a sound had loaded prevented the volume from being changed. 46 | 47 | ## 1.1.8 (July 10, 2013) 48 | - `FIXED`: `urls` method now works again, and can take a string rather than an array if only one url is being passed. 49 | - `FIXED`: Make `node.play` async when not using webAudio (thanks Alex Dong). 50 | 51 | ## 1.1.7 (May 30, 2013) 52 | - `FIXED`: Hotfix for a missing parameter that somehow missed the 1.1.6 commit in global muting. 53 | 54 | ## 1.1.6 (May 30, 2013) 55 | - `ADDED`: A general `fade` method that allows a playing sound to be faded from one volume to another. 56 | - `DEPRECATED`: The `fadeIn` and `fadeOut` methods should no longer be used and have been deprecated. These will be removed in a future major release. 57 | - `FIXED`: No longer require the sprite parameter to be passed into the `play` method when just passing a callback function. 58 | - `FIXED`: Cleaned up global muting code. (thanks arnorhs). 59 | 60 | ## 1.1.5 (May 3, 2013) 61 | - `ADDED`: Support for the Ogg Opus codec (thanks Andrew Carpenter). 62 | - `ADDED`: Semver tags for easy package management (thanks Martin Reurings). 63 | - `ADDED`: Improve style/readability of code that discovers which audio file extension to use (thanks Fabien). 64 | - `ADDED`: The `onend` event now passes the soundId back as the 2nd parameter of the callback (thanks Ross Cairns). 65 | - `FIXED`: A few small typos in the comments. (thanks VAS). 66 | 67 | ## 1.1.4 (April 28, 2013) 68 | - `FIXED`: A few small bugs that broke global mute and unmute when using HTML5 Audio. 69 | 70 | ## 1.1.3 (April 27, 2013) 71 | - `FIXED`: Bug that prevented global mute from working 100% of the time when using HTML5 Audio. 72 | 73 | ## 1.1.2 (April 24, 2013) 74 | - `FIXED`: Calling `volume` before `play` now works as expected. 75 | - `FIXED`: Edge case issue with cache cleaning. 76 | - `FIXED`: Load event didn't fire when new URLs were loaded after the initial load. 77 | 78 | ## 1.1.1 (April 17, 2013) 79 | - `ADDED`: `onloaderror` event fired when sound fails to load (thanks Thiago de Barros Laceda). 80 | - `ADDED`: `format` property that overrides the URL extraction of the file format (thanks Kenan Shifflett). 81 | - `FIXED`: AMD implementation now only defines one module and removes global scope (thanks Kenan Shifflett). 82 | - `FIXED`: Broken chaining with `play` method. 83 | 84 | ## 1.1.0 (April 11, 2013) 85 | - `ADDED:` New `pos3d` method that allows for positional audio (Web Audio API only). 86 | - `ADDED:` Multi-playback control system that allows for control of specific play instances when sprites are used. A callback has been added to the `play` method that returns the `soundId` for the playback instance. This can then be passed as the optional last parameter to other methods to control that specific playback instead of the whole sound object. 87 | - `ADDED:` Pass the `Howl` object reference as the first parameter in the custom event callbacks. 88 | - `ADDED:` New optional parameter in sprite defintions to define a sprite as looping rather than the whole track. In the sprite definition array, set the 3rd value to true for looping (`spriteName: [pos, duration, loop]`). 89 | - `FIXED:` Now all audio acts as a sound sprite internally, which helps to fix several lingering bugs (doesn't affect the API at all). 90 | - `FIXED:` Improved implementation of Web Audio API looping. 91 | - `FIXED:` Improved implementation of HTML5 Audio looping. 92 | - `FIXED:` Issue that caused the fallback to not work when testing locally. 93 | - `FIXED:` Fire `onend` event at the end of `fadeOut`. 94 | - `FIXED:` Prevent errors from being thrown on browsers that don't support HTML5 Audio. 95 | - `FIXED:` Various code cleanup and optimizations. 96 | 97 | ## 1.0.13 (March 20, 2013) 98 | - `ADDED:` Support for AMD loading as a module (thanks @mostlygeek). 99 | 100 | ## 1.0.12 (March 28, 2013) 101 | - `ADDED:` Automatically switch to HTML5 Audio if there is an error due to CORS. 102 | - `FIXED:` Check that only numbers get passed into volume methods. 103 | 104 | ## 1.0.11 (March 8, 2013) 105 | - `ADDED:` Exposed `usingWebAudio` value through the global `Howler` object. 106 | - `FIXED:` Issue with non-sprite HTML5 Audio clips becoming unplayable (thanks Paul Morris). 107 | 108 | ## 1.0.10 (March 1, 2013) 109 | - `FIXED:` Issue that caused simultaneous playback of audio sprites to break while using HTML5 Audio. 110 | 111 | ## 1.0.9 (March 1, 2013) 112 | - `ADDED:` Spec-implementation detection to cover new and deprecated Web Audio API methods (thanks @canuckistani). 113 | 114 | ## 1.0.8 (February 25, 2013) 115 | - `ADDED:` New `onplay` event. 116 | - `ADDED:` Support for playing audio from base64 encoded strings. 117 | - `FIXED:` Issue with soundId not being unique when multiple sounds were played simultaneously. 118 | - `FIXED:` Verify that an HTML5 Audio Node is ready to play before playing it. 119 | - `FIXED:` Issue with `onend` timer not getting cleared all the time. 120 | 121 | ## 1.0.7 (February 18, 2013) 122 | - `FIXED:` Cancel the correct timer when multiple HTML5 Audio sounds are played at the same time. 123 | - `FIXED:` Make sure howler.js is future-compatible with UglifyJS 2. 124 | - `FIXED:` Duration now gets set correctly when pulled from cache. 125 | - `FIXED:` Tiny typo in README.md (thanks @johnfn). 126 | 127 | ## 1.0.6 (February 8, 2013) 128 | - `FIXED:` Issue with global mute calls happening before an HTML5 Audio element is loaded. 129 | 130 | ## 1.0.5 (February 7, 2013) 131 | - `FIXED:` Global mute now also mutes all future sounds that are played until `unmute` is called. 132 | 133 | ## 1.0.4 (February 6, 2013) 134 | - `ADDED:` Support for WebM audio. 135 | - `FIXED:` Issue with volume changes when on HTML5 Audio. 136 | - `FIXED:` Round volume values to fix inconsistencies in fade in/out methods. 137 | 138 | ## 1.0.3 (February 2, 2013) 139 | - `FIXED:` Make sure `self` is always defined before returning it. 140 | 141 | ## 1.0.2 (February 1, 2013) 142 | - `ADDED:` New `off` method that allows for the removal of custom events. 143 | - `FIXED:` Issue with chaining the `on` method. 144 | - `FIXED:` Small typo in documentation. 145 | 146 | ## 1.0.1 (January 30, 2013) 147 | - `ADDED:` New `buffer` property that allows you to force the use of HTML5 on specific sounds to allow streaming of large audio files. 148 | - `ADDED:` Support for multiple events per event type. 149 | - `FIXED:` Issue with method chaining before a sound was ready to play. 150 | - `FIXED:` Use `self` everywhere instead of `this` to maintain consistency. 151 | 152 | ## 1.0.0 (January 28, 2013) 153 | - First commit -------------------------------------------------------------------------------- /howler.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * howler.js v1.1.17 3 | * howlerjs.com 4 | * 5 | * (c) 2013-2014, James Simpson of GoldFire Studios 6 | * goldfirestudios.com 7 | * 8 | * MIT License 9 | */ 10 | !function(){var e={},t=null,n=!0,r=!1;if("undefined"!=typeof AudioContext)t=new AudioContext;else if("undefined"!=typeof webkitAudioContext)t=new webkitAudioContext;else if("undefined"!=typeof Audio){n=!1;try{new Audio}catch(i){r=!0}}else n=!1,r=!0;if(n){var s=void 0===t.createGain?t.createGainNode():t.createGain();s.gain.value=1,s.connect(t.destination)}var o=function(){this._volume=1,this._muted=!1,this.usingWebAudio=n,this.noAudio=r,this._howls=[]};o.prototype={volume:function(e){var t=this;if(e=parseFloat(e),e>=0&&1>=e){t._volume=e,n&&(s.gain.value=e);for(var r in t._howls)if(t._howls.hasOwnProperty(r)&&t._howls[r]._webAudio===!1)for(var i=0;i=2?s:o.match(/data\:audio\/([^?]+);/),!s)return t.on("loaderror"),void 0;s=s[1]}if(f[s]){n=t._urls[i];break}}if(!n)return t.on("loaderror"),void 0;if(t._src=n,t._webAudio)c(t,n);else{var a=new Audio;t._audioNode.push(a),a.src=n,a._pos=0,a.preload="auto",a.volume=u._muted?0:t._volume*u.volume(),e[n]=t;var l=function(){t._duration=Math.ceil(10*a.duration)/10,0===Object.getOwnPropertyNames(t._sprite).length&&(t._sprite={_default:[0,1e3*t._duration]}),t._loaded||(t._loaded=!0,t.on("load")),t._autoplay&&t.play(),a.removeEventListener("canplaythrough",l,!1)};a.addEventListener("canplaythrough",l,!1),a.load()}return t},urls:function(e){var t=this;return e?(t.stop(),t._urls="string"==typeof e?[e]:e,t._loaded=!1,t.load(),t):t._urls},play:function(e,n){var r=this;return"function"==typeof e&&(n=e),e&&"function"!=typeof e||(e="_default"),r._loaded?r._sprite[e]?(r._inactiveNode(function(i){i._sprite=e;var s,o=i._pos>0?i._pos:r._sprite[e][0]/1e3,a=r._sprite[e][1]/1e3-i._pos,f=!(!r._loop&&!r._sprite[e][2]),l="string"==typeof n?n:Math.round(Date.now()*Math.random())+"";if(function(){var t={id:l,sprite:e,loop:f};s=setTimeout(function(){!r._webAudio&&f&&r.stop(t.id,t.timer).play(e,t.id),r._webAudio&&!f&&(r._nodeById(t.id).paused=!0,r._nodeById(t.id)._pos=0),r._webAudio||f||r.stop(t.id,t.timer),r.on("end",l)},1e3*a),r._onendTimer.push(s),t.timer=r._onendTimer[r._onendTimer.length-1]}(),r._webAudio){var c=r._sprite[e][0]/1e3,h=r._sprite[e][1]/1e3;i.id=l,i.paused=!1,p(r,[f,c,h],l),r._playStart=t.currentTime,i.gain.value=r._volume,void 0===i.bufferSource.start?i.bufferSource.noteGrainOn(0,o,a):i.bufferSource.start(0,o,a)}else{if(4!==i.readyState)return r._clearEndTimer(s),function(){var t=r,s=e,o=n,u=i,a=function(){t.play(s,o),u.removeEventListener("canplaythrough",a,!1)};u.addEventListener("canplaythrough",a,!1)}(),r;i.id=l,i.currentTime=o,i.muted=u._muted,i.volume=r._volume*u.volume(),setTimeout(function(){i.play()},0)}return r.on("play"),"function"==typeof n&&n(l),r}),r):("function"==typeof n&&n(),r):(r.on("load",function(){r.play(e,n)}),r)},pause:function(e,t){var n=this;if(!n._loaded)return n.on("play",function(){n.pause(e)}),n;n._clearEndTimer(t||0);var r=e?n._nodeById(e):n._activeNode();if(r)if(r._pos=n.pos(null,e),n._webAudio){if(!r.bufferSource||r.paused)return n;r.paused=!0,void 0===r.bufferSource.stop?r.bufferSource.noteOff(0):r.bufferSource.stop(0)}else r.pause();return n.on("pause"),n},stop:function(e,t){var n=this;if(!n._loaded)return n.on("play",function(){n.stop(e)}),n;n._clearEndTimer(t||0);var r=e?n._nodeById(e):n._activeNode();if(r)if(r._pos=0,n._webAudio){if(!r.bufferSource||r.paused)return n;r.paused=!0,void 0===r.bufferSource.stop?r.bufferSource.noteOff(0):r.bufferSource.stop(0)}else r.pause(),r.currentTime=0;return n},mute:function(e){var t=this;if(!t._loaded)return t.on("play",function(){t.mute(e)}),t;var n=e?t._nodeById(e):t._activeNode();return n&&(t._webAudio?n.gain.value=0:n.volume=0),t},unmute:function(e){var t=this;if(!t._loaded)return t.on("play",function(){t.unmute(e)}),t;var n=e?t._nodeById(e):t._activeNode();return n&&(t._webAudio?n.gain.value=t._volume:n.volume=t._volume),t},volume:function(e,t){var n=this;if(e=parseFloat(e),e>=0&&1>=e){if(n._volume=e,!n._loaded)return n.on("play",function(){n.volume(e,t)}),n;var r=t?n._nodeById(t):n._activeNode();return r&&(n._webAudio?r.gain.value=e:r.volume=e*u.volume()),n}return n._volume},loop:function(e){var t=this;return"boolean"==typeof e?(t._loop=e,t):t._loop},sprite:function(e){var t=this;return"object"==typeof e?(t._sprite=e,t):t._sprite},pos:function(e,n){var r=this;if(!r._loaded)return r.on("load",function(){r.pos(e)}),"number"==typeof e?r:r._pos||0;e=parseFloat(e);var i=n?r._nodeById(n):r._activeNode();if(i)return e>=0?(r.pause(n),i._pos=e,r.play(i._sprite,n),r):r._webAudio?i._pos+(t.currentTime-r._playStart):i.currentTime;if(e>=0)return r;for(var s=0;s=0||0>e))return i._pos3d;if(i._webAudio){var s=r?i._nodeById(r):i._activeNode();s&&(i._pos3d=[e,t,n],s.panner.setPosition(e,t,n))}return i},fade:function(e,t,n,r,i){var s=this,o=Math.abs(e-t),u=e>t?"down":"up",a=o/.01,f=n/a;if(!s._loaded)return s.on("load",function(){s.fade(e,t,n,r,i)}),s;s.volume(e,i);for(var l=1;a>=l;l++)!function(){var e=s._volume+("up"===u?.01:-.01)*l,n=Math.round(1e3*e)/1e3,o=t;setTimeout(function(){s.volume(n,i),n===o&&r&&r()},f*l)}()},fadeIn:function(e,t,n){return this.volume(0).play().fade(0,e,t,n)},fadeOut:function(e,t,n,r){var i=this;return i.fade(i._volume,e,t,function(){n&&n(),i.pause(r),i.on("end")},r)},_nodeById:function(e){for(var t=this,n=t._audioNode[0],r=0;r=0&&!(5>=n);e--)t._audioNode[e].paused&&(t._webAudio&&t._audioNode[e].disconnect(0),n--,t._audioNode.splice(e,1))},_clearEndTimer:function(e){var t=this,n=t._onendTimer.indexOf(e);n=n>=0?n:0,t._onendTimer[n]&&(clearTimeout(t._onendTimer[n]),t._onendTimer.splice(n,1))},_setupAudioNode:function(){var e=this,n=e._audioNode,r=e._audioNode.length;return n[r]=void 0===t.createGain?t.createGainNode():t.createGain(),n[r].gain.value=e._volume,n[r].paused=!0,n[r]._pos=0,n[r].readyState=4,n[r].connect(s),n[r].panner=t.createPanner(),n[r].panner.setPosition(e._pos3d[0],e._pos3d[1],e._pos3d[2]),n[r].panner.connect(n[r]),n[r]},on:function(e,t){var n=this,r=n["_on"+e];if("function"==typeof t)r.push(t);else for(var i=0;i=0&&u._howls.splice(i,1),delete e[t._src],t=null}},n)var c=function(n,r){if(r in e)n._duration=e[r].duration,h(n);else{var i=new XMLHttpRequest;i.open("GET",r,!0),i.responseType="arraybuffer",i.onload=function(){t.decodeAudioData(i.response,function(t){t&&(e[r]=t,h(n,t))},function(){n.on("loaderror")})},i.onerror=function(){n._webAudio&&(n._buffer=!0,n._webAudio=!1,n._audioNode=[],delete n._gainNode,n.load())};try{i.send()}catch(s){i.onerror()}}},h=function(e,t){e._duration=t?t.duration:e._duration,0===Object.getOwnPropertyNames(e._sprite).length&&(e._sprite={_default:[0,1e3*e._duration]}),e._loaded||(e._loaded=!0,e.on("load")),e._autoplay&&e.play()},p=function(n,r,i){var s=n._nodeById(i);s.bufferSource=t.createBufferSource(),s.bufferSource.buffer=e[n._src],s.bufferSource.connect(s.panner),s.bufferSource.loop=r[0],r[0]&&(s.bufferSource.loopStart=r[1],s.bufferSource.loopEnd=r[1]+r[2]),s.bufferSource.playbackRate.value=n._rate};"function"==typeof define&&define.amd&&define(function(){return{Howler:u,Howl:l}}),"undefined"!=typeof exports&&(exports.Howler=u,exports.Howl=l),window.Howler=u,window.Howl=l}() -------------------------------------------------------------------------------- /howler.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * howler.js v1.1.17 3 | * howlerjs.com 4 | * 5 | * (c) 2013-2014, James Simpson of GoldFire Studios 6 | * goldfirestudios.com 7 | * 8 | * MIT License 9 | */ 10 | 11 | (function() { 12 | // setup 13 | var cache = {}; 14 | 15 | // setup the audio context 16 | var ctx = null, 17 | usingWebAudio = true, 18 | noAudio = false; 19 | if (typeof AudioContext !== 'undefined') { 20 | ctx = new AudioContext(); 21 | } else if (typeof webkitAudioContext !== 'undefined') { 22 | ctx = new webkitAudioContext(); 23 | } else if (typeof Audio !== 'undefined') { 24 | usingWebAudio = false; 25 | try { 26 | new Audio(); 27 | } catch(e) { 28 | noAudio = true; 29 | } 30 | } else { 31 | usingWebAudio = false; 32 | noAudio = true; 33 | } 34 | 35 | // create a master gain node 36 | if (usingWebAudio) { 37 | var masterGain = (typeof ctx.createGain === 'undefined') ? ctx.createGainNode() : ctx.createGain(); 38 | masterGain.gain.value = 1; 39 | masterGain.connect(ctx.destination); 40 | } 41 | 42 | // create global controller 43 | var HowlerGlobal = function() { 44 | this._volume = 1; 45 | this._muted = false; 46 | this.usingWebAudio = usingWebAudio; 47 | this.noAudio = noAudio; 48 | this._howls = []; 49 | }; 50 | HowlerGlobal.prototype = { 51 | /** 52 | * Get/set the global volume for all sounds. 53 | * @param {Float} vol Volume from 0.0 to 1.0. 54 | * @return {Howler/Float} Returns self or current volume. 55 | */ 56 | volume: function(vol) { 57 | var self = this; 58 | 59 | // make sure volume is a number 60 | vol = parseFloat(vol); 61 | 62 | if (vol >= 0 && vol <= 1) { 63 | self._volume = vol; 64 | 65 | if (usingWebAudio) { 66 | masterGain.gain.value = vol; 67 | } 68 | 69 | // loop through cache and change volume of all nodes that are using HTML5 Audio 70 | for (var key in self._howls) { 71 | if (self._howls.hasOwnProperty(key) && self._howls[key]._webAudio === false) { 72 | // loop through the audio nodes 73 | for (var i=0; i= 2) ? ext : urlItem.match(/data\:audio\/([^?]+);/); 219 | 220 | if (ext) { 221 | ext = ext[1]; 222 | } else { 223 | self.on('loaderror'); 224 | return; 225 | } 226 | } 227 | 228 | if (codecs[ext]) { 229 | url = self._urls[i]; 230 | break; 231 | } 232 | } 233 | 234 | if (!url) { 235 | self.on('loaderror'); 236 | return; 237 | } 238 | 239 | self._src = url; 240 | 241 | if (self._webAudio) { 242 | loadBuffer(self, url); 243 | } else { 244 | var newNode = new Audio(); 245 | self._audioNode.push(newNode); 246 | 247 | // setup the new audio node 248 | newNode.src = url; 249 | newNode._pos = 0; 250 | newNode.preload = 'auto'; 251 | newNode.volume = (Howler._muted) ? 0 : self._volume * Howler.volume(); 252 | 253 | // add this sound to the cache 254 | cache[url] = self; 255 | 256 | // setup the event listener to start playing the sound 257 | // as soon as it has buffered enough 258 | var listener = function() { 259 | // round up the duration when using HTML5 Audio to account for the lower precision 260 | self._duration = Math.ceil(newNode.duration * 10) / 10; 261 | 262 | // setup a sprite if none is defined 263 | if (Object.getOwnPropertyNames(self._sprite).length === 0) { 264 | self._sprite = {_default: [0, self._duration * 1000]}; 265 | } 266 | 267 | if (!self._loaded) { 268 | self._loaded = true; 269 | self.on('load'); 270 | } 271 | 272 | if (self._autoplay) { 273 | self.play(); 274 | } 275 | 276 | // clear the event listener 277 | newNode.removeEventListener('canplaythrough', listener, false); 278 | }; 279 | newNode.addEventListener('canplaythrough', listener, false); 280 | newNode.load(); 281 | } 282 | 283 | return self; 284 | }, 285 | 286 | /** 287 | * Get/set the URLs to be pulled from to play in this source. 288 | * @param {Array} urls Arry of URLs to load from 289 | * @return {Howl} Returns self or the current URLs 290 | */ 291 | urls: function(urls) { 292 | var self = this; 293 | 294 | if (urls) { 295 | self.stop(); 296 | self._urls = (typeof urls === 'string') ? [urls] : urls; 297 | self._loaded = false; 298 | self.load(); 299 | 300 | return self; 301 | } else { 302 | return self._urls; 303 | } 304 | }, 305 | 306 | /** 307 | * Play a sound from the current time (0 by default). 308 | * @param {String} sprite (optional) Plays from the specified position in the sound sprite definition. 309 | * @param {Function} callback (optional) Returns the unique playback id for this sound instance. 310 | * @return {Howl} 311 | */ 312 | play: function(sprite, callback) { 313 | var self = this; 314 | 315 | // if no sprite was passed but a callback was, update the variables 316 | if (typeof sprite === 'function') { 317 | callback = sprite; 318 | } 319 | 320 | // use the default sprite if none is passed 321 | if (!sprite || typeof sprite === 'function') { 322 | sprite = '_default'; 323 | } 324 | 325 | // if the sound hasn't been loaded, add it to the event queue 326 | if (!self._loaded) { 327 | self.on('load', function() { 328 | self.play(sprite, callback); 329 | }); 330 | 331 | return self; 332 | } 333 | 334 | // if the sprite doesn't exist, play nothing 335 | if (!self._sprite[sprite]) { 336 | if (typeof callback === 'function') callback(); 337 | return self; 338 | } 339 | 340 | // get the node to playback 341 | self._inactiveNode(function(node) { 342 | // persist the sprite being played 343 | node._sprite = sprite; 344 | 345 | // determine where to start playing from 346 | var pos = (node._pos > 0) ? node._pos : self._sprite[sprite][0] / 1000, 347 | duration = self._sprite[sprite][1] / 1000 - node._pos; 348 | 349 | // determine if this sound should be looped 350 | var loop = !!(self._loop || self._sprite[sprite][2]); 351 | 352 | // set timer to fire the 'onend' event 353 | var soundId = (typeof callback === 'string') ? callback : Math.round(Date.now() * Math.random()) + '', 354 | timerId; 355 | (function() { 356 | var data = { 357 | id: soundId, 358 | sprite: sprite, 359 | loop: loop 360 | }; 361 | timerId = setTimeout(function() { 362 | // if looping, restart the track 363 | if (!self._webAudio && loop) { 364 | self.stop(data.id, data.timer).play(sprite, data.id); 365 | } 366 | 367 | // set web audio node to paused at end 368 | if (self._webAudio && !loop) { 369 | self._nodeById(data.id).paused = true; 370 | self._nodeById(data.id)._pos = 0; 371 | } 372 | 373 | // end the track if it is HTML audio and a sprite 374 | if (!self._webAudio && !loop) { 375 | self.stop(data.id, data.timer); 376 | } 377 | 378 | // fire ended event 379 | self.on('end', soundId); 380 | }, duration * 1000); 381 | 382 | // store the reference to the timer 383 | self._onendTimer.push(timerId); 384 | 385 | // remember which timer to cancel 386 | data.timer = self._onendTimer[self._onendTimer.length - 1]; 387 | })(); 388 | 389 | if (self._webAudio) { 390 | var loopStart = self._sprite[sprite][0] / 1000, 391 | loopEnd = self._sprite[sprite][1] / 1000; 392 | 393 | // set the play id to this node and load into context 394 | node.id = soundId; 395 | node.paused = false; 396 | refreshBuffer(self, [loop, loopStart, loopEnd], soundId); 397 | self._playStart = ctx.currentTime; 398 | node.gain.value = self._volume; 399 | 400 | if (typeof node.bufferSource.start === 'undefined') { 401 | node.bufferSource.noteGrainOn(0, pos, duration); 402 | } else { 403 | node.bufferSource.start(0, pos, duration); 404 | } 405 | } else { 406 | if (node.readyState === 4) { 407 | node.id = soundId; 408 | node.currentTime = pos; 409 | node.muted = Howler._muted; 410 | node.volume = self._volume * Howler.volume(); 411 | setTimeout(function() { node.play(); }, 0); 412 | } else { 413 | self._clearEndTimer(timerId); 414 | 415 | (function(){ 416 | var sound = self, 417 | playSprite = sprite, 418 | fn = callback, 419 | newNode = node; 420 | var listener = function() { 421 | sound.play(playSprite, fn); 422 | 423 | // clear the event listener 424 | newNode.removeEventListener('canplaythrough', listener, false); 425 | }; 426 | newNode.addEventListener('canplaythrough', listener, false); 427 | })(); 428 | 429 | return self; 430 | } 431 | } 432 | 433 | // fire the play event and send the soundId back in the callback 434 | self.on('play'); 435 | if (typeof callback === 'function') callback(soundId); 436 | 437 | return self; 438 | }); 439 | 440 | return self; 441 | }, 442 | 443 | /** 444 | * Pause playback and save the current position. 445 | * @param {String} id (optional) The play instance ID. 446 | * @param {String} timerId (optional) Clear the correct timeout ID. 447 | * @return {Howl} 448 | */ 449 | pause: function(id, timerId) { 450 | var self = this; 451 | 452 | // if the sound hasn't been loaded, add it to the event queue 453 | if (!self._loaded) { 454 | self.on('play', function() { 455 | self.pause(id); 456 | }); 457 | 458 | return self; 459 | } 460 | 461 | // clear 'onend' timer 462 | self._clearEndTimer(timerId || 0); 463 | 464 | var activeNode = (id) ? self._nodeById(id) : self._activeNode(); 465 | if (activeNode) { 466 | activeNode._pos = self.pos(null, id); 467 | 468 | if (self._webAudio) { 469 | // make sure the sound has been created 470 | if (!activeNode.bufferSource || activeNode.paused) { 471 | return self; 472 | } 473 | 474 | activeNode.paused = true; 475 | if (typeof activeNode.bufferSource.stop === 'undefined') { 476 | activeNode.bufferSource.noteOff(0); 477 | } else { 478 | activeNode.bufferSource.stop(0); 479 | } 480 | } else { 481 | activeNode.pause(); 482 | } 483 | } 484 | 485 | self.on('pause'); 486 | 487 | return self; 488 | }, 489 | 490 | /** 491 | * Stop playback and reset to start. 492 | * @param {String} id (optional) The play instance ID. 493 | * @param {String} timerId (optional) Clear the correct timeout ID. 494 | * @return {Howl} 495 | */ 496 | stop: function(id, timerId) { 497 | var self = this; 498 | 499 | // if the sound hasn't been loaded, add it to the event queue 500 | if (!self._loaded) { 501 | self.on('play', function() { 502 | self.stop(id); 503 | }); 504 | 505 | return self; 506 | } 507 | 508 | // clear 'onend' timer 509 | self._clearEndTimer(timerId || 0); 510 | 511 | var activeNode = (id) ? self._nodeById(id) : self._activeNode(); 512 | if (activeNode) { 513 | activeNode._pos = 0; 514 | 515 | if (self._webAudio) { 516 | // make sure the sound has been created 517 | if (!activeNode.bufferSource || activeNode.paused) { 518 | return self; 519 | } 520 | 521 | activeNode.paused = true; 522 | 523 | if (typeof activeNode.bufferSource.stop === 'undefined') { 524 | activeNode.bufferSource.noteOff(0); 525 | } else { 526 | activeNode.bufferSource.stop(0); 527 | } 528 | } else { 529 | activeNode.pause(); 530 | activeNode.currentTime = 0; 531 | } 532 | } 533 | 534 | return self; 535 | }, 536 | 537 | /** 538 | * Mute this sound. 539 | * @param {String} id (optional) The play instance ID. 540 | * @return {Howl} 541 | */ 542 | mute: function(id) { 543 | var self = this; 544 | 545 | // if the sound hasn't been loaded, add it to the event queue 546 | if (!self._loaded) { 547 | self.on('play', function() { 548 | self.mute(id); 549 | }); 550 | 551 | return self; 552 | } 553 | 554 | var activeNode = (id) ? self._nodeById(id) : self._activeNode(); 555 | if (activeNode) { 556 | if (self._webAudio) { 557 | activeNode.gain.value = 0; 558 | } else { 559 | activeNode.volume = 0; 560 | } 561 | } 562 | 563 | return self; 564 | }, 565 | 566 | /** 567 | * Unmute this sound. 568 | * @param {String} id (optional) The play instance ID. 569 | * @return {Howl} 570 | */ 571 | unmute: function(id) { 572 | var self = this; 573 | 574 | // if the sound hasn't been loaded, add it to the event queue 575 | if (!self._loaded) { 576 | self.on('play', function() { 577 | self.unmute(id); 578 | }); 579 | 580 | return self; 581 | } 582 | 583 | var activeNode = (id) ? self._nodeById(id) : self._activeNode(); 584 | if (activeNode) { 585 | if (self._webAudio) { 586 | activeNode.gain.value = self._volume; 587 | } else { 588 | activeNode.volume = self._volume; 589 | } 590 | } 591 | 592 | return self; 593 | }, 594 | 595 | /** 596 | * Get/set volume of this sound. 597 | * @param {Float} vol Volume from 0.0 to 1.0. 598 | * @param {String} id (optional) The play instance ID. 599 | * @return {Howl/Float} Returns self or current volume. 600 | */ 601 | volume: function(vol, id) { 602 | var self = this; 603 | 604 | // make sure volume is a number 605 | vol = parseFloat(vol); 606 | 607 | if (vol >= 0 && vol <= 1) { 608 | self._volume = vol; 609 | 610 | // if the sound hasn't been loaded, add it to the event queue 611 | if (!self._loaded) { 612 | self.on('play', function() { 613 | self.volume(vol, id); 614 | }); 615 | 616 | return self; 617 | } 618 | 619 | var activeNode = (id) ? self._nodeById(id) : self._activeNode(); 620 | if (activeNode) { 621 | if (self._webAudio) { 622 | activeNode.gain.value = vol; 623 | } else { 624 | activeNode.volume = vol * Howler.volume(); 625 | } 626 | } 627 | 628 | return self; 629 | } else { 630 | return self._volume; 631 | } 632 | }, 633 | 634 | /** 635 | * Get/set whether to loop the sound. 636 | * @param {Boolean} loop To loop or not to loop, that is the question. 637 | * @return {Howl/Boolean} Returns self or current looping value. 638 | */ 639 | loop: function(loop) { 640 | var self = this; 641 | 642 | if (typeof loop === 'boolean') { 643 | self._loop = loop; 644 | 645 | return self; 646 | } else { 647 | return self._loop; 648 | } 649 | }, 650 | 651 | /** 652 | * Get/set sound sprite definition. 653 | * @param {Object} sprite Example: {spriteName: [offset, duration, loop]} 654 | * @param {Integer} offset Where to begin playback in milliseconds 655 | * @param {Integer} duration How long to play in milliseconds 656 | * @param {Boolean} loop (optional) Set true to loop this sprite 657 | * @return {Howl} Returns current sprite sheet or self. 658 | */ 659 | sprite: function(sprite) { 660 | var self = this; 661 | 662 | if (typeof sprite === 'object') { 663 | self._sprite = sprite; 664 | 665 | return self; 666 | } else { 667 | return self._sprite; 668 | } 669 | }, 670 | 671 | /** 672 | * Get/set the position of playback. 673 | * @param {Float} pos The position to move current playback to. 674 | * @param {String} id (optional) The play instance ID. 675 | * @return {Howl/Float} Returns self or current playback position. 676 | */ 677 | pos: function(pos, id) { 678 | var self = this; 679 | 680 | // if the sound hasn't been loaded, add it to the event queue 681 | if (!self._loaded) { 682 | self.on('load', function() { 683 | self.pos(pos); 684 | }); 685 | 686 | return typeof pos === 'number' ? self : self._pos || 0; 687 | } 688 | 689 | // make sure we are dealing with a number for pos 690 | pos = parseFloat(pos); 691 | 692 | var activeNode = (id) ? self._nodeById(id) : self._activeNode(); 693 | if (activeNode) { 694 | if (pos >= 0) { 695 | self.pause(id); 696 | activeNode._pos = pos; 697 | self.play(activeNode._sprite, id); 698 | 699 | return self; 700 | } else { 701 | return self._webAudio ? activeNode._pos + (ctx.currentTime - self._playStart) : activeNode.currentTime; 702 | } 703 | } else if (pos >= 0) { 704 | return self; 705 | } else { 706 | // find the first inactive node to return the pos for 707 | for (var i=0; i= 0 || x < 0) { 745 | if (self._webAudio) { 746 | var activeNode = (id) ? self._nodeById(id) : self._activeNode(); 747 | if (activeNode) { 748 | self._pos3d = [x, y, z]; 749 | activeNode.panner.setPosition(x, y, z); 750 | } 751 | } 752 | } else { 753 | return self._pos3d; 754 | } 755 | 756 | return self; 757 | }, 758 | 759 | /** 760 | * Fade a currently playing sound between two volumes. 761 | * @param {Number} from The volume to fade from (0.0 to 1.0). 762 | * @param {Number} to The volume to fade to (0.0 to 1.0). 763 | * @param {Number} len Time in milliseconds to fade. 764 | * @param {Function} callback (optional) Fired when the fade is complete. 765 | * @param {String} id (optional) The play instance ID. 766 | * @return {Howl} 767 | */ 768 | fade: function(from, to, len, callback, id) { 769 | var self = this, 770 | diff = Math.abs(from - to), 771 | dir = from > to ? 'down' : 'up', 772 | steps = diff / 0.01, 773 | stepTime = len / steps; 774 | 775 | // if the sound hasn't been loaded, add it to the event queue 776 | if (!self._loaded) { 777 | self.on('load', function() { 778 | self.fade(from, to, len, callback, id); 779 | }); 780 | 781 | return self; 782 | } 783 | 784 | // set the volume to the start position 785 | self.volume(from, id); 786 | 787 | for (var i=1; i<=steps; i++) { 788 | (function() { 789 | var change = self._volume + (dir === 'up' ? 0.01 : -0.01) * i, 790 | vol = Math.round(1000 * change) / 1000, 791 | toVol = to; 792 | 793 | setTimeout(function() { 794 | self.volume(vol, id); 795 | 796 | if (vol === toVol) { 797 | if (callback) callback(); 798 | } 799 | }, stepTime * i); 800 | })(); 801 | } 802 | }, 803 | 804 | /** 805 | * [DEPRECATED] Fade in the current sound. 806 | * @param {Float} to Volume to fade to (0.0 to 1.0). 807 | * @param {Number} len Time in milliseconds to fade. 808 | * @param {Function} callback 809 | * @return {Howl} 810 | */ 811 | fadeIn: function(to, len, callback) { 812 | return this.volume(0).play().fade(0, to, len, callback); 813 | }, 814 | 815 | /** 816 | * [DEPRECATED] Fade out the current sound and pause when finished. 817 | * @param {Float} to Volume to fade to (0.0 to 1.0). 818 | * @param {Number} len Time in milliseconds to fade. 819 | * @param {Function} callback 820 | * @param {String} id (optional) The play instance ID. 821 | * @return {Howl} 822 | */ 823 | fadeOut: function(to, len, callback, id) { 824 | var self = this; 825 | 826 | return self.fade(self._volume, to, len, function() { 827 | if (callback) callback(); 828 | self.pause(id); 829 | 830 | // fire ended event 831 | self.on('end'); 832 | }, id); 833 | }, 834 | 835 | /** 836 | * Get an audio node by ID. 837 | * @return {Howl} Audio node. 838 | */ 839 | _nodeById: function(id) { 840 | var self = this, 841 | node = self._audioNode[0]; 842 | 843 | // find the node with this ID 844 | for (var i=0; i=0; i--) { 932 | if (inactive <= 5) { 933 | break; 934 | } 935 | 936 | if (self._audioNode[i].paused) { 937 | // disconnect the audio source if using Web Audio 938 | if (self._webAudio) { 939 | self._audioNode[i].disconnect(0); 940 | } 941 | 942 | inactive--; 943 | self._audioNode.splice(i, 1); 944 | } 945 | } 946 | }, 947 | 948 | /** 949 | * Clear 'onend' timeout before it ends. 950 | * @param {Number} timerId The ID of the sound to be cancelled. 951 | */ 952 | _clearEndTimer: function(timerId) { 953 | var self = this, 954 | timer = self._onendTimer.indexOf(timerId); 955 | 956 | // make sure the timer gets cleared 957 | timer = timer >= 0 ? timer : 0; 958 | 959 | if (self._onendTimer[timer]) { 960 | clearTimeout(self._onendTimer[timer]); 961 | self._onendTimer.splice(timer, 1); 962 | } 963 | }, 964 | 965 | /** 966 | * Setup the gain node and panner for a Web Audio instance. 967 | * @return {Object} The new audio node. 968 | */ 969 | _setupAudioNode: function() { 970 | var self = this, 971 | node = self._audioNode, 972 | index = self._audioNode.length; 973 | 974 | // create gain node 975 | node[index] = (typeof ctx.createGain === 'undefined') ? ctx.createGainNode() : ctx.createGain(); 976 | node[index].gain.value = self._volume; 977 | node[index].paused = true; 978 | node[index]._pos = 0; 979 | node[index].readyState = 4; 980 | node[index].connect(masterGain); 981 | 982 | // create the panner 983 | node[index].panner = ctx.createPanner(); 984 | node[index].panner.setPosition(self._pos3d[0], self._pos3d[1], self._pos3d[2]); 985 | node[index].panner.connect(node[index]); 986 | 987 | return node[index]; 988 | }, 989 | 990 | /** 991 | * Call/set custom events. 992 | * @param {String} event Event type. 993 | * @param {Function} fn Function to call. 994 | * @return {Howl} 995 | */ 996 | on: function(event, fn) { 997 | var self = this, 998 | events = self['_on' + event]; 999 | 1000 | if (typeof fn === "function") { 1001 | events.push(fn); 1002 | } else { 1003 | for (var i=0; i= 0) { 1064 | Howler._howls.splice(index, 1); 1065 | } 1066 | 1067 | // delete this sound from the cache 1068 | delete cache[self._src]; 1069 | self = null; 1070 | } 1071 | 1072 | }; 1073 | 1074 | // only define these functions when using WebAudio 1075 | if (usingWebAudio) { 1076 | 1077 | /** 1078 | * Buffer a sound from URL (or from cache) and decode to audio source (Web Audio API). 1079 | * @param {Object} obj The Howl object for the sound to load. 1080 | * @param {String} url The path to the sound file. 1081 | */ 1082 | var loadBuffer = function(obj, url) { 1083 | // check if the buffer has already been cached 1084 | if (url in cache) { 1085 | // set the duration from the cache 1086 | obj._duration = cache[url].duration; 1087 | 1088 | // load the sound into this object 1089 | loadSound(obj); 1090 | } else { 1091 | // load the buffer from the URL 1092 | var xhr = new XMLHttpRequest(); 1093 | xhr.open('GET', url, true); 1094 | xhr.responseType = 'arraybuffer'; 1095 | xhr.onload = function() { 1096 | // decode the buffer into an audio source 1097 | ctx.decodeAudioData( 1098 | xhr.response, 1099 | function(buffer) { 1100 | if (buffer) { 1101 | cache[url] = buffer; 1102 | loadSound(obj, buffer); 1103 | } 1104 | }, 1105 | function(err) { 1106 | obj.on('loaderror'); 1107 | } 1108 | ); 1109 | }; 1110 | xhr.onerror = function() { 1111 | // if there is an error, switch the sound to HTML Audio 1112 | if (obj._webAudio) { 1113 | obj._buffer = true; 1114 | obj._webAudio = false; 1115 | obj._audioNode = []; 1116 | delete obj._gainNode; 1117 | obj.load(); 1118 | } 1119 | }; 1120 | try { 1121 | xhr.send(); 1122 | } catch (e) { 1123 | xhr.onerror(); 1124 | } 1125 | } 1126 | }; 1127 | 1128 | /** 1129 | * Finishes loading the Web Audio API sound and fires the loaded event 1130 | * @param {Object} obj The Howl object for the sound to load. 1131 | * @param {Objecct} buffer The decoded buffer sound source. 1132 | */ 1133 | var loadSound = function(obj, buffer) { 1134 | // set the duration 1135 | obj._duration = (buffer) ? buffer.duration : obj._duration; 1136 | 1137 | // setup a sprite if none is defined 1138 | if (Object.getOwnPropertyNames(obj._sprite).length === 0) { 1139 | obj._sprite = {_default: [0, obj._duration * 1000]}; 1140 | } 1141 | 1142 | // fire the loaded event 1143 | if (!obj._loaded) { 1144 | obj._loaded = true; 1145 | obj.on('load'); 1146 | } 1147 | 1148 | if (obj._autoplay) { 1149 | obj.play(); 1150 | } 1151 | }; 1152 | 1153 | /** 1154 | * Load the sound back into the buffer source. 1155 | * @param {Object} obj The sound to load. 1156 | * @param {Array} loop Loop boolean, pos, and duration. 1157 | * @param {String} id (optional) The play instance ID. 1158 | */ 1159 | var refreshBuffer = function(obj, loop, id) { 1160 | // determine which node to connect to 1161 | var node = obj._nodeById(id); 1162 | 1163 | // setup the buffer source for playback 1164 | node.bufferSource = ctx.createBufferSource(); 1165 | node.bufferSource.buffer = cache[obj._src]; 1166 | node.bufferSource.connect(node.panner); 1167 | node.bufferSource.loop = loop[0]; 1168 | if (loop[0]) { 1169 | node.bufferSource.loopStart = loop[1]; 1170 | node.bufferSource.loopEnd = loop[1] + loop[2]; 1171 | } 1172 | node.bufferSource.playbackRate.value = obj._rate; 1173 | }; 1174 | 1175 | } 1176 | 1177 | /** 1178 | * Add support for AMD (Asynchronous Module Definition) libraries such as require.js. 1179 | */ 1180 | if (typeof define === 'function' && define.amd) { 1181 | define(function() { 1182 | return { 1183 | Howler: Howler, 1184 | Howl: Howl 1185 | }; 1186 | }); 1187 | } 1188 | 1189 | /** 1190 | * Add support for CommonJS libraries such as browserify. 1191 | */ 1192 | if (typeof exports !== 'undefined') { 1193 | exports.Howler = Howler; 1194 | exports.Howl = Howl; 1195 | } 1196 | 1197 | // define globally in case AMD is not available or available but not used 1198 | window.Howler = Howler; 1199 | window.Howl = Howl; 1200 | 1201 | })(); --------------------------------------------------------------------------------