alcomputers.0.10.1b.source-code.howler.core.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of totalcomputers Show documentation
Show all versions of totalcomputers Show documentation
Computers in vanilla Minecraft | TotalOS SDK
/*!
* howler.js v2.2.3
* howlerjs.com
*
* (c) 2013-2020, James Simpson of GoldFire Studios
* goldfirestudios.com
*
* MIT License
*/
(function() {
'use strict';
/** Global Methods **/
/***************************************************************************/
/**
* Create the global controller. All contained methods and properties apply
* to all sounds that are currently playing or will be in the future.
*/
var HowlerGlobal = function() {
this.init();
};
HowlerGlobal.prototype = {
/**
* Initialize the global Howler object.
* @return {Howler}
*/
init: function() {
var self = this || Howler;
// Create a global ID counter.
self._counter = 1000;
// Pool of unlocked HTML5 Audio objects.
self._html5AudioPool = [];
self.html5PoolSize = 10;
// Internal properties.
self._codecs = {};
self._howls = [];
self._muted = false;
self._volume = 1;
self._canPlayEvent = 'canplaythrough';
self._navigator = (typeof window !== 'undefined' && window.navigator) ? window.navigator : null;
// Public properties.
self.masterGain = null;
self.noAudio = false;
self.usingWebAudio = true;
self.autoSuspend = true;
self.ctx = null;
// Set to false to disable the auto audio unlocker.
self.autoUnlock = true;
// Setup the various state values for global tracking.
self._setup();
return self;
},
/**
* Get/set the global volume for all sounds.
* @param {Float} vol Volume from 0.0 to 1.0.
* @return {Howler/Float} Returns self or current volume.
*/
volume: function(vol) {
var self = this || Howler;
vol = parseFloat(vol);
// If we don't have an AudioContext created yet, run the setup.
if (!self.ctx) {
setupAudioContext();
}
if (typeof vol !== 'undefined' && vol >= 0 && vol <= 1) {
self._volume = vol;
// Don't update any of the nodes if we are muted.
if (self._muted) {
return self;
}
// When using Web Audio, we just need to adjust the master gain.
if (self.usingWebAudio) {
self.masterGain.gain.setValueAtTime(vol, Howler.ctx.currentTime);
}
// Loop through and change volume for all HTML5 audio nodes.
for (var i=0; i=0; i--) {
self._howls[i].unload();
}
// Create a new AudioContext to make sure it is fully reset.
if (self.usingWebAudio && self.ctx && typeof self.ctx.close !== 'undefined') {
self.ctx.close();
self.ctx = null;
setupAudioContext();
}
return self;
},
/**
* Check for codec support of specific extension.
* @param {String} ext Audio file extention.
* @return {Boolean}
*/
codecs: function(ext) {
return (this || Howler)._codecs[ext.replace(/^x-/, '')];
},
/**
* Setup various state values for global tracking.
* @return {Howler}
*/
_setup: function() {
var self = this || Howler;
// Keeps track of the suspend/resume state of the AudioContext.
self.state = self.ctx ? self.ctx.state || 'suspended' : 'suspended';
// Automatically begin the 30-second suspend process
self._autoSuspend();
// Check if audio is available.
if (!self.usingWebAudio) {
// No audio is available on this system if noAudio is set to true.
if (typeof Audio !== 'undefined') {
try {
var test = new Audio();
// Check if the canplaythrough event is available.
if (typeof test.oncanplaythrough === 'undefined') {
self._canPlayEvent = 'canplay';
}
} catch(e) {
self.noAudio = true;
}
} else {
self.noAudio = true;
}
}
// Test to make sure audio isn't disabled in Internet Explorer.
try {
var test = new Audio();
if (test.muted) {
self.noAudio = true;
}
} catch (e) {}
// Check for supported codecs.
if (!self.noAudio) {
self._setupCodecs();
}
return self;
},
/**
* Check for browser support for various codecs and cache the results.
* @return {Howler}
*/
_setupCodecs: function() {
var self = this || Howler;
var audioTest = null;
// Must wrap in a try/catch because IE11 in server mode throws an error.
try {
audioTest = (typeof Audio !== 'undefined') ? new Audio() : null;
} catch (err) {
return self;
}
if (!audioTest || typeof audioTest.canPlayType !== 'function') {
return self;
}
var mpegTest = audioTest.canPlayType('audio/mpeg;').replace(/^no$/, '');
// Opera version <33 has mixed MP3 support, so we need to check for and block it.
var ua = self._navigator ? self._navigator.userAgent : '';
var checkOpera = ua.match(/OPR\/([0-6].)/g);
var isOldOpera = (checkOpera && parseInt(checkOpera[0].split('/')[1], 10) < 33);
var checkSafari = ua.indexOf('Safari') !== -1 && ua.indexOf('Chrome') === -1;
var safariVersion = ua.match(/Version\/(.*?) /);
var isOldSafari = (checkSafari && safariVersion && parseInt(safariVersion[1], 10) < 15);
self._codecs = {
mp3: !!(!isOldOpera && (mpegTest || audioTest.canPlayType('audio/mp3;').replace(/^no$/, ''))),
mpeg: !!mpegTest,
opus: !!audioTest.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/, ''),
ogg: !!audioTest.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, ''),
oga: !!audioTest.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, ''),
wav: !!(audioTest.canPlayType('audio/wav; codecs="1"') || audioTest.canPlayType('audio/wav')).replace(/^no$/, ''),
aac: !!audioTest.canPlayType('audio/aac;').replace(/^no$/, ''),
caf: !!audioTest.canPlayType('audio/x-caf;').replace(/^no$/, ''),
m4a: !!(audioTest.canPlayType('audio/x-m4a;') || audioTest.canPlayType('audio/m4a;') || audioTest.canPlayType('audio/aac;')).replace(/^no$/, ''),
m4b: !!(audioTest.canPlayType('audio/x-m4b;') || audioTest.canPlayType('audio/m4b;') || audioTest.canPlayType('audio/aac;')).replace(/^no$/, ''),
mp4: !!(audioTest.canPlayType('audio/x-mp4;') || audioTest.canPlayType('audio/mp4;') || audioTest.canPlayType('audio/aac;')).replace(/^no$/, ''),
weba: !!(!isOldSafari && audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, '')),
webm: !!(!isOldSafari && audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, '')),
dolby: !!audioTest.canPlayType('audio/mp4; codecs="ec-3"').replace(/^no$/, ''),
flac: !!(audioTest.canPlayType('audio/x-flac;') || audioTest.canPlayType('audio/flac;')).replace(/^no$/, '')
};
return self;
},
/**
* Some browsers/devices will only allow audio to be played after a user interaction.
* Attempt to automatically unlock audio on the first user interaction.
* Concept from: http://paulbakaus.com/tutorials/html5/web-audio-on-ios/
* @return {Howler}
*/
_unlockAudio: function() {
var self = this || Howler;
// Only run this if Web Audio is supported and it hasn't already been unlocked.
if (self._audioUnlocked || !self.ctx) {
return;
}
self._audioUnlocked = false;
self.autoUnlock = false;
// Some mobile devices/platforms have distortion issues when opening/closing tabs and/or web views.
// Bugs in the browser (especially Mobile Safari) can cause the sampleRate to change from 44100 to 48000.
// By calling Howler.unload(), we create a new AudioContext with the correct sampleRate.
if (!self._mobileUnloaded && self.ctx.sampleRate !== 44100) {
self._mobileUnloaded = true;
self.unload();
}
// Scratch buffer for enabling iOS to dispose of web audio buffers correctly, as per:
// http://stackoverflow.com/questions/24119684
self._scratchBuffer = self.ctx.createBuffer(1, 1, 22050);
// Call this method on touch start to create and play a buffer,
// then check if the audio actually played to determine if
// audio has now been unlocked on iOS, Android, etc.
var unlock = function(e) {
// Create a pool of unlocked HTML5 Audio objects that can
// be used for playing sounds without user interaction. HTML5
// Audio objects must be individually unlocked, as opposed
// to the WebAudio API which only needs a single activation.
// This must occur before WebAudio setup or the source.onended
// event will not fire.
while (self._html5AudioPool.length < self.html5PoolSize) {
try {
var audioNode = new Audio();
// Mark this Audio object as unlocked to ensure it can get returned
// to the unlocked pool when released.
audioNode._unlocked = true;
// Add the audio node to the pool.
self._releaseHtml5Audio(audioNode);
} catch (e) {
self.noAudio = true;
break;
}
}
// Loop through any assigned audio nodes and unlock them.
for (var i=0; i= 55.
if (typeof self.ctx.resume === 'function') {
self.ctx.resume();
}
// Setup a timeout to check that we are unlocked on the next event loop.
source.onended = function() {
source.disconnect(0);
// Update the unlocked state and prevent this check from happening again.
self._audioUnlocked = true;
// Remove the touch start listener.
document.removeEventListener('touchstart', unlock, true);
document.removeEventListener('touchend', unlock, true);
document.removeEventListener('click', unlock, true);
document.removeEventListener('keydown', unlock, true);
// Let all sounds know that audio has been unlocked.
for (var i=0; i 0 ? sound._seek : self._sprite[sprite][0] / 1000);
var duration = Math.max(0, ((self._sprite[sprite][0] + self._sprite[sprite][1]) / 1000) - seek);
var timeout = (duration * 1000) / Math.abs(sound._rate);
var start = self._sprite[sprite][0] / 1000;
var stop = (self._sprite[sprite][0] + self._sprite[sprite][1]) / 1000;
sound._sprite = sprite;
// Mark the sound as ended instantly so that this async playback
// doesn't get grabbed by another call to play while this one waits to start.
sound._ended = false;
// Update the parameters of the sound.
var setParams = function() {
sound._paused = false;
sound._seek = seek;
sound._start = start;
sound._stop = stop;
sound._loop = !!(sound._loop || self._sprite[sprite][2]);
};
// End the sound instantly if seek is at the end.
if (seek >= stop) {
self._ended(sound);
return;
}
// Begin the actual playback.
var node = sound._node;
if (self._webAudio) {
// Fire this when the sound is ready to play to begin Web Audio playback.
var playWebAudio = function() {
self._playLock = false;
setParams();
self._refreshBuffer(sound);
// Setup the playback params.
var vol = (sound._muted || self._muted) ? 0 : sound._volume;
node.gain.setValueAtTime(vol, Howler.ctx.currentTime);
sound._playStart = Howler.ctx.currentTime;
// Play the sound using the supported method.
if (typeof node.bufferSource.start === 'undefined') {
sound._loop ? node.bufferSource.noteGrainOn(0, seek, 86400) : node.bufferSource.noteGrainOn(0, seek, duration);
} else {
sound._loop ? node.bufferSource.start(0, seek, 86400) : node.bufferSource.start(0, seek, duration);
}
// Start a new timer if none is present.
if (timeout !== Infinity) {
self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout);
}
if (!internal) {
setTimeout(function() {
self._emit('play', sound._id);
self._loadQueue();
}, 0);
}
};
if (Howler.state === 'running' && Howler.ctx.state !== 'interrupted') {
playWebAudio();
} else {
self._playLock = true;
// Wait for the audio context to resume before playing.
self.once('resume', playWebAudio);
// Cancel the end timer.
self._clearTimer(sound._id);
}
} else {
// Fire this when the sound is ready to play to begin HTML5 Audio playback.
var playHtml5 = function() {
node.currentTime = seek;
node.muted = sound._muted || self._muted || Howler._muted || node.muted;
node.volume = sound._volume * Howler.volume();
node.playbackRate = sound._rate;
// Some browsers will throw an error if this is called without user interaction.
try {
var play = node.play();
// Support older browsers that don't support promises, and thus don't have this issue.
if (play && typeof Promise !== 'undefined' && (play instanceof Promise || typeof play.then === 'function')) {
// Implements a lock to prevent DOMException: The play() request was interrupted by a call to pause().
self._playLock = true;
// Set param values immediately.
setParams();
// Releases the lock and executes queued actions.
play
.then(function() {
self._playLock = false;
node._unlocked = true;
if (!internal) {
self._emit('play', sound._id);
} else {
self._loadQueue();
}
})
.catch(function() {
self._playLock = false;
self._emit('playerror', sound._id, 'Playback was unable to start. This is most commonly an issue ' +
'on mobile devices and Chrome where playback was not within a user interaction.');
// Reset the ended and paused values.
sound._ended = true;
sound._paused = true;
});
} else if (!internal) {
self._playLock = false;
setParams();
self._emit('play', sound._id);
}
// Setting rate before playing won't work in IE, so we set it again here.
node.playbackRate = sound._rate;
// If the node is still paused, then we can assume there was a playback issue.
if (node.paused) {
self._emit('playerror', sound._id, 'Playback was unable to start. This is most commonly an issue ' +
'on mobile devices and Chrome where playback was not within a user interaction.');
return;
}
// Setup the end timer on sprites or listen for the ended event.
if (sprite !== '__default' || sound._loop) {
self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout);
} else {
self._endTimers[sound._id] = function() {
// Fire ended on this audio node.
self._ended(sound);
// Clear this listener.
node.removeEventListener('ended', self._endTimers[sound._id], false);
};
node.addEventListener('ended', self._endTimers[sound._id], false);
}
} catch (err) {
self._emit('playerror', sound._id, err);
}
};
// If this is streaming audio, make sure the src is set and load again.
if (node.src === 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA') {
node.src = self._src;
node.load();
}
// Play immediately if ready, or wait for the 'canplaythrough'e vent.
var loadedNoReadyState = (window && window.ejecta) || (!node.readyState && Howler._navigator.isCocoonJS);
if (node.readyState >= 3 || loadedNoReadyState) {
playHtml5();
} else {
self._playLock = true;
self._state = 'loading';
var listener = function() {
self._state = 'loaded';
s
// Begin playback.
playHtml5();
// Clear this listener.
node.removeEventListener(Howler._canPlayEvent, listener, false);
};
node.addEventListener(Howler._canPlayEvent, listener, false);
// Cancel the end timer.
self._clearTimer(sound._id);
}
}
return sound._id;
},
/**
* Pause playback and save current position.
* @param {Number} id The sound ID (empty to pause all in group).
* @return {Howl}
*/
pause: function(id) {
var self = this;
// If the sound hasn't loaded or a play() promise is pending, add it to the load queue to pause when capable.
if (self._state !== 'loaded' || self._playLock) {
self._queue.push({
event: 'pause',
action: function() {
self.pause(id);
}
});
return self;
}
// If no id is passed, get all ID's to be paused.
var ids = self._getSoundIds(id);
for (var i=0; i Returns the group's volume value.
* volume(id) -> Returns the sound id's current volume.
* volume(vol) -> Sets the volume of all sounds in this Howl group.
* volume(vol, id) -> Sets the volume of passed sound id.
* @return {Howl/Number} Returns self or current volume.
*/
volume: function() {
var self = this;
var args = arguments;
var vol, id;
// Determine the values based on arguments.
if (args.length === 0) {
// Return the value of the groups' volume.
return self._volume;
} else if (args.length === 1 || args.length === 2 && typeof args[1] === 'undefined') {
// First check if this is an ID, and if not, assume it is a new volume.
var ids = self._getSoundIds();
var index = ids.indexOf(args[0]);
if (index >= 0) {
id = parseInt(args[0], 10);
} else {
vol = parseFloat(args[0]);
}
} else if (args.length >= 2) {
vol = parseFloat(args[0]);
id = parseInt(args[1], 10);
}
// Update the volume or return the current volume.
var sound;
if (typeof vol !== 'undefined' && vol >= 0 && vol <= 1) {
// If the sound hasn't loaded, add it to the load queue to change volume when capable.
if (self._state !== 'loaded'|| self._playLock) {
self._queue.push({
event: 'volume',
action: function() {
self.volume.apply(self, args);
}
});
return self;
}
// Set the group volume.
if (typeof id === 'undefined') {
self._volume = vol;
}
// Update one or all volumes.
id = self._getSoundIds(id);
for (var i=0; i 0) ? len / steps : len);
var lastTick = Date.now();
// Store the value being faded to.
sound._fadeTo = to;
// Update the volume value on each interval tick.
sound._interval = setInterval(function() {
// Update the volume based on the time since the last tick.
var tick = (Date.now() - lastTick) / len;
lastTick = Date.now();
vol += diff * tick;
// Round to within 2 decimal points.
vol = Math.round(vol * 100) / 100;
// Make sure the volume is in the right bounds.
if (diff < 0) {
vol = Math.max(to, vol);
} else {
vol = Math.min(to, vol);
}
// Change the volume.
if (self._webAudio) {
sound._volume = vol;
} else {
self.volume(vol, sound._id, true);
}
// Set the group's volume.
if (isGroup) {
self._volume = vol;
}
// When the fade is complete, stop it and fire event.
if ((to < from && vol <= to) || (to > from && vol >= to)) {
clearInterval(sound._interval);
sound._interval = null;
sound._fadeTo = null;
self.volume(to, sound._id);
self._emit('fade', sound._id);
}
}, stepLen);
},
/**
* Internal method that stops the currently playing fade when
* a new fade starts, volume is changed or the sound is stopped.
* @param {Number} id The sound id.
* @return {Howl}
*/
_stopFade: function(id) {
var self = this;
var sound = self._soundById(id);
if (sound && sound._interval) {
if (self._webAudio) {
sound._node.gain.cancelScheduledValues(Howler.ctx.currentTime);
}
clearInterval(sound._interval);
sound._interval = null;
self.volume(sound._fadeTo, id);
sound._fadeTo = null;
self._emit('fade', id);
}
return self;
},
/**
* Get/set the loop parameter on a sound. This method can optionally take 0, 1 or 2 arguments.
* loop() -> Returns the group's loop value.
* loop(id) -> Returns the sound id's loop value.
* loop(loop) -> Sets the loop value for all sounds in this Howl group.
* loop(loop, id) -> Sets the loop value of passed sound id.
* @return {Howl/Boolean} Returns self or current loop value.
*/
loop: function() {
var self = this;
var args = arguments;
var loop, id, sound;
// Determine the values for loop and id.
if (args.length === 0) {
// Return the grou's loop value.
return self._loop;
} else if (args.length === 1) {
if (typeof args[0] === 'boolean') {
loop = args[0];
self._loop = loop;
} else {
// Return this sound's loop value.
sound = self._soundById(parseInt(args[0], 10));
return sound ? sound._loop : false;
}
} else if (args.length === 2) {
loop = args[0];
id = parseInt(args[1], 10);
}
// If no id is passed, get all ID's to be looped.
var ids = self._getSoundIds(id);
for (var i=0; i Returns the first sound node's current playback rate.
* rate(id) -> Returns the sound id's current playback rate.
* rate(rate) -> Sets the playback rate of all sounds in this Howl group.
* rate(rate, id) -> Sets the playback rate of passed sound id.
* @return {Howl/Number} Returns self or the current playback rate.
*/
rate: function() {
var self = this;
var args = arguments;
var rate, id;
// Determine the values based on arguments.
if (args.length === 0) {
// We will simply return the current rate of the first node.
id = self._sounds[0]._id;
} else if (args.length === 1) {
// First check if this is an ID, and if not, assume it is a new rate value.
var ids = self._getSoundIds();
var index = ids.indexOf(args[0]);
if (index >= 0) {
id = parseInt(args[0], 10);
} else {
rate = parseFloat(args[0]);
}
} else if (args.length === 2) {
rate = parseFloat(args[0]);
id = parseInt(args[1], 10);
}
// Update the playback rate or return the current value.
var sound;
if (typeof rate === 'number') {
// If the sound hasn't loaded, add it to the load queue to change playback rate when capable.
if (self._state !== 'loaded' || self._playLock) {
self._queue.push({
event: 'rate',
action: function() {
self.rate.apply(self, args);
}
});
return self;
}
// Set the group rate.
if (typeof id === 'undefined') {
self._rate = rate;
}
// Update one or all volumes.
id = self._getSoundIds(id);
for (var i=0; i Returns the first sound node's current seek position.
* seek(id) -> Returns the sound id's current seek position.
* seek(seek) -> Sets the seek position of the first sound node.
* seek(seek, id) -> Sets the seek position of passed sound id.
* @return {Howl/Number} Returns self or the current seek position.
*/
seek: function() {
var self = this;
var args = arguments;
var seek, id;
// Determine the values based on arguments.
if (args.length === 0) {
// We will simply return the current position of the first node.
if (self._sounds.length) {
id = self._sounds[0]._id;
}
} else if (args.length === 1) {
// First check if this is an ID, and if not, assume it is a new seek position.
var ids = self._getSoundIds();
var index = ids.indexOf(args[0]);
if (index >= 0) {
id = parseInt(args[0], 10);
} else if (self._sounds.length) {
id = self._sounds[0]._id;
seek = parseFloat(args[0]);
}
} else if (args.length === 2) {
seek = parseFloat(args[0]);
id = parseInt(args[1], 10);
}
// If there is no ID, bail out.
if (typeof id === 'undefined') {
return 0;
}
// If the sound hasn't loaded, add it to the load queue to seek when capable.
if (typeof seek === 'number' && (self._state !== 'loaded' || self._playLock)) {
self._queue.push({
event: 'seek',
action: function() {
self.seek.apply(self, args);
}
});
return self;
}
// Get the sound.
var sound = self._soundById(id);
if (sound) {
if (typeof seek === 'number' && seek >= 0) {
// Pause the sound and update position for restarting playback.
var playing = self.playing(id);
if (playing) {
self.pause(id, true);
}
// Move the position of the track and cancel timer.
sound._seek = seek;
sound._ended = false;
self._clearTimer(id);
// Update the seek position for HTML5 Audio.
if (!self._webAudio && sound._node && !isNaN(sound._node.duration)) {
sound._node.currentTime = seek;
}
// Seek and emit when ready.
var seekAndEmit = function() {
// Restart the playback if the sound was playing.
if (playing) {
self.play(id, true);
}
self._emit('seek', id);
};
// Wait for the play lock to be unset before emitting (HTML5 Audio).
if (playing && !self._webAudio) {
var emitSeek = function() {
if (!self._playLock) {
seekAndEmit();
} else {
setTimeout(emitSeek, 0);
}
};
setTimeout(emitSeek, 0);
} else {
seekAndEmit();
}
} else {
if (self._webAudio) {
var realTime = self.playing(id) ? Howler.ctx.currentTime - sound._playStart : 0;
var rateSeek = sound._rateSeek ? sound._rateSeek - sound._seek : 0;
return sound._seek + (rateSeek + realTime * Math.abs(sound._rate));
} else {
return sound._node.currentTime;
}
}
}
return self;
},
/**
* Check if a specific sound is currently playing or not (if id is provided), or check if at least one of the sounds in the group is playing or not.
* @param {Number} id The sound id to check. If none is passed, the whole sound group is checked.
* @return {Boolean} True if playing and false if not.
*/
playing: function(id) {
var self = this;
// Check the passed sound ID (if any).
if (typeof id === 'number') {
var sound = self._soundById(id);
return sound ? !sound._paused : false;
}
// Otherwise, loop through all sounds and check if any are playing.
for (var i=0; i= 0) {
Howler._howls.splice(index, 1);
}
// Delete this sound from the cache (if no other Howl is using it).
var remCache = true;
for (i=0; i= 0) {
remCache = false;
break;
}
}
if (cache && remCache) {
delete cache[self._src];
}
// Clear global errors.
Howler.noAudio = false;
// Clear out `self`.
self._state = 'unloaded';
self._sounds = [];
self = null;
return null;
},
/**
* Listen to a custom event.
* @param {String} event Event name.
* @param {Function} fn Listener to call.
* @param {Number} id (optional) Only listen to events for this sound.
* @param {Number} once (INTERNAL) Marks event to fire only once.
* @return {Howl}
*/
on: function(event, fn, id, once) {
var self = this;
var events = self['_on' + event];
if (typeof fn === 'function') {
events.push(once ? {id: id, fn: fn, once: once} : {id: id, fn: fn});
}
return self;
},
/**
* Remove a custom event. Call without parameters to remove all events.
* @param {String} event Event name.
* @param {Function} fn Listener to remove. Leave empty to remove all.
* @param {Number} id (optional) Only remove events for this sound.
* @return {Howl}
*/
off: function(event, fn, id) {
var self = this;
var events = self['_on' + event];
var i = 0;
// Allow passing just an event and ID.
if (typeof fn === 'number') {
id = fn;
fn = null;
}
if (fn || id) {
// Loop through event store and remove the passed function.
for (i=0; i=0; i--) {
// Only fire the listener if the correct ID is used.
if (!events[i].id || events[i].id === id || event === 'load') {
setTimeout(function(fn) {
fn.call(this, id, msg);
}.bind(self, events[i].fn), 0);
// If this event was setup with `once`, remove it.
if (events[i].once) {
self.off(event, events[i].fn, events[i].id);
}
}
}
// Pass the event type into load queue so that it can continue stepping.
self._loadQueue(event);
return self;
},
/**
* Queue of actions initiated before the sound has loaded.
* These will be called in sequence, with the next only firing
* after the previous has finished executing (even if async like play).
* @return {Howl}
*/
_loadQueue: function(event) {
var self = this;
if (self._queue.length > 0) {
var task = self._queue[0];
// Remove this task if a matching event was passed.
if (task.event === event) {
self._queue.shift();
self._loadQueue();
}
// Run the task if no event type is passed.
if (!event) {
task.action();
}
}
return self;
},
/**
* Fired when playback ends at the end of the duration.
* @param {Sound} sound The sound object to work with.
* @return {Howl}
*/
_ended: function(sound) {
var self = this;
var sprite = sound._sprite;
// If we are using IE and there was network latency we may be clipping
// audio before it completes playing. Lets check the node to make sure it
// believes it has completed, before ending the playback.
if (!self._webAudio && sound._node && !sound._node.paused && !sound._node.ended && sound._node.currentTime < sound._stop) {
setTimeout(self._ended.bind(self, sound), 100);
return self;
}
// Should this sound loop?
var loop = !!(sound._loop || self._sprite[sprite][2]);
// Fire the ended event.
self._emit('end', sound._id);
// Restart the playback for HTML5 Audio loop.
if (!self._webAudio && loop) {
self.stop(sound._id, true).play(sound._id);
}
// Restart this timer if on a Web Audio loop.
if (self._webAudio && loop) {
self._emit('play', sound._id);
sound._seek = sound._start || 0;
sound._rateSeek = 0;
sound._playStart = Howler.ctx.currentTime;
var timeout = ((sound._stop - sound._start) * 1000) / Math.abs(sound._rate);
self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout);
}
// Mark the node as paused.
if (self._webAudio && !loop) {
sound._paused = true;
sound._ended = true;
sound._seek = sound._start || 0;
sound._rateSeek = 0;
self._clearTimer(sound._id);
// Clean up the buffer source.
self._cleanBuffer(sound._node);
// Attempt to auto-suspend AudioContext if no sounds are still playing.
Howler._autoSuspend();
}
// When using a sprite, end the track.
if (!self._webAudio && !loop) {
self.stop(sound._id, true);
}
return self;
},
/**
* Clear the end timer for a sound playback.
* @param {Number} id The sound ID.
* @return {Howl}
*/
_clearTimer: function(id) {
var self = this;
if (self._endTimers[id]) {
// Clear the timeout or remove the ended listener.
if (typeof self._endTimers[id] !== 'function') {
clearTimeout(self._endTimers[id]);
} else {
var sound = self._soundById(id);
if (sound && sound._node) {
sound._node.removeEventListener('ended', self._endTimers[id], false);
}
}
delete self._endTimers[id];
}
return self;
},
/**
* Return the sound identified by this ID, or return null.
* @param {Number} id Sound ID
* @return {Object} Sound object or null.
*/
_soundById: function(id) {
var self = this;
// Loop through all sounds and find the one with this ID.
for (var i=0; i=0; i--) {
if (cnt <= limit) {
return;
}
if (self._sounds[i]._ended) {
// Disconnect the audio source when using Web Audio.
if (self._webAudio && self._sounds[i]._node) {
self._sounds[i]._node.disconnect(0);
}
// Remove sounds until we have the pool size.
self._sounds.splice(i, 1);
cnt--;
}
}
},
/**
* Get all ID's from the sounds pool.
* @param {Number} id Only return one ID if one is passed.
* @return {Array} Array of IDs.
*/
_getSoundIds: function(id) {
var self = this;
if (typeof id === 'undefined') {
var ids = [];
for (var i=0; i= 0;
if (!node.bufferSource) {
return self;
}
if (Howler._scratchBuffer && node.bufferSource) {
node.bufferSource.onended = null;
node.bufferSource.disconnect(0);
if (isIOS) {
try { node.bufferSource.buffer = Howler._scratchBuffer; } catch(e) {}
}
}
node.bufferSource = null;
return self;
},
/**
* Set the source to a 0-second silence to stop any downloading (except in IE).
* @param {Object} node Audio node to clear.
*/
_clearSound: function(node) {
var checkIE = /MSIE |Trident\//.test(Howler._navigator && Howler._navigator.userAgent);
if (!checkIE) {
node.src = 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA';
}
}
};
/** Single Sound Methods **/
/***************************************************************************/
/**
* Setup the sound object, which each node attached to a Howl group is contained in.
* @param {Object} howl The Howl parent group.
*/
var Sound = function(howl) {
this._parent = howl;
this.init();
};
Sound.prototype = {
/**
* Initialize a new Sound object.
* @return {Sound}
*/
init: function() {
var self = this;
var parent = self._parent;
// Setup the default parameters.
self._muted = parent._muted;
self._loop = parent._loop;
self._volume = parent._volume;
self._rate = parent._rate;
self._seek = 0;
self._paused = true;
self._ended = true;
self._sprite = '__default';
// Generate a unique ID for this sound.
self._id = ++Howler._counter;
// Add itself to the parent's pool.
parent._sounds.push(self);
// Create the new node.
self.create();
return self;
},
/**
* Create and setup a new sound object, whether HTML5 Audio or Web Audio.
* @return {Sound}
*/
create: function() {
var self = this;
var parent = self._parent;
var volume = (Howler._muted || self._muted || self._parent._muted) ? 0 : self._volume;
if (parent._webAudio) {
// Create the gain node for controlling volume (the source will connect to this).
self._node = (typeof Howler.ctx.createGain === 'undefined') ? Howler.ctx.createGainNode() : Howler.ctx.createGain();
self._node.gain.setValueAtTime(volume, Howler.ctx.currentTime);
self._node.paused = true;
self._node.connect(Howler.masterGain);
} else if (!Howler.noAudio) {
// Get an unlocked Audio object from the pool.
self._node = Howler._obtainHtml5Audio();
// Listen for errors (http://dev.w3.org/html5/spec-author-view/spec.html#mediaerror).
self._errorFn = self._errorListener.bind(self);
self._node.addEventListener('error', self._errorFn, false);
// Listen for 'canplaythrough' event to let us know the sound is ready.
self._loadFn = self._loadListener.bind(self);
self._node.addEventListener(Howler._canPlayEvent, self._loadFn, false);
// Listen for the 'ended' event on the sound to account for edge-case where
// a finite sound has a duration of Infinity.
self._endFn = self._endListener.bind(self);
self._node.addEventListener('ended', self._endFn, false);
// Setup the new audio node.
self._node.src = parent._src;
self._node.preload = parent._preload === true ? 'auto' : parent._preload;
self._node.volume = volume * Howler.volume();
// Begin loading the source.
self._node.load();
}
return self;
},
/**
* Reset the parameters of this sound to the original state (for recycle).
* @return {Sound}
*/
reset: function() {
var self = this;
var parent = self._parent;
// Reset all of the parameters of this sound.
self._muted = parent._muted;
self._loop = parent._loop;
self._volume = parent._volume;
self._rate = parent._rate;
self._seek = 0;
self._rateSeek = 0;
self._paused = true;
self._ended = true;
self._sprite = '__default';
// Generate a new ID so that it isn't confused with the previous sound.
self._id = ++Howler._counter;
return self;
},
/**
* HTML5 Audio error listener callback.
*/
_errorListener: function() {
var self = this;
// Fire an error event and pass back the code.
self._parent._emit('loaderror', self._id, self._node.error ? self._node.error.code : 0);
// Clear the event listener.
self._node.removeEventListener('error', self._errorFn, false);
},
/**
* HTML5 Audio canplaythrough listener callback.
*/
_loadListener: function() {
var self = this;
var parent = self._parent;
// Round up the duration to account for the lower precision in HTML5 Audio.
parent._duration = Math.ceil(self._node.duration * 10) / 10;
// Setup a sprite if none is defined.
if (Object.keys(parent._sprite).length === 0) {
parent._sprite = {__default: [0, parent._duration * 1000]};
}
if (parent._state !== 'loaded') {
parent._state = 'loaded';
parent._emit('load');
parent._loadQueue();
}
// Clear the event listener.
self._node.removeEventListener(Howler._canPlayEvent, self._loadFn, false);
},
/**
* HTML5 Audio ended listener callback.
*/
_endListener: function() {
var self = this;
var parent = self._parent;
// Only handle the `ended`` event if the duration is Infinity.
if (parent._duration === Infinity) {
// Update the parent duration to match the real audio duration.
// Round up the duration to account for the lower precision in HTML5 Audio.
parent._duration = Math.ceil(self._node.duration * 10) / 10;
// Update the sprite that corresponds to the real duration.
if (parent._sprite.__default[1] === Infinity) {
parent._sprite.__default[1] = parent._duration * 1000;
}
// Run the regular ended method.
parent._ended(self);
}
// Clear the event listener since the duration is now correct.
self._node.removeEventListener('ended', self._endFn, false);
}
};
/** Helper Methods **/
/***************************************************************************/
var cache = {};
/**
* Buffer a sound from URL, Data URI or cache and decode to audio source (Web Audio API).
* @param {Howl} self
*/
var loadBuffer = function(self) {
var url = self._src;
// Check if the buffer has already been cached and use it instead.
if (cache[url]) {
// Set the duration from the cache.
self._duration = cache[url].duration;
// Load the sound into this Howl.
loadSound(self);
return;
}
if (/^data:[^;]+;base64,/.test(url)) {
// Decode the base64 data URI without XHR, since some browsers don't support it.
var data = atob(url.split(',')[1]);
var dataView = new Uint8Array(data.length);
for (var i=0; i 0) {
cache[self._src] = buffer;
loadSound(self, buffer);
} else {
error();
}
};
// Decode the buffer into an audio source.
if (typeof Promise !== 'undefined' && Howler.ctx.decodeAudioData.length === 1) {
Howler.ctx.decodeAudioData(arraybuffer).then(success).catch(error);
} else {
Howler.ctx.decodeAudioData(arraybuffer, success, error);
}
}
/**
* Sound is now loaded, so finish setting everything up and fire the loaded event.
* @param {Howl} self
* @param {Object} buffer The decoded buffer sound source.
*/
var loadSound = function(self, buffer) {
// Set the duration.
if (buffer && !self._duration) {
self._duration = buffer.duration;
}
// Setup a sprite if none is defined.
if (Object.keys(self._sprite).length === 0) {
self._sprite = {__default: [0, self._duration * 1000]};
}
// Fire the loaded event.
if (self._state !== 'loaded') {
self._state = 'loaded';
self._emit('load');
self._loadQueue();
}
};
/**
* Setup the audio context when available, or switch to HTML5 Audio mode.
*/
var setupAudioContext = function() {
// If we have already detected that Web Audio isn't supported, don't run this step again.
if (!Howler.usingWebAudio) {
return;
}
// Check if we are using Web Audio and setup the AudioContext if we are.
try {
if (typeof AudioContext !== 'undefined') {
Howler.ctx = new AudioContext();
} else if (typeof webkitAudioContext !== 'undefined') {
Howler.ctx = new webkitAudioContext();
} else {
Howler.usingWebAudio = false;
}
} catch(e) {
Howler.usingWebAudio = false;
}
// If the audio context creation still failed, set using web audio to false.
if (!Howler.ctx) {
Howler.usingWebAudio = false;
}
// Check if a webview is being used on iOS8 or earlier (rather than the browser).
// If it is, disable Web Audio as it causes crashing.
var iOS = (/iP(hone|od|ad)/.test(Howler._navigator && Howler._navigator.platform));
var appVersion = Howler._navigator && Howler._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/);
var version = appVersion ? parseInt(appVersion[1], 10) : null;
if (iOS && version && version < 9) {
var safari = /safari/.test(Howler._navigator && Howler._navigator.userAgent.toLowerCase());
if (Howler._navigator && !safari) {
Howler.usingWebAudio = false;
}
}
// Create and expose the master GainNode when using Web Audio (useful for plugins or advanced usage).
if (Howler.usingWebAudio) {
Howler.masterGain = (typeof Howler.ctx.createGain === 'undefined') ? Howler.ctx.createGainNode() : Howler.ctx.createGain();
Howler.masterGain.gain.setValueAtTime(Howler._muted ? 0 : Howler._volume, Howler.ctx.currentTime);
Howler.masterGain.connect(Howler.ctx.destination);
}
// Re-run the setup on Howler.
Howler._setup();
};
// Add support for AMD (Asynchronous Module Definition) libraries such as require.js.
if (typeof define === 'function' && define.amd) {
define([], function() {
return {
Howler: Howler,
Howl: Howl
};
});
}
// Add support for CommonJS libraries such as browserify.
if (typeof exports !== 'undefined') {
exports.Howler = Howler;
exports.Howl = Howl;
}
// Add to global in Node.js (for testing, etc).
if (typeof global !== 'undefined') {
global.HowlerGlobal = HowlerGlobal;
global.Howler = Howler;
global.Howl = Howl;
global.Sound = Sound;
} else if (typeof window !== 'undefined') { // Define globally in case AMD is not available or unused.
window.HowlerGlobal = HowlerGlobal;
window.Howler = Howler;
window.Howl = Howl;
window.Sound = Sound;
}
})();