diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b512c09d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..6e778b4f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "trailingComma": "all", + "singleQuote": true +} diff --git a/bower.json b/bower.json deleted file mode 100644 index cebe94d4..00000000 --- a/bower.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "howler.js", - "description": "Javascript audio library for the modern web.", - "homepage": "https://howlerjs.com", - "keywords": [ - "howler", - "howler.js", - "audio", - "sound", - "web audio", - "webaudio", - "html5", - "html5 audio", - "audio sprite", - "audiosprite" - ], - "authors": [ - "James Simpson (http://goldfirestudios.com)" - ], - "repository": { - "type": "git", - "url": "git://github.com/goldfire/howler.js.git" - }, - "main": "dist/howler.js", - "license": "MIT", - "moduleType": [ - "amd", - "globals", - "node" - ], - "ignore": [ - "tests", - "examples" - ] -} diff --git a/dist/core.d.ts b/dist/core.d.ts new file mode 100644 index 00000000..e7d2342b --- /dev/null +++ b/dist/core.d.ts @@ -0,0 +1,12 @@ +/*! + * howler.js v2.2.3 + * howlerjs.com + * + * (c) 2013-2020, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */ +import Howler from './howler'; +import Howl from './howl'; +export { Howler, Howl }; diff --git a/dist/core.js b/dist/core.js new file mode 100644 index 00000000..d8bd4cb8 --- /dev/null +++ b/dist/core.js @@ -0,0 +1,1526 @@ +// src/howler.ts +var Howler = class { + constructor() { + this.masterGain = null; + this.noAudio = false; + this.usingWebAudio = true; + this.autoSuspend = true; + this.ctx = null; + this.autoUnlock = true; + this._counter = 1e3; + this._html5AudioPool = []; + this.html5PoolSize = 10; + this._codecs = {}; + this._howls = []; + this._muted = false; + this._volume = 1; + this._canPlayEvent = "canplaythrough"; + this._navigator = window.navigator; + this._audioUnlocked = false; + this._mobileUnloaded = false; + this.state = "suspended"; + this._suspendTimer = null; + this._setup(); + } + volume(vol) { + const volume = parseFloat(vol); + if (!this.ctx) { + this._setupAudioContext(); + } + if (typeof volume !== "undefined" && volume >= 0 && volume <= 1) { + this._volume = volume; + if (this._muted) { + return this; + } + if (this.usingWebAudio && this.masterGain && this.ctx) { + this.masterGain.gain.setValueAtTime(volume, this.ctx.currentTime); + } + for (var i = 0; i < this._howls.length; i++) { + if (!this._howls[i]._webAudio) { + var ids = this._howls[i]._getSoundIds(); + for (var j = 0; j < ids.length; j++) { + var sound = this._howls[i]._soundById(ids[j]); + if (sound && sound._node) { + sound._node.volume = sound._volume * volume; + } + } + } + } + return volume; + } + return this._volume; + } + mute(muted) { + if (!this.ctx) { + this._setupAudioContext(); + } + this._muted = muted; + if (this.usingWebAudio && this.masterGain && this.ctx) { + this.masterGain.gain.setValueAtTime(muted ? 0 : this._volume, this.ctx.currentTime); + } + for (var i = 0; i < this._howls.length; i++) { + if (!this._howls[i]._webAudio) { + var ids = this._howls[i]._getSoundIds(); + for (var j = 0; j < ids.length; j++) { + var sound = this._howls[i]._soundById(ids[j]); + if (sound && sound._node) { + sound._node.muted = muted ? true : sound._muted; + } + } + } + } + return this; + } + stop() { + for (var i = 0; i < this._howls.length; i++) { + this._howls[i].stop(); + } + return this; + } + unload() { + for (var i = this._howls.length - 1; i >= 0; i--) { + this._howls[i].unload(); + } + if (this.usingWebAudio && this.ctx && typeof this.ctx.close !== "undefined") { + this.ctx.close(); + this.ctx = null; + this._setupAudioContext(); + } + return this; + } + codecs(ext) { + return this._codecs[ext.replace(/^x-/, "")]; + } + _setup() { + this.state = this.ctx ? this.ctx.state || "suspended" : "suspended"; + this._autoSuspend(); + if (!this.usingWebAudio) { + if (typeof Audio !== "undefined") { + try { + var test = new Audio(); + if (typeof test.oncanplaythrough === "undefined") { + this._canPlayEvent = "canplay"; + } + } catch (e) { + this.noAudio = true; + } + } else { + this.noAudio = true; + } + } + try { + var test = new Audio(); + if (test.muted) { + this.noAudio = true; + } + } catch (e) { + } + if (!this.noAudio) { + this._setupCodecs(); + } + return this; + } + _setupAudioContext() { + if (!this.usingWebAudio) { + return; + } + try { + if (typeof AudioContext !== "undefined") { + this.ctx = new AudioContext(); + } else if (typeof webkitAudioContext !== "undefined") { + this.ctx = new webkitAudioContext(); + } else { + this.usingWebAudio = false; + } + } catch (e) { + this.usingWebAudio = false; + } + if (!this.ctx) { + this.usingWebAudio = false; + } + var iOS = /iP(hone|od|ad)/.test(this._navigator && this._navigator.platform); + var appVersion = this._navigator && this._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/); + var version = appVersion ? parseInt(appVersion[1], 10) : null; + if (iOS && version && version < 9) { + var safari = /safari/.test(this._navigator && this._navigator.userAgent.toLowerCase()); + if (this._navigator && !safari) { + this.usingWebAudio = false; + } + } + if (this.usingWebAudio) { + this.masterGain = typeof this.ctx.createGain === "undefined" ? this.ctx.createGainNode() : this.ctx.createGain(); + this.masterGain.gain.setValueAtTime(this._muted ? 0 : this._volume, this.ctx.currentTime); + this.masterGain.connect(this.ctx.destination); + } + this._setup(); + } + _setupCodecs() { + let audioTest = null; + try { + audioTest = typeof Audio !== "undefined" ? new Audio() : null; + } catch (err) { + return this; + } + if (!audioTest || typeof audioTest.canPlayType !== "function") { + return this; + } + const mpegTest = audioTest.canPlayType("audio/mpeg;").replace(/^no$/, ""); + const ua = this._navigator ? this._navigator.userAgent : ""; + const checkOpera = ua.match(/OPR\/([0-6].)/g); + const isOldOpera = checkOpera && parseInt(checkOpera[0].split("/")[1], 10) < 33; + const checkSafari = ua.indexOf("Safari") !== -1 && ua.indexOf("Chrome") === -1; + const safariVersion = ua.match(/Version\/(.*?) /); + const isOldSafari = checkSafari && safariVersion && parseInt(safariVersion[1], 10) < 15; + this._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 this; + } + _unlockAudio() { + if (this._audioUnlocked || !this.ctx) { + return this; + } + this.autoUnlock = false; + if (!this._mobileUnloaded && this.ctx.sampleRate !== 44100) { + this._mobileUnloaded = true; + this.unload(); + } + this._scratchBuffer = this.ctx.createBuffer(1, 1, 22050); + const unlock = () => { + while (this._html5AudioPool.length < this.html5PoolSize) { + try { + var audioNode = new Audio(); + audioNode._unlocked = true; + this._releaseHtml5Audio(audioNode); + } catch (e) { + this.noAudio = true; + break; + } + } + for (var i = 0; i < this._howls.length; i++) { + if (!this._howls[i]._webAudio) { + var ids = this._howls[i]._getSoundIds(); + for (var j = 0; j < ids.length; j++) { + var sound = this._howls[i]._soundById(ids[j]); + if (sound && sound._node && !sound._node._unlocked) { + sound._node._unlocked = true; + sound._node.load(); + } + } + } + } + this._autoResume(); + const source = this.ctx.createBufferSource(); + source.buffer = this._scratchBuffer; + source.connect(this.ctx.destination); + if (typeof source.start === "undefined") { + source.noteOn(0); + } else { + source.start(0); + } + if (this.ctx && typeof this.ctx.resume === "function") { + this.ctx.resume(); + } + source.onended = () => { + source.disconnect(0); + this._audioUnlocked = true; + document.removeEventListener("touchstart", unlock, true); + document.removeEventListener("touchend", unlock, true); + document.removeEventListener("click", unlock, true); + document.removeEventListener("keydown", unlock, true); + for (var i2 = 0; i2 < this._howls.length; i2++) { + this._howls[i2]._emit("unlock"); + } + }; + }; + document.addEventListener("touchstart", unlock, true); + document.addEventListener("touchend", unlock, true); + document.addEventListener("click", unlock, true); + document.addEventListener("keydown", unlock, true); + return this; + } + _obtainHtml5Audio() { + if (this._html5AudioPool.length) { + return this._html5AudioPool.pop(); + } + var testPlay = new Audio().play(); + if (testPlay && typeof Promise !== "undefined" && (testPlay instanceof Promise || typeof testPlay.then === "function")) { + testPlay.catch(function() { + console.warn("HTML5 Audio pool exhausted, returning potentially locked audio object."); + }); + } + return new Audio(); + } + _releaseHtml5Audio(audio) { + if (audio._unlocked) { + this._html5AudioPool.push(audio); + } + return this; + } + _autoSuspend() { + if (!this.autoSuspend || !this.ctx || typeof this.ctx.suspend === "undefined" || !this.usingWebAudio) { + return; + } + for (var i = 0; i < this._howls.length; i++) { + if (this._howls[i]._webAudio) { + for (var j = 0; j < this._howls[i]._sounds.length; j++) { + if (!this._howls[i]._sounds[j]._paused) { + return this; + } + } + } + } + if (this._suspendTimer) { + clearTimeout(this._suspendTimer); + } + this._suspendTimer = setTimeout(() => { + if (!this.autoSuspend) { + return; + } + this._suspendTimer = null; + this.state = "suspending"; + const handleSuspension = () => { + this.state = "suspended"; + if (this._resumeAfterSuspend) { + delete this._resumeAfterSuspend; + this._autoResume(); + } + }; + this.ctx.suspend().then(handleSuspension, handleSuspension); + }, 3e4); + return this; + } + _autoResume() { + if (!this.ctx || typeof this.ctx.resume === "undefined" || !this.usingWebAudio) { + return; + } + if (this.state === "running" && this.ctx.state !== "interrupted" && this._suspendTimer) { + clearTimeout(this._suspendTimer); + this._suspendTimer = null; + } else if (this.state === "suspended" || this.state === "running" && this.ctx.state === "interrupted") { + this.ctx.resume().then(() => { + this.state = "running"; + for (var i = 0; i < this._howls.length; i++) { + this._howls[i]._emit("resume"); + } + }); + if (this._suspendTimer) { + clearTimeout(this._suspendTimer); + this._suspendTimer = null; + } + } else if (this.state === "suspending") { + this._resumeAfterSuspend = true; + } + return this; + } +}; +var HowlerSingleton = new Howler(); +var howler_default = HowlerSingleton; + +// src/helpers.ts +var cache = {}; +function loadBuffer(self) { + var url = self._src; + if (cache[url]) { + self._duration = cache[url].duration; + loadSound(self); + return; + } + if (/^data:[^;]+;base64,/.test(url)) { + var data = atob(url.split(",")[1]); + var dataView = new Uint8Array(data.length); + for (var i = 0; i < data.length; ++i) { + dataView[i] = data.charCodeAt(i); + } + decodeAudioData(dataView.buffer, self); + } else { + var xhr = new XMLHttpRequest(); + xhr.open(self._xhr.method, url, true); + xhr.withCredentials = self._xhr.withCredentials; + xhr.responseType = "arraybuffer"; + if (self._xhr) { + Object.keys(self._xhr).forEach(function(key) { + xhr.setRequestHeader(key, self._xhr[key]); + }); + } + xhr.onload = () => { + var code = (xhr.status + "")[0]; + if (code !== "0" && code !== "2" && code !== "3") { + self._emit("loaderror", null, "Failed loading audio file with status: " + xhr.status + "."); + return; + } + decodeAudioData(xhr.response, self); + }; + xhr.onerror = () => { + if (self._webAudio) { + self._html5 = true; + self._webAudio = false; + self._sounds = []; + delete cache[url]; + self.load(); + } + }; + safeXhrSend(xhr); + } +} +function safeXhrSend(xhr) { + try { + xhr.send(); + } catch (e) { + console.error("XHR Request failed: ", e); + } +} +function decodeAudioData(arraybuffer, self) { + function error() { + self._emit("loaderror", null, "Decoding audio data failed."); + } + function success(buffer) { + if (buffer && self._sounds.length > 0) { + cache[self._src] = buffer; + loadSound(self, buffer); + } else { + error(); + } + } + if (typeof Promise !== "undefined" && howler_default.ctx.decodeAudioData.length === 1) { + howler_default.ctx.decodeAudioData(arraybuffer).then(success).catch(error); + } else { + howler_default.ctx.decodeAudioData(arraybuffer, success, error); + } +} +function loadSound(self, buffer) { + if (buffer && !self._duration) { + self._duration = buffer.duration; + } + if (Object.keys(self._sprite).length === 0) { + self._sprite = { __default: [0, self._duration * 1e3] }; + } + if (self._state !== "loaded") { + self._state = "loaded"; + self._emit("load"); + self._loadQueue(); + } +} + +// src/sound.ts +var Sound = class { + constructor(howl) { + this._seek = 0; + this._paused = true; + this._ended = true; + this._sprite = "__default"; + this._errorFn = () => { + }; + this._loadFn = () => { + }; + this._endFn = () => { + }; + this._playStart = 0; + this._start = 0; + this._stop = 0; + this._fadeTo = null; + this._interval = null; + this._parent = howl; + this._muted = Boolean(howl._muted); + this._loop = Boolean(howl._loop); + this._volume = howl._volume; + this._rate = howl._rate; + this._id = ++howler_default._counter; + this._parent._sounds.push(this); + if (this._parent._webAudio && howler_default.ctx) { + this._node = typeof howler_default.ctx.createGain === "undefined" ? howler_default.ctx.createGainNode() : howler_default.ctx.createGain(); + } else { + this._node = howler_default._obtainHtml5Audio(); + } + this.create(); + } + create() { + var parent = this._parent; + var volume = howler_default._muted || this._muted || this._parent._muted ? 0 : this._volume; + if (parent._webAudio && howler_default.ctx) { + this._node.gain.setValueAtTime(volume, howler_default.ctx.currentTime); + this._node.paused = true; + this._node.connect(howler_default.masterGain); + } else if (!howler_default.noAudio) { + this._errorFn = this._errorListener.bind(this); + this._node.addEventListener("error", this._errorFn, false); + this._loadFn = this._loadListener.bind(this); + this._node.addEventListener(howler_default._canPlayEvent, this._loadFn, false); + this._endFn = this._endListener.bind(this); + this._node.addEventListener("ended", this._endFn, false); + this._node.src = parent._src; + this._node.preload = parent._preload === true ? "auto" : parent._preload; + this._node.volume = volume * howler_default.volume(); + this._node.load(); + } + return this; + } + reset() { + var parent = this._parent; + this._muted = parent._muted; + this._loop = parent._loop; + this._volume = parent._volume; + this._rate = parent._rate; + this._seek = 0; + this._rateSeek = 0; + this._paused = true; + this._ended = true; + this._sprite = "__default"; + this._id = ++howler_default._counter; + return this; + } + _errorListener() { + this._parent._emit("loaderror", this._id, this._node.error instanceof MediaError ? this._node.error.code : 0); + this._node.removeEventListener("error", this._errorFn, false); + } + _loadListener() { + const parent = this._parent; + parent._duration = Math.ceil(this._node.duration * 10) / 10; + if (Object.keys(parent._sprite).length === 0) { + parent._sprite = { __default: [0, parent._duration * 1e3] }; + } + if (parent._state !== "loaded") { + parent._state = "loaded"; + parent._emit("load"); + parent._loadQueue(); + } + this._node.removeEventListener(howler_default._canPlayEvent, this._loadFn, false); + } + _endListener() { + const parent = this._parent; + if (parent._duration === Infinity) { + parent._duration = Math.ceil(this._node.duration * 10) / 10; + if (parent._sprite.__default[1] === Infinity) { + parent._sprite.__default[1] = parent._duration * 1e3; + } + parent._ended(this); + } + this._node.removeEventListener("ended", this._endFn, false); + } +}; +var sound_default = Sound; + +// src/howl.ts +var Howl = class { + constructor(o) { + this._autoplay = false; + this._format = []; + this._html5 = false; + this._muted = false; + this._loop = false; + this._pool = 5; + this._preload = true; + this._rate = 1; + this._sprite = {}; + this._src = []; + this._volume = 1; + this._duration = 0; + this._state = "unloaded"; + this._sounds = []; + this._endTimers = {}; + this._queue = []; + this._playLock = false; + this._onend = []; + this._onfade = []; + this._onload = []; + this._onloaderror = []; + this._onplayerror = []; + this._onpause = []; + this._onplay = []; + this._onstop = []; + this._onmute = []; + this._onvolume = []; + this._onrate = []; + this._onseek = []; + this._onunlock = []; + this._onresume = []; + if (!o.src || o.src.length === 0) { + console.error("An array of source files must be passed with any new Howl."); + return; + } + if (!howler_default.ctx) { + howler_default._setupAudioContext(); + } + this._format = o.format === void 0 ? [] : typeof o.format !== "string" ? o.format : [o.format]; + this._html5 = o.html5 || false; + this._muted = o.mute || false; + this._loop = o.loop || false; + this._pool = o.pool || 5; + this._preload = typeof o.preload === "boolean" || o.preload === "metadata" ? o.preload : true; + this._rate = o.rate || 1; + this._sprite = o.sprite || {}; + this._src = typeof o.src !== "string" ? o.src : [o.src]; + this._volume = o.volume !== void 0 ? o.volume : 1; + this._xhr = { + method: o.xhr && o.xhr.method ? o.xhr.method : "GET", + headers: o.xhr && o.xhr.headers ? o.xhr.headers : void 0, + withCredentials: o.xhr && o.xhr.withCredentials ? o.xhr.withCredentials : false + }; + this._onend = o.onend ? [{ fn: o.onend }] : []; + this._onfade = o.onfade ? [{ fn: o.onfade }] : []; + this._onload = o.onload ? [{ fn: o.onload }] : []; + this._onloaderror = o.onloaderror ? [{ fn: o.onloaderror }] : []; + this._onplayerror = o.onplayerror ? [{ fn: o.onplayerror }] : []; + this._onpause = o.onpause ? [{ fn: o.onpause }] : []; + this._onplay = o.onplay ? [{ fn: o.onplay }] : []; + this._onstop = o.onstop ? [{ fn: o.onstop }] : []; + this._onmute = o.onmute ? [{ fn: o.onmute }] : []; + this._onvolume = o.onvolume ? [{ fn: o.onvolume }] : []; + this._onrate = o.onrate ? [{ fn: o.onrate }] : []; + this._onseek = o.onseek ? [{ fn: o.onseek }] : []; + this._onunlock = o.onunlock ? [{ fn: o.onunlock }] : []; + this._onresume = []; + this._webAudio = howler_default.usingWebAudio && !this._html5; + if (typeof howler_default.ctx !== "undefined" && howler_default.ctx && howler_default.autoUnlock) { + howler_default._unlockAudio(); + } + howler_default._howls.push(this); + if (this._autoplay) { + this._queue.push({ + event: "play", + action: () => { + this.play(); + } + }); + } + if (this._preload) { + this.load(); + } + } + load() { + var url = null; + if (howler_default.noAudio) { + this._emit("loaderror", null, "No audio support."); + return this; + } + if (typeof this._src === "string") { + this._src = [this._src]; + } + for (var i = 0; i < this._src.length; i++) { + var ext, str; + if (this._format && this._format[i]) { + ext = this._format[i]; + } else { + str = this._src[i]; + if (typeof str !== "string") { + this._emit("loaderror", null, "Non-string found in selected audio sources - ignoring."); + continue; + } + ext = /^data:audio\/([^;,]+);/i.exec(str); + if (!ext) { + ext = /\.([^.]+)$/.exec(str.split("?", 1)[0]); + } + if (ext) { + ext = ext[1].toLowerCase(); + } + } + if (!ext) { + console.warn('No file extension was found. Consider using the "format" property or specify an extension.'); + } + if (ext && howler_default.codecs(ext)) { + url = this._src[i]; + break; + } + } + if (!url) { + this._emit("loaderror", null, "No codec support for selected audio sources."); + return this; + } + this._src = url; + this._state = "loading"; + if (window.location.protocol === "https:" && url.slice(0, 5) === "http:") { + this._html5 = true; + this._webAudio = false; + } + new sound_default(this); + if (this._webAudio) { + loadBuffer(this); + } + return this; + } + play(sprite, internal) { + var id = null; + if (typeof sprite === "number") { + id = sprite; + sprite = null; + } else if (typeof sprite === "string" && this._state === "loaded" && !this._sprite[sprite]) { + return null; + } else if (typeof sprite === "undefined") { + sprite = "__default"; + if (!this._playLock) { + var num = 0; + for (var i = 0; i < this._sounds.length; i++) { + if (this._sounds[i]._paused && !this._sounds[i]._ended) { + num++; + id = this._sounds[i]._id; + } + } + if (num === 1) { + sprite = null; + } else { + id = null; + } + } + } + const sound = id ? this._soundById(id) : this._inactiveSound(); + if (!sound) { + return null; + } + if (id && !sprite) { + sprite = sound._sprite || "__default"; + } + if (this._state !== "loaded") { + sound._sprite = sprite; + sound._ended = false; + var soundId = sound._id; + this._queue.push({ + event: "play", + action: () => { + this.play(soundId); + } + }); + return soundId; + } + if (id && !sound._paused) { + if (!internal) { + this._loadQueue("play"); + } + return sound._id; + } + if (this._webAudio) { + howler_default._autoResume(); + } + const seek = Math.max(0, sound._seek > 0 ? sound._seek : this._sprite[sprite][0] / 1e3); + const duration = Math.max(0, (this._sprite[sprite][0] + this._sprite[sprite][1]) / 1e3 - seek); + const timeout = duration * 1e3 / Math.abs(sound._rate); + const start = this._sprite[sprite][0] / 1e3; + const stop = (this._sprite[sprite][0] + this._sprite[sprite][1]) / 1e3; + sound._sprite = sprite; + sound._ended = false; + const setParams = () => { + sound._paused = false; + sound._seek = seek; + sound._start = start; + sound._stop = stop; + sound._loop = !!(sound._loop || this._sprite[sprite][2]); + }; + if (seek >= stop) { + this._ended(sound); + return; + } + const node = sound._node; + if (this._webAudio) { + const playWebAudio = () => { + this._playLock = false; + setParams(); + this._refreshBuffer(sound); + const vol = sound._muted || this._muted ? 0 : sound._volume; + node.gain.setValueAtTime(vol, howler_default.ctx.currentTime); + sound._playStart = howler_default.ctx.currentTime; + 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); + } + if (timeout !== Infinity) { + this._endTimers[sound._id] = setTimeout(this._ended.bind(this, sound), timeout); + } + if (!internal) { + setTimeout(() => { + this._emit("play", sound._id); + this._loadQueue(); + }, 0); + } + }; + if (howler_default.state === "running" && howler_default.ctx.state !== "interrupted") { + playWebAudio(); + } else { + this._playLock = true; + this.once("resume", playWebAudio); + this._clearTimer(sound._id); + } + } else { + const playHtml5 = () => { + node.currentTime = seek; + node.muted = sound._muted || this._muted || howler_default._muted || node.muted; + node.volume = sound._volume * howler_default.volume(); + node.playbackRate = sound._rate; + try { + const play = node.play(); + if (play && typeof Promise !== "undefined" && (play instanceof Promise || typeof play.then === "function")) { + this._playLock = true; + setParams(); + play.then(() => { + this._playLock = false; + node._unlocked = true; + if (!internal) { + this._emit("play", sound._id); + } else { + this._loadQueue(); + } + }).catch(() => { + this._playLock = false; + this._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."); + sound._ended = true; + sound._paused = true; + }); + } else if (!internal) { + this._playLock = false; + setParams(); + this._emit("play", sound._id); + } + node.playbackRate = sound._rate; + if (node.paused) { + this._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; + } + if (sprite !== "__default" || sound._loop) { + this._endTimers[sound._id] = setTimeout(this._ended.bind(this, sound), timeout); + } else { + this._endTimers[sound._id] = () => { + this._ended(sound); + node.removeEventListener("ended", this._endTimers[sound._id], false); + }; + node.addEventListener("ended", this._endTimers[sound._id], false); + } + } catch (err) { + this._emit("playerror", sound._id, err); + } + }; + if (node.src === "data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA") { + node.src = this._src; + node.load(); + } + const loadedNoReadyState = window && window.ejecta || !node.readyState && howler_default._navigator.isCocoonJS; + if (node.readyState >= 3 || loadedNoReadyState) { + playHtml5(); + } else { + this._playLock = true; + this._state = "loading"; + const listener = () => { + this._state = "loaded"; + playHtml5(); + node.removeEventListener(howler_default._canPlayEvent, listener, false); + }; + node.addEventListener(howler_default._canPlayEvent, listener, false); + this._clearTimer(sound._id); + } + } + return sound._id; + } + pause(id, skipEmit) { + if (this._state !== "loaded" || this._playLock) { + this._queue.push({ + event: "pause", + action: () => { + this.pause(id); + } + }); + return this; + } + var ids = this._getSoundIds(id); + for (var i = 0; i < ids.length; i++) { + this._clearTimer(ids[i]); + var sound = this._soundById(ids[i]); + if (sound && !sound._paused) { + sound._seek = this.seek(ids[i]); + sound._rateSeek = 0; + sound._paused = true; + this._stopFade(ids[i]); + if (sound._node) { + if (this._webAudio) { + if (!sound._node.bufferSource) { + continue; + } + if (typeof sound._node.bufferSource.stop === "undefined") { + sound._node.bufferSource.noteOff(0); + } else { + sound._node.bufferSource.stop(0); + } + this._cleanBuffer(sound._node); + } else if (!isNaN(sound._node.duration) || sound._node.duration === Infinity) { + sound._node.pause(); + } + } + } + if (!skipEmit) { + this._emit("pause", sound ? sound._id : null); + } + } + return this; + } + stop(id, internal) { + if (this._state !== "loaded" || this._playLock) { + this._queue.push({ + event: "stop", + action: () => { + this.stop(id); + } + }); + return this; + } + var ids = this._getSoundIds(id); + for (var i = 0; i < ids.length; i++) { + this._clearTimer(ids[i]); + var sound = this._soundById(ids[i]); + if (sound) { + sound._seek = sound._start || 0; + sound._rateSeek = 0; + sound._paused = true; + sound._ended = true; + this._stopFade(ids[i]); + if (sound._node) { + if (this._webAudio) { + if (sound._node.bufferSource) { + if (typeof sound._node.bufferSource.stop === "undefined") { + sound._node.bufferSource.noteOff(0); + } else { + sound._node.bufferSource.stop(0); + } + this._cleanBuffer(sound._node); + } + } else if (!isNaN(sound._node.duration) || sound._node.duration === Infinity) { + sound._node.currentTime = sound._start || 0; + sound._node.pause(); + if (sound._node.duration === Infinity) { + this._clearSound(sound._node); + } + } + } + if (!internal) { + this._emit("stop", sound._id); + } + } + } + return this; + } + mute(muted, id) { + if (this._state !== "loaded" || this._playLock) { + this._queue.push({ + event: "mute", + action: () => { + this.mute(muted, id); + } + }); + return this; + } + if (typeof id === "undefined") { + if (typeof muted === "boolean") { + this._muted = muted; + } else { + return this._muted; + } + } + var ids = this._getSoundIds(id); + for (var i = 0; i < ids.length; i++) { + var sound = this._soundById(ids[i]); + if (sound) { + sound._muted = muted; + if (sound._interval) { + this._stopFade(sound._id); + } + if (this._webAudio && sound._node && howler_default.ctx) { + sound._node.gain.setValueAtTime(muted ? 0 : sound._volume, howler_default.ctx.currentTime); + } else if (sound._node) { + sound._node.muted = howler_default._muted ? true : muted; + } + this._emit("mute", sound._id); + } + } + return this; + } + volume(...args) { + let vol, id; + if (args.length === 0) { + return this._volume; + } else if (args.length === 1 || args.length === 2 && typeof args[1] === "undefined") { + var ids = this._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); + } + let sound; + if (typeof vol !== "undefined" && vol >= 0 && vol <= 1) { + if (this._state !== "loaded" || this._playLock) { + this._queue.push({ + event: "volume", + action: () => { + this.volume.apply(this, args); + } + }); + return this; + } + if (typeof id === "undefined") { + this._volume = vol; + } + id = this._getSoundIds(id); + for (var i = 0; i < id.length; i++) { + sound = this._soundById(id[i]); + if (sound) { + sound._volume = vol; + if (!args[2]) { + this._stopFade(id[i]); + } + if (this._webAudio && sound._node && !sound._muted && howler_default.ctx) { + sound._node.gain.setValueAtTime(vol, howler_default.ctx.currentTime); + } else if (sound._node && !sound._muted) { + sound._node.volume = vol * howler_default.volume(); + } + this._emit("volume", sound._id); + } + } + } else { + sound = id ? this._soundById(id) : this._sounds[0]; + return sound ? sound._volume : 0; + } + return this; + } + fade(from, to, len, id) { + if (this._state !== "loaded" || this._playLock) { + this._queue.push({ + event: "fade", + action: () => { + this.fade(from, to, len, id); + } + }); + return this; + } + from = Math.min(Math.max(0, parseFloat(from)), 1); + to = Math.min(Math.max(0, parseFloat(to)), 1); + len = parseFloat(len); + this.volume(from, id); + var ids = this._getSoundIds(id); + for (var i = 0; i < ids.length; i++) { + var sound = this._soundById(ids[i]); + if (sound) { + if (!id) { + this._stopFade(ids[i]); + } + if (this._webAudio && !sound._muted && howler_default.ctx) { + var currentTime = howler_default.ctx.currentTime; + var end = currentTime + len / 1e3; + sound._volume = from; + sound._node.gain.setValueAtTime(from, currentTime); + sound._node.gain.linearRampToValueAtTime(to, end); + } + this._startFadeInterval(sound, from, to, len, ids[i], typeof id === "undefined"); + } + } + return this; + } + _startFadeInterval(sound, from, to, len, id, isGroup) { + var vol = from; + var diff = to - from; + var steps = Math.abs(diff / 0.01); + var stepLen = Math.max(4, steps > 0 ? len / steps : len); + var lastTick = Date.now(); + sound._fadeTo = to; + sound._interval = setInterval(() => { + var tick = (Date.now() - lastTick) / len; + lastTick = Date.now(); + vol += diff * tick; + vol = Math.round(vol * 100) / 100; + if (diff < 0) { + vol = Math.max(to, vol); + } else { + vol = Math.min(to, vol); + } + if (this._webAudio) { + sound._volume = vol; + } else { + this.volume(vol, sound._id, true); + } + if (isGroup) { + this._volume = vol; + } + if (to < from && vol <= to || to > from && vol >= to) { + if (typeof sound._interval === "number") { + clearInterval(sound._interval); + } + sound._interval = null; + sound._fadeTo = null; + this.volume(to, sound._id); + this._emit("fade", sound._id); + } + }, stepLen); + } + _stopFade(id) { + var sound = this._soundById(id); + if (sound && sound._interval) { + if (this._webAudio && howler_default.ctx) { + sound._node.gain.cancelScheduledValues(howler_default.ctx.currentTime); + } + clearInterval(sound._interval); + sound._interval = null; + this.volume(sound._fadeTo, id); + sound._fadeTo = null; + this._emit("fade", id); + } + return this; + } + loop(...args) { + let loop, id, sound; + if (args.length === 0) { + return this._loop; + } else if (args.length === 1) { + if (typeof args[0] === "boolean") { + loop = args[0]; + this._loop = loop; + } else { + sound = this._soundById(parseInt(args[0], 10)); + return sound ? sound._loop : false; + } + } else if (args.length === 2) { + loop = args[0]; + id = parseInt(args[1], 10); + } + var ids = this._getSoundIds(id); + for (var i = 0; i < ids.length; i++) { + sound = this._soundById(ids[i]); + if (sound) { + sound._loop = loop; + if (this._webAudio && sound._node && sound._node.bufferSource) { + sound._node.bufferSource.loop = loop; + if (loop) { + sound._node.bufferSource.loopStart = sound._start || 0; + sound._node.bufferSource.loopEnd = sound._stop; + if (this.playing(ids[i])) { + this.pause(ids[i], true); + this.play(ids[i], true); + } + } + } + } + } + return this; + } + rate(...args) { + let rate, id; + if (args.length === 0) { + id = this._sounds[0]._id; + } else if (args.length === 1) { + var ids = this._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); + } + let sound; + if (typeof rate === "number") { + if (this._state !== "loaded" || this._playLock) { + this._queue.push({ + event: "rate", + action: () => { + this.rate.apply(this, args); + } + }); + return this; + } + if (typeof id === "undefined") { + this._rate = rate; + } + id = this._getSoundIds(id); + for (var i = 0; i < id.length; i++) { + sound = this._soundById(id[i]); + if (sound && howler_default.ctx) { + if (this.playing(id[i])) { + sound._rateSeek = this.seek(id[i]); + sound._playStart = this._webAudio ? howler_default.ctx.currentTime : sound._playStart; + } + sound._rate = rate; + if (this._webAudio && sound._node && sound._node.bufferSource) { + sound._node.bufferSource.playbackRate.setValueAtTime(rate, howler_default.ctx.currentTime); + } else if (sound._node) { + sound._node.playbackRate = rate; + } + const seek = this.seek(id[i]); + const duration = (this._sprite[sound._sprite][0] + this._sprite[sound._sprite][1]) / 1e3 - seek; + const timeout = duration * 1e3 / Math.abs(sound._rate); + if (this._endTimers[id[i]] || !sound._paused) { + this._clearTimer(id[i]); + this._endTimers[id[i]] = setTimeout(this._ended.bind(this, sound), timeout); + } + this._emit("rate", sound._id); + } + } + } else { + sound = this._soundById(id); + return sound ? sound._rate : this._rate; + } + return this; + } + seek(...args) { + let seek, id; + if (args.length === 0) { + if (this._sounds.length) { + id = this._sounds[0]._id; + } + } else if (args.length === 1) { + var ids = this._getSoundIds(); + var index = ids.indexOf(args[0]); + if (index >= 0) { + id = parseInt(args[0], 10); + } else if (this._sounds.length) { + id = this._sounds[0]._id; + seek = parseFloat(args[0]); + } + } else if (args.length === 2) { + seek = parseFloat(args[0]); + id = parseInt(args[1], 10); + } + if (typeof id === "undefined") { + return 0; + } + if (typeof seek === "number" && (this._state !== "loaded" || this._playLock)) { + this._queue.push({ + event: "seek", + action: () => { + this.seek.apply(this, args); + } + }); + return this; + } + var sound = this._soundById(id); + if (sound) { + if (typeof seek === "number" && seek >= 0) { + var playing = this.playing(id); + if (playing) { + this.pause(id, true); + } + sound._seek = seek; + sound._ended = false; + this._clearTimer(id); + if (!this._webAudio && sound._node && !isNaN(sound._node.duration)) { + sound._node.currentTime = seek; + } + const seekAndEmit = () => { + if (playing) { + this.play(id, true); + } + this._emit("seek", id); + }; + if (playing && !this._webAudio) { + const emitSeek = () => { + if (!this._playLock) { + seekAndEmit(); + } else { + setTimeout(emitSeek, 0); + } + }; + setTimeout(emitSeek, 0); + } else { + seekAndEmit(); + } + } else { + if (this._webAudio && howler_default.ctx) { + const realTime = this.playing(id) ? howler_default.ctx.currentTime - sound._playStart : 0; + const rateSeek = sound._rateSeek ? sound._rateSeek - sound._seek : 0; + return sound._seek + (rateSeek + realTime * Math.abs(sound._rate)); + } else { + return sound._node.currentTime; + } + } + } + return this; + } + playing(id) { + if (typeof id === "number") { + var sound = this._soundById(id); + return sound ? !sound._paused : false; + } + for (var i = 0; i < this._sounds.length; i++) { + if (!this._sounds[i]._paused) { + return true; + } + } + return false; + } + duration(id) { + var duration = this._duration; + var sound = this._soundById(id); + if (sound) { + duration = this._sprite[sound._sprite][1] / 1e3; + } + return duration; + } + state() { + return this._state; + } + unload() { + var sounds = this._sounds; + for (let i = 0; i < sounds.length; i++) { + if (!sounds[i]._paused) { + this.stop(sounds[i]._id); + } + if (!this._webAudio) { + this._clearSound(sounds[i]._node); + sounds[i]._node.removeEventListener("error", sounds[i]._errorFn, false); + sounds[i]._node.removeEventListener(howler_default._canPlayEvent, sounds[i]._loadFn, false); + sounds[i]._node.removeEventListener("ended", sounds[i]._endFn, false); + howler_default._releaseHtml5Audio(sounds[i]._node); + } + delete sounds[i]._node; + this._clearTimer(sounds[i]._id); + } + var index = howler_default._howls.indexOf(this); + if (index >= 0) { + howler_default._howls.splice(index, 1); + } + var remCache = true; + for (let i = 0; i < howler_default._howls.length; i++) { + if (howler_default._howls[i]._src === this._src || this._src.indexOf(howler_default._howls[i]._src) >= 0) { + remCache = false; + break; + } + } + if (cache && remCache) { + delete cache[this._src]; + } + howler_default.noAudio = false; + this._state = "unloaded"; + this._sounds = []; + return null; + } + on(event, fn, id, once) { + var events = this["_on" + event]; + if (typeof fn === "function") { + events.push(once ? { id, fn, once } : { id, fn }); + } + return this; + } + off(event, fn, id) { + var events = this["_on" + event]; + var i = 0; + if (typeof fn === "number") { + id = fn; + fn = null; + } + if (fn || id) { + for (i = 0; i < events.length; i++) { + var isId = id === events[i].id; + if (fn === events[i].fn && isId || !fn && isId) { + events.splice(i, 1); + break; + } + } + } else if (event) { + this["_on" + event] = []; + } else { + var keys = Object.keys(this); + for (i = 0; i < keys.length; i++) { + if (keys[i].indexOf("_on") === 0 && Array.isArray(this[keys[i]])) { + this[keys[i]] = []; + } + } + } + return this; + } + once(event, fn, id) { + this.on(event, fn, id, 1); + return this; + } + _emit(event, id, msg) { + var events = this["_on" + event]; + for (var i = events.length - 1; i >= 0; i--) { + if (!events[i].id || events[i].id === id || event === "load") { + setTimeout(((fn) => { + fn.call(this, id, msg); + }).bind(this, events[i].fn), 0); + if (events[i].once) { + this.off(event, events[i].fn, events[i].id); + } + } + } + this._loadQueue(event); + return this; + } + _loadQueue(event) { + if (this._queue.length > 0) { + var task = this._queue[0]; + if (task.event === event) { + this._queue.shift(); + this._loadQueue(); + } + if (!event) { + task.action(); + } + } + return this; + } + _ended(sound) { + var sprite = sound._sprite; + if (!this._webAudio && sound._node && !sound._node.paused && !sound._node.ended && sound._node.currentTime < sound._stop) { + setTimeout(this._ended.bind(this, sound), 100); + return this; + } + var loop = !!(sound._loop || this._sprite[sprite][2]); + this._emit("end", sound._id); + if (!this._webAudio && loop) { + this.stop(sound._id, true).play(sound._id); + } + if (this._webAudio && loop && howler_default.ctx) { + this._emit("play", sound._id); + sound._seek = sound._start || 0; + sound._rateSeek = 0; + sound._playStart = howler_default.ctx.currentTime; + var timeout = (sound._stop - sound._start) * 1e3 / Math.abs(sound._rate); + this._endTimers[sound._id] = setTimeout(this._ended.bind(this, sound), timeout); + } + if (this._webAudio && !loop) { + sound._paused = true; + sound._ended = true; + sound._seek = sound._start || 0; + sound._rateSeek = 0; + this._clearTimer(sound._id); + this._cleanBuffer(sound._node); + howler_default._autoSuspend(); + } + if (!this._webAudio && !loop) { + this.stop(sound._id, true); + } + return this; + } + _clearTimer(id) { + if (this._endTimers[id]) { + if (typeof this._endTimers[id] !== "function") { + clearTimeout(this._endTimers[id]); + } else { + var sound = this._soundById(id); + if (sound && sound._node) { + sound._node.removeEventListener("ended", this._endTimers[id], false); + } + } + delete this._endTimers[id]; + } + return this; + } + _soundById(id) { + for (var i = 0; i < this._sounds.length; i++) { + if (id === this._sounds[i]._id) { + return this._sounds[i]; + } + } + return null; + } + _inactiveSound() { + this._drain(); + for (let i = 0; i < this._sounds.length; i++) { + if (this._sounds[i]._ended) { + return this._sounds[i].reset(); + } + } + return new sound_default(this); + } + _drain() { + const limit = this._pool; + let cnt = 0; + let i = 0; + if (this._sounds.length < limit) { + return; + } + for (i = 0; i < this._sounds.length; i++) { + if (this._sounds[i]._ended) { + cnt++; + } + } + for (i = this._sounds.length - 1; i >= 0; i--) { + if (cnt <= limit) { + return; + } + if (this._sounds[i]._ended) { + if (this._webAudio && this._sounds[i]._node) { + this._sounds[i]._node.disconnect(0); + } + this._sounds.splice(i, 1); + cnt--; + } + } + } + _getSoundIds(id) { + if (typeof id === "undefined") { + var ids = []; + for (var i = 0; i < this._sounds.length; i++) { + ids.push(this._sounds[i]._id); + } + return ids; + } else { + return [id]; + } + } + _refreshBuffer(sound) { + sound._node.bufferSource = howler_default.ctx.createBufferSource(); + sound._node.bufferSource.buffer = cache[this._src]; + if (sound._panner) { + sound._node.bufferSource.connect(sound._panner); + } else { + sound._node.bufferSource.connect(sound._node); + } + sound._node.bufferSource.loop = sound._loop; + if (sound._loop) { + sound._node.bufferSource.loopStart = sound._start || 0; + sound._node.bufferSource.loopEnd = sound._stop || 0; + } + sound._node.bufferSource.playbackRate.setValueAtTime(sound._rate, howler_default.ctx.currentTime); + return this; + } + _cleanBuffer(node) { + var isIOS = howler_default._navigator && howler_default._navigator.vendor.indexOf("Apple") >= 0; + if (howler_default._scratchBuffer && node.bufferSource) { + node.bufferSource.onended = null; + node.bufferSource.disconnect(0); + if (isIOS) { + try { + node.bufferSource.buffer = howler_default._scratchBuffer; + } catch (e) { + } + } + } + node.bufferSource = null; + return this; + } + _clearSound(node) { + var checkIE = /MSIE |Trident\//.test(howler_default._navigator && howler_default._navigator.userAgent); + if (!checkIE) { + node.src = "data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA"; + } + } +}; +var howl_default = Howl; +export { + howl_default as Howl, + howler_default as Howler +}; +/*! + * howler.js v2.2.3 + * howlerjs.com + * + * (c) 2013-2020, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */ diff --git a/dist/helpers.d.ts b/dist/helpers.d.ts new file mode 100644 index 00000000..ef53ed7c --- /dev/null +++ b/dist/helpers.d.ts @@ -0,0 +1,9 @@ +import Howl from './howl'; +export declare const cache: {}; +/** + * Buffer a sound from URL, Data URI or cache and decode to audio source (Web Audio API). + */ +export declare function loadBuffer(self: Howl): void; +export declare const isHTMLAudioElement: (node: any) => node is HTMLAudioElement; +export declare const isGainNode: (node: any) => node is GainNode; +export declare const isAudioBufferSourceNode: (node: any) => node is AudioBufferSourceNode; diff --git a/dist/howl.d.ts b/dist/howl.d.ts new file mode 100644 index 00000000..0315314f --- /dev/null +++ b/dist/howl.d.ts @@ -0,0 +1,401 @@ +import Sound from './sound'; +export declare type HowlCallback = (soundId: number) => void; +export declare type HowlErrorCallback = (soundId: number, error: unknown) => void; +export interface SoundSpriteDefinitions { + [name: string]: [number, number] | [number, number, boolean]; +} +export interface HowlXHROptions { + method?: string; + headers?: Record; + withCredentials?: boolean; +} +export interface HowlListeners { + /** + * Fires when the sound has been stopped. The first parameter is the ID of the sound. + */ + onstop?: HowlCallback; + /** + * Fires when the sound has been paused. The first parameter is the ID of the sound. + */ + onpause?: HowlCallback; + /** + * Fires when the sound is loaded. + */ + onload?: HowlCallback; + /** + * Fires when the sound has been muted/unmuted. The first parameter is the ID of the sound. + */ + onmute?: HowlCallback; + /** + * Fires when the sound's volume has changed. The first parameter is the ID of the sound. + */ + onvolume?: HowlCallback; + /** + * Fires when the sound's playback rate has changed. The first parameter is the ID of the sound. + */ + onrate?: HowlCallback; + /** + * Fires when the sound has been seeked. The first parameter is the ID of the sound. + */ + onseek?: HowlCallback; + /** + * Fires when the current sound finishes fading in/out. The first parameter is the ID of the sound. + */ + onfade?: HowlCallback; + /** + * Fires when audio has been automatically unlocked through a touch/click event. + */ + onunlock?: HowlCallback; + /** + * Fires when the sound finishes playing (if it is looping, it'll fire at the end of each loop). + * The first parameter is the ID of the sound. + */ + onend?: HowlCallback; + /** + * Fires when the sound begins playing. The first parameter is the ID of the sound. + */ + onplay?: HowlCallback; + /** + * Fires when the sound is unable to load. The first parameter is the ID of the sound (if it exists) and the second is the error message/code. + */ + onloaderror?: HowlErrorCallback; + /** + * Fires when the sound is unable to play. The first parameter is the ID of the sound and the second is the error message/code. + */ + onplayerror?: HowlErrorCallback; +} +export interface HowlOptions extends HowlListeners { + /** + * The sources to the track(s) to be loaded for the sound (URLs or base64 data URIs). 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 extension using the format property. + * + * @default `[]` + */ + src?: string | string[]; + /** + * The volume of the specific track, from 0.0 to 1.0. + * + * @default `1.0` + */ + volume?: number; + /** + * 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. + * + * @default `false` + */ + html5?: boolean; + /** + * Set to true to automatically loop the sound forever. + * + * @default `false` + */ + loop?: boolean; + /** + * Automatically begin downloading the audio file when the Howl is defined. If using HTML5 Audio, + * you can set this to 'metadata' to only preload the file's metadata (to get its duration without + * download the entire file, for example). + * + * @default `true` + */ + preload?: boolean | 'metadata'; + /** + * Set to true to automatically start playback when sound is loaded. + * + * @default `false` + */ + autoplay?: boolean; + /** + * Set to true to load the audio muted. + * + * @default `false` + */ + mute?: boolean; + /** + * 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. An easy way to generate + * compatible sound sprites is with audiosprite. + * + * @default `{}` + */ + sprite?: SoundSpriteDefinitions; + /** + * The rate of playback. 0.5 to 4.0, with 1.0 being normal speed. + * + * @default `1.0` + */ + rate?: number; + /** + * The size of the inactive sounds pool. Once sounds are stopped or finish playing, they are marked + * as ended and ready for cleanup. We keep a pool of these to recycle for improved performance. + * Generally this doesn't need to be changed. It is important to keep in mind that when a sound is + * paused, it won't be removed from the pool and will still be considered active so that it can be + * resumed later. + * + * @default `5` + */ + pool?: number; + /** + * howler.js automatically detects your file format from the extension, but you may also specify a + * format in situations where extraction won't work (such as with a SoundCloud stream). + * + * @default `[]` + */ + format?: string[]; + /** + * When using Web Audio, howler.js uses an XHR request to load the audio files. If you need to send + * custom headers, set the HTTP method or enable withCredentials (see reference), include them with + * this parameter. Each is optional (method defaults to GET, headers default to undefined and + * withCredentials defaults to false). + */ + xhr?: HowlXHROptions; +} +declare type HowlCallbacks = Array<{ + fn: HowlCallback; +}>; +declare type HowlErrorCallbacks = Array<{ + fn: HowlErrorCallback; +}>; +declare type HowlEvent = 'play' | 'end' | 'pause' | 'stop' | 'mute' | 'volume' | 'rate' | 'seek' | 'fade' | 'unlock' | 'load' | 'loaderror' | 'playerror'; +interface HowlEventHandler { + event: HowlEvent; + action: () => void; +} +declare class Howl { + _autoplay: boolean; + _format: string[]; + _html5: boolean; + _muted: boolean; + _loop: boolean; + _pool: number; + _preload: boolean | 'metadata'; + _rate: number; + _sprite: SoundSpriteDefinitions; + _src: string | string[]; + _volume: number; + _xhr: HowlOptions['xhr']; + _duration: number; + _state: 'unloaded' | 'loading' | 'loaded'; + _sounds: Sound[]; + _endTimers: {}; + _queue: HowlEventHandler[]; + _playLock: boolean; + _onend: HowlCallbacks; + _onfade: HowlCallbacks; + _onload: HowlCallbacks; + _onloaderror: HowlErrorCallbacks; + _onplayerror: HowlErrorCallbacks; + _onpause: HowlCallbacks; + _onplay: HowlCallbacks; + _onstop: HowlCallbacks; + _onmute: HowlCallbacks; + _onvolume: HowlCallbacks; + _onrate: HowlCallbacks; + _onseek: HowlCallbacks; + _onunlock: HowlCallbacks; + _onresume: HowlCallbacks; + _webAudio: boolean; + /** + * Create an audio group controller. + * @param o Passed in properties for this group. + */ + constructor(o: HowlOptions); + /** + * Load the audio file. + */ + load(): this; + /** + * Play a sound or resume previous playback. + * @param sprite Sprite name for sprite playback or sound id to continue previous. + * @param internal Internal Use: true prevents event firing. + * @return Sound ID. + */ + play(sprite?: string | number, internal?: boolean): number | null | undefined; + /** + * Pause playback and save current position. + * @param id The sound ID (empty to pause all in group). + * @param skipEmit If true, the `pause` event won't be emitted. + */ + pause(id: number, skipEmit?: boolean): this; + /** + * Stop playback and reset to start. + * @param id The sound ID (empty to stop all in group). + * @param internal Internal Use: true prevents event firing. + */ + stop(id?: number, internal?: boolean): this; + /** + * Mute/unmute a single sound or all sounds in this Howl group. + * @param muted Set to true to mute and false to unmute. + * @param id The sound ID to update (omit to mute/unmute all). + */ + mute(muted: boolean, id: number): boolean | this; + /** + * Get/set the volume of this sound or of the Howl group. This method can optionally take 0, 1 or 2 arguments. + * volume() -> 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 Returns this or current volume. + */ + volume(...args: any[]): number | this; + /** + * Fade a currently playing sound between two volumes (if no id is passed, all sounds will fade). + * @param from The value to fade from (0.0 to 1.0). + * @param to The volume to fade to (0.0 to 1.0). + * @param len Time in milliseconds to fade. + * @param id The sound id (omit to fade all sounds). + */ + fade(from: number | string, to: number | string, len: number | string, id: number): this; + /** + * Starts the internal interval to fade a sound. + * @param sound Reference to sound to fade. + * @param from The value to fade from (0.0 to 1.0). + * @param to The volume to fade to (0.0 to 1.0). + * @param len Time in milliseconds to fade. + * @param id The sound id to fade. + * @param isGroup If true, set the volume on the group. + */ + _startFadeInterval(sound: Sound, from: number, to: number, len: number, id: number, isGroup: boolean): void; + /** + * 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(id: any): this; + /** + * 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 Returns this or current loop value. + */ + loop(...args: any[]): any; + /** + * Get/set the playback rate of a sound. This method can optionally take 0, 1 or 2 arguments. + * rate() -> 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 Returns this or the current playback rate. + */ + rate(...args: any[]): any; + /** + * Get/set the seek position of a sound. This method can optionally take 0, 1 or 2 arguments. + * seek() -> 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 Returns this or the current seek position. + */ + seek(...args: any[]): number | this; + /** + * 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 id The sound id to check. If none is passed, the whole sound group is checked. + * @return True if playing and false if not. + */ + playing(id: number): boolean; + /** + * Get the duration of this sound. Passing a sound id will return the sprite duration. + * @param id The sound id to check. If none is passed, return full source duration. + * @return Audio duration in seconds. + */ + duration(id: number): number; + /** + * Returns the current loaded state of this Howl. + * @return 'unloaded', 'loading', 'loaded' + */ + state(): "loaded" | "loading" | "unloaded"; + /** + * Unload and destroy the current Howl object. + * This will immediately stop all sound instances attached to this group. + */ + unload(): 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. + */ + on(event: string, fn: Function, id?: number, once?: number): this; + /** + * 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(event: any, fn: any, id: any): this; + /** + * Listen to a custom event and remove it once fired. + * @param event Event name. + * @param fn Listener to call. + * @param id (optional) Only listen to events for this sound. + */ + once(event: string, fn: Function, id?: number): this; + /** + * Emit all events of a specific type and pass the sound id. + * @param event Event name. + * @param id Sound ID. + * @param msg Message to go with event. + */ + _emit(event: string, id?: number | null, msg?: string | number): this; + /** + * 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). + */ + _loadQueue(event?: string): this; + /** + * Fired when playback ends at the end of the duration. + * @param sound The sound object to work with. + */ + _ended(sound: Sound): this; + /** + * Clear the end timer for a sound playback. + * @param {Number} id The sound ID. + */ + _clearTimer(id: number): this; + /** + * Return the sound identified by this ID, or return null. + * @param id Sound ID + * @return Sound object or null. + */ + _soundById(id: number): Sound | null; + /** + * Return an inactive sound from the pool or create a new one. + * @return Sound playback object. + */ + _inactiveSound(): Sound; + /** + * Drain excess inactive sounds from the pool. + */ + _drain(): void; + /** + * Get all ID's from the sounds pool. + * @param id Only return one ID if one is passed. + * @return Array of IDs. + */ + _getSoundIds(id?: number): number[]; + /** + * Load the sound back into the buffer source. + * @param sound The sound object to work with. + */ + _refreshBuffer(sound: Sound): this; + /** + * Prevent memory leaks by cleaning up the buffer source after playback. + * @param {Object} node Sound's audio node containing the buffer source. + * @return {Howl} + */ + _cleanBuffer(node: Sound['_node']): this; + /** + * Set the source to a 0-second silence to stop any downloading (except in IE). + * @param {Object} node Audio node to clear. + */ + _clearSound(node: any): void; +} +export default Howl; diff --git a/dist/howler.core.min.js b/dist/howler.core.min.js deleted file mode 100644 index c260dc37..00000000 --- a/dist/howler.core.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! howler.js v2.2.3 | (c) 2013-2020, James Simpson of GoldFire Studios | MIT License | howlerjs.com */ -!function(){"use strict";var e=function(){this.init()};e.prototype={init:function(){var e=this||n;return e._counter=1e3,e._html5AudioPool=[],e.html5PoolSize=10,e._codecs={},e._howls=[],e._muted=!1,e._volume=1,e._canPlayEvent="canplaythrough",e._navigator="undefined"!=typeof window&&window.navigator?window.navigator:null,e.masterGain=null,e.noAudio=!1,e.usingWebAudio=!0,e.autoSuspend=!0,e.ctx=null,e.autoUnlock=!0,e._setup(),e},volume:function(e){var o=this||n;if(e=parseFloat(e),o.ctx||_(),void 0!==e&&e>=0&&e<=1){if(o._volume=e,o._muted)return o;o.usingWebAudio&&o.masterGain.gain.setValueAtTime(e,n.ctx.currentTime);for(var t=0;t=0;o--)e._howls[o].unload();return e.usingWebAudio&&e.ctx&&void 0!==e.ctx.close&&(e.ctx.close(),e.ctx=null,_()),e},codecs:function(e){return(this||n)._codecs[e.replace(/^x-/,"")]},_setup:function(){var e=this||n;if(e.state=e.ctx?e.ctx.state||"suspended":"suspended",e._autoSuspend(),!e.usingWebAudio)if("undefined"!=typeof Audio)try{var o=new Audio;void 0===o.oncanplaythrough&&(e._canPlayEvent="canplay")}catch(n){e.noAudio=!0}else e.noAudio=!0;try{var o=new Audio;o.muted&&(e.noAudio=!0)}catch(e){}return e.noAudio||e._setupCodecs(),e},_setupCodecs:function(){var e=this||n,o=null;try{o="undefined"!=typeof Audio?new Audio:null}catch(n){return e}if(!o||"function"!=typeof o.canPlayType)return e;var t=o.canPlayType("audio/mpeg;").replace(/^no$/,""),r=e._navigator?e._navigator.userAgent:"",a=r.match(/OPR\/([0-6].)/g),u=a&&parseInt(a[0].split("/")[1],10)<33,d=-1!==r.indexOf("Safari")&&-1===r.indexOf("Chrome"),i=r.match(/Version\/(.*?) /),_=d&&i&&parseInt(i[1],10)<15;return e._codecs={mp3:!(u||!t&&!o.canPlayType("audio/mp3;").replace(/^no$/,"")),mpeg:!!t,opus:!!o.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/,""),ogg:!!o.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),oga:!!o.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),wav:!!(o.canPlayType('audio/wav; codecs="1"')||o.canPlayType("audio/wav")).replace(/^no$/,""),aac:!!o.canPlayType("audio/aac;").replace(/^no$/,""),caf:!!o.canPlayType("audio/x-caf;").replace(/^no$/,""),m4a:!!(o.canPlayType("audio/x-m4a;")||o.canPlayType("audio/m4a;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),m4b:!!(o.canPlayType("audio/x-m4b;")||o.canPlayType("audio/m4b;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),mp4:!!(o.canPlayType("audio/x-mp4;")||o.canPlayType("audio/mp4;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),weba:!(_||!o.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,"")),webm:!(_||!o.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,"")),dolby:!!o.canPlayType('audio/mp4; codecs="ec-3"').replace(/^no$/,""),flac:!!(o.canPlayType("audio/x-flac;")||o.canPlayType("audio/flac;")).replace(/^no$/,"")},e},_unlockAudio:function(){var e=this||n;if(!e._audioUnlocked&&e.ctx){e._audioUnlocked=!1,e.autoUnlock=!1,e._mobileUnloaded||44100===e.ctx.sampleRate||(e._mobileUnloaded=!0,e.unload()),e._scratchBuffer=e.ctx.createBuffer(1,1,22050);var o=function(n){for(;e._html5AudioPool.length0?d._seek:t._sprite[e][0]/1e3),s=Math.max(0,(t._sprite[e][0]+t._sprite[e][1])/1e3-_),l=1e3*s/Math.abs(d._rate),c=t._sprite[e][0]/1e3,f=(t._sprite[e][0]+t._sprite[e][1])/1e3;d._sprite=e,d._ended=!1;var p=function(){d._paused=!1,d._seek=_,d._start=c,d._stop=f,d._loop=!(!d._loop&&!t._sprite[e][2])};if(_>=f)return void t._ended(d);var m=d._node;if(t._webAudio){var v=function(){t._playLock=!1,p(),t._refreshBuffer(d);var e=d._muted||t._muted?0:d._volume;m.gain.setValueAtTime(e,n.ctx.currentTime),d._playStart=n.ctx.currentTime,void 0===m.bufferSource.start?d._loop?m.bufferSource.noteGrainOn(0,_,86400):m.bufferSource.noteGrainOn(0,_,s):d._loop?m.bufferSource.start(0,_,86400):m.bufferSource.start(0,_,s),l!==1/0&&(t._endTimers[d._id]=setTimeout(t._ended.bind(t,d),l)),o||setTimeout(function(){t._emit("play",d._id),t._loadQueue()},0)};"running"===n.state&&"interrupted"!==n.ctx.state?v():(t._playLock=!0,t.once("resume",v),t._clearTimer(d._id))}else{var h=function(){m.currentTime=_,m.muted=d._muted||t._muted||n._muted||m.muted,m.volume=d._volume*n.volume(),m.playbackRate=d._rate;try{var r=m.play();if(r&&"undefined"!=typeof Promise&&(r instanceof Promise||"function"==typeof r.then)?(t._playLock=!0,p(),r.then(function(){t._playLock=!1,m._unlocked=!0,o?t._loadQueue():t._emit("play",d._id)}).catch(function(){t._playLock=!1,t._emit("playerror",d._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."),d._ended=!0,d._paused=!0})):o||(t._playLock=!1,p(),t._emit("play",d._id)),m.playbackRate=d._rate,m.paused)return void t._emit("playerror",d._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.");"__default"!==e||d._loop?t._endTimers[d._id]=setTimeout(t._ended.bind(t,d),l):(t._endTimers[d._id]=function(){t._ended(d),m.removeEventListener("ended",t._endTimers[d._id],!1)},m.addEventListener("ended",t._endTimers[d._id],!1))}catch(e){t._emit("playerror",d._id,e)}};"data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA"===m.src&&(m.src=t._src,m.load());var y=window&&window.ejecta||!m.readyState&&n._navigator.isCocoonJS;if(m.readyState>=3||y)h();else{t._playLock=!0,t._state="loading";var g=function(){t._state="loaded",h(),m.removeEventListener(n._canPlayEvent,g,!1)};m.addEventListener(n._canPlayEvent,g,!1),t._clearTimer(d._id)}}return d._id},pause:function(e){var n=this;if("loaded"!==n._state||n._playLock)return n._queue.push({event:"pause",action:function(){n.pause(e)}}),n;for(var o=n._getSoundIds(e),t=0;t=0?o=parseInt(r[0],10):e=parseFloat(r[0])}else r.length>=2&&(e=parseFloat(r[0]),o=parseInt(r[1],10));var a;if(!(void 0!==e&&e>=0&&e<=1))return a=o?t._soundById(o):t._sounds[0],a?a._volume:0;if("loaded"!==t._state||t._playLock)return t._queue.push({event:"volume",action:function(){t.volume.apply(t,r)}}),t;void 0===o&&(t._volume=e),o=t._getSoundIds(o);for(var u=0;u0?t/_:t),l=Date.now();e._fadeTo=o,e._interval=setInterval(function(){var r=(Date.now()-l)/t;l=Date.now(),d+=i*r,d=Math.round(100*d)/100,d=i<0?Math.max(o,d):Math.min(o,d),u._webAudio?e._volume=d:u.volume(d,e._id,!0),a&&(u._volume=d),(on&&d>=o)&&(clearInterval(e._interval),e._interval=null,e._fadeTo=null,u.volume(o,e._id),u._emit("fade",e._id))},s)},_stopFade:function(e){var o=this,t=o._soundById(e);return t&&t._interval&&(o._webAudio&&t._node.gain.cancelScheduledValues(n.ctx.currentTime),clearInterval(t._interval),t._interval=null,o.volume(t._fadeTo,e),t._fadeTo=null,o._emit("fade",e)),o},loop:function(){var e,n,o,t=this,r=arguments;if(0===r.length)return t._loop;if(1===r.length){if("boolean"!=typeof r[0])return!!(o=t._soundById(parseInt(r[0],10)))&&o._loop;e=r[0],t._loop=e}else 2===r.length&&(e=r[0],n=parseInt(r[1],10));for(var a=t._getSoundIds(n),u=0;u=0?o=parseInt(r[0],10):e=parseFloat(r[0])}else 2===r.length&&(e=parseFloat(r[0]),o=parseInt(r[1],10));var d;if("number"!=typeof e)return d=t._soundById(o),d?d._rate:t._rate;if("loaded"!==t._state||t._playLock)return t._queue.push({event:"rate",action:function(){t.rate.apply(t,r)}}),t;void 0===o&&(t._rate=e),o=t._getSoundIds(o);for(var i=0;i=0?o=parseInt(r[0],10):t._sounds.length&&(o=t._sounds[0]._id,e=parseFloat(r[0]))}else 2===r.length&&(e=parseFloat(r[0]),o=parseInt(r[1],10));if(void 0===o)return 0;if("number"==typeof e&&("loaded"!==t._state||t._playLock))return t._queue.push({event:"seek",action:function(){t.seek.apply(t,r)}}),t;var d=t._soundById(o);if(d){if(!("number"==typeof e&&e>=0)){if(t._webAudio){var i=t.playing(o)?n.ctx.currentTime-d._playStart:0,_=d._rateSeek?d._rateSeek-d._seek:0;return d._seek+(_+i*Math.abs(d._rate))}return d._node.currentTime}var s=t.playing(o);s&&t.pause(o,!0),d._seek=e,d._ended=!1,t._clearTimer(o),t._webAudio||!d._node||isNaN(d._node.duration)||(d._node.currentTime=e);var l=function(){s&&t.play(o,!0),t._emit("seek",o)};if(s&&!t._webAudio){var c=function(){t._playLock?setTimeout(c,0):l()};setTimeout(c,0)}else l()}return t},playing:function(e){var n=this;if("number"==typeof e){var o=n._soundById(e);return!!o&&!o._paused}for(var t=0;t=0&&n._howls.splice(a,1);var u=!0;for(t=0;t=0){u=!1;break}return r&&u&&delete r[e._src],n.noAudio=!1,e._state="unloaded",e._sounds=[],e=null,null},on:function(e,n,o,t){var r=this,a=r["_on"+e];return"function"==typeof n&&a.push(t?{id:o,fn:n,once:t}:{id:o,fn:n}),r},off:function(e,n,o){var t=this,r=t["_on"+e],a=0;if("number"==typeof n&&(o=n,n=null),n||o)for(a=0;a=0;a--)r[a].id&&r[a].id!==n&&"load"!==e||(setTimeout(function(e){e.call(this,n,o)}.bind(t,r[a].fn),0),r[a].once&&t.off(e,r[a].fn,r[a].id));return t._loadQueue(e),t},_loadQueue:function(e){var n=this;if(n._queue.length>0){var o=n._queue[0];o.event===e&&(n._queue.shift(),n._loadQueue()),e||o.action()}return n},_ended:function(e){var o=this,t=e._sprite;if(!o._webAudio&&e._node&&!e._node.paused&&!e._node.ended&&e._node.currentTime=0;t--){if(o<=n)return;e._sounds[t]._ended&&(e._webAudio&&e._sounds[t]._node&&e._sounds[t]._node.disconnect(0),e._sounds.splice(t,1),o--)}}},_getSoundIds:function(e){var n=this;if(void 0===e){for(var o=[],t=0;t=0;if(n._scratchBuffer&&e.bufferSource&&(e.bufferSource.onended=null,e.bufferSource.disconnect(0),t))try{e.bufferSource.buffer=n._scratchBuffer}catch(e){}return e.bufferSource=null,o},_clearSound:function(e){/MSIE |Trident\//.test(n._navigator&&n._navigator.userAgent)||(e.src="data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA")}};var t=function(e){this._parent=e,this.init()};t.prototype={init:function(){var e=this,o=e._parent;return e._muted=o._muted,e._loop=o._loop,e._volume=o._volume,e._rate=o._rate,e._seek=0,e._paused=!0,e._ended=!0,e._sprite="__default",e._id=++n._counter,o._sounds.push(e),e.create(),e},create:function(){var e=this,o=e._parent,t=n._muted||e._muted||e._parent._muted?0:e._volume;return o._webAudio?(e._node=void 0===n.ctx.createGain?n.ctx.createGainNode():n.ctx.createGain(),e._node.gain.setValueAtTime(t,n.ctx.currentTime),e._node.paused=!0,e._node.connect(n.masterGain)):n.noAudio||(e._node=n._obtainHtml5Audio(),e._errorFn=e._errorListener.bind(e),e._node.addEventListener("error",e._errorFn,!1),e._loadFn=e._loadListener.bind(e),e._node.addEventListener(n._canPlayEvent,e._loadFn,!1),e._endFn=e._endListener.bind(e),e._node.addEventListener("ended",e._endFn,!1),e._node.src=o._src,e._node.preload=!0===o._preload?"auto":o._preload,e._node.volume=t*n.volume(),e._node.load()),e},reset:function(){var e=this,o=e._parent;return e._muted=o._muted,e._loop=o._loop,e._volume=o._volume,e._rate=o._rate,e._seek=0,e._rateSeek=0,e._paused=!0,e._ended=!0,e._sprite="__default",e._id=++n._counter,e},_errorListener:function(){var e=this;e._parent._emit("loaderror",e._id,e._node.error?e._node.error.code:0),e._node.removeEventListener("error",e._errorFn,!1)},_loadListener:function(){var e=this,o=e._parent;o._duration=Math.ceil(10*e._node.duration)/10,0===Object.keys(o._sprite).length&&(o._sprite={__default:[0,1e3*o._duration]}),"loaded"!==o._state&&(o._state="loaded",o._emit("load"),o._loadQueue()),e._node.removeEventListener(n._canPlayEvent,e._loadFn,!1)},_endListener:function(){var e=this,n=e._parent;n._duration===1/0&&(n._duration=Math.ceil(10*e._node.duration)/10,n._sprite.__default[1]===1/0&&(n._sprite.__default[1]=1e3*n._duration),n._ended(e)),e._node.removeEventListener("ended",e._endFn,!1)}};var r={},a=function(e){var n=e._src;if(r[n])return e._duration=r[n].duration,void i(e);if(/^data:[^;]+;base64,/.test(n)){for(var o=atob(n.split(",")[1]),t=new Uint8Array(o.length),a=0;a0?(r[o._src]=e,i(o,e)):t()};"undefined"!=typeof Promise&&1===n.ctx.decodeAudioData.length?n.ctx.decodeAudioData(e).then(a).catch(t):n.ctx.decodeAudioData(e,a,t)},i=function(e,n){n&&!e._duration&&(e._duration=n.duration),0===Object.keys(e._sprite).length&&(e._sprite={__default:[0,1e3*e._duration]}),"loaded"!==e._state&&(e._state="loaded",e._emit("load"),e._loadQueue())},_=function(){if(n.usingWebAudio){try{"undefined"!=typeof AudioContext?n.ctx=new AudioContext:"undefined"!=typeof webkitAudioContext?n.ctx=new webkitAudioContext:n.usingWebAudio=!1}catch(e){n.usingWebAudio=!1}n.ctx||(n.usingWebAudio=!1);var e=/iP(hone|od|ad)/.test(n._navigator&&n._navigator.platform),o=n._navigator&&n._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/),t=o?parseInt(o[1],10):null;if(e&&t&&t<9){var r=/safari/.test(n._navigator&&n._navigator.userAgent.toLowerCase());n._navigator&&!r&&(n.usingWebAudio=!1)}n.usingWebAudio&&(n.masterGain=void 0===n.ctx.createGain?n.ctx.createGainNode():n.ctx.createGain(),n.masterGain.gain.setValueAtTime(n._muted?0:n._volume,n.ctx.currentTime),n.masterGain.connect(n.ctx.destination)),n._setup()}};"function"==typeof define&&define.amd&&define([],function(){return{Howler:n,Howl:o}}),"undefined"!=typeof exports&&(exports.Howler=n,exports.Howl=o),"undefined"!=typeof global?(global.HowlerGlobal=e,global.Howler=n,global.Howl=o,global.Sound=t):"undefined"!=typeof window&&(window.HowlerGlobal=e,window.Howler=n,window.Howl=o,window.Sound=t)}(); \ No newline at end of file diff --git a/dist/howler.d.ts b/dist/howler.d.ts new file mode 100644 index 00000000..63e68a25 --- /dev/null +++ b/dist/howler.d.ts @@ -0,0 +1,99 @@ +import Howl from './howl'; +export interface HowlerAudioElement extends HTMLAudioElement { + _unlocked: boolean; +} +declare type HowlerAudioContextState = AudioContextState | 'suspending' | 'closed' | 'interrupted'; +export declare type HowlerAudioContext = Omit & { + state: AudioContextState | 'interrupted'; +}; +declare class Howler { + masterGain: GainNode | null; + noAudio: boolean; + usingWebAudio: boolean; + autoSuspend: boolean; + ctx: HowlerAudioContext | null; + autoUnlock: boolean; + _counter: number; + _html5AudioPool: Array; + html5PoolSize: number; + _codecs: {}; + _howls: Array; + _muted: boolean; + _volume: number; + _canPlayEvent: string; + _navigator: Navigator; + _audioUnlocked: boolean; + _mobileUnloaded: boolean; + state: HowlerAudioContextState; + _suspendTimer: number | null; + _resumeAfterSuspend?: boolean; + _scratchBuffer: any; + /** + * Create the global controller. All contained methods and properties apply + * to all sounds that are currently playing or will be in the future. + */ + constructor(); + /** + * Get/set the global volume for all sounds. + * @param vol Volume from 0.0 to 1.0. + * @return Returns self or current volume. + */ + volume(vol?: number | string): number | this; + /** + * Handle muting and unmuting globally. + * @param muted Is muted or not. + */ + mute(muted: boolean): this; + /** + * Handle stopping all sounds globally. + */ + stop(): this; + /** + * Unload and destroy all currently loaded Howl objects. + */ + unload(): this; + /** + * Check for codec support of specific extension. + * @param ext Audio file extention. + */ + codecs(ext: string): any; + /** + * Setup various state values for global tracking. + */ + _setup(): this; + /** + * Setup the audio context when available, or switch to HTML5 Audio mode. + */ + _setupAudioContext(): void; + /** + * Check for browser support for various codecs and cache the results. + */ + _setupCodecs(): this; + /** + * 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/ + */ + _unlockAudio(): this; + /** + * Get an unlocked HTML5 Audio object from the pool. If none are left, + * return a new Audio object and throw a warning. + * @return HTML5 Audio object. + */ + _obtainHtml5Audio(): HTMLAudioElement | undefined; + /** + * Return an activated HTML5 Audio object to the pool. + */ + _releaseHtml5Audio(audio: HowlerAudioElement): this; + /** + * Automatically suspend the Web Audio AudioContext after no sound has played for 30 seconds. + * This saves processing/energy and fixes various browser-specific bugs with audio getting stuck. + */ + _autoSuspend(): this | undefined; + /** + * Automatically resume the Web Audio AudioContext when a new sound is played. + */ + _autoResume(): this | undefined; +} +declare const HowlerSingleton: Howler; +export default HowlerSingleton; diff --git a/dist/howler.js b/dist/howler.js deleted file mode 100644 index a2758c70..00000000 --- a/dist/howler.js +++ /dev/null @@ -1,3242 +0,0 @@ -/*! - * 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'; - - // 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 (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; - } -})(); - - -/*! - * Spatial Plugin - Adds support for stereo and 3D audio where Web Audio is supported. - * - * howler.js v2.2.3 - * howlerjs.com - * - * (c) 2013-2020, James Simpson of GoldFire Studios - * goldfirestudios.com - * - * MIT License - */ - -(function() { - - 'use strict'; - - // Setup default properties. - HowlerGlobal.prototype._pos = [0, 0, 0]; - HowlerGlobal.prototype._orientation = [0, 0, -1, 0, 1, 0]; - - /** Global Methods **/ - /***************************************************************************/ - - /** - * Helper method to update the stereo panning position of all current Howls. - * Future Howls will not use this value unless explicitly set. - * @param {Number} pan A value of -1.0 is all the way left and 1.0 is all the way right. - * @return {Howler/Number} Self or current stereo panning value. - */ - HowlerGlobal.prototype.stereo = function(pan) { - var self = this; - - // Stop right here if not using Web Audio. - if (!self.ctx || !self.ctx.listener) { - return self; - } - - // Loop through all Howls and update their stereo panning. - for (var i=self._howls.length-1; i>=0; i--) { - self._howls[i].stereo(pan); - } - - return self; - }; - - /** - * Get/set the position of the listener in 3D cartesian space. Sounds using - * 3D position will be relative to the listener's position. - * @param {Number} x The x-position of the listener. - * @param {Number} y The y-position of the listener. - * @param {Number} z The z-position of the listener. - * @return {Howler/Array} Self or current listener position. - */ - HowlerGlobal.prototype.pos = function(x, y, z) { - var self = this; - - // Stop right here if not using Web Audio. - if (!self.ctx || !self.ctx.listener) { - return self; - } - - // Set the defaults for optional 'y' & 'z'. - y = (typeof y !== 'number') ? self._pos[1] : y; - z = (typeof z !== 'number') ? self._pos[2] : z; - - if (typeof x === 'number') { - self._pos = [x, y, z]; - - if (typeof self.ctx.listener.positionX !== 'undefined') { - self.ctx.listener.positionX.setTargetAtTime(self._pos[0], Howler.ctx.currentTime, 0.1); - self.ctx.listener.positionY.setTargetAtTime(self._pos[1], Howler.ctx.currentTime, 0.1); - self.ctx.listener.positionZ.setTargetAtTime(self._pos[2], Howler.ctx.currentTime, 0.1); - } else { - self.ctx.listener.setPosition(self._pos[0], self._pos[1], self._pos[2]); - } - } else { - return self._pos; - } - - return self; - }; - - /** - * Get/set the direction the listener is pointing in the 3D cartesian space. - * A front and up vector must be provided. The front is the direction the - * face of the listener is pointing, and up is the direction the top of the - * listener is pointing. Thus, these values are expected to be at right angles - * from each other. - * @param {Number} x The x-orientation of the listener. - * @param {Number} y The y-orientation of the listener. - * @param {Number} z The z-orientation of the listener. - * @param {Number} xUp The x-orientation of the top of the listener. - * @param {Number} yUp The y-orientation of the top of the listener. - * @param {Number} zUp The z-orientation of the top of the listener. - * @return {Howler/Array} Returns self or the current orientation vectors. - */ - HowlerGlobal.prototype.orientation = function(x, y, z, xUp, yUp, zUp) { - var self = this; - - // Stop right here if not using Web Audio. - if (!self.ctx || !self.ctx.listener) { - return self; - } - - // Set the defaults for optional 'y' & 'z'. - var or = self._orientation; - y = (typeof y !== 'number') ? or[1] : y; - z = (typeof z !== 'number') ? or[2] : z; - xUp = (typeof xUp !== 'number') ? or[3] : xUp; - yUp = (typeof yUp !== 'number') ? or[4] : yUp; - zUp = (typeof zUp !== 'number') ? or[5] : zUp; - - if (typeof x === 'number') { - self._orientation = [x, y, z, xUp, yUp, zUp]; - - if (typeof self.ctx.listener.forwardX !== 'undefined') { - self.ctx.listener.forwardX.setTargetAtTime(x, Howler.ctx.currentTime, 0.1); - self.ctx.listener.forwardY.setTargetAtTime(y, Howler.ctx.currentTime, 0.1); - self.ctx.listener.forwardZ.setTargetAtTime(z, Howler.ctx.currentTime, 0.1); - self.ctx.listener.upX.setTargetAtTime(xUp, Howler.ctx.currentTime, 0.1); - self.ctx.listener.upY.setTargetAtTime(yUp, Howler.ctx.currentTime, 0.1); - self.ctx.listener.upZ.setTargetAtTime(zUp, Howler.ctx.currentTime, 0.1); - } else { - self.ctx.listener.setOrientation(x, y, z, xUp, yUp, zUp); - } - } else { - return or; - } - - return self; - }; - - /** Group Methods **/ - /***************************************************************************/ - - /** - * Add new properties to the core init. - * @param {Function} _super Core init method. - * @return {Howl} - */ - Howl.prototype.init = (function(_super) { - return function(o) { - var self = this; - - // Setup user-defined default properties. - self._orientation = o.orientation || [1, 0, 0]; - self._stereo = o.stereo || null; - self._pos = o.pos || null; - self._pannerAttr = { - coneInnerAngle: typeof o.coneInnerAngle !== 'undefined' ? o.coneInnerAngle : 360, - coneOuterAngle: typeof o.coneOuterAngle !== 'undefined' ? o.coneOuterAngle : 360, - coneOuterGain: typeof o.coneOuterGain !== 'undefined' ? o.coneOuterGain : 0, - distanceModel: typeof o.distanceModel !== 'undefined' ? o.distanceModel : 'inverse', - maxDistance: typeof o.maxDistance !== 'undefined' ? o.maxDistance : 10000, - panningModel: typeof o.panningModel !== 'undefined' ? o.panningModel : 'HRTF', - refDistance: typeof o.refDistance !== 'undefined' ? o.refDistance : 1, - rolloffFactor: typeof o.rolloffFactor !== 'undefined' ? o.rolloffFactor : 1 - }; - - // Setup event listeners. - self._onstereo = o.onstereo ? [{fn: o.onstereo}] : []; - self._onpos = o.onpos ? [{fn: o.onpos}] : []; - self._onorientation = o.onorientation ? [{fn: o.onorientation}] : []; - - // Complete initilization with howler.js core's init function. - return _super.call(this, o); - }; - })(Howl.prototype.init); - - /** - * Get/set the stereo panning of the audio source for this sound or all in the group. - * @param {Number} pan A value of -1.0 is all the way left and 1.0 is all the way right. - * @param {Number} id (optional) The sound ID. If none is passed, all in group will be updated. - * @return {Howl/Number} Returns self or the current stereo panning value. - */ - Howl.prototype.stereo = function(pan, id) { - var self = this; - - // Stop right here if not using Web Audio. - if (!self._webAudio) { - return self; - } - - // If the sound hasn't loaded, add it to the load queue to change stereo pan when capable. - if (self._state !== 'loaded') { - self._queue.push({ - event: 'stereo', - action: function() { - self.stereo(pan, id); - } - }); - - return self; - } - - // Check for PannerStereoNode support and fallback to PannerNode if it doesn't exist. - var pannerType = (typeof Howler.ctx.createStereoPanner === 'undefined') ? 'spatial' : 'stereo'; - - // Setup the group's stereo panning if no ID is passed. - if (typeof id === 'undefined') { - // Return the group's stereo panning if no parameters are passed. - if (typeof pan === 'number') { - self._stereo = pan; - self._pos = [pan, 0, 0]; - } else { - return self._stereo; - } - } - - // Change the streo panning of one or all sounds in group. - var ids = self._getSoundIds(id); - for (var i=0; i Returns the group's values. - * pannerAttr(id) -> Returns the sound id's values. - * pannerAttr(o) -> Set's the values of all sounds in this Howl group. - * pannerAttr(o, id) -> Set's the values of passed sound id. - * - * Attributes: - * coneInnerAngle - (360 by default) A parameter for directional audio sources, this is an angle, in degrees, - * inside of which there will be no volume reduction. - * coneOuterAngle - (360 by default) A parameter for directional audio sources, this is an angle, in degrees, - * outside of which the volume will be reduced to a constant value of `coneOuterGain`. - * coneOuterGain - (0 by default) A parameter for directional audio sources, this is the gain outside of the - * `coneOuterAngle`. It is a linear value in the range `[0, 1]`. - * distanceModel - ('inverse' by default) Determines algorithm used to reduce volume as audio moves away from - * listener. Can be `linear`, `inverse` or `exponential. - * maxDistance - (10000 by default) The maximum distance between source and listener, after which the volume - * will not be reduced any further. - * refDistance - (1 by default) A reference distance for reducing volume as source moves further from the listener. - * This is simply a variable of the distance model and has a different effect depending on which model - * is used and the scale of your coordinates. Generally, volume will be equal to 1 at this distance. - * rolloffFactor - (1 by default) How quickly the volume reduces as source moves from listener. This is simply a - * variable of the distance model and can be in the range of `[0, 1]` with `linear` and `[0, ∞]` - * with `inverse` and `exponential`. - * panningModel - ('HRTF' by default) Determines which spatialization algorithm is used to position audio. - * Can be `HRTF` or `equalpower`. - * - * @return {Howl/Object} Returns self or current panner attributes. - */ - Howl.prototype.pannerAttr = function() { - var self = this; - var args = arguments; - var o, id, sound; - - // Stop right here if not using Web Audio. - if (!self._webAudio) { - return self; - } - - // Determine the values based on arguments. - if (args.length === 0) { - // Return the group's panner attribute values. - return self._pannerAttr; - } else if (args.length === 1) { - if (typeof args[0] === 'object') { - o = args[0]; - - // Set the grou's panner attribute values. - if (typeof id === 'undefined') { - if (!o.pannerAttr) { - o.pannerAttr = { - coneInnerAngle: o.coneInnerAngle, - coneOuterAngle: o.coneOuterAngle, - coneOuterGain: o.coneOuterGain, - distanceModel: o.distanceModel, - maxDistance: o.maxDistance, - refDistance: o.refDistance, - rolloffFactor: o.rolloffFactor, - panningModel: o.panningModel - }; - } - - self._pannerAttr = { - coneInnerAngle: typeof o.pannerAttr.coneInnerAngle !== 'undefined' ? o.pannerAttr.coneInnerAngle : self._coneInnerAngle, - coneOuterAngle: typeof o.pannerAttr.coneOuterAngle !== 'undefined' ? o.pannerAttr.coneOuterAngle : self._coneOuterAngle, - coneOuterGain: typeof o.pannerAttr.coneOuterGain !== 'undefined' ? o.pannerAttr.coneOuterGain : self._coneOuterGain, - distanceModel: typeof o.pannerAttr.distanceModel !== 'undefined' ? o.pannerAttr.distanceModel : self._distanceModel, - maxDistance: typeof o.pannerAttr.maxDistance !== 'undefined' ? o.pannerAttr.maxDistance : self._maxDistance, - refDistance: typeof o.pannerAttr.refDistance !== 'undefined' ? o.pannerAttr.refDistance : self._refDistance, - rolloffFactor: typeof o.pannerAttr.rolloffFactor !== 'undefined' ? o.pannerAttr.rolloffFactor : self._rolloffFactor, - panningModel: typeof o.pannerAttr.panningModel !== 'undefined' ? o.pannerAttr.panningModel : self._panningModel - }; - } - } else { - // Return this sound's panner attribute values. - sound = self._soundById(parseInt(args[0], 10)); - return sound ? sound._pannerAttr : self._pannerAttr; - } - } else if (args.length === 2) { - o = args[0]; - id = parseInt(args[1], 10); - } - - // Update the values of the specified sounds. - var ids = self._getSoundIds(id); - for (var i=0; i=0&&e<=1){if(o._volume=e,o._muted)return o;o.usingWebAudio&&o.masterGain.gain.setValueAtTime(e,n.ctx.currentTime);for(var t=0;t=0;o--)e._howls[o].unload();return e.usingWebAudio&&e.ctx&&void 0!==e.ctx.close&&(e.ctx.close(),e.ctx=null,_()),e},codecs:function(e){return(this||n)._codecs[e.replace(/^x-/,"")]},_setup:function(){var e=this||n;if(e.state=e.ctx?e.ctx.state||"suspended":"suspended",e._autoSuspend(),!e.usingWebAudio)if("undefined"!=typeof Audio)try{var o=new Audio;void 0===o.oncanplaythrough&&(e._canPlayEvent="canplay")}catch(n){e.noAudio=!0}else e.noAudio=!0;try{var o=new Audio;o.muted&&(e.noAudio=!0)}catch(e){}return e.noAudio||e._setupCodecs(),e},_setupCodecs:function(){var e=this||n,o=null;try{o="undefined"!=typeof Audio?new Audio:null}catch(n){return e}if(!o||"function"!=typeof o.canPlayType)return e;var t=o.canPlayType("audio/mpeg;").replace(/^no$/,""),r=e._navigator?e._navigator.userAgent:"",a=r.match(/OPR\/([0-6].)/g),u=a&&parseInt(a[0].split("/")[1],10)<33,d=-1!==r.indexOf("Safari")&&-1===r.indexOf("Chrome"),i=r.match(/Version\/(.*?) /),_=d&&i&&parseInt(i[1],10)<15;return e._codecs={mp3:!(u||!t&&!o.canPlayType("audio/mp3;").replace(/^no$/,"")),mpeg:!!t,opus:!!o.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/,""),ogg:!!o.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),oga:!!o.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),wav:!!(o.canPlayType('audio/wav; codecs="1"')||o.canPlayType("audio/wav")).replace(/^no$/,""),aac:!!o.canPlayType("audio/aac;").replace(/^no$/,""),caf:!!o.canPlayType("audio/x-caf;").replace(/^no$/,""),m4a:!!(o.canPlayType("audio/x-m4a;")||o.canPlayType("audio/m4a;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),m4b:!!(o.canPlayType("audio/x-m4b;")||o.canPlayType("audio/m4b;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),mp4:!!(o.canPlayType("audio/x-mp4;")||o.canPlayType("audio/mp4;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),weba:!(_||!o.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,"")),webm:!(_||!o.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,"")),dolby:!!o.canPlayType('audio/mp4; codecs="ec-3"').replace(/^no$/,""),flac:!!(o.canPlayType("audio/x-flac;")||o.canPlayType("audio/flac;")).replace(/^no$/,"")},e},_unlockAudio:function(){var e=this||n;if(!e._audioUnlocked&&e.ctx){e._audioUnlocked=!1,e.autoUnlock=!1,e._mobileUnloaded||44100===e.ctx.sampleRate||(e._mobileUnloaded=!0,e.unload()),e._scratchBuffer=e.ctx.createBuffer(1,1,22050);var o=function(n){for(;e._html5AudioPool.length0?d._seek:t._sprite[e][0]/1e3),s=Math.max(0,(t._sprite[e][0]+t._sprite[e][1])/1e3-_),l=1e3*s/Math.abs(d._rate),c=t._sprite[e][0]/1e3,f=(t._sprite[e][0]+t._sprite[e][1])/1e3;d._sprite=e,d._ended=!1;var p=function(){d._paused=!1,d._seek=_,d._start=c,d._stop=f,d._loop=!(!d._loop&&!t._sprite[e][2])};if(_>=f)return void t._ended(d);var m=d._node;if(t._webAudio){var v=function(){t._playLock=!1,p(),t._refreshBuffer(d);var e=d._muted||t._muted?0:d._volume;m.gain.setValueAtTime(e,n.ctx.currentTime),d._playStart=n.ctx.currentTime,void 0===m.bufferSource.start?d._loop?m.bufferSource.noteGrainOn(0,_,86400):m.bufferSource.noteGrainOn(0,_,s):d._loop?m.bufferSource.start(0,_,86400):m.bufferSource.start(0,_,s),l!==1/0&&(t._endTimers[d._id]=setTimeout(t._ended.bind(t,d),l)),o||setTimeout(function(){t._emit("play",d._id),t._loadQueue()},0)};"running"===n.state&&"interrupted"!==n.ctx.state?v():(t._playLock=!0,t.once("resume",v),t._clearTimer(d._id))}else{var h=function(){m.currentTime=_,m.muted=d._muted||t._muted||n._muted||m.muted,m.volume=d._volume*n.volume(),m.playbackRate=d._rate;try{var r=m.play();if(r&&"undefined"!=typeof Promise&&(r instanceof Promise||"function"==typeof r.then)?(t._playLock=!0,p(),r.then(function(){t._playLock=!1,m._unlocked=!0,o?t._loadQueue():t._emit("play",d._id)}).catch(function(){t._playLock=!1,t._emit("playerror",d._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."),d._ended=!0,d._paused=!0})):o||(t._playLock=!1,p(),t._emit("play",d._id)),m.playbackRate=d._rate,m.paused)return void t._emit("playerror",d._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.");"__default"!==e||d._loop?t._endTimers[d._id]=setTimeout(t._ended.bind(t,d),l):(t._endTimers[d._id]=function(){t._ended(d),m.removeEventListener("ended",t._endTimers[d._id],!1)},m.addEventListener("ended",t._endTimers[d._id],!1))}catch(e){t._emit("playerror",d._id,e)}};"data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA"===m.src&&(m.src=t._src,m.load());var y=window&&window.ejecta||!m.readyState&&n._navigator.isCocoonJS;if(m.readyState>=3||y)h();else{t._playLock=!0,t._state="loading";var g=function(){t._state="loaded",h(),m.removeEventListener(n._canPlayEvent,g,!1)};m.addEventListener(n._canPlayEvent,g,!1),t._clearTimer(d._id)}}return d._id},pause:function(e){var n=this;if("loaded"!==n._state||n._playLock)return n._queue.push({event:"pause",action:function(){n.pause(e)}}),n;for(var o=n._getSoundIds(e),t=0;t=0?o=parseInt(r[0],10):e=parseFloat(r[0])}else r.length>=2&&(e=parseFloat(r[0]),o=parseInt(r[1],10));var a;if(!(void 0!==e&&e>=0&&e<=1))return a=o?t._soundById(o):t._sounds[0],a?a._volume:0;if("loaded"!==t._state||t._playLock)return t._queue.push({event:"volume",action:function(){t.volume.apply(t,r)}}),t;void 0===o&&(t._volume=e),o=t._getSoundIds(o);for(var u=0;u0?t/_:t),l=Date.now();e._fadeTo=o,e._interval=setInterval(function(){var r=(Date.now()-l)/t;l=Date.now(),d+=i*r,d=Math.round(100*d)/100,d=i<0?Math.max(o,d):Math.min(o,d),u._webAudio?e._volume=d:u.volume(d,e._id,!0),a&&(u._volume=d),(on&&d>=o)&&(clearInterval(e._interval),e._interval=null,e._fadeTo=null,u.volume(o,e._id),u._emit("fade",e._id))},s)},_stopFade:function(e){var o=this,t=o._soundById(e);return t&&t._interval&&(o._webAudio&&t._node.gain.cancelScheduledValues(n.ctx.currentTime),clearInterval(t._interval),t._interval=null,o.volume(t._fadeTo,e),t._fadeTo=null,o._emit("fade",e)),o},loop:function(){var e,n,o,t=this,r=arguments;if(0===r.length)return t._loop;if(1===r.length){if("boolean"!=typeof r[0])return!!(o=t._soundById(parseInt(r[0],10)))&&o._loop;e=r[0],t._loop=e}else 2===r.length&&(e=r[0],n=parseInt(r[1],10));for(var a=t._getSoundIds(n),u=0;u=0?o=parseInt(r[0],10):e=parseFloat(r[0])}else 2===r.length&&(e=parseFloat(r[0]),o=parseInt(r[1],10));var d;if("number"!=typeof e)return d=t._soundById(o),d?d._rate:t._rate;if("loaded"!==t._state||t._playLock)return t._queue.push({event:"rate",action:function(){t.rate.apply(t,r)}}),t;void 0===o&&(t._rate=e),o=t._getSoundIds(o);for(var i=0;i=0?o=parseInt(r[0],10):t._sounds.length&&(o=t._sounds[0]._id,e=parseFloat(r[0]))}else 2===r.length&&(e=parseFloat(r[0]),o=parseInt(r[1],10));if(void 0===o)return 0;if("number"==typeof e&&("loaded"!==t._state||t._playLock))return t._queue.push({event:"seek",action:function(){t.seek.apply(t,r)}}),t;var d=t._soundById(o);if(d){if(!("number"==typeof e&&e>=0)){if(t._webAudio){var i=t.playing(o)?n.ctx.currentTime-d._playStart:0,_=d._rateSeek?d._rateSeek-d._seek:0;return d._seek+(_+i*Math.abs(d._rate))}return d._node.currentTime}var s=t.playing(o);s&&t.pause(o,!0),d._seek=e,d._ended=!1,t._clearTimer(o),t._webAudio||!d._node||isNaN(d._node.duration)||(d._node.currentTime=e);var l=function(){s&&t.play(o,!0),t._emit("seek",o)};if(s&&!t._webAudio){var c=function(){t._playLock?setTimeout(c,0):l()};setTimeout(c,0)}else l()}return t},playing:function(e){var n=this;if("number"==typeof e){var o=n._soundById(e);return!!o&&!o._paused}for(var t=0;t=0&&n._howls.splice(a,1);var u=!0;for(t=0;t=0){u=!1;break}return r&&u&&delete r[e._src],n.noAudio=!1,e._state="unloaded",e._sounds=[],e=null,null},on:function(e,n,o,t){var r=this,a=r["_on"+e];return"function"==typeof n&&a.push(t?{id:o,fn:n,once:t}:{id:o,fn:n}),r},off:function(e,n,o){var t=this,r=t["_on"+e],a=0;if("number"==typeof n&&(o=n,n=null),n||o)for(a=0;a=0;a--)r[a].id&&r[a].id!==n&&"load"!==e||(setTimeout(function(e){e.call(this,n,o)}.bind(t,r[a].fn),0),r[a].once&&t.off(e,r[a].fn,r[a].id));return t._loadQueue(e),t},_loadQueue:function(e){var n=this;if(n._queue.length>0){var o=n._queue[0];o.event===e&&(n._queue.shift(),n._loadQueue()),e||o.action()}return n},_ended:function(e){var o=this,t=e._sprite;if(!o._webAudio&&e._node&&!e._node.paused&&!e._node.ended&&e._node.currentTime=0;t--){if(o<=n)return;e._sounds[t]._ended&&(e._webAudio&&e._sounds[t]._node&&e._sounds[t]._node.disconnect(0),e._sounds.splice(t,1),o--)}}},_getSoundIds:function(e){var n=this;if(void 0===e){for(var o=[],t=0;t=0;if(n._scratchBuffer&&e.bufferSource&&(e.bufferSource.onended=null,e.bufferSource.disconnect(0),t))try{e.bufferSource.buffer=n._scratchBuffer}catch(e){}return e.bufferSource=null,o},_clearSound:function(e){/MSIE |Trident\//.test(n._navigator&&n._navigator.userAgent)||(e.src="data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA")}};var t=function(e){this._parent=e,this.init()};t.prototype={init:function(){var e=this,o=e._parent;return e._muted=o._muted,e._loop=o._loop,e._volume=o._volume,e._rate=o._rate,e._seek=0,e._paused=!0,e._ended=!0,e._sprite="__default",e._id=++n._counter,o._sounds.push(e),e.create(),e},create:function(){var e=this,o=e._parent,t=n._muted||e._muted||e._parent._muted?0:e._volume;return o._webAudio?(e._node=void 0===n.ctx.createGain?n.ctx.createGainNode():n.ctx.createGain(),e._node.gain.setValueAtTime(t,n.ctx.currentTime),e._node.paused=!0,e._node.connect(n.masterGain)):n.noAudio||(e._node=n._obtainHtml5Audio(),e._errorFn=e._errorListener.bind(e),e._node.addEventListener("error",e._errorFn,!1),e._loadFn=e._loadListener.bind(e),e._node.addEventListener(n._canPlayEvent,e._loadFn,!1),e._endFn=e._endListener.bind(e),e._node.addEventListener("ended",e._endFn,!1),e._node.src=o._src,e._node.preload=!0===o._preload?"auto":o._preload,e._node.volume=t*n.volume(),e._node.load()),e},reset:function(){var e=this,o=e._parent;return e._muted=o._muted,e._loop=o._loop,e._volume=o._volume,e._rate=o._rate,e._seek=0,e._rateSeek=0,e._paused=!0,e._ended=!0,e._sprite="__default",e._id=++n._counter,e},_errorListener:function(){var e=this;e._parent._emit("loaderror",e._id,e._node.error?e._node.error.code:0),e._node.removeEventListener("error",e._errorFn,!1)},_loadListener:function(){var e=this,o=e._parent;o._duration=Math.ceil(10*e._node.duration)/10,0===Object.keys(o._sprite).length&&(o._sprite={__default:[0,1e3*o._duration]}),"loaded"!==o._state&&(o._state="loaded",o._emit("load"),o._loadQueue()),e._node.removeEventListener(n._canPlayEvent,e._loadFn,!1)},_endListener:function(){var e=this,n=e._parent;n._duration===1/0&&(n._duration=Math.ceil(10*e._node.duration)/10,n._sprite.__default[1]===1/0&&(n._sprite.__default[1]=1e3*n._duration),n._ended(e)),e._node.removeEventListener("ended",e._endFn,!1)}};var r={},a=function(e){var n=e._src;if(r[n])return e._duration=r[n].duration,void i(e);if(/^data:[^;]+;base64,/.test(n)){for(var o=atob(n.split(",")[1]),t=new Uint8Array(o.length),a=0;a0?(r[o._src]=e,i(o,e)):t()};"undefined"!=typeof Promise&&1===n.ctx.decodeAudioData.length?n.ctx.decodeAudioData(e).then(a).catch(t):n.ctx.decodeAudioData(e,a,t)},i=function(e,n){n&&!e._duration&&(e._duration=n.duration),0===Object.keys(e._sprite).length&&(e._sprite={__default:[0,1e3*e._duration]}),"loaded"!==e._state&&(e._state="loaded",e._emit("load"),e._loadQueue())},_=function(){if(n.usingWebAudio){try{"undefined"!=typeof AudioContext?n.ctx=new AudioContext:"undefined"!=typeof webkitAudioContext?n.ctx=new webkitAudioContext:n.usingWebAudio=!1}catch(e){n.usingWebAudio=!1}n.ctx||(n.usingWebAudio=!1);var e=/iP(hone|od|ad)/.test(n._navigator&&n._navigator.platform),o=n._navigator&&n._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/),t=o?parseInt(o[1],10):null;if(e&&t&&t<9){var r=/safari/.test(n._navigator&&n._navigator.userAgent.toLowerCase());n._navigator&&!r&&(n.usingWebAudio=!1)}n.usingWebAudio&&(n.masterGain=void 0===n.ctx.createGain?n.ctx.createGainNode():n.ctx.createGain(),n.masterGain.gain.setValueAtTime(n._muted?0:n._volume,n.ctx.currentTime),n.masterGain.connect(n.ctx.destination)),n._setup()}};"function"==typeof define&&define.amd&&define([],function(){return{Howler:n,Howl:o}}),"undefined"!=typeof exports&&(exports.Howler=n,exports.Howl=o),"undefined"!=typeof global?(global.HowlerGlobal=e,global.Howler=n,global.Howl=o,global.Sound=t):"undefined"!=typeof window&&(window.HowlerGlobal=e,window.Howler=n,window.Howl=o,window.Sound=t)}(); -/*! Spatial Plugin */ -!function(){"use strict";HowlerGlobal.prototype._pos=[0,0,0],HowlerGlobal.prototype._orientation=[0,0,-1,0,1,0],HowlerGlobal.prototype.stereo=function(e){var n=this;if(!n.ctx||!n.ctx.listener)return n;for(var t=n._howls.length-1;t>=0;t--)n._howls[t].stereo(e);return n},HowlerGlobal.prototype.pos=function(e,n,t){var r=this;return r.ctx&&r.ctx.listener?(n="number"!=typeof n?r._pos[1]:n,t="number"!=typeof t?r._pos[2]:t,"number"!=typeof e?r._pos:(r._pos=[e,n,t],void 0!==r.ctx.listener.positionX?(r.ctx.listener.positionX.setTargetAtTime(r._pos[0],Howler.ctx.currentTime,.1),r.ctx.listener.positionY.setTargetAtTime(r._pos[1],Howler.ctx.currentTime,.1),r.ctx.listener.positionZ.setTargetAtTime(r._pos[2],Howler.ctx.currentTime,.1)):r.ctx.listener.setPosition(r._pos[0],r._pos[1],r._pos[2]),r)):r},HowlerGlobal.prototype.orientation=function(e,n,t,r,o,i){var a=this;if(!a.ctx||!a.ctx.listener)return a;var s=a._orientation;return n="number"!=typeof n?s[1]:n,t="number"!=typeof t?s[2]:t,r="number"!=typeof r?s[3]:r,o="number"!=typeof o?s[4]:o,i="number"!=typeof i?s[5]:i,"number"!=typeof e?s:(a._orientation=[e,n,t,r,o,i],void 0!==a.ctx.listener.forwardX?(a.ctx.listener.forwardX.setTargetAtTime(e,Howler.ctx.currentTime,.1),a.ctx.listener.forwardY.setTargetAtTime(n,Howler.ctx.currentTime,.1),a.ctx.listener.forwardZ.setTargetAtTime(t,Howler.ctx.currentTime,.1),a.ctx.listener.upX.setTargetAtTime(r,Howler.ctx.currentTime,.1),a.ctx.listener.upY.setTargetAtTime(o,Howler.ctx.currentTime,.1),a.ctx.listener.upZ.setTargetAtTime(i,Howler.ctx.currentTime,.1)):a.ctx.listener.setOrientation(e,n,t,r,o,i),a)},Howl.prototype.init=function(e){return function(n){var t=this;return t._orientation=n.orientation||[1,0,0],t._stereo=n.stereo||null,t._pos=n.pos||null,t._pannerAttr={coneInnerAngle:void 0!==n.coneInnerAngle?n.coneInnerAngle:360,coneOuterAngle:void 0!==n.coneOuterAngle?n.coneOuterAngle:360,coneOuterGain:void 0!==n.coneOuterGain?n.coneOuterGain:0,distanceModel:void 0!==n.distanceModel?n.distanceModel:"inverse",maxDistance:void 0!==n.maxDistance?n.maxDistance:1e4,panningModel:void 0!==n.panningModel?n.panningModel:"HRTF",refDistance:void 0!==n.refDistance?n.refDistance:1,rolloffFactor:void 0!==n.rolloffFactor?n.rolloffFactor:1},t._onstereo=n.onstereo?[{fn:n.onstereo}]:[],t._onpos=n.onpos?[{fn:n.onpos}]:[],t._onorientation=n.onorientation?[{fn:n.onorientation}]:[],e.call(this,n)}}(Howl.prototype.init),Howl.prototype.stereo=function(n,t){var r=this;if(!r._webAudio)return r;if("loaded"!==r._state)return r._queue.push({event:"stereo",action:function(){r.stereo(n,t)}}),r;var o=void 0===Howler.ctx.createStereoPanner?"spatial":"stereo";if(void 0===t){if("number"!=typeof n)return r._stereo;r._stereo=n,r._pos=[n,0,0]}for(var i=r._getSoundIds(t),a=0;a=0;t--)n._howls[t].stereo(e);return n},HowlerGlobal.prototype.pos=function(e,n,t){var r=this;return r.ctx&&r.ctx.listener?(n="number"!=typeof n?r._pos[1]:n,t="number"!=typeof t?r._pos[2]:t,"number"!=typeof e?r._pos:(r._pos=[e,n,t],void 0!==r.ctx.listener.positionX?(r.ctx.listener.positionX.setTargetAtTime(r._pos[0],Howler.ctx.currentTime,.1),r.ctx.listener.positionY.setTargetAtTime(r._pos[1],Howler.ctx.currentTime,.1),r.ctx.listener.positionZ.setTargetAtTime(r._pos[2],Howler.ctx.currentTime,.1)):r.ctx.listener.setPosition(r._pos[0],r._pos[1],r._pos[2]),r)):r},HowlerGlobal.prototype.orientation=function(e,n,t,r,o,i){var a=this;if(!a.ctx||!a.ctx.listener)return a;var s=a._orientation;return n="number"!=typeof n?s[1]:n,t="number"!=typeof t?s[2]:t,r="number"!=typeof r?s[3]:r,o="number"!=typeof o?s[4]:o,i="number"!=typeof i?s[5]:i,"number"!=typeof e?s:(a._orientation=[e,n,t,r,o,i],void 0!==a.ctx.listener.forwardX?(a.ctx.listener.forwardX.setTargetAtTime(e,Howler.ctx.currentTime,.1),a.ctx.listener.forwardY.setTargetAtTime(n,Howler.ctx.currentTime,.1),a.ctx.listener.forwardZ.setTargetAtTime(t,Howler.ctx.currentTime,.1),a.ctx.listener.upX.setTargetAtTime(r,Howler.ctx.currentTime,.1),a.ctx.listener.upY.setTargetAtTime(o,Howler.ctx.currentTime,.1),a.ctx.listener.upZ.setTargetAtTime(i,Howler.ctx.currentTime,.1)):a.ctx.listener.setOrientation(e,n,t,r,o,i),a)},Howl.prototype.init=function(e){return function(n){var t=this;return t._orientation=n.orientation||[1,0,0],t._stereo=n.stereo||null,t._pos=n.pos||null,t._pannerAttr={coneInnerAngle:void 0!==n.coneInnerAngle?n.coneInnerAngle:360,coneOuterAngle:void 0!==n.coneOuterAngle?n.coneOuterAngle:360,coneOuterGain:void 0!==n.coneOuterGain?n.coneOuterGain:0,distanceModel:void 0!==n.distanceModel?n.distanceModel:"inverse",maxDistance:void 0!==n.maxDistance?n.maxDistance:1e4,panningModel:void 0!==n.panningModel?n.panningModel:"HRTF",refDistance:void 0!==n.refDistance?n.refDistance:1,rolloffFactor:void 0!==n.rolloffFactor?n.rolloffFactor:1},t._onstereo=n.onstereo?[{fn:n.onstereo}]:[],t._onpos=n.onpos?[{fn:n.onpos}]:[],t._onorientation=n.onorientation?[{fn:n.onorientation}]:[],e.call(this,n)}}(Howl.prototype.init),Howl.prototype.stereo=function(n,t){var r=this;if(!r._webAudio)return r;if("loaded"!==r._state)return r._queue.push({event:"stereo",action:function(){r.stereo(n,t)}}),r;var o=void 0===Howler.ctx.createStereoPanner?"spatial":"stereo";if(void 0===t){if("number"!=typeof n)return r._stereo;r._stereo=n,r._pos=[n,0,0]}for(var i=r._getSoundIds(t),a=0;a { + // Build declaration files with TSC since they aren't built by esbuild. + execSync('npx tsc'); + + // TODO: output minimal package.json with + // 1) export map for all possible entry points into the library + // 2) type: "module" to use esm + // 3) type definition path for built-in TS support + + const buildTime = ((performance.now() - startTime) / 1000).toLocaleString( + 'en-US', + { + minimumFractionDigits: 3, + maximumFractionDigits: 3, + }, + ); + console.log(`✅ Finished in ${buildTime} s\n`); + }) + .catch((e) => { + console.error(e); + process.exit(1); + }); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..0c3442a9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,758 @@ +{ + "name": "howler", + "version": "3.0.0@alpha.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "version": "3.0.0@alpha.0", + "license": "MIT", + "dependencies": { + "fs-extra": "^10.0.0" + }, + "devDependencies": { + "esbuild": "^0.13.2", + "prettier": "^2.4.1", + "typescript": "^4.4.3", + "vite": "^2.5.10" + } + }, + "node_modules/esbuild": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.2.tgz", + "integrity": "sha512-/tpIqo45hyRREGqh7hsIut8GwY1X2n9IhKbIwRIXUO6IohzG3/RarSGX7dT2eNvYzIbQmelpX+ZyuIphE5u+Bw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "optionalDependencies": { + "esbuild-android-arm64": "0.13.2", + "esbuild-darwin-64": "0.13.2", + "esbuild-darwin-arm64": "0.13.2", + "esbuild-freebsd-64": "0.13.2", + "esbuild-freebsd-arm64": "0.13.2", + "esbuild-linux-32": "0.13.2", + "esbuild-linux-64": "0.13.2", + "esbuild-linux-arm": "0.13.2", + "esbuild-linux-arm64": "0.13.2", + "esbuild-linux-mips64le": "0.13.2", + "esbuild-linux-ppc64le": "0.13.2", + "esbuild-openbsd-64": "0.13.2", + "esbuild-sunos-64": "0.13.2", + "esbuild-windows-32": "0.13.2", + "esbuild-windows-64": "0.13.2", + "esbuild-windows-arm64": "0.13.2" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.2.tgz", + "integrity": "sha512-Eh2paXUWYqf5JgikdkC0LnhtjSC8tGAz/L2kJRlMC0o3DzOBIxcmT2fdzBerdhW4roY0bOExfcO6deI1qsxI/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/esbuild-darwin-64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.2.tgz", + "integrity": "sha512-jqp6uXHIIAWZ8kxRqFjxyMmIE1cuSbINellwwigOgk44eLg74ls82oqjY72MbDAowPivQkOU/fF7tsyaGQf5Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.2.tgz", + "integrity": "sha512-bD4oAyPZdzOWEA/JoX0sAitOhjJRwhomhWMeyRyowtlVQhQleG2ijRUKTvkq4CAvSobrW5EnZxjvHNKJ5L7zJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.2.tgz", + "integrity": "sha512-fFJ0yc3lZyfwca+F5OPN/s+izozWryUQpN8aUMIdUkOa7UKX0h3xXrKnkDgdOo8vy3d1A6zHH0/4f2VJfEzLCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.2.tgz", + "integrity": "sha512-DWBZauEfjmqdfWxIacI+KBEim3ulOjtvK+WVm1bX67XlfyUVIkD915OIfT2EBhQUWmv+Z0tZZwskSMNj5DKojw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/esbuild-linux-32": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.2.tgz", + "integrity": "sha512-Gt2rNqqRCRh1QQC2d83KP0iWIXWQYpns7l2+41a1n0PQxXkQ5AarpjjL9mUzdXtcZauNXbUvWwBKAuBTCW+XQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/esbuild-linux-64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.2.tgz", + "integrity": "sha512-yT0D5Xly8oGHuqq975k1XUyULHzk3fN/ZlTY+awlU+nCFsYPZ43NE5msGpxlNQu8i6KOXQEke5GXN3y5d+Zd4g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/esbuild-linux-arm": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.2.tgz", + "integrity": "sha512-KXeyotqj9jbvCjbSpwnxDE8E8jKoBgrgbJpOvvY5Zz7Pp2fAwu/94vWQtE/jPEJndY4C4MSs+ryJLFWzmLOa4w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.2.tgz", + "integrity": "sha512-qwXL+3NDCWiC8RMKBBETpuOWdC+pUAUS+pwg9jJmapYblLdVKkyRtwF/ogj06TdYs6riSSNikW8HK/Xs0HHbbQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.2.tgz", + "integrity": "sha512-sx8eheRX2XC2ppNAsbQm8/VUcU8XPYGpJK0BEyRefqHONz6u5Ib2guUdOz2Wh4YlbA7oOd482lHjprXSTwUcrQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.2.tgz", + "integrity": "sha512-y8iZ3qy2TIAKKsZ6xSopCztHOtGW9oiYDl22vQ0UIoVWjnfRKrbSzX7Y2F94y32hSvRWle6OhAIC+UpS5nQmIA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.2.tgz", + "integrity": "sha512-g6AYrjBeV9OK624bw0KQ1TjHJQSW+X1Yicyd1NvDWqSFpMqKAjw7EUX4tA87mOFqv8BflPGr4f43ySgNvSVzIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/esbuild-sunos-64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.2.tgz", + "integrity": "sha512-hIXvFIyrqwFd6v62XSra0ctCUXDS9Tu5D6QYbvnbhEoBmvD/TmEJRYRH48/+xmRifKJLzu6aegcrjAsDmaww7g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ] + }, + "node_modules/esbuild-windows-32": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.2.tgz", + "integrity": "sha512-Y767LG0NFkw0sPoDVOTKC5gaj4vURjjWfSCCDV5awpXXxBKOF2zsIp3aia4KvVoivoSSeRPk3emDd0OPHuPrKg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/esbuild-windows-64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.2.tgz", + "integrity": "sha512-01b59kVJUMasctn6lzswC0drchr7zO75QtF22o5w0nlOw0Zorw0loY/8i5choFuWc30gXJId9qBSc1zPvt7uEw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.2.tgz", + "integrity": "sha512-HxyY604ytmh8NkPYyS1TdIB/bFS7DWd1hP90e8Ovo/elEdN5I13h0tyIatDYZkXKS0Ztk+9T/3h6K0fI1a/4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/fs-extra": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz", + "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/graceful-fs": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/is-core-module": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz", + "integrity": "sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/nanocolors": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/nanocolors/-/nanocolors-0.2.3.tgz", + "integrity": "sha512-RxGTOApG8prHMA08UBMOT6qYzcBBW2EMBv7SRBqoXg/Dqp6G3yT7kLy1tpFuYLO+5h7eajmdiIHJA8oewN58XQ==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.1.25", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz", + "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.3.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.8.tgz", + "integrity": "sha512-GT5bTjjZnwDifajzczOC+r3FI3Cu+PgPvrsjhQdRqa2kTJ4968/X9CUce9xttIB0xOs5c6xf0TCWZo/y9lF6bA==", + "dev": true, + "dependencies": { + "nanocolors": "^0.2.2", + "nanoid": "^3.1.25", + "source-map-js": "^0.6.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/prettier": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz", + "integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "dependencies": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "2.57.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.57.0.tgz", + "integrity": "sha512-bKQIh1rWKofRee6mv8SrF2HdP6pea5QkwBZSMImJysFj39gQuiV8MEPBjXOCpzk3wSYp63M2v2wkWBmFC8O/rg==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz", + "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", + "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/vite": { + "version": "2.5.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.5.10.tgz", + "integrity": "sha512-0ObiHTi5AHyXdJcvZ67HMsDgVpjT5RehvVKv6+Q0jFZ7zDI28PF5zK9mYz2avxdA+4iJMdwCz6wnGNnn4WX5Gg==", + "dev": true, + "dependencies": { + "esbuild": "^0.12.17", + "postcss": "^8.3.6", + "resolve": "^1.20.0", + "rollup": "^2.38.5" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": ">=12.2.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.12.29", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.29.tgz", + "integrity": "sha512-w/XuoBCSwepyiZtIRsKsetiLDUVGPVw1E/R3VTFSecIy8UR7Cq3SOtwKHJMFoVqqVG36aGkzh4e8BvpO1Fdc7g==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + } + } + }, + "dependencies": { + "esbuild": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.2.tgz", + "integrity": "sha512-/tpIqo45hyRREGqh7hsIut8GwY1X2n9IhKbIwRIXUO6IohzG3/RarSGX7dT2eNvYzIbQmelpX+ZyuIphE5u+Bw==", + "dev": true, + "requires": { + "esbuild-android-arm64": "0.13.2", + "esbuild-darwin-64": "0.13.2", + "esbuild-darwin-arm64": "0.13.2", + "esbuild-freebsd-64": "0.13.2", + "esbuild-freebsd-arm64": "0.13.2", + "esbuild-linux-32": "0.13.2", + "esbuild-linux-64": "0.13.2", + "esbuild-linux-arm": "0.13.2", + "esbuild-linux-arm64": "0.13.2", + "esbuild-linux-mips64le": "0.13.2", + "esbuild-linux-ppc64le": "0.13.2", + "esbuild-openbsd-64": "0.13.2", + "esbuild-sunos-64": "0.13.2", + "esbuild-windows-32": "0.13.2", + "esbuild-windows-64": "0.13.2", + "esbuild-windows-arm64": "0.13.2" + } + }, + "esbuild-android-arm64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.2.tgz", + "integrity": "sha512-Eh2paXUWYqf5JgikdkC0LnhtjSC8tGAz/L2kJRlMC0o3DzOBIxcmT2fdzBerdhW4roY0bOExfcO6deI1qsxI/A==", + "dev": true, + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.2.tgz", + "integrity": "sha512-jqp6uXHIIAWZ8kxRqFjxyMmIE1cuSbINellwwigOgk44eLg74ls82oqjY72MbDAowPivQkOU/fF7tsyaGQf5Zg==", + "dev": true, + "optional": true + }, + "esbuild-darwin-arm64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.2.tgz", + "integrity": "sha512-bD4oAyPZdzOWEA/JoX0sAitOhjJRwhomhWMeyRyowtlVQhQleG2ijRUKTvkq4CAvSobrW5EnZxjvHNKJ5L7zJg==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.2.tgz", + "integrity": "sha512-fFJ0yc3lZyfwca+F5OPN/s+izozWryUQpN8aUMIdUkOa7UKX0h3xXrKnkDgdOo8vy3d1A6zHH0/4f2VJfEzLCg==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.2.tgz", + "integrity": "sha512-DWBZauEfjmqdfWxIacI+KBEim3ulOjtvK+WVm1bX67XlfyUVIkD915OIfT2EBhQUWmv+Z0tZZwskSMNj5DKojw==", + "dev": true, + "optional": true + }, + "esbuild-linux-32": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.2.tgz", + "integrity": "sha512-Gt2rNqqRCRh1QQC2d83KP0iWIXWQYpns7l2+41a1n0PQxXkQ5AarpjjL9mUzdXtcZauNXbUvWwBKAuBTCW+XQg==", + "dev": true, + "optional": true + }, + "esbuild-linux-64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.2.tgz", + "integrity": "sha512-yT0D5Xly8oGHuqq975k1XUyULHzk3fN/ZlTY+awlU+nCFsYPZ43NE5msGpxlNQu8i6KOXQEke5GXN3y5d+Zd4g==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.2.tgz", + "integrity": "sha512-KXeyotqj9jbvCjbSpwnxDE8E8jKoBgrgbJpOvvY5Zz7Pp2fAwu/94vWQtE/jPEJndY4C4MSs+ryJLFWzmLOa4w==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.2.tgz", + "integrity": "sha512-qwXL+3NDCWiC8RMKBBETpuOWdC+pUAUS+pwg9jJmapYblLdVKkyRtwF/ogj06TdYs6riSSNikW8HK/Xs0HHbbQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.2.tgz", + "integrity": "sha512-sx8eheRX2XC2ppNAsbQm8/VUcU8XPYGpJK0BEyRefqHONz6u5Ib2guUdOz2Wh4YlbA7oOd482lHjprXSTwUcrQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.2.tgz", + "integrity": "sha512-y8iZ3qy2TIAKKsZ6xSopCztHOtGW9oiYDl22vQ0UIoVWjnfRKrbSzX7Y2F94y32hSvRWle6OhAIC+UpS5nQmIA==", + "dev": true, + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.2.tgz", + "integrity": "sha512-g6AYrjBeV9OK624bw0KQ1TjHJQSW+X1Yicyd1NvDWqSFpMqKAjw7EUX4tA87mOFqv8BflPGr4f43ySgNvSVzIw==", + "dev": true, + "optional": true + }, + "esbuild-sunos-64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.2.tgz", + "integrity": "sha512-hIXvFIyrqwFd6v62XSra0ctCUXDS9Tu5D6QYbvnbhEoBmvD/TmEJRYRH48/+xmRifKJLzu6aegcrjAsDmaww7g==", + "dev": true, + "optional": true + }, + "esbuild-windows-32": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.2.tgz", + "integrity": "sha512-Y767LG0NFkw0sPoDVOTKC5gaj4vURjjWfSCCDV5awpXXxBKOF2zsIp3aia4KvVoivoSSeRPk3emDd0OPHuPrKg==", + "dev": true, + "optional": true + }, + "esbuild-windows-64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.2.tgz", + "integrity": "sha512-01b59kVJUMasctn6lzswC0drchr7zO75QtF22o5w0nlOw0Zorw0loY/8i5choFuWc30gXJId9qBSc1zPvt7uEw==", + "dev": true, + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.2.tgz", + "integrity": "sha512-HxyY604ytmh8NkPYyS1TdIB/bFS7DWd1hP90e8Ovo/elEdN5I13h0tyIatDYZkXKS0Ztk+9T/3h6K0fI1a/4tQ==", + "dev": true, + "optional": true + }, + "fs-extra": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz", + "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "is-core-module": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz", + "integrity": "sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "nanocolors": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/nanocolors/-/nanocolors-0.2.3.tgz", + "integrity": "sha512-RxGTOApG8prHMA08UBMOT6qYzcBBW2EMBv7SRBqoXg/Dqp6G3yT7kLy1tpFuYLO+5h7eajmdiIHJA8oewN58XQ==", + "dev": true + }, + "nanoid": { + "version": "3.1.25", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz", + "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "postcss": { + "version": "8.3.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.8.tgz", + "integrity": "sha512-GT5bTjjZnwDifajzczOC+r3FI3Cu+PgPvrsjhQdRqa2kTJ4968/X9CUce9xttIB0xOs5c6xf0TCWZo/y9lF6bA==", + "dev": true, + "requires": { + "nanocolors": "^0.2.2", + "nanoid": "^3.1.25", + "source-map-js": "^0.6.2" + } + }, + "prettier": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz", + "integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==", + "dev": true + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "rollup": { + "version": "2.57.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.57.0.tgz", + "integrity": "sha512-bKQIh1rWKofRee6mv8SrF2HdP6pea5QkwBZSMImJysFj39gQuiV8MEPBjXOCpzk3wSYp63M2v2wkWBmFC8O/rg==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "source-map-js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz", + "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==", + "dev": true + }, + "typescript": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", + "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==", + "dev": true + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + }, + "vite": { + "version": "2.5.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.5.10.tgz", + "integrity": "sha512-0ObiHTi5AHyXdJcvZ67HMsDgVpjT5RehvVKv6+Q0jFZ7zDI28PF5zK9mYz2avxdA+4iJMdwCz6wnGNnn4WX5Gg==", + "dev": true, + "requires": { + "esbuild": "^0.12.17", + "fsevents": "~2.3.2", + "postcss": "^8.3.6", + "resolve": "^1.20.0", + "rollup": "^2.38.5" + }, + "dependencies": { + "esbuild": { + "version": "0.12.29", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.29.tgz", + "integrity": "sha512-w/XuoBCSwepyiZtIRsKsetiLDUVGPVw1E/R3VTFSecIy8UR7Cq3SOtwKHJMFoVqqVG36aGkzh4e8BvpO1Fdc7g==", + "dev": true + } + } + } + } +} diff --git a/package.json b/package.json index e3c222d1..50b3d9f1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "howler", - "version": "2.2.3", + "version": "3.0.0@alpha.0", "description": "Javascript audio library for the modern web.", "homepage": "https://howlerjs.com", "keywords": [ @@ -22,20 +22,28 @@ "url": "git://github.com/goldfire/howler.js.git" }, "scripts": { - "build": "VERSION=`printf 'v' && node -e 'console.log(require(\"./package.json\").version)'` && sed -i '' '2s/.*/ * howler.js '\"$VERSION\"'/' src/howler.core.js && sed -i '' '4s/.*/ * howler.js '\"$VERSION\"'/' src/plugins/howler.spatial.js && uglifyjs --preamble \"/*! howler.js $VERSION | (c) 2013-2020, James Simpson of GoldFire Studios | MIT License | howlerjs.com */\" src/howler.core.js -c -m --screw-ie8 -o dist/howler.core.min.js && uglifyjs --preamble \"/*! howler.js $VERSION | Spatial Plugin | (c) 2013-2020, James Simpson of GoldFire Studios | MIT License | howlerjs.com */\" src/plugins/howler.spatial.js -c -m --screw-ie8 -o dist/howler.spatial.min.js && awk 'FNR==1{echo \"\"}1' dist/howler.core.min.js dist/howler.spatial.min.js | sed '3s~.*~/*! Spatial Plugin */~' | perl -pe 'chomp if eof' > dist/howler.min.js && awk '(NR>1 && FNR==1){printf (\"\\n\\n\")};1' src/howler.core.js src/plugins/howler.spatial.js > dist/howler.js", + "test": "vite", + "test:network": "vite --host", + "build": "node esbuild.js", "release": "VERSION=`printf 'v' && node -e 'console.log(require(\"./package.json\").version)'` && git tag $VERSION && git push && git push origin $VERSION && npm publish" }, "devDependencies": { - "uglify-js": "2.x" + "esbuild": "^0.13.2", + "prettier": "^2.4.1", + "typescript": "^4.4.3", + "vite": "^2.5.10" }, + "type": "module", "main": "dist/howler.js", "license": "MIT", "files": [ - "src", "dist/howler.js", "dist/howler.min.js", "dist/howler.core.min.js", "dist/howler.spatial.min.js", "LICENSE.md" - ] + ], + "dependencies": { + "fs-extra": "^10.0.0" + } } diff --git a/src/core.ts b/src/core.ts new file mode 100644 index 00000000..ae91fddc --- /dev/null +++ b/src/core.ts @@ -0,0 +1,13 @@ +/*! + * howler.js v2.2.3 + * howlerjs.com + * + * (c) 2013-2020, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */ +import Howler from './howler'; +import Howl from './howl'; + +export { Howler, Howl }; diff --git a/src/helpers.ts b/src/helpers.ts new file mode 100644 index 00000000..af388f8b --- /dev/null +++ b/src/helpers.ts @@ -0,0 +1,159 @@ +import Howl, { HowlXHROptions } from './howl'; +import Howler, { HowlerAudioContext } from './howler'; + +export const cache = {}; + +/** + * Buffer a sound from URL, Data URI or cache and decode to audio source (Web Audio API). + */ +export function loadBuffer(self: Howl) { + var url = self._src as string; + + // 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 < data.length; ++i) { + dataView[i] = data.charCodeAt(i); + } + + decodeAudioData(dataView.buffer, self); + } else { + // Load the buffer from the URL. + var xhr = new XMLHttpRequest(); + xhr.open((self._xhr as HowlXHROptions).method as string, url, true); + xhr.withCredentials = (self._xhr as HowlXHROptions) + .withCredentials as boolean; + xhr.responseType = 'arraybuffer'; + + // Apply any custom headers to the request. + if (self._xhr as HowlXHROptions) { + Object.keys(self._xhr as HowlXHROptions).forEach(function (key) { + xhr.setRequestHeader(key, (self._xhr as HowlXHROptions)[key]); + }); + } + + xhr.onload = () => { + // Make sure we get a successful response back. + var code = (xhr.status + '')[0]; + if (code !== '0' && code !== '2' && code !== '3') { + self._emit( + 'loaderror', + null, + 'Failed loading audio file with status: ' + xhr.status + '.', + ); + return; + } + + decodeAudioData(xhr.response, self); + }; + xhr.onerror = () => { + // If there is an error, switch to HTML5 Audio. + if (self._webAudio) { + self._html5 = true; + self._webAudio = false; + self._sounds = []; + delete cache[url]; + self.load(); + } + }; + safeXhrSend(xhr); + } +} + +/** + * Send the XHR request wrapped in a try/catch. + * @param xhr XHR to send. + */ +function safeXhrSend(xhr: XMLHttpRequest) { + try { + xhr.send(); + } catch (e) { + console.error('XHR Request failed: ', e); + } +} + +/** + * Decode audio data from an array buffer. + * @param arraybuffer The audio data. + * @param self + */ +function decodeAudioData(arraybuffer: ArrayBuffer, self: Howl) { + // Fire a load error if something broke. + function error() { + self._emit('loaderror', null, 'Decoding audio data failed.'); + } + + // Load the sound on success. + function success(buffer: AudioBuffer) { + if (buffer && self._sounds.length > 0) { + cache[self._src as string] = buffer; + loadSound(self, buffer); + } else { + error(); + } + } + + // Decode the buffer into an audio source. + if ( + typeof Promise !== 'undefined' && + (Howler.ctx as HowlerAudioContext).decodeAudioData.length === 1 + ) { + (Howler.ctx as HowlerAudioContext) + .decodeAudioData(arraybuffer) + .then(success) + .catch(error); + } else { + (Howler.ctx as HowlerAudioContext).decodeAudioData( + arraybuffer, + success, + error, + ); + } +} + +/** + * Sound is now loaded, so finish setting everything up and fire the loaded event. + * @param self + * @param buffer The decoded buffer sound source. + */ +function loadSound(self: Howl, buffer?: AudioBuffer) { + // 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(); + } +} + +// NOTE: Maybe remove these +export const isHTMLAudioElement = (node: any): node is HTMLAudioElement => + (node as HTMLAudioElement).playbackRate !== undefined; + +export const isGainNode = (node: any): node is GainNode => + (node as GainNode).connect !== undefined; + +export const isAudioBufferSourceNode = ( + node: any, +): node is AudioBufferSourceNode => node instanceof AudioBufferSourceNode; diff --git a/src/howl.ts b/src/howl.ts new file mode 100644 index 00000000..4cc631cc --- /dev/null +++ b/src/howl.ts @@ -0,0 +1,2041 @@ +import Howler, { HowlerAudioContext, HowlerAudioElement } from './howler'; +import { loadBuffer, cache } from './helpers'; +import Sound, { HowlGainNode } from './sound'; + +export type HowlCallback = (soundId: number) => void; +export type HowlErrorCallback = (soundId: number, error: unknown) => void; + +export interface SoundSpriteDefinitions { + [name: string]: [number, number] | [number, number, boolean]; +} + +export interface HowlXHROptions { + method?: string; + headers?: Record; + withCredentials?: boolean; +} + +export interface HowlListeners { + /** + * Fires when the sound has been stopped. The first parameter is the ID of the sound. + */ + onstop?: HowlCallback; + + /** + * Fires when the sound has been paused. The first parameter is the ID of the sound. + */ + onpause?: HowlCallback; + + /** + * Fires when the sound is loaded. + */ + onload?: HowlCallback; + + /** + * Fires when the sound has been muted/unmuted. The first parameter is the ID of the sound. + */ + onmute?: HowlCallback; + + /** + * Fires when the sound's volume has changed. The first parameter is the ID of the sound. + */ + onvolume?: HowlCallback; + + /** + * Fires when the sound's playback rate has changed. The first parameter is the ID of the sound. + */ + onrate?: HowlCallback; + + /** + * Fires when the sound has been seeked. The first parameter is the ID of the sound. + */ + onseek?: HowlCallback; + + /** + * Fires when the current sound finishes fading in/out. The first parameter is the ID of the sound. + */ + onfade?: HowlCallback; + + /** + * Fires when audio has been automatically unlocked through a touch/click event. + */ + onunlock?: HowlCallback; + + /** + * Fires when the sound finishes playing (if it is looping, it'll fire at the end of each loop). + * The first parameter is the ID of the sound. + */ + onend?: HowlCallback; + + /** + * Fires when the sound begins playing. The first parameter is the ID of the sound. + */ + onplay?: HowlCallback; + + /** + * Fires when the sound is unable to load. The first parameter is the ID of the sound (if it exists) and the second is the error message/code. + */ + onloaderror?: HowlErrorCallback; + + /** + * Fires when the sound is unable to play. The first parameter is the ID of the sound and the second is the error message/code. + */ + onplayerror?: HowlErrorCallback; +} + +export interface HowlOptions extends HowlListeners { + /** + * The sources to the track(s) to be loaded for the sound (URLs or base64 data URIs). 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 extension using the format property. + * + * @default `[]` + */ + src?: string | string[]; + + /** + * The volume of the specific track, from 0.0 to 1.0. + * + * @default `1.0` + */ + volume?: number; + + /** + * 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. + * + * @default `false` + */ + html5?: boolean; + + /** + * Set to true to automatically loop the sound forever. + * + * @default `false` + */ + loop?: boolean; + + /** + * Automatically begin downloading the audio file when the Howl is defined. If using HTML5 Audio, + * you can set this to 'metadata' to only preload the file's metadata (to get its duration without + * download the entire file, for example). + * + * @default `true` + */ + preload?: boolean | 'metadata'; + + /** + * Set to true to automatically start playback when sound is loaded. + * + * @default `false` + */ + autoplay?: boolean; + + /** + * Set to true to load the audio muted. + * + * @default `false` + */ + mute?: boolean; + + /** + * 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. An easy way to generate + * compatible sound sprites is with audiosprite. + * + * @default `{}` + */ + sprite?: SoundSpriteDefinitions; + + /** + * The rate of playback. 0.5 to 4.0, with 1.0 being normal speed. + * + * @default `1.0` + */ + rate?: number; + + /** + * The size of the inactive sounds pool. Once sounds are stopped or finish playing, they are marked + * as ended and ready for cleanup. We keep a pool of these to recycle for improved performance. + * Generally this doesn't need to be changed. It is important to keep in mind that when a sound is + * paused, it won't be removed from the pool and will still be considered active so that it can be + * resumed later. + * + * @default `5` + */ + pool?: number; + + /** + * howler.js automatically detects your file format from the extension, but you may also specify a + * format in situations where extraction won't work (such as with a SoundCloud stream). + * + * @default `[]` + */ + format?: string[]; + + /** + * When using Web Audio, howler.js uses an XHR request to load the audio files. If you need to send + * custom headers, set the HTTP method or enable withCredentials (see reference), include them with + * this parameter. Each is optional (method defaults to GET, headers default to undefined and + * withCredentials defaults to false). + */ + xhr?: HowlXHROptions; +} + +type HowlCallbacks = Array<{ fn: HowlCallback }>; +type HowlErrorCallbacks = Array<{ fn: HowlErrorCallback }>; + +type HowlEvent = + | 'play' + | 'end' + | 'pause' + | 'stop' + | 'mute' + | 'volume' + | 'rate' + | 'seek' + | 'fade' + | 'unlock' + | 'load' + | 'loaderror' + | 'playerror'; + +interface HowlEventHandler { + event: HowlEvent; + action: () => void; +} + +class Howl { + // User defined properties + _autoplay: boolean = false; + _format: string[] = []; + _html5: boolean = false; + _muted: boolean = false; + _loop: boolean = false; + _pool: number = 5; + _preload: boolean | 'metadata' = true; + _rate: number = 1; + _sprite: SoundSpriteDefinitions = {}; + _src: string | string[] = []; + _volume: number = 1; + _xhr: HowlOptions['xhr']; + + // Other default properties. + _duration = 0; + _state: 'unloaded' | 'loading' | 'loaded' = 'unloaded'; + _sounds: Sound[] = []; + _endTimers = {}; + _queue: HowlEventHandler[] = []; + _playLock = false; + + _onend: HowlCallbacks = []; + _onfade: HowlCallbacks = []; + _onload: HowlCallbacks = []; + _onloaderror: HowlErrorCallbacks = []; + _onplayerror: HowlErrorCallbacks = []; + _onpause: HowlCallbacks = []; + _onplay: HowlCallbacks = []; + _onstop: HowlCallbacks = []; + _onmute: HowlCallbacks = []; + _onvolume: HowlCallbacks = []; + _onrate: HowlCallbacks = []; + _onseek: HowlCallbacks = []; + _onunlock: HowlCallbacks = []; + _onresume: HowlCallbacks = []; + + // @ts-expect-error Not definitely assigned in constructor, likely due to using a module. + _webAudio: boolean; + + /** + * Create an audio group controller. + * @param o Passed in properties for this group. + */ + constructor(o: HowlOptions) { + // Throw an error if no source is provided. + if (!o.src || o.src.length === 0) { + console.error( + 'An array of source files must be passed with any new Howl.', + ); + return; + } + + // If we don't have an AudioContext created yet, run the setup. + if (!Howler.ctx) { + Howler._setupAudioContext(); + } + + // Setup user-defined default properties. + this._format = + o.format === undefined + ? [] + : typeof o.format !== 'string' + ? o.format + : [o.format]; + this._html5 = o.html5 || false; + this._muted = o.mute || false; + this._loop = o.loop || false; + this._pool = o.pool || 5; + + this._preload = + typeof o.preload === 'boolean' || o.preload === 'metadata' + ? o.preload + : true; + this._rate = o.rate || 1; + this._sprite = o.sprite || {}; + this._src = typeof o.src !== 'string' ? o.src : [o.src]; + this._volume = o.volume !== undefined ? o.volume : 1; + this._xhr = { + method: o.xhr && o.xhr.method ? o.xhr.method : 'GET', + headers: o.xhr && o.xhr.headers ? o.xhr.headers : undefined, + withCredentials: + o.xhr && o.xhr.withCredentials ? o.xhr.withCredentials : false, + }; + + // Setup event listeners. + this._onend = o.onend ? [{ fn: o.onend }] : []; + this._onfade = o.onfade ? [{ fn: o.onfade }] : []; + this._onload = o.onload ? [{ fn: o.onload }] : []; + this._onloaderror = o.onloaderror ? [{ fn: o.onloaderror }] : []; + this._onplayerror = o.onplayerror ? [{ fn: o.onplayerror }] : []; + this._onpause = o.onpause ? [{ fn: o.onpause }] : []; + this._onplay = o.onplay ? [{ fn: o.onplay }] : []; + this._onstop = o.onstop ? [{ fn: o.onstop }] : []; + this._onmute = o.onmute ? [{ fn: o.onmute }] : []; + this._onvolume = o.onvolume ? [{ fn: o.onvolume }] : []; + this._onrate = o.onrate ? [{ fn: o.onrate }] : []; + this._onseek = o.onseek ? [{ fn: o.onseek }] : []; + this._onunlock = o.onunlock ? [{ fn: o.onunlock }] : []; + this._onresume = []; + + // Web Audio or HTML5 Audio? + this._webAudio = Howler.usingWebAudio && !this._html5; + + // Automatically try to enable audio. + if (typeof Howler.ctx !== 'undefined' && Howler.ctx && Howler.autoUnlock) { + Howler._unlockAudio(); + } + + // Keep track of this Howl group in the global controller. + Howler._howls.push(this); + + // If they selected autoplay, add a play event to the load queue. + if (this._autoplay) { + this._queue.push({ + event: 'play', + action: () => { + this.play(); + }, + }); + } + + // Load the source file unless otherwise specified. + if (this._preload) { + this.load(); + } + } + + /** + * Load the audio file. + */ + load() { + var url: string | null = null; + + // If no audio is available, quit immediately. + if (Howler.noAudio) { + this._emit('loaderror', null, 'No audio support.'); + return this; + } + + // Make sure our source is in an array. + if (typeof this._src === 'string') { + this._src = [this._src]; + } + + // Loop through the sources and pick the first one that is compatible. + for (var i = 0; i < this._src.length; i++) { + var ext, str; + + if (this._format && this._format[i]) { + // If an extension was specified, use that instead. + ext = this._format[i]; + } else { + // Make sure the source is a string. + str = this._src[i]; + if (typeof str !== 'string') { + this._emit( + 'loaderror', + null, + 'Non-string found in selected audio sources - ignoring.', + ); + continue; + } + + // Extract the file extension from the URL or base64 data URI. + ext = /^data:audio\/([^;,]+);/i.exec(str); + if (!ext) { + ext = /\.([^.]+)$/.exec(str.split('?', 1)[0]); + } + + if (ext) { + ext = ext[1].toLowerCase(); + } + } + + // Log a warning if no extension was found. + if (!ext) { + console.warn( + 'No file extension was found. Consider using the "format" property or specify an extension.', + ); + } + + // Check if this extension is available. + if (ext && Howler.codecs(ext)) { + url = this._src[i]; + break; + } + } + + if (!url) { + this._emit( + 'loaderror', + null, + 'No codec support for selected audio sources.', + ); + return this; + } + + this._src = url; + this._state = 'loading'; + + // If the hosting page is HTTPS and the source isn't, + // drop down to HTML5 Audio to avoid Mixed Content errors. + if (window.location.protocol === 'https:' && url.slice(0, 5) === 'http:') { + this._html5 = true; + this._webAudio = false; + } + + // Create a new sound object and add it to the pool. + new Sound(this); + + // Load and decode the audio data for playback. + if (this._webAudio) { + loadBuffer(this); + } + + return this; + } + + /** + * Play a sound or resume previous playback. + * @param sprite Sprite name for sprite playback or sound id to continue previous. + * @param internal Internal Use: true prevents event firing. + * @return Sound ID. + */ + play(sprite?: string | number, internal?: boolean) { + var id: number | null = null; + + // Determine if a sprite, sound id or nothing was passed + if (typeof sprite === 'number') { + id = sprite; + // @ts-expect-error Not sure how to handle this with TypeScript. + sprite = null; + } else if ( + typeof sprite === 'string' && + this._state === 'loaded' && + !this._sprite[sprite] + ) { + // If the passed sprite doesn't exist, do nothing. + return null; + } else if (typeof sprite === 'undefined') { + // Use the default sound sprite (plays the full audio length). + sprite = '__default'; + + // Check if there is a single paused sound that isn't ended. + // If there is, play that sound. If not, continue as usual. + if (!this._playLock) { + var num = 0; + for (var i = 0; i < this._sounds.length; i++) { + if (this._sounds[i]._paused && !this._sounds[i]._ended) { + num++; + id = this._sounds[i]._id; + } + } + + if (num === 1) { + // @ts-expect-error Not sure how to handle this with TypeScript. + sprite = null; + } else { + id = null; + } + } + } + + // Get the selected node, or get one from the pool. + const sound = id ? this._soundById(id) : this._inactiveSound(); + + // If the sound doesn't exist, do nothing. + if (!sound) { + return null; + } + + // Select the sprite definition. + if (id && !sprite) { + sprite = sound._sprite || '__default'; + } + + // If the sound hasn't loaded, we must wait to get the audio's duration. + // We also need to wait to make sure we don't run into race conditions with + // the order of function calls. + if (this._state !== 'loaded') { + // Set the sprite value on this sound. + sound._sprite = sprite as string; + + // Mark this sound as not ended in case another sound is played before this one loads. + sound._ended = false; + + // Add the sound to the queue to be played on load. + var soundId = sound._id; + this._queue.push({ + event: 'play', + action: () => { + this.play(soundId); + }, + }); + + return soundId; + } + + // Don't play the sound if an id was passed and it is already playing. + if (id && !sound._paused) { + // Trigger the play event, in order to keep iterating through queue. + if (!internal) { + this._loadQueue('play'); + } + + return sound._id; + } + + // Make sure the AudioContext isn't suspended, and resume it if it is. + if (this._webAudio) { + Howler._autoResume(); + } + + // Determine how long to play for and where to start playing. + const seek = Math.max( + 0, + sound._seek > 0 ? sound._seek : this._sprite[sprite as string][0] / 1000, + ); + const duration = Math.max( + 0, + (this._sprite[sprite as string][0] + this._sprite[sprite as string][1]) / + 1000 - + seek, + ); + const timeout = (duration * 1000) / Math.abs(sound._rate); + const start = this._sprite[sprite as string][0] / 1000; + const stop = + (this._sprite[sprite as string][0] + this._sprite[sprite as string][1]) / + 1000; + sound._sprite = sprite as string; + + // 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. + const setParams = () => { + sound._paused = false; + sound._seek = seek; + sound._start = start; + sound._stop = stop; + sound._loop = !!(sound._loop || this._sprite[sprite as string][2]); + }; + + // End the sound instantly if seek is at the end. + if (seek >= stop) { + this._ended(sound); + return; + } + + // Begin the actual playback. + const node = sound._node; + if (this._webAudio) { + // Fire this when the sound is ready to play to begin Web Audio playback. + const playWebAudio = () => { + this._playLock = false; + setParams(); + this._refreshBuffer(sound); + + // Setup the playback params. + const vol = sound._muted || this._muted ? 0 : sound._volume; + (node as HowlGainNode).gain.setValueAtTime( + vol, + (Howler.ctx as HowlerAudioContext).currentTime, + ); + sound._playStart = (Howler.ctx as HowlerAudioContext).currentTime; + + // Play the sound using the supported method. + if ( + typeof ((node as HowlGainNode).bufferSource as AudioBufferSourceNode) + .start === 'undefined' + ) { + sound._loop + ? ((node as HowlGainNode).bufferSource as AudioBufferSourceNode) + // @ts-expect-error Support older browsers. + .noteGrainOn(0, seek, 86400) + : ((node as HowlGainNode).bufferSource as AudioBufferSourceNode) + // @ts-expect-error Support older browsers. + .noteGrainOn(0, seek, duration); + } else { + sound._loop + ? ( + (node as HowlGainNode).bufferSource as AudioBufferSourceNode + ).start(0, seek, 86400) + : ( + (node as HowlGainNode).bufferSource as AudioBufferSourceNode + ).start(0, seek, duration); + } + + // Start a new timer if none is present. + if (timeout !== Infinity) { + this._endTimers[sound._id] = setTimeout( + this._ended.bind(this, sound), + timeout, + ); + } + + if (!internal) { + setTimeout(() => { + this._emit('play', sound._id); + this._loadQueue(); + }, 0); + } + }; + + if ( + Howler.state === 'running' && + (Howler.ctx as HowlerAudioContext).state !== 'interrupted' + ) { + playWebAudio(); + } else { + this._playLock = true; + + // Wait for the audio context to resume before playing. + this.once('resume', playWebAudio); + + // Cancel the end timer. + this._clearTimer(sound._id); + } + } else { + // Fire this when the sound is ready to play to begin HTML5 Audio playback. + const playHtml5 = () => { + node.currentTime = seek; + (node as HTMLAudioElement).muted = + sound._muted || + this._muted || + Howler._muted || + (node as HTMLAudioElement).muted; + node.volume = sound._volume * (Howler.volume() as number); + (node as HTMLAudioElement).playbackRate = sound._rate; + + // Some browsers will throw an error if this is called without user interaction. + try { + const play = (node as HTMLAudioElement).play(); + + // Support older browsers that don't support promises, and thus don't have this issue. + if ( + play && + typeof Promise !== 'undefined' && + // @ts-expect-error + (play instanceof Promise || typeof play.then === 'function') + ) { + // Implements a lock to prevent DOMException: The play() request was interrupted by a call to pause(). + this._playLock = true; + + // Set param values immediately. + setParams(); + + // Releases the lock and executes queued actions. + play + .then(() => { + this._playLock = false; + (node as HowlerAudioElement)._unlocked = true; + if (!internal) { + this._emit('play', sound._id); + } else { + this._loadQueue(); + } + }) + .catch(() => { + this._playLock = false; + this._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) { + this._playLock = false; + setParams(); + this._emit('play', sound._id); + } + + // Setting rate before playing won't work in IE, so we set it again here. + (node as HTMLAudioElement).playbackRate = sound._rate; + + // If the node is still paused, then we can assume there was a playback issue. + if (node.paused) { + this._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) { + this._endTimers[sound._id] = setTimeout( + this._ended.bind(this, sound), + timeout, + ); + } else { + this._endTimers[sound._id] = () => { + // Fire ended on this audio node. + this._ended(sound); + + // Clear this listener. + node.removeEventListener( + 'ended', + this._endTimers[sound._id], + false, + ); + }; + node.addEventListener('ended', this._endTimers[sound._id], false); + } + } catch (err) { + this._emit('playerror', sound._id, err as any); + } + }; + + // If this is streaming audio, make sure the src is set and load again. + if ( + (node as HTMLAudioElement).src === + 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA' + ) { + (node as HTMLAudioElement).src = this._src as string; + (node as HTMLAudioElement).load(); + } + + // Play immediately if ready, or wait for the 'canplaythrough'event. + const loadedNoReadyState = + // @ts-expect-error Support old browsers + (window && window.ejecta) || + // @ts-expect-error Support old browsers + (!node.readyState && Howler._navigator.isCocoonJS); + if ((node as HTMLAudioElement).readyState >= 3 || loadedNoReadyState) { + playHtml5(); + } else { + this._playLock = true; + this._state = 'loading'; + + const listener = () => { + this._state = 'loaded'; + + // Begin playback. + playHtml5(); + + // Clear this listener. + node.removeEventListener(Howler._canPlayEvent, listener, false); + }; + node.addEventListener(Howler._canPlayEvent, listener, false); + + // Cancel the end timer. + this._clearTimer(sound._id); + } + } + + return sound._id; + } + + /** + * Pause playback and save current position. + * @param id The sound ID (empty to pause all in group). + * @param skipEmit If true, the `pause` event won't be emitted. + */ + pause(id: number, skipEmit?: boolean) { + // If the sound hasn't loaded or a play() promise is pending, add it to the load queue to pause when capable. + if (this._state !== 'loaded' || this._playLock) { + this._queue.push({ + event: 'pause', + action: () => { + this.pause(id); + }, + }); + + return this; + } + + // If no id is passed, get all ID's to be paused. + var ids = this._getSoundIds(id); + + for (var i = 0; i < ids.length; i++) { + // Clear the end timer. + this._clearTimer(ids[i]); + + // Get the sound. + var sound = this._soundById(ids[i]); + + if (sound && !sound._paused) { + // Reset the seek position. + sound._seek = this.seek(ids[i]) as number; + sound._rateSeek = 0; + sound._paused = true; + + // Stop currently running fades. + this._stopFade(ids[i]); + + if (sound._node) { + if (this._webAudio) { + // Make sure the sound has been created. + if (!(sound._node as HowlGainNode).bufferSource) { + continue; + } + + if ( + typeof ( + (sound._node as HowlGainNode) + .bufferSource as AudioBufferSourceNode + ).stop === 'undefined' + ) { + ( + (sound._node as HowlGainNode) + .bufferSource as AudioBufferSourceNode + ) + // @ts-expect-error Support older browsers. + .noteOff(0); + } else { + ( + (sound._node as HowlGainNode) + .bufferSource as AudioBufferSourceNode + ).stop(0); + } + + // Clean up the buffer source. + this._cleanBuffer(sound._node); + } else if ( + !isNaN((sound._node as HTMLAudioElement).duration) || + (sound._node as HTMLAudioElement).duration === Infinity + ) { + (sound._node as HTMLAudioElement).pause(); + } + } + } + + // Fire the pause event, unless skipEmit is `true` + if (!skipEmit) { + this._emit('pause', sound ? sound._id : null); + } + } + + return this; + } + + /** + * Stop playback and reset to start. + * @param id The sound ID (empty to stop all in group). + * @param internal Internal Use: true prevents event firing. + */ + stop(id?: number, internal?: boolean) { + // If the sound hasn't loaded, add it to the load queue to stop when capable. + if (this._state !== 'loaded' || this._playLock) { + this._queue.push({ + event: 'stop', + action: () => { + this.stop(id); + }, + }); + + return this; + } + + // If no id is passed, get all ID's to be stopped. + var ids = this._getSoundIds(id); + + for (var i = 0; i < ids.length; i++) { + // Clear the end timer. + this._clearTimer(ids[i]); + + // Get the sound. + var sound = this._soundById(ids[i]); + + if (sound) { + // Reset the seek position. + sound._seek = sound._start || 0; + sound._rateSeek = 0; + sound._paused = true; + sound._ended = true; + + // Stop currently running fades. + this._stopFade(ids[i]); + + if (sound._node) { + if (this._webAudio) { + // Make sure the sound's AudioBufferSourceNode has been created. + if ((sound._node as HowlGainNode).bufferSource) { + if ( + typeof ( + (sound._node as HowlGainNode) + .bufferSource as AudioBufferSourceNode + ).stop === 'undefined' + ) { + ( + (sound._node as HowlGainNode) + .bufferSource as AudioBufferSourceNode + ) + // @ts-expect-error Support older browsers + .noteOff(0); + } else { + ( + (sound._node as HowlGainNode) + .bufferSource as AudioBufferSourceNode + ).stop(0); + } + + // Clean up the buffer source. + this._cleanBuffer(sound._node); + } + } else if ( + !isNaN((sound._node as HTMLAudioElement).duration) || + (sound._node as HTMLAudioElement).duration === Infinity + ) { + sound._node.currentTime = sound._start || 0; + (sound._node as HTMLAudioElement).pause(); + + // If this is a live stream, stop download once the audio is stopped. + if ((sound._node as HTMLAudioElement).duration === Infinity) { + this._clearSound(sound._node); + } + } + } + + if (!internal) { + this._emit('stop', sound._id); + } + } + } + + return this; + } + + /** + * Mute/unmute a single sound or all sounds in this Howl group. + * @param muted Set to true to mute and false to unmute. + * @param id The sound ID to update (omit to mute/unmute all). + */ + mute(muted: boolean, id: number) { + // If the sound hasn't loaded, add it to the load queue to mute when capable. + if (this._state !== 'loaded' || this._playLock) { + this._queue.push({ + event: 'mute', + action: () => { + this.mute(muted, id); + }, + }); + + return this; + } + + // If applying mute/unmute to all sounds, update the group's value. + if (typeof id === 'undefined') { + if (typeof muted === 'boolean') { + this._muted = muted; + } else { + return this._muted; + } + } + + // If no id is passed, get all ID's to be muted. + var ids = this._getSoundIds(id); + + for (var i = 0; i < ids.length; i++) { + // Get the sound. + var sound = this._soundById(ids[i]); + + if (sound) { + sound._muted = muted; + + // Cancel active fade and set the volume to the end value. + if (sound._interval) { + this._stopFade(sound._id); + } + + if (this._webAudio && sound._node && Howler.ctx) { + (sound._node as HowlGainNode).gain.setValueAtTime( + muted ? 0 : sound._volume, + Howler.ctx.currentTime, + ); + } else if (sound._node) { + (sound._node as HTMLAudioElement).muted = Howler._muted + ? true + : muted; + } + + this._emit('mute', sound._id); + } + } + + return this; + } + + /** + * Get/set the volume of this sound or of the Howl group. This method can optionally take 0, 1 or 2 arguments. + * volume() -> 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 Returns this or current volume. + */ + volume(...args) { + let vol, id; + + // Determine the values based on arguments. + if (args.length === 0) { + // Return the value of the groups' volume. + return this._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 = this._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. + let sound: Sound | null; + 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 (this._state !== 'loaded' || this._playLock) { + this._queue.push({ + event: 'volume', + action: () => { + this.volume.apply(this, args); + }, + }); + + return this; + } + + // Set the group volume. + if (typeof id === 'undefined') { + this._volume = vol; + } + + // Update one or all volumes. + id = this._getSoundIds(id); + for (var i = 0; i < id.length; i++) { + // Get the sound. + sound = this._soundById(id[i]); + + if (sound) { + sound._volume = vol; + + // Stop currently running fades. + if (!args[2]) { + this._stopFade(id[i]); + } + + if (this._webAudio && sound._node && !sound._muted && Howler.ctx) { + (sound._node as HowlGainNode).gain.setValueAtTime( + vol, + Howler.ctx.currentTime, + ); + } else if (sound._node && !sound._muted) { + sound._node.volume = vol * (Howler.volume() as number); + } + + this._emit('volume', sound._id); + } + } + } else { + sound = id ? this._soundById(id) : this._sounds[0]; + return sound ? sound._volume : 0; + } + + return this; + } + + /** + * Fade a currently playing sound between two volumes (if no id is passed, all sounds will fade). + * @param from The value to fade from (0.0 to 1.0). + * @param to The volume to fade to (0.0 to 1.0). + * @param len Time in milliseconds to fade. + * @param id The sound id (omit to fade all sounds). + */ + fade( + from: number | string, + to: number | string, + len: number | string, + id: number, + ) { + // If the sound hasn't loaded, add it to the load queue to fade when capable. + if (this._state !== 'loaded' || this._playLock) { + this._queue.push({ + event: 'fade', + action: () => { + this.fade(from, to, len, id); + }, + }); + + return this; + } + + // Make sure the to/from/len values are numbers. + from = Math.min(Math.max(0, parseFloat(from as string)), 1); + to = Math.min(Math.max(0, parseFloat(to as string)), 1); + len = parseFloat(len as string); + + // Set the volume to the start position. + this.volume(from, id); + + // Fade the volume of one or all sounds. + var ids = this._getSoundIds(id); + for (var i = 0; i < ids.length; i++) { + // Get the sound. + var sound = this._soundById(ids[i]); + + // Create a linear fade or fall back to timeouts with HTML5 Audio. + if (sound) { + // Stop the previous fade if no sprite is being used (otherwise, volume handles this). + if (!id) { + this._stopFade(ids[i]); + } + + // If we are using Web Audio, let the native methods do the actual fade. + if (this._webAudio && !sound._muted && Howler.ctx) { + var currentTime = Howler.ctx.currentTime; + var end = currentTime + len / 1000; + sound._volume = from; + (sound._node as HowlGainNode).gain.setValueAtTime(from, currentTime); + (sound._node as HowlGainNode).gain.linearRampToValueAtTime(to, end); + } + + this._startFadeInterval( + sound, + from, + to, + len, + ids[i], + typeof id === 'undefined', + ); + } + } + + return this; + } + + /** + * Starts the internal interval to fade a sound. + * @param sound Reference to sound to fade. + * @param from The value to fade from (0.0 to 1.0). + * @param to The volume to fade to (0.0 to 1.0). + * @param len Time in milliseconds to fade. + * @param id The sound id to fade. + * @param isGroup If true, set the volume on the group. + */ + _startFadeInterval( + sound: Sound, + from: number, + to: number, + len: number, + id: number, + isGroup: boolean, + ) { + var vol = from; + var diff = to - from; + var steps = Math.abs(diff / 0.01); + var stepLen = Math.max(4, steps > 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(() => { + // 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 (this._webAudio) { + sound._volume = vol; + } else { + this.volume(vol, sound._id, true); + } + + // Set the group's volume. + if (isGroup) { + this._volume = vol; + } + + // When the fade is complete, stop it and fire event. + if ((to < from && vol <= to) || (to > from && vol >= to)) { + if (typeof sound._interval === 'number') { + clearInterval(sound._interval); + } + sound._interval = null; + sound._fadeTo = null; + this.volume(to, sound._id); + this._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(id) { + var sound = this._soundById(id); + + if (sound && sound._interval) { + if (this._webAudio && Howler.ctx) { + (sound._node as HowlGainNode).gain.cancelScheduledValues( + Howler.ctx.currentTime, + ); + } + + clearInterval(sound._interval); + sound._interval = null; + this.volume(sound._fadeTo, id); + sound._fadeTo = null; + this._emit('fade', id); + } + + return this; + } + + /** + * 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 Returns this or current loop value. + */ + loop(...args) { + let loop, id, sound; + + // Determine the values for loop and id. + if (args.length === 0) { + // Return the grou's loop value. + return this._loop; + } else if (args.length === 1) { + if (typeof args[0] === 'boolean') { + loop = args[0]; + this._loop = loop; + } else { + // Return this sound's loop value. + sound = this._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 = this._getSoundIds(id); + for (var i = 0; i < ids.length; i++) { + sound = this._soundById(ids[i]); + + if (sound) { + sound._loop = loop; + if (this._webAudio && sound._node && sound._node.bufferSource) { + sound._node.bufferSource.loop = loop; + if (loop) { + sound._node.bufferSource.loopStart = sound._start || 0; + sound._node.bufferSource.loopEnd = sound._stop; + + // If playing, restart playback to ensure looping updates. + if (this.playing(ids[i])) { + this.pause(ids[i], true); + this.play(ids[i], true); + } + } + } + } + } + + return this; + } + + /** + * Get/set the playback rate of a sound. This method can optionally take 0, 1 or 2 arguments. + * rate() -> 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 Returns this or the current playback rate. + */ + rate(...args) { + let rate, id; + + // Determine the values based on arguments. + if (args.length === 0) { + // We will simply return the current rate of the first node. + id = this._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 = this._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. + let sound; + if (typeof rate === 'number') { + // If the sound hasn't loaded, add it to the load queue to change playback rate when capable. + if (this._state !== 'loaded' || this._playLock) { + this._queue.push({ + event: 'rate', + action: () => { + this.rate.apply(this, args); + }, + }); + + return this; + } + + // Set the group rate. + if (typeof id === 'undefined') { + this._rate = rate; + } + + // Update one or all volumes. + id = this._getSoundIds(id); + for (var i = 0; i < id.length; i++) { + // Get the sound. + sound = this._soundById(id[i]); + + if (sound && Howler.ctx) { + // Keep track of our position when the rate changed and update the playback + // start position so we can properly adjust the seek position for time elapsed. + if (this.playing(id[i])) { + sound._rateSeek = this.seek(id[i]); + sound._playStart = this._webAudio + ? Howler.ctx.currentTime + : sound._playStart; + } + sound._rate = rate; + + // Change the playback rate. + if (this._webAudio && sound._node && sound._node.bufferSource) { + sound._node.bufferSource.playbackRate.setValueAtTime( + rate, + Howler.ctx.currentTime, + ); + } else if (sound._node) { + sound._node.playbackRate = rate; + } + + // Reset the timers. + const seek = this.seek(id[i]) as number; + const duration = + (this._sprite[sound._sprite][0] + this._sprite[sound._sprite][1]) / + 1000 - + seek; + const timeout = (duration * 1000) / Math.abs(sound._rate); + + // Start a new end timer if sound is already playing. + if (this._endTimers[id[i]] || !sound._paused) { + this._clearTimer(id[i]); + this._endTimers[id[i]] = setTimeout( + this._ended.bind(this, sound), + timeout, + ); + } + + this._emit('rate', sound._id); + } + } + } else { + sound = this._soundById(id); + return sound ? sound._rate : this._rate; + } + + return this; + } + + /** + * Get/set the seek position of a sound. This method can optionally take 0, 1 or 2 arguments. + * seek() -> 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 Returns this or the current seek position. + */ + seek(...args) { + let seek, id; + + // Determine the values based on arguments. + if (args.length === 0) { + // We will simply return the current position of the first node. + if (this._sounds.length) { + id = this._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 = this._getSoundIds(); + var index = ids.indexOf(args[0]); + if (index >= 0) { + id = parseInt(args[0], 10); + } else if (this._sounds.length) { + id = this._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' && + (this._state !== 'loaded' || this._playLock) + ) { + this._queue.push({ + event: 'seek', + action: () => { + this.seek.apply(this, args); + }, + }); + + return this; + } + + // Get the sound. + var sound = this._soundById(id); + + if (sound) { + if (typeof seek === 'number' && seek >= 0) { + // Pause the sound and update position for restarting playback. + var playing = this.playing(id); + if (playing) { + this.pause(id, true); + } + + // Move the position of the track and cancel timer. + sound._seek = seek; + sound._ended = false; + this._clearTimer(id); + + // Update the seek position for HTML5 Audio. + if ( + !this._webAudio && + sound._node && + !isNaN((sound._node as HTMLAudioElement).duration) + ) { + sound._node.currentTime = seek; + } + + // Seek and emit when ready. + const seekAndEmit = () => { + // Restart the playback if the sound was playing. + if (playing) { + this.play(id, true); + } + + this._emit('seek', id); + }; + + // Wait for the play lock to be unset before emitting (HTML5 Audio). + if (playing && !this._webAudio) { + const emitSeek = () => { + if (!this._playLock) { + seekAndEmit(); + } else { + setTimeout(emitSeek, 0); + } + }; + setTimeout(emitSeek, 0); + } else { + seekAndEmit(); + } + } else { + if (this._webAudio && Howler.ctx) { + const realTime = this.playing(id) + ? Howler.ctx.currentTime - sound._playStart + : 0; + const rateSeek = sound._rateSeek ? sound._rateSeek - sound._seek : 0; + return sound._seek + (rateSeek + realTime * Math.abs(sound._rate)); + } else { + return sound._node.currentTime; + } + } + } + + return this; + } + + /** + * 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 id The sound id to check. If none is passed, the whole sound group is checked. + * @return True if playing and false if not. + */ + playing(id: number) { + // Check the passed sound ID (if any). + if (typeof id === 'number') { + var sound = this._soundById(id); + return sound ? !sound._paused : false; + } + + // Otherwise, loop through all sounds and check if any are playing. + for (var i = 0; i < this._sounds.length; i++) { + if (!this._sounds[i]._paused) { + return true; + } + } + + return false; + } + + /** + * Get the duration of this sound. Passing a sound id will return the sprite duration. + * @param id The sound id to check. If none is passed, return full source duration. + * @return Audio duration in seconds. + */ + duration(id: number) { + var duration = this._duration; + + // If we pass an ID, get the sound and return the sprite length. + var sound = this._soundById(id); + if (sound) { + duration = this._sprite[sound._sprite][1] / 1000; + } + + return duration; + } + + /** + * Returns the current loaded state of this Howl. + * @return 'unloaded', 'loading', 'loaded' + */ + state() { + return this._state; + } + + /** + * Unload and destroy the current Howl object. + * This will immediately stop all sound instances attached to this group. + */ + unload() { + // Stop playing any active sounds. + var sounds = this._sounds; + for (let i = 0; i < sounds.length; i++) { + // Stop the sound if it is currently playing. + if (!sounds[i]._paused) { + this.stop(sounds[i]._id); + } + + // Remove the source or disconnect. + if (!this._webAudio) { + // Set the source to 0-second silence to stop any downloading (except in IE). + this._clearSound(sounds[i]._node); + + // Remove any event listeners. + sounds[i]._node.removeEventListener('error', sounds[i]._errorFn, false); + sounds[i]._node.removeEventListener( + Howler._canPlayEvent, + sounds[i]._loadFn, + false, + ); + sounds[i]._node.removeEventListener('ended', sounds[i]._endFn, false); + + // Release the Audio object back to the pool. + Howler._releaseHtml5Audio(sounds[i]._node as HowlerAudioElement); + } + + // Empty out all of the nodes. + // @ts-expect-error Disable type checking to avoid dynamic edge case. + delete sounds[i]._node; + + // Make sure all timers are cleared out. + this._clearTimer(sounds[i]._id); + } + + // Remove the references in the global Howler object. + var index = Howler._howls.indexOf(this); + if (index >= 0) { + Howler._howls.splice(index, 1); + } + + // Delete this sound from the cache (if no other Howl is using it). + var remCache = true; + for (let i = 0; i < Howler._howls.length; i++) { + if ( + Howler._howls[i]._src === this._src || + this._src.indexOf(Howler._howls[i]._src as string) >= 0 + ) { + remCache = false; + break; + } + } + + if (cache && remCache) { + delete cache[this._src as string]; + } + + // Clear global errors. + Howler.noAudio = false; + + // Clear out `this`. + this._state = 'unloaded'; + this._sounds = []; + // NOTE: This is operation is not allowed in modern TS + JS. Don't know how to replace it though. + // this = 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. + */ + on(event: string, fn: Function, id?: number, once?: number) { + var events = this['_on' + event]; + + if (typeof fn === 'function') { + events.push(once ? { id: id, fn: fn, once: once } : { id: id, fn: fn }); + } + + return this; + } + + /** + * 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(event, fn, id) { + var events = this['_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 < events.length; i++) { + var isId = id === events[i].id; + if ((fn === events[i].fn && isId) || (!fn && isId)) { + events.splice(i, 1); + break; + } + } + } else if (event) { + // Clear out all events of this type. + this['_on' + event] = []; + } else { + // Clear out all events of every type. + var keys = Object.keys(this); + for (i = 0; i < keys.length; i++) { + if (keys[i].indexOf('_on') === 0 && Array.isArray(this[keys[i]])) { + this[keys[i]] = []; + } + } + } + + return this; + } + + /** + * Listen to a custom event and remove it once fired. + * @param event Event name. + * @param fn Listener to call. + * @param id (optional) Only listen to events for this sound. + */ + once(event: string, fn: Function, id?: number) { + // Setup the event listener. + this.on(event, fn, id, 1); + + return this; + } + + /** + * Emit all events of a specific type and pass the sound id. + * @param event Event name. + * @param id Sound ID. + * @param msg Message to go with event. + */ + _emit(event: string, id?: number | null, msg?: string | number) { + var events = this['_on' + event]; + + // Loop through event store and fire all functions. + for (var i = events.length - 1; i >= 0; i--) { + // Only fire the listener if the correct ID is used. + if (!events[i].id || events[i].id === id || event === 'load') { + setTimeout( + ((fn) => { + fn.call(this, id, msg); + }).bind(this, events[i].fn), + 0, + ); + + // If this event was setup with `once`, remove it. + if (events[i].once) { + this.off(event, events[i].fn, events[i].id); + } + } + } + + // Pass the event type into load queue so that it can continue stepping. + this._loadQueue(event); + + return this; + } + + /** + * 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). + */ + _loadQueue(event?: string) { + if (this._queue.length > 0) { + var task = this._queue[0]; + + // Remove this task if a matching event was passed. + if (task.event === event) { + this._queue.shift(); + this._loadQueue(); + } + + // Run the task if no event type is passed. + if (!event) { + task.action(); + } + } + + return this; + } + + /** + * Fired when playback ends at the end of the duration. + * @param sound The sound object to work with. + */ + _ended(sound: Sound) { + 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 ( + !this._webAudio && + sound._node && + !sound._node.paused && + !(sound._node as HowlerAudioElement).ended && + sound._node.currentTime < sound._stop + ) { + setTimeout(this._ended.bind(this, sound), 100); + return this; + } + + // Should this sound loop? + var loop = !!(sound._loop || this._sprite[sprite][2]); + + // Fire the ended event. + this._emit('end', sound._id); + + // Restart the playback for HTML5 Audio loop. + if (!this._webAudio && loop) { + this.stop(sound._id, true).play(sound._id); + } + + // Restart this timer if on a Web Audio loop. + if (this._webAudio && loop && Howler.ctx) { + this._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); + this._endTimers[sound._id] = setTimeout( + this._ended.bind(this, sound), + timeout, + ); + } + + // Mark the node as paused. + if (this._webAudio && !loop) { + sound._paused = true; + sound._ended = true; + sound._seek = sound._start || 0; + sound._rateSeek = 0; + this._clearTimer(sound._id); + + // Clean up the buffer source. + this._cleanBuffer(sound._node); + + // Attempt to auto-suspend AudioContext if no sounds are still playing. + Howler._autoSuspend(); + } + + // When using a sprite, end the track. + if (!this._webAudio && !loop) { + this.stop(sound._id, true); + } + + return this; + } + + /** + * Clear the end timer for a sound playback. + * @param {Number} id The sound ID. + */ + _clearTimer(id: number) { + if (this._endTimers[id]) { + // Clear the timeout or remove the ended listener. + if (typeof this._endTimers[id] !== 'function') { + clearTimeout(this._endTimers[id]); + } else { + var sound = this._soundById(id); + if (sound && sound._node) { + sound._node.removeEventListener('ended', this._endTimers[id], false); + } + } + + delete this._endTimers[id]; + } + + return this; + } + /** + * Return the sound identified by this ID, or return null. + * @param id Sound ID + * @return Sound object or null. + */ + _soundById(id: number) { + // Loop through all sounds and find the one with this ID. + for (var i = 0; i < this._sounds.length; i++) { + if (id === this._sounds[i]._id) { + return this._sounds[i]; + } + } + + return null; + } + + /** + * Return an inactive sound from the pool or create a new one. + * @return Sound playback object. + */ + _inactiveSound() { + this._drain(); + + // Find the first inactive node to recycle. + for (let i = 0; i < this._sounds.length; i++) { + if (this._sounds[i]._ended) { + return this._sounds[i].reset(); + } + } + + // If no inactive node was found, create a new one. + return new Sound(this); + } + + /** + * Drain excess inactive sounds from the pool. + */ + _drain() { + const limit = this._pool; + let cnt = 0; + let i = 0; + + // If there are less sounds than the max pool size, we are done. + if (this._sounds.length < limit) { + return; + } + + // Count the number of inactive sounds. + for (i = 0; i < this._sounds.length; i++) { + if (this._sounds[i]._ended) { + cnt++; + } + } + + // Remove excess inactive sounds, going in reverse order. + for (i = this._sounds.length - 1; i >= 0; i--) { + if (cnt <= limit) { + return; + } + + if (this._sounds[i]._ended) { + // Disconnect the audio source when using Web Audio. + if (this._webAudio && this._sounds[i]._node) { + (this._sounds[i]._node as HowlGainNode).disconnect(0); + } + + // Remove sounds until we have the pool size. + this._sounds.splice(i, 1); + cnt--; + } + } + } + + /** + * Get all ID's from the sounds pool. + * @param id Only return one ID if one is passed. + * @return Array of IDs. + */ + _getSoundIds(id?: number) { + if (typeof id === 'undefined') { + var ids: number[] = []; + for (var i = 0; i < this._sounds.length; i++) { + ids.push(this._sounds[i]._id); + } + + return ids; + } else { + return [id]; + } + } + + /** + * Load the sound back into the buffer source. + * @param sound The sound object to work with. + */ + _refreshBuffer(sound: Sound) { + // Setup the buffer source for playback. + (sound._node as HowlGainNode).bufferSource = ( + Howler.ctx as HowlerAudioContext + ).createBufferSource(); + ( + (sound._node as HowlGainNode).bufferSource as AudioBufferSourceNode + ).buffer = cache[this._src as string]; + + // Connect to the correct node. + if (sound._panner) { + ( + (sound._node as HowlGainNode).bufferSource as AudioBufferSourceNode + ).connect(sound._panner); + } else { + ( + (sound._node as HowlGainNode).bufferSource as AudioBufferSourceNode + ).connect(sound._node as HowlGainNode); + } + + // Setup looping and playback rate. + ((sound._node as HowlGainNode).bufferSource as AudioBufferSourceNode).loop = + sound._loop; + if (sound._loop) { + ( + (sound._node as HowlGainNode).bufferSource as AudioBufferSourceNode + ).loopStart = sound._start || 0; + ( + (sound._node as HowlGainNode).bufferSource as AudioBufferSourceNode + ).loopEnd = sound._stop || 0; + } + ( + (sound._node as HowlGainNode).bufferSource as AudioBufferSourceNode + ).playbackRate.setValueAtTime( + sound._rate, + (Howler.ctx as HowlerAudioContext).currentTime, + ); + + return this; + } + + /** + * Prevent memory leaks by cleaning up the buffer source after playback. + * @param {Object} node Sound's audio node containing the buffer source. + * @return {Howl} + */ + _cleanBuffer(node: Sound['_node']) { + var isIOS = + Howler._navigator && Howler._navigator.vendor.indexOf('Apple') >= 0; + + if (Howler._scratchBuffer && (node as HowlGainNode).bufferSource) { + ((node as HowlGainNode).bufferSource as AudioBufferSourceNode).onended = + null; + ((node as HowlGainNode).bufferSource as AudioBufferSourceNode).disconnect( + 0, + ); + if (isIOS) { + try { + ( + (node as HowlGainNode).bufferSource as AudioBufferSourceNode + ).buffer = Howler._scratchBuffer; + } catch (e) {} + } + } + (node as HowlGainNode).bufferSource = null; + + return this; + } + + /** + * Set the source to a 0-second silence to stop any downloading (except in IE). + * @param {Object} node Audio node to clear. + */ + _clearSound(node) { + var checkIE = /MSIE |Trident\//.test( + Howler._navigator && Howler._navigator.userAgent, + ); + if (!checkIE) { + node.src = + 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA'; + } + } +} + +export default Howl; diff --git a/src/howler.ts b/src/howler.ts new file mode 100644 index 00000000..c9c6fa67 --- /dev/null +++ b/src/howler.ts @@ -0,0 +1,661 @@ +import Howl from './howl'; + +export interface HowlerAudioElement extends HTMLAudioElement { + _unlocked: boolean; +} + +type HowlerAudioContextState = + | AudioContextState + | 'suspending' + | 'closed' + | 'interrupted'; + +export type HowlerAudioContext = Omit & { + // In iOS Safari, the state can also be set to 'interrupted' + // https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/state#resuming_interrupted_play_states_in_ios_safari + state: AudioContextState | 'interrupted'; +}; + +class Howler { + // Public properties. + masterGain: GainNode | null = null; + noAudio = false; + usingWebAudio = true; + autoSuspend = true; + ctx: HowlerAudioContext | null = null; + + // Set to false to disable the auto audio unlocker. + autoUnlock = true; + + // Create a global ID counter. + _counter: number = 1000; + + // Pool of unlocked HTML5 Audio objects. + _html5AudioPool: Array = []; + html5PoolSize: number = 10; + + // Internal properties + _codecs = {}; + _howls: Array = []; + _muted = false; + _volume = 1; + _canPlayEvent = 'canplaythrough'; + _navigator = window.navigator; + _audioUnlocked: boolean = false; + _mobileUnloaded: boolean = false; + // IDEA: Maybe rename to `_state` to indicate that this is an internal property? + state: HowlerAudioContextState = 'suspended'; + _suspendTimer: number | null = null; + _resumeAfterSuspend?: boolean; + + _scratchBuffer: any; + + /** + * Create the global controller. All contained methods and properties apply + * to all sounds that are currently playing or will be in the future. + */ + constructor() { + // Setup the various state values for global tracking. + this._setup(); + } + + /** + * Get/set the global volume for all sounds. + * @param vol Volume from 0.0 to 1.0. + * @return Returns self or current volume. + */ + volume(vol?: number | string) { + const volume = parseFloat(vol as string); + + // If we don't have an AudioContext created yet, run the setup. + if (!this.ctx) { + this._setupAudioContext(); + } + + if (typeof volume !== 'undefined' && volume >= 0 && volume <= 1) { + this._volume = volume; + + // Don't update any of the nodes if we are muted. + if (this._muted) { + return this; + } + + // When using Web Audio, we just need to adjust the master gain. + if (this.usingWebAudio && this.masterGain && this.ctx) { + this.masterGain.gain.setValueAtTime(volume, this.ctx.currentTime); + } + + // Loop through and change volume for all HTML5 audio nodes. + for (var i = 0; i < this._howls.length; i++) { + if (!this._howls[i]._webAudio) { + // Get all of the sounds in this Howl group. + var ids = this._howls[i]._getSoundIds(); + + // Loop through all sounds and change the volumes. + for (var j = 0; j < ids.length; j++) { + var sound = this._howls[i]._soundById(ids[j]); + + if (sound && sound._node) { + sound._node.volume = sound._volume * volume; + } + } + } + } + + return volume; + } + + return this._volume; + } + + /** + * Handle muting and unmuting globally. + * @param muted Is muted or not. + */ + mute(muted: boolean) { + // If we don't have an AudioContext created yet, run the setup. + if (!this.ctx) { + this._setupAudioContext(); + } + + this._muted = muted; + + // With Web Audio, we just need to mute the master gain. + if (this.usingWebAudio && this.masterGain && this.ctx) { + this.masterGain.gain.setValueAtTime( + muted ? 0 : this._volume, + this.ctx.currentTime, + ); + } + + // Loop through and mute all HTML5 Audio nodes. + for (var i = 0; i < this._howls.length; i++) { + if (!this._howls[i]._webAudio) { + // Get all of the sounds in this Howl group. + var ids = this._howls[i]._getSoundIds(); + + // Loop through all sounds and mark the audio node as muted. + for (var j = 0; j < ids.length; j++) { + var sound = this._howls[i]._soundById(ids[j]); + + if (sound && sound._node) { + (sound._node as HowlerAudioElement).muted = muted + ? true + : sound._muted; + } + } + } + } + + return this; + } + + /** + * Handle stopping all sounds globally. + */ + stop() { + // Loop through all Howls and stop them. + for (var i = 0; i < this._howls.length; i++) { + this._howls[i].stop(); + } + + return this; + } + + /** + * Unload and destroy all currently loaded Howl objects. + */ + unload() { + for (var i = this._howls.length - 1; i >= 0; i--) { + this._howls[i].unload(); + } + + // Create a new AudioContext to make sure it is fully reset. + if ( + this.usingWebAudio && + this.ctx && + typeof this.ctx.close !== 'undefined' + ) { + this.ctx.close(); + this.ctx = null; + this._setupAudioContext(); + } + + return this; + } + + /** + * Check for codec support of specific extension. + * @param ext Audio file extention. + */ + codecs(ext: string) { + return this._codecs[ext.replace(/^x-/, '')]; + } + + /** + * Setup various state values for global tracking. + */ + _setup() { + // Keeps track of the suspend/resume state of the AudioContext. + this.state = this.ctx ? this.ctx.state || 'suspended' : 'suspended'; + + // Automatically begin the 30-second suspend process + this._autoSuspend(); + + // Check if audio is available. + if (!this.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') { + this._canPlayEvent = 'canplay'; + } + } catch (e) { + this.noAudio = true; + } + } else { + this.noAudio = true; + } + } + + // Test to make sure audio isn't disabled in Internet Explorer. + try { + var test = new Audio(); + if (test.muted) { + this.noAudio = true; + } + } catch (e) {} + + // Check for supported codecs. + if (!this.noAudio) { + this._setupCodecs(); + } + + return this; + } + + /** + * Setup the audio context when available, or switch to HTML5 Audio mode. + */ + _setupAudioContext() { + // If we have already detected that Web Audio isn't supported, don't run this step again. + if (!this.usingWebAudio) { + return; + } + + // Check if we are using Web Audio and setup the AudioContext if we are. + try { + if (typeof AudioContext !== 'undefined') { + this.ctx = new AudioContext(); + // @ts-expect-error Safari backwards compatibility + } else if (typeof webkitAudioContext !== 'undefined') { + // @ts-expect-error Safari backwards compatibility + this.ctx = new webkitAudioContext(); + } else { + this.usingWebAudio = false; + } + } catch (e) { + this.usingWebAudio = false; + } + + // If the audio context creation still failed, set using web audio to false. + if (!this.ctx) { + this.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( + this._navigator && this._navigator.platform, + ); + var appVersion = + this._navigator && + this._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/); + var version = appVersion ? parseInt(appVersion[1], 10) : null; + if (iOS && version && version < 9) { + var safari = /safari/.test( + this._navigator && this._navigator.userAgent.toLowerCase(), + ); + if (this._navigator && !safari) { + this.usingWebAudio = false; + } + } + + // Create and expose the master GainNode when using Web Audio (useful for plugins or advanced usage). + if (this.usingWebAudio) { + this.masterGain = + typeof (this.ctx as HowlerAudioContext).createGain === 'undefined' + ? // @ts-expect-error Support old browsers + (this.ctx as HowlerAudioContext).createGainNode() + : (this.ctx as HowlerAudioContext).createGain(); + (this.masterGain as GainNode).gain.setValueAtTime( + this._muted ? 0 : this._volume, + (this.ctx as HowlerAudioContext).currentTime, + ); + (this.masterGain as GainNode).connect( + (this.ctx as HowlerAudioContext).destination, + ); + } + + // Re-run the setup on Howler. + this._setup(); + } + + /** + * Check for browser support for various codecs and cache the results. + */ + _setupCodecs() { + let audioTest: HTMLAudioElement | null = 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 this; + } + + if (!audioTest || typeof audioTest.canPlayType !== 'function') { + return this; + } + + const mpegTest = audioTest.canPlayType('audio/mpeg;').replace(/^no$/, ''); + + // Opera version <33 has mixed MP3 support, so we need to check for and block it. + const ua = this._navigator ? this._navigator.userAgent : ''; + const checkOpera = ua.match(/OPR\/([0-6].)/g); + const isOldOpera = + checkOpera && parseInt(checkOpera[0].split('/')[1], 10) < 33; + const checkSafari = + ua.indexOf('Safari') !== -1 && ua.indexOf('Chrome') === -1; + const safariVersion = ua.match(/Version\/(.*?) /); + const isOldSafari = + checkSafari && safariVersion && parseInt(safariVersion[1], 10) < 15; + + this._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 this; + } + + /** + * 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/ + */ + _unlockAudio() { + // Only run this if Web Audio is supported and it hasn't already been unlocked. + if (this._audioUnlocked || !this.ctx) { + return this; + } + + this.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 (!this._mobileUnloaded && this.ctx.sampleRate !== 44100) { + this._mobileUnloaded = true; + this.unload(); + } + + // Scratch buffer for enabling iOS to dispose of web audio buffers correctly, as per: + // http://stackoverflow.com/questions/24119684 + this._scratchBuffer = this.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. + const unlock = () => { + // 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 (this._html5AudioPool.length < this.html5PoolSize) { + try { + var audioNode = new Audio() as HowlerAudioElement; + + // 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. + this._releaseHtml5Audio(audioNode); + } catch (e) { + this.noAudio = true; + break; + } + } + + // Loop through any assigned audio nodes and unlock them. + for (var i = 0; i < this._howls.length; i++) { + if (!this._howls[i]._webAudio) { + // Get all of the sounds in this Howl group. + var ids = this._howls[i]._getSoundIds(); + + // Loop through all sounds and unlock the audio nodes. + for (var j = 0; j < ids.length; j++) { + var sound = this._howls[i]._soundById(ids[j]); + + if ( + sound && + sound._node && + !(sound._node as HowlerAudioElement)._unlocked + ) { + (sound._node as HowlerAudioElement)._unlocked = true; + (sound._node as HTMLAudioElement).load(); + } + } + } + } + + // Fix Android can not play in suspend state. + this._autoResume(); + + // Create an empty buffer. + const source = (this.ctx as HowlerAudioContext).createBufferSource(); + source.buffer = this._scratchBuffer; + source.connect((this.ctx as HowlerAudioContext).destination); + + // Play the empty buffer. + if (typeof source.start === 'undefined') { + // @ts-expect-error .noteOn() only exists in old browsers. + source.noteOn(0); + } else { + source.start(0); + } + + // Calling resume() on a stack initiated by user gesture is what actually unlocks the audio on Android Chrome >= 55. + if (this.ctx && typeof this.ctx.resume === 'function') { + this.ctx.resume(); + } + + // Setup a timeout to check that we are unlocked on the next event loop. + source.onended = () => { + source.disconnect(0); + + // Update the unlocked state and prevent this check from happening again. + this._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 < this._howls.length; i++) { + this._howls[i]._emit('unlock'); + } + }; + }; + + // Setup a touch start listener to attempt an unlock in. + document.addEventListener('touchstart', unlock, true); + document.addEventListener('touchend', unlock, true); + document.addEventListener('click', unlock, true); + document.addEventListener('keydown', unlock, true); + + return this; + } + + /** + * Get an unlocked HTML5 Audio object from the pool. If none are left, + * return a new Audio object and throw a warning. + * @return HTML5 Audio object. + */ + _obtainHtml5Audio() { + // Return the next object from the pool if one exists. + if (this._html5AudioPool.length) { + return this._html5AudioPool.pop(); + } + + // Check if the audio is locked and throw a warning. + var testPlay = new Audio().play(); + if ( + testPlay && + typeof Promise !== 'undefined' && + // @ts-expect-error + (testPlay instanceof Promise || typeof testPlay.then === 'function') + ) { + testPlay.catch(function () { + console.warn( + 'HTML5 Audio pool exhausted, returning potentially locked audio object.', + ); + }); + } + + return new Audio(); + } + + /** + * Return an activated HTML5 Audio object to the pool. + */ + _releaseHtml5Audio(audio: HowlerAudioElement) { + // Don't add audio to the pool if we don't know if it has been unlocked. + if (audio._unlocked) { + this._html5AudioPool.push(audio); + } + + return this; + } + + /** + * Automatically suspend the Web Audio AudioContext after no sound has played for 30 seconds. + * This saves processing/energy and fixes various browser-specific bugs with audio getting stuck. + */ + _autoSuspend() { + if ( + !this.autoSuspend || + !this.ctx || + typeof this.ctx.suspend === 'undefined' || + !this.usingWebAudio + ) { + return; + } + + // Check if any sounds are playing. + for (var i = 0; i < this._howls.length; i++) { + if (this._howls[i]._webAudio) { + for (var j = 0; j < this._howls[i]._sounds.length; j++) { + if (!this._howls[i]._sounds[j]._paused) { + return this; + } + } + } + } + + if (this._suspendTimer) { + clearTimeout(this._suspendTimer); + } + + // If no sound has played after 30 seconds, suspend the context. + this._suspendTimer = setTimeout(() => { + if (!this.autoSuspend) { + return; + } + + this._suspendTimer = null; + this.state = 'suspending'; + + // Handle updating the state of the audio context after suspending. + const handleSuspension = () => { + this.state = 'suspended'; + + if (this._resumeAfterSuspend) { + delete this._resumeAfterSuspend; + this._autoResume(); + } + }; + + // Either the state gets suspended or it is interrupted. + // Either way, we need to update the state to suspended. + (this.ctx as HowlerAudioContext) + .suspend() + .then(handleSuspension, handleSuspension); + }, 30000); + + return this; + } + + /** + * Automatically resume the Web Audio AudioContext when a new sound is played. + */ + _autoResume() { + if ( + !this.ctx || + typeof this.ctx.resume === 'undefined' || + !this.usingWebAudio + ) { + return; + } + + if ( + this.state === 'running' && + this.ctx.state !== 'interrupted' && + this._suspendTimer + ) { + clearTimeout(this._suspendTimer); + this._suspendTimer = null; + } else if ( + this.state === 'suspended' || + (this.state === 'running' && this.ctx.state === 'interrupted') + ) { + this.ctx.resume().then(() => { + this.state = 'running'; + + // Emit to all Howls that the audio has resumed. + for (var i = 0; i < this._howls.length; i++) { + this._howls[i]._emit('resume'); + } + }); + + if (this._suspendTimer) { + clearTimeout(this._suspendTimer); + this._suspendTimer = null; + } + } else if (this.state === 'suspending') { + this._resumeAfterSuspend = true; + } + + return this; + } +} + +const HowlerSingleton = new Howler(); + +export default HowlerSingleton; diff --git a/src/sound.ts b/src/sound.ts new file mode 100644 index 00000000..39d604e3 --- /dev/null +++ b/src/sound.ts @@ -0,0 +1,207 @@ +import Howler, { HowlerAudioElement } from './howler'; +import Howl from './howl'; + +export interface HowlGainNode extends GainNode { + bufferSource: AudioBufferSourceNode | null; + paused: boolean; + volume: number; + currentTime: number; +} + +class Sound { + _parent: Howl; + _muted: boolean; + _loop: boolean; + _volume: number; + _rate: number; + _seek: number = 0; + _paused: boolean = true; + _ended: boolean = true; + _sprite: string = '__default'; + _id: number; + + _node: HowlGainNode | HowlerAudioElement; + _errorFn: EventListener = () => {}; + _loadFn: EventListener = () => {}; + _endFn: EventListener = () => {}; + // TODO: Add better type when adding the spatial audio plugin. + _panner?: AudioParam; + + _rateSeek?: number; + _playStart: number = 0; + _start: number = 0; + _stop: number = 0; + _fadeTo: number | null = null; + _interval: number | null = null; + + /** + * Setup the sound object, which each node attached to a Howl group is contained in. + * @param howl The Howl parent group. + */ + constructor(howl: Howl) { + this._parent = howl; + + // Setup the default parameters. + this._muted = Boolean(howl._muted); + this._loop = Boolean(howl._loop); + this._volume = howl._volume; + this._rate = howl._rate; + + // Generate a unique ID for this sound. + this._id = ++Howler._counter; + + // Add itself to the parent's pool. + this._parent._sounds.push(this); + + if (this._parent._webAudio && Howler.ctx) { + // Create the gain node for controlling volume (the source will connect to this). + this._node = ( + typeof Howler.ctx.createGain === 'undefined' + ? // @ts-expect-error Support old browsers + Howler.ctx.createGainNode() + : Howler.ctx.createGain() + ) as HowlGainNode; + } else { + // Get an unlocked Audio object from the pool. + this._node = Howler._obtainHtml5Audio() as HowlerAudioElement; + } + + // Create the new node. + this.create(); + } + + /** + * Create and setup a new sound object, whether HTML5 Audio or Web Audio. + */ + create() { + var parent = this._parent; + var volume = + Howler._muted || this._muted || this._parent._muted ? 0 : this._volume; + + if (parent._webAudio && Howler.ctx) { + (this._node as HowlGainNode).gain.setValueAtTime( + volume, + Howler.ctx.currentTime, + ); + (this._node as HowlGainNode).paused = true; + (this._node as HowlGainNode).connect(Howler.masterGain as GainNode); + } else if (!Howler.noAudio) { + // Listen for errors (http://dev.w3.org/html5/spec-author-view/spec.html#mediaerror). + this._errorFn = this._errorListener.bind(this); + this._node.addEventListener('error', this._errorFn, false); + + // Listen for 'canplaythrough' event to let us know the sound is ready. + this._loadFn = this._loadListener.bind(this); + this._node.addEventListener(Howler._canPlayEvent, this._loadFn, false); + + // Listen for the 'ended' event on the sound to account for edge-case where + // a finite sound has a duration of Infinity. + this._endFn = this._endListener.bind(this); + this._node.addEventListener('ended', this._endFn, false); + + // Setup the new audio node. + (this._node as HTMLAudioElement).src = parent._src as string; + (this._node as HTMLAudioElement).preload = + parent._preload === true ? 'auto' : (parent._preload as string); + this._node.volume = volume * (Howler.volume() as number); + + // Begin loading the source. + (this._node as HTMLAudioElement).load(); + } + + return this; + } + + /** + * Reset the parameters of this sound to the original state (for recycle). + */ + reset() { + var parent = this._parent; + + // Reset all of the parameters of this sound. + this._muted = parent._muted; + this._loop = parent._loop; + this._volume = parent._volume; + this._rate = parent._rate; + this._seek = 0; + this._rateSeek = 0; + this._paused = true; + this._ended = true; + this._sprite = '__default'; + + // Generate a new ID so that it isn't confused with the previous sound. + this._id = ++Howler._counter; + + return this; + } + + /** + * HTML5 Audio error listener callback. + */ + _errorListener() { + // Fire an error event and pass back the code. + this._parent._emit( + 'loaderror', + this._id, + (this._node as HTMLAudioElement).error instanceof MediaError + ? ((this._node as HTMLAudioElement).error as MediaError).code + : 0, + ); + + // Clear the event listener. + this._node.removeEventListener('error', this._errorFn, false); + } + + /** + * HTML5 Audio canplaythrough listener callback. + */ + _loadListener() { + const parent = this._parent; + + // Round up the duration to account for the lower precision in HTML5 Audio. + parent._duration = + Math.ceil((this._node as HTMLAudioElement).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. + this._node.removeEventListener(Howler._canPlayEvent, this._loadFn, false); + } + + /** + * HTML5 Audio ended listener callback. + */ + _endListener() { + const parent = this._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((this._node as HTMLAudioElement).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(this); + } + + // Clear the event listener since the duration is now correct. + this._node.removeEventListener('ended', this._endFn, false); + } +} + +export default Sound; diff --git a/tests/core.html5audio.html b/tests/core.html5audio.html index ecf3ae67..b4debe6c 100644 --- a/tests/core.html5audio.html +++ b/tests/core.html5audio.html @@ -1,17 +1,16 @@ - + - - - Howler.js Core HTML5 Audio Tests - - - -
-
- - -
- - - - \ No newline at end of file + + + Howler.js Core HTML5 Audio Tests + + + +
+
+ + +
+ + + diff --git a/tests/core.webaudio.html b/tests/core.webaudio.html index 7c16bae6..c6c6d258 100644 --- a/tests/core.webaudio.html +++ b/tests/core.webaudio.html @@ -1,16 +1,15 @@ - + - - - Howler.js Core Web Audio Tests - - - -
-
- -
- - - - \ No newline at end of file + + + Howler.js Core Web Audio Tests + + + +
+
+ +
+ + + diff --git a/tests/css/styles.css b/tests/css/styles.css index 501b568e..de1af0a5 100644 --- a/tests/css/styles.css +++ b/tests/css/styles.css @@ -1,6 +1,6 @@ html { width: 100%; - height: 100%; + height: 100%; overflow: hidden; padding: 0; margin: 0; @@ -36,7 +36,7 @@ body { height: 100%; text-align: center; } -.button{ +.button { background: #a2998c; border: none; outline: 0; @@ -93,4 +93,4 @@ body { .button { padding: 5px; } -} \ No newline at end of file +} diff --git a/tests/index.html b/tests/index.html index a30b0201..f47a8225 100644 --- a/tests/index.html +++ b/tests/index.html @@ -1,29 +1,29 @@ - + - - - Howler.js Tests - - - -
- - - -
+ + + Howler.js Tests + + + +
+ + + +
- - - \ No newline at end of file + document.getElementById('spatial').onclick = function () { + window.location = 'spatial.html'; + }; + + + diff --git a/tests/js/core.html5audio.js b/tests/js/core.html5audio.js index cd546ef7..3c63e68a 100644 --- a/tests/js/core.html5audio.js +++ b/tests/js/core.html5audio.js @@ -1,14 +1,16 @@ +import { Howl } from '../../dist/core'; + // Cache the label for later use. -var label = document.getElementById('label'); -var start = document.getElementById('start'); +const label = document.getElementById('label'); +const start = document.getElementById('start'); // Setup the sounds to be used. -var sound1 = new Howl({ +const sound1 = new Howl({ src: ['audio/sound1.webm', 'audio/sound1.mp3'], - html5: true + html5: true, }); -var sound2 = new Howl({ +const sound2 = new Howl({ src: ['audio/sound2.webm', 'audio/sound2.mp3'], html5: true, sprite: { @@ -17,235 +19,238 @@ var sound2 = new Howl({ three: [4000, 350], four: [6000, 380], five: [8000, 340], - beat: [10000, 11163] - } + beat: [10000, 11163], + }, }); // Enable the start button when the sounds have loaded. -sound1.once('load', function() { +sound1.once('load', () => { start.removeAttribute('disabled'); start.innerHTML = 'BEGIN CORE TESTS'; }); // Define the tests to run. -var id; -var tests = [ - function(fn) { +let id; +const tests = [ + (fn) => { id = sound1.play(); label.innerHTML = 'PLAYING'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.pause(id); label.innerHTML = 'PAUSED'; setTimeout(fn, 1500); }, - function(fn) { + (fn) => { sound1.play(id); label.innerHTML = 'RESUMING'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.stop(id); label.innerHTML = 'STOPPED'; setTimeout(fn, 1500); }, - function(fn) { + (fn) => { sound1.play(id); label.innerHTML = 'PLAY FROM START'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.rate(1.5, id); label.innerHTML = 'SPEED UP'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.rate(1, id); label.innerHTML = 'SLOW DOWN'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.fade(1, 0, 2000, id); label.innerHTML = 'FADE OUT'; - sound1.once('fade', function() { + sound1.once('fade', () => { fn(); }); }, - function(fn) { + (fn) => { sound1.fade(0, 1, 2000, id); label.innerHTML = 'FADE IN'; - sound1.once('fade', function() { + sound1.once('fade', () => { fn(); }); }, - function(fn) { + (fn) => { sound1.mute(true, id); label.innerHTML = 'MUTE'; setTimeout(fn, 1500); }, - function(fn) { + (fn) => { sound1.mute(false, id); label.innerHTML = 'UNMUTE'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.volume(0.5, id); label.innerHTML = 'HALF VOLUME'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.volume(1, id); label.innerHTML = 'FULL VOLUME'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.seek(0, id); label.innerHTML = 'SEEK TO START'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { id = sound1.play(); label.innerHTML = 'PLAY 2ND'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.mute(true); label.innerHTML = 'MUTE GROUP'; setTimeout(fn, 1500); }, - function(fn) { + (fn) => { sound1.mute(false); label.innerHTML = 'UNMUTE GROUP'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.volume(0.5); label.innerHTML = 'HALF VOLUME GROUP'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.fade(0.5, 0, 2000); label.innerHTML = 'FADE OUT GROUP'; - sound1.once('fade', function() { + sound1.once('fade', () => { if (sound1._onfade.length === 0) { fn(); } }); }, - function(fn) { + (fn) => { sound1.fade(0, 1, 2000); label.innerHTML = 'FADE IN GROUP'; - sound1.once('fade', function() { + sound1.once('fade', () => { if (sound1._onfade.length === 0) { fn(); } }); }, - function(fn) { + (fn) => { sound1.stop(); label.innerHTML = 'STOP GROUP'; setTimeout(fn, 1500); }, - function(fn) { + (fn) => { id = sound2.play('beat'); label.innerHTML = 'PLAY SPRITE'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound2.pause(id); label.innerHTML = 'PAUSE SPRITE'; setTimeout(fn, 1000); }, - function(fn) { + (fn) => { sound2.play(id); label.innerHTML = 'RESUME SPRITE'; setTimeout(fn, 1500); }, - function(fn) { - var sounds = ['one', 'two', 'three', 'four', 'five']; - for (var i=0; i { + const sounds = ['one', 'two', 'three', 'four', 'five']; + for (let i = 0; i < sounds.length; i++) { + setTimeout( + ((i) => { + sound2.play(sounds[i]); + }).bind(null, i), + i * 500, + ); } label.innerHTML = 'MULTIPLE SPRITES'; setTimeout(fn, 3000); }, - function(fn) { - var sprite = sound2.play('one'); + (fn) => { + const sprite = sound2.play('one'); sound2.loop(true, sprite); label.innerHTML = 'LOOP SPRITE'; - setTimeout(function() { + setTimeout(() => { sound2.loop(false, sprite); fn(); }, 3000); }, - function(fn) { + (fn) => { sound2.fade(1, 0, 2000, id); label.innerHTML = 'FADE OUT SPRITE'; - sound2.once('fade', function() { + sound2.once('fade', () => { fn(); }); - } + }, ]; // Create a method that will call the next in the series. -var chain = function(i) { - return function() { +const chain = (i) => { + return () => { if (tests[i]) { tests[i](chain(++i)); } else { @@ -254,7 +259,7 @@ var chain = function(i) { label.style.color = '#74b074'; // Wait for 5 seconds and then go back to the tests index. - setTimeout(function() { + setTimeout(() => { window.location = './'; }, 5000); } @@ -262,7 +267,11 @@ var chain = function(i) { }; // Listen to a click on the button to being the tests. -start.addEventListener('click', function() { - tests[0](chain(1)); - start.style.display = 'none'; -}, false); \ No newline at end of file +start.addEventListener( + 'click', + () => { + tests[0](chain(1)); + start.style.display = 'none'; + }, + false, +); diff --git a/tests/js/core.webaudio.js b/tests/js/core.webaudio.js index f6690310..e7a2e959 100644 --- a/tests/js/core.webaudio.js +++ b/tests/js/core.webaudio.js @@ -1,13 +1,15 @@ +import { Howl, Howler } from '../../dist/core.js'; + // Cache the label for later use. -var label = document.getElementById('label'); -var start = document.getElementById('start'); +const label = document.getElementById('label'); +const start = document.getElementById('start'); // Setup the sounds to be used. -var sound1 = new Howl({ - src: ['audio/sound1.webm', 'audio/sound1.mp3'] +const sound1 = new Howl({ + src: ['audio/sound1.webm', 'audio/sound1.mp3'], }); -var sound2 = new Howl({ +const sound2 = new Howl({ src: ['audio/sound2.webm', 'audio/sound2.mp3'], sprite: { one: [0, 450], @@ -15,237 +17,248 @@ var sound2 = new Howl({ three: [4000, 350], four: [6000, 380], five: [8000, 340], - beat: [10000, 11163] - } + beat: [10000, 11163], + }, }); // Enable the start button when the sounds have loaded. -sound1.once('load', function() { +sound1.once('load', () => { start.removeAttribute('disabled'); start.innerHTML = 'BEGIN CORE TESTS'; }); // Define the tests to run. -var id; -var tests = [ - function(fn) { - sound1.once('play', function() { +let id; +const tests = [ + (fn) => { + sound1.once('play', () => { label.innerHTML = 'PLAYING'; setTimeout(fn, 2000); }); - + id = sound1.play(); }, - function(fn) { + (fn) => { sound1.pause(id); label.innerHTML = 'PAUSED'; setTimeout(fn, 1500); }, - function(fn) { + (fn) => { sound1.play(id); label.innerHTML = 'RESUMING'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.stop(id); label.innerHTML = 'STOPPED'; setTimeout(fn, 1500); }, - function(fn) { + (fn) => { sound1.play(id); label.innerHTML = 'PLAY FROM START'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.rate(1.5, id); label.innerHTML = 'SPEED UP'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.rate(1, id); label.innerHTML = 'SLOW DOWN'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.fade(1, 0, 2000, id); label.innerHTML = 'FADE OUT'; - sound1.once('fade', function() { - fn(); - }, id); + sound1.once( + 'fade', + () => { + fn(); + }, + id, + ); }, - function(fn) { + (fn) => { sound1.fade(0, 1, 2000, id); label.innerHTML = 'FADE IN'; - sound1.once('fade', function() { - fn(); - }, id); + sound1.once( + 'fade', + () => { + fn(); + }, + id, + ); }, - function(fn) { + (fn) => { sound1.mute(true, id); label.innerHTML = 'MUTE'; setTimeout(fn, 1500); }, - function(fn) { + (fn) => { sound1.mute(false, id); label.innerHTML = 'UNMUTE'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.volume(0.5, id); label.innerHTML = 'HALF VOLUME'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.volume(1, id); label.innerHTML = 'FULL VOLUME'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.seek(0, id); label.innerHTML = 'SEEK TO START'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { id = sound1.play(); label.innerHTML = 'PLAY 2ND'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.mute(true); label.innerHTML = 'MUTE GROUP'; setTimeout(fn, 1500); }, - function(fn) { + (fn) => { sound1.mute(false); label.innerHTML = 'UNMUTE GROUP'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.volume(0.5); label.innerHTML = 'HALF VOLUME GROUP'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.fade(0.5, 0, 2000); label.innerHTML = 'FADE OUT GROUP'; - sound1.once('fade', function() { + sound1.once('fade', () => { if (sound1._onfade.length === 0) { fn(); } }); }, - function(fn) { + (fn) => { sound1.fade(0, 1, 2000); label.innerHTML = 'FADE IN GROUP'; - sound1.once('fade', function() { + sound1.once('fade', () => { if (sound1._onfade.length === 0) { fn(); } }); }, - function(fn) { + (fn) => { sound1.stop(); label.innerHTML = 'STOP GROUP'; setTimeout(fn, 1500); }, - function(fn) { + (fn) => { id = sound2.play('beat'); label.innerHTML = 'PLAY SPRITE'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound2.pause(id); label.innerHTML = 'PAUSE SPRITE'; setTimeout(fn, 1000); }, - function(fn) { + (fn) => { sound2.play(id); label.innerHTML = 'RESUME SPRITE'; setTimeout(fn, 1500); }, - function(fn) { - var sounds = ['one', 'two', 'three', 'four', 'five']; - for (var i=0; i { + const sounds = ['one', 'two', 'three', 'four', 'five']; + for (let i = 0; i < sounds.length; i++) { + setTimeout( + ((i) => { + sound2.play(sounds[i]); + }).bind(null, i), + i * 500, + ); } label.innerHTML = 'MULTIPLE SPRITES'; setTimeout(fn, 3000); }, - function(fn) { - var sprite = sound2.play('one'); + (fn) => { + const sprite = sound2.play('one'); sound2.loop(true, sprite); label.innerHTML = 'LOOP SPRITE'; - setTimeout(function() { + setTimeout(() => { sound2.loop(false, sprite); fn(); }, 3000); }, - function(fn) { + (fn) => { sound2.fade(1, 0, 2000, id); label.innerHTML = 'FADE OUT SPRITE'; - sound2.once('fade', function() { + sound2.once('fade', () => { fn(); }); - } + }, ]; // Create a method that will call the next in the series. -var chain = function(i) { - return function() { +const chain = (i) => { + return () => { if (tests[i]) { tests[i](chain(++i)); } else { @@ -253,7 +266,7 @@ var chain = function(i) { label.style.color = '#74b074'; // Wait for 5 seconds and then go back to the tests index. - setTimeout(function() { + setTimeout(() => { window.location = './'; }, 5000); } @@ -263,10 +276,14 @@ var chain = function(i) { // If Web Audio isn't available, send them to hTML5 test. if (Howler.usingWebAudio) { // Listen to a click on the button to being the tests. - start.addEventListener('click', function() { - tests[0](chain(1)); - start.style.display = 'none'; - }, false); + start.addEventListener( + 'click', + () => { + tests[0](chain(1)); + start.style.display = 'none'; + }, + false, + ); } else { window.location = 'core.html5audio.html'; -} \ No newline at end of file +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..ab52595b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "declaration": true, + "target": "es2018", + "lib": ["es2018", "dom"], + "strict": true, + "noImplicitAny": false, + "esModuleInterop": true, + "allowJs": true, + "moduleResolution": "node", + "outDir": "dist", + "module": "esnext", + "resolveJsonModule": true, + "emitDeclarationOnly": true + }, + "include": ["src/*.ts"] +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 00000000..38c20e5a --- /dev/null +++ b/vite.config.js @@ -0,0 +1,5 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + root: "tests", +});