From d4181408457b3f6b408a95731303a4b9a6e1e593 Mon Sep 17 00:00:00 2001 From: eatsjobs Date: Sat, 22 Nov 2025 02:49:12 +0100 Subject: [PATCH 01/25] commit gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..04c01ba7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ \ No newline at end of file From c6ceae80c5d03077c63b696a2209ccc8dc1d606f Mon Sep 17 00:00:00 2001 From: eatsjobs Date: Sat, 22 Nov 2025 02:49:41 +0100 Subject: [PATCH 02/25] remove dist --- bower.json | 35 - dist/howler.core.min.js | 2 - dist/howler.js | 3248 ------------------------------------ dist/howler.min.js | 4 - dist/howler.spatial.min.js | 2 - 5 files changed, 3291 deletions(-) delete mode 100644 bower.json delete mode 100644 dist/howler.core.min.js delete mode 100644 dist/howler.js delete mode 100644 dist/howler.min.js delete mode 100644 dist/howler.spatial.min.js 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/howler.core.min.js b/dist/howler.core.min.js deleted file mode 100644 index 3901ac4f..00000000 --- a/dist/howler.core.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! howler.js v2.2.4 | (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\/(\d+)/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(!e.bufferSource)return o;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.js b/dist/howler.js deleted file mode 100644 index bb5a463e..00000000 --- a/dist/howler.js +++ /dev/null @@ -1,3248 +0,0 @@ -/*! - * howler.js v2.2.4 - * 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\/(\d+)/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 (!node.bufferSource) { - return self; - } - - if (Howler._scratchBuffer && node.bufferSource) { - node.bufferSource.onended = null; - node.bufferSource.disconnect(0); - if (isIOS) { - try { node.bufferSource.buffer = Howler._scratchBuffer; } catch(e) {} - } - } - node.bufferSource = null; - - return self; - }, - - /** - * Set the source to a 0-second silence to stop any downloading (except in IE). - * @param {Object} node Audio node to clear. - */ - _clearSound: function(node) { - var checkIE = /MSIE |Trident\//.test(Howler._navigator && Howler._navigator.userAgent); - if (!checkIE) { - node.src = 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA'; - } - } - }; - - /** Single Sound Methods **/ - /***************************************************************************/ - - /** - * Setup the sound object, which each node attached to a Howl group is contained in. - * @param {Object} howl The Howl parent group. - */ - var Sound = function(howl) { - this._parent = howl; - this.init(); - }; - Sound.prototype = { - /** - * Initialize a new Sound object. - * @return {Sound} - */ - init: function() { - var self = this; - var parent = self._parent; - - // Setup the default parameters. - self._muted = parent._muted; - self._loop = parent._loop; - self._volume = parent._volume; - self._rate = parent._rate; - self._seek = 0; - self._paused = true; - self._ended = true; - self._sprite = '__default'; - - // Generate a unique ID for this sound. - self._id = ++Howler._counter; - - // Add itself to the parent's pool. - parent._sounds.push(self); - - // Create the new node. - self.create(); - - return self; - }, - - /** - * Create and setup a new sound object, whether HTML5 Audio or Web Audio. - * @return {Sound} - */ - create: function() { - var self = this; - var parent = self._parent; - var volume = (Howler._muted || self._muted || self._parent._muted) ? 0 : self._volume; - - if (parent._webAudio) { - // Create the gain node for controlling volume (the source will connect to this). - self._node = (typeof Howler.ctx.createGain === 'undefined') ? Howler.ctx.createGainNode() : Howler.ctx.createGain(); - self._node.gain.setValueAtTime(volume, Howler.ctx.currentTime); - self._node.paused = true; - self._node.connect(Howler.masterGain); - } else if (!Howler.noAudio) { - // Get an unlocked Audio object from the pool. - self._node = Howler._obtainHtml5Audio(); - - // Listen for errors (http://dev.w3.org/html5/spec-author-view/spec.html#mediaerror). - self._errorFn = self._errorListener.bind(self); - self._node.addEventListener('error', self._errorFn, false); - - // Listen for 'canplaythrough' event to let us know the sound is ready. - self._loadFn = self._loadListener.bind(self); - self._node.addEventListener(Howler._canPlayEvent, self._loadFn, false); - - // Listen for the 'ended' event on the sound to account for edge-case where - // a finite sound has a duration of Infinity. - self._endFn = self._endListener.bind(self); - self._node.addEventListener('ended', self._endFn, false); - - // Setup the new audio node. - self._node.src = parent._src; - self._node.preload = parent._preload === true ? 'auto' : parent._preload; - self._node.volume = volume * Howler.volume(); - - // Begin loading the source. - self._node.load(); - } - - return self; - }, - - /** - * Reset the parameters of this sound to the original state (for recycle). - * @return {Sound} - */ - reset: function() { - var self = this; - var parent = self._parent; - - // Reset all of the parameters of this sound. - self._muted = parent._muted; - self._loop = parent._loop; - self._volume = parent._volume; - self._rate = parent._rate; - self._seek = 0; - self._rateSeek = 0; - self._paused = true; - self._ended = true; - self._sprite = '__default'; - - // Generate a new ID so that it isn't confused with the previous sound. - self._id = ++Howler._counter; - - return self; - }, - - /** - * HTML5 Audio error listener callback. - */ - _errorListener: function() { - var self = this; - - // Fire an error event and pass back the code. - self._parent._emit('loaderror', self._id, self._node.error ? self._node.error.code : 0); - - // Clear the event listener. - self._node.removeEventListener('error', self._errorFn, false); - }, - - /** - * HTML5 Audio canplaythrough listener callback. - */ - _loadListener: function() { - var self = this; - var parent = self._parent; - - // Round up the duration to account for the lower precision in HTML5 Audio. - parent._duration = Math.ceil(self._node.duration * 10) / 10; - - // Setup a sprite if none is defined. - if (Object.keys(parent._sprite).length === 0) { - parent._sprite = {__default: [0, parent._duration * 1000]}; - } - - if (parent._state !== 'loaded') { - parent._state = 'loaded'; - parent._emit('load'); - parent._loadQueue(); - } - - // Clear the event listener. - self._node.removeEventListener(Howler._canPlayEvent, self._loadFn, false); - }, - - /** - * HTML5 Audio ended listener callback. - */ - _endListener: function() { - var self = this; - var parent = self._parent; - - // Only handle the `ended`` event if the duration is Infinity. - if (parent._duration === Infinity) { - // Update the parent duration to match the real audio duration. - // Round up the duration to account for the lower precision in HTML5 Audio. - parent._duration = Math.ceil(self._node.duration * 10) / 10; - - // Update the sprite that corresponds to the real duration. - if (parent._sprite.__default[1] === Infinity) { - parent._sprite.__default[1] = parent._duration * 1000; - } - - // Run the regular ended method. - parent._ended(self); - } - - // Clear the event listener since the duration is now correct. - self._node.removeEventListener('ended', self._endFn, false); - } - }; - - /** Helper Methods **/ - /***************************************************************************/ - - var cache = {}; - - /** - * Buffer a sound from URL, Data URI or cache and decode to audio source (Web Audio API). - * @param {Howl} self - */ - var loadBuffer = function(self) { - var url = self._src; - - // Check if the buffer has already been cached and use it instead. - if (cache[url]) { - // Set the duration from the cache. - self._duration = cache[url].duration; - - // Load the sound into this Howl. - loadSound(self); - - return; - } - - if (/^data:[^;]+;base64,/.test(url)) { - // Decode the base64 data URI without XHR, since some browsers don't support it. - var data = atob(url.split(',')[1]); - var dataView = new Uint8Array(data.length); - for (var i=0; i 0) { - cache[self._src] = buffer; - loadSound(self, buffer); - } else { - error(); - } - }; - - // Decode the buffer into an audio source. - if (typeof Promise !== 'undefined' && Howler.ctx.decodeAudioData.length === 1) { - Howler.ctx.decodeAudioData(arraybuffer).then(success).catch(error); - } else { - Howler.ctx.decodeAudioData(arraybuffer, success, error); - } - } - - /** - * Sound is now loaded, so finish setting everything up and fire the loaded event. - * @param {Howl} self - * @param {Object} buffer The decoded buffer sound source. - */ - var loadSound = function(self, buffer) { - // Set the duration. - if (buffer && !self._duration) { - self._duration = buffer.duration; - } - - // Setup a sprite if none is defined. - if (Object.keys(self._sprite).length === 0) { - self._sprite = {__default: [0, self._duration * 1000]}; - } - - // Fire the loaded event. - if (self._state !== 'loaded') { - self._state = 'loaded'; - self._emit('load'); - self._loadQueue(); - } - }; - - /** - * Setup the audio context when available, or switch to HTML5 Audio mode. - */ - var setupAudioContext = function() { - // If we have already detected that Web Audio isn't supported, don't run this step again. - if (!Howler.usingWebAudio) { - return; - } - - // Check if we are using Web Audio and setup the AudioContext if we are. - try { - if (typeof AudioContext !== 'undefined') { - Howler.ctx = new AudioContext(); - } else if (typeof webkitAudioContext !== 'undefined') { - Howler.ctx = new webkitAudioContext(); - } else { - Howler.usingWebAudio = false; - } - } catch(e) { - Howler.usingWebAudio = false; - } - - // If the audio context creation still failed, set using web audio to false. - if (!Howler.ctx) { - Howler.usingWebAudio = false; - } - - // Check if a webview is being used on iOS8 or earlier (rather than the browser). - // If it is, disable Web Audio as it causes crashing. - var iOS = (/iP(hone|od|ad)/.test(Howler._navigator && Howler._navigator.platform)); - var appVersion = Howler._navigator && Howler._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/); - var version = appVersion ? parseInt(appVersion[1], 10) : null; - if (iOS && version && version < 9) { - var safari = /safari/.test(Howler._navigator && Howler._navigator.userAgent.toLowerCase()); - if (Howler._navigator && !safari) { - Howler.usingWebAudio = false; - } - } - - // Create and expose the master GainNode when using Web Audio (useful for plugins or advanced usage). - if (Howler.usingWebAudio) { - Howler.masterGain = (typeof Howler.ctx.createGain === 'undefined') ? Howler.ctx.createGainNode() : Howler.ctx.createGain(); - Howler.masterGain.gain.setValueAtTime(Howler._muted ? 0 : Howler._volume, Howler.ctx.currentTime); - Howler.masterGain.connect(Howler.ctx.destination); - } - - // Re-run the setup on Howler. - Howler._setup(); - }; - - // Add support for AMD (Asynchronous Module Definition) libraries such as require.js. - if (typeof define === 'function' && define.amd) { - define([], function() { - return { - Howler: Howler, - Howl: Howl - }; - }); - } - - // Add support for CommonJS libraries such as browserify. - if (typeof exports !== 'undefined') { - exports.Howler = Howler; - exports.Howl = Howl; - } - - // Add to global in Node.js (for testing, etc). - if (typeof global !== 'undefined') { - global.HowlerGlobal = HowlerGlobal; - global.Howler = Howler; - global.Howl = Howl; - global.Sound = Sound; - } else if (typeof window !== 'undefined') { // Define globally in case AMD is not available or unused. - window.HowlerGlobal = HowlerGlobal; - window.Howler = Howler; - window.Howl = Howl; - window.Sound = Sound; - } -})(); - - -/*! - * Spatial Plugin - Adds support for stereo and 3D audio where Web Audio is supported. - * - * howler.js v2.2.4 - * 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\/(\d+)/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(!e.bufferSource)return o;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 p=a._orientation;return n="number"!=typeof n?p[1]:n,t="number"!=typeof t?p[2]:t,r="number"!=typeof r?p[3]:r,o="number"!=typeof o?p[4]:o,i="number"!=typeof i?p[5]:i,"number"!=typeof e?p:(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 p=a._orientation;return n="number"!=typeof n?p[1]:n,t="number"!=typeof t?p[2]:t,r="number"!=typeof r?p[3]:r,o="number"!=typeof o?p[4]:o,i="number"!=typeof i?p[5]:i,"number"!=typeof e?p:(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 Date: Sat, 22 Nov 2025 02:50:19 +0100 Subject: [PATCH 03/25] Switch code to es module --- package-lock.json | 1144 +++++++++++++++ package.json | 27 +- src/helpers/audio-context.ts | 53 + src/helpers/audio-loader.ts | 111 ++ src/helpers/index.ts | 12 + src/howler.core.js | 2587 --------------------------------- src/howler.core.ts | 1899 ++++++++++++++++++++++++ src/index.ts | 12 + src/plugins/howler.spatial.js | 659 --------- src/plugins/index.ts | 13 + src/plugins/plugin.ts | 258 ++++ src/plugins/spatial-plugin.ts | 273 ++++ src/plugins/spatial.ts | 6 + src/types.ts | 55 + tsconfig.json | 21 + tsdown.config.ts | 10 + 16 files changed, 3887 insertions(+), 3253 deletions(-) create mode 100644 package-lock.json create mode 100644 src/helpers/audio-context.ts create mode 100644 src/helpers/audio-loader.ts create mode 100644 src/helpers/index.ts delete mode 100644 src/howler.core.js create mode 100644 src/howler.core.ts create mode 100644 src/index.ts delete mode 100644 src/plugins/howler.spatial.js create mode 100644 src/plugins/index.ts create mode 100644 src/plugins/plugin.ts create mode 100644 src/plugins/spatial-plugin.ts create mode 100644 src/plugins/spatial.ts create mode 100644 src/types.ts create mode 100644 tsconfig.json create mode 100644 tsdown.config.ts diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..d3eca93d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1144 @@ +{ + "name": "howler", + "version": "2.2.4", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "howler", + "version": "2.2.4", + "license": "MIT", + "devDependencies": { + "tsdown": "^0.16.6", + "typescript": "^5.9.3", + "uglify-js": "2.x" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", + "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", + "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@tybys/wasm-util": "^0.10.1" + } + }, + "node_modules/@oxc-project/runtime": { + "version": "0.96.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.96.0.tgz", + "integrity": "sha512-34lh4o9CcSw09Hx6fKihPu85+m+4pmDlkXwJrLvN5nMq5JrcGhhihVM415zDqT8j8IixO1PYYdQZRN4SwQCncg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.98.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.98.0.tgz", + "integrity": "sha512-Vzmd6FsqVuz5HQVcRC/hrx7Ujo3WEVeQP7C2UNP5uy1hUY4SQvMB+93jxkI1KRHz9a/6cni3glPOtvteN+zpsw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@quansync/fs": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@quansync/fs/-/fs-0.1.5.tgz", + "integrity": "sha512-lNS9hL2aS2NZgNW7BBj+6EBl4rOf8l+tQ0eRY6JWCI8jI2kc53gSoqbjojU0OnAWhzoXiOjFyGsHcDGePB3lhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "quansync": "^0.2.11" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.51.tgz", + "integrity": "sha512-Ctn8FUXKWWQI9pWC61P1yumS9WjQtelNS9riHwV7oCkknPGaAry4o7eFx2KgoLMnI2BgFJYpW7Im8/zX3BuONg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.51.tgz", + "integrity": "sha512-EL1aRW2Oq15ShUEkBPsDtLMO8GTqfb/ktM/dFaVzXKQiEE96Ss6nexMgfgQrg8dGnNpndFyffVDb5IdSibsu1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.51.tgz", + "integrity": "sha512-uGtYKlFen9pMIPvkHPWZVDtmYhMQi5g5Ddsndg1gf3atScKYKYgs5aDP4DhHeTwGXQglhfBG7lEaOIZ4UAIWww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.51.tgz", + "integrity": "sha512-JRoVTQtHYbZj1P07JLiuTuXjiBtIa7ag7/qgKA6CIIXnAcdl4LrOf7nfDuHPJcuRKaP5dzecMgY99itvWfmUFQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.51.tgz", + "integrity": "sha512-BKATVnpPZ0TYBW9XfDwyd4kPGgvf964HiotIwUgpMrFOFYWqpZ+9ONNzMV4UFAYC7Hb5C2qgYQk/qj2OnAd4RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.51.tgz", + "integrity": "sha512-xLd7da5jkfbVsBCm1buIRdWtuXY8+hU3+6ESXY/Tk5X5DPHaifrUblhYDgmA34dQt6WyNC2kfXGgrduPEvDI6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.51.tgz", + "integrity": "sha512-EQFXTgHxxTzv3t5EmjUP/DfxzFYx9sMndfLsYaAY4DWF6KsK1fXGYsiupif6qPTViPC9eVmRm78q0pZU/kuIPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.51.tgz", + "integrity": "sha512-p5P6Xpa68w3yFaAdSzIZJbj+AfuDnMDqNSeglBXM7UlJT14Q4zwK+rV+8Mhp9MiUb4XFISZtbI/seBprhkQbiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.51.tgz", + "integrity": "sha512-sNVVyLa8HB8wkFipdfz1s6i0YWinwpbMWk5hO5S+XAYH2UH67YzUT13gs6wZTKg2x/3gtgXzYnHyF5wMIqoDAw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.51.tgz", + "integrity": "sha512-e/JMTz9Q8+T3g/deEi8DK44sFWZWGKr9AOCW5e8C8SCVWzAXqYXAG7FXBWBNzWEZK0Rcwo9TQHTQ9Q0gXgdCaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.51.tgz", + "integrity": "sha512-We3LWqSu6J9s5Y0MK+N7fUiiu37aBGPG3Pc347EoaROuAwkCS2u9xJ5dpIyLW4B49CIbS3KaPmn4kTgPb3EyPw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.0.7" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.51.tgz", + "integrity": "sha512-fj56buHRuMM+r/cb6ZYfNjNvO/0xeFybI6cTkTROJatdP4fvmQ1NS8D/Lm10FCSDEOkqIz8hK3TGpbAThbPHsA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-ia32-msvc": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.51.tgz", + "integrity": "sha512-fkqEqaeEx8AySXiDm54b/RdINb3C0VovzJA3osMhZsbn6FoD73H0AOIiaVAtGr6x63hefruVKTX8irAm4Jkt2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.51.tgz", + "integrity": "sha512-CWuLG/HMtrVcjKGa0C4GnuxONrku89g0+CsH8nT0SNhOtREXuzwgjIXNJImpE/A/DMf9JF+1Xkrq/YRr+F/rCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.51.tgz", + "integrity": "sha512-51/8cNXMrqWqX3o8DZidhwz1uYq0BhHDDSfVygAND1Skx5s1TDw3APSSxCMcFFedwgqGcx34gRouwY+m404BBQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha512-GrTZLRpmp6wIC2ztrWW9MjjTgSKccffgFagbNDOX95/dcjEcYZibYTeaOntySQLcdw1ztBoFkviiUvTMbb9MYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/ast-kit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz", + "integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "pathe": "^2.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/birpc": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.8.0.tgz", + "integrity": "sha512-Bz2a4qD/5GRhiHSwj30c/8kC8QGj12nNDwz3D4ErQ4Xhy35dsSDvF+RA/tWpjyU0pdGtSDiEk6B5fBGE1qNVhw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha512-wzLkDa4K/mzI1OSITC+DUyjgIl/ETNHE9QvYgy6J6Jvqyyz4C0Xfd+lQhb19sX2jMpZV4IssUn0VDVmglV+s4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha512-Baz3aNe2gd2LP2qk5U+sDk/m4oSuwSDcBfayTCTBoWpfIGO5XFxPmjILQII4NGiZjD6DoDI6kf7gKaxkf7s3VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha512-GIOYRizG+TGoc7Wgc1LiOTLare95R3mzKgoln+Q/lE4ceiYH19gUpl0l0Ffq4lJDEf3FxujMe6IBfOCs7pfqNA==", + "dev": true, + "license": "ISC", + "dependencies": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/diff": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", + "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dts-resolver": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/dts-resolver/-/dts-resolver-2.1.3.tgz", + "integrity": "sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "oxc-resolver": ">=11.0.0" + }, + "peerDependenciesMeta": { + "oxc-resolver": { + "optional": true + } + } + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha512-k+yt5n3l48JU4k8ftnKG6V7u32wyH2NfKzeMto9F/QRE0amxy/LayxwlvjjkZEIzqR+19IrtFO8p5kB9QaYUFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/obug": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.0.tgz", + "integrity": "sha512-uu/tgLPoa75CFA7UDkmqspKbefvZh1WMPwkU3bNr0PY746a/+xwXVgbw5co5C3GvJj3h5u8g/pbxXzI0gd1QFg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha512-yqINtL/G7vs2v+dFIZmFUDbnVyFUJFKd6gK22Kgo6R4jfJGFtisKyncWDDULgjfqf4ASQuIQyjJ7XZ+3aWpsAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "align-text": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.51.tgz", + "integrity": "sha512-ZRLgPlS91l4JztLYEZnmMcd3Umcla1hkXJgiEiR4HloRJBBoeaX8qogTu5Jfu36rRMVLndzqYv0h+M5gJAkUfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.98.0", + "@rolldown/pluginutils": "1.0.0-beta.51" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-beta.51", + "@rolldown/binding-darwin-arm64": "1.0.0-beta.51", + "@rolldown/binding-darwin-x64": "1.0.0-beta.51", + "@rolldown/binding-freebsd-x64": "1.0.0-beta.51", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.51", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.51", + "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.51", + "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.51", + "@rolldown/binding-linux-x64-musl": "1.0.0-beta.51", + "@rolldown/binding-openharmony-arm64": "1.0.0-beta.51", + "@rolldown/binding-wasm32-wasi": "1.0.0-beta.51", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.51", + "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.51", + "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.51" + } + }, + "node_modules/rolldown-plugin-dts": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/rolldown-plugin-dts/-/rolldown-plugin-dts-0.18.0.tgz", + "integrity": "sha512-2CJtKYa9WPClZxkJeCt4bGUegQvQKQ1VJp9jFJzG0h8I/80XI6qDgoWfVJUOEhT2swbsRQh/42N1RIWvbXT4rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/generator": "^7.28.5", + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "ast-kit": "^2.2.0", + "birpc": "^2.8.0", + "dts-resolver": "^2.1.3", + "get-tsconfig": "^4.13.0", + "magic-string": "^0.30.21", + "obug": "^2.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@ts-macro/tsc": "^0.3.6", + "@typescript/native-preview": ">=7.0.0-dev.20250601.1", + "rolldown": "^1.0.0-beta.51", + "typescript": "^5.0.0", + "vue-tsc": "~3.1.0" + }, + "peerDependenciesMeta": { + "@ts-macro/tsc": { + "optional": true + }, + "@typescript/native-preview": { + "optional": true + }, + "typescript": { + "optional": true + }, + "vue-tsc": { + "optional": true + } + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tsdown": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/tsdown/-/tsdown-0.16.6.tgz", + "integrity": "sha512-g3xHEnGdfwJTlXhEkqww3Q/KlCfyNFw4rnzuQ9Gqw8T2xjDYrw94qmSw5wYYTAW5zV1sEfWDlfgxZo5mmtu0NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansis": "^4.2.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "diff": "^8.0.2", + "empathic": "^2.0.0", + "hookable": "^5.5.3", + "obug": "^2.1.0", + "rolldown": "1.0.0-beta.51", + "rolldown-plugin-dts": "^0.18.0", + "semver": "^7.7.3", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tree-kill": "^1.2.2", + "unconfig-core": "^7.4.1", + "unrun": "^0.2.11" + }, + "bin": { + "tsdown": "dist/run.mjs" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@arethetypeswrong/core": "^0.18.1", + "@vitejs/devtools": "^0.0.0-alpha.17", + "publint": "^0.3.0", + "typescript": "^5.0.0", + "unplugin-lightningcss": "^0.4.0", + "unplugin-unused": "^0.5.0" + }, + "peerDependenciesMeta": { + "@arethetypeswrong/core": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "publint": { + "optional": true + }, + "typescript": { + "optional": true + }, + "unplugin-lightningcss": { + "optional": true + }, + "unplugin-unused": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha512-qLq/4y2pjcU3vhlhseXGGJ7VbFO4pBANu0kwl8VCa9KEI0V8VfZIx2Fy3w01iSTA/pGwKZSmu/+I4etLNDdt5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "source-map": "~0.5.1", + "yargs": "~3.10.0" + }, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + }, + "optionalDependencies": { + "uglify-to-browserify": "~1.0.0" + } + }, + "node_modules/uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha512-vb2s1lYx2xBtUgy+ta+b2J/GLVUR+wmpINwHePmPRhOsIVCG2wDzKJ0n14GslH1BifsqVzSOwQhRaCAsZ/nI4Q==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/unconfig-core": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/unconfig-core/-/unconfig-core-7.4.1.tgz", + "integrity": "sha512-Bp/bPZjV2Vl/fofoA2OYLSnw1Z0MOhCX7zHnVCYrazpfZvseBbGhwcNQMxsg185Mqh7VZQqK3C8hFG/Dyng+yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@quansync/fs": "^0.1.5", + "quansync": "^0.2.11" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/unrun": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/unrun/-/unrun-0.2.11.tgz", + "integrity": "sha512-HjUuNLRGfRxMvxkwOuO/CpkSzdizTPPApbarLplsTzUm8Kex+nS9eomKU1qgVus6WGWkDYhtf/mgNxGEpyTR6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/runtime": "^0.96.0", + "rolldown": "1.0.0-beta.51" + }, + "bin": { + "unrun": "dist/cli.mjs" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/Gugustinette" + }, + "peerDependencies": { + "synckit": "^0.11.11" + }, + "peerDependenciesMeta": { + "synckit": { + "optional": true + } + } + }, + "node_modules/window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha512-1pTPQDKTdd61ozlKGNCjhNRd+KPmgLSGa3mZTHoOliaGcESD8G1PXhh7c1fgiPjVbNVfgy2Faw4BI8/m0cC8Mg==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q==", + "dev": true, + "license": "MIT/X11", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha512-QFzUah88GAGy9lyDKGBqZdkYApt63rCXYBGYnEP4xDJPXNqXXnBDACnbrXnViV6jRSqAePwrATi2i8mfYm4L1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + } +} diff --git a/package.json b/package.json index 82ea2c02..302cb764 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "2.2.4", "description": "Javascript audio library for the modern web.", "homepage": "https://howlerjs.com", + "type": "module", "keywords": [ "howler", "howler.js", @@ -22,20 +23,32 @@ "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", - "release": "VERSION=`printf 'v' && node -e 'console.log(require(\"./package.json\").version)'` && git tag $VERSION && git push && git push origin $VERSION && npm publish" + "build": "tsdown", + "release": "npm run build && git add dist && git commit -m 'build: update dist files' && npm publish" }, "devDependencies": { - "uglify-js": "2.x" + "tsdown": "^0.16.6", + "typescript": "^5.9.3" }, "main": "dist/howler.js", + "module": "dist/howler.mjs", + "types": "dist/howler.d.ts", + "exports": { + ".": { + "import": "./dist/howler.mjs", + "require": "./dist/howler.js", + "types": "./dist/howler.d.ts" + }, + "./plugins/spatial": { + "import": "./dist/plugins/spatial.mjs", + "require": "./dist/plugins/spatial.js", + "types": "./dist/plugins/spatial.d.ts" + } + }, "license": "MIT", "files": [ "src", - "dist/howler.js", - "dist/howler.min.js", - "dist/howler.core.min.js", - "dist/howler.spatial.min.js", + "dist", "LICENSE.md" ] } diff --git a/src/helpers/audio-context.ts b/src/helpers/audio-context.ts new file mode 100644 index 00000000..346babfc --- /dev/null +++ b/src/helpers/audio-context.ts @@ -0,0 +1,53 @@ +/*! + * Howler.js Audio Context Setup Helper + * howlerjs.com + * + * (c) 2013-2020, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */ + +import { Howler } from '../howler.core'; + +export const setupAudioContext = () => { + if (!Howler.usingWebAudio) { + return; + } + + try { + if (typeof window.AudioContext !== 'undefined') { + Howler.ctx = new window.AudioContext(); + } else if (typeof (window as any).webkitAudioContext !== 'undefined') { + Howler.ctx = new (window as any).webkitAudioContext(); + } else { + Howler.usingWebAudio = false; + } + } catch (e) { + Howler.usingWebAudio = false; + } + + if (!Howler.ctx) { + Howler.usingWebAudio = false; + } + + const iOS = /iP(hone|od|ad)/.test((Howler._navigator && Howler._navigator.platform || "")); + const appVersion = Howler._navigator && Howler._navigator.appVersion?.match(/OS (\d+)_(\d+)_?(\d+)?/); + const version = appVersion ? parseInt(appVersion[1], 10) : null; + if (iOS && version && version < 9) { + const safari = /safari/.test(Howler._navigator?.userAgent.toLowerCase() ?? ''); + if (Howler._navigator && !safari) { + Howler.usingWebAudio = false; + } + } + + if (Howler.usingWebAudio && Howler.ctx) { + Howler.masterGain = typeof Howler.ctx.createGain === 'undefined' ? (Howler.ctx as any).createGainNode() : Howler.ctx.createGain(); + if (Howler.masterGain) { + Howler.masterGain.gain.setValueAtTime(Howler._muted ? 0 : Howler._volume, Howler.ctx.currentTime); + Howler.masterGain.connect(Howler.ctx.destination); + } + } + + Howler._setup(); +}; diff --git a/src/helpers/audio-loader.ts b/src/helpers/audio-loader.ts new file mode 100644 index 00000000..127c6ce8 --- /dev/null +++ b/src/helpers/audio-loader.ts @@ -0,0 +1,111 @@ +/*! + * Howler.js Audio Loading Helpers + * howlerjs.com + * + * (c) 2013-2020, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */ + +import type { Howl } from '../howler.core'; +import { cache } from '../types'; +import { Howler } from '../howler.core'; + +export const loadBuffer = (self: Howl) => { + const url = self._src as string; + + if (cache[url]) { + self._duration = cache[url].duration; + loadSound(self); + return; + } + + if (/^data:[^;]+;base64,/.test(url)) { + const data = atob(url.split(',')[1]); + const dataView = new Uint8Array(data.length); + for (let i = 0; i < data.length; ++i) { + dataView[i] = data.charCodeAt(i); + } + + decodeAudioData(dataView.buffer, self); + } else { + const xhr = new XMLHttpRequest(); + xhr.open(self._xhr.method, url, true); + xhr.withCredentials = self._xhr.withCredentials; + xhr.responseType = 'arraybuffer'; + + if (self._xhr.headers) { + Object.keys(self._xhr.headers).forEach((key) => { + xhr.setRequestHeader(key, self._xhr.headers![key]); + }); + } + + xhr.onload = () => { + const 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); + } +}; + +export const safeXhrSend = (xhr: XMLHttpRequest) => { + try { + xhr.send(); + } catch (e) { + if (xhr.onerror) { + xhr.onerror(new Event('error')); + } + } +}; + +export const decodeAudioData = (arraybuffer: ArrayBuffer, self: Howl) => { + const error = () => { + self._emit('loaderror', null, 'Decoding audio data failed.'); + }; + + const success = (buffer: AudioBuffer) => { + if (buffer && self._sounds.length > 0) { + cache[self._src as string] = buffer; + loadSound(self, buffer); + } else { + error(); + } + }; + + if (typeof Promise !== 'undefined' && Howler.ctx!.decodeAudioData.length === 1) { + (Howler.ctx!.decodeAudioData(arraybuffer) as Promise).then(success).catch(error); + } else { + Howler.ctx!.decodeAudioData(arraybuffer, success, error); + } +}; + +export const loadSound = (self: Howl, buffer?: AudioBuffer) => { + if (buffer && !self._duration) { + self._duration = buffer.duration; + } + + if (Object.keys(self._sprite).length === 0) { + self._sprite = { __default: [0, self._duration * 1000] }; + } + + if (self._state !== 'loaded') { + self._state = 'loaded'; + self._emit('load'); + self._loadQueue(); + } +}; diff --git a/src/helpers/index.ts b/src/helpers/index.ts new file mode 100644 index 00000000..97ab9074 --- /dev/null +++ b/src/helpers/index.ts @@ -0,0 +1,12 @@ +/*! + * Howler.js Helper Functions + * howlerjs.com + * + * (c) 2013-2020, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */ + +export { loadBuffer, safeXhrSend, decodeAudioData, loadSound } from './audio-loader'; +export { setupAudioContext } from './audio-context'; diff --git a/src/howler.core.js b/src/howler.core.js deleted file mode 100644 index 5198bdfc..00000000 --- a/src/howler.core.js +++ /dev/null @@ -1,2587 +0,0 @@ -/*! - * howler.js v2.2.4 - * 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\/(\d+)/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 (!node.bufferSource) { - return self; - } - - if (Howler._scratchBuffer && node.bufferSource) { - node.bufferSource.onended = null; - node.bufferSource.disconnect(0); - if (isIOS) { - try { node.bufferSource.buffer = Howler._scratchBuffer; } catch(e) {} - } - } - node.bufferSource = null; - - return self; - }, - - /** - * Set the source to a 0-second silence to stop any downloading (except in IE). - * @param {Object} node Audio node to clear. - */ - _clearSound: function(node) { - var checkIE = /MSIE |Trident\//.test(Howler._navigator && Howler._navigator.userAgent); - if (!checkIE) { - node.src = 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA'; - } - } - }; - - /** Single Sound Methods **/ - /***************************************************************************/ - - /** - * Setup the sound object, which each node attached to a Howl group is contained in. - * @param {Object} howl The Howl parent group. - */ - var Sound = function(howl) { - this._parent = howl; - this.init(); - }; - Sound.prototype = { - /** - * Initialize a new Sound object. - * @return {Sound} - */ - init: function() { - var self = this; - var parent = self._parent; - - // Setup the default parameters. - self._muted = parent._muted; - self._loop = parent._loop; - self._volume = parent._volume; - self._rate = parent._rate; - self._seek = 0; - self._paused = true; - self._ended = true; - self._sprite = '__default'; - - // Generate a unique ID for this sound. - self._id = ++Howler._counter; - - // Add itself to the parent's pool. - parent._sounds.push(self); - - // Create the new node. - self.create(); - - return self; - }, - - /** - * Create and setup a new sound object, whether HTML5 Audio or Web Audio. - * @return {Sound} - */ - create: function() { - var self = this; - var parent = self._parent; - var volume = (Howler._muted || self._muted || self._parent._muted) ? 0 : self._volume; - - if (parent._webAudio) { - // Create the gain node for controlling volume (the source will connect to this). - self._node = (typeof Howler.ctx.createGain === 'undefined') ? Howler.ctx.createGainNode() : Howler.ctx.createGain(); - self._node.gain.setValueAtTime(volume, Howler.ctx.currentTime); - self._node.paused = true; - self._node.connect(Howler.masterGain); - } else if (!Howler.noAudio) { - // Get an unlocked Audio object from the pool. - self._node = Howler._obtainHtml5Audio(); - - // Listen for errors (http://dev.w3.org/html5/spec-author-view/spec.html#mediaerror). - self._errorFn = self._errorListener.bind(self); - self._node.addEventListener('error', self._errorFn, false); - - // Listen for 'canplaythrough' event to let us know the sound is ready. - self._loadFn = self._loadListener.bind(self); - self._node.addEventListener(Howler._canPlayEvent, self._loadFn, false); - - // Listen for the 'ended' event on the sound to account for edge-case where - // a finite sound has a duration of Infinity. - self._endFn = self._endListener.bind(self); - self._node.addEventListener('ended', self._endFn, false); - - // Setup the new audio node. - self._node.src = parent._src; - self._node.preload = parent._preload === true ? 'auto' : parent._preload; - self._node.volume = volume * Howler.volume(); - - // Begin loading the source. - self._node.load(); - } - - return self; - }, - - /** - * Reset the parameters of this sound to the original state (for recycle). - * @return {Sound} - */ - reset: function() { - var self = this; - var parent = self._parent; - - // Reset all of the parameters of this sound. - self._muted = parent._muted; - self._loop = parent._loop; - self._volume = parent._volume; - self._rate = parent._rate; - self._seek = 0; - self._rateSeek = 0; - self._paused = true; - self._ended = true; - self._sprite = '__default'; - - // Generate a new ID so that it isn't confused with the previous sound. - self._id = ++Howler._counter; - - return self; - }, - - /** - * HTML5 Audio error listener callback. - */ - _errorListener: function() { - var self = this; - - // Fire an error event and pass back the code. - self._parent._emit('loaderror', self._id, self._node.error ? self._node.error.code : 0); - - // Clear the event listener. - self._node.removeEventListener('error', self._errorFn, false); - }, - - /** - * HTML5 Audio canplaythrough listener callback. - */ - _loadListener: function() { - var self = this; - var parent = self._parent; - - // Round up the duration to account for the lower precision in HTML5 Audio. - parent._duration = Math.ceil(self._node.duration * 10) / 10; - - // Setup a sprite if none is defined. - if (Object.keys(parent._sprite).length === 0) { - parent._sprite = {__default: [0, parent._duration * 1000]}; - } - - if (parent._state !== 'loaded') { - parent._state = 'loaded'; - parent._emit('load'); - parent._loadQueue(); - } - - // Clear the event listener. - self._node.removeEventListener(Howler._canPlayEvent, self._loadFn, false); - }, - - /** - * HTML5 Audio ended listener callback. - */ - _endListener: function() { - var self = this; - var parent = self._parent; - - // Only handle the `ended`` event if the duration is Infinity. - if (parent._duration === Infinity) { - // Update the parent duration to match the real audio duration. - // Round up the duration to account for the lower precision in HTML5 Audio. - parent._duration = Math.ceil(self._node.duration * 10) / 10; - - // Update the sprite that corresponds to the real duration. - if (parent._sprite.__default[1] === Infinity) { - parent._sprite.__default[1] = parent._duration * 1000; - } - - // Run the regular ended method. - parent._ended(self); - } - - // Clear the event listener since the duration is now correct. - self._node.removeEventListener('ended', self._endFn, false); - } - }; - - /** Helper Methods **/ - /***************************************************************************/ - - var cache = {}; - - /** - * Buffer a sound from URL, Data URI or cache and decode to audio source (Web Audio API). - * @param {Howl} self - */ - var loadBuffer = function(self) { - var url = self._src; - - // Check if the buffer has already been cached and use it instead. - if (cache[url]) { - // Set the duration from the cache. - self._duration = cache[url].duration; - - // Load the sound into this Howl. - loadSound(self); - - return; - } - - if (/^data:[^;]+;base64,/.test(url)) { - // Decode the base64 data URI without XHR, since some browsers don't support it. - var data = atob(url.split(',')[1]); - var dataView = new Uint8Array(data.length); - for (var i=0; i 0) { - cache[self._src] = buffer; - loadSound(self, buffer); - } else { - error(); - } - }; - - // Decode the buffer into an audio source. - if (typeof Promise !== 'undefined' && Howler.ctx.decodeAudioData.length === 1) { - Howler.ctx.decodeAudioData(arraybuffer).then(success).catch(error); - } else { - Howler.ctx.decodeAudioData(arraybuffer, success, error); - } - } - - /** - * Sound is now loaded, so finish setting everything up and fire the loaded event. - * @param {Howl} self - * @param {Object} buffer The decoded buffer sound source. - */ - var loadSound = function(self, buffer) { - // Set the duration. - if (buffer && !self._duration) { - self._duration = buffer.duration; - } - - // Setup a sprite if none is defined. - if (Object.keys(self._sprite).length === 0) { - self._sprite = {__default: [0, self._duration * 1000]}; - } - - // Fire the loaded event. - if (self._state !== 'loaded') { - self._state = 'loaded'; - self._emit('load'); - self._loadQueue(); - } - }; - - /** - * Setup the audio context when available, or switch to HTML5 Audio mode. - */ - var setupAudioContext = function() { - // If we have already detected that Web Audio isn't supported, don't run this step again. - if (!Howler.usingWebAudio) { - return; - } - - // Check if we are using Web Audio and setup the AudioContext if we are. - try { - if (typeof AudioContext !== 'undefined') { - Howler.ctx = new AudioContext(); - } else if (typeof webkitAudioContext !== 'undefined') { - Howler.ctx = new webkitAudioContext(); - } else { - Howler.usingWebAudio = false; - } - } catch(e) { - Howler.usingWebAudio = false; - } - - // If the audio context creation still failed, set using web audio to false. - if (!Howler.ctx) { - Howler.usingWebAudio = false; - } - - // Check if a webview is being used on iOS8 or earlier (rather than the browser). - // If it is, disable Web Audio as it causes crashing. - var iOS = (/iP(hone|od|ad)/.test(Howler._navigator && Howler._navigator.platform)); - var appVersion = Howler._navigator && Howler._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/); - var version = appVersion ? parseInt(appVersion[1], 10) : null; - if (iOS && version && version < 9) { - var safari = /safari/.test(Howler._navigator && Howler._navigator.userAgent.toLowerCase()); - if (Howler._navigator && !safari) { - Howler.usingWebAudio = false; - } - } - - // Create and expose the master GainNode when using Web Audio (useful for plugins or advanced usage). - if (Howler.usingWebAudio) { - Howler.masterGain = (typeof Howler.ctx.createGain === 'undefined') ? Howler.ctx.createGainNode() : Howler.ctx.createGain(); - Howler.masterGain.gain.setValueAtTime(Howler._muted ? 0 : Howler._volume, Howler.ctx.currentTime); - Howler.masterGain.connect(Howler.ctx.destination); - } - - // Re-run the setup on Howler. - Howler._setup(); - }; - - // Add support for AMD (Asynchronous Module Definition) libraries such as require.js. - if (typeof define === 'function' && define.amd) { - define([], function() { - return { - Howler: Howler, - Howl: Howl - }; - }); - } - - // Add support for CommonJS libraries such as browserify. - if (typeof exports !== 'undefined') { - exports.Howler = Howler; - exports.Howl = Howl; - } - - // Add to global in Node.js (for testing, etc). - if (typeof global !== 'undefined') { - global.HowlerGlobal = HowlerGlobal; - global.Howler = Howler; - global.Howl = Howl; - global.Sound = Sound; - } else if (typeof window !== 'undefined') { // Define globally in case AMD is not available or unused. - window.HowlerGlobal = HowlerGlobal; - window.Howler = Howler; - window.Howl = Howl; - window.Sound = Sound; - } -})(); diff --git a/src/howler.core.ts b/src/howler.core.ts new file mode 100644 index 00000000..41c813a9 --- /dev/null +++ b/src/howler.core.ts @@ -0,0 +1,1899 @@ +/*! + * howler.js v2.2.4 + * howlerjs.com + * + * (c) 2013-2020, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */ +// Import shared types +import { cache, HowlOptions, EventListener, QueueItem } from './types'; + +// Import helper functions +import { loadBuffer, setupAudioContext } from './helpers'; + +// Import plugin manager +import { globalPluginManager } from './plugins'; + +export class HowlerGlobal { + _counter: number = 1000; + _html5AudioPool: HTMLAudioElement[] = []; + html5PoolSize: number = 10; + _codecs: Record = {}; + _howls: Howl[] = []; + _muted: boolean = false; + _volume: number = 1; + _canPlayEvent: string = 'canplaythrough'; + _navigator: Navigator | null = null; + masterGain: GainNode | null = null; + noAudio: boolean = false; + usingWebAudio: boolean = true; + autoSuspend: boolean = true; + ctx: AudioContext | null = null; + autoUnlock: boolean = true; + state: string = 'suspended'; + _audioUnlocked: boolean = false; + _scratchBuffer: AudioBuffer | null = null; + _suspendTimer: ReturnType | null = null; + _resumeAfterSuspend?: boolean; + _mobileUnloaded?: boolean; + + constructor() { + this.init(); + } + + init(): HowlerGlobal { + const self = this; + + self._counter = 1000; + self._html5AudioPool = []; + self.html5PoolSize = 10; + self._codecs = {}; + self._howls = []; + self._muted = false; + self._volume = 1; + self._canPlayEvent = 'canplaythrough'; + self._navigator = typeof window !== 'undefined' && window.navigator ? window.navigator : null; + + self.masterGain = null; + self.noAudio = false; + self.usingWebAudio = true; + self.autoSuspend = true; + self.ctx = null; + self.autoUnlock = true; + + self._setup(); + + // Execute plugin hooks + globalPluginManager.executeHowlerInit(self); + + return self; + } + + volume(vol?: number): number | HowlerGlobal { + const self = this; + if (vol !== undefined) { + vol = parseFloat(vol as any); + + if (!self.ctx) { + setupAudioContext(); + } + + if (typeof vol === 'number' && vol >= 0 && vol <= 1) { + self._volume = vol; + + if (self._muted) { + return self; + } + + if (self.usingWebAudio) { + self.masterGain!.gain.setValueAtTime(vol, Howler.ctx!.currentTime); + } + + for (let i = 0; i < self._howls.length; i++) { + if (!self._howls[i]._webAudio) { + const ids = self._howls[i]._getSoundIds(); + for (let j = 0; j < ids.length; j++) { + const sound = self._howls[i]._soundById(ids[j]); + if (sound && sound._node) { + sound._node.volume = sound._volume * vol; + } + } + } + } + + return self; + } + } + + return self._volume; + } + + mute(muted: boolean): HowlerGlobal { + const self = this; + + if (!self.ctx) { + setupAudioContext(); + } + + self._muted = muted; + + if (self.usingWebAudio) { + self.masterGain!.gain.setValueAtTime(muted ? 0 : self._volume, Howler.ctx!.currentTime); + } + + for (let i = 0; i < self._howls.length; i++) { + if (!self._howls[i]._webAudio) { + const ids = self._howls[i]._getSoundIds(); + for (let j = 0; j < ids.length; j++) { + const sound = self._howls[i]._soundById(ids[j]); + if (sound && sound._node) { + sound._node.muted = muted ? true : sound._muted; + } + } + } + } + + return self; + } + + stop(): HowlerGlobal { + const self = this; + + for (let i = 0; i < self._howls.length; i++) { + self._howls[i].stop(); + } + + return self; + } + + unload(): HowlerGlobal { + const self = this; + + for (let i = self._howls.length - 1; i >= 0; i--) { + self._howls[i].unload(); + } + + if (self.usingWebAudio && self.ctx && typeof self.ctx.close !== 'undefined') { + self.ctx.close(); + self.ctx = null; + setupAudioContext(); + } + + return self; + } + + codecs(ext: string): boolean { + return (this || Howler)._codecs[ext.replace(/^x-/, '')]; + } + + _setup(): HowlerGlobal { + const self = this; + + self.state = self.ctx ? self.ctx.state || 'suspended' : 'suspended'; + self._autoSuspend(); + + if (!self.usingWebAudio) { + if (typeof window.Audio !== 'undefined') { + try { + const test = new window.Audio(); + if (typeof test.oncanplaythrough === 'undefined') { + self._canPlayEvent = 'canplay'; + } + } catch (e) { + self.noAudio = true; + } + } else { + self.noAudio = true; + } + } + + try { + const test = new window.Audio(); + if (test.muted) { + self.noAudio = true; + } + } catch (e) {} + + if (!self.noAudio) { + self._setupCodecs(); + } + + return self; + } + + _setupCodecs(): HowlerGlobal { + const self = this; + let audioTest: any = null; + + try { + audioTest = typeof window.Audio !== 'undefined' ? new window.Audio() : null; + } catch (err) { + return self; + } + + if (!audioTest || typeof audioTest.canPlayType !== 'function') { + return self; + } + + const mpegTest = audioTest.canPlayType('audio/mpeg;').replace(/^no$/, ''); + const ua = self._navigator ? self._navigator.userAgent : ''; + const checkOpera = ua.match(/OPR\/(\d+)/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; + + 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; + } + + _unlockAudio(): void { + const self = this; + + if (self._audioUnlocked || !self.ctx) { + return; + } + + self._audioUnlocked = false; + self.autoUnlock = false; + + if (!self._mobileUnloaded && self.ctx.sampleRate !== 44100) { + self._mobileUnloaded = true; + self.unload(); + } + + self._scratchBuffer = self.ctx.createBuffer(1, 1, 22050); + + const unlock = () => { + while (self._html5AudioPool.length < self.html5PoolSize) { + try { + const audioNode = new (window as any).Audio(); + audioNode._unlocked = true; + self._releaseHtml5Audio(audioNode); + } catch (e) { + self.noAudio = true; + break; + } + } + + for (let i = 0; i < self._howls.length; i++) { + if (!self._howls[i]._webAudio) { + const ids = self._howls[i]._getSoundIds(); + for (let j = 0; j < ids.length; j++) { + const sound = self._howls[i]._soundById(ids[j]); + if (sound && sound._node && !(sound._node as any)._unlocked) { + (sound._node as any)._unlocked = true; + sound._node.load(); + } + } + } + } + + self._autoResume(); + + const source = self.ctx!.createBufferSource(); + source.buffer = self._scratchBuffer; + source.connect(self.ctx!.destination); + + if (typeof source.start === 'undefined') { + (source as any).noteOn(0); + } else { + source.start(0); + } + + if (typeof self.ctx!.resume === 'function') { + self.ctx!.resume(); + } + + source.onended = () => { + source.disconnect(0); + self._audioUnlocked = true; + + document.removeEventListener('touchstart', unlock, true); + document.removeEventListener('touchend', unlock, true); + document.removeEventListener('click', unlock, true); + document.removeEventListener('keydown', unlock, true); + + for (let i = 0; i < self._howls.length; i++) { + self._howls[i]._emit('unlock'); + } + }; + }; + + document.addEventListener('touchstart', unlock as any, true); + document.addEventListener('touchend', unlock as any, true); + document.addEventListener('click', unlock as any, true); + document.addEventListener('keydown', unlock as any, true); + } + + _obtainHtml5Audio(): HTMLAudioElement { + const self = this; + + if (self._html5AudioPool.length) { + return self._html5AudioPool.pop()!; + } + + const testPlay = new (window as any).Audio().play(); + if (testPlay && typeof Promise !== 'undefined' && (testPlay instanceof Promise || typeof (testPlay as any).then === 'function')) { + (testPlay as any).catch(() => { + console.warn('HTML5 Audio pool exhausted, returning potentially locked audio object.'); + }); + } + + return new (window as any).Audio(); + } + + _releaseHtml5Audio(audio: any): HowlerGlobal { + const self = this; + + if (audio._unlocked) { + self._html5AudioPool.push(audio); + } + + return self; + } + + _autoSuspend(): void { + const self = this; + + if (!self.autoSuspend || !self.ctx || typeof self.ctx.suspend === 'undefined' || !Howler.usingWebAudio) { + return; + } + + for (let i = 0; i < self._howls.length; i++) { + if (self._howls[i]._webAudio) { + for (let j = 0; j < self._howls[i]._sounds.length; j++) { + if (!self._howls[i]._sounds[j]._paused) { + return; + } + } + } + } + + if (self._suspendTimer) { + clearTimeout(self._suspendTimer); + } + + self._suspendTimer = setTimeout(() => { + if (!self.autoSuspend) { + return; + } + + self._suspendTimer = null; + self.state = 'suspending'; + + const handleSuspension = () => { + self.state = 'suspended'; + + if (self._resumeAfterSuspend) { + delete self._resumeAfterSuspend; + self._autoResume(); + } + }; + + self.ctx!.suspend().then(handleSuspension, handleSuspension); + }, 30000); + } + + _autoResume(): void { + const self = this; + + if (!self.ctx || typeof self.ctx.resume === 'undefined' || !Howler.usingWebAudio) { + return; + } + + if (self.state === 'running' && self.ctx.state !== 'interrupted' && self._suspendTimer) { + clearTimeout(self._suspendTimer); + self._suspendTimer = null; + } else if (self.state === 'suspended' || (self.state === 'running' && self.ctx.state === 'interrupted')) { + self.ctx.resume().then(() => { + self.state = 'running'; + + for (let i = 0; i < self._howls.length; i++) { + self._howls[i]._emit('resume'); + } + }); + + if (self._suspendTimer) { + clearTimeout(self._suspendTimer); + self._suspendTimer = null; + } + } else if (self.state === 'suspending') { + self._resumeAfterSuspend = true; + } + } +} + +// Setup the global audio controller +const Howler = new HowlerGlobal(); + +class Sound { + _parent: Howl; + _muted: boolean = false; + _loop: boolean = false; + _volume: number = 1; + _rate: number = 1; + _seek: number = 0; + _paused: boolean = true; + _ended: boolean = true; + _sprite: string = '__default'; + _id: number = 0; + _node: HTMLAudioElement | any = null; + _playStart: number = 0; + _rateSeek: number = 0; + _errorFn?: any; + _loadFn?: any; + _endFn?: any; + _start?: number; + _stop?: number; + _panner?: PannerNode | StereoPannerNode; + _fadeTo?: number; + _interval?: ReturnType; + + constructor(howl: Howl) { + this._parent = howl; + this.init(); + } + + init(): Sound { + const self = this; + const parent = self._parent; + + 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'; + + self._id = ++Howler._counter; + + parent._sounds.push(self); + + self.create(); + + // Execute plugin hooks + globalPluginManager.executeSoundCreate(self, parent); + + return self; + } + + create(): Sound { + const self = this; + const parent = self._parent; + const volume = Howler._muted || self._muted || parent._muted ? 0 : self._volume; + + if (parent._webAudio) { + 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) { + self._node = Howler._obtainHtml5Audio(); + + self._errorFn = self._errorListener.bind(self); + self._node.addEventListener('error', self._errorFn, false); + + self._loadFn = self._loadListener.bind(self); + self._node.addEventListener(Howler._canPlayEvent, self._loadFn, false); + + self._endFn = self._endListener.bind(self); + self._node.addEventListener('ended', self._endFn, false); + + self._node.src = parent._src; + self._node.preload = parent._preload === true ? 'auto' : parent._preload; + const volumeOrHowler = Howler.volume(); + if (typeof volumeOrHowler === 'number') { + self._node.volume = volume * volumeOrHowler; + } + + self._node.load(); + } + + return self; + } + + reset(): Sound { + const self = this; + const parent = self._parent; + + 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'; + + self._id = ++Howler._counter; + + return self; + } + + _errorListener(): void { + const self = this; + self._parent._emit('loaderror', self._id, self._node.error ? self._node.error.code : 0); + self._node.removeEventListener('error', self._errorFn, false); + } + + _loadListener(): void { + const self = this; + const parent = self._parent; + + parent._duration = Math.ceil(self._node.duration * 10) / 10; + + 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(); + + // Execute plugin hooks + globalPluginManager.executeHowlLoad(parent); + } + + self._node.removeEventListener(Howler._canPlayEvent, self._loadFn, false); + } + + _endListener(): void { + const self = this; + const parent = self._parent; + + if (parent._duration === Infinity) { + parent._duration = Math.ceil(self._node.duration * 10) / 10; + + if (parent._sprite.__default[1] === Infinity) { + parent._sprite.__default[1] = parent._duration * 1000; + } + + parent._ended(self); + } + + self._node.removeEventListener('ended', self._endFn, false); + } +} + +class Howl { + _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: Record = {}; + _src: string | string[] = []; + _volume: number = 1; + _xhr: { method: string; headers?: Record; withCredentials: boolean } = { method: 'GET', withCredentials: false }; + _duration: number = 0; + _state: string = 'unloaded'; + _sounds: Sound[] = []; + _endTimers: Record = {}; + _queue: QueueItem[] = []; + _playLock: boolean = false; + _webAudio: boolean = false; + _onend: EventListener[] = []; + _onfade: EventListener[] = []; + _onload: EventListener[] = []; + _onloaderror: EventListener[] = []; + _onplayerror: EventListener[] = []; + _onpause: EventListener[] = []; + _onplay: EventListener[] = []; + _onstop: EventListener[] = []; + _onmute: EventListener[] = []; + _onvolume: EventListener[] = []; + _onrate: EventListener[] = []; + _onseek: EventListener[] = []; + _onunlock: EventListener[] = []; + _onresume: EventListener[] = []; + + constructor(o: HowlOptions) { + if (!o.src || o.src.length === 0) { + console.error('An array of source files must be passed with any new Howl.'); + return; + } + + this.init(o); + } + + init(o: HowlOptions): Howl { + const self = this; + + if (!Howler.ctx) { + setupAudioContext(); + } + + self._autoplay = o.autoplay || false; + self._format = typeof o.format !== 'string' ? o.format || [] : [o.format]; + self._html5 = o.html5 || false; + self._muted = o.mute || false; + self._loop = o.loop || false; + self._pool = o.pool || 5; + self._preload = typeof o.preload === 'boolean' || o.preload === 'metadata' ? o.preload : true; + self._rate = o.rate || 1; + self._sprite = o.sprite || {}; + self._src = typeof o.src !== 'string' ? o.src : [o.src]; + self._volume = o.volume !== undefined ? o.volume : 1; + self._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 + }; + + self._duration = 0; + self._state = 'unloaded'; + self._sounds = []; + self._endTimers = {}; + self._queue = []; + self._playLock = false; + + self._onend = o.onend ? [{ fn: o.onend }] : []; + self._onfade = o.onfade ? [{ fn: o.onfade }] : []; + self._onload = o.onload ? [{ fn: o.onload }] : []; + self._onloaderror = o.onloaderror ? [{ fn: o.onloaderror }] : []; + self._onplayerror = o.onplayerror ? [{ fn: o.onplayerror }] : []; + self._onpause = o.onpause ? [{ fn: o.onpause }] : []; + self._onplay = o.onplay ? [{ fn: o.onplay }] : []; + self._onstop = o.onstop ? [{ fn: o.onstop }] : []; + self._onmute = o.onmute ? [{ fn: o.onmute }] : []; + self._onvolume = o.onvolume ? [{ fn: o.onvolume }] : []; + self._onrate = o.onrate ? [{ fn: o.onrate }] : []; + self._onseek = o.onseek ? [{ fn: o.onseek }] : []; + self._onunlock = o.onunlock ? [{ fn: o.onunlock }] : []; + self._onresume = []; + + self._webAudio = Howler.usingWebAudio && !self._html5; + + if (typeof Howler.ctx !== 'undefined' && Howler.ctx && Howler.autoUnlock) { + Howler._unlockAudio(); + } + + Howler._howls.push(self); + + // Execute plugin hooks + globalPluginManager.executeHowlCreate(self, o); + + if (self._autoplay) { + self._queue.push({ + event: 'play', + action: () => { + self.play(); + } + }); + } + + if (self._preload && self._preload !== 'none') { + self.load(); + } + + return self; + } + + load(): Howl { + const self = this; + let url: string | null = null; + + if (Howler.noAudio) { + self._emit('loaderror', null, 'No audio support.'); + return self; + } + + if (typeof self._src === 'string') { + self._src = [self._src]; + } + + for (let i = 0; i < (self._src as string[]).length; i++) { + let ext: string | null; + const str = (self._src as string[])[i]; + + if (self._format && self._format[i]) { + ext = self._format[i]; + } else { + if (typeof str !== 'string') { + self._emit('loaderror', null, 'Non-string found in selected audio sources - ignoring.'); + continue; + } + + let extMatch = /^data:audio\/([^;,]+);/i.exec(str); + if (!extMatch) { + extMatch = /\.([^.]+)$/.exec(str.split('?', 1)[0]); + } + + ext = extMatch ? extMatch[1].toLowerCase() : null; + } + + if (!ext) { + console.warn('No file extension was found. Consider using the "format" property or specify an extension.'); + } + + if (ext && Howler.codecs(ext)) { + url = (self._src as string[])[i]; + break; + } + } + + if (!url) { + self._emit('loaderror', null, 'No codec support for selected audio sources.'); + return self; + } + + self._src = url; + self._state = 'loading'; + + if (typeof window !== 'undefined' && window.location.protocol === 'https:' && url.slice(0, 5) === 'http:') { + self._html5 = true; + self._webAudio = false; + } + + new Sound(self); + + if (self._webAudio) { + loadBuffer(self); + } + + return self; + } + + play(sprite?: string | number, internal?: boolean): number | null { + const self = this; + let id: number | null = null; + + if (typeof sprite === 'number') { + id = sprite; + sprite = undefined; + } else if (typeof sprite === 'string' && self._state === 'loaded' && !self._sprite[sprite]) { + return null; + } else if (typeof sprite === 'undefined') { + sprite = '__default'; + + if (!self._playLock) { + let num = 0; + for (let i = 0; i < self._sounds.length; i++) { + if (self._sounds[i]._paused && !self._sounds[i]._ended) { + num++; + id = self._sounds[i]._id; + } + } + + if (num === 1) { + sprite = undefined; + } else { + id = null; + } + } + } + + const sound = id ? self._soundById(id) : self._inactiveSound(); + + if (!sound) { + return null; + } + + if (id && !sprite) { + sprite = sound._sprite || '__default'; + } + + if (self._state !== 'loaded') { + sound._sprite = sprite; + sound._ended = false; + + const soundId = sound._id; + self._queue.push({ + event: 'play', + action: () => { + self.play(soundId); + } + }); + + return soundId; + } + + if (id && !sound._paused) { + if (!internal) { + self._loadQueue('play'); + } + + return sound._id; + } + + if (self._webAudio) { + Howler._autoResume(); + } + + const seek = Math.max(0, sound._seek > 0 ? sound._seek : self._sprite[sprite!][0] / 1000); + const duration = Math.max(0, (self._sprite[sprite!][0] + self._sprite[sprite!][1]) / 1000 - seek); + const timeout = (duration * 1000) / Math.abs(sound._rate); + const start = self._sprite[sprite!][0] / 1000; + const stop = (self._sprite[sprite!][0] + self._sprite[sprite!][1]) / 1000; + sound._sprite = sprite!; + + sound._ended = false; + + const setParams = () => { + sound._paused = false; + sound._seek = seek; + sound._start = start; + sound._stop = stop; + sound._loop = !!(sound._loop || self._sprite[sprite!][2]); + }; + + if (seek >= stop) { + self._ended(sound); + return sound._id; + } + + const node = sound._node; + + if (self._webAudio) { + const playWebAudio = () => { + self._playLock = false; + setParams(); + self._refreshBuffer(sound); + + const vol = sound._muted || self._muted ? 0 : sound._volume; + node.gain.setValueAtTime(vol, Howler.ctx!.currentTime); + sound._playStart = Howler.ctx!.currentTime; + + if (typeof (node.bufferSource as any).start === 'undefined') { + (node.bufferSource as any).noteGrainOn(0, seek, sound._loop ? 86400 : duration); + } else { + (node.bufferSource as any).start(0, seek, sound._loop ? 86400 : duration); + } + + if (timeout !== Infinity) { + self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout); + } + + if (!internal) { + setTimeout(() => { + self._emit('play', sound._id); + self._loadQueue(); + }, 0); + } + }; + + if (Howler.state === 'running' && Howler.ctx!.state !== 'interrupted') { + playWebAudio(); + } else { + self._playLock = true; + self.once('resume', playWebAudio); + self._clearTimer(sound._id); + } + } else { + const playHtml5 = () => { + node.currentTime = seek; + node.muted = sound._muted || self._muted || Howler._muted || node.muted; + node.volume = sound._volume * Howler.volume(); + node.playbackRate = sound._rate; + + try { + const play = node.play(); + + if (play && typeof Promise !== 'undefined' && (play instanceof Promise || typeof (play as any).then === 'function')) { + self._playLock = true; + + setParams(); + + (play as any) + .then(() => { + self._playLock = false; + (node as any)._unlocked = true; + if (!internal) { + self._emit('play', sound._id); + } else { + self._loadQueue(); + } + }) + .catch(() => { + 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.'); + sound._ended = true; + sound._paused = true; + }); + } else if (!internal) { + self._playLock = false; + setParams(); + self._emit('play', sound._id); + } + + node.playbackRate = sound._rate; + + 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; + } + + if (sprite !== '__default' || sound._loop) { + self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout); + } else { + self._endTimers[sound._id] = () => { + self._ended(sound); + 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 (node.src === 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA') { + node.src = self._src; + node.load(); + } + + const loadedNoReadyState = (typeof (window as any).ejecta !== 'undefined') || (!node.readyState && Howler._navigator && (Howler._navigator as any).isCocoonJS); + if (node.readyState >= 3 || loadedNoReadyState) { + playHtml5(); + } else { + self._playLock = true; + self._state = 'loading'; + + const listener = () => { + self._state = 'loaded'; + playHtml5(); + node.removeEventListener(Howler._canPlayEvent, listener, false); + }; + node.addEventListener(Howler._canPlayEvent, listener as any, false); + + self._clearTimer(sound._id); + } + } + + return sound._id; + } + + pause(id?: number): Howl { + const self = this; + + if (self._state !== 'loaded' || self._playLock) { + self._queue.push({ + event: 'pause', + action: () => { + self.pause(id); + } + }); + + return self; + } + + const ids = self._getSoundIds(id); + + for (let i = 0; i < ids.length; i++) { + self._clearTimer(ids[i]); + + const sound = self._soundById(ids[i]); + + if (sound && !sound._paused) { + sound._seek = self.seek(ids[i]) as number; + sound._rateSeek = 0; + sound._paused = true; + + self._stopFade(ids[i]); + + if (sound._node) { + if (self._webAudio) { + if (!sound._node.bufferSource) { + continue; + } + + if (typeof (sound._node.bufferSource as any).stop === 'undefined') { + (sound._node.bufferSource as any).noteOff(0); + } else { + (sound._node.bufferSource as any).stop(0); + } + + self._cleanBuffer(sound._node); + } else if (!isNaN(sound._node.duration) || sound._node.duration === Infinity) { + sound._node.pause(); + } + } + } + + if (!arguments[1]) { + self._emit('pause', sound ? sound._id : null); + } + } + + return self; + } + + stop(id?: number, internal?: boolean): Howl { + const self = this; + + if (self._state !== 'loaded' || self._playLock) { + self._queue.push({ + event: 'stop', + action: () => { + self.stop(id); + } + }); + + return self; + } + + const ids = self._getSoundIds(id); + + for (let i = 0; i < ids.length; i++) { + self._clearTimer(ids[i]); + + const sound = self._soundById(ids[i]); + + if (sound) { + sound._seek = sound._start || 0; + sound._rateSeek = 0; + sound._paused = true; + sound._ended = true; + + self._stopFade(ids[i]); + + if (sound._node) { + if (self._webAudio) { + if (sound._node.bufferSource) { + if (typeof (sound._node.bufferSource as any).stop === 'undefined') { + (sound._node.bufferSource as any).noteOff(0); + } else { + (sound._node.bufferSource as any).stop(0); + } + + self._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) { + self._clearSound(sound._node); + } + } + } + + if (!internal) { + self._emit('stop', sound._id); + } + } + } + + return self; + } + + mute(muted: boolean, id?: number): boolean | Howl { + const self = this; + + if (self._state !== 'loaded' || self._playLock) { + self._queue.push({ + event: 'mute', + action: () => { + self.mute(muted, id); + } + }); + + return self; + } + + if (typeof id === 'undefined') { + if (typeof muted === 'boolean') { + self._muted = muted; + } else { + return self._muted; + } + } + + const ids = self._getSoundIds(id); + + for (let i = 0; i < ids.length; i++) { + const sound = self._soundById(ids[i]); + + if (sound) { + sound._muted = muted; + + if (sound._interval) { + self._stopFade(sound._id); + } + + if (self._webAudio && sound._node) { + sound._node.gain.setValueAtTime(muted ? 0 : sound._volume, Howler.ctx!.currentTime); + } else if (sound._node) { + sound._node.muted = Howler._muted ? true : muted; + } + + self._emit('mute', sound._id); + } + } + + return self; + } + + volume(): number; + volume(vol: number): Howl; + volume(vol?: number): number | Howl { + const self = this; + const args = arguments; + let volume: number | undefined; + let id: number | undefined; + + if (args.length === 0) { + return self._volume; + } else if (args.length === 1 || (args.length === 2 && typeof args[1] === 'undefined')) { + const ids = self._getSoundIds(); + const index = ids.indexOf(args[0] as any); + if (index >= 0) { + id = parseInt(args[0] as any, 10); + } else { + volume = parseFloat(args[0] as any); + } + } else if (args.length >= 2) { + volume = parseFloat(args[0] as any); + id = parseInt(args[1] as any, 10); + } + + let sound; + if (typeof volume !== 'undefined' && volume >= 0 && volume <= 1) { + if (self._state !== 'loaded' || self._playLock) { + self._queue.push({ + event: 'volume', + action: () => { + self.volume.apply(self, args as any); + } + }); + + return self; + } + + if (typeof id === 'undefined') { + self._volume = volume; + } + + id = self._getSoundIds(id); + for (let i = 0; i < (id as any).length; i++) { + sound = self._soundById((id as any)[i]); + + if (sound) { + sound._volume = volume; + + if (!(args as any)[2]) { + self._stopFade((id as any)[i]); + } + + if (self._webAudio && sound._node && !sound._muted) { + sound._node.gain.setValueAtTime(volume, Howler.ctx!.currentTime); + } else if (sound._node && !sound._muted) { + const volumeMultiplierOrGlobal = Howler.volume(); + if (typeof volumeMultiplierOrGlobal === 'number') { + sound._node.volume = volume * volumeMultiplierOrGlobal; + } + } + + self._emit('volume', sound._id); + } + } + } else { + sound = id ? self._soundById(id) : self._sounds[0]; + return sound ? sound._volume : 0; + } + + return self; + } + + fade(from: number, to: number, len: number, id?: number): Howl { + const self = this; + + if (self._state !== 'loaded' || self._playLock) { + self._queue.push({ + event: 'fade', + action: () => { + self.fade(from, to, len, id); + } + }); + + return self; + } + + from = Math.min(Math.max(0, parseFloat(from as any)), 1); + to = Math.min(Math.max(0, parseFloat(to as any)), 1); + len = parseFloat(len as any); + + self.volume(from, id); + + const ids = self._getSoundIds(id); + for (let i = 0; i < ids.length; i++) { + const sound = self._soundById(ids[i]); + + if (sound) { + if (!id) { + self._stopFade(ids[i]); + } + + if (self._webAudio && !sound._muted) { + const currentTime = Howler.ctx!.currentTime; + const end = currentTime + len / 1000; + sound._volume = from; + sound._node.gain.setValueAtTime(from, currentTime); + sound._node.gain.linearRampToValueAtTime(to, end); + } + + self._startFadeInterval(sound, from, to, len, ids[i], typeof id === 'undefined'); + } + } + + return self; + } + + _startFadeInterval(sound: Sound, from: number, to: number, len: number, id: number, isGroup: boolean): void { + const self = this; + let vol = from; + const diff = to - from; + const steps = Math.abs(diff / 0.01); + const stepLen = Math.max(4, steps > 0 ? len / steps : len); + let lastTick = Date.now(); + + sound._fadeTo = to; + + sound._interval = setInterval(() => { + const 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 (self._webAudio) { + sound._volume = vol; + } else { + self.volume(vol, sound._id, true); + } + + if (isGroup) { + self._volume = vol; + } + + if ((to < from && vol <= to) || (to > from && vol >= to)) { + clearInterval(sound._interval as any); + sound._interval = undefined; + sound._fadeTo = undefined; + self.volume(to, sound._id); + self._emit('fade', sound._id); + } + }, stepLen); + } + + _stopFade(id: number): Howl { + const self = this; + const sound = self._soundById(id); + + if (sound && sound._interval) { + if (self._webAudio) { + sound._node.gain.cancelScheduledValues(Howler.ctx!.currentTime); + } + + clearInterval(sound._interval as any); + sound._interval = undefined; + self.volume(sound._fadeTo as number, id); + sound._fadeTo = undefined; + self._emit('fade', id); + } + + return self; + } + + loop(): boolean; + loop(loop: boolean): Howl; + loop(loop?: boolean): boolean | Howl { + const self = this; + const args = arguments; + let loopVal: boolean | undefined; + let id: number | undefined; + let sound: Sound | null = null; + + if (args.length === 0) { + return self._loop; + } else if (args.length === 1) { + if (typeof args[0] === 'boolean') { + loopVal = args[0] as boolean; + self._loop = loopVal; + } else { + sound = self._soundById(parseInt(args[0] as any, 10)); + return sound ? sound._loop : false; + } + } else if (args.length === 2) { + loopVal = args[0] as boolean; + id = parseInt(args[1] as any, 10); + } + + const ids = self._getSoundIds(id); + for (let i = 0; i < ids.length; i++) { + sound = self._soundById(ids[i]); + + if (sound) { + sound._loop = loopVal as boolean; + if (self._webAudio && sound._node && (sound._node.bufferSource as any)) { + (sound._node.bufferSource as any).loop = loopVal; + if (loopVal) { + (sound._node.bufferSource as any).loopStart = sound._start || 0; + (sound._node.bufferSource as any).loopEnd = sound._stop; + + if (self.playing(ids[i])) { + self.pause(ids[i], true); + self.play(ids[i], true); + } + } + } + } + } + + return self; + } + + rate(): number; + rate(rate: number): Howl; + rate(rate?: number): number | Howl { + const self = this; + const args = arguments; + let rateVal: number | undefined; + let id: number | undefined; + + if (args.length === 0) { + id = self._sounds[0]._id; + } else if (args.length === 1) { + const ids = self._getSoundIds(); + const index = ids.indexOf(args[0] as any); + if (index >= 0) { + id = parseInt(args[0] as any, 10); + } else { + rateVal = parseFloat(args[0] as any); + } + } else if (args.length === 2) { + rateVal = parseFloat(args[0] as any); + id = parseInt(args[1] as any, 10); + } + + let sound; + if (typeof rateVal === 'number') { + if (self._state !== 'loaded' || self._playLock) { + self._queue.push({ + event: 'rate', + action: () => { + self.rate.apply(self, args as any); + } + }); + + return self; + } + + if (typeof id === 'undefined') { + self._rate = rateVal; + } + + id = self._getSoundIds(id); + for (let i = 0; i < (id as any).length; i++) { + sound = self._soundById((id as any)[i]); + + if (sound) { + if (self.playing((id as any)[i])) { + sound._rateSeek = self.seek((id as any)[i]) as number; + sound._playStart = self._webAudio ? Howler.ctx!.currentTime : sound._playStart; + } + sound._rate = rateVal; + + if (self._webAudio && sound._node && (sound._node.bufferSource as any)) { + (sound._node.bufferSource as any).playbackRate.setValueAtTime(rateVal, Howler.ctx!.currentTime); + } else if (sound._node) { + sound._node.playbackRate = rateVal; + } + + const seek = self.seek((id as any)[i]) as number; + const duration = (self._sprite[sound._sprite][0] + self._sprite[sound._sprite][1]) / 1000 - seek; + const timeout = (duration * 1000) / Math.abs(sound._rate); + + if (self._endTimers[(id as any)[i]] || !sound._paused) { + self._clearTimer((id as any)[i]); + self._endTimers[(id as any)[i]] = setTimeout(self._ended.bind(self, sound), timeout); + } + + self._emit('rate', sound._id); + } + } + } else { + sound = self._soundById(id); + return sound ? sound._rate : self._rate; + } + + return self; + } + + seek(): number; + seek(seek: number): Howl; + seek(seek?: number): number | Howl { + const self = this; + const args = arguments; + let seekVal: number | undefined; + let id: number | undefined; + + if (args.length === 0) { + if (self._sounds.length) { + id = self._sounds[0]._id; + } + } else if (args.length === 1) { + const ids = self._getSoundIds(); + const index = ids.indexOf(args[0] as any); + if (index >= 0) { + id = parseInt(args[0] as any, 10); + } else if (self._sounds.length) { + id = self._sounds[0]._id; + seekVal = parseFloat(args[0] as any); + } + } else if (args.length === 2) { + seekVal = parseFloat(args[0] as any); + id = parseInt(args[1] as any, 10); + } + + if (typeof id === 'undefined') { + return 0; + } + + if (typeof seekVal === 'number' && (self._state !== 'loaded' || self._playLock)) { + self._queue.push({ + event: 'seek', + action: () => { + self.seek.apply(self, args as any); + } + }); + + return self; + } + + const sound = self._soundById(id); + + if (sound) { + if (typeof seekVal === 'number' && seekVal >= 0) { + const playing = self.playing(id); + if (playing) { + self.pause(id, true); + } + + sound._seek = seekVal; + sound._ended = false; + self._clearTimer(id); + + if (!self._webAudio && sound._node && !isNaN(sound._node.duration)) { + sound._node.currentTime = seekVal; + } + + const seekAndEmit = () => { + if (playing) { + self.play(id, true); + } + + self._emit('seek', id); + }; + + if (playing && !self._webAudio) { + const emitSeek = () => { + if (!self._playLock) { + seekAndEmit(); + } else { + setTimeout(emitSeek, 0); + } + }; + setTimeout(emitSeek, 0); + } else { + seekAndEmit(); + } + } else { + if (self._webAudio) { + const realTime = self.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 self; + } + + playing(id?: number): boolean { + const self = this; + + if (typeof id === 'number') { + const sound = self._soundById(id); + return sound ? !sound._paused : false; + } + + for (let i = 0; i < self._sounds.length; i++) { + if (!self._sounds[i]._paused) { + return true; + } + } + + return false; + } + + duration(id?: number): number { + const self = this; + let duration = self._duration; + + const sound = self._soundById(id); + if (sound) { + duration = self._sprite[sound._sprite][1] / 1000; + } + + return duration; + } + + state(): string { + return this._state; + } + + unload(): null { + const self = this; + + // Execute plugin hooks before destruction + globalPluginManager.executeHowlDestroy(self); + + const sounds = self._sounds; + for (let i = 0; i < sounds.length; i++) { + if (!sounds[i]._paused) { + self.stop(sounds[i]._id); + } + + if (!self._webAudio) { + self._clearSound(sounds[i]._node); + + 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); + + Howler._releaseHtml5Audio(sounds[i]._node); + } + + delete sounds[i]._node; + + self._clearTimer(sounds[i]._id); + } + + const index = Howler._howls.indexOf(self); + if (index >= 0) { + Howler._howls.splice(index, 1); + } + + let remCache = true; + for (let i = 0; i < Howler._howls.length; i++) { + if (Howler._howls[i]._src === self._src || (self._src as string).indexOf(Howler._howls[i]._src as string) >= 0) { + remCache = false; + break; + } + } + + if (cache && remCache) { + delete cache[self._src as string]; + } + + Howler.noAudio = false; + + self._state = 'unloaded'; + self._sounds = []; + + return null; + } + + on(event: string, fn: (...args: any[]) => void, id?: number, once?: boolean): Howl { + const self = this; + const events = (self as any)['_on' + event]; + + if (typeof fn === 'function') { + events.push(once ? { id, fn, once } : { id, fn }); + } + + return self; + } + + off(event: string, fn?: (...args: any[]) => void, id?: number): Howl { + const self = this; + const events = (self as any)['_on' + event]; + let i = 0; + + if (typeof fn === 'number') { + id = fn; + fn = undefined; + } + + if (fn || id) { + for (i = 0; i < events.length; i++) { + const isId = id === events[i].id; + if ((fn === events[i].fn && isId) || (!fn && isId)) { + events.splice(i, 1); + break; + } + } + } else if (event) { + (self as any)['_on' + event] = []; + } else { + const keys = Object.keys(self); + for (i = 0; i < keys.length; i++) { + if (keys[i].indexOf('_on') === 0 && Array.isArray((self as any)[keys[i]])) { + (self as any)[keys[i]] = []; + } + } + } + + return self; + } + + once(event: string, fn: (...args: any[]) => void, id?: number): Howl { + const self = this; + + self.on(event, fn, id, true); + + return self; + } + + _emit(event: string, id?: number | null, msg?: string): Howl { + const self = this; + const events = (self as any)['_on' + event]; + + for (let i = events.length - 1; i >= 0; i--) { + 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 (events[i].once) { + self.off(event, events[i].fn, events[i].id); + } + } + } + + self._loadQueue(event); + + return self; + } + + _loadQueue(event?: string): Howl { + const self = this; + + if (self._queue.length > 0) { + const task = self._queue[0]; + + if (task.event === event) { + self._queue.shift(); + self._loadQueue(); + } + + if (!event) { + task.action(); + } + } + + return self; + } + + _ended(sound: Sound): Howl { + const self = this; + const sprite = sound._sprite; + + 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; + } + + const loop = !!(sound._loop || self._sprite[sprite][2]); + + self._emit('end', sound._id); + + if (!self._webAudio && loop) { + self.stop(sound._id, true).play(sound._id); + } + + if (self._webAudio && loop) { + self._emit('play', sound._id); + sound._seek = sound._start || 0; + sound._rateSeek = 0; + sound._playStart = Howler.ctx!.currentTime; + + const timeout = ((sound._stop! - (sound._start || 0)) * 1000) / Math.abs(sound._rate); + self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout); + } + + if (self._webAudio && !loop) { + sound._paused = true; + sound._ended = true; + sound._seek = sound._start || 0; + sound._rateSeek = 0; + self._clearTimer(sound._id); + + self._cleanBuffer(sound._node); + + Howler._autoSuspend(); + } + + if (!self._webAudio && !loop) { + self.stop(sound._id, true); + } + + return self; + } + + _clearTimer(id: number): Howl { + const self = this; + + if (self._endTimers[id]) { + if (typeof self._endTimers[id] !== 'function') { + clearTimeout(self._endTimers[id]); + } else { + const sound = self._soundById(id); + if (sound && sound._node) { + sound._node.removeEventListener('ended', self._endTimers[id], false); + } + } + + delete self._endTimers[id]; + } + + return self; + } + + _soundById(id: number): Sound | null { + const self = this; + + for (let i = 0; i < self._sounds.length; i++) { + if (id === self._sounds[i]._id) { + return self._sounds[i]; + } + } + + return null; + } + + _inactiveSound(): Sound { + const self = this; + + self._drain(); + + for (let i = 0; i < self._sounds.length; i++) { + if (self._sounds[i]._ended) { + return self._sounds[i].reset(); + } + } + + return new Sound(self); + } + + _drain(): void { + const self = this; + const limit = self._pool; + let cnt = 0; + + if (self._sounds.length < limit) { + return; + } + + for (let i = 0; i < self._sounds.length; i++) { + if (self._sounds[i]._ended) { + cnt++; + } + } + + for (let i = self._sounds.length - 1; i >= 0; i--) { + if (cnt <= limit) { + return; + } + + if (self._sounds[i]._ended) { + if (self._webAudio && self._sounds[i]._node) { + self._sounds[i]._node.disconnect(0); + } + + self._sounds.splice(i, 1); + cnt--; + } + } + } + + _getSoundIds(id?: number): number[] { + const self = this; + + if (typeof id === 'undefined') { + const ids: number[] = []; + for (let i = 0; i < self._sounds.length; i++) { + ids.push(self._sounds[i]._id); + } + + return ids; + } else { + return [id]; + } + } + + _refreshBuffer(sound: Sound): Howl { + const self = this; + + sound._node.bufferSource = Howler.ctx!.createBufferSource(); + sound._node.bufferSource.buffer = cache[self._src as string]; + + 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.ctx!.currentTime); + + return self; + } + + _cleanBuffer(node: any): Howl { + const self = this; + const isIOS = Howler._navigator && Howler._navigator.vendor.indexOf('Apple') >= 0; + + if (!node.bufferSource) { + return self; + } + + if (Howler._scratchBuffer && node.bufferSource) { + node.bufferSource.onended = null; + node.bufferSource.disconnect(0); + if (isIOS) { + try { + node.bufferSource.buffer = Howler._scratchBuffer; + } catch (e) {} + } + } + node.bufferSource = null; + + return self; + } + + _clearSound(node: HTMLAudioElement): void { + const checkIE = /MSIE |Trident\//.test((Howler._navigator && Howler._navigator.userAgent) || ''); + if (!checkIE) { + node.src = 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA'; + } + } +} + +// Export for ESM +export * from './types'; +export { Howler, Howl, Sound }; +export default { Howler, Howl, Sound }; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..041c2c46 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,12 @@ +/** + * Howler.js - Javascript Audio Library + * Main entry point for the library + */ + +// Core library exports +export { Howler, Howl, Sound } from './howler.core'; +export type { HowlOptions } from './types'; + +// Plugin system exports +export { PluginManager, HowlerPlugin, globalPluginManager } from './plugins'; +export type { PluginHooks } from './plugins'; diff --git a/src/plugins/howler.spatial.js b/src/plugins/howler.spatial.js deleted file mode 100644 index 6ae0fdcd..00000000 --- a/src/plugins/howler.spatial.js +++ /dev/null @@ -1,659 +0,0 @@ -/*! - * Spatial Plugin - Adds support for stereo and 3D audio where Web Audio is supported. - * - * howler.js v2.2.4 - * 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 void; + + /** + * Called when Howler global instance is initialized + */ + onHowlerInit?: (howler: HowlerGlobal) => void; + + /** + * Called when a new Howl instance is created + */ + onHowlCreate?: (howl: Howl, options: HowlOptions) => void; + + /** + * Called when a Sound instance is created + */ + onSoundCreate?: (sound: Sound, parent: Howl) => void; + + /** + * Called when a Howl instance is loaded + */ + onHowlLoad?: (howl: Howl) => void; + + /** + * Called when a Howl instance is destroyed + */ + onHowlDestroy?: (howl: Howl) => void; +} + +/** + * Base plugin class + * Extend this class to create a custom Howler plugin + */ +export abstract class HowlerPlugin { + /** + * Unique plugin name (must be unique across all plugins) + */ + abstract readonly name: string; + + /** + * Plugin version (optional) + */ + readonly version?: string; + + /** + * Get the hooks provided by this plugin + */ + abstract getHooks(): PluginHooks; + + /** + * Called when plugin is about to be unregistered + * Use this to clean up resources + */ + onUnregister?(): void; +} + +/** + * Plugin registration metadata + */ +export interface RegisteredPlugin { + plugin: HowlerPlugin; + hooks: PluginHooks; + enabled: boolean; +} + +/** + * Plugin manager for registering and executing plugins + */ +export class PluginManager { + private plugins: Map = new Map(); + private hookHistory: Map = new Map(); + + /** + * Register a plugin + * @param plugin - The plugin to register + * @throws Error if plugin name already exists + */ + register(plugin: HowlerPlugin): void { + if (this.plugins.has(plugin.name)) { + throw new Error(`Plugin "${plugin.name}" is already registered`); + } + + const hooks = plugin.getHooks(); + const registered: RegisteredPlugin = { + plugin, + hooks, + enabled: true + }; + + this.plugins.set(plugin.name, registered); + + // Execute onRegister hook if provided + if (hooks.onRegister) { + try { + hooks.onRegister(); + } catch (error) { + console.error(`Error during onRegister for plugin "${plugin.name}":`, error); + } + } + } + + /** + * Unregister a plugin + * @param pluginName - The name of the plugin to unregister + */ + unregister(pluginName: string): void { + const registered = this.plugins.get(pluginName); + if (!registered) { + throw new Error(`Plugin "${pluginName}" is not registered`); + } + + // Call cleanup hook + if (registered.plugin.onUnregister) { + try { + registered.plugin.onUnregister(); + } catch (error) { + console.error(`Error during onUnregister for plugin "${pluginName}":`, error); + } + } + + this.plugins.delete(pluginName); + } + + /** + * Check if a plugin is registered + * @param pluginName - The name of the plugin + */ + isRegistered(pluginName: string): boolean { + return this.plugins.has(pluginName); + } + + /** + * Enable a plugin + * @param pluginName - The name of the plugin to enable + */ + enable(pluginName: string): void { + const registered = this.plugins.get(pluginName); + if (!registered) { + throw new Error(`Plugin "${pluginName}" is not registered`); + } + registered.enabled = true; + } + + /** + * Disable a plugin (keeps it registered but doesn't execute hooks) + * @param pluginName - The name of the plugin to disable + */ + disable(pluginName: string): void { + const registered = this.plugins.get(pluginName); + if (!registered) { + throw new Error(`Plugin "${pluginName}" is not registered`); + } + registered.enabled = false; + } + + /** + * Get all registered plugins + */ + getPlugins(): ReadonlyMap { + return new Map(this.plugins); + } + + /** + * Execute onHowlerInit hooks + */ + executeHowlerInit(howler: HowlerGlobal): void { + this._executeHooks('onHowlerInit', (hooks) => { + if (hooks.onHowlerInit) { + hooks.onHowlerInit(howler); + } + }); + } + + /** + * Execute onHowlCreate hooks + */ + executeHowlCreate(howl: Howl, options: HowlOptions): void { + this._executeHooks('onHowlCreate', (hooks) => { + if (hooks.onHowlCreate) { + hooks.onHowlCreate(howl, options); + } + }); + } + + /** + * Execute onSoundCreate hooks + */ + executeSoundCreate(sound: Sound, parent: Howl): void { + this._executeHooks('onSoundCreate', (hooks) => { + if (hooks.onSoundCreate) { + hooks.onSoundCreate(sound, parent); + } + }); + } + + /** + * Execute onHowlLoad hooks + */ + executeHowlLoad(howl: Howl): void { + this._executeHooks('onHowlLoad', (hooks) => { + if (hooks.onHowlLoad) { + hooks.onHowlLoad(howl); + } + }); + } + + /** + * Execute onHowlDestroy hooks + */ + executeHowlDestroy(howl: Howl): void { + this._executeHooks('onHowlDestroy', (hooks) => { + if (hooks.onHowlDestroy) { + hooks.onHowlDestroy(howl); + } + }); + } + + /** + * Internal hook execution with error handling + */ + private _executeHooks(hookName: string, callback: (hooks: PluginHooks) => void): void { + for (const [pluginName, registered] of this.plugins) { + if (!registered.enabled) { + continue; + } + + try { + callback(registered.hooks); + } catch (error) { + console.error(`Error in hook "${hookName}" for plugin "${pluginName}":`, error); + } + } + } +} + +/** + * Global plugin manager instance + */ +export const globalPluginManager = new PluginManager(); diff --git a/src/plugins/spatial-plugin.ts b/src/plugins/spatial-plugin.ts new file mode 100644 index 00000000..c90103e1 --- /dev/null +++ b/src/plugins/spatial-plugin.ts @@ -0,0 +1,273 @@ +/*! + * Spatial Plugin for Howler.js + * Adds 3D spatial audio and stereo panning support + * howlerjs.com + * + * (c) 2013-2020, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */ + +import { HowlerPlugin, type PluginHooks } from './plugin'; +import { Howl, HowlerGlobal, Sound } from '../howler.core'; +import type { HowlOptions } from '../howler.core'; + +/** + * Extended Howler class with spatial audio support + */ +export class SpatialHowler extends (HowlerGlobal as any) { + _pos: [number, number, number] = [0, 0, 0]; + _orientation: [number, number, number, number, number, number] = [0, 0, -1, 0, 1, 0]; + + stereo(pan?: number): this | number { + if (!this.ctx || !this.ctx.listener) { + console.warn('Spatial audio unavailable: Web Audio API not supported'); + return this; + } + + if (typeof pan === 'number') { + for (let i = 0; i < this._howls.length; i++) { + (this._howls[i] as any).stereo?.(pan); + } + return this; + } + + return (this as any)._stereo ?? 0; + } + + pos(x?: number, y?: number, z?: number): this | [number, number, number] { + if (!this.ctx || !this.ctx.listener) { + console.warn('Spatial audio unavailable: Web Audio API not supported'); + return this; + } + + if (typeof x === 'number') { + const y_val = typeof y === 'number' ? y : this._pos[1]; + const z_val = typeof z === 'number' ? z : this._pos[2]; + this._pos = [x, y_val, z_val]; + + // Set listener position using appropriate API + if (typeof this.ctx.listener.positionX !== 'undefined') { + this.ctx.listener.positionX.setTargetAtTime(x, this.ctx.currentTime, 0.1); + this.ctx.listener.positionY.setTargetAtTime(y_val, this.ctx.currentTime, 0.1); + this.ctx.listener.positionZ.setTargetAtTime(z_val, this.ctx.currentTime, 0.1); + } else { + (this.ctx.listener as any).setPosition(x, y_val, z_val); + } + + return this; + } + + return this._pos; + } + + orientation( + x?: number, + y?: number, + z?: number, + xUp?: number, + yUp?: number, + zUp?: number + ): this | [number, number, number, number, number, number] { + if (!this.ctx || !this.ctx.listener) { + console.warn('Spatial audio unavailable: Web Audio API not supported'); + return this; + } + + if (typeof x === 'number') { + const or = this._orientation; + const y_val = typeof y === 'number' ? y : or[1]; + const z_val = typeof z === 'number' ? z : or[2]; + const xUp_val = typeof xUp === 'number' ? xUp : or[3]; + const yUp_val = typeof yUp === 'number' ? yUp : or[4]; + const zUp_val = typeof zUp === 'number' ? zUp : or[5]; + this._orientation = [x, y_val, z_val, xUp_val, yUp_val, zUp_val]; + + // Set listener orientation using appropriate API + if (typeof this.ctx.listener.forwardX !== 'undefined') { + this.ctx.listener.forwardX.setTargetAtTime(x, this.ctx.currentTime, 0.1); + this.ctx.listener.forwardY.setTargetAtTime(y_val, this.ctx.currentTime, 0.1); + this.ctx.listener.forwardZ.setTargetAtTime(z_val, this.ctx.currentTime, 0.1); + this.ctx.listener.upX.setTargetAtTime(xUp_val, this.ctx.currentTime, 0.1); + this.ctx.listener.upY.setTargetAtTime(yUp_val, this.ctx.currentTime, 0.1); + this.ctx.listener.upZ.setTargetAtTime(zUp_val, this.ctx.currentTime, 0.1); + } else { + (this.ctx.listener as any).setOrientation(x, y_val, z_val, xUp_val, yUp_val, zUp_val); + } + + return this; + } + + return this._orientation; + } +} + +/** + * Extended Howl class with spatial audio support + */ +export class SpatialHowl extends Howl { + _orientation: [number, number, number] = [1, 0, 0]; + _stereo: number | null = null; + _pos: [number, number, number] | null = null; + _pannerAttr: { + coneInnerAngle: number; + coneOuterAngle: number; + coneOuterGain: number; + distanceModel: string; + maxDistance: number; + refDistance: number; + rolloffFactor: number; + panningModel: string; + } = { + coneInnerAngle: 360, + coneOuterAngle: 360, + coneOuterGain: 0, + distanceModel: 'inverse', + maxDistance: 10000, + refDistance: 1, + rolloffFactor: 1, + panningModel: 'HRTF', + }; + + constructor(o: HowlOptions & any) { + super(o); + // Initialize spatial properties from options + this._orientation = o.orientation || [1, 0, 0]; + this._stereo = o.stereo || null; + this._pos = o.pos || null; + this._pannerAttr = { + coneInnerAngle: o.coneInnerAngle ?? 360, + coneOuterAngle: o.coneOuterAngle ?? 360, + coneOuterGain: o.coneOuterGain ?? 0, + distanceModel: o.distanceModel ?? 'inverse', + maxDistance: o.maxDistance ?? 10000, + refDistance: o.refDistance ?? 1, + rolloffFactor: o.rolloffFactor ?? 1, + panningModel: o.panningModel ?? 'HRTF', + }; + } + + stereo(pan?: number, id?: number): this | number { + if (typeof pan === 'number') { + this._stereo = pan; + // Apply to all sounds + for (let i = 0; i < this._sounds.length; i++) { + (this._sounds[i] as any)._stereo = pan; + } + return this; + } + return this._stereo ?? 0; + } + + pos(x?: number, y?: number, z?: number, id?: number): this | [number, number, number] { + if (typeof x === 'number') { + const y_val = typeof y === 'number' ? y : (this._pos?.[1] ?? 0); + const z_val = typeof z === 'number' ? z : (this._pos?.[2] ?? 0); + this._pos = [x, y_val, z_val]; + return this; + } + return this._pos ?? [0, 0, 0]; + } + + orientation(x?: number, y?: number, z?: number, id?: number): this | [number, number, number] { + if (typeof x === 'number') { + const y_val = typeof y === 'number' ? y : this._orientation[1]; + const z_val = typeof z === 'number' ? z : this._orientation[2]; + this._orientation = [x, y_val, z_val]; + return this; + } + return this._orientation; + } + + pannerAttr(o?: any, id?: number): any { + if (o) { + Object.assign(this._pannerAttr, o); + return this; + } + return this._pannerAttr; + } +} + +/** + * Extended Sound class with spatial audio support + */ +export class SpatialSound extends Sound { + _orientation: [number, number, number] = [1, 0, 0]; + _stereo: number | null = null; + _pos: [number, number, number] | null = null; + _pannerAttr: { + coneInnerAngle: number; + coneOuterAngle: number; + coneOuterGain: number; + distanceModel: string; + maxDistance: number; + refDistance: number; + rolloffFactor: number; + panningModel: string; + } = { + coneInnerAngle: 360, + coneOuterAngle: 360, + coneOuterGain: 0, + distanceModel: 'inverse', + maxDistance: 10000, + refDistance: 1, + rolloffFactor: 1, + panningModel: 'HRTF', + }; + _panner?: PannerNode | StereoPannerNode; + + constructor(howl: Howl) { + super(howl); + // Inherit spatial properties from parent + if (howl instanceof SpatialHowl) { + this._orientation = howl._orientation; + this._stereo = howl._stereo; + this._pos = howl._pos; + this._pannerAttr = howl._pannerAttr; + } + } +} + +/** + * Spatial Audio Plugin + * Adds 3D spatial audio and stereo panning capabilities to Howler + * + * Usage: + * ```typescript + * import { globalPluginManager } from 'howler/plugins'; + * import { SpatialAudioPlugin } from 'howler/plugins/spatial'; + * + * globalPluginManager.register(new SpatialAudioPlugin()); + * + * // Then use the spatial classes for your audio objects: + * const sound = new SpatialHowl({ src: 'audio.mp3' }); + * sound.pos(10, 20, 30); + * sound.stereo(0.5); + * ``` + */ +export class SpatialAudioPlugin extends HowlerPlugin { + readonly name = 'spatial-audio'; + readonly version = '1.0.0'; + + getHooks(): PluginHooks { + return { + onHowlerInit: this.onHowlerInit.bind(this), + }; + } + + /** + * Initialize spatial audio global state when Howler is initialized. + * Note: Users should instantiate SpatialHowl and SpatialSound directly + * instead of using the base Howl and Sound classes when they need spatial features. + */ + private onHowlerInit(howler: HowlerGlobal): void { + console.info( + 'Spatial Audio Plugin registered. Use SpatialHowl and SpatialSound classes for spatial audio features.' + ); + } + + onUnregister(): void { + // Cleanup if needed + } +} diff --git a/src/plugins/spatial.ts b/src/plugins/spatial.ts new file mode 100644 index 00000000..98715ad7 --- /dev/null +++ b/src/plugins/spatial.ts @@ -0,0 +1,6 @@ +/** + * Howler.js - Spatial Plugin Entry Point + */ + + // Spatial Audio Plugin + export { SpatialAudioPlugin, SpatialHowler, SpatialHowl, SpatialSound } from './spatial-plugin'; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..043b38ad --- /dev/null +++ b/src/types.ts @@ -0,0 +1,55 @@ +/*! + * Howler.js Type Definitions + * howlerjs.com + * + * (c) 2013-2020, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */ + +export interface HowlOptions { + src: string | string[]; + autoplay?: boolean; + format?: string | string[]; + html5?: boolean; + mute?: boolean; + loop?: boolean; + pool?: number; + preload?: boolean | 'metadata'; + rate?: number; + sprite?: Record; + volume?: number; + xhr?: { + method?: string; + headers?: Record; + withCredentials?: boolean; + }; + onend?: () => void; + onfade?: () => void; + onload?: () => void; + onloaderror?: (id: number, msg: string) => void; + onplayerror?: (id: number, msg: string) => void; + onpause?: () => void; + onplay?: () => void; + onstop?: () => void; + onmute?: () => void; + onvolume?: () => void; + onrate?: () => void; + onseek?: () => void; + onunlock?: () => void; +} + +export interface EventListener { + id?: number; + fn: (...args: any[]) => void; + once?: boolean; +} + +export interface QueueItem { + event: string; + action: () => void; +} + +// Global audio context cache +export const cache: Record = {}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..82bd4be1 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "allowJs": false + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/tsdown.config.ts b/tsdown.config.ts new file mode 100644 index 00000000..77ebf75b --- /dev/null +++ b/tsdown.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'tsdown'; + +export default defineConfig({ + entry: 'src/**/*.ts', + format: 'esm', + platform: 'browser', + unbundle: true, + treeshake: true, + minify: true, +}); From a4cd9384e956ac4067202303b369e8be455f9e79 Mon Sep 17 00:00:00 2001 From: eatsjobs Date: Sat, 22 Nov 2025 02:56:25 +0100 Subject: [PATCH 04/25] migrate tests and samples --- examples/3d/index.html | 20 ++-- examples/3d/js/camera.js | 2 +- examples/3d/js/controls.js | 2 +- examples/3d/js/game.js | 10 +- examples/3d/js/map.js | 2 +- examples/3d/js/player.js | 2 +- examples/3d/js/sound.js | 8 +- examples/3d/js/texture.js | 2 +- examples/player/index.html | 96 +++++++-------- examples/player/player.js | 5 +- examples/player/siriwave.js | 225 ++++++++++++++++++------------------ examples/radio/index.html | 167 +++++++++++++------------- examples/radio/radio.js | 4 +- examples/sprite/index.html | 89 +++++++------- examples/sprite/sprite.js | 4 +- tests/core.html5audio.html | 37 +++--- tests/core.webaudio.html | 35 +++--- tests/js/core.html5audio.js | 10 +- tests/js/core.webaudio.js | 10 +- tests/js/spatial.js | 17 ++- tests/spatial.html | 12 +- 21 files changed, 412 insertions(+), 347 deletions(-) diff --git a/examples/3d/index.html b/examples/3d/index.html index e29a57b5..8c524c5f 100644 --- a/examples/3d/index.html +++ b/examples/3d/index.html @@ -5,20 +5,20 @@ Howler.js 3D Spatial Audio + - - - - - - - - - + - \ No newline at end of file + diff --git a/examples/3d/js/camera.js b/examples/3d/js/camera.js index c3e83ce7..39a97718 100644 --- a/examples/3d/js/camera.js +++ b/examples/3d/js/camera.js @@ -14,7 +14,7 @@ * Camera that draws everything you see on the screen from the player's perspective. * @param {Number} resolution Resolution to render at (higher has better quality, but lower performance). */ -var Camera = function(resolution) { +export var Camera = function(resolution) { this.width = canvas.width = window.innerWidth; this.height = canvas.height = window.innerHeight; this.resolution = resolution; diff --git a/examples/3d/js/controls.js b/examples/3d/js/controls.js index 9e2ace9f..fac5f3ef 100644 --- a/examples/3d/js/controls.js +++ b/examples/3d/js/controls.js @@ -13,7 +13,7 @@ /** * Defines and handles the various controls. */ -var Controls = function() { +export var Controls = function() { // Define our control key codes and states. this.codes = { // Arrows diff --git a/examples/3d/js/game.js b/examples/3d/js/game.js index 15742902..c7a2deb3 100644 --- a/examples/3d/js/game.js +++ b/examples/3d/js/game.js @@ -10,6 +10,12 @@ 'use strict'; +import { Sound } from './sound.js'; +import { Player } from './player.js'; +import { Controls } from './controls.js'; +import { Map } from './map.js'; +import { Camera } from './camera.js'; + // Cache some commonly used values. var circle = Math.PI * 2; var isMobile = /iPhone|iPad|iPod|Android|BlackBerry|BB10|Silk/i.test(navigator.userAgent); @@ -28,7 +34,7 @@ var Game = function() { this.controls = new Controls(); this.map = new Map(25); this.camera = new Camera(isMobile ? 256 : 512); - + requestAnimationFrame(this.tick.bind(this)); }; Game.prototype = { @@ -54,4 +60,4 @@ Game.prototype = { var game = new Game(); // Generate the new map. -game.map.setup(); \ No newline at end of file +game.map.setup(); diff --git a/examples/3d/js/map.js b/examples/3d/js/map.js index 8bbe6710..6d3faaac 100644 --- a/examples/3d/js/map.js +++ b/examples/3d/js/map.js @@ -14,7 +14,7 @@ * Generates the map and calculates the casting of arrays for the camera to display on screen. * @param {Number} size Grid size of the map to use. */ -var Map = function(size) { +export var Map = function(size) { this.size = size; this.grid = new Array(size * size); this.skybox = new Texture('./assets/skybox.jpg', 4096, 1024); diff --git a/examples/3d/js/player.js b/examples/3d/js/player.js index 38f07790..5a812ec4 100644 --- a/examples/3d/js/player.js +++ b/examples/3d/js/player.js @@ -17,7 +17,7 @@ * @param {Number} dir Direction they are facing in radians. * @param {Number} speed Speed they walk at. */ -var Player = function(x, y, dir, speed) { +export var Player = function(x, y, dir, speed) { this.x = x; this.y = y; this.dir = dir; diff --git a/examples/3d/js/sound.js b/examples/3d/js/sound.js index 876f0e48..37ead799 100644 --- a/examples/3d/js/sound.js +++ b/examples/3d/js/sound.js @@ -10,12 +10,14 @@ 'use strict'; +import { SpatialHowl } from 'howler/plugins/spatial'; + /** * Setup and control all of the game's audio. */ -var Sound = function() { - // Setup the shared Howl. - this.sound = new Howl({ +export var Sound = function() { + // Setup the shared SpatialHowl. + this.sound = new SpatialHowl({ src: ['./assets/sprite.webm', './assets/sprite.mp3'], sprite: { lightning: [2000, 4147], diff --git a/examples/3d/js/texture.js b/examples/3d/js/texture.js index 09f10c25..656ec9bc 100644 --- a/examples/3d/js/texture.js +++ b/examples/3d/js/texture.js @@ -16,7 +16,7 @@ * @param {Number} w Image width. * @param {Number} h Image height. */ -var Texture = function(src, w, h) { +export var Texture = function(src, w, h) { this.image = new Image(); this.image.src = src; this.width = w; diff --git a/examples/player/index.html b/examples/player/index.html index 96307658..520b38cd 100644 --- a/examples/player/index.html +++ b/examples/player/index.html @@ -1,52 +1,58 @@ - - - - Howler.js Audio Player - - - - -
- -
0:00
-
0:00
-
+ + + + Howler.js Audio Player + + + + + +
+ +
0:00
+
0:00
+
- -
-
-
-
-
-
-
-
-
-
-
+ +
+
+
+
+
+
+
+
+
+
+
- -
-
-
+ +
+
+
- -
-
-
+ +
+
+
- -
-
-
-
-
+ +
+
+
+
+
- - - - - - \ No newline at end of file + + + + diff --git a/examples/player/player.js b/examples/player/player.js index 028529d8..989bf403 100644 --- a/examples/player/player.js +++ b/examples/player/player.js @@ -8,8 +8,11 @@ * MIT License */ +import { Howl } from 'howler'; +import { SiriWave } from './siriwave.js'; + // Cache references to DOM elements. -var elms = ['track', 'timer', 'duration', 'playBtn', 'pauseBtn', 'prevBtn', 'nextBtn', 'playlistBtn', 'volumeBtn', 'progress', 'bar', 'wave', 'loading', 'playlist', 'list', 'volume', 'barEmpty', 'barFull', 'sliderBtn']; +const elms = ['track', 'timer', 'duration', 'playBtn', 'pauseBtn', 'prevBtn', 'nextBtn', 'playlistBtn', 'volumeBtn', 'progress', 'bar', 'wave', 'loading', 'playlist', 'list', 'volume', 'barEmpty', 'barFull', 'sliderBtn']; elms.forEach(function(elm) { window[elm] = document.getElementById(elm); }); diff --git a/examples/player/siriwave.js b/examples/player/siriwave.js index 7a691f85..552b601e 100644 --- a/examples/player/siriwave.js +++ b/examples/player/siriwave.js @@ -1,148 +1,147 @@ /* Modified from https://github.com/CaffeinaLab/SiriWaveJS */ -(function() { +export class SiriWave { + constructor(opt) { + opt = opt || {}; -function SiriWave(opt) { - opt = opt || {}; + this.phase = 0; + this.run = false; - this.phase = 0; - this.run = false; + // UI vars - // UI vars + this.ratio = opt.ratio || window.devicePixelRatio || 1; - this.ratio = opt.ratio || window.devicePixelRatio || 1; + this.width = this.ratio * (opt.width || 320); + this.width_2 = this.width / 2; + this.width_4 = this.width / 4; - this.width = this.ratio * (opt.width || 320); - this.width_2 = this.width / 2; - this.width_4 = this.width / 4; + this.height = this.ratio * (opt.height || 100); + this.height_2 = this.height / 2; - this.height = this.ratio * (opt.height || 100); - this.height_2 = this.height / 2; + this.MAX = (this.height_2) - 4; - this.MAX = (this.height_2) - 4; + // Constructor opt - // Constructor opt + this.amplitude = opt.amplitude || 1; + this.speed = opt.speed || 0.2; + this.frequency = opt.frequency || 6; + this.color = (() => { + const hex2rgb = (hex) => { + const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; + hex = hex.replace(shorthandRegex, (m,r,g,b) => r + r + g + g + b + b); + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? + parseInt(result[1],16).toString()+','+parseInt(result[2], 16).toString()+','+parseInt(result[3], 16).toString() + : null; + }; + return hex2rgb(opt.color || '#fff') || '255,255,255'; + })(); - this.amplitude = opt.amplitude || 1; - this.speed = opt.speed || 0.2; - this.frequency = opt.frequency || 6; - this.color = (function hex2rgb(hex){ - var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; - hex = hex.replace(shorthandRegex, function(m,r,g,b) { return r + r + g + g + b + b; }); - var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result ? - parseInt(result[1],16).toString()+','+parseInt(result[2], 16).toString()+','+parseInt(result[3], 16).toString() - : null; - })(opt.color || '#fff') || '255,255,255'; + // Canvas - // Canvas + this.canvas = document.createElement('canvas'); + this.canvas.width = this.width; + this.canvas.height = this.height; + if (opt.cover) { + this.canvas.style.width = this.canvas.style.height = '100%'; + } else { + this.canvas.style.width = (this.width / this.ratio) + 'px'; + this.canvas.style.height = (this.height / this.ratio) + 'px'; + } - this.canvas = document.createElement('canvas'); - this.canvas.width = this.width; - this.canvas.height = this.height; - if (opt.cover) { - this.canvas.style.width = this.canvas.style.height = '100%'; - } else { - this.canvas.style.width = (this.width / this.ratio) + 'px'; - this.canvas.style.height = (this.height / this.ratio) + 'px'; - }; + this.container = opt.container || document.body; + this.container.appendChild(this.canvas); - this.container = opt.container || document.body; - this.container.appendChild(this.canvas); + this.ctx = this.canvas.getContext('2d'); - this.ctx = this.canvas.getContext('2d'); + // Start - // Start + if (opt.autostart) { + this.start(); + } + } + + _GATF_cache = {}; - if (opt.autostart) { - this.start(); + _globAttFunc(x) { + if (this._GATF_cache[x] == null) { + this._GATF_cache[x] = Math.pow(4/(4+Math.pow(x,4)), 4); + } + return this._GATF_cache[x]; } -} -SiriWave.prototype._GATF_cache = {}; -SiriWave.prototype._globAttFunc = function(x) { - if (SiriWave.prototype._GATF_cache[x] == null) { - SiriWave.prototype._GATF_cache[x] = Math.pow(4/(4+Math.pow(x,4)), 4); + _xpos(i) { + return this.width_2 + i * this.width_4; } - return SiriWave.prototype._GATF_cache[x]; -}; - -SiriWave.prototype._xpos = function(i) { - return this.width_2 + i * this.width_4; -}; - -SiriWave.prototype._ypos = function(i, attenuation) { - var att = (this.MAX * this.amplitude) / attenuation; - return this.height_2 + this._globAttFunc(i) * att * Math.sin(this.frequency * i - this.phase); -}; - -SiriWave.prototype._drawLine = function(attenuation, color, width){ - this.ctx.moveTo(0,0); - this.ctx.beginPath(); - this.ctx.strokeStyle = color; - this.ctx.lineWidth = width || 1; - - var i = -2; - while ((i += 0.01) <= 2) { - var y = this._ypos(i, attenuation); - if (Math.abs(i) >= 1.90) y = this.height_2; - this.ctx.lineTo(this._xpos(i), y); + + _ypos(i, attenuation) { + const att = (this.MAX * this.amplitude) / attenuation; + return this.height_2 + this._globAttFunc(i) * att * Math.sin(this.frequency * i - this.phase); } - this.ctx.stroke(); -}; + _drawLine(attenuation, color, width) { + this.ctx.moveTo(0,0); + this.ctx.beginPath(); + this.ctx.strokeStyle = color; + this.ctx.lineWidth = width || 1; -SiriWave.prototype._clear = function() { - this.ctx.globalCompositeOperation = 'destination-out'; - this.ctx.fillRect(0, 0, this.width, this.height); - this.ctx.globalCompositeOperation = 'source-over'; -}; + let i = -2; + while ((i += 0.01) <= 2) { + const y = this._ypos(i, attenuation); + if (Math.abs(i) >= 1.90) y = this.height_2; + this.ctx.lineTo(this._xpos(i), y); + } -SiriWave.prototype._draw = function() { - if (this.run === false) return; + this.ctx.stroke(); + } - this.phase = (this.phase + Math.PI*this.speed) % (2*Math.PI); + _clear() { + this.ctx.globalCompositeOperation = 'destination-out'; + this.ctx.fillRect(0, 0, this.width, this.height); + this.ctx.globalCompositeOperation = 'source-over'; + } - this._clear(); - this._drawLine(-2, 'rgba(' + this.color + ',0.1)'); - this._drawLine(-6, 'rgba(' + this.color + ',0.2)'); - this._drawLine(4, 'rgba(' + this.color + ',0.4)'); - this._drawLine(2, 'rgba(' + this.color + ',0.6)'); - this._drawLine(1, 'rgba(' + this.color + ',1)', 1.5); + _draw() { + if (this.run === false) return; - if (window.requestAnimationFrame) { - requestAnimationFrame(this._draw.bind(this)); - return; - }; - setTimeout(this._draw.bind(this), 20); -}; + this.phase = (this.phase + Math.PI*this.speed) % (2*Math.PI); -/* API */ + this._clear(); + this._drawLine(-2, 'rgba(' + this.color + ',0.1)'); + this._drawLine(-6, 'rgba(' + this.color + ',0.2)'); + this._drawLine(4, 'rgba(' + this.color + ',0.4)'); + this._drawLine(2, 'rgba(' + this.color + ',0.6)'); + this._drawLine(1, 'rgba(' + this.color + ',1)', 1.5); -SiriWave.prototype.start = function() { - this.phase = 0; - this.run = true; - this._draw(); -}; + if (window.requestAnimationFrame) { + requestAnimationFrame(this._draw.bind(this)); + return; + } + setTimeout(this._draw.bind(this), 20); + } -SiriWave.prototype.stop = function() { - this.phase = 0; - this.run = false; -}; + /* API */ -SiriWave.prototype.setSpeed = function(v) { - this.speed = v; -}; + start() { + this.phase = 0; + this.run = true; + this._draw(); + } -SiriWave.prototype.setNoise = SiriWave.prototype.setAmplitude = function(v) { - this.amplitude = Math.max(Math.min(v, 1), 0); -}; + stop() { + this.phase = 0; + this.run = false; + } + setSpeed(v) { + this.speed = v; + } -if (typeof define === 'function' && define.amd) { - define(function(){ return SiriWave; }); - return; -}; -window.SiriWave = SiriWave; + setNoise(v) { + this.amplitude = Math.max(Math.min(v, 1), 0); + } -})(); \ No newline at end of file + setAmplitude(v) { + this.setNoise(v); + } +} diff --git a/examples/radio/index.html b/examples/radio/index.html index 38e696ce..0fdd5ee5 100644 --- a/examples/radio/index.html +++ b/examples/radio/index.html @@ -1,83 +1,90 @@ - - - - Howler.js Radio - - - - -
-
-
-
-
LIVE
-
-
-
-
-
-
-
-
-
-
-
-
-
LIVE
-
-
-
-
-
-
-
-
-
-
-
-
-
LIVE
-
-
-
-
-
-
-
-
-
-
-
-
-
LIVE
-
-
-
-
-
-
-
-
-
-
-
-
-
LIVE
-
-
-
-
-
-
-
-
-
-
+ + + + Howler.js Radio + + + + + +
+
+
+
+
LIVE
+
+
+
+
+
+
+
+
+
+
+
+
+
LIVE
+
+
+
+
+
+
+
+
+
+
+
+
+
LIVE
+
+
+
+
+
+
+
+
+
+
+
+
+
LIVE
+
+
+
+
+
+
+
+
+
+
+
+
+
LIVE
+
+
+
+
+
+
+
+
+
+
- - - - - \ No newline at end of file + + + + diff --git a/examples/radio/radio.js b/examples/radio/radio.js index 24dc8a2b..0b8dac9c 100644 --- a/examples/radio/radio.js +++ b/examples/radio/radio.js @@ -8,8 +8,10 @@ * MIT License */ +import { Howl } from 'howler'; + // Cache references to DOM elements. -var elms = ['station0', 'title0', 'live0', 'playing0', 'station1', 'title1', 'live1', 'playing1', 'station2', 'title2', 'live2', 'playing2', 'station3', 'title3', 'live3', 'playing3', 'station4', 'title4', 'live4', 'playing4']; +const elms = ['station0', 'title0', 'live0', 'playing0', 'station1', 'title1', 'live1', 'playing1', 'station2', 'title2', 'live2', 'playing2', 'station3', 'title3', 'live3', 'playing3', 'station4', 'title4', 'live4', 'playing4']; elms.forEach(function(elm) { window[elm] = document.getElementById(elm); }); diff --git a/examples/sprite/index.html b/examples/sprite/index.html index 7e3abf6b..c10426e2 100644 --- a/examples/sprite/index.html +++ b/examples/sprite/index.html @@ -1,45 +1,54 @@ - - - - Howler.js Audio Sprite Visual - - - - -
- Audio Sprite Visual
- Click a section of the waveform to play the sprite. -
+ + + + Howler.js Audio Sprite Visual + + + + + +
+ Audio Sprite Visual
+ Click a section of the waveform to play the sprite. +
- -
+ +
- -
-
-
one
-
-
-
two
-
-
-
three
-
-
-
four
-
-
-
five
-
-
-
beat
-
-
+ +
+
+
one
+
+
+
two
+
+
+
three
+
+
+
four
+
+
+
five
+
+
+
beat
+
+
- - - - - \ No newline at end of file + + + + diff --git a/examples/sprite/sprite.js b/examples/sprite/sprite.js index 618edd6f..ea5c40a4 100644 --- a/examples/sprite/sprite.js +++ b/examples/sprite/sprite.js @@ -8,8 +8,10 @@ * MIT License */ +import { Howl } from 'howler'; + // Cache references to DOM elements. -var elms = ['waveform', 'sprite0', 'sprite1', 'sprite2', 'sprite3', 'sprite4', 'sprite5']; +const elms = ['waveform', 'sprite0', 'sprite1', 'sprite2', 'sprite3', 'sprite4', 'sprite5']; elms.forEach(function(elm) { window[elm] = document.getElementById(elm); }); diff --git a/tests/core.html5audio.html b/tests/core.html5audio.html index ecf3ae67..ffe36e7a 100644 --- a/tests/core.html5audio.html +++ b/tests/core.html5audio.html @@ -1,17 +1,24 @@ - - - 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..44d9255f 100644 --- a/tests/core.webaudio.html +++ b/tests/core.webaudio.html @@ -1,16 +1,23 @@ - - - Howler.js Core Web Audio Tests - - - -
-
- -
- - - - \ No newline at end of file + + + Howler.js Core Web Audio Tests + + + + +
+
+ +
+ + + diff --git a/tests/js/core.html5audio.js b/tests/js/core.html5audio.js index cd546ef7..14db9972 100644 --- a/tests/js/core.html5audio.js +++ b/tests/js/core.html5audio.js @@ -1,14 +1,16 @@ +import { Howl } from 'howler'; + // 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 }); -var sound2 = new Howl({ +const sound2 = new Howl({ src: ['audio/sound2.webm', 'audio/sound2.mp3'], html5: true, sprite: { diff --git a/tests/js/core.webaudio.js b/tests/js/core.webaudio.js index f6690310..a6dcfc5f 100644 --- a/tests/js/core.webaudio.js +++ b/tests/js/core.webaudio.js @@ -1,13 +1,15 @@ +import { Howl } from 'howler'; + // 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'] }); -var sound2 = new Howl({ +const sound2 = new Howl({ src: ['audio/sound2.webm', 'audio/sound2.mp3'], sprite: { one: [0, 450], diff --git a/tests/js/spatial.js b/tests/js/spatial.js index a7aa810d..f4ef1184 100644 --- a/tests/js/spatial.js +++ b/tests/js/spatial.js @@ -1,13 +1,18 @@ +import { SpatialHowl, SpatialAudioPlugin, globalPluginManager } from 'howler/plugins/spatial'; + +// Register the Spatial Audio Plugin +globalPluginManager.register(new SpatialAudioPlugin()); + // 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 SpatialHowl({ src: ['audio/sound1.webm', 'audio/sound1.mp3'] }); -var sound2 = new Howl({ +const sound2 = new SpatialHowl({ src: ['audio/sound2.webm', 'audio/sound2.mp3'], sprite: { one: [0, 450], @@ -33,7 +38,7 @@ var tests = [ label.innerHTML = 'PLAYING'; setTimeout(fn, 2000); }); - + id = sound1.play(); }, @@ -116,4 +121,4 @@ if (Howler.usingWebAudio) { }, false); } else { window.location = 'core.html5audio.html'; -} \ No newline at end of file +} diff --git a/tests/spatial.html b/tests/spatial.html index a9776d75..3b9265c6 100644 --- a/tests/spatial.html +++ b/tests/spatial.html @@ -4,14 +4,20 @@ Howler.js Spatial Plugin Tests +
- - - + \ No newline at end of file From 0c74344ecfa8487bd8e6a38f91c7635fbc2c214e Mon Sep 17 00:00:00 2001 From: eatsjobs Date: Sat, 22 Nov 2025 02:58:29 +0100 Subject: [PATCH 05/25] add es module shim in index.htmls --- examples/3d/index.html | 4 ++++ examples/player/index.html | 4 ++++ examples/radio/index.html | 4 ++++ examples/sprite/index.html | 4 ++++ tests/core.html5audio.html | 4 ++++ tests/core.webaudio.html | 4 ++++ tests/spatial.html | 4 ++++ 7 files changed, 28 insertions(+) diff --git a/examples/3d/index.html b/examples/3d/index.html index 8c524c5f..7fe44b18 100644 --- a/examples/3d/index.html +++ b/examples/3d/index.html @@ -13,6 +13,10 @@ } } + diff --git a/examples/player/index.html b/examples/player/index.html index 520b38cd..76c1e413 100644 --- a/examples/player/index.html +++ b/examples/player/index.html @@ -13,6 +13,10 @@ } } + diff --git a/examples/radio/index.html b/examples/radio/index.html index 0fdd5ee5..53c72db7 100644 --- a/examples/radio/index.html +++ b/examples/radio/index.html @@ -13,6 +13,10 @@ } } + diff --git a/examples/sprite/index.html b/examples/sprite/index.html index c10426e2..b379a81d 100644 --- a/examples/sprite/index.html +++ b/examples/sprite/index.html @@ -13,6 +13,10 @@ } } + diff --git a/tests/core.html5audio.html b/tests/core.html5audio.html index ffe36e7a..d7d2bb82 100644 --- a/tests/core.html5audio.html +++ b/tests/core.html5audio.html @@ -12,6 +12,10 @@ } } +
diff --git a/tests/core.webaudio.html b/tests/core.webaudio.html index 44d9255f..d115f5a9 100644 --- a/tests/core.webaudio.html +++ b/tests/core.webaudio.html @@ -12,6 +12,10 @@ } } +
diff --git a/tests/spatial.html b/tests/spatial.html index 3b9265c6..528a4677 100644 --- a/tests/spatial.html +++ b/tests/spatial.html @@ -12,6 +12,10 @@ } } +
From a6758c1ef56015819211d43ef7c69bdfce81585e Mon Sep 17 00:00:00 2001 From: eatsjobs Date: Sat, 22 Nov 2025 03:02:06 +0100 Subject: [PATCH 06/25] Fix package.json --- package.json | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 302cb764..400697f1 100644 --- a/package.json +++ b/package.json @@ -30,18 +30,16 @@ "tsdown": "^0.16.6", "typescript": "^5.9.3" }, - "main": "dist/howler.js", - "module": "dist/howler.mjs", - "types": "dist/howler.d.ts", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", "exports": { ".": { - "import": "./dist/howler.mjs", - "require": "./dist/howler.js", - "types": "./dist/howler.d.ts" + "import": "./dist/index.js", + "types": "./dist/index.d.ts" }, "./plugins/spatial": { - "import": "./dist/plugins/spatial.mjs", - "require": "./dist/plugins/spatial.js", + "import": "./dist/plugins/spatial.js", "types": "./dist/plugins/spatial.d.ts" } }, From 187bf04936cc44ea3d1e02e7bc415b38f91ad3ca Mon Sep 17 00:00:00 2001 From: eatsjobs Date: Sat, 22 Nov 2025 03:02:21 +0100 Subject: [PATCH 07/25] Fix html import maps --- tests/core.html5audio.html | 4 ++-- tests/core.webaudio.html | 4 ++-- tests/spatial.html | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/core.html5audio.html b/tests/core.html5audio.html index d7d2bb82..b3594442 100644 --- a/tests/core.html5audio.html +++ b/tests/core.html5audio.html @@ -7,8 +7,8 @@ diff --git a/tests/core.webaudio.html b/tests/core.webaudio.html index d115f5a9..72db50cc 100644 --- a/tests/core.webaudio.html +++ b/tests/core.webaudio.html @@ -7,8 +7,8 @@ diff --git a/tests/spatial.html b/tests/spatial.html index 528a4677..cdc2882d 100644 --- a/tests/spatial.html +++ b/tests/spatial.html @@ -7,8 +7,8 @@ From cd2ae8fe15215f71140dd0f5a4c25c7cfc76e9e2 Mon Sep 17 00:00:00 2001 From: eatsjobs Date: Sat, 22 Nov 2025 03:03:49 +0100 Subject: [PATCH 08/25] update README.md --- README.md | 176 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 160 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 4a4766f0..76337ee4 100644 --- a/README.md +++ b/README.md @@ -23,14 +23,18 @@ Follow on Twitter for howler.js and development-related discussion: [@GoldFireSt * As light as 7kb gzipped ### Browser Compatibility -Tested in the following browsers/versions: -* Google Chrome 7.0+ -* Internet Explorer 9.0+ -* Firefox 4.0+ -* Safari 5.1.4+ -* Mobile Safari 6.0+ (after user input) -* Opera 12.0+ -* Microsoft Edge + +Howler.js v3.0+ requires ES Module support. The library works in all modern browsers that support ES modules: + +* Google Chrome 61.0+ +* Firefox 67.0+ +* Safari 11.1+ +* Edge 79.0+ +* Opera 48.0+ +* Mobile Safari 11.0+ +* Android Chrome 61.0+ + +**Legacy Browser Support**: For older browsers without native ES module support, use the provided `es-module-shims` polyfill. All test and example files include this polyfill for maximum compatibility. For even older browser support (IE9+), use Howler.js v2.x which uses CommonJS/UMD format. ### Live Demos * [Audio Player](https://howlerjs.com/#player) @@ -80,21 +84,161 @@ In the browser: ``` -As a dependency: +As an ES Module (ESM): ```javascript -import {Howl, Howler} from 'howler'; +import { Howl, Howler } from 'howler'; +import { SpatialAudioPlugin } from 'howler/plugins/spatial'; ``` -```javascript -const {Howl, Howler} = require('howler'); +Howler.js v3.0+ is distributed exclusively as ES Modules. For CommonJS support, use v2.x. + +Distribution files: + +* **Main entry point** (`howler`): Core Howler.js library + * `dist/index.js` - Main entry point with Howler, Howl, Sound classes + * `dist/howler.core.js` - Core audio implementation + * `dist/index.d.ts` - TypeScript type definitions +* **Spatial plugin entry point** (`howler/plugins/spatial`): Spatial audio plugin + * `dist/plugins/spatial.js` - Spatial audio plugin with 3D positioning support + * `dist/plugins/spatial-plugin.js` - SpatialAudioPlugin implementation + * `dist/plugins/spatial.d.ts` - Spatial plugin type definitions +* **Plugin System** (`howler`): Hook-based plugin architecture + * `dist/plugins/plugin.js` - PluginManager and HowlerPlugin base class + +**Note**: Howler.js v3.0+ is built entirely in TypeScript and distributed as ES Modules only, enabling better tree-shaking and code splitting. The plugin system allows safe extension without prototype mutation. + +### Plugin System + +Howler.js v3.0+ includes a modern, hook-based plugin architecture that allows you to extend functionality safely and composably. + +#### Architecture Overview + +The plugin system is built around: +- **HowlerPlugin**: Base class for creating plugins +- **PluginManager**: Manages plugin registration and lifecycle +- **Plugin Hooks**: Well-defined lifecycle events for plugins to hook into + +#### Creating a Plugin + +Plugins extend `HowlerPlugin` and implement hooks: + +```typescript +import { HowlerPlugin, type PluginHooks } from 'howler'; +import type { HowlerGlobal, Howl } from 'howler'; + +export class MyPlugin extends HowlerPlugin { + readonly name = 'my-plugin'; + + getHooks(): PluginHooks { + return { + onHowlerInit: (howler: HowlerGlobal) => { + console.log('Howler initialized'); + }, + onHowlCreate: (howl: Howl, options) => { + console.log('Howl instance created', howl); + }, + }; + } +} +``` + +#### Using Plugins + +Register plugins with the global plugin manager: + +```typescript +import { globalPluginManager, Howl } from 'howler'; +import { SpatialAudioPlugin } from 'howler/plugins/spatial'; + +// Register the spatial audio plugin +globalPluginManager.register(new SpatialAudioPlugin()); + +// Now use Howler as normal - plugin hooks execute automatically +const sound = new Howl({ + src: ['audio.mp3'], + pos: [0, 0, 0], // Spatial audio options available +}); + +// Use spatial methods +sound.pos(10, 5, 0); +Howler.stereo(0.5); ``` -Included distribution files: +#### Managing Plugins + +```typescript +import { globalPluginManager } from 'howler'; + +// Check if plugin is registered +if (globalPluginManager.isRegistered('spatial-audio')) { + console.log('Spatial audio is available'); +} + +// Disable a plugin temporarily +globalPluginManager.disable('spatial-audio'); + +// Re-enable it +globalPluginManager.enable('spatial-audio'); + +// Unregister a plugin +globalPluginManager.unregister('spatial-audio'); +``` + +#### Available Plugins + +##### Spatial Audio Plugin + +Adds 3D spatial audio and stereo panning support: + +```typescript +import { globalPluginManager } from 'howler'; +import { SpatialAudioPlugin } from 'howler/plugins/spatial'; + +globalPluginManager.register(new SpatialAudioPlugin()); + +const sound = new Howl({ + src: ['audio.mp3'], + pos: [0, 0, 0], // 3D position + stereo: 0, // Stereo pan (-1 to 1) + orientation: [1, 0, 0], // Direction the sound is facing + coneInnerAngle: 360, // Cone parameters + distanceModel: 'inverse', // Distance attenuation model +}); + +// Set listener position (where the "ear" is) +Howler.pos(0, 0, 0); + +// Set listener orientation (which way they're facing) +Howler.orientation(0, 0, -1, 0, 1, 0); + +// Set sound position (where the sound is in 3D space) +sound.pos(10, 5, 0); + +// Apply stereo panning +sound.stereo(0.5); + +// Configure panner attributes +sound.pannerAttr({ + distanceModel: 'inverse', + maxDistance: 10000, + refDistance: 1, + rolloffFactor: 1, + panningModel: 'HRTF' +}); +``` + +#### Plugin Lifecycle Hooks + +Available hooks that plugins can implement: -* **howler**: This is the default and fully bundled source that includes `howler.core` and `howler.spatial`. It includes all functionality that howler comes with. -* **howler.core**: This includes only the core functionality that aims to create parity between Web Audio and HTML5 Audio. It doesn't include any of the spatial/stereo audio functionality. -* **howler.spatial**: This is a plugin that adds spatial/stereo audio functionality. It requires `howler.core` to operate as it is simply an add-on to the core. +- `onRegister()` - Called when plugin is registered +- `onHowlerInit(howler)` - Called when Howler global is initialized +- `onHowlCreate(howl, options)` - Called when a Howl instance is created +- `onSoundCreate(sound, parent)` - Called when a Sound instance is created +- `onHowlLoad(howl)` - Called when a Howl instance loads +- `onHowlDestroy(howl)` - Called when a Howl instance is destroyed +- `onUnregister()` - Called when plugin is unregistered (cleanup) ### Examples From daafd0f5a49ee3d23a19f6481e574526ca986fbf Mon Sep 17 00:00:00 2001 From: eatsjobs Date: Sat, 22 Nov 2025 03:11:21 +0100 Subject: [PATCH 09/25] update package.json --- package-lock.json | 203 +--------------------------------------------- package.json | 6 +- 2 files changed, 7 insertions(+), 202 deletions(-) diff --git a/package-lock.json b/package-lock.json index d3eca93d..e0f64ebc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,16 @@ { "name": "howler", - "version": "2.2.4", + "version": "3.0.0-alpha.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "howler", - "version": "2.2.4", + "version": "3.0.0-alpha.1", "license": "MIT", "devDependencies": { - "tsdown": "^0.16.6", - "typescript": "^5.9.3", - "uglify-js": "2.x" + "tsdown": "~0.16.6", + "typescript": "~5.9.3" } }, "node_modules/@babel/generator": { @@ -456,21 +455,6 @@ "tslib": "^2.4.0" } }, - "node_modules/align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha512-GrTZLRpmp6wIC2ztrWW9MjjTgSKccffgFagbNDOX95/dcjEcYZibYTeaOntySQLcdw1ztBoFkviiUvTMbb9MYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2", - "longest": "^1.0.1", - "repeat-string": "^1.5.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ansis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", @@ -518,30 +502,6 @@ "node": ">=8" } }, - "node_modules/camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha512-wzLkDa4K/mzI1OSITC+DUyjgIl/ETNHE9QvYgy6J6Jvqyyz4C0Xfd+lQhb19sX2jMpZV4IssUn0VDVmglV+s4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha512-Baz3aNe2gd2LP2qk5U+sDk/m4oSuwSDcBfayTCTBoWpfIGO5XFxPmjILQII4NGiZjD6DoDI6kf7gKaxkf7s3VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "align-text": "^0.1.3", - "lazy-cache": "^1.0.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -558,28 +518,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha512-GIOYRizG+TGoc7Wgc1LiOTLare95R3mzKgoln+Q/lE4ceiYH19gUpl0l0Ffq4lJDEf3FxujMe6IBfOCs7pfqNA==", - "dev": true, - "license": "ISC", - "dependencies": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/diff": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", @@ -659,13 +597,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true, - "license": "MIT" - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -679,39 +610,6 @@ "node": ">=6" } }, - "node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha512-k+yt5n3l48JU4k8ftnKG6V7u32wyH2NfKzeMto9F/QRE0amxy/LayxwlvjjkZEIzqR+19IrtFO8p5kB9QaYUFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -784,16 +682,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -804,19 +692,6 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/right-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha512-yqINtL/G7vs2v+dFIZmFUDbnVyFUJFKd6gK22Kgo6R4jfJGFtisKyncWDDULgjfqf4ASQuIQyjJ7XZ+3aWpsAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "align-text": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/rolldown": { "version": "1.0.0-beta.51", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.51.tgz", @@ -908,16 +783,6 @@ "node": ">=10" } }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/tinyexec": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", @@ -1038,34 +903,6 @@ "node": ">=14.17" } }, - "node_modules/uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha512-qLq/4y2pjcU3vhlhseXGGJ7VbFO4pBANu0kwl8VCa9KEI0V8VfZIx2Fy3w01iSTA/pGwKZSmu/+I4etLNDdt5w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "source-map": "~0.5.1", - "yargs": "~3.10.0" - }, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - }, - "optionalDependencies": { - "uglify-to-browserify": "~1.0.0" - } - }, - "node_modules/uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha512-vb2s1lYx2xBtUgy+ta+b2J/GLVUR+wmpINwHePmPRhOsIVCG2wDzKJ0n14GslH1BifsqVzSOwQhRaCAsZ/nI4Q==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/unconfig-core": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/unconfig-core/-/unconfig-core-7.4.1.tgz", @@ -1107,38 +944,6 @@ "optional": true } } - }, - "node_modules/window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha512-1pTPQDKTdd61ozlKGNCjhNRd+KPmgLSGa3mZTHoOliaGcESD8G1PXhh7c1fgiPjVbNVfgy2Faw4BI8/m0cC8Mg==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q==", - "dev": true, - "license": "MIT/X11", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha512-QFzUah88GAGy9lyDKGBqZdkYApt63rCXYBGYnEP4xDJPXNqXXnBDACnbrXnViV6jRSqAePwrATi2i8mfYm4L1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.0" - } } } } diff --git a/package.json b/package.json index 400697f1..a5810444 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "howler", - "version": "2.2.4", + "version": "3.0.0-alpha.1", "description": "Javascript audio library for the modern web.", "homepage": "https://howlerjs.com", "type": "module", @@ -27,8 +27,8 @@ "release": "npm run build && git add dist && git commit -m 'build: update dist files' && npm publish" }, "devDependencies": { - "tsdown": "^0.16.6", - "typescript": "^5.9.3" + "tsdown": "~0.16.6", + "typescript": "~5.9.3" }, "main": "dist/index.js", "module": "dist/index.js", From 74d5ba00be87f775d005fa35ca17332fafd7199f Mon Sep 17 00:00:00 2001 From: eatsjobs Date: Sat, 22 Nov 2025 19:00:22 +0100 Subject: [PATCH 10/25] Simplify plugin API: remove enable/disable methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since plugins are enabled by default when registered and automatically removed when unregistered, the enable/disable/isEnabled methods add unnecessary complexity. Plugins now have a simpler lifecycle: registered means active, unregistered means inactive. Changes: - Remove enable/disable/isEnabled methods from PluginManager - Remove enabled flag from RegisteredPlugin interface - Simplify _executeHooks to always execute for registered plugins - Update README documentation to remove enable/disable examples 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 251 +++++++++++++++++++++++++++++++++--------- src/plugins/plugin.ts | 32 +----- 2 files changed, 202 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 76337ee4..5147bdda 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ [![howler.js](https://s3.amazonaws.com/howler.js/howler-logo.png "howler.js")](https://howlerjs.com) # Description + [howler.js](https://howlerjs.com) is an audio library for the modern web. It defaults to [Web Audio API](http://webaudio.github.io/web-audio-api/) and falls back to [HTML5 Audio](https://html.spec.whatwg.org/multipage/embedded-content.html#the-audio-element). This makes working with audio in JavaScript easy and reliable across all platforms. Additional information, live demos and a user showcase are available at [howlerjs.com](https://howlerjs.com). @@ -8,6 +9,7 @@ Additional information, live demos and a user showcase are available at [howlerj Follow on Twitter for howler.js and development-related discussion: [@GoldFireStudios](https://twitter.com/goldfirestudios). ### Features + * Single API for all audio needs * Defaults to Web Audio API and falls back to HTML5 Audio * Handles edge cases and bugs across environments @@ -37,6 +39,7 @@ Howler.js v3.0+ requires ES Module support. The library works in all modern brow **Legacy Browser Support**: For older browsers without native ES module support, use the provided `es-module-shims` polyfill. All test and example files include this polyfill for maximum compatibility. For even older browser support (IE9+), use Howler.js v2.x which uses CommonJS/UMD format. ### Live Demos + * [Audio Player](https://howlerjs.com/#player) * [Radio](https://howlerjs.com/#radio) * [Spatial Audio](https://howlerjs.com/#spatial) @@ -45,6 +48,7 @@ Howler.js v3.0+ requires ES Module support. The library works in all modern brow # Documentation ### Contents + * [Quick Start](#quick-start) * [Examples](#examples) * [Core](#core) @@ -115,9 +119,10 @@ Howler.js v3.0+ includes a modern, hook-based plugin architecture that allows yo #### Architecture Overview The plugin system is built around: -- **HowlerPlugin**: Base class for creating plugins -- **PluginManager**: Manages plugin registration and lifecycle -- **Plugin Hooks**: Well-defined lifecycle events for plugins to hook into + +* **HowlerPlugin**: Base class for creating plugins +* **PluginManager**: Manages plugin registration and lifecycle +* **Plugin Hooks**: Well-defined lifecycle events for plugins to hook into #### Creating a Plugin @@ -145,44 +150,49 @@ export class MyPlugin extends HowlerPlugin { #### Using Plugins -Register plugins with the global plugin manager: +Register plugins directly with Howler: ```typescript -import { globalPluginManager, Howl } from 'howler'; -import { SpatialAudioPlugin } from 'howler/plugins/spatial'; +import { Howler, Howl } from 'howler'; +import { SpatialAudioPlugin, type SpatialHowler, type SpatialHowl } from 'howler/plugins/spatial'; // Register the spatial audio plugin -globalPluginManager.register(new SpatialAudioPlugin()); +Howler.addPlugin(new SpatialAudioPlugin()); -// Now use Howler as normal - plugin hooks execute automatically -const sound = new Howl({ +// Type annotations for TypeScript support +const listener: SpatialHowler = Howler as SpatialHowler; +const sound: SpatialHowl = new Howl({ src: ['audio.mp3'], pos: [0, 0, 0], // Spatial audio options available }); -// Use spatial methods +// Use spatial methods with full type support sound.pos(10, 5, 0); -Howler.stereo(0.5); +listener.stereo(0.5); +listener.orientation(1, 0, 0, 0, 1, 0); ``` #### Managing Plugins ```typescript +import { Howler } from 'howler'; +import { SpatialAudioPlugin } from 'howler/plugins/spatial'; + +const spatialPlugin = new SpatialAudioPlugin(); + +// Register a plugin +Howler.addPlugin(spatialPlugin); + +// Unregister a plugin (pass the plugin instance) +Howler.removePlugin(spatialPlugin); + +// For more advanced management, use the global plugin manager: import { globalPluginManager } from 'howler'; // Check if plugin is registered if (globalPluginManager.isRegistered('spatial-audio')) { console.log('Spatial audio is available'); } - -// Disable a plugin temporarily -globalPluginManager.disable('spatial-audio'); - -// Re-enable it -globalPluginManager.enable('spatial-audio'); - -// Unregister a plugin -globalPluginManager.unregister('spatial-audio'); ``` #### Available Plugins @@ -192,10 +202,11 @@ globalPluginManager.unregister('spatial-audio'); Adds 3D spatial audio and stereo panning support: ```typescript -import { globalPluginManager } from 'howler'; +import { Howler, Howl } from 'howler'; import { SpatialAudioPlugin } from 'howler/plugins/spatial'; -globalPluginManager.register(new SpatialAudioPlugin()); +// Register the plugin +Howler.addPlugin(new SpatialAudioPlugin()); const sound = new Howl({ src: ['audio.mp3'], @@ -232,18 +243,18 @@ sound.pannerAttr({ Available hooks that plugins can implement: -- `onRegister()` - Called when plugin is registered -- `onHowlerInit(howler)` - Called when Howler global is initialized -- `onHowlCreate(howl, options)` - Called when a Howl instance is created -- `onSoundCreate(sound, parent)` - Called when a Sound instance is created -- `onHowlLoad(howl)` - Called when a Howl instance loads -- `onHowlDestroy(howl)` - Called when a Howl instance is destroyed -- `onUnregister()` - Called when plugin is unregistered (cleanup) - +* `onRegister()` - Called when plugin is registered +* `onHowlerInit(howler)` - Called when Howler global is initialized +* `onHowlCreate(howl, options)` - Called when a Howl instance is created +* `onSoundCreate(sound, parent)` - Called when a Sound instance is created +* `onHowlLoad(howl)` - Called when a Howl instance loads +* `onHowlDestroy(howl)` - Called when a Howl instance is destroyed +* `onUnregister()` - Called when plugin is unregistered (cleanup) ### Examples -##### Most basic, play an MP3: +#### Most basic, play an MP3 + ```javascript var sound = new Howl({ src: ['sound.mp3'] @@ -252,7 +263,8 @@ var sound = new Howl({ sound.play(); ``` -##### Streaming audio (for live audio or large files): +#### Streaming audio (for live audio or large files) + ```javascript var sound = new Howl({ src: ['stream.mp3'], @@ -262,7 +274,8 @@ var sound = new Howl({ sound.play(); ``` -##### More playback options: +##### More playback options + ```javascript var sound = new Howl({ src: ['sound.webm', 'sound.mp3', 'sound.wav'], @@ -275,7 +288,8 @@ var sound = new Howl({ }); ``` -##### Define and play a sound sprite: +##### Define and play a sound sprite + ```javascript var sound = new Howl({ src: ['sounds.webm', 'sounds.mp3'], @@ -290,7 +304,8 @@ var sound = new Howl({ sound.play('laser'); ``` -##### Listen for events: +##### Listen for events + ```javascript var sound = new Howl({ src: ['sound.webm', 'sound.mp3'] @@ -307,7 +322,8 @@ sound.on('end', function(){ }); ``` -##### Control multiple sounds: +##### Control multiple sounds + ```javascript var sound = new Howl({ src: ['sound.webm', 'sound.mp3'] @@ -323,7 +339,8 @@ sound.fade(1, 0, 1000, id1); sound.rate(1.5, id2); ``` -##### ES6: +##### ES6 + ```javascript import {Howl, Howler} from 'howler'; @@ -339,29 +356,44 @@ sound.play(); Howler.volume(0.5); ``` - More in-depth examples (with accompanying live demos) can be found in the [examples directory](https://github.com/goldfire/howler.js/tree/master/examples). - ## Core ### Options + #### src `Array/String` `[]` *`required`* + 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. + #### volume `Number` `1.0` + The volume of the specific track, from `0.0` to `1.0`. + #### html5 `Boolean` `false` + 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. + #### loop `Boolean` `false` + Set to `true` to automatically loop the sound forever. + #### preload `Boolean|String` `true` -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). + +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). + #### autoplay `Boolean` `false` + Set to `true` to automatically start playback when sound is loaded. + #### mute `Boolean` `false` + Set to `true` to load the audio muted. + #### sprite `Object` `{}` + 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](https://github.com/tonistiigi/audiosprite). + ```javascript new Howl({ sprite: { @@ -369,21 +401,30 @@ new Howl({ }, }); ``` + #### rate `Number` `1.0` + The rate of playback. 0.5 to 4.0, with 1.0 being normal speed. + #### pool `Number` `5` + 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. + #### format `Array` `[]` + 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). + #### xhr `Object` `null` + 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](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials)), include them with this parameter. Each is optional (method defaults to `GET`, headers default to `null` and withCredentials defaults to `false`). For example: + ```javascript // Using each of the properties. new Howl({ xhr: { method: 'POST', headers: { - Authorization: 'Bearer:' + token, + Authorization: `Bearer: ${token}`, }, withCredentials: true, } @@ -396,200 +437,301 @@ new Howl({ } }); ``` + #### onload `Function` + Fires when the sound is loaded. + #### onloaderror `Function` + 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. The load error codes are [defined in the spec](http://dev.w3.org/html5/spec-author-view/spec.html#mediaerror): + * **1** - The fetching process for the media resource was aborted by the user agent at the user's request. * **2** - A network error of some description caused the user agent to stop fetching the media resource, after the resource was established to be usable. * **3** - An error of some description occurred while decoding the media resource, after the resource was established to be usable. * **4** - The media resource indicated by the src attribute or assigned media provider object was not suitable. + #### onplayerror `Function` + 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. + #### onplay `Function` + Fires when the sound begins playing. The first parameter is the ID of the sound. + #### onend `Function` + 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. + #### onpause `Function` + Fires when the sound has been paused. The first parameter is the ID of the sound. + #### onstop `Function` + Fires when the sound has been stopped. The first parameter is the ID of the sound. + #### onmute `Function` + Fires when the sound has been muted/unmuted. The first parameter is the ID of the sound. + #### onvolume `Function` + Fires when the sound's volume has changed. The first parameter is the ID of the sound. + #### onrate `Function` + Fires when the sound's playback rate has changed. The first parameter is the ID of the sound. + #### onseek `Function` + Fires when the sound has been seeked. The first parameter is the ID of the sound. + #### onfade `Function` + Fires when the current sound finishes fading in/out. The first parameter is the ID of the sound. + #### onunlock `Function` -Fires when audio has been automatically unlocked through a touch/click event. +Fires when audio has been automatically unlocked through a touch/click event. ### Methods + #### play([sprite/id]) + Begins playback of a sound. Returns the sound id to be used with other methods. Only method that can't be chained. + * **sprite/id**: `String/Number` `optional` Takes one parameter that can either be a sprite or sound ID. If a sprite is passed, a new sound will play based on the sprite's definition. If a sound ID is passed, the previously played sound will be played (for example, after pausing it). However, if an ID of a sound that has been drained from the pool is passed, nothing will play. #### pause([id]) + Pauses playback of sound or group, saving the `seek` of playback. + * **id**: `Number` `optional` The sound ID. If none is passed, all sounds in group are paused. #### stop([id]) + Stops playback of sound, resetting `seek` to `0`. + * **id**: `Number` `optional` The sound ID. If none is passed, all sounds in group are stopped. #### mute([muted], [id]) + Mutes the sound, but doesn't pause the playback. + * **muted**: `Boolean` `optional` True to mute and false to unmute. * **id**: `Number` `optional` The sound ID. If none is passed, all sounds in group are stopped. #### volume([volume], [id]) + Get/set volume of this sound or the group. This method optionally takes 0, 1 or 2 arguments. + * **volume**: `Number` `optional` Volume from `0.0` to `1.0`. * **id**: `Number` `optional` The sound ID. If none is passed, all sounds in group have volume altered relative to their own volume. #### fade(from, to, duration, [id]) + Fade a currently playing sound between two volumes. Fires the `fade` event when complete. + * **from**: `Number` Volume to fade from (`0.0` to `1.0`). * **to**: `Number` Volume to fade to (`0.0` to `1.0`). * **duration**: `Number` Time in milliseconds to fade. * **id**: `Number` `optional` The sound ID. If none is passed, all sounds in group will fade. #### rate([rate], [id]) + Get/set the rate of playback for a sound. This method optionally takes 0, 1 or 2 arguments. + * **rate**: `Number` `optional` The rate of playback. 0.5 to 4.0, with 1.0 being normal speed. * **id**: `Number` `optional` The sound ID. If none is passed, playback rate of all sounds in group will change. #### seek([seek], [id]) + Get/set the position of playback for a sound. This method optionally takes 0, 1 or 2 arguments. + * **seek**: `Number` `optional` The position to move current playback to (in seconds). * **id**: `Number` `optional` The sound ID. If none is passed, the first sound will seek. #### loop([loop], [id]) + Get/set whether to loop the sound or group. This method can optionally take 0, 1 or 2 arguments. + * **loop**: `Boolean` `optional` To loop or not to loop, that is the question. * **id**: `Number` `optional` The sound ID. If none is passed, all sounds in group will have their `loop` property updated. #### state() + Check the load status of the `Howl`, returns a `unloaded`, `loading` or `loaded`. #### playing([id]) + Check if a sound is currently playing or not, returns a `Boolean`. If no sound ID is passed, check if any sound in the `Howl` group is playing. + * **id**: `Number` `optional` The sound ID to check. #### duration([id]) + Get the duration of the audio source (in seconds). Will return 0 until after the `load` event fires. + * **id**: `Number` `optional` The sound ID to check. Passing an ID will return the duration of the sprite being played on this instance; otherwise, the full source duration is returned. #### on(event, function, [id]) + Listen for events. Multiple events can be added by calling this multiple times. + * **event**: `String` Name of event to fire/set (`load`, `loaderror`, `playerror`, `play`, `end`, `pause`, `stop`, `mute`, `volume`, `rate`, `seek`, `fade`, `unlock`). * **function**: `Function` Define function to fire on event. * **id**: `Number` `optional` Only listen to events for this sound id. #### once(event, function, [id]) + Same as `on`, but it removes itself after the callback is fired. + * **event**: `String` Name of event to fire/set (`load`, `loaderror`, `playerror`, `play`, `end`, `pause`, `stop`, `mute`, `volume`, `rate`, `seek`, `fade`, `unlock`). * **function**: `Function` Define function to fire on event. * **id**: `Number` `optional` Only listen to events for this sound id. #### off(event, [function], [id]) + Remove event listener that you've set. Call without parameters to remove all events. + * **event**: `String` Name of event (`load`, `loaderror`, `playerror`, `play`, `end`, `pause`, `stop`, `mute`, `volume`, `rate`, `seek`, `fade`, `unlock`). * **function**: `Function` `optional` The listener to remove. Omit this to remove all events of type. * **id**: `Number` `optional` Only remove events for this sound id. #### load() + This is called by default, but if you set `preload` to false, you must call `load` before you can play any sounds. #### unload() -Unload and destroy a Howl object. This will immediately stop all sounds attached to this sound and remove it from the cache. +Unload and destroy a Howl object. This will immediately stop all sounds attached to this sound and remove it from the cache. ### Global Options + #### usingWebAudio `Boolean` + `true` if the Web Audio API is available. + #### noAudio `Boolean` + `true` if no audio is available. + #### autoUnlock `Boolean` `true` + Automatically attempts to enable audio on mobile (iOS, Android, etc) devices and desktop Chrome/Safari. + #### html5PoolSize `Number` `10` + Each HTML5 Audio object must be unlocked individually, so we keep a global pool of unlocked nodes to share between all `Howl` instances. This pool gets created on the first user interaction and is set to the size of this property. + #### autoSuspend `Boolean` `true` + Automatically suspends the Web Audio AudioContext after 30 seconds of inactivity to decrease processing and energy usage. Automatically resumes upon new playback. Set this property to `false` to disable this behavior. + #### ctx `Boolean` *`Web Audio Only`* + Exposes the `AudioContext` with Web Audio API. + #### masterGain `Boolean` *`Web Audio Only`* -Exposes the master `GainNode` with Web Audio API. This can be useful for writing plugins or advanced usage. +Exposes the master `GainNode` with Web Audio API. This can be useful for writing plugins or advanced usage. ### Global Methods + The following methods are used to modify all sounds globally, and are called from the `Howler` object. + #### mute(muted) + Mute or unmute all sounds. + * **muted**: `Boolean` True to mute and false to unmute. #### volume([volume]) + Get/set the global volume for all sounds, relative to their own volume. + * **volume**: `Number` `optional` Volume from `0.0` to `1.0`. #### stop() + Stop all sounds and reset their seek position to the beginning. #### codecs(ext) + Check supported audio codecs. Returns `true` if the codec is supported in the current browser. + * **ext**: `String` File extension. One of: "mp3", "mpeg", "opus", "ogg", "oga", "wav", "aac", "caf", "m4a", "m4b", "mp4", "weba", "webm", "dolby", "flac". #### unload() -Unload and destroy all currently loaded Howl objects. This will immediately stop all sounds and remove them from cache. +Unload and destroy all currently loaded Howl objects. This will immediately stop all sounds and remove them from cache. ## Plugin: Spatial ### Options + #### orientation `Array` `[1, 0, 0]` + Sets the direction the audio source is pointing in the 3D cartesian coordinate space. Depending on how directional the sound is, based on the `cone` attributes, a sound pointing away from the listener can be quiet or silent. + #### stereo `Number` `null` + Sets the stereo panning value of the audio source for this sound or group. This makes it easy to setup left/right panning with a value of `-1.0` being far left and a value of `1.0` being far right. + #### pos `Array` `null` + Sets the 3D spatial position of the audio source for this sound or group relative to the global listener. + #### pannerAttr `Object` + Sets the panner node's attributes for a sound or group of sounds. See the `pannerAttr` method for all available options. + #### onstereo `Function` + Fires when the current sound has the stereo panning changed. The first parameter is the ID of the sound. + #### onpos `Function` + Fires when the current sound has the listener position changed. The first parameter is the ID of the sound. + #### onorientation `Function` -Fires when the current sound has the direction of the listener changed. The first parameter is the ID of the sound. +Fires when the current sound has the direction of the listener changed. The first parameter is the ID of the sound. ### Methods + #### stereo(pan, [id]) + Get/set the stereo panning of the audio source for this sound or all in the group. + * **pan**: `Number` A value of `-1.0` is all the way left and `1.0` is all the way right. * **id**: `Number` `optional` The sound ID. If none is passed, all in group will be updated. #### pos(x, y, z, [id]) + Get/set the 3D spatial position of the audio source for this sound or group relative to the global listener. + * **x**: `Number` The x-position of the audio source. * **y**: `Number` The y-position of the audio source. * **z**: `Number` The z-position of the audio source. * **id**: `Number` `optional` The sound ID. If none is passed, all in group will be updated. #### orientation(x, y, z, [id]) + Get/set the direction the audio source is pointing in the 3D cartesian coordinate space. Depending on how directional the sound is, based on the `cone` attributes, a sound pointing away from the listener can be quiet or silent. + * **x**: `Number` The x-orientation of the source. * **y**: `Number` The y-orientation of the source. * **z**: `Number` The z-orientation of the source. * **id**: `Number` `optional` The sound ID. If none is passed, all in group will be updated. #### pannerAttr(o, [id]) + Get/set the panner node's attributes for a sound or group of sounds. + * **o**: `Object` All values to update. * **coneInnerAngle** `360` A parameter for directional audio sources, this is an angle, in degrees, inside of which there will be no volume reduction. * **coneOuterAngle** `360` 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`. @@ -601,20 +743,26 @@ Get/set the panner node's attributes for a sound or group of sounds. * **panningModel** `HRTF` Determines which spatialization algorithm is used to position audio. Can be `HRTF` or `equalpower`. * **id**: `Number` `optional` The sound ID. If none is passed, all in group will be updated. - ### Global Methods + #### stereo(pan) + Helper method to update the stereo panning position of all current `Howls`. Future `Howls` will not use this value unless explicitly set. + * **pan**: `Number` A value of -1.0 is all the way left and 1.0 is all the way right. #### pos(x, y, z) + Get/set the position of the listener in 3D cartesian space. Sounds using 3D position will be relative to the listener's position. + * **x**: `Number` The x-position of the listener. * **y**: `Number` The y-position of the listener. * **z**: `Number` The z-position of the listener. #### orientation(x, y, z, xUp, yUp, zUp) + 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. + * **x**: `Number` The x-orientation of listener. * **y**: `Number` The y-orientation of listener. * **z**: `Number` The z-orientation of listener. @@ -622,8 +770,8 @@ Get/set the direction the listener is pointing in the 3D cartesian space. A fron * **yUp**: `Number` The y-orientation of the top of the listener. * **zUp**: `Number` The z-orientation of the top of the listener. - ### Group Playback + Each `new Howl()` instance is also a group. You can play multiple sound instances from the `Howl` and control them individually or as a group (note: each `Howl` can only contain a single audio file). For example, the following plays two sounds from a sprite, changes their volume together and then pauses both of them at the same time. ```javascript @@ -648,8 +796,8 @@ setTimeout(function() { }, 1000); ``` - ### Mobile/Chrome Playback + By default, audio on mobile browsers and Chrome/Safari is locked until a sound is played within a user interaction, and then it plays normally the rest of the page session ([Apple documentation](https://developer.apple.com/library/safari/documentation/audiovideo/conceptual/using_html5_audio_video/PlayingandSynthesizingSounds/PlayingandSynthesizingSounds.html)). The default behavior of howler.js is to attempt to silently unlock audio playback by playing an empty buffer on the first `touchend` event. This behavior can be disabled by calling: ```javascript @@ -671,8 +819,8 @@ var sound = new Howl({ sound.play(); ``` - ### Dolby Audio Playback + Full support for playback of the Dolby Audio format (currently support in Edge and Safari) is included. However, you must specify that the file you are loading is `dolby` since it is in a `mp4` container. ```javascript @@ -683,9 +831,11 @@ var dolbySound = new Howl({ ``` ### Facebook Instant Games + Howler.js provides audio support for the new [Facebook Instant Games](https://developers.facebook.com/docs/games/instant-games/engine-recommendations) platform. If you encounter any issues while developing for Instant Games, open an issue with the tag `[IG]`. ### Format Recommendations + Howler.js supports a wide array of audio codecs that have varying browser support ("mp3", "opus", "ogg", "wav", "aac", "m4a", "m4b", "mp4", "webm", ...), but if you want full browser coverage you still need to use at least two of them. If your goal is to have the best balance of small filesize and high quality, based on extensive production testing, your best bet is to default to `webm` and fallback to `mp3`. `webm` has nearly full browser coverage with a great combination of compression and quality. You'll need the `mp3` fallback for Internet Explorer. It is important to remember that howler.js selects the first compatible sound from your array of sources. So if you want `webm` to be used before `mp3`, you need to put the sources in that order. @@ -697,6 +847,7 @@ ffmpeg -i sound1.wav -dash 1 sound1.webm ``` ### Sponsors + Support the ongoing development of howler.js and get your logo on our README with a link to your site [[become a sponsor](https://github.com/sponsors/goldfire)]. You can also become a backer at a lower tier and get your name in the [BACKERS](https://github.com/goldfire/howler.js/blob/master/BACKERS.md) list. All support is greatly appreciated! [![GoldFire Studios](https://s3.amazonaws.com/howler.js/sponsors/goldfire_studios.png "GoldFire Studios")](https://goldfirestudios.com) diff --git a/src/plugins/plugin.ts b/src/plugins/plugin.ts index f5efa418..d2169940 100644 --- a/src/plugins/plugin.ts +++ b/src/plugins/plugin.ts @@ -79,7 +79,6 @@ export abstract class HowlerPlugin { export interface RegisteredPlugin { plugin: HowlerPlugin; hooks: PluginHooks; - enabled: boolean; } /** @@ -102,8 +101,7 @@ export class PluginManager { const hooks = plugin.getHooks(); const registered: RegisteredPlugin = { plugin, - hooks, - enabled: true + hooks }; this.plugins.set(plugin.name, registered); @@ -148,30 +146,6 @@ export class PluginManager { return this.plugins.has(pluginName); } - /** - * Enable a plugin - * @param pluginName - The name of the plugin to enable - */ - enable(pluginName: string): void { - const registered = this.plugins.get(pluginName); - if (!registered) { - throw new Error(`Plugin "${pluginName}" is not registered`); - } - registered.enabled = true; - } - - /** - * Disable a plugin (keeps it registered but doesn't execute hooks) - * @param pluginName - The name of the plugin to disable - */ - disable(pluginName: string): void { - const registered = this.plugins.get(pluginName); - if (!registered) { - throw new Error(`Plugin "${pluginName}" is not registered`); - } - registered.enabled = false; - } - /** * Get all registered plugins */ @@ -239,10 +213,6 @@ export class PluginManager { */ private _executeHooks(hookName: string, callback: (hooks: PluginHooks) => void): void { for (const [pluginName, registered] of this.plugins) { - if (!registered.enabled) { - continue; - } - try { callback(registered.hooks); } catch (error) { From d515b9b0c8e738aa49611e306988127141e0964a Mon Sep 17 00:00:00 2001 From: eatsjobs Date: Sat, 22 Nov 2025 19:45:31 +0100 Subject: [PATCH 11/25] reviewed plugin system --- src/howler.core.ts | 71 +++++++- src/index.ts | 4 +- src/plugins/plugin.ts | 32 ++-- src/plugins/spatial-plugin.ts | 306 ++++++++++++++++++---------------- src/plugins/spatial.ts | 7 +- 5 files changed, 254 insertions(+), 166 deletions(-) diff --git a/src/howler.core.ts b/src/howler.core.ts index 41c813a9..b2879e7c 100644 --- a/src/howler.core.ts +++ b/src/howler.core.ts @@ -8,13 +8,13 @@ * MIT License */ // Import shared types -import { cache, HowlOptions, EventListener, QueueItem } from './types'; +import { cache, EventListener, HowlOptions, QueueItem } from './types'; // Import helper functions import { loadBuffer, setupAudioContext } from './helpers'; // Import plugin manager -import { globalPluginManager } from './plugins'; +import { globalPluginManager, HowlerPlugin } from './plugins'; export class HowlerGlobal { _counter: number = 1000; @@ -65,8 +65,9 @@ export class HowlerGlobal { self._setup(); - // Execute plugin hooks - globalPluginManager.executeHowlerInit(self); + // Register the Howler instance with the plugin manager + // This allows plugins registered after initialization to access it via onRegister + globalPluginManager.setHowlerInstance(self); return self; } @@ -168,6 +169,28 @@ export class HowlerGlobal { return (this || Howler)._codecs[ext.replace(/^x-/, '')]; } + /** + * Register a plugin with Howler + * @param plugin - The plugin to register + * @returns this for chaining + * @throws Error if a plugin with the same name is already registered + */ + addPlugin(plugin: HowlerPlugin): HowlerGlobal { + globalPluginManager.register(plugin); + return this; + } + + /** + * Unregister a plugin from Howler + * @param plugin - The plugin instance to unregister + * @returns this for chaining + * @throws Error if the plugin is not registered + */ + removePlugin(plugin: HowlerPlugin): HowlerGlobal { + globalPluginManager.unregister(plugin.name); + return this; + } + _setup(): HowlerGlobal { const self = this; @@ -890,7 +913,8 @@ class Howl { const playHtml5 = () => { node.currentTime = seek; node.muted = sound._muted || self._muted || Howler._muted || node.muted; - node.volume = sound._volume * Howler.volume(); + const volume = Howler.volume(); + node.volume = sound._volume * (typeof volume === 'number' ? volume : 1); node.playbackRate = sound._rate; try { @@ -939,8 +963,8 @@ class Howl { }; node.addEventListener('ended', self._endTimers[sound._id], false); } - } catch (err) { - self._emit('playerror', sound._id, err); + } catch (err: unknown) { + self._emit('playerror', sound._id, err instanceof Error ? err.message : String(err)); } }; @@ -1893,7 +1917,38 @@ class Howl { } } +/** + * Type declaration for Howler with optional spatial audio mixin methods. + * These methods are added dynamically by the SpatialAudioPlugin at runtime. + */ +export interface HowlerInstance extends HowlerGlobal { + // Optional spatial audio methods added by plugin + _pos?: [number, number, number]; + _orientation?: [number, number, number, number, number, number]; + pos?(x?: number, y?: number, z?: number): any; + orientation?(x?: number, y?: number, z?: number, xUp?: number, yUp?: number, zUp?: number): any; + stereo?(pan?: number): any; +} + +/** + * Type declaration for Howl with optional spatial audio mixin methods. + * These methods are added dynamically by the SpatialAudioPlugin at runtime. + */ +export interface HowlInstance extends Howl { + // Optional spatial audio properties added by plugin + _pos?: [number, number, number] | null; + _orientation?: [number, number, number]; + _stereo?: number | null; + _pannerAttr?: any; + + // Optional spatial audio methods added by plugin + pos?(x?: number, y?: number, z?: number, id?: number): any; + orientation?(x?: number, y?: number, z?: number, id?: number): any; + stereo?(pan?: number, id?: number): any; + pannerAttr?(o?: any, id?: number): any; +} + // Export for ESM export * from './types'; -export { Howler, Howl, Sound }; +export { Howl, Howler, Sound }; export default { Howler, Howl, Sound }; diff --git a/src/index.ts b/src/index.ts index 041c2c46..11abd614 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,9 +4,9 @@ */ // Core library exports -export { Howler, Howl, Sound } from './howler.core'; +export { Howl, Howler, Sound } from './howler.core'; export type { HowlOptions } from './types'; // Plugin system exports -export { PluginManager, HowlerPlugin, globalPluginManager } from './plugins'; +export { globalPluginManager, HowlerPlugin, PluginManager } from './plugins'; export type { PluginHooks } from './plugins'; diff --git a/src/plugins/plugin.ts b/src/plugins/plugin.ts index d2169940..a81c3a9f 100644 --- a/src/plugins/plugin.ts +++ b/src/plugins/plugin.ts @@ -8,7 +8,7 @@ * MIT License */ -import type { HowlerGlobal, Howl, Sound } from '../howler.core'; +import type { Howl, HowlerGlobal, Sound } from '../howler.core'; import type { HowlOptions } from '../types'; /** @@ -87,6 +87,7 @@ export interface RegisteredPlugin { export class PluginManager { private plugins: Map = new Map(); private hookHistory: Map = new Map(); + private howlerInstance: HowlerGlobal | null = null; /** * Register a plugin @@ -107,10 +108,12 @@ export class PluginManager { this.plugins.set(plugin.name, registered); // Execute onRegister hook if provided + // Plugins can use this hook to initialize themselves, even if Howler is + // already initialized. The howlerInstance is available via getHowlerInstance() if (hooks.onRegister) { try { hooks.onRegister(); - } catch (error) { + } catch (error: unknown) { console.error(`Error during onRegister for plugin "${plugin.name}":`, error); } } @@ -147,21 +150,26 @@ export class PluginManager { } /** - * Get all registered plugins + * Set the Howler instance reference for late-registered plugins + * @internal */ - getPlugins(): ReadonlyMap { - return new Map(this.plugins); + setHowlerInstance(howler: HowlerGlobal): void { + this.howlerInstance = howler; } /** - * Execute onHowlerInit hooks + * Get the Howler instance (if initialized) + * This can be used by plugins in their onRegister hook to apply initialization */ - executeHowlerInit(howler: HowlerGlobal): void { - this._executeHooks('onHowlerInit', (hooks) => { - if (hooks.onHowlerInit) { - hooks.onHowlerInit(howler); - } - }); + getHowlerInstance(): HowlerGlobal | null { + return this.howlerInstance; + } + + /** + * Get all registered plugins + */ + getPlugins(): ReadonlyMap { + return new Map(this.plugins); } /** diff --git a/src/plugins/spatial-plugin.ts b/src/plugins/spatial-plugin.ts index c90103e1..6a1290fb 100644 --- a/src/plugins/spatial-plugin.ts +++ b/src/plugins/spatial-plugin.ts @@ -9,43 +9,69 @@ * MIT License */ -import { HowlerPlugin, type PluginHooks } from './plugin'; -import { Howl, HowlerGlobal, Sound } from '../howler.core'; import type { HowlOptions } from '../howler.core'; +import { Howl, HowlerGlobal } from '../howler.core'; +import { HowlerPlugin, type PluginHooks, globalPluginManager } from './plugin'; /** - * Extended Howler class with spatial audio support + * Spatial audio properties */ -export class SpatialHowler extends (HowlerGlobal as any) { - _pos: [number, number, number] = [0, 0, 0]; - _orientation: [number, number, number, number, number, number] = [0, 0, -1, 0, 1, 0]; +export interface SpatialAudioState { + _pos: [number, number, number]; + _orientation: [number, number, number, number, number, number]; + _stereo?: number; +} - stereo(pan?: number): this | number { - if (!this.ctx || !this.ctx.listener) { - console.warn('Spatial audio unavailable: Web Audio API not supported'); - return this; - } +/** + * Howler instance with spatial audio capabilities + */ +export type SpatialHowler = HowlerGlobal & SpatialAudioState & { + pos(x?: number, y?: number, z?: number): any; + orientation(x?: number, y?: number, z?: number, xUp?: number, yUp?: number, zUp?: number): any; + stereo(pan?: number): any; +}; - if (typeof pan === 'number') { - for (let i = 0; i < this._howls.length; i++) { - (this._howls[i] as any).stereo?.(pan); - } - return this; - } +/** + * Howl instance with spatial audio capabilities + */ +export type SpatialHowl = Howl & { + _pos?: [number, number, number] | null; + _orientation?: [number, number, number]; + _stereo?: number | null; + _pannerAttr?: any; + pos(x?: number, y?: number, z?: number, id?: number): any; + orientation(x?: number, y?: number, z?: number, id?: number): any; + stereo(pan?: number, id?: number): any; + pannerAttr(o?: any, id?: number): any; +}; - return (this as any)._stereo ?? 0; - } +/** + * Mixin function to add spatial audio to HowlerGlobal (listener) + */ +export function withSpatialListener( + instance: T +): T & SpatialAudioState & { + pos(x?: number, y?: number, z?: number): any; + orientation(x?: number, y?: number, z?: number, xUp?: number, yUp?: number, zUp?: number): any; + stereo(pan?: number): any; +} { + const spatial = instance as any; - pos(x?: number, y?: number, z?: number): this | [number, number, number] { + // Initialize spatial properties + spatial._pos = [0, 0, 0]; + spatial._orientation = [0, 0, -1, 0, 1, 0]; + + // Add pos method to set listener position + spatial.pos = function (x?: number, y?: number, z?: number) { if (!this.ctx || !this.ctx.listener) { console.warn('Spatial audio unavailable: Web Audio API not supported'); return this; } if (typeof x === 'number') { - const y_val = typeof y === 'number' ? y : this._pos[1]; - const z_val = typeof z === 'number' ? z : this._pos[2]; - this._pos = [x, y_val, z_val]; + const y_val = typeof y === 'number' ? y : spatial._pos[1]; + const z_val = typeof z === 'number' ? z : spatial._pos[2]; + spatial._pos = [x, y_val, z_val]; // Set listener position using appropriate API if (typeof this.ctx.listener.positionX !== 'undefined') { @@ -59,30 +85,31 @@ export class SpatialHowler extends (HowlerGlobal as any) { return this; } - return this._pos; - } + return spatial._pos; + }; - orientation( + // Add orientation method to set listener orientation + spatial.orientation = function ( x?: number, y?: number, z?: number, xUp?: number, yUp?: number, zUp?: number - ): this | [number, number, number, number, number, number] { + ) { if (!this.ctx || !this.ctx.listener) { console.warn('Spatial audio unavailable: Web Audio API not supported'); return this; } if (typeof x === 'number') { - const or = this._orientation; + const or = spatial._orientation; const y_val = typeof y === 'number' ? y : or[1]; const z_val = typeof z === 'number' ? z : or[2]; const xUp_val = typeof xUp === 'number' ? xUp : or[3]; const yUp_val = typeof yUp === 'number' ? yUp : or[4]; const zUp_val = typeof zUp === 'number' ? zUp : or[5]; - this._orientation = [x, y_val, z_val, xUp_val, yUp_val, zUp_val]; + spatial._orientation = [x, y_val, z_val, xUp_val, yUp_val, zUp_val]; // Set listener orientation using appropriate API if (typeof this.ctx.listener.forwardX !== 'undefined') { @@ -93,33 +120,63 @@ export class SpatialHowler extends (HowlerGlobal as any) { this.ctx.listener.upY.setTargetAtTime(yUp_val, this.ctx.currentTime, 0.1); this.ctx.listener.upZ.setTargetAtTime(zUp_val, this.ctx.currentTime, 0.1); } else { - (this.ctx.listener as any).setOrientation(x, y_val, z_val, xUp_val, yUp_val, zUp_val); + (this.ctx.listener as any).setOrientation( + x, + y_val, + z_val, + xUp_val, + yUp_val, + zUp_val + ); } return this; } - return this._orientation; - } + return spatial._orientation; + }; + + // Add stereo method + spatial.stereo = function (pan?: number) { + if (!this.ctx || !this.ctx.listener) { + console.warn('Spatial audio unavailable: Web Audio API not supported'); + return this; + } + + if (typeof pan === 'number') { + for (let i = 0; i < this._howls.length; i++) { + (this._howls[i] as any).stereo?.(pan); + } + return this; + } + + return spatial._stereo ?? 0; + }; + + return spatial; } /** - * Extended Howl class with spatial audio support + * Mixin function to add spatial audio to Howl instances */ -export class SpatialHowl extends Howl { - _orientation: [number, number, number] = [1, 0, 0]; - _stereo: number | null = null; - _pos: [number, number, number] | null = null; - _pannerAttr: { - coneInnerAngle: number; - coneOuterAngle: number; - coneOuterGain: number; - distanceModel: string; - maxDistance: number; - refDistance: number; - rolloffFactor: number; - panningModel: string; - } = { +export function withSpatialHowl( + instance: T +): T & { + _orientation: [number, number, number]; + _stereo: number | null; + _pos: [number, number, number] | null; + _pannerAttr: any; + pos(x?: number, y?: number, z?: number, id?: number): any; + orientation(x?: number, y?: number, z?: number, id?: number): any; + stereo(pan?: number, id?: number): any; + pannerAttr(o?: any, id?: number): any; +} { + const spatial = instance as any; + + spatial._orientation = [1, 0, 0]; + spatial._stereo = null; + spatial._pos = null; + spatial._pannerAttr = { coneInnerAngle: 360, coneOuterAngle: 360, coneOuterGain: 0, @@ -130,120 +187,66 @@ export class SpatialHowl extends Howl { panningModel: 'HRTF', }; - constructor(o: HowlOptions & any) { - super(o); - // Initialize spatial properties from options - this._orientation = o.orientation || [1, 0, 0]; - this._stereo = o.stereo || null; - this._pos = o.pos || null; - this._pannerAttr = { - coneInnerAngle: o.coneInnerAngle ?? 360, - coneOuterAngle: o.coneOuterAngle ?? 360, - coneOuterGain: o.coneOuterGain ?? 0, - distanceModel: o.distanceModel ?? 'inverse', - maxDistance: o.maxDistance ?? 10000, - refDistance: o.refDistance ?? 1, - rolloffFactor: o.rolloffFactor ?? 1, - panningModel: o.panningModel ?? 'HRTF', - }; - } - - stereo(pan?: number, id?: number): this | number { + spatial.stereo = function (pan?: number, id?: number) { if (typeof pan === 'number') { - this._stereo = pan; - // Apply to all sounds - for (let i = 0; i < this._sounds.length; i++) { - (this._sounds[i] as any)._stereo = pan; - } + spatial._stereo = pan; return this; } - return this._stereo ?? 0; - } + return spatial._stereo ?? 0; + }; - pos(x?: number, y?: number, z?: number, id?: number): this | [number, number, number] { + spatial.pos = function (x?: number, y?: number, z?: number, id?: number) { if (typeof x === 'number') { - const y_val = typeof y === 'number' ? y : (this._pos?.[1] ?? 0); - const z_val = typeof z === 'number' ? z : (this._pos?.[2] ?? 0); - this._pos = [x, y_val, z_val]; + const y_val = typeof y === 'number' ? y : (spatial._pos?.[1] ?? 0); + const z_val = typeof z === 'number' ? z : (spatial._pos?.[2] ?? 0); + spatial._pos = [x, y_val, z_val]; return this; } - return this._pos ?? [0, 0, 0]; - } + return spatial._pos ?? [0, 0, 0]; + }; - orientation(x?: number, y?: number, z?: number, id?: number): this | [number, number, number] { + spatial.orientation = function (x?: number, y?: number, z?: number, id?: number) { if (typeof x === 'number') { - const y_val = typeof y === 'number' ? y : this._orientation[1]; - const z_val = typeof z === 'number' ? z : this._orientation[2]; - this._orientation = [x, y_val, z_val]; + const y_val = typeof y === 'number' ? y : spatial._orientation[1]; + const z_val = typeof z === 'number' ? z : spatial._orientation[2]; + spatial._orientation = [x, y_val, z_val]; return this; } - return this._orientation; - } + return spatial._orientation; + }; - pannerAttr(o?: any, id?: number): any { + spatial.pannerAttr = function (o?: any, id?: number) { if (o) { - Object.assign(this._pannerAttr, o); + Object.assign(spatial._pannerAttr, o); return this; } - return this._pannerAttr; - } -} - -/** - * Extended Sound class with spatial audio support - */ -export class SpatialSound extends Sound { - _orientation: [number, number, number] = [1, 0, 0]; - _stereo: number | null = null; - _pos: [number, number, number] | null = null; - _pannerAttr: { - coneInnerAngle: number; - coneOuterAngle: number; - coneOuterGain: number; - distanceModel: string; - maxDistance: number; - refDistance: number; - rolloffFactor: number; - panningModel: string; - } = { - coneInnerAngle: 360, - coneOuterAngle: 360, - coneOuterGain: 0, - distanceModel: 'inverse', - maxDistance: 10000, - refDistance: 1, - rolloffFactor: 1, - panningModel: 'HRTF', + return spatial._pannerAttr; }; - _panner?: PannerNode | StereoPannerNode; - - constructor(howl: Howl) { - super(howl); - // Inherit spatial properties from parent - if (howl instanceof SpatialHowl) { - this._orientation = howl._orientation; - this._stereo = howl._stereo; - this._pos = howl._pos; - this._pannerAttr = howl._pannerAttr; - } - } + + return spatial; } /** * Spatial Audio Plugin - * Adds 3D spatial audio and stereo panning capabilities to Howler + * Adds 3D spatial audio and stereo panning capabilities to Howler and Howl instances * * Usage: * ```typescript - * import { globalPluginManager } from 'howler/plugins'; + * import { Howler } from 'howler'; * import { SpatialAudioPlugin } from 'howler/plugins/spatial'; * - * globalPluginManager.register(new SpatialAudioPlugin()); + * // Register the plugin + * Howler.addPlugin(new SpatialAudioPlugin()); + * + * // Use spatial methods on Howler listener: + * Howler.pos(10, 20, 30); + * Howler.orientation(1, 0, 0, 0, 1, 0); * - * // Then use the spatial classes for your audio objects: - * const sound = new SpatialHowl({ src: 'audio.mp3' }); + * // Use spatial methods on Howl instances: + * const sound = new Howl({ src: 'audio.mp3' }); * sound.pos(10, 20, 30); * sound.stereo(0.5); + * sound.pannerAttr({ refDistance: 0.8 }); * ``` */ export class SpatialAudioPlugin extends HowlerPlugin { @@ -252,22 +255,41 @@ export class SpatialAudioPlugin extends HowlerPlugin { getHooks(): PluginHooks { return { - onHowlerInit: this.onHowlerInit.bind(this), + onRegister: this.onRegister.bind(this), + onHowlCreate: this.onHowlCreate.bind(this), }; } /** - * Initialize spatial audio global state when Howler is initialized. - * Note: Users should instantiate SpatialHowl and SpatialSound directly - * instead of using the base Howl and Sound classes when they need spatial features. + * Initialize spatial audio when the plugin is registered. + * This is called whether the Howler is already initialized or not. */ - private onHowlerInit(howler: HowlerGlobal): void { - console.info( - 'Spatial Audio Plugin registered. Use SpatialHowl and SpatialSound classes for spatial audio features.' - ); + private onRegister(): void { + // Apply the spatial audio mixin to Howler if it's initialized + const howler = globalPluginManager.getHowlerInstance(); + if (howler) { + withSpatialListener(howler); + } + } + + /** + * Extend Howl instances with spatial audio methods via mixin. + */ + private onHowlCreate(howl: Howl, _options: HowlOptions): void { + withSpatialHowl(howl); } onUnregister(): void { - // Cleanup if needed + // Remove spatial audio methods from Howler instance + const howler = globalPluginManager.getHowlerInstance(); + if (howler) { + // Remove spatial audio properties and methods + delete (howler as any)._pos; + delete (howler as any)._orientation; + delete (howler as any)._stereo; + delete (howler as any).pos; + delete (howler as any).orientation; + delete (howler as any).stereo; + } } } diff --git a/src/plugins/spatial.ts b/src/plugins/spatial.ts index 98715ad7..0815a945 100644 --- a/src/plugins/spatial.ts +++ b/src/plugins/spatial.ts @@ -2,5 +2,8 @@ * Howler.js - Spatial Plugin Entry Point */ - // Spatial Audio Plugin - export { SpatialAudioPlugin, SpatialHowler, SpatialHowl, SpatialSound } from './spatial-plugin'; +// Export the plugin for use with Howler.addPlugin() +export { SpatialAudioPlugin } from './spatial-plugin'; + +// Export spatial types for TypeScript support +export type { SpatialAudioState, SpatialHowler, SpatialHowl } from './spatial-plugin'; From ea56e584d41c703f3c836176eb8fdf4a9dc8bc64 Mon Sep 17 00:00:00 2001 From: eatsjobs Date: Sat, 22 Nov 2025 19:45:52 +0100 Subject: [PATCH 12/25] examples and tests --- examples/3d/README.md | 5 +- examples/3d/index.html | 44 ++--- examples/3d/js/camera.js | 310 +++++++++++++++--------------- examples/3d/js/controls.js | 135 +++++++------- examples/3d/js/game.js | 89 +++++---- examples/3d/js/howlerConfig.js | 29 +++ examples/3d/js/map.js | 332 ++++++++++++++++++--------------- examples/3d/js/player.js | 121 ++++++------ examples/3d/js/sound.js | 79 ++++---- examples/3d/js/texture.js | 16 +- examples/3d/js/utils.js | 7 + examples/player/index.html | 104 +++++------ examples/player/player.js | 2 +- examples/player/siriwave.js | 8 +- examples/radio/index.html | 166 ++++++++--------- examples/radio/radio.js | 121 ++++++------ tests/core.html5audio.html | 46 ++--- tests/core.webaudio.html | 44 ++--- tests/index.html | 50 ++--- tests/js/core.webaudio.js | 2 +- tests/js/spatial.js | 9 +- tests/spatial.html | 46 ++--- 22 files changed, 935 insertions(+), 830 deletions(-) create mode 100644 examples/3d/js/howlerConfig.js create mode 100644 examples/3d/js/utils.js diff --git a/examples/3d/README.md b/examples/3d/README.md index 20504951..85e48250 100644 --- a/examples/3d/README.md +++ b/examples/3d/README.md @@ -3,6 +3,7 @@ ![3D Spatial Audio](https://s3.amazonaws.com/howler.js/screenshot-3d.jpg "3D Audio Screenshot") ## Description + This example displays how to use howler.js and the Spatial Plugin to add immersive spatial audio into 3D (or other) environments. * Controls: Arrow Keys, WASD and Touch @@ -12,10 +13,12 @@ This example displays how to use howler.js and the Spatial Plugin to add immersi * All game sounds implemented with a single sound sprite and `Howl` instance. ## How-To + 1. Clone the git repo or download the source: * **Clone:** `git clone https://github.com/goldfire/howler.js.git` * **Download:** [https://github.com/goldfire/howler.js/archive/master.zip](https://github.com/goldfire/howler.js/archive/master.zip) 2. Open `index.html` in your favorite browser. ## Credits -Inspired by the great post on JavaScript ray casting on [PlayfulJS](http://www.playfuljs.com/a-first-person-engine-in-265-lines/). \ No newline at end of file + +Inspired by the great post on JavaScript ray casting on [PlayfulJS](http://www.playfuljs.com/a-first-person-engine-in-265-lines/). diff --git a/examples/3d/index.html b/examples/3d/index.html index 7fe44b18..f2e417b7 100644 --- a/examples/3d/index.html +++ b/examples/3d/index.html @@ -1,28 +1,28 @@ - + - - - - Howler.js 3D Spatial Audio - - - + - - - - + + + + - - - + + + diff --git a/examples/3d/js/camera.js b/examples/3d/js/camera.js index 39a97718..84e9d6f6 100644 --- a/examples/3d/js/camera.js +++ b/examples/3d/js/camera.js @@ -7,156 +7,168 @@ * * MIT License */ - -'use strict'; - +import { game } from "./game.js"; +import { canvas, circle, ctx, isMobile } from "./utils.js"; /** * Camera that draws everything you see on the screen from the player's perspective. * @param {Number} resolution Resolution to render at (higher has better quality, but lower performance). */ -export var Camera = function(resolution) { - this.width = canvas.width = window.innerWidth; - this.height = canvas.height = window.innerHeight; - this.resolution = resolution; - this.spacing = this.width / resolution; - this.focalLen = this.height / this.width; - this.range = isMobile ? 9 : 18; - this.lightRange = 9; - this.scale = canvas.width / 1200; -}; -Camera.prototype = { - /** - * Draw the skybox based on the player's direction. - */ - drawSky: function() { - var dir = game.player.dir; - var sky = game.map.skybox; - var ambient = game.map.light; - var width = sky.width * (this.height / sky.height) * 2; - var left = (dir / circle) * -width; - - ctx.save(); - ctx.drawImage(sky.image, left, 0, width, this.height); - if (left < width - this.width) { - ctx.drawImage(sky.image, left + width, 0, width, this.height); - } - if (ambient > 0) { - ctx.fillStyle = '#fff'; - ctx.globalAlpha = ambient * 0.1; - ctx.fillRect(0, this.height * 0.5, this.width, this.height * 0.5); - } - ctx.restore(); - }, - - /** - * Based on the resolution, split the scene up and draw it column by column. - */ - drawCols: function() { - var x, angle, ray; - - ctx.save(); - - for (var col=0; col=0; i--) { - step = ray[i]; - drops = Math.pow(Math.random(), 100) * i; - rain = (drops > 0) && this.project(0.2, angle, step.dist); - - var tex = (step.type === 1) ? tex1 : tex2; - - if (i === hit) { - texX = Math.floor(tex.width * step.offset); - wall = this.project(step.height, angle, step.dist); - - ctx.globalAlpha = 1; - ctx.drawImage(tex.image, texX, 0, 1, tex.height, left, wall.top, width, wall.height); - - ctx.fillStyle = '#000'; - ctx.globalAlpha = Math.max((step.dist + step.shading) / this.lightRange - game.map.light, 0); - ctx.fillRect(left, wall.top, width, wall.height); - } - - ctx.fillStyle = '#fff'; - ctx.globalAlpha = 0.15; - while (--drops > 0) { - ctx.fillRect(left, Math.random() * rain.top, 1, rain.height); - } - } - }, - - /** - * Draw the hand holding the gun and implement a "bobbing" to simulate walking. - */ - drawHand: function() { - var hand = game.player.hand; - var steps = game.player.steps; - var scaleFactor = this.scale * 6; - - // Calculate the position of each hand relative to the steps taken. - var xScale = Math.cos(steps * 2); - var yScale = Math.sin(steps * 4); - var bobX = xScale * scaleFactor; - var bobY = yScale * scaleFactor; - var x = (canvas.width - (hand.width * this.scale) + scaleFactor) + bobX; - var y = (canvas.height - (hand.height * this.scale) + scaleFactor) + bobY; - var w = hand.width * this.scale; - var h = hand.height * this.scale; - - ctx.drawImage(hand.image, x, y, w, h); - }, - - /** - * Based on the angle and distance, determine how we are going to project the image. - * @param {Number} height Wall piece height. - * @param {Number} angle Angle of the ray. - * @param {Number} dist Distnace from the player. - * @return {Object} top and height - */ - project: function(height, angle, dist) { - var z = dist * Math.cos(angle); - var wallH = this.height * height / z; - var bottom = this.height / 2 * (1 + 1 / z); - - return { - top: bottom - wallH, - height: wallH - }; - }, - - /** - * Render the sky, walls and hand in the correct order. - */ - render: function() { - this.drawSky(); - this.drawCols(); - this.drawHand(); - } -}; +export class Camera { + constructor(resolution) { + this.width = canvas.width = window.innerWidth; + this.height = canvas.height = window.innerHeight; + this.resolution = resolution; + this.spacing = this.width / resolution; + this.focalLen = this.height / this.width; + this.range = isMobile ? 9 : 18; + this.lightRange = 9; + this.scale = canvas.width / 1200; + } + + drawSky() { + var dir = game.player.dir; + var sky = game.map.skybox; + var ambient = game.map.light; + var width = sky.width * (this.height / sky.height) * 2; + var left = (dir / circle) * -width; + + ctx.save(); + ctx.drawImage(sky.image, left, 0, width, this.height); + if (left < width - this.width) { + ctx.drawImage(sky.image, left + width, 0, width, this.height); + } + if (ambient > 0) { + ctx.fillStyle = "#fff"; + ctx.globalAlpha = ambient * 0.1; + ctx.fillRect(0, this.height * 0.5, this.width, this.height * 0.5); + } + ctx.restore(); + + } + + /** + * Based on the resolution, split the scene up and draw it column by column. + */ + drawCols() { + var x, angle, ray; + + ctx.save(); + + for (var col = 0; col < this.resolution; col++) { + x = col / this.resolution - 0.5; + angle = Math.atan2(x, this.focalLen); + ray = game.map.cast(game.player, game.player.dir + angle, this.range); + + this.drawCol(col, ray, angle); + } + + ctx.restore(); + } + + + /** + * Draw a single column of the scene. + * @param {Number} col Which column in the sequence. + * @param {Array} ray Ray to follow. + * @param {Number} angle Angle of the ray. + */ + drawCol(col, ray, angle) { + var step, drops, rain, texX, wall; + var tex1 = game.map.wall; + var tex2 = game.map.speaker; + var left = Math.floor(col * this.spacing); + var width = Math.ceil(this.spacing); + var hit = -1; + + // Find the next wall hit. + while (++hit < ray.length && ray[hit].height <= 0); + + // Draw the wall sections and rain drops. + for (var i = ray.length - 1; i >= 0; i--) { + step = ray[i]; + drops = Math.random() ** 100 * i; + rain = drops > 0 && this.project(0.2, angle, step.dist); + + var tex = step.type === 1 ? tex1 : tex2; + + if (i === hit) { + texX = Math.floor(tex.width * step.offset); + wall = this.project(step.height, angle, step.dist); + + ctx.globalAlpha = 1; + ctx.drawImage( + tex.image, + texX, + 0, + 1, + tex.height, + left, + wall.top, + width, + wall.height, + ); + + ctx.fillStyle = "#000"; + ctx.globalAlpha = Math.max( + (step.dist + step.shading) / this.lightRange - game.map.light, + 0, + ); + ctx.fillRect(left, wall.top, width, wall.height); + } + + ctx.fillStyle = "#fff"; + ctx.globalAlpha = 0.15; + while (--drops > 0) { + ctx.fillRect(left, Math.random() * rain.top, 1, rain.height); + } + } + } + + /** + * Draw the hand holding the gun and implement a "bobbing" to simulate walking. + */ + drawHand() { + var hand = game.player.hand; + var steps = game.player.steps; + var scaleFactor = this.scale * 6; + + // Calculate the position of each hand relative to the steps taken. + var xScale = Math.cos(steps * 2); + var yScale = Math.sin(steps * 4); + var bobX = xScale * scaleFactor; + var bobY = yScale * scaleFactor; + var x = canvas.width - hand.width * this.scale + scaleFactor + bobX; + var y = canvas.height - hand.height * this.scale + scaleFactor + bobY; + var w = hand.width * this.scale; + var h = hand.height * this.scale; + + ctx.drawImage(hand.image, x, y, w, h); + } + + /** + * Based on the angle and distance, determine how we are going to project the image. + * @param {Number} height Wall piece height. + * @param {Number} angle Angle of the ray. + * @param {Number} dist Distnace from the player. + * @return {Object} top and height + */ + project(height, angle, dist) { + var z = dist * Math.cos(angle); + var wallH = (this.height * height) / z; + var bottom = (this.height / 2) * (1 + 1 / z); + + return { + top: bottom - wallH, + height: wallH, + }; + } + + /** + * Render the sky, walls and hand in the correct order. + */ + render() { + this.drawSky(); + this.drawCols(); + this.drawHand(); + } +} \ No newline at end of file diff --git a/examples/3d/js/controls.js b/examples/3d/js/controls.js index fac5f3ef..f76c6b63 100644 --- a/examples/3d/js/controls.js +++ b/examples/3d/js/controls.js @@ -8,79 +8,84 @@ * MIT License */ -'use strict'; - /** * Defines and handles the various controls. */ -export var Controls = function() { - // Define our control key codes and states. - this.codes = { - // Arrows - 37: 'left', 39: 'right', 38: 'front', 40: 'back', - // WASD - 65: 'left', 68: 'right', 87: 'front', 83: 'back', - }; - this.states = {left: false, right: false, front: false, back: false}; +export class Controls { + constructor() { + // Define our control key codes and states. + this.codes = { + // Arrows + 37: "left", + 39: "right", + 38: "front", + 40: "back", + // WASD + 65: "left", + 68: "right", + 87: "front", + 83: "back", + }; + this.states = { left: false, right: false, front: false, back: false }; + + // Setup the DOM listeners. + document.addEventListener("keydown", this.key.bind(this, true), false); + document.addEventListener("keyup", this.key.bind(this, false), false); + document.addEventListener("touchstart", this.touch.bind(this), false); + document.addEventListener("touchmove", this.touch.bind(this), false); + document.addEventListener("touchend", this.touchEnd.bind(this), false); + } - // Setup the DOM listeners. - document.addEventListener('keydown', this.key.bind(this, true), false); - document.addEventListener('keyup', this.key.bind(this, false), false); - document.addEventListener('touchstart', this.touch.bind(this), false); - document.addEventListener('touchmove', this.touch.bind(this), false); - document.addEventListener('touchend', this.touchEnd.bind(this), false); -}; -Controls.prototype = { - /** - * Handle all keydown and keyup events and update our internal controls state. - * @param {Boolean} pressed Whether or not the key is being pressed. - * @param {Object} event DOM event data including the key being pressed. - */ - key: function(pressed, event) { - var state = this.codes[event.keyCode]; + /** + * Handle all keydown and keyup events and update our internal controls state. + * @param {Boolean} pressed Whether or not the key is being pressed. + * @param {Object} event DOM event data including the key being pressed. + */ + key(pressed, event) { + const state = this.codes[event.keyCode]; - if (!state) { - return; - } + if (!state) { + return; + } - this.states[state] = pressed; - event.preventDefault && event.preventDefault(); - event.stopPropagation && event.stopPropagation(); - }, + this.states[state] = pressed; + event.preventDefault && event.preventDefault(); + event.stopPropagation && event.stopPropagation(); + } - /** - * Listen for touch events and determine which key to simulate. - * @param {Object} event DOM event data including the position touched. - */ - touch: function(event) { - var touches = event.touches[0]; + /** + * Listen for touch events and determine which key to simulate. + * @param {Object} event DOM event data including the position touched. + */ + touch(event) { + const touches = event.touches[0]; - // Reset the states. - this.touchEnd(event); + // Reset the states. + this.touchEnd(event); - // Determine which key to simulate. - if (touches.pageY < window.innerHeight * 0.3) { - this.key(true, {keyCode: 38}); - } else if (touches.pageY > window.innerHeight * 0.7) { - this.key(true, {keyCode: 40}); - } else if (touches.pageX < window.innerWidth * 0.5) { - this.key(true, {keyCode: 37}); - } else if (touches.pageX > window.innerWidth * 0.5) { - this.key(true, {keyCode: 39}); - } - }, + // Determine which key to simulate. + if (touches.pageY < window.innerHeight * 0.3) { + this.key(true, { keyCode: 38 }); + } else if (touches.pageY > window.innerHeight * 0.7) { + this.key(true, { keyCode: 40 }); + } else if (touches.pageX < window.innerWidth * 0.5) { + this.key(true, { keyCode: 37 }); + } else if (touches.pageX > window.innerWidth * 0.5) { + this.key(true, { keyCode: 39 }); + } + } - /** - * Fired to reset all key statuses based on no fingers being on the screen. - * @param {Object} event DOM event data including the position touched. - */ - touchEnd: function(event) { - this.states.left = false; - this.states.right = false; - this.states.front = false; - this.states.back = false; + /** + * Fired to reset all key statuses based on no fingers being on the screen. + * @param {Object} event DOM event data including the position touched. + */ + touchEnd(event) { + this.states.left = false; + this.states.right = false; + this.states.front = false; + this.states.back = false; - event.preventDefault(); - event.stopPropagation(); - } -}; + event.preventDefault(); + event.stopPropagation(); + } +} diff --git a/examples/3d/js/game.js b/examples/3d/js/game.js index c7a2deb3..e1242648 100644 --- a/examples/3d/js/game.js +++ b/examples/3d/js/game.js @@ -8,56 +8,53 @@ * MIT License */ -'use strict'; +import { Howler } from "./howlerConfig.js"; +import { Camera } from "./camera.js"; +import { Controls } from "./controls.js"; +import { Map } from "./map.js"; +import { Player } from "./player.js"; +import { Sound } from "./sound.js"; +import { isMobile } from "./utils.js"; -import { Sound } from './sound.js'; -import { Player } from './player.js'; -import { Controls } from './controls.js'; -import { Map } from './map.js'; -import { Camera } from './camera.js'; - -// Cache some commonly used values. -var circle = Math.PI * 2; -var isMobile = /iPhone|iPad|iPod|Android|BlackBerry|BB10|Silk/i.test(navigator.userAgent); -var canvas = document.getElementById('canvas'); -var ctx = canvas.getContext('2d'); /** * Main game class that runs the tick and sets up all other components. */ -var Game = function() { - this.lastTime = 0; - - // Setup our different game components. - this.audio = new Sound(); - this.player = new Player(10, 26, Math.PI * 1.9, 2.5); - this.controls = new Controls(); - this.map = new Map(25); - this.camera = new Camera(isMobile ? 256 : 512); - - requestAnimationFrame(this.tick.bind(this)); -}; -Game.prototype = { - /** - * Main game loop that renders the full scene on each screen refresh. - * @param {Number} time - */ - tick: function(time) { - var ms = time - this.lastTime; - this.lastTime = time; - - // Update the different components of the scene. - this.map.update(ms / 1000); - this.player.update(ms / 1000); - this.camera.render(this.player, this.map); - - // Continue the game loop. - requestAnimationFrame(this.tick.bind(this)); - } -}; - -// Setup and start the new game instance. -var game = new Game(); - +class Game { + constructor() { + this.lastTime = 0; + this.Howler = Howler; + + // Setup our different game components. + this.audio = new Sound(); + this.player = new Player(10, 26, Math.PI * 1.9, 2.5); + this.controls = new Controls(); + this.map = new Map(25); + this.camera = new Camera(isMobile ? 256 : 512); + + this.tick = this.tick.bind(this); + + requestAnimationFrame(this.tick); + } + /** + * Main game loop that renders the full scene on each screen refresh. + * @param {Number} time + */ + tick(time) { + var ms = time - this.lastTime; + this.lastTime = time; + + // Update the different components of the scene. + this.map.update(ms / 1000); + this.player.update(ms / 1000); + this.camera.render(this.player, this.map); + + // Continue the game loop. + requestAnimationFrame(this.tick); + } +} // Generate the new map. +const game = new Game(); +// Setup and start the new game instance. game.map.setup(); +export { game }; diff --git a/examples/3d/js/howlerConfig.js b/examples/3d/js/howlerConfig.js new file mode 100644 index 00000000..d3aa553a --- /dev/null +++ b/examples/3d/js/howlerConfig.js @@ -0,0 +1,29 @@ +/*! + * Howler.js 3D Sound Demo - Configuration + * howlerjs.com + * + * (c) 2013-2020, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */ + +import { Howl, Howler } from 'howler'; +import { SpatialAudioPlugin } from 'howler/plugins/spatial'; + +const spatialAudioPlugin = new SpatialAudioPlugin(); +// Register the spatial audio plugin on module initialization +Howler.addPlugin(spatialAudioPlugin); + +/** + * Pre-configured Howler instance with spatial audio capabilities + * @type {import('howler/plugins/spatial').SpatialHowler} + */ +export { Howler }; + +/** + * Howl constructor for creating sound instances with spatial audio support + * @type {new (...args: any[]) => import('howler/plugins/spatial').SpatialHowl} + */ +export { Howl }; + diff --git a/examples/3d/js/map.js b/examples/3d/js/map.js index 6d3faaac..8ab202b9 100644 --- a/examples/3d/js/map.js +++ b/examples/3d/js/map.js @@ -8,159 +8,189 @@ * MIT License */ -'use strict'; +import { game } from "./game.js"; +import { Texture } from "./texture.js"; /** * Generates the map and calculates the casting of arrays for the camera to display on screen. * @param {Number} size Grid size of the map to use. */ -export var Map = function(size) { - this.size = size; - this.grid = new Array(size * size); - this.skybox = new Texture('./assets/skybox.jpg', 4096, 1024); - this.wall = new Texture('./assets/wall.jpg', 1024, 1024); - this.speaker = new Texture('./assets/speaker.jpg', 1024, 1024); - this.light = 0; - - // Define the pre-defined map template on a 25x25 grid. - this.grid = [1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1]; -}; -Map.prototype = { - /** - * Sets up the map including the speaker audio points. - */ - setup: function() { - // Loop through the tiles and setup the audio listeners. - for (var i=0; i this.size - 1 || y < 0 || y > this.size - 1) { - return -1; - } - - return this.grid[y * this.size + x]; - }, - - /** - * Emit a ray to beginb uilding the scene. - * @param {Number} sin Sine of the cast angle. - * @param {Number} cos Cosine of the cast angle. - * @param {Number} range Max length of the ray. - * @param {Object} origin x, y, height and sitance - */ - ray: function(sin, cos, range, origin) { - var stepX = this.step(sin, cos, origin.x, origin.y, false); - var stepY = this.step(cos, sin, origin.y, origin.x, true); - - var inspectX = [sin, cos, stepX, 1, 0, origin.dist, stepX.y]; - var inspectY = [sin, cos, stepY, 0, 1, origin.dist, stepY.x]; - var next = this.inspect.apply(this, (stepX.len2 < stepY.len2) ? inspectX : inspectY); - - if (next.dist > range) { - return [origin]; - } - - return [origin].concat(this.ray(sin, cos, range, next)); - }, - - /** - * Processes each step along the ray. - * @param {Number} rise Slope of line: sine of the cast angle. - * @param {Number} run Slope of line: cosine of the cast angle. - * @param {Number} x Origin x-position. - * @param {Number} y Origin y-position. - * @param {Boolean} inverted - */ - step: function(rise, run, x, y, inverted) { - if (run === 0) { - return {len2: Infinity}; - } - - var dx = run > 0 ? Math.floor(x + 1) - x : Math.ceil(x - 1) - x; - var dy = dx * (rise / run); - - return { - x: inverted ? y + dy : x + dx, - y: inverted ? x + dx : y + dy, - len2: dx * dx + dy * dy - }; - }, - - /** - * Inspect the next position to determine distance, height, shading, etc. - * @param {Number} sin Sine of the cast angle. - * @param {Number} cos Cosine of the cast angle. - * @param {Object} step x, y and length of the step. - * @param {Number} shiftX X shifted by 1 or 0. - * @param {Number} shiftY Y shifted by 1 or 0. - * @param {Number} dist Distnace from origin. - * @param {Number} offset Step offset. - */ - inspect: function(sin, cos, step, shiftX, shiftY, dist, offset) { - var dx = (cos < 0) ? shiftX : 0; - var dy = (sin < 0) ? shiftY : 0; - - step.type = this.check(step.x - dx, step.y - dy); - step.height = (step.type) > 0 ? 1 : 0; - step.dist = dist + Math.sqrt(step.len2); - - if (shiftX) { - step.shading = (cos < 0) ? 2 : 0; - } else { - step.shading = (sin < 0) ? 2 : 1; - } - - step.offset = offset - Math.floor(offset); - - return step; - }, - - /** - * Casts a ray from the camera and returns the results. - * @param {Object} point Player/camera's x/y position. - * @param {Number} angle Angle (in radians) of camera. - * @param {Number} range Max length of the ray. - */ - cast: function(point, angle, range) { - var sin = Math.sin(angle); - var cos = Math.cos(angle); - - return this.ray(sin, cos, range, { - x: point.x, - y: point.y, - height: 0, - dist: 0 - }); - }, - - /** - * Update loop on the map, in this case used to add in lightning by adjusting global lighting. - * @param {Number} secs Seconds since last tick. - */ - update: function(secs) { - if (this.light > 0) { - this.light = Math.max(this.light - 10 * secs, 0); - } else if (Math.random() * 6 < secs) { - this.light = 2; - - // Play the lightning sound. - game.audio.lightning(); - } - } -}; +export class Map { + constructor(size) { + this.size = size; + this.grid = new Array(size * size); + this.skybox = new Texture("./assets/skybox.jpg", 4096, 1024); + this.wall = new Texture("./assets/wall.jpg", 1024, 1024); + this.speaker = new Texture("./assets/speaker.jpg", 1024, 1024); + this.light = 0; + + // Define the pre-defined map template on a 25x25 grid. + this.grid = [ + 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, + 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, + 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, + 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, + 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, + 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, + 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, + 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, + 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, + 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, + 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, + 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, + 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, + ]; + } + /** + * Sets up the map including the speaker audio points. + */ + setup() { + // Loop through the tiles and setup the audio listeners. + for (var i = 0; i < this.grid.length; i++) { + if (this.grid[i] === 2) { + var y = Math.floor(i / this.size); + var x = i % this.size; + game.audio.speaker(x, y); + } + } + } + + /** + * + * @param {number} x + * @param {number} y + * @returns + */ + check(x, y) { + x = Math.floor(x); + y = Math.floor(y); + + if (x < 0 || x > this.size - 1 || y < 0 || y > this.size - 1) { + return -1; + } + + return this.grid[y * this.size + x]; + } + + /** + * Emit a ray to beginb uilding the scene. + * @param {Number} sin Sine of the cast angle. + * @param {Number} cos Cosine of the cast angle. + * @param {Number} range Max length of the ray. + * @param {Object} origin x, y, height and sitance + */ + ray(sin, cos, range, origin) { + var stepX = this.step(sin, cos, origin.x, origin.y, false); + var stepY = this.step(cos, sin, origin.y, origin.x, true); + + var inspectX = [sin, cos, stepX, 1, 0, origin.dist, stepX.y]; + var inspectY = [sin, cos, stepY, 0, 1, origin.dist, stepY.x]; + var next = this.inspect.apply( + this, + stepX.len2 < stepY.len2 ? inspectX : inspectY, + ); + + if (next.dist > range) { + return [origin]; + } + + return [origin].concat(this.ray(sin, cos, range, next)); + } + + /** + * Processes each step along the ray. + * @param {Number} rise Slope of line: sine of the cast angle. + * @param {Number} run Slope of line: cosine of the cast angle. + * @param {Number} x Origin x-position. + * @param {Number} y Origin y-position. + * @param {Boolean} inverted + */ + step(rise, run, x, y, inverted) { + if (run === 0) { + return { len2: Infinity }; + } + + var dx = run > 0 ? Math.floor(x + 1) - x : Math.ceil(x - 1) - x; + var dy = dx * (rise / run); + + return { + x: inverted ? y + dy : x + dx, + y: inverted ? x + dx : y + dy, + len2: dx * dx + dy * dy, + }; + } + + /** + * Inspect the next position to determine distance, height, shading, etc. + * @param {Number} sin Sine of the cast angle. + * @param {Number} cos Cosine of the cast angle. + * @param {Object} step x, y and length of the step. + * @param {Number} shiftX X shifted by 1 or 0. + * @param {Number} shiftY Y shifted by 1 or 0. + * @param {Number} dist Distnace from origin. + * @param {Number} offset Step offset. + */ + inspect(sin, cos, step, shiftX, shiftY, dist, offset) { + var dx = cos < 0 ? shiftX : 0; + var dy = sin < 0 ? shiftY : 0; + + step.type = this.check(step.x - dx, step.y - dy); + step.height = step.type > 0 ? 1 : 0; + step.dist = dist + Math.sqrt(step.len2); + + if (shiftX) { + step.shading = cos < 0 ? 2 : 0; + } else { + step.shading = sin < 0 ? 2 : 1; + } + + step.offset = offset - Math.floor(offset); + + return step; + } + + /** + * Casts a ray from the camera and returns the results. + * @param {Object} point Player/camera's x/y position. + * @param {Number} angle Angle (in radians) of camera. + * @param {Number} range Max length of the ray. + */ + cast(point, angle, range) { + var sin = Math.sin(angle); + var cos = Math.cos(angle); + + return this.ray(sin, cos, range, { + x: point.x, + y: point.y, + height: 0, + dist: 0, + }); + } + + /** + * Update loop on the map, in this case used to add in lightning by adjusting global lighting. + * @param {Number} secs Seconds since last tick. + */ + update(secs) { + if (this.light > 0) { + this.light = Math.max(this.light - 10 * secs, 0); + } else if (Math.random() * 6 < secs) { + this.light = 2; + + // Play the lightning sound. + game.audio.lightning(); + } + } +} diff --git a/examples/3d/js/player.js b/examples/3d/js/player.js index 5a812ec4..edb2fb5b 100644 --- a/examples/3d/js/player.js +++ b/examples/3d/js/player.js @@ -7,73 +7,78 @@ * * MIT License */ - -'use strict'; - +import { game } from "./game.js"; +import { Howler } from "./howlerConfig.js"; +import { Texture } from "./texture.js"; +import { circle } from "./utils.js"; /** * The player from which we cast the rays. - * @param {Number} x Starting x-position. - * @param {Number} y Starting y-position. - * @param {Number} dir Direction they are facing in radians. - * @param {Number} speed Speed they walk at. + * @class */ -export var Player = function(x, y, dir, speed) { - this.x = x; - this.y = y; - this.dir = dir; - this.speed = speed || 3; - this.steps = 0; - this.hand = new Texture('./assets/gun.png', 512, 360); +export class Player { + /** + * @param {Number} x Starting x-position. + * @param {Number} y Starting y-position. + * @param {Number} dir Direction they are facing in radians. + * @param {Number} speed Speed they walk at. + */ + constructor(x, y, dir, speed) { + this.x = x; + this.y = y; + this.dir = dir; + this.speed = speed || 3; + this.steps = 0; + this.hand = new Texture("./assets/gun.png", 512, 360); + + // Update the position of the audio listener. + Howler.pos(this.x, this.y, -0.5); - // Update the position of the audio listener. - Howler.pos(this.x, this.y, -0.5); + // Update the direction and orientation. + this.rotate(dir); + } - // Update the direction and orientation. - this.rotate(dir); -}; -Player.prototype = { - /** - * Rotate the player's viewing direction. - * @param {Number} angle Angle to rotate by. - */ - rotate: function(angle) { - this.dir = (this.dir + angle + circle) % circle; + /** + * Rotate the player's viewing direction. + * @param {Number} angle Angle to rotate by. + */ + rotate(angle) { + this.dir = (this.dir + angle + circle) % circle; - // Calculate the rotation vector and update the orientation of the listener. - var x = Math.cos(this.dir); - var y = 0; - var z = Math.sin(this.dir); - Howler.orientation(x, y, z, 0, 1, 0); - }, + // Calculate the rotation vector and update the orientation of the listener. + const x = Math.cos(this.dir); + const y = 0; + const z = Math.sin(this.dir); + Howler.orientation(x, y, z, 0, 1, 0); + } - /** - * Handle walking based on the state of inputs. - * @param {Number} dist Distance to walk based on time elapsed. - */ - walk: function(dist) { - var dx = Math.cos(this.dir) * dist; - var dy = Math.sin(this.dir) * dist; + /** + * Handle walking based on the state of inputs. + * @param {Number} dist Distance to walk based on time elapsed. + */ + walk(dist) { + const dx = Math.cos(this.dir) * dist; + const dy = Math.sin(this.dir) * dist; - // Move the player if they can walk here. - this.x += (game.map.check(this.x + dx, this.y) <= 0) ? dx : 0; - this.y += (game.map.check(this.x, this.y + dy) <= 0) ? dy : 0; + // Move the player if they can walk here. + this.x += game.map.check(this.x + dx, this.y) <= 0 ? dx : 0; + this.y += game.map.check(this.x, this.y + dy) <= 0 ? dy : 0; - this.steps += dist; + this.steps += dist; - // Update the position of the audio listener. - Howler.pos(this.x, this.y, -0.5); - }, + // Update the position of the audio listener. + Howler.pos(this.x, this.y, -0.5); + } - /** - * Update the player position and rotation on each tick. - * @param {Number} secs Seconds since last update. - */ - update: function(secs) { - var states = game.controls.states; + /** + * Update the player position and rotation on each tick. + * @param {Number} secs Seconds since last update. + */ + update(secs) { + const states = game.controls.states; - if (states.left) this.rotate(-Math.PI * secs); - if (states.right) this.rotate(Math.PI * secs); - if (states.front) this.walk(this.speed * secs); - if (states.back) this.walk(-this.speed * secs); - } -}; + if (states.left) this.rotate(-Math.PI * secs); + if (states.right) this.rotate(Math.PI * secs); + if (states.front) this.walk(this.speed * secs); + if (states.back) this.walk(-this.speed * secs); + } +} diff --git a/examples/3d/js/sound.js b/examples/3d/js/sound.js index 37ead799..fda379ac 100644 --- a/examples/3d/js/sound.js +++ b/examples/3d/js/sound.js @@ -8,81 +8,80 @@ * MIT License */ -'use strict'; - -import { SpatialHowl } from 'howler/plugins/spatial'; - +import { Howl } from './howlerConfig.js'; +import { game } from './game.js'; /** * Setup and control all of the game's audio. */ -export var Sound = function() { - // Setup the shared SpatialHowl. - this.sound = new SpatialHowl({ - src: ['./assets/sprite.webm', './assets/sprite.mp3'], - sprite: { - lightning: [2000, 4147], - rain: [8000, 9962, true], - thunder: [19000, 13858], - music: [34000, 31994, true] - }, - volume: 0 - }); +export class Sound { + constructor() { + // Setup the shared Howl with spatial audio capabilities (via plugin). + this.sound = new Howl({ + src: ['./assets/sprite.webm', './assets/sprite.mp3'], + sprite: { + lightning: [2000, 4147], + rain: [8000, 9962, true], + thunder: [19000, 13858], + music: [34000, 31994, true] + }, + volume: 0 + }); + + // Begin playing background sounds. + this.rain(); + this.thunder(); + } - // Begin playing background sounds. - this.rain(); - this.thunder(); -}; -Sound.prototype = { /** * Play a rain loop in the background. */ - rain: function() { + rain() { this._rain = this.sound.play('rain'); this.sound.volume(0.2, this._rain); - }, + } /** * Randomly play thunder sounds periodically. */ - thunder: function() { - setTimeout(function() { + thunder() { + setTimeout(() => { // Play the thunder sound in a random position. - var x = Math.round(100 * (2 - (Math.random() * 4))) / 100; - var y = Math.round(100 * (2 - (Math.random() * 4))) / 100; + const x = Math.round(100 * (2 - (Math.random() * 4))) / 100; + const y = Math.round(100 * (2 - (Math.random() * 4))) / 100; this._thunder = this.sound.play('thunder'); this.sound.pos(x, y, -0.5, this._thunder); this.sound.volume(1, this._thunder); // Schedule the next clap. this.thunder(); - }.bind(this), 5000 + Math.round(Math.random() * 15000)); - }, + }, 5000 + Math.round(Math.random() * 15000)); + } /** * Play lightning in a random location with a random rate/pitch. */ - lightning: function() { - var x = Math.round(100 * (2.5 - (Math.random() * 5))) / 100; - var y = Math.round(100 * (2.5 - (Math.random() * 5))) / 100; - var rate = Math.round(100 * (0.4 + (Math.random() * 1.25))) / 100; + lightning() { + const x = Math.round(100 * (2.5 - (Math.random() * 5))) / 100; + const y = Math.round(100 * (2.5 - (Math.random() * 5))) / 100; + const rate = Math.round(100 * (0.4 + (Math.random() * 1.25))) / 100; // Play the lightning sound. - var id = this.sound.play('lightning'); + const id = this.sound.play('lightning'); // Change the position and rate. this.sound.pos(x, y, -0.5, id); this.sound.rate(rate, id); this.sound.volume(1, id); - }, + } /** * Setup a speaker in 3D space to play music from. * @param {Number} x x-tile position of speaker. * @param {Number} y y-tile position of speaker. */ - speaker: function(x, y) { - var soundId = game.audio.sound.play('music'); - this.sound.once('play', function() { + speaker(x, y) { + const soundId = game.audio.sound.play('music'); + this.sound.once('play', () => { // Set the position of the speaker in 3D space. this.sound.pos(x + 0.5, y + 0.5, -0.5, soundId); this.sound.volume(1, soundId); @@ -94,6 +93,6 @@ Sound.prototype = { rolloffFactor: 2.5, distanceModel: 'exponential' }, soundId); - }.bind(this), soundId); + }, soundId); } -}; +} diff --git a/examples/3d/js/texture.js b/examples/3d/js/texture.js index 656ec9bc..69383a74 100644 --- a/examples/3d/js/texture.js +++ b/examples/3d/js/texture.js @@ -8,17 +8,17 @@ * MIT License */ -'use strict'; - /** * Load a texture and store its details. * @param {String} src Image URL. * @param {Number} w Image width. * @param {Number} h Image height. */ -export var Texture = function(src, w, h) { - this.image = new Image(); - this.image.src = src; - this.width = w; - this.height = h; -}; \ No newline at end of file +export class Texture { + constructor(src, w, h) { + this.image = new Image(); + this.image.src = src; + this.width = w; + this.height = h; + } +} \ No newline at end of file diff --git a/examples/3d/js/utils.js b/examples/3d/js/utils.js new file mode 100644 index 00000000..26893c1b --- /dev/null +++ b/examples/3d/js/utils.js @@ -0,0 +1,7 @@ +// Cache some commonly used values. +export const circle = Math.PI * 2; +export const isMobile = /iPhone|iPad|iPod|Android|BlackBerry|BB10|Silk/i.test( + navigator.userAgent, +); +export const canvas = document.getElementById("canvas"); +export const ctx = canvas.getContext("2d"); diff --git a/examples/player/index.html b/examples/player/index.html index 76c1e413..63b15697 100644 --- a/examples/player/index.html +++ b/examples/player/index.html @@ -1,62 +1,62 @@ - + - - - - Howler.js Audio Player - - - + - - - -
- -
0:00
-
0:00
-
+ + + +
+ +
0:00
+
0:00
+
- -
-
-
-
-
-
-
-
-
-
-
+ +
+
+
+
+
+
+
+
+
+
+
- -
-
-
+ +
+
+
- -
-
-
+ +
+
+
- -
-
-
-
-
+ +
+
+
+
+
- - - + + + diff --git a/examples/player/player.js b/examples/player/player.js index 989bf403..c686f2e3 100644 --- a/examples/player/player.js +++ b/examples/player/player.js @@ -8,7 +8,7 @@ * MIT License */ -import { Howl } from 'howler'; +import { Howl, Howler } from 'howler'; import { SiriWave } from './siriwave.js'; // Cache references to DOM elements. diff --git a/examples/player/siriwave.js b/examples/player/siriwave.js index 552b601e..22d6c226 100644 --- a/examples/player/siriwave.js +++ b/examples/player/siriwave.js @@ -25,6 +25,8 @@ export class SiriWave { this.amplitude = opt.amplitude || 1; this.speed = opt.speed || 0.2; this.frequency = opt.frequency || 6; + + this._draw = this._draw.bind(this); this.color = (() => { const hex2rgb = (hex) => { const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; @@ -87,7 +89,7 @@ export class SiriWave { let i = -2; while ((i += 0.01) <= 2) { - const y = this._ypos(i, attenuation); + let y = this._ypos(i, attenuation); if (Math.abs(i) >= 1.90) y = this.height_2; this.ctx.lineTo(this._xpos(i), y); } @@ -114,10 +116,10 @@ export class SiriWave { this._drawLine(1, 'rgba(' + this.color + ',1)', 1.5); if (window.requestAnimationFrame) { - requestAnimationFrame(this._draw.bind(this)); + requestAnimationFrame(this._draw); return; } - setTimeout(this._draw.bind(this), 20); + setTimeout(this._draw, 20); } /* API */ diff --git a/examples/radio/index.html b/examples/radio/index.html index 53c72db7..8896761c 100644 --- a/examples/radio/index.html +++ b/examples/radio/index.html @@ -1,94 +1,94 @@ - + - - - - Howler.js Radio - - - + - - - -
-
-
-
-
LIVE
-
-
-
-
-
-
-
-
+ + + +
+
+
+
+
LIVE
+
+
+
+
+
+
-
-
-
-
LIVE
-
-
-
-
-
-
-
-
+
+
+
+
+
+
LIVE
+
+
+
+
+
+
-
-
-
-
LIVE
-
-
-
-
-
-
-
-
+
+
+
+
+
+
LIVE
+
+
+
+
+
+
-
-
-
-
LIVE
-
-
-
-
-
-
-
-
+
+
+
+
+
+
LIVE
+
+
+
+
+
+
-
-
-
-
LIVE
-
-
-
-
-
-
-
-
+
+
+
+
+
+
LIVE
+
+
+
+
+
+
-
+
+
+
- - - + + + diff --git a/examples/radio/radio.js b/examples/radio/radio.js index 0b8dac9c..2ee6e65a 100644 --- a/examples/radio/radio.js +++ b/examples/radio/radio.js @@ -8,7 +8,18 @@ * MIT License */ -import { Howl } from 'howler'; +import { Howl, Howler } from 'howler'; + +// Patch Howler's HTML5 audio element creation to set crossOrigin for CORS support +const originalObtainHtml5Audio = Howler._obtainHtml5Audio; +Howler._obtainHtml5Audio = function() { + const audio = originalObtainHtml5Audio.call(this); + // Set crossOrigin before src is set to enable CORS + if (audio && audio.crossOrigin !== undefined) { + audio.crossOrigin = 'anonymous'; + } + return audio; +}; // Cache references to DOM elements. const elms = ['station0', 'title0', 'live0', 'playing0', 'station1', 'title1', 'live1', 'playing1', 'station2', 'title2', 'live2', 'playing2', 'station3', 'title3', 'live3', 'playing3', 'station4', 'title4', 'live4', 'playing4']; @@ -21,39 +32,37 @@ elms.forEach(function(elm) { * Includes all methods for playing, stopping, etc. * @param {Array} stations Array of objects with station details ({title, src, howl, ...}). */ -var Radio = function(stations) { - var self = this; - - self.stations = stations; - self.index = 0; - - // Setup the display for each station. - for (var i=0; i ' + self.stations[i].title; - window['station' + i].addEventListener('click', function(index) { - var isNotPlaying = (self.stations[index].howl && !self.stations[index].howl.playing()); - - // Stop other sounds or the current one. - radio.stop(); - - // If the station isn't already playing or it doesn't exist, play it. - if (isNotPlaying || !self.stations[index].howl) { - radio.play(index); - } - }.bind(self, i)); +class Radio { + constructor(stations) { + this.stations = stations; + this.index = 0; + + // Setup the display for each station. + for (let i = 0; i < this.stations.length; i++) { + window['title' + i].innerHTML = '' + this.stations[i].freq + ' ' + this.stations[i].title; + window['station' + i].addEventListener('click', () => { + const isNotPlaying = (this.stations[i].howl && !this.stations[i].howl.playing()); + + // Stop other sounds or the current one. + radio.stop(); + + // If the station isn't already playing or it doesn't exist, play it. + if (isNotPlaying || !this.stations[i].howl) { + radio.play(i); + } + }); + } } -}; -Radio.prototype = { + /** * Play a station with a specific index. * @param {Number} index Index in the array of stations. */ - play: function(index) { - var self = this; - var sound; + play(index) { + let sound; - index = typeof index === 'number' ? index : self.index; - var data = self.stations[index]; + index = typeof index === 'number' ? index : this.index; + const data = this.stations[index]; // If we already loaded this track, use the current one. // Otherwise, setup and load a new Howl. @@ -63,46 +72,51 @@ Radio.prototype = { sound = data.howl = new Howl({ src: data.src, html5: true, // A live stream can only be played through HTML5 Audio. - format: ['mp3', 'aac'] + format: ['mp3', 'aac'], + onloaderror: function(id, error) { + console.error('Error loading stream:', error); + alert('Failed to load radio stream. Please try another station.'); + }, + onplayerror: function(id, error) { + console.error('Error playing stream:', error); + alert('Failed to play radio stream. Please try another station.'); + } }); + } // Begin playing the sound. sound.play(); // Toggle the display. - self.toggleStationDisplay(index, true); + this.toggleStationDisplay(index, true); // Keep track of the index we are currently playing. - self.index = index; - }, + this.index = index; + } /** * Stop a station's live stream. */ - stop: function() { - var self = this; - + stop() { // Get the Howl we want to manipulate. - var sound = self.stations[self.index].howl; + const sound = this.stations[this.index].howl; // Toggle the display. - self.toggleStationDisplay(self.index, false); + this.toggleStationDisplay(this.index, false); // Stop the sound. if (sound) { sound.unload(); } - }, + } /** * Toggle the display of a station to off/on. * @param {Number} index Index of the station to toggle. * @param {Boolean} state true is on and false is off. */ - toggleStationDisplay: function(index, state) { - var self = this; - + toggleStationDisplay(index, state) { // Highlight/un-highlight the row. window['station' + index].style.backgroundColor = state ? 'rgba(255, 255, 255, 0.33)' : ''; @@ -112,38 +126,39 @@ Radio.prototype = { // Show/hide the "playing" animation. window['playing' + index].style.display = state ? 'block' : 'none'; } -}; +} // Setup our new radio and pass in the stations. -var radio = new Radio([ +// Using streams that are known to work with browser-based players +const radio = new Radio([ { freq: '81.4', - title: "BBC Radio 1", - src: 'https://stream.live.vc.bbcmedia.co.uk/bbc_radio_one', + title: "Chill Out Zone", + src: 'https://streams.fluxfm.de/Chillout/mp3-320/streams.fluxfm.de/', howl: null }, { freq: '89.9', - title: "Hip Hop Hits", - src: 'https://streaming.radio.co/s97881c7e0/listen', + title: "Radio Paradise", + src: 'https://stream.radioparadise.com/mp3-128', howl: null }, { freq: '98.9', - title: "CNN", - src: 'https://tunein.streamguys1.com/cnn-new', + title: "Smooth Jazz", + src: 'https://streams.fluxfm.de/SmoothJazz/mp3-320/streams.fluxfm.de/', howl: null }, { freq: '103.3', - title: "80's Hits", - src: 'https://rfcmedia.streamguys1.com/80hits.mp3', + title: "Classic Rock", + src: 'https://streams.fluxfm.de/ClassicRock/mp3-320/streams.fluxfm.de/', howl: null }, { freq: '107.7', - title: "Today's Hits", - src: 'https://rfcmedia.streamguys1.com/MusicPulse.mp3', + title: "Radio Paradise (Alternative)", + src: 'https://stream.radioparadise.com/aac-128', howl: null } ]); diff --git a/tests/core.html5audio.html b/tests/core.html5audio.html index b3594442..a7607dd8 100644 --- a/tests/core.html5audio.html +++ b/tests/core.html5audio.html @@ -1,28 +1,28 @@ - + - - - Howler.js Core HTML5 Audio Tests - - - + - - -
-
- - -
- - + + +
+
+ + +
+ + diff --git a/tests/core.webaudio.html b/tests/core.webaudio.html index 72db50cc..c4b7c137 100644 --- a/tests/core.webaudio.html +++ b/tests/core.webaudio.html @@ -1,27 +1,27 @@ - + - - - Howler.js Core Web Audio Tests - - - + - - -
-
- -
- - + + +
+
+ +
+ + diff --git a/tests/index.html b/tests/index.html index a30b0201..50578f6a 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.webaudio.js b/tests/js/core.webaudio.js index a6dcfc5f..e4228012 100644 --- a/tests/js/core.webaudio.js +++ b/tests/js/core.webaudio.js @@ -1,4 +1,4 @@ -import { Howl } from 'howler'; +import { Howl, Howler } from 'howler'; // Cache the label for later use. const label = document.getElementById('label'); diff --git a/tests/js/spatial.js b/tests/js/spatial.js index f4ef1184..b5712537 100644 --- a/tests/js/spatial.js +++ b/tests/js/spatial.js @@ -1,18 +1,19 @@ -import { SpatialHowl, SpatialAudioPlugin, globalPluginManager } from 'howler/plugins/spatial'; +import { Howl, Howler } from 'howler'; +import { SpatialAudioPlugin } from 'howler/plugins/spatial'; // Register the Spatial Audio Plugin -globalPluginManager.register(new SpatialAudioPlugin()); +Howler.addPlugin(new SpatialAudioPlugin()); // Cache the label for later use. const label = document.getElementById('label'); const start = document.getElementById('start'); // Setup the sounds to be used. -const sound1 = new SpatialHowl({ +const sound1 = new Howl({ src: ['audio/sound1.webm', 'audio/sound1.mp3'] }); -const sound2 = new SpatialHowl({ +const sound2 = new Howl({ src: ['audio/sound2.webm', 'audio/sound2.mp3'], sprite: { one: [0, 450], diff --git a/tests/spatial.html b/tests/spatial.html index cdc2882d..4270a9fe 100644 --- a/tests/spatial.html +++ b/tests/spatial.html @@ -1,27 +1,27 @@ - + - - - Howler.js Spatial Plugin Tests - - - + - - -
-
- -
- - - \ No newline at end of file + + +
+
+ +
+ + + From bf97cda6690a730983a3c47c269083e92c6dc34f Mon Sep 17 00:00:00 2001 From: eatsjobs Date: Sat, 22 Nov 2025 22:09:03 +0100 Subject: [PATCH 13/25] Add target browsers for ES module support in tsdown.config.ts --- tsdown.config.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tsdown.config.ts b/tsdown.config.ts index 77ebf75b..c6c1ed6f 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -7,4 +7,13 @@ export default defineConfig({ unbundle: true, treeshake: true, minify: true, + // Target browsers that support ES modules natively + // Chrome 61+, Firefox 60+, Safari 11+, Edge 16+, Opera 48+ + target: [ + 'chrome61', + 'firefox60', + 'safari11', + 'edge16', + 'opera48' + ] }); From 11f774525ee08e9826569bcb589f95ca08ad5d65 Mon Sep 17 00:00:00 2001 From: eatsjobs Date: Sat, 22 Nov 2025 22:10:13 +0100 Subject: [PATCH 14/25] Add lightweight user agent parser for browser/device detection --- src/helpers/audio-context.ts | 8 +- src/helpers/index.ts | 1 + src/helpers/light-ua-parser.ts | 135 +++++++++++++++++++++++++++++++++ src/howler.core.ts | 21 ++--- 4 files changed, 148 insertions(+), 17 deletions(-) create mode 100644 src/helpers/light-ua-parser.ts diff --git a/src/helpers/audio-context.ts b/src/helpers/audio-context.ts index 346babfc..41c06241 100644 --- a/src/helpers/audio-context.ts +++ b/src/helpers/audio-context.ts @@ -9,6 +9,7 @@ */ import { Howler } from '../howler.core'; +import { isIOS, getIOSVersion, isSafari } from './light-ua-parser'; export const setupAudioContext = () => { if (!Howler.usingWebAudio) { @@ -31,11 +32,10 @@ export const setupAudioContext = () => { Howler.usingWebAudio = false; } - const iOS = /iP(hone|od|ad)/.test((Howler._navigator && Howler._navigator.platform || "")); - const appVersion = Howler._navigator && Howler._navigator.appVersion?.match(/OS (\d+)_(\d+)_?(\d+)?/); - const version = appVersion ? parseInt(appVersion[1], 10) : null; + const iOS = isIOS(Howler._navigator); + const version = getIOSVersion(Howler._navigator); if (iOS && version && version < 9) { - const safari = /safari/.test(Howler._navigator?.userAgent.toLowerCase() ?? ''); + const safari = isSafari(Howler._navigator); if (Howler._navigator && !safari) { Howler.usingWebAudio = false; } diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 97ab9074..a4538e9d 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -10,3 +10,4 @@ export { loadBuffer, safeXhrSend, decodeAudioData, loadSound } from './audio-loader'; export { setupAudioContext } from './audio-context'; +export { isIOS, getIOSVersion, isSafari, getSafariVersion, isOldSafari, isOpera, getOperaVersion, isOldOpera, isIE, isAppleVendor, isChromeBased, isCocoonJS } from './light-ua-parser'; diff --git a/src/helpers/light-ua-parser.ts b/src/helpers/light-ua-parser.ts new file mode 100644 index 00000000..93db0213 --- /dev/null +++ b/src/helpers/light-ua-parser.ts @@ -0,0 +1,135 @@ +/*! + * Lightweight User Agent Parser for Howler.js + * Provides simple browser/device detection utilities + * howlerjs.com + * + * (c) 2013-2020, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */ + +/** + * Get the user agent string from navigator + */ +function getUserAgent(navigator: Navigator | null): string { + return navigator?.userAgent || ''; +} + +/** + * Get the platform string from navigator + */ +function getPlatform(navigator: Navigator | null): string { + return navigator?.platform || ''; +} + +/** + * Check if the device is iOS (iPhone, iPad, iPod) + */ +export function isIOS(navigator: Navigator | null): boolean { + return /iP(hone|od|ad)/.test(getPlatform(navigator)); +} + +/** + * Get iOS version from appVersion + * Returns the major version number or null if not iOS or version can't be determined + */ +export function getIOSVersion(navigator: Navigator | null): number | null { + if (!isIOS(navigator)) { + return null; + } + + const appVersion = navigator?.appVersion?.match(/OS (\d+)_(\d+)_?(\d+)?/); + return appVersion ? parseInt(appVersion[1], 10) : null; +} + +/** + * Check if the browser is Safari (not Chrome-based) + */ +export function isSafari(navigator: Navigator | null): boolean { + const ua = getUserAgent(navigator); + return ua.indexOf('Safari') !== -1 && ua.indexOf('Chrome') === -1; +} + +/** + * Get Safari version from user agent + * Returns the major version number or null if not Safari or version can't be determined + */ +export function getSafariVersion(navigator: Navigator | null): number | null { + if (!isSafari(navigator)) { + return null; + } + + const ua = getUserAgent(navigator); + const versionMatch = ua.match(/Version\/(.*?) /); + return versionMatch ? parseInt(versionMatch[1], 10) : null; +} + +/** + * Check if Safari is an old version (before version 15) + */ +export function isOldSafari(navigator: Navigator | null): boolean { + const version = getSafariVersion(navigator); + return version !== null && version < 15; +} + +/** + * Check if the browser is Opera + */ +export function isOpera(navigator: Navigator | null): boolean { + const ua = getUserAgent(navigator); + return /OPR\//.test(ua); +} + +/** + * Get Opera version from user agent + * Returns the major version number or null if not Opera or version can't be determined + */ +export function getOperaVersion(navigator: Navigator | null): number | null { + if (!isOpera(navigator)) { + return null; + } + + const ua = getUserAgent(navigator); + const versionMatch = ua.match(/OPR\/(\d+)/); + return versionMatch ? parseInt(versionMatch[1], 10) : null; +} + +/** + * Check if Opera is an old version (before version 33) + */ +export function isOldOpera(navigator: Navigator | null): boolean { + const version = getOperaVersion(navigator); + return version !== null && version < 33; +} + +/** + * Check if the browser is Internet Explorer (MSIE or Trident) + */ +export function isIE(navigator: Navigator | null): boolean { + const ua = getUserAgent(navigator); + return /MSIE |Trident\//.test(ua); +} + +/** + * Check if the browser vendor is Apple + */ +export function isAppleVendor(navigator: Navigator | null): boolean { + return (navigator?.vendor?.indexOf('Apple') ?? -1) >= 0; +} + +/** + * Check if the browser is Chrome-based (Chrome, Edge, etc.) + */ +export function isChromeBased(navigator: Navigator | null): boolean { + const ua = getUserAgent(navigator); + return ua.indexOf('Chrome') !== -1; +} + +/** + * Check if the browser is CocoonJS (for game engines) + */ +export function isCocoonJS(navigator: Navigator | null): boolean { + return !!(navigator as any)?.isCocoonJS; +} + diff --git a/src/howler.core.ts b/src/howler.core.ts index b2879e7c..1590f433 100644 --- a/src/howler.core.ts +++ b/src/howler.core.ts @@ -11,7 +11,7 @@ import { cache, EventListener, HowlOptions, QueueItem } from './types'; // Import helper functions -import { loadBuffer, setupAudioContext } from './helpers'; +import { loadBuffer, setupAudioContext, isOldOpera, isOldSafari, isAppleVendor, isIE } from './helpers'; // Import plugin manager import { globalPluginManager, HowlerPlugin } from './plugins'; @@ -241,15 +241,11 @@ export class HowlerGlobal { } const mpegTest = audioTest.canPlayType('audio/mpeg;').replace(/^no$/, ''); - const ua = self._navigator ? self._navigator.userAgent : ''; - const checkOpera = ua.match(/OPR\/(\d+)/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; + const oldOpera = isOldOpera(self._navigator); + const oldSafari = isOldSafari(self._navigator); self._codecs = { - mp3: !!(!isOldOpera && (mpegTest || audioTest.canPlayType('audio/mp3;').replace(/^no$/, ''))), + mp3: !!(!oldOpera && (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$/, ''), @@ -260,8 +256,8 @@ export class HowlerGlobal { 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$/, '')), + weba: !!(!oldSafari && audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, '')), + webm: !!(!oldSafari && 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$/, '') }; @@ -1889,7 +1885,7 @@ class Howl { _cleanBuffer(node: any): Howl { const self = this; - const isIOS = Howler._navigator && Howler._navigator.vendor.indexOf('Apple') >= 0; + const isIOS = isAppleVendor(Howler._navigator); if (!node.bufferSource) { return self; @@ -1910,8 +1906,7 @@ class Howl { } _clearSound(node: HTMLAudioElement): void { - const checkIE = /MSIE |Trident\//.test((Howler._navigator && Howler._navigator.userAgent) || ''); - if (!checkIE) { + if (!isIE(Howler._navigator)) { node.src = 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA'; } } From 0af8f3921ebbf47e8adc39cdbbd51ccea1b3c3fe Mon Sep 17 00:00:00 2001 From: eatsjobs Date: Sat, 22 Nov 2025 23:09:33 +0100 Subject: [PATCH 15/25] milestone. tests pass, examples working --- src/helpers/audio-loader.ts | 6 +- src/howler.core.ts | 11 +- src/plugins/plugin.ts | 18 +- src/plugins/spatial-plugin.ts | 804 ++++++++++++++++++++++++++-------- src/plugins/spatial.ts | 6 +- 5 files changed, 668 insertions(+), 177 deletions(-) diff --git a/src/helpers/audio-loader.ts b/src/helpers/audio-loader.ts index 127c6ce8..72239a9d 100644 --- a/src/helpers/audio-loader.ts +++ b/src/helpers/audio-loader.ts @@ -9,8 +9,9 @@ */ import type { Howl } from '../howler.core'; -import { cache } from '../types'; import { Howler } from '../howler.core'; +import { globalPluginManager } from '../plugins'; +import { cache } from '../types'; export const loadBuffer = (self: Howl) => { const url = self._src as string; @@ -107,5 +108,8 @@ export const loadSound = (self: Howl, buffer?: AudioBuffer) => { self._state = 'loaded'; self._emit('load'); self._loadQueue(); + + // Execute plugin hooks + globalPluginManager.executeHowlLoad(self); } }; diff --git a/src/howler.core.ts b/src/howler.core.ts index 1590f433..c12632c0 100644 --- a/src/howler.core.ts +++ b/src/howler.core.ts @@ -11,7 +11,7 @@ import { cache, EventListener, HowlOptions, QueueItem } from './types'; // Import helper functions -import { loadBuffer, setupAudioContext, isOldOpera, isOldSafari, isAppleVendor, isIE } from './helpers'; +import { isAppleVendor, isIE, isOldOpera, isOldSafari, loadBuffer, setupAudioContext } from './helpers'; // Import plugin manager import { globalPluginManager, HowlerPlugin } from './plugins'; @@ -191,6 +191,15 @@ export class HowlerGlobal { return this; } + /** + * Check if a plugin is registered + * @param pluginName - The name of the plugin to check + * @returns true if the plugin is registered, false otherwise + */ + hasPlugin(pluginName: string): boolean { + return globalPluginManager.isRegistered(pluginName); + } + _setup(): HowlerGlobal { const self = this; diff --git a/src/plugins/plugin.ts b/src/plugins/plugin.ts index a81c3a9f..eb658ac8 100644 --- a/src/plugins/plugin.ts +++ b/src/plugins/plugin.ts @@ -86,7 +86,6 @@ export interface RegisteredPlugin { */ export class PluginManager { private plugins: Map = new Map(); - private hookHistory: Map = new Map(); private howlerInstance: HowlerGlobal | null = null; /** @@ -117,6 +116,15 @@ export class PluginManager { console.error(`Error during onRegister for plugin "${plugin.name}":`, error); } } + + // If Howler is already initialized, execute onHowlerInit hook for this plugin + if (this.howlerInstance && hooks.onHowlerInit) { + try { + hooks.onHowlerInit(this.howlerInstance); + } catch (error: unknown) { + console.error(`Error during onHowlerInit for plugin "${plugin.name}":`, error); + } + } } /** @@ -151,10 +159,18 @@ export class PluginManager { /** * Set the Howler instance reference for late-registered plugins + * Also executes onHowlerInit hooks for any plugins already registered * @internal */ setHowlerInstance(howler: HowlerGlobal): void { this.howlerInstance = howler; + + // Execute onHowlerInit hooks for all registered plugins + this._executeHooks('onHowlerInit', (hooks) => { + if (hooks.onHowlerInit) { + hooks.onHowlerInit(howler); + } + }); } /** diff --git a/src/plugins/spatial-plugin.ts b/src/plugins/spatial-plugin.ts index 6a1290fb..4ba13a98 100644 --- a/src/plugins/spatial-plugin.ts +++ b/src/plugins/spatial-plugin.ts @@ -10,47 +10,186 @@ */ import type { HowlOptions } from '../howler.core'; -import { Howl, HowlerGlobal } from '../howler.core'; +import { Howl, Howler, HowlerGlobal, Sound } from '../howler.core'; import { HowlerPlugin, type PluginHooks, globalPluginManager } from './plugin'; /** - * Spatial audio properties + * Extended HowlOptions with spatial audio properties + */ +export interface SpatialHowlOptions extends HowlOptions { + pos?: [number, number, number]; + orientation?: [number, number, number]; + stereo?: number; + coneInnerAngle?: number; + coneOuterAngle?: number; + coneOuterGain?: number; + distanceModel?: 'linear' | 'inverse' | 'exponential'; + maxDistance?: number; + panningModel?: 'equalpower' | 'HRTF'; + refDistance?: number; + rolloffFactor?: number; + onstereo?: () => void; + onpos?: () => void; + onorientation?: () => void; +} + +/** + * Spatial audio properties for HowlerGlobal */ export interface SpatialAudioState { _pos: [number, number, number]; _orientation: [number, number, number, number, number, number]; - _stereo?: number; +} + +/** + * Spatial audio properties for Howl + */ +export interface SpatialHowlState { + _pos: [number, number, number] | null; + _orientation: [number, number, number]; + _stereo: number | null; + _pannerAttr: { + coneInnerAngle: number; + coneOuterAngle: number; + coneOuterGain: number; + distanceModel: 'linear' | 'inverse' | 'exponential'; + maxDistance: number; + panningModel: 'equalpower' | 'HRTF'; + refDistance: number; + rolloffFactor: number; + }; + _onstereo: Array<{ fn: () => void }>; + _onpos: Array<{ fn: () => void }>; + _onorientation: Array<{ fn: () => void }>; +} + +/** + * Spatial audio properties for Sound + */ +export interface SpatialSoundState { + _pos: [number, number, number] | null; + _orientation: [number, number, number]; + _stereo: number | null; + _pannerAttr: { + coneInnerAngle: number; + coneOuterAngle: number; + coneOuterGain: number; + distanceModel: 'linear' | 'inverse' | 'exponential'; + maxDistance: number; + panningModel: 'equalpower' | 'HRTF'; + refDistance: number; + rolloffFactor: number; + }; } /** * Howler instance with spatial audio capabilities + * Use this type when the spatial plugin is registered + * + * @example + * ```typescript + * import { Howler } from 'howler'; + * import { SpatialAudioPlugin, type SpatialHowler } from 'howler/plugins/spatial'; + * + * Howler.addPlugin(new SpatialAudioPlugin()); + * + * const howler: SpatialHowler = Howler as SpatialHowler; + * howler.pos(10, 20, 30); + * ``` */ export type SpatialHowler = HowlerGlobal & SpatialAudioState & { - pos(x?: number, y?: number, z?: number): any; - orientation(x?: number, y?: number, z?: number, xUp?: number, yUp?: number, zUp?: number): any; - stereo(pan?: number): any; + pos(x?: number, y?: number, z?: number): SpatialHowler | [number, number, number]; + orientation(x?: number, y?: number, z?: number, xUp?: number, yUp?: number, zUp?: number): SpatialHowler | [number, number, number, number, number, number]; + stereo(pan?: number): SpatialHowler; }; /** * Howl instance with spatial audio capabilities + * Use this type when the spatial plugin is registered + * + * @example + * ```typescript + * import { Howl } from 'howler'; + * import { SpatialAudioPlugin, type SpatialHowl, type SpatialHowlOptions } from 'howler/plugins/spatial'; + * + * Howler.addPlugin(new SpatialAudioPlugin()); + * + * const sound: SpatialHowl = new Howl({ + * src: ['sound.mp3'], + * pos: [10, 20, 30] + * } as SpatialHowlOptions) as SpatialHowl; + * + * sound.pos(5, 10, 15); + * sound.stereo(0.5); + * ``` */ -export type SpatialHowl = Howl & { - _pos?: [number, number, number] | null; - _orientation?: [number, number, number]; - _stereo?: number | null; - _pannerAttr?: any; - pos(x?: number, y?: number, z?: number, id?: number): any; - orientation(x?: number, y?: number, z?: number, id?: number): any; - stereo(pan?: number, id?: number): any; - pannerAttr(o?: any, id?: number): any; +export type SpatialHowl = Howl & SpatialHowlState & { + pos(x?: number, y?: number, z?: number, id?: number): SpatialHowl | [number, number, number]; + orientation(x?: number, y?: number, z?: number, id?: number): SpatialHowl | [number, number, number]; + stereo(pan?: number, id?: number): SpatialHowl | number; + pannerAttr(o?: any, id?: number): SpatialHowl | any; }; +/** + * Setup a panner node for a sound + */ +function setupPanner(sound: Sound & SpatialSoundState, type: 'stereo' | 'spatial' = 'spatial'): void { + if (!Howler.ctx) { + return; + } + + // Create the new panner node + if (type === 'spatial') { + sound._panner = Howler.ctx.createPanner(); + const panner = sound._panner as PannerNode; + panner.coneInnerAngle = sound._pannerAttr.coneInnerAngle; + panner.coneOuterAngle = sound._pannerAttr.coneOuterAngle; + panner.coneOuterGain = sound._pannerAttr.coneOuterGain; + panner.distanceModel = sound._pannerAttr.distanceModel; + panner.maxDistance = sound._pannerAttr.maxDistance; + panner.refDistance = sound._pannerAttr.refDistance; + panner.rolloffFactor = sound._pannerAttr.rolloffFactor; + panner.panningModel = sound._pannerAttr.panningModel; + + if (sound._pos) { + if (typeof (panner as any).positionX !== 'undefined') { + (panner as any).positionX.setValueAtTime(sound._pos[0], Howler.ctx!.currentTime); + (panner as any).positionY.setValueAtTime(sound._pos[1], Howler.ctx!.currentTime); + (panner as any).positionZ.setValueAtTime(sound._pos[2], Howler.ctx!.currentTime); + } else { + panner.setPosition(sound._pos[0], sound._pos[1], sound._pos[2]); + } + } + + if (typeof (panner as any).orientationX !== 'undefined') { + (panner as any).orientationX.setValueAtTime(sound._orientation[0], Howler.ctx!.currentTime); + (panner as any).orientationY.setValueAtTime(sound._orientation[1], Howler.ctx!.currentTime); + (panner as any).orientationZ.setValueAtTime(sound._orientation[2], Howler.ctx!.currentTime); + } else { + panner.setOrientation(sound._orientation[0], sound._orientation[1], sound._orientation[2]); + } + } else { + sound._panner = Howler.ctx.createStereoPanner(); + const stereoPanner = sound._panner as StereoPannerNode; + if (sound._stereo !== null) { + stereoPanner.pan.setValueAtTime(sound._stereo, Howler.ctx!.currentTime); + } + } + + // Connect panner to the sound's node + sound._panner.connect(sound._node); + + // Update connections if sound is playing + if (!sound._paused) { + (sound._parent as any).pause(sound._id, true); + (sound._parent as any).play(sound._id, true); + } +} + /** * Mixin function to add spatial audio to HowlerGlobal (listener) */ -export function withSpatialListener( - instance: T -): T & SpatialAudioState & { +function withSpatialListener(instance: HowlerGlobal): HowlerGlobal & SpatialAudioState & { pos(x?: number, y?: number, z?: number): any; orientation(x?: number, y?: number, z?: number, xUp?: number, yUp?: number, zUp?: number): any; stereo(pan?: number): any; @@ -64,28 +203,28 @@ export function withSpatialListener( // Add pos method to set listener position spatial.pos = function (x?: number, y?: number, z?: number) { if (!this.ctx || !this.ctx.listener) { - console.warn('Spatial audio unavailable: Web Audio API not supported'); return this; } + // Set the defaults for optional 'y' & 'z' + y = typeof y !== 'number' ? this._pos[1] : y; + z = typeof z !== 'number' ? this._pos[2] : z; + if (typeof x === 'number') { - const y_val = typeof y === 'number' ? y : spatial._pos[1]; - const z_val = typeof z === 'number' ? z : spatial._pos[2]; - spatial._pos = [x, y_val, z_val]; + this._pos = [x, y, z]; - // Set listener position using appropriate API if (typeof this.ctx.listener.positionX !== 'undefined') { - this.ctx.listener.positionX.setTargetAtTime(x, this.ctx.currentTime, 0.1); - this.ctx.listener.positionY.setTargetAtTime(y_val, this.ctx.currentTime, 0.1); - this.ctx.listener.positionZ.setTargetAtTime(z_val, this.ctx.currentTime, 0.1); + (this.ctx.listener.positionX as any).setTargetAtTime(this._pos[0], Howler.ctx!.currentTime, 0.1); + (this.ctx.listener.positionY as any).setTargetAtTime(this._pos[1], Howler.ctx!.currentTime, 0.1); + (this.ctx.listener.positionZ as any).setTargetAtTime(this._pos[2], Howler.ctx!.currentTime, 0.1); } else { - (this.ctx.listener as any).setPosition(x, y_val, z_val); + (this.ctx.listener as any).setPosition(this._pos[0], this._pos[1], this._pos[2]); } - - return this; + } else { + return this._pos; } - return spatial._pos; + return this; }; // Add orientation method to set listener orientation @@ -98,129 +237,49 @@ export function withSpatialListener( zUp?: number ) { if (!this.ctx || !this.ctx.listener) { - console.warn('Spatial audio unavailable: Web Audio API not supported'); return this; } + // Set the defaults for optional parameters + const or = this._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') { - const or = spatial._orientation; - const y_val = typeof y === 'number' ? y : or[1]; - const z_val = typeof z === 'number' ? z : or[2]; - const xUp_val = typeof xUp === 'number' ? xUp : or[3]; - const yUp_val = typeof yUp === 'number' ? yUp : or[4]; - const zUp_val = typeof zUp === 'number' ? zUp : or[5]; - spatial._orientation = [x, y_val, z_val, xUp_val, yUp_val, zUp_val]; - - // Set listener orientation using appropriate API + this._orientation = [x, y, z, xUp, yUp, zUp]; + if (typeof this.ctx.listener.forwardX !== 'undefined') { - this.ctx.listener.forwardX.setTargetAtTime(x, this.ctx.currentTime, 0.1); - this.ctx.listener.forwardY.setTargetAtTime(y_val, this.ctx.currentTime, 0.1); - this.ctx.listener.forwardZ.setTargetAtTime(z_val, this.ctx.currentTime, 0.1); - this.ctx.listener.upX.setTargetAtTime(xUp_val, this.ctx.currentTime, 0.1); - this.ctx.listener.upY.setTargetAtTime(yUp_val, this.ctx.currentTime, 0.1); - this.ctx.listener.upZ.setTargetAtTime(zUp_val, this.ctx.currentTime, 0.1); + (this.ctx.listener.forwardX as any).setTargetAtTime(x, Howler.ctx!.currentTime, 0.1); + (this.ctx.listener.forwardY as any).setTargetAtTime(y, Howler.ctx!.currentTime, 0.1); + (this.ctx.listener.forwardZ as any).setTargetAtTime(z, Howler.ctx!.currentTime, 0.1); + (this.ctx.listener.upX as any).setTargetAtTime(xUp, Howler.ctx!.currentTime, 0.1); + (this.ctx.listener.upY as any).setTargetAtTime(yUp, Howler.ctx!.currentTime, 0.1); + (this.ctx.listener.upZ as any).setTargetAtTime(zUp, Howler.ctx!.currentTime, 0.1); } else { - (this.ctx.listener as any).setOrientation( - x, - y_val, - z_val, - xUp_val, - yUp_val, - zUp_val - ); + (this.ctx.listener as any).setOrientation(x, y, z, xUp, yUp, zUp); } - - return this; + } else { + return or; } - return spatial._orientation; + return this; }; // Add stereo method spatial.stereo = function (pan?: number) { if (!this.ctx || !this.ctx.listener) { - console.warn('Spatial audio unavailable: Web Audio API not supported'); - return this; - } - - if (typeof pan === 'number') { - for (let i = 0; i < this._howls.length; i++) { - (this._howls[i] as any).stereo?.(pan); - } - return this; - } - - return spatial._stereo ?? 0; - }; - - return spatial; -} - -/** - * Mixin function to add spatial audio to Howl instances - */ -export function withSpatialHowl( - instance: T -): T & { - _orientation: [number, number, number]; - _stereo: number | null; - _pos: [number, number, number] | null; - _pannerAttr: any; - pos(x?: number, y?: number, z?: number, id?: number): any; - orientation(x?: number, y?: number, z?: number, id?: number): any; - stereo(pan?: number, id?: number): any; - pannerAttr(o?: any, id?: number): any; -} { - const spatial = instance as any; - - spatial._orientation = [1, 0, 0]; - spatial._stereo = null; - spatial._pos = null; - spatial._pannerAttr = { - coneInnerAngle: 360, - coneOuterAngle: 360, - coneOuterGain: 0, - distanceModel: 'inverse', - maxDistance: 10000, - refDistance: 1, - rolloffFactor: 1, - panningModel: 'HRTF', - }; - - spatial.stereo = function (pan?: number, id?: number) { - if (typeof pan === 'number') { - spatial._stereo = pan; return this; } - return spatial._stereo ?? 0; - }; - spatial.pos = function (x?: number, y?: number, z?: number, id?: number) { - if (typeof x === 'number') { - const y_val = typeof y === 'number' ? y : (spatial._pos?.[1] ?? 0); - const z_val = typeof z === 'number' ? z : (spatial._pos?.[2] ?? 0); - spatial._pos = [x, y_val, z_val]; - return this; + // Loop through all Howls and update their stereo panning + for (let i = this._howls.length - 1; i >= 0; i--) { + (this._howls[i] as any).stereo?.(pan); } - return spatial._pos ?? [0, 0, 0]; - }; - spatial.orientation = function (x?: number, y?: number, z?: number, id?: number) { - if (typeof x === 'number') { - const y_val = typeof y === 'number' ? y : spatial._orientation[1]; - const z_val = typeof z === 'number' ? z : spatial._orientation[2]; - spatial._orientation = [x, y_val, z_val]; - return this; - } - return spatial._orientation; - }; - - spatial.pannerAttr = function (o?: any, id?: number) { - if (o) { - Object.assign(spatial._pannerAttr, o); - return this; - } - return spatial._pannerAttr; + return this; }; return spatial; @@ -229,25 +288,6 @@ export function withSpatialHowl( /** * Spatial Audio Plugin * Adds 3D spatial audio and stereo panning capabilities to Howler and Howl instances - * - * Usage: - * ```typescript - * import { Howler } from 'howler'; - * import { SpatialAudioPlugin } from 'howler/plugins/spatial'; - * - * // Register the plugin - * Howler.addPlugin(new SpatialAudioPlugin()); - * - * // Use spatial methods on Howler listener: - * Howler.pos(10, 20, 30); - * Howler.orientation(1, 0, 0, 0, 1, 0); - * - * // Use spatial methods on Howl instances: - * const sound = new Howl({ src: 'audio.mp3' }); - * sound.pos(10, 20, 30); - * sound.stereo(0.5); - * sound.pannerAttr({ refDistance: 0.8 }); - * ``` */ export class SpatialAudioPlugin extends HowlerPlugin { readonly name = 'spatial-audio'; @@ -255,41 +295,459 @@ export class SpatialAudioPlugin extends HowlerPlugin { getHooks(): PluginHooks { return { - onRegister: this.onRegister.bind(this), + onHowlerInit: this.onHowlerInit.bind(this), onHowlCreate: this.onHowlCreate.bind(this), + onSoundCreate: this.onSoundCreate.bind(this), + onHowlLoad: this.onHowlLoad.bind(this), }; } /** - * Initialize spatial audio when the plugin is registered. - * This is called whether the Howler is already initialized or not. + * Initialize spatial audio when Howler is initialized + * This is called either: + * - When Howler initializes (if plugin was registered before) + * - Immediately during registration (if Howler is already initialized) */ - private onRegister(): void { - // Apply the spatial audio mixin to Howler if it's initialized - const howler = globalPluginManager.getHowlerInstance(); - if (howler) { - withSpatialListener(howler); + private onHowlerInit(howler: HowlerGlobal): void { + withSpatialListener(howler); + } + + /** + * Extend Howl instances with spatial audio methods + */ + private onHowlCreate(howl: Howl, options: HowlOptions): void { + const spatialOptions = options as SpatialHowlOptions; + const spatial = howl as any; + + // Setup user-defined default properties + spatial._orientation = spatialOptions.orientation || [1, 0, 0]; + spatial._stereo = spatialOptions.stereo !== undefined ? spatialOptions.stereo : null; + spatial._pos = spatialOptions.pos || null; + spatial._pannerAttr = { + coneInnerAngle: typeof spatialOptions.coneInnerAngle !== 'undefined' ? spatialOptions.coneInnerAngle : 360, + coneOuterAngle: typeof spatialOptions.coneOuterAngle !== 'undefined' ? spatialOptions.coneOuterAngle : 360, + coneOuterGain: typeof spatialOptions.coneOuterGain !== 'undefined' ? spatialOptions.coneOuterGain : 0, + distanceModel: typeof spatialOptions.distanceModel !== 'undefined' ? spatialOptions.distanceModel : 'inverse', + maxDistance: typeof spatialOptions.maxDistance !== 'undefined' ? spatialOptions.maxDistance : 10000, + panningModel: typeof spatialOptions.panningModel !== 'undefined' ? spatialOptions.panningModel : 'HRTF', + refDistance: typeof spatialOptions.refDistance !== 'undefined' ? spatialOptions.refDistance : 1, + rolloffFactor: typeof spatialOptions.rolloffFactor !== 'undefined' ? spatialOptions.rolloffFactor : 1, + }; + + // Setup event listeners + spatial._onstereo = spatialOptions.onstereo ? [{ fn: spatialOptions.onstereo }] : []; + spatial._onpos = spatialOptions.onpos ? [{ fn: spatialOptions.onpos }] : []; + spatial._onorientation = spatialOptions.onorientation ? [{ fn: spatialOptions.onorientation }] : []; + + // Add stereo method + spatial.stereo = function (pan?: number, id?: number) { + const self = this as any; + + // 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 + 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 + const pannerType = + typeof Howler.ctx!.createStereoPanner !== 'undefined' ? 'stereo' : 'spatial'; + + const ids = self._getSoundIds(id); + for (let i = 0; i < ids.length; i++) { + const sound = self._soundById(ids[i]) as any; + if (sound) { + sound._stereo = pan; + + // Create a new panner node if one doesn't already exist + if (!sound._panner) { + // Make sure we have a position to setup the node with + if (!sound._pos) { + sound._pos = self._pos || [0, 0, -0.5]; + } + setupPanner(sound, pannerType); + } else if (pannerType === 'stereo' && sound._panner instanceof StereoPannerNode) { + sound._panner.pan.setValueAtTime(pan, Howler.ctx!.currentTime); + } + } + } + + // Fire event + self._emit('stereo', id); + + return self; + }; + + // Add pos method + spatial.pos = function (x?: number, y?: number, z?: number, id?: number) { + const self = this as any; + + // 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 + if (self._state !== 'loaded') { + self._queue.push({ + event: 'pos', + action: function () { + self.pos(x, y, z, id); + }, + }); + return self; + } + + // Set the defaults for optional 'y' & 'z' + y = typeof y !== 'number' ? (self._pos ? self._pos[1] : 0) : y; + z = typeof z !== 'number' ? (self._pos ? self._pos[2] : 0) : z; + + if (typeof x === 'number') { + const ids = self._getSoundIds(id); + for (let i = 0; i < ids.length; i++) { + const sound = self._soundById(ids[i]) as any; + if (sound) { + sound._pos = [x, y, z]; + + // Create a new panner node if one doesn't already exist + if (!sound._panner) { + setupPanner(sound, 'spatial'); + } else if (sound._panner instanceof PannerNode) { + // Update position + if (typeof (sound._panner as any).positionX !== 'undefined') { + (sound._panner as any).positionX.setValueAtTime(x, Howler.ctx!.currentTime); + (sound._panner as any).positionY.setValueAtTime(y, Howler.ctx!.currentTime); + (sound._panner as any).positionZ.setValueAtTime(z, Howler.ctx!.currentTime); + } else { + sound._panner.setPosition(x, y, z); + } + } + } + } + + // Fire event + self._emit('pos', id); + + return self; + } else { + // Return the position of the first sound or the group's position + if (typeof id === 'number') { + const sound = self._soundById(id) as any; + return sound ? sound._pos || [0, 0, 0] : [0, 0, 0]; + } + return self._pos || [0, 0, 0]; + } + }; + + // Add orientation method + spatial.orientation = function (x?: number, y?: number, z?: number, id?: number) { + const self = this as any; + + // 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 + if (self._state !== 'loaded') { + self._queue.push({ + event: 'orientation', + action: function () { + self.orientation(x, y, z, id); + }, + }); + return self; + } + + // Set the defaults for optional 'y' & 'z' + y = typeof y !== 'number' ? self._orientation[1] : y; + z = typeof z !== 'number' ? self._orientation[2] : z; + + if (typeof x === 'number') { + const ids = self._getSoundIds(id); + for (let i = 0; i < ids.length; i++) { + const sound = self._soundById(ids[i]) as any; + if (sound) { + sound._orientation = [x, y, z]; + + // Create a new panner node if one doesn't already exist + if (!sound._panner) { + if (!sound._pos) { + sound._pos = self._pos || [0, 0, -0.5]; + } + setupPanner(sound, 'spatial'); + } else if (sound._panner instanceof PannerNode) { + // Update orientation + if (typeof (sound._panner as any).orientationX !== 'undefined') { + (sound._panner as any).orientationX.setValueAtTime(x, Howler.ctx!.currentTime); + (sound._panner as any).orientationY.setValueAtTime(y, Howler.ctx!.currentTime); + (sound._panner as any).orientationZ.setValueAtTime(z, Howler.ctx!.currentTime); + } else { + sound._panner.setOrientation(x, y, z); + } + } + } + } + + // Fire event + self._emit('orientation', id); + + return self; + } else { + // Return the orientation of the first sound or the group's orientation + if (typeof id === 'number') { + const sound = self._soundById(id) as any; + return sound ? sound._orientation : [1, 0, 0]; + } + return self._orientation; + } + }; + + // Add pannerAttr method + spatial.pannerAttr = function (o?: any, id?: number) { + const self = this as any; + const args = arguments; + let sound: any = null; + + if (args.length === 0) { + // Return this sound's panner attribute values + return self._pannerAttr; + } else if (args.length === 1) { + if (typeof args[0] === 'number') { + // Return this sound's panner attribute values + sound = self._soundById(parseInt(args[0] as any, 10)); + return sound ? sound._pannerAttr : self._pannerAttr; + } else { + // Update all sounds in the group + o = args[0]; + } + } else if (args.length === 2) { + o = args[0]; + id = parseInt(args[1] as any, 10); + } + + // Update the values of the specified sounds + const ids = self._getSoundIds(id); + for (let i = 0; i < ids.length; i++) { + sound = self._soundById(ids[i]) as any; + + if (sound) { + // Merge the new values into the sound + const pa = sound._pannerAttr; + sound._pannerAttr = { + coneInnerAngle: typeof o.coneInnerAngle !== 'undefined' ? o.coneInnerAngle : pa.coneInnerAngle, + coneOuterAngle: typeof o.coneOuterAngle !== 'undefined' ? o.coneOuterAngle : pa.coneOuterAngle, + coneOuterGain: typeof o.coneOuterGain !== 'undefined' ? o.coneOuterGain : pa.coneOuterGain, + distanceModel: typeof o.distanceModel !== 'undefined' ? o.distanceModel : pa.distanceModel, + maxDistance: typeof o.maxDistance !== 'undefined' ? o.maxDistance : pa.maxDistance, + refDistance: typeof o.refDistance !== 'undefined' ? o.refDistance : pa.refDistance, + rolloffFactor: typeof o.rolloffFactor !== 'undefined' ? o.rolloffFactor : pa.rolloffFactor, + panningModel: typeof o.panningModel !== 'undefined' ? o.panningModel : pa.panningModel, + }; + + // Create a new panner node if one doesn't already exist + let panner = sound._panner; + if (!panner) { + // Make sure we have a position to setup the node with + if (!sound._pos) { + sound._pos = self._pos || [0, 0, -0.5]; + } + + // Create a new panner node + setupPanner(sound, 'spatial'); + panner = sound._panner; + } + + // Update the panner values + if (panner instanceof PannerNode) { + panner.coneInnerAngle = sound._pannerAttr.coneInnerAngle; + panner.coneOuterAngle = sound._pannerAttr.coneOuterAngle; + panner.coneOuterGain = sound._pannerAttr.coneOuterGain; + panner.distanceModel = sound._pannerAttr.distanceModel; + panner.maxDistance = sound._pannerAttr.maxDistance; + panner.refDistance = sound._pannerAttr.refDistance; + panner.rolloffFactor = sound._pannerAttr.rolloffFactor; + panner.panningModel = sound._pannerAttr.panningModel; + } + } + } + + return self; + }; + } + + /** + * Extend Sound instances with spatial audio properties + */ + private onSoundCreate(sound: Sound, parent: Howl): void { + const spatialParent = parent as any; + const spatialSound = sound as any; + + // Setup user-defined default properties + spatialSound._orientation = spatialParent._orientation; + spatialSound._stereo = spatialParent._stereo; + spatialSound._pos = spatialParent._pos; + spatialSound._pannerAttr = spatialParent._pannerAttr; + + // Wrap the reset method to handle spatial cleanup + if (!spatialSound._originalReset) { + spatialSound._originalReset = spatialSound.reset.bind(spatialSound); + spatialSound.reset = function () { + const self = this as any; + const parent = self._parent as any; + + // Reset all spatial plugin properties on this sound + self._orientation = parent._orientation; + self._stereo = parent._stereo; + self._pos = parent._pos; + self._pannerAttr = parent._pannerAttr; + + // If a stereo or position was specified, set it up + if (self._stereo !== null && self._stereo !== undefined) { + parent.stereo(self._stereo, self._id); + } else if (self._pos) { + parent.pos(self._pos[0], self._pos[1], self._pos[2], self._id); + } else if (self._panner) { + // Disconnect the panner + self._panner.disconnect(0); + self._panner = undefined; + parent._refreshBuffer(self); + } + + // Complete resetting of the sound + return self._originalReset(); + }; + } + + // If a stereo or position was specified, set it up + if (spatialSound._stereo !== null && spatialSound._stereo !== undefined) { + spatialParent.stereo(spatialSound._stereo, spatialSound._id); + } else if (spatialSound._pos) { + spatialParent.pos(spatialSound._pos[0], spatialSound._pos[1], spatialSound._pos[2], spatialSound._id); } } /** - * Extend Howl instances with spatial audio methods via mixin. + * Handle load queue for spatial audio */ - private onHowlCreate(howl: Howl, _options: HowlOptions): void { - withSpatialHowl(howl); + private onHowlLoad(howl: Howl): void { + const spatial = howl as any; + + // Process any queued spatial audio actions + if (spatial._queue) { + for (let i = 0; i < spatial._queue.length; i++) { + const task = spatial._queue[i]; + if (task.event === 'stereo' || task.event === 'pos' || task.event === 'orientation') { + task.action(); + } + } + } } onUnregister(): void { - // Remove spatial audio methods from Howler instance const howler = globalPluginManager.getHowlerInstance(); - if (howler) { - // Remove spatial audio properties and methods - delete (howler as any)._pos; - delete (howler as any)._orientation; - delete (howler as any)._stereo; - delete (howler as any).pos; - delete (howler as any).orientation; - delete (howler as any).stereo; + if (!howler) { + return; + } + + // Clean up Howler instance + delete (howler as any)._pos; + delete (howler as any)._orientation; + delete (howler as any).pos; + delete (howler as any).orientation; + delete (howler as any).stereo; + + // Clean up all Howl instances + for (let i = 0; i < howler._howls.length; i++) { + const howl = howler._howls[i] as any; + + // Remove spatial methods from Howl + delete howl.stereo; + delete howl.pos; + delete howl.orientation; + delete howl.pannerAttr; + + // Remove spatial properties from Howl + delete howl._pos; + delete howl._orientation; + delete howl._stereo; + delete howl._pannerAttr; + delete howl._onstereo; + delete howl._onpos; + delete howl._onorientation; + + // Clean up all Sound instances in this Howl + if (howl._sounds) { + for (let j = 0; j < howl._sounds.length; j++) { + const sound = howl._sounds[j] as any; + + // Disconnect and remove panner nodes + if (sound._panner) { + const wasPlaying = !sound._paused; + + try { + // Disconnect panner from the audio graph + sound._panner.disconnect(0); + } catch (e) { + // Panner may already be disconnected + } + + // Remove panner reference + sound._panner = undefined; + + // If the sound was playing, we need to stop it and clean up the buffer source + // The user will need to call play() again to resume without spatial audio + if (wasPlaying && howl._webAudio && sound._node && sound._node.bufferSource) { + try { + // Stop the current buffer source + if (typeof (sound._node.bufferSource as any).stop === 'undefined') { + (sound._node.bufferSource as any).noteOff(0); + } else { + (sound._node.bufferSource as any).stop(0); + } + sound._node.bufferSource.disconnect(0); + } catch (e) { + // Buffer source may already be stopped or disconnected + } + + // Mark as paused so the audio graph can be reconnected on next play + sound._paused = true; + + // Clean up the buffer source - it will be recreated on next play + howl._cleanBuffer(sound._node); + } + } + + // Remove spatial properties from Sound + delete sound._pos; + delete sound._orientation; + delete sound._stereo; + delete sound._pannerAttr; + + // Restore original reset method if it was wrapped + if (sound._originalReset) { + sound.reset = sound._originalReset; + delete sound._originalReset; + } + } + } + + // Remove spatial-related queue items + if (howl._queue) { + howl._queue = howl._queue.filter((item: any) => { + return item.event !== 'stereo' && item.event !== 'pos' && item.event !== 'orientation'; + }); + } } } } diff --git a/src/plugins/spatial.ts b/src/plugins/spatial.ts index 0815a945..4c8fa634 100644 --- a/src/plugins/spatial.ts +++ b/src/plugins/spatial.ts @@ -6,4 +6,8 @@ export { SpatialAudioPlugin } from './spatial-plugin'; // Export spatial types for TypeScript support -export type { SpatialAudioState, SpatialHowler, SpatialHowl } from './spatial-plugin'; +export type { + SpatialAudioState, SpatialHowl, SpatialHowler, SpatialHowlOptions, SpatialHowlState, + SpatialSoundState +} from './spatial-plugin'; + From b4fda7a1dde30b7fbc8bf0ddef4d8b97dd88053d Mon Sep 17 00:00:00 2001 From: eatsjobs Date: Sat, 22 Nov 2025 23:51:42 +0100 Subject: [PATCH 16/25] Update CHANGELOG and README for Howler.js v3.0 release - Added comprehensive details about the breaking changes, new features, and improvements in the CHANGELOG for the first alpha release of Howler.js v3.0, including the migration guide from v2.x. - Updated README to reflect the new TypeScript support, plugin registration, and usage examples, including HTML import maps for cleaner module imports. - Enhanced documentation with TypeScript type annotations and examples for better developer experience. --- CHANGELOG.md | 153 ++++++++++++++++++++++++++++++++- README.md | 237 +++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 323 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b71f5c6a..c3bcf761 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,86 @@ +# 3.0.0 + +This is the first alpha release of Howler.js v3.0, a major rewrite that migrates the library to TypeScript and introduces a modern plugin architecture. + +## Breaking Changes + +- **ES Modules Only**: Howler.js v3.0+ is distributed exclusively as ES Modules. For CommonJS/UMD support, use v2.x. +- **TypeScript**: The library is now written in TypeScript with full type definitions included. +- **Spatial Plugin**: The spatial audio functionality has been moved to a plugin that must be explicitly registered. +- **Browser Support**: Requires modern browsers with ES Module support (Chrome 61+, Firefox 67+, Safari 11.1+, Edge 79+). + +### New Features + +- `ADDED` Complete TypeScript rewrite with full type definitions +- `ADDED` Modern plugin system with hook-based architecture (`HowlerPlugin`, `PluginManager`) +- `ADDED` Spatial audio plugin refactored to use the new plugin system +- `ADDED` Lightweight UA parser for consistent browser/device detection +- `ADDED` TypeScript types exported: `SpatialHowler`, `SpatialHowl`, `SpatialHowlOptions` +- `ADDED` Plugin lifecycle hooks: `onHowlerInit`, `onHowlCreate`, `onSoundCreate`, `onHowlLoad`, `onHowlDestroy`, `onUnregister` +- `ADDED` `Howler.addPlugin()` and `Howler.removePlugin()` methods for plugin management +- `ADDED` `Howler.hasPlugin()` method to check if a plugin is registered +- `ADDED` Import map examples in documentation for cleaner CDN usage +- `ADDED` Comprehensive TypeScript examples throughout documentation + +### Improvements + +- `CHANGED` All user agent detection now uses centralized `light-ua-parser` functions +- `CHANGED` Spatial plugin properly cleans up state when unregistered +- `CHANGED` Plugin hooks execute correctly for both HTML5 and WebAudio backends +- `CHANGED` Improved error handling in plugin system +- `CHANGED` Better separation of concerns with modular architecture +- `CHANGED` All code examples migrated to TypeScript with proper types + +### Migration Guide + +**From v2.x to v3.0:** + +1. **Update imports to ES Modules:** + + ```typescript + // v2.x (CommonJS) + const { Howl, Howler } = require('howler'); + + // v3.0 (ES Modules) + import { Howl, Howler } from 'howler'; + ``` + +2. **Register spatial plugin if needed:** + + ```typescript + // v3.0 - Spatial audio is now a plugin + import { SpatialAudioPlugin } from 'howler/plugins/spatial'; + const plugin = new SpatialAudioPlugin() + Howler.addPlugin(plugin); + ``` + +3. **Use TypeScript types for better IDE support:** + + ```typescript + import type { SpatialHowler, SpatialHowl } from 'howler/plugins/spatial'; + const howler: SpatialHowler = Howler as SpatialHowler; + const sound: SpatialHowl = new Howl({...}) as SpatialHowl; + ``` + +### Documentation + +- Updated all code examples to TypeScript +- Added import map examples for CDN and local builds +- Comprehensive plugin system documentation +- TypeScript type usage examples throughout + ## 2.2.3 (September 20, 2023) + - `FIXED` Invalid regex detection of Opera versions 100+ ([#1676](https://github.com/goldfire/howler.js/pull/1676)). - `FIXED` The `pannerAttr` method wouldn't set the values the first time it was called ([#1497](https://github.com/goldfire/howler.js/issues/1497)). - `FIXED` Error when refreshing the buffer on a sound that has already been unloaded ([#1508](https://github.com/goldfire/howler.js/issues/1508)). ## 2.2.3 (June 30, 2021) + - `FIXED` Fatal error in Chrome for iOS ([#1491](https://github.com/goldfire/howler.js/issues/1491)). ## 2.2.2 (June 27, 2021) + The README has been updated with more examples and various clarifications. PRs/issues with suggestions for further improvements are appreciated. - `CHANGED` Include `keydown` event when unlocking audio ([#1417](https://github.com/goldfire/howler.js/pull/1417)). @@ -17,12 +91,14 @@ The README has been updated with more examples and various clarifications. PRs/i - `FIXED` Before a sound had loaded, calling `pause()` after `seek()` didn't have the intended behavior ([#1439](https://github.com/goldfire/howler.js/issues/1439)). ## 2.2.1 (Oct 25, 2020) + - `FIXED` The latest Safari 14 changed how WAV support was detected ([#1415](https://github.com/goldfire/howler.js/pull/1415)). - `FIXED` Edge case that could cause an infinite loop while fading ([#1369](https://github.com/goldfire/howler.js/pull/1369)). - `FIXED` Calling `seek` without a seek value while a file was still loading no longer adds it to the queue and correctly returns `0` ([#1189](https://github.com/goldfire/howler.js/issues/1189)). - `FIXED` Correctly handle finite audio files that return `Infinity` duration in Safari ([#658](https://github.com/goldfire/howler.js/pull/658)). ## 2.2.0 (May 17, 2020) + - `ADDED` New `xhr` property that allows setting custom headers (such as for auth), changing the `withCredentials` setting and specifying the HTTP method for the request. These only apply to Web Audio ([#997](https://github.com/goldfire/howler.js/pull/997)). - `ADDED` New `Howler.stop()` global stop method to stop all sounds at once ([#1308](https://github.com/goldfire/howler.js/issues/1308)). - `ADDED` Support for `m4b` audio format ([#1170](https://github.com/goldfire/howler.js/pull/1170)). @@ -33,24 +109,29 @@ The README has been updated with more examples and various clarifications. PRs/i - `FIXED` Memory leak in Safari when an audio context can't be unlocked ([#1338](https://github.com/goldfire/howler.js/pull/1338)). ### Breaking Changes -* The `xhrWithCredentials` property is now included in the `xhr` property object with key `withCredentials`. + +- The `xhrWithCredentials` property is now included in the `xhr` property object with key `withCredentials`. ## 2.1.3 (December 24, 2019) + - `FIXED` Don't try to obtain HTML5 audio if there is no audio support ([#1191](https://github.com/goldfire/howler.js/issues/1191)). - `FIXED` The x/y/z orientations for the top of the listener weren't being set properly ([#1221](https://github.com/goldfire/howler.js/pull/1221)). - `FIXED` Race condition that could prevent looping audio from always looping ([#1225](https://github.com/goldfire/howler.js/pull/1225)). - `FIXED` Race condition that could cause the main volume to be reset to 1 if called before `unlockAudio` ([#1210](https://github.com/goldfire/howler.js/pull/1210)). ## 2.1.2 (April 19, 2019) + - `FIXED` Removed browser check for auto play unlock since all major browsers now implement this. - `FIXED` Live streams now stop downloading when they are stopped, also fixing issue in Chrome with stopping twice ([#1129](https://github.com/goldfire/howler.js/issues/1129)). - `FIXED` Prevent error in Edge when `Audio` isn't supported ([#1147](https://github.com/goldfire/howler.js/issues/1147)). ## 2.1.1 (December 21, 2018) + - `FIXED` Regression that broke simple play/pause usage in certain edge cases ([#1101](https://github.com/goldfire/howler.js/issues/1101)). - `FIXED` Loading and unloading multiple Howls with the same src could cause them all to unload ([#1103](https://github.com/goldfire/howler.js/issues/1103)). ## 2.1.0 (December 12, 2018) + - `ADDED` Howler now maintains a general pool of HTML5 Audio nodes that are unlocked on first user input, which fixes issues with subsequent HTML5 Audio plays not working ([#1008](https://github.com/goldfire/howler.js/pull/1008)). - `ADDED` New global `html5PoolSize` option that allows setting the default size of the HTML5 Audio object pool ([#1008](https://github.com/goldfire/howler.js/pull/1008)). - `CHANGED` Since locking of audio is no longer mobile-only, `mobileAutoEnable` has been renamed to `autoUnlock`. @@ -64,15 +145,19 @@ The README has been updated with more examples and various clarifications. PRs/i - `FIXED` Calling `play` twice before a sound had loaded could lead to both sounds having the same `ID` ([#1060](https://github.com/goldfire/howler.js/issues/1060)). ### Breaking Changes -* If you are directly setting `Howler.mobileAutoEnable` (it defaults to true), then you should change this to `Howler.autoUnlock`. -* The new HTML5 Audio object pool shouldn't change anything for 99% of use-cases, but if for whatever reason you don't want to use the pool, you can set `html5PoolSize` to 0 to bypass using the pool. + +- If you are directly setting `Howler.mobileAutoEnable` (it defaults to true), then you should change this to `Howler.autoUnlock`. + +- The new HTML5 Audio object pool shouldn't change anything for 99% of use-cases, but if for whatever reason you don't want to use the pool, you can set `html5PoolSize` to 0 to bypass using the pool. ## 2.0.15 (August 24, 2018) + - `FIXED` Errors with touch events and blocked click events in Chrome ([#1003](https://github.com/goldfire/howler.js/issues/1003) [#1011](https://github.com/goldfire/howler.js/issues/1011) [#1025](https://github.com/goldfire/howler.js/issues/1025) [#1026](https://github.com/goldfire/howler.js/issues/1026)). - `FIXED` Audio decoding error wasn't always handled correctly ([#1019](https://github.com/goldfire/howler.js/pull/1019)). - `FIXED` Potential error during playback in Internet Explorer 11 ([#1016](https://github.com/goldfire/howler.js/pull/1016)). ## 2.0.14 (July 12, 2018) + - `CHANGED` Auto unlocking of audio now runs on Chrome to fix issue with HTML5 Audio needing user interaction. - `CHANGED` Added a new `unlock` event that is fired when the auto unlock happens. - `CHANGED` A `playerror` now gets fired when HTML5 Audio fails to play due to lack of user interaction. @@ -83,35 +168,42 @@ The README has been updated with more examples and various clarifications. PRs/i - `FIXED` Another issue in Safari where spatial orientation was throwing an error. ## 2.0.13 (June 22, 2018) + - `FIXED` Prevent `stop` event from firing alongside `end` when using HTML5 Audio ([#974](https://github.com/goldfire/howler.js/issues/074)). - `FIXED` Correctly reset a `Sound` after using spatial audio ([#962](https://github.com/goldfire/howler.js/issues/962)). - `FIXED` Remove a `Howl` from cache when unloaded after failing to load ([#978](https://github.com/goldfire/howler.js/issues/978)). - `FIXED` Race condition could lead to error when cleaning the buffer. ## 2.0.12 (May 9, 2018) + - `FIXED` The previous Chrome deprecation fixes broke spatial positioning in Safari. ## 2.0.10 (May 5, 2018) + - `FIXED` Fixed another Chrome deprecation warning when using panning methods ([#923](https://github.com/goldfire/howler.js/issues/923)). - `FIXED` Playback rate wasn't working correctly in Internet Explorer when defined in the `Howl` constructor ([#936](https://github.com/goldfire/howler.js/issues/936)). - `FIXED` Looped audio would only play twice in Internet Explorer ([#921](https://github.com/goldfire/howler.js/issues/921)). ## 2.0.9 (February 10, 2018) + - `FIXED` More accurate HTML5 Audio `end` timer and fix for Firefox streams ending early ([#883](https://github.com/goldfire/howler.js/issues/883)). - `FIXED` Prevent `play` events from duplicating in certain instances ([#899](https://github.com/goldfire/howler.js/issues/899)). - `FIXED` Add second parameter to HTML5 Audio playback promise to fix Safari error ([#896](https://github.com/goldfire/howler.js/pull/896)). - `FIXED` Refactored the internal queue system to fix various edge cases. ## 2.0.8 (January 19, 2018) + - `CHANGED` Fades now use elapsed time to be more accurate when intervals are inconsistent ([#885](https://github.com/goldfire/howler.js/issues/885)). - `CHANGED` Improve timing of short fades ([#884](https://github.com/goldfire/howler.js/issues/884)). - `FIXED` Fixed another Chrome deprecation when setting playback rate. - `FIXED` Prevent `onplay` from firing when first setting `stereo` value ([#843](https://github.com/goldfire/howler.js/issues/843)). ## 2.0.7 (December 18, 2017) + - `FIXED` Accidental `const` was included in the previous version. ## 2.0.6 (December 15, 2017) + - `FIXED` Replaced deprecated `gain.value` and `gain.pan.value` with `setValueAtTime` ([#856](https://github.com/goldfire/howler.js/issues/856)). - `FIXED` Audio sprites weren't ending correctly in Internet Explorer 11 ([#841](https://github.com/goldfire/howler.js/issues/841)). - `FIXED` Correctly set group volume when fading ([#539](https://github.com/goldfire/howler.js/issues/539)). @@ -120,6 +212,7 @@ The README has been updated with more examples and various clarifications. PRs/i - `FIXED` Incorrect reference to global `_scratchBuffer` ([#834](https://github.com/goldfire/howler.js/pull/834)). ## 2.0.5 (October 6, 2017) + - `ADDED` Add support for `withCredentials` to Web Audio XHR requests ([#610](https://github.com/goldfire/howler.js/pull/610)). - `ADDED` Add `playerror` event for when mobile HTML5 audio is unable to play ([#774](https://github.com/goldfire/howler.js/issues/774)). - `FIXED` Refactor fade method to eliminate bind memory allocations (no change to API). @@ -130,6 +223,7 @@ The README has been updated with more examples and various clarifications. PRs/i - `FIXED` Various corrections and improvements to the spatial audio documentation. ## 2.0.4 (June 9, 2017) + - `CHANGED` Removed the `resuming` state, which wasn't actually being used and was leading to a bug on Android ([#679](https://github.com/goldfire/howler.js/pull/679)). - `CHANGED` Any playback initiated before the sound has loaded will now go into the queue to fix various race conditions ([#714](https://github.com/goldfire/howler.js/pull/714)). - `FIXED` Correctly initialize an AudioContext with the global mute status ([#714](https://github.com/goldfire/howler.js/pull/714)). @@ -143,6 +237,7 @@ The README has been updated with more examples and various clarifications. PRs/i - `FIXED` `npm` warning caused by invalid license definition ([#763](https://github.com/goldfire/howler.js/pull/763)). ## 2.0.3 (March 11, 2017) + - `CHANGED` Unloading a sound no longer fires the `end` event ([#675](https://github.com/goldfire/howler.js/pull/675)). - `FIXED` Remove `setTimeout` wrapper on HTML5 `play` call to fix issues on mobile browsers ([#694](https://github.com/goldfire/howler.js/pull/694)). - `FIXED` Remove rare possibility of duplicate sound ID's by using global counter ([#709](https://github.com/goldfire/howler.js/issues/709)). @@ -152,6 +247,7 @@ The README has been updated with more examples and various clarifications. PRs/i - `FIXED` Regression in Firefox relating to spatial audio ([#664](https://github.com/goldfire/howler.js/issues/664)). ## 2.0.2 (December 4, 2016) + - `FIXED` Wait to begin playback until AudioContext has resumed ([#643](https://github.com/goldfire/howler.js/issues/643)). - `FIXED` Run `noAudio` check on initial setup instead of waiting for first `Howl` ([#619](https://github.com/goldfire/howler.js/issues/619)). - `FIXED` Add `play` event to start of queue when `autoplay` is used ([#659](https://github.com/goldfire/howler.js/issues/659)). @@ -161,6 +257,7 @@ The README has been updated with more examples and various clarifications. PRs/i - `FIXED` Always fire pause event even if sound is already paused ([#639](https://github.com/goldfire/howler.js/issues/639)). ## 2.0.1 (October 14, 2016) + - `ADDED` Support for FLAC audio files. - `FIXED` Improve fading performance when short fade times are used ([#621](https://github.com/goldfire/howler.js/issues/621)). - `FIXED` Correctly handle fades from 0 to 0 volume ([#575](https://github.com/goldfire/howler.js/issues/575)). @@ -172,6 +269,7 @@ The README has been updated with more examples and various clarifications. PRs/i - `FIXED` Set correct loop start/end when calling `loop` on a sprite ([#604](https://github.com/goldfire/howler.js/issues/604)). ## 2.0.0 (July 19, 2016) + This major release contains just a few breaking changes outlined below. Howler.js has been rewritten from the ground up using the knowledge and work since the initial release. There's a long list of additions and improvements, which I urge you to read through as the library has evolved quite a bit over this time. The biggest change is how you should think about your audio when using howler.js. There is now the concept of global (`Howler`), group (`Howl`) and single sound (`Sound`). Each sound that is played gets its own `Sound` object that can be manipulated, giving much greater control over playback, whether using sprites or not. `Howl` method calls can then apply to one sound or all in the group. @@ -187,6 +285,7 @@ Howler.js now also has the concept of plugins. The core represents 100% compatib Read more about the update [in this blog post](http://goldfirestudios.com/blog/143/howler.js-v2.0-Released). ### Breaking Changes + - The `buffer` option is now named `html5`. Use this to force HTML5 Audio usage. - The `urls` option is now named `src` to specify the audio file(s) to play. - The `pos` method has been renamed to `seek`. @@ -231,6 +330,7 @@ sound.once('fade', function(){ ``` ### New Features + - Lots of general code cleanup, simplification and reorganization. - Howler.js is now modularized. The core represents the initial goal for howler.js with 100% compatibility across HTML5 Audio and Web Audio. The spatial plugin adds spatial and stereo support through Web Audio API. - The new structure allows for full control of sprite playback (this was buggy or didn't work at all before). @@ -255,6 +355,7 @@ sound.once('fade', function(){ - Web Audio AudioContext now automatically suspends and resumes to lower processing and power usage. ### Bug Fixes + - Improved the `ext` option and made it especially usefully for playing streams (for example, SoundCloud). - The `fade` method now uses native Web Audio fading when in that mode. - Fades are now automatically stopped when a new one is started, volume is changed or the sound is paused/stopped. @@ -284,25 +385,31 @@ sound.once('fade', function(){ - Only setup AudioContext after first `Howl` is setup so that background audio on mobile devices behaves as expected. ## 1.1.29 (January 22, 2016) + - `ADDED` Error messages added onto each `loaderror` event (thanks Philip Silva). - `FIXED` Various edge-case bugs by no longer comparing functions by string in `.off()` (thanks richard-livingston). - `FIXED` Edge case where multiple overlapping instances of the same sound won't all fire `end` (thanks richard-livingston). - `FIXED` `end` event now fires correctly when changing the `rate` of a sound. ## 1.1.28 (October 22, 2015) + - `FIXED` Typo with iOS enabler that was preventing it from working. ## 1.1.27 (October 2, 2015) + - `FIXED` Automatic audio unlocking on iOS 9 by switching to `touchend` from `touchstart`. ## 1.1.26 (April 21, 2015) + - `FIXED` Looping in Chrome due to a change in the Web Audio spec implemented in Chrome 42. ## 1.1.25 (July 29, 2014) + - `ADDED` The `AudioContext` is now available on the global `Howler` object (thanks Matt DesLauriers). - `FIXED` When falling back to HTML5 Audio due to XHR error, delete cache for source file to prevent multi-playback issues. ## 1.1.24 (July 20, 2014) + - `FIXED` Improved performance of loading files using data URIs (thanks Rob Wu). - `FIXED` Data URIs now work with Web Audio API (thanks Rob Wu). - `FIXED` Omitting the second parameter of the `off` method now correctly clears all events by that name (thanks Gabriel Munteanu). @@ -310,37 +417,44 @@ sound.once('fade', function(){ - `FIXED` Small error fix in iOS check. ## 1.1.23 (July 2, 2014) + - `FIXED` Playing multiple sprites rapidly with HTML5 Audio cause the sprite to break due to a v1.1.22 update. - `FIXED` Don't run the iOS test if there is no audio context, which prevents a breaking error. ## 1.1.22 (June 28, 2014) + - `ADDED` Howler will now automatically attempt to unlock audio on iOS (thanks Federico Brigante). - `ADDED` New `codecs` global Howler method to check for codec support in the current browser (thanks Jay Oster). - `FIXED` End timers are now correctly cleaned up when a sound naturally completes rather than being forced to stop. ## 1.1.21 (May 28, 2014) + - `ADDED` Support for npm and bower (thanks Morantron). - `ADDED` Support for audio/aac, audio/m4a and audio/mp4 mime types (thanks Federico Brigante). - `FIXED` Calculation of duration after pausing a sprite that was sometimes causing unexpected behavior. - `FIXED` Clear the event listener when creating a new HTML5 Audio node. ## 1.1.20 (April 18, 2014) + - `ADDED` When using Web Audio API, the panningModel now defaults to 'equalpower' to give higher quality sound. It then automatically switches to 'HRTF' when using 3D sound. This can also be overridden with the new `model` property. - `FIXED` Another bug causing issues in CocoonJS (thanks Olivier Biot). - `FIXED` Issue that could have caused invalid state errors and a memory leak when unloading in Internet Explorer. - `FIXED` The documentation has been updated to include the `rate` property. ## 1.1.19 (April 14, 2014) + - `ADDED` Added CocoonJS support (thanks Olivier Biot). - `FIXED` Several issues with pausing sprite instances by overhauling how end timers are tracked and cleared internally. - `FIXED` Prevent error when using a server-side require where window is absent (thanks AlexMost). ## 1.1.18 (March 23, 2014) + - `FIXED` Muting a looping sound now correctly keeps the sound muted when using HTML5 Audio. - `FIXED` Wrap AudioContext creation in try/catch to gracefully handle browser bugs: [Chromium issue](https://code.google.com/p/chromium/issues/detail?id=308784) (thanks Chris Buckley). - `FIXED` Listen for HTML5 Audio errors and fire `loaderror` if any are encountered (thanks digitaltonic). ## 1.1.17 (February 5, 2014) + - `FIXED` Another bug in Chrome that would throw an error when pausing/stopping when a source is already stopped. - `ADDED` CommonJS support for things like Browserify (thanks Michal Kuklis). - `ADDED` Support for playback mp4 files. @@ -349,10 +463,12 @@ sound.once('fade', function(){ - `FIXED` The `onend` callback now correctly fires when changing the pos of a sound after it has started playing and when it is using HTML5 Audio. ## 1.1.16 (January 8, 2014) + - `FIXED` Prevent InvalidStateError when unloading a sound that has already been stopped. - `FIXED` Bug in unload method that prevented the first sound from being unloaded. ## 1.1.15 (December 28, 2013) + - `FIXED` Bug that prevented master volume from being set to 0. - `FIXED` Bug that prevented initial volume from being set to 0. - `FIXED` Update the README to accurately show `autoplay` as defaulting to `false`. @@ -361,45 +477,55 @@ sound.once('fade', function(){ - `FIXED` An issue with looping after resuming playback when in WebAudio playback (thanks anzev). ## 1.1.14 (October 18, 2013) + - `FIXED` Critical bug fix that was breaking support on some browsers and some codecs. ## 1.1.13 (October 17, 2013) + - `FIXED` Code cleanup by removing redundant `canPlay` object (thanks Fabien). - `FIXED` File extensions are now detected correctly if there is a query string with dots in the filename (thanks theshock). - `FIXED` Fire `onloaderror` if a bad filename is passed with the `urls` property. ## 1.1.12 (September 12, 2013) + - `UPDATED` Changed AMD definition to anonymous module and define it as global always (thanks Fabien). - `ADDED` Added the `rate` property to `Howl` object creation, allowing you to specify the playback rate. This only works when using Web Audio (thanks Qqwy). - `FIXED` Prevent some instances of IE9 from throwing "Not Implemented" error (thanks Tero Tilus). ## 1.1.11 (July 28, 2013) + - `FIXED` Bug caused by trying to disconnect audio node when using HTML5 Audio. - `FIXED` Correctly return the sound's position when it is paused. - `FIXED` Another bug that caused looping sounds to not always correctly resume after a pause. ## 1.1.10 (July 26, 2013) + - `ADDED` New `unload` method to destroy a Howl object. This will stop all associated sounds instantly and remove the sound from the cache. - `FIXED` When using Web Audio, loop from the correct position after pausing the sound halfway through. - `FIXED` Always return a number when getting a sound's position with the `pos` method, and always return the reference to the sound when setting a sound that hasn't loaded. ## 1.1.9 (July 11, 2013) + - `FIXED` Issue where calling the `volume` method before a sound had loaded prevented the volume from being changed. ## 1.1.8 (July 10, 2013) + - `FIXED` `urls` method now works again, and can take a string rather than an array if only one url is being passed. - `FIXED` Make `node.play` async when not using webAudio (thanks Alex Dong). ## 1.1.7 (May 30, 2013) + - `FIXED` Hotfix for a missing parameter that somehow missed the 1.1.6 commit in global muting. ## 1.1.6 (May 30, 2013) + - `ADDED` A general `fade` method that allows a playing sound to be faded from one volume to another. - `DEPRECATED` The `fadeIn` and `fadeOut` methods should no longer be used and have been deprecated. These will be removed in a future major release. - `FIXED` No longer require the sprite parameter to be passed into the `play` method when just passing a callback function. - `FIXED` Cleaned up global muting code. (thanks arnorhs). ## 1.1.5 (May 3, 2013) + - `ADDED` Support for the Ogg Opus codec (thanks Andrew Carpenter). - `ADDED` Semver tags for easy package management (thanks Martin Reurings). - `ADDED` Improve style/readability of code that discovers which audio file extension to use (thanks Fabien). @@ -407,23 +533,28 @@ sound.once('fade', function(){ - `FIXED` A few small typos in the comments. (thanks VAS). ## 1.1.4 (April 28, 2013) + - `FIXED` A few small bugs that broke global mute and unmute when using HTML5 Audio. ## 1.1.3 (April 27, 2013) + - `FIXED` Bug that prevented global mute from working 100% of the time when using HTML5 Audio. ## 1.1.2 (April 24, 2013) + - `FIXED` Calling `volume` before `play` now works as expected. - `FIXED` Edge case issue with cache cleaning. - `FIXED` Load event didn't fire when new URLs were loaded after the initial load. ## 1.1.1 (April 17, 2013) + - `ADDED` `onloaderror` event fired when sound fails to load (thanks Thiago de Barros Laceda). - `ADDED` `format` property that overrides the URL extraction of the file format (thanks Kenan Shifflett). - `FIXED` AMD implementation now only defines one module and removes global scope (thanks Kenan Shifflett). - `FIXED` Broken chaining with `play` method. ## 1.1.0 (April 11, 2013) + - `ADDED` New `pos3d` method that allows for positional audio (Web Audio API only). - `ADDED` Multi-playback control system that allows for control of specific play instances when sprites are used. A callback has been added to the `play` method that returns the `soundId` for the playback instance. This can then be passed as the optional last parameter to other methods to control that specific playback instead of the whole sound object. - `ADDED` Pass the `Howl` object reference as the first parameter in the custom event callbacks. @@ -437,23 +568,29 @@ sound.once('fade', function(){ - `FIXED` Various code cleanup and optimizations. ## 1.0.13 (March 20, 2013) + - `ADDED` Support for AMD loading as a module (thanks @mostlygeek). ## 1.0.12 (March 28, 2013) + - `ADDED` Automatically switch to HTML5 Audio if there is an error due to CORS. - `FIXED` Check that only numbers get passed into volume methods. ## 1.0.11 (March 8, 2013) + - `ADDED` Exposed `usingWebAudio` value through the global `Howler` object. - `FIXED` Issue with non-sprite HTML5 Audio clips becoming unplayable (thanks Paul Morris). ## 1.0.10 (March 1, 2013) + - `FIXED` Issue that caused simultaneous playback of audio sprites to break while using HTML5 Audio. ## 1.0.9 (March 1, 2013) + - `ADDED` Spec-implementation detection to cover new and deprecated Web Audio API methods (thanks @canuckistani). ## 1.0.8 (February 25, 2013) + - `ADDED` New `onplay` event. - `ADDED` Support for playing audio from base64 encoded strings. - `FIXED` Issue with soundId not being unique when multiple sounds were played simultaneously. @@ -461,35 +598,43 @@ sound.once('fade', function(){ - `FIXED` Issue with `onend` timer not getting cleared all the time. ## 1.0.7 (February 18, 2013) + - `FIXED` Cancel the correct timer when multiple HTML5 Audio sounds are played at the same time. - `FIXED` Make sure howler.js is future-compatible with UglifyJS 2. - `FIXED` Duration now gets set correctly when pulled from cache. - `FIXED` Tiny typo in README.md (thanks @johnfn). ## 1.0.6 (February 8, 2013) + - `FIXED` Issue with global mute calls happening before an HTML5 Audio element is loaded. ## 1.0.5 (February 7, 2013) + - `FIXED` Global mute now also mutes all future sounds that are played until `unmute` is called. ## 1.0.4 (February 6, 2013) + - `ADDED` Support for WebM audio. - `FIXED` Issue with volume changes when on HTML5 Audio. - `FIXED` Round volume values to fix inconsistencies in fade in/out methods. ## 1.0.3 (February 2, 2013) -- `FIXED` Make sure `self` is always defined before returning it. + +- `FIXED` Make sure `self` is always defined before returning it. ## 1.0.2 (February 1, 2013) + - `ADDED` New `off` method that allows for the removal of custom events. - `FIXED` Issue with chaining the `on` method. - `FIXED` Small typo in documentation. ## 1.0.1 (January 30, 2013) + - `ADDED` New `buffer` property that allows you to force the use of HTML5 on specific sounds to allow streaming of large audio files. - `ADDED` Support for multiple events per event type. - `FIXED` Issue with method chaining before a sound was ready to play. - `FIXED` Use `self` everywhere instead of `this` to maintain consistency. ## 1.0.0 (January 28, 2013) + - First commit diff --git a/README.md b/README.md index 5147bdda..c9e0d099 100644 --- a/README.md +++ b/README.md @@ -77,20 +77,9 @@ Several options to get up and running: * Install with [Bower](http://bower.io/): `bower install howler` * Hosted CDN: [`cdnjs`](https://cdnjs.com/libraries/howler) [`jsDelivr`](https://www.jsdelivr.com/projects/howler.js) -In the browser: - -```html - - -``` - As an ES Module (ESM): -```javascript +```typescript import { Howl, Howler } from 'howler'; import { SpatialAudioPlugin } from 'howler/plugins/spatial'; ``` @@ -152,6 +141,8 @@ export class MyPlugin extends HowlerPlugin { Register plugins directly with Howler: +**TypeScript/ES Modules:** + ```typescript import { Howler, Howl } from 'howler'; import { SpatialAudioPlugin, type SpatialHowler, type SpatialHowl } from 'howler/plugins/spatial'; @@ -160,11 +151,13 @@ import { SpatialAudioPlugin, type SpatialHowler, type SpatialHowl } from 'howler Howler.addPlugin(new SpatialAudioPlugin()); // Type annotations for TypeScript support +import type { SpatialHowlOptions } from 'howler/plugins/spatial'; + const listener: SpatialHowler = Howler as SpatialHowler; const sound: SpatialHowl = new Howl({ src: ['audio.mp3'], pos: [0, 0, 0], // Spatial audio options available -}); +} as SpatialHowlOptions) as SpatialHowl; // Use spatial methods with full type support sound.pos(10, 5, 0); @@ -172,6 +165,61 @@ listener.stereo(0.5); listener.orientation(1, 0, 0, 0, 1, 0); ``` +**HTML with Script Type Module:** + +```html + + + + Howler.js Spatial Audio Example + + + + + + + + +``` + #### Managing Plugins ```typescript @@ -185,14 +233,6 @@ Howler.addPlugin(spatialPlugin); // Unregister a plugin (pass the plugin instance) Howler.removePlugin(spatialPlugin); - -// For more advanced management, use the global plugin manager: -import { globalPluginManager } from 'howler'; - -// Check if plugin is registered -if (globalPluginManager.isRegistered('spatial-audio')) { - console.log('Spatial audio is available'); -} ``` #### Available Plugins @@ -201,27 +241,34 @@ if (globalPluginManager.isRegistered('spatial-audio')) { Adds 3D spatial audio and stereo panning support: +**TypeScript/ES Modules:** + ```typescript import { Howler, Howl } from 'howler'; import { SpatialAudioPlugin } from 'howler/plugins/spatial'; +import type { SpatialHowler, SpatialHowl, SpatialHowlOptions } from 'howler/plugins/spatial'; // Register the plugin Howler.addPlugin(new SpatialAudioPlugin()); -const sound = new Howl({ +// Type Howler with spatial capabilities +const howler: SpatialHowler = Howler as SpatialHowler; + +// Create sound with spatial audio options +const sound: SpatialHowl = new Howl({ src: ['audio.mp3'], pos: [0, 0, 0], // 3D position stereo: 0, // Stereo pan (-1 to 1) orientation: [1, 0, 0], // Direction the sound is facing coneInnerAngle: 360, // Cone parameters distanceModel: 'inverse', // Distance attenuation model -}); +} as SpatialHowlOptions) as SpatialHowl; // Set listener position (where the "ear" is) -Howler.pos(0, 0, 0); +howler.pos(0, 0, 0); // Set listener orientation (which way they're facing) -Howler.orientation(0, 0, -1, 0, 1, 0); +howler.orientation(0, 0, -1, 0, 1, 0); // Set sound position (where the sound is in 3D space) sound.pos(10, 5, 0); @@ -241,32 +288,69 @@ sound.pannerAttr({ #### Plugin Lifecycle Hooks -Available hooks that plugins can implement: +Available hooks that plugins can implement in their `getHooks()` method: -* `onRegister()` - Called when plugin is registered -* `onHowlerInit(howler)` - Called when Howler global is initialized +* `onHowlerInit(howler)` - Called when Howler global is initialized. This is called either when Howler initializes (if plugin was registered before) or immediately during registration (if Howler is already initialized). * `onHowlCreate(howl, options)` - Called when a Howl instance is created * `onSoundCreate(sound, parent)` - Called when a Sound instance is created * `onHowlLoad(howl)` - Called when a Howl instance loads * `onHowlDestroy(howl)` - Called when a Howl instance is destroyed -* `onUnregister()` - Called when plugin is unregistered (cleanup) + +**Note:** `onUnregister()` is a cleanup method on the `HowlerPlugin` class itself (not a hook), which is called when the plugin is unregistered. Override this method in your plugin class to perform cleanup. ### Examples #### Most basic, play an MP3 -```javascript -var sound = new Howl({ +**TypeScript:** + +```typescript +import { Howl } from 'howler'; + +const sound = new Howl({ src: ['sound.mp3'] }); sound.play(); ``` +**HTML with Script Type Module:** + +```html + + + + Howler.js Basic Example + + + + + + + + +``` + #### Streaming audio (for live audio or large files) -```javascript -var sound = new Howl({ +```typescript +import { Howl } from 'howler'; + +const sound = new Howl({ src: ['stream.mp3'], html5: true }); @@ -276,13 +360,15 @@ sound.play(); ##### More playback options -```javascript -var sound = new Howl({ +```typescript +import { Howl } from 'howler'; + +const sound = new Howl({ src: ['sound.webm', 'sound.mp3', 'sound.wav'], autoplay: true, loop: true, volume: 0.5, - onend: function() { + onend: () => { console.log('Finished!'); } }); @@ -290,8 +376,10 @@ var sound = new Howl({ ##### Define and play a sound sprite -```javascript -var sound = new Howl({ +```typescript +import { Howl } from 'howler'; + +const sound = new Howl({ src: ['sounds.webm', 'sounds.mp3'], sprite: { blast: [0, 3000], @@ -306,43 +394,51 @@ sound.play('laser'); ##### Listen for events -```javascript -var sound = new Howl({ +```typescript +import { Howl } from 'howler'; + +const sound = new Howl({ src: ['sound.webm', 'sound.mp3'] }); // Clear listener after first call. -sound.once('load', function(){ +sound.once('load', () => { sound.play(); }); // Fires when the sound finishes playing. -sound.on('end', function(){ +sound.on('end', () => { console.log('Finished!'); }); ``` ##### Control multiple sounds -```javascript -var sound = new Howl({ +```typescript +import { Howl } from 'howler'; + +const sound = new Howl({ src: ['sound.webm', 'sound.mp3'] }); // Play returns a unique Sound ID that can be passed // into any method on Howl to control that specific sound. -var id1 = sound.play(); -var id2 = sound.play(); +const id1: number | null = sound.play(); +const id2: number | null = sound.play(); // Fade out the first sound and speed up the second. -sound.fade(1, 0, 1000, id1); -sound.rate(1.5, id2); +if (id1 !== null) { + sound.fade(1, 0, 1000, id1); +} +if (id2 !== null) { + sound.rate(1.5, id2); +} ``` -##### ES6 +##### ES Modules -```javascript -import {Howl, Howler} from 'howler'; +```typescript +import { Howl, Howler } from 'howler'; // Setup the new Howl. const sound = new Howl({ @@ -394,10 +490,13 @@ Set to `true` to load the audio muted. 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](https://github.com/tonistiigi/audiosprite). -```javascript +```typescript +import { Howl } from 'howler'; + new Howl({ + src: ['sounds.mp3'], sprite: { - key1: [offset, duration, (loop)] + key1: [offset, duration, loop] // loop is optional boolean }, }); ``` @@ -418,9 +517,12 @@ howler.js automatically detects your file format from the extension, but you may 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](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials)), include them with this parameter. Each is optional (method defaults to `GET`, headers default to `null` and withCredentials defaults to `false`). For example: -```javascript +```typescript +import { Howl } from 'howler'; + // Using each of the properties. new Howl({ + src: ['audio.mp3'], xhr: { method: 'POST', headers: { @@ -432,6 +534,7 @@ new Howl({ // Only changing the method. new Howl({ + src: ['audio.mp3'], xhr: { method: 'POST', } @@ -774,8 +877,10 @@ Get/set the direction the listener is pointing in the 3D cartesian space. A fron Each `new Howl()` instance is also a group. You can play multiple sound instances from the `Howl` and control them individually or as a group (note: each `Howl` can only contain a single audio file). For example, the following plays two sounds from a sprite, changes their volume together and then pauses both of them at the same time. -```javascript -var sound = new Howl({ +```typescript +import { Howl } from 'howler'; + +const sound = new Howl({ src: ['sound.webm', 'sound.mp3'], sprite: { track01: [0, 20000], @@ -800,17 +905,21 @@ setTimeout(function() { By default, audio on mobile browsers and Chrome/Safari is locked until a sound is played within a user interaction, and then it plays normally the rest of the page session ([Apple documentation](https://developer.apple.com/library/safari/documentation/audiovideo/conceptual/using_html5_audio_video/PlayingandSynthesizingSounds/PlayingandSynthesizingSounds.html)). The default behavior of howler.js is to attempt to silently unlock audio playback by playing an empty buffer on the first `touchend` event. This behavior can be disabled by calling: -```javascript +```typescript +import { Howler, Howl } from 'howler'; + Howler.autoUnlock = false; ``` If you try to play audio automatically on page load, you can listen to a `playerror` event and then wait for the `unlock` event to try and play the audio again: -```javascript -var sound = new Howl({ +```typescript +import { Howl } from 'howler'; + +const sound = new Howl({ src: ['sound.webm', 'sound.mp3'], - onplayerror: function() { - sound.once('unlock', function() { + onplayerror: () => { + sound.once('unlock', () => { sound.play(); }); } @@ -823,8 +932,10 @@ sound.play(); Full support for playback of the Dolby Audio format (currently support in Edge and Safari) is included. However, you must specify that the file you are loading is `dolby` since it is in a `mp4` container. -```javascript -var dolbySound = new Howl({ +```typescript +import { Howl } from 'howler'; + +const dolbySound = new Howl({ src: ['sound.mp4', 'sound.webm', 'sound.mp3'], format: ['dolby', 'webm', 'mp3'] }); From 57597ec902213f06e036ef3750990c3e036b3f0a Mon Sep 17 00:00:00 2001 From: eatsjobs Date: Sat, 22 Nov 2025 23:54:33 +0100 Subject: [PATCH 17/25] Refactor HowlerGlobal and Sound classes for improved readability and consistency - Removed the `init` method from HowlerGlobal and directly initialized properties in the constructor for clarity. - Replaced `self` references with `this` for consistency across methods. - Simplified the plugin registration process by removing the `onRegister` hook from the PluginHooks interface, streamlining the plugin lifecycle. - Enhanced documentation comments for better understanding of plugin initialization behavior. This refactor aims to enhance code maintainability and readability while ensuring existing functionality remains intact. --- src/howler.core.ts | 1103 ++++++++++++++++++++--------------------- src/plugins/plugin.ts | 21 +- 2 files changed, 539 insertions(+), 585 deletions(-) diff --git a/src/howler.core.ts b/src/howler.core.ts index c12632c0..244bfa57 100644 --- a/src/howler.core.ts +++ b/src/howler.core.ts @@ -40,63 +40,55 @@ export class HowlerGlobal { _mobileUnloaded?: boolean; constructor() { - this.init(); - } - - init(): HowlerGlobal { - const self = this; - - self._counter = 1000; - self._html5AudioPool = []; - self.html5PoolSize = 10; - self._codecs = {}; - self._howls = []; - self._muted = false; - self._volume = 1; - self._canPlayEvent = 'canplaythrough'; - self._navigator = typeof window !== 'undefined' && window.navigator ? window.navigator : null; - - self.masterGain = null; - self.noAudio = false; - self.usingWebAudio = true; - self.autoSuspend = true; - self.ctx = null; - self.autoUnlock = true; - - self._setup(); + // Initialize all properties (explicit initialization ensures correct values) + this._counter = 1000; + this._html5AudioPool = []; + this.html5PoolSize = 10; + this._codecs = {}; + this._howls = []; + this._muted = false; + this._volume = 1; + this._canPlayEvent = 'canplaythrough'; + this._navigator = typeof window !== 'undefined' && window.navigator ? window.navigator : null; + this.masterGain = null; + this.noAudio = false; + this.usingWebAudio = true; + this.autoSuspend = true; + this.ctx = null; + this.autoUnlock = true; + + // Setup Howler (codecs, audio context, etc.) + this._setup(); // Register the Howler instance with the plugin manager - // This allows plugins registered after initialization to access it via onRegister - globalPluginManager.setHowlerInstance(self); - - return self; + // This triggers onHowlerInit hooks for any plugins already registered + globalPluginManager.setHowlerInstance(this); } volume(vol?: number): number | HowlerGlobal { - const self = this; if (vol !== undefined) { vol = parseFloat(vol as any); - if (!self.ctx) { + if (!this.ctx) { setupAudioContext(); } if (typeof vol === 'number' && vol >= 0 && vol <= 1) { - self._volume = vol; + this._volume = vol; - if (self._muted) { - return self; + if (this._muted) { + return this; } - if (self.usingWebAudio) { - self.masterGain!.gain.setValueAtTime(vol, Howler.ctx!.currentTime); + if (this.usingWebAudio) { + this.masterGain!.gain.setValueAtTime(vol, Howler.ctx!.currentTime); } - for (let i = 0; i < self._howls.length; i++) { - if (!self._howls[i]._webAudio) { - const ids = self._howls[i]._getSoundIds(); + for (let i = 0; i < this._howls.length; i++) { + if (!this._howls[i]._webAudio) { + const ids = this._howls[i]._getSoundIds(); for (let j = 0; j < ids.length; j++) { - const sound = self._howls[i]._soundById(ids[j]); + const sound = this._howls[i]._soundById(ids[j]); if (sound && sound._node) { sound._node.volume = sound._volume * vol; } @@ -104,31 +96,29 @@ export class HowlerGlobal { } } - return self; + return this; } } - return self._volume; + return this._volume; } mute(muted: boolean): HowlerGlobal { - const self = this; - - if (!self.ctx) { + if (!this.ctx) { setupAudioContext(); } - self._muted = muted; + this._muted = muted; - if (self.usingWebAudio) { - self.masterGain!.gain.setValueAtTime(muted ? 0 : self._volume, Howler.ctx!.currentTime); + if (this.usingWebAudio) { + this.masterGain!.gain.setValueAtTime(muted ? 0 : this._volume, Howler.ctx!.currentTime); } - for (let i = 0; i < self._howls.length; i++) { - if (!self._howls[i]._webAudio) { - const ids = self._howls[i]._getSoundIds(); + for (let i = 0; i < this._howls.length; i++) { + if (!this._howls[i]._webAudio) { + const ids = this._howls[i]._getSoundIds(); for (let j = 0; j < ids.length; j++) { - const sound = self._howls[i]._soundById(ids[j]); + const sound = this._howls[i]._soundById(ids[j]); if (sound && sound._node) { sound._node.muted = muted ? true : sound._muted; } @@ -136,33 +126,29 @@ export class HowlerGlobal { } } - return self; + return this; } stop(): HowlerGlobal { - const self = this; - - for (let i = 0; i < self._howls.length; i++) { - self._howls[i].stop(); + for (let i = 0; i < this._howls.length; i++) { + this._howls[i].stop(); } - return self; + return this; } unload(): HowlerGlobal { - const self = this; - - for (let i = self._howls.length - 1; i >= 0; i--) { - self._howls[i].unload(); + for (let i = this._howls.length - 1; i >= 0; i--) { + this._howls[i].unload(); } - if (self.usingWebAudio && self.ctx && typeof self.ctx.close !== 'undefined') { - self.ctx.close(); - self.ctx = null; + if (this.usingWebAudio && this.ctx && typeof this.ctx.close !== 'undefined') { + this.ctx.close(); + this.ctx = null; setupAudioContext(); } - return self; + return this; } codecs(ext: string): boolean { @@ -201,59 +187,56 @@ export class HowlerGlobal { } _setup(): HowlerGlobal { - const self = this; + this.state = this.ctx ? this.ctx.state || 'suspended' : 'suspended'; + this._autoSuspend(); - self.state = self.ctx ? self.ctx.state || 'suspended' : 'suspended'; - self._autoSuspend(); - - if (!self.usingWebAudio) { + if (!this.usingWebAudio) { if (typeof window.Audio !== 'undefined') { try { const test = new window.Audio(); if (typeof test.oncanplaythrough === 'undefined') { - self._canPlayEvent = 'canplay'; + this._canPlayEvent = 'canplay'; } } catch (e) { - self.noAudio = true; + this.noAudio = true; } } else { - self.noAudio = true; + this.noAudio = true; } } try { const test = new window.Audio(); if (test.muted) { - self.noAudio = true; + this.noAudio = true; } } catch (e) {} - if (!self.noAudio) { - self._setupCodecs(); + if (!this.noAudio) { + this._setupCodecs(); } - return self; + return this; } _setupCodecs(): HowlerGlobal { - const self = this; let audioTest: any = null; try { audioTest = typeof window.Audio !== 'undefined' ? new window.Audio() : null; } catch (err) { - return self; + return this; } if (!audioTest || typeof audioTest.canPlayType !== 'function') { - return self; + return this; } const mpegTest = audioTest.canPlayType('audio/mpeg;').replace(/^no$/, ''); - const oldOpera = isOldOpera(self._navigator); - const oldSafari = isOldSafari(self._navigator); + const oldOpera = isOldOpera(this._navigator); + const oldSafari = isOldSafari(this._navigator); - self._codecs = { + this._codecs = { mp3: !!(!oldOpera && (mpegTest || audioTest.canPlayType('audio/mp3;').replace(/^no$/, ''))), mpeg: !!mpegTest, opus: !!audioTest.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/, ''), @@ -271,43 +254,41 @@ export class HowlerGlobal { flac: !!(audioTest.canPlayType('audio/x-flac;') || audioTest.canPlayType('audio/flac;')).replace(/^no$/, '') }; - return self; + return this; } _unlockAudio(): void { - const self = this; - - if (self._audioUnlocked || !self.ctx) { + if (this._audioUnlocked || !this.ctx) { return; } - self._audioUnlocked = false; - self.autoUnlock = false; + this._audioUnlocked = false; + this.autoUnlock = false; - if (!self._mobileUnloaded && self.ctx.sampleRate !== 44100) { - self._mobileUnloaded = true; - self.unload(); + if (!this._mobileUnloaded && this.ctx.sampleRate !== 44100) { + this._mobileUnloaded = true; + this.unload(); } - self._scratchBuffer = self.ctx.createBuffer(1, 1, 22050); + this._scratchBuffer = this.ctx.createBuffer(1, 1, 22050); const unlock = () => { - while (self._html5AudioPool.length < self.html5PoolSize) { + while (this._html5AudioPool.length < this.html5PoolSize) { try { const audioNode = new (window as any).Audio(); audioNode._unlocked = true; - self._releaseHtml5Audio(audioNode); + this._releaseHtml5Audio(audioNode); } catch (e) { - self.noAudio = true; + this.noAudio = true; break; } } - for (let i = 0; i < self._howls.length; i++) { - if (!self._howls[i]._webAudio) { - const ids = self._howls[i]._getSoundIds(); + for (let i = 0; i < this._howls.length; i++) { + if (!this._howls[i]._webAudio) { + const ids = this._howls[i]._getSoundIds(); for (let j = 0; j < ids.length; j++) { - const sound = self._howls[i]._soundById(ids[j]); + const sound = this._howls[i]._soundById(ids[j]); if (sound && sound._node && !(sound._node as any)._unlocked) { (sound._node as any)._unlocked = true; sound._node.load(); @@ -316,11 +297,11 @@ export class HowlerGlobal { } } - self._autoResume(); + this._autoResume(); - const source = self.ctx!.createBufferSource(); - source.buffer = self._scratchBuffer; - source.connect(self.ctx!.destination); + const source = this.ctx!.createBufferSource(); + source.buffer = this._scratchBuffer; + source.connect(this.ctx!.destination); if (typeof source.start === 'undefined') { (source as any).noteOn(0); @@ -328,21 +309,21 @@ export class HowlerGlobal { source.start(0); } - if (typeof self.ctx!.resume === 'function') { - self.ctx!.resume(); + if (typeof this.ctx!.resume === 'function') { + this.ctx!.resume(); } source.onended = () => { source.disconnect(0); - self._audioUnlocked = true; + this._audioUnlocked = true; document.removeEventListener('touchstart', unlock, true); document.removeEventListener('touchend', unlock, true); document.removeEventListener('click', unlock, true); document.removeEventListener('keydown', unlock, true); - for (let i = 0; i < self._howls.length; i++) { - self._howls[i]._emit('unlock'); + for (let i = 0; i < this._howls.length; i++) { + this._howls[i]._emit('unlock'); } }; }; @@ -354,10 +335,8 @@ export class HowlerGlobal { } _obtainHtml5Audio(): HTMLAudioElement { - const self = this; - - if (self._html5AudioPool.length) { - return self._html5AudioPool.pop()!; + if (this._html5AudioPool.length) { + return this._html5AudioPool.pop()!; } const testPlay = new (window as any).Audio().play(); @@ -371,82 +350,76 @@ export class HowlerGlobal { } _releaseHtml5Audio(audio: any): HowlerGlobal { - const self = this; - if (audio._unlocked) { - self._html5AudioPool.push(audio); + this._html5AudioPool.push(audio); } - return self; + return this; } _autoSuspend(): void { - const self = this; - - if (!self.autoSuspend || !self.ctx || typeof self.ctx.suspend === 'undefined' || !Howler.usingWebAudio) { + if (!this.autoSuspend || !this.ctx || typeof this.ctx.suspend === 'undefined' || !Howler.usingWebAudio) { return; } - for (let i = 0; i < self._howls.length; i++) { - if (self._howls[i]._webAudio) { - for (let j = 0; j < self._howls[i]._sounds.length; j++) { - if (!self._howls[i]._sounds[j]._paused) { + for (let i = 0; i < this._howls.length; i++) { + if (this._howls[i]._webAudio) { + for (let j = 0; j < this._howls[i]._sounds.length; j++) { + if (!this._howls[i]._sounds[j]._paused) { return; } } } } - if (self._suspendTimer) { - clearTimeout(self._suspendTimer); + if (this._suspendTimer) { + clearTimeout(this._suspendTimer); } - self._suspendTimer = setTimeout(() => { - if (!self.autoSuspend) { + this._suspendTimer = setTimeout(() => { + if (!this.autoSuspend) { return; } - self._suspendTimer = null; - self.state = 'suspending'; + this._suspendTimer = null; + this.state = 'suspending'; const handleSuspension = () => { - self.state = 'suspended'; + this.state = 'suspended'; - if (self._resumeAfterSuspend) { - delete self._resumeAfterSuspend; - self._autoResume(); + if (this._resumeAfterSuspend) { + delete this._resumeAfterSuspend; + this._autoResume(); } }; - self.ctx!.suspend().then(handleSuspension, handleSuspension); + this.ctx!.suspend().then(handleSuspension, handleSuspension); }, 30000); } _autoResume(): void { - const self = this; - - if (!self.ctx || typeof self.ctx.resume === 'undefined' || !Howler.usingWebAudio) { + if (!this.ctx || typeof this.ctx.resume === 'undefined' || !Howler.usingWebAudio) { return; } - if (self.state === 'running' && self.ctx.state !== 'interrupted' && self._suspendTimer) { - clearTimeout(self._suspendTimer); - self._suspendTimer = null; - } else if (self.state === 'suspended' || (self.state === 'running' && self.ctx.state === 'interrupted')) { - self.ctx.resume().then(() => { - self.state = 'running'; + 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 (let i = 0; i < self._howls.length; i++) { - self._howls[i]._emit('resume'); + for (let i = 0; i < this._howls.length; i++) { + this._howls[i]._emit('resume'); } }); - if (self._suspendTimer) { - clearTimeout(self._suspendTimer); - self._suspendTimer = null; + if (this._suspendTimer) { + clearTimeout(this._suspendTimer); + this._suspendTimer = null; } - } else if (self.state === 'suspending') { - self._resumeAfterSuspend = true; + } else if (this.state === 'suspending') { + this._resumeAfterSuspend = true; } } } @@ -483,95 +456,95 @@ class Sound { } init(): Sound { - const self = this; - const parent = self._parent; + + const parent = this._parent; - 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'; + this._muted = parent._muted; + this._loop = parent._loop; + this._volume = parent._volume; + this._rate = parent._rate; + this._seek = 0; + this._paused = true; + this._ended = true; + this._sprite = '__default'; - self._id = ++Howler._counter; + this._id = ++Howler._counter; - parent._sounds.push(self); + parent._sounds.push(this); - self.create(); + this.create(); // Execute plugin hooks - globalPluginManager.executeSoundCreate(self, parent); + globalPluginManager.executeSoundCreate(this, parent); - return self; + return this; } create(): Sound { - const self = this; - const parent = self._parent; - const volume = Howler._muted || self._muted || parent._muted ? 0 : self._volume; + + const parent = this._parent; + const volume = Howler._muted || this._muted || parent._muted ? 0 : this._volume; if (parent._webAudio) { - 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); + this._node = typeof Howler.ctx!.createGain === 'undefined' ? Howler.ctx!.createGainNode() : Howler.ctx!.createGain(); + this._node.gain.setValueAtTime(volume, Howler.ctx!.currentTime); + this._node.paused = true; + this._node.connect(Howler.masterGain); } else if (!Howler.noAudio) { - self._node = Howler._obtainHtml5Audio(); + this._node = Howler._obtainHtml5Audio(); - self._errorFn = self._errorListener.bind(self); - self._node.addEventListener('error', self._errorFn, false); + this._errorFn = this._errorListener.bind(this); + this._node.addEventListener('error', this._errorFn, false); - self._loadFn = self._loadListener.bind(self); - self._node.addEventListener(Howler._canPlayEvent, self._loadFn, false); + this._loadFn = this._loadListener.bind(this); + this._node.addEventListener(Howler._canPlayEvent, this._loadFn, false); - self._endFn = self._endListener.bind(self); - self._node.addEventListener('ended', self._endFn, false); + this._endFn = this._endListener.bind(this); + this._node.addEventListener('ended', this._endFn, false); - self._node.src = parent._src; - self._node.preload = parent._preload === true ? 'auto' : parent._preload; + this._node.src = parent._src; + this._node.preload = parent._preload === true ? 'auto' : parent._preload; const volumeOrHowler = Howler.volume(); if (typeof volumeOrHowler === 'number') { - self._node.volume = volume * volumeOrHowler; + this._node.volume = volume * volumeOrHowler; } - self._node.load(); + this._node.load(); } - return self; + return this; } reset(): Sound { - const self = this; - const parent = self._parent; - - 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'; - - self._id = ++Howler._counter; - - return self; + + const 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._counter; + + return this; } _errorListener(): void { - const self = this; - self._parent._emit('loaderror', self._id, self._node.error ? self._node.error.code : 0); - self._node.removeEventListener('error', self._errorFn, false); + + this._parent._emit('loaderror', this._id, this._node.error ? this._node.error.code : 0); + this._node.removeEventListener('error', this._errorFn, false); } _loadListener(): void { - const self = this; - const parent = self._parent; + + const parent = this._parent; - parent._duration = Math.ceil(self._node.duration * 10) / 10; + parent._duration = Math.ceil(this._node.duration * 10) / 10; if (Object.keys(parent._sprite).length === 0) { parent._sprite = { __default: [0, parent._duration * 1000] }; @@ -586,24 +559,24 @@ class Sound { globalPluginManager.executeHowlLoad(parent); } - self._node.removeEventListener(Howler._canPlayEvent, self._loadFn, false); + this._node.removeEventListener(Howler._canPlayEvent, this._loadFn, false); } _endListener(): void { - const self = this; - const parent = self._parent; + + const parent = this._parent; if (parent._duration === Infinity) { - parent._duration = Math.ceil(self._node.duration * 10) / 10; + parent._duration = Math.ceil(this._node.duration * 10) / 10; if (parent._sprite.__default[1] === Infinity) { parent._sprite.__default[1] = parent._duration * 1000; } - parent._ended(self); + parent._ended(this); } - self._node.removeEventListener('ended', self._endFn, false); + this._node.removeEventListener('ended', this._endFn, false); } } @@ -652,100 +625,97 @@ class Howl { } init(o: HowlOptions): Howl { - const self = this; - if (!Howler.ctx) { setupAudioContext(); } - self._autoplay = o.autoplay || false; - self._format = typeof o.format !== 'string' ? o.format || [] : [o.format]; - self._html5 = o.html5 || false; - self._muted = o.mute || false; - self._loop = o.loop || false; - self._pool = o.pool || 5; - self._preload = typeof o.preload === 'boolean' || o.preload === 'metadata' ? o.preload : true; - self._rate = o.rate || 1; - self._sprite = o.sprite || {}; - self._src = typeof o.src !== 'string' ? o.src : [o.src]; - self._volume = o.volume !== undefined ? o.volume : 1; - self._xhr = { + this._autoplay = o.autoplay || false; + this._format = 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 }; - self._duration = 0; - self._state = 'unloaded'; - self._sounds = []; - self._endTimers = {}; - self._queue = []; - self._playLock = false; - - self._onend = o.onend ? [{ fn: o.onend }] : []; - self._onfade = o.onfade ? [{ fn: o.onfade }] : []; - self._onload = o.onload ? [{ fn: o.onload }] : []; - self._onloaderror = o.onloaderror ? [{ fn: o.onloaderror }] : []; - self._onplayerror = o.onplayerror ? [{ fn: o.onplayerror }] : []; - self._onpause = o.onpause ? [{ fn: o.onpause }] : []; - self._onplay = o.onplay ? [{ fn: o.onplay }] : []; - self._onstop = o.onstop ? [{ fn: o.onstop }] : []; - self._onmute = o.onmute ? [{ fn: o.onmute }] : []; - self._onvolume = o.onvolume ? [{ fn: o.onvolume }] : []; - self._onrate = o.onrate ? [{ fn: o.onrate }] : []; - self._onseek = o.onseek ? [{ fn: o.onseek }] : []; - self._onunlock = o.onunlock ? [{ fn: o.onunlock }] : []; - self._onresume = []; - - self._webAudio = Howler.usingWebAudio && !self._html5; + this._duration = 0; + this._state = 'unloaded'; + this._sounds = []; + this._endTimers = {}; + this._queue = []; + this._playLock = 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.usingWebAudio && !this._html5; if (typeof Howler.ctx !== 'undefined' && Howler.ctx && Howler.autoUnlock) { Howler._unlockAudio(); } - Howler._howls.push(self); + Howler._howls.push(this); // Execute plugin hooks - globalPluginManager.executeHowlCreate(self, o); + globalPluginManager.executeHowlCreate(this, o); - if (self._autoplay) { - self._queue.push({ + if (this._autoplay) { + this._queue.push({ event: 'play', action: () => { - self.play(); + this.play(); } }); } - if (self._preload && self._preload !== 'none') { - self.load(); + if (this._preload && this._preload !== 'none') { + this.load(); } - return self; + return this; } load(): Howl { - const self = this; let url: string | null = null; if (Howler.noAudio) { - self._emit('loaderror', null, 'No audio support.'); - return self; + this._emit('loaderror', null, 'No audio support.'); + return this; } - if (typeof self._src === 'string') { - self._src = [self._src]; + if (typeof this._src === 'string') { + this._src = [this._src]; } - for (let i = 0; i < (self._src as string[]).length; i++) { + for (let i = 0; i < (this._src as string[]).length; i++) { let ext: string | null; - const str = (self._src as string[])[i]; + const str = (this._src as string[])[i]; - if (self._format && self._format[i]) { - ext = self._format[i]; + if (this._format && this._format[i]) { + ext = this._format[i]; } else { if (typeof str !== 'string') { - self._emit('loaderror', null, 'Non-string found in selected audio sources - ignoring.'); + this._emit('loaderror', null, 'Non-string found in selected audio sources - ignoring.'); continue; } @@ -762,51 +732,51 @@ class Howl { } if (ext && Howler.codecs(ext)) { - url = (self._src as string[])[i]; + url = (this._src as string[])[i]; break; } } if (!url) { - self._emit('loaderror', null, 'No codec support for selected audio sources.'); - return self; + this._emit('loaderror', null, 'No codec support for selected audio sources.'); + return this; } - self._src = url; - self._state = 'loading'; + this._src = url; + this._state = 'loading'; if (typeof window !== 'undefined' && window.location.protocol === 'https:' && url.slice(0, 5) === 'http:') { - self._html5 = true; - self._webAudio = false; + this._html5 = true; + this._webAudio = false; } - new Sound(self); + new Sound(this); - if (self._webAudio) { - loadBuffer(self); + if (this._webAudio) { + loadBuffer(this); } - return self; + return this; } play(sprite?: string | number, internal?: boolean): number | null { - const self = this; + let id: number | null = null; if (typeof sprite === 'number') { id = sprite; sprite = undefined; - } else if (typeof sprite === 'string' && self._state === 'loaded' && !self._sprite[sprite]) { + } else if (typeof sprite === 'string' && this._state === 'loaded' && !this._sprite[sprite]) { return null; } else if (typeof sprite === 'undefined') { sprite = '__default'; - if (!self._playLock) { + if (!this._playLock) { let num = 0; - for (let i = 0; i < self._sounds.length; i++) { - if (self._sounds[i]._paused && !self._sounds[i]._ended) { + for (let i = 0; i < this._sounds.length; i++) { + if (this._sounds[i]._paused && !this._sounds[i]._ended) { num++; - id = self._sounds[i]._id; + id = this._sounds[i]._id; } } @@ -818,7 +788,7 @@ class Howl { } } - const sound = id ? self._soundById(id) : self._inactiveSound(); + const sound = id ? this._soundById(id) : this._inactiveSound(); if (!sound) { return null; @@ -828,15 +798,15 @@ class Howl { sprite = sound._sprite || '__default'; } - if (self._state !== 'loaded') { + if (this._state !== 'loaded') { sound._sprite = sprite; sound._ended = false; const soundId = sound._id; - self._queue.push({ + this._queue.push({ event: 'play', action: () => { - self.play(soundId); + this.play(soundId); } }); @@ -845,21 +815,21 @@ class Howl { if (id && !sound._paused) { if (!internal) { - self._loadQueue('play'); + this._loadQueue('play'); } return sound._id; } - if (self._webAudio) { + if (this._webAudio) { Howler._autoResume(); } - const seek = Math.max(0, sound._seek > 0 ? sound._seek : self._sprite[sprite!][0] / 1000); - const duration = Math.max(0, (self._sprite[sprite!][0] + self._sprite[sprite!][1]) / 1000 - seek); + const seek = Math.max(0, sound._seek > 0 ? sound._seek : this._sprite[sprite!][0] / 1000); + const duration = Math.max(0, (this._sprite[sprite!][0] + this._sprite[sprite!][1]) / 1000 - seek); const timeout = (duration * 1000) / Math.abs(sound._rate); - const start = self._sprite[sprite!][0] / 1000; - const stop = (self._sprite[sprite!][0] + self._sprite[sprite!][1]) / 1000; + const start = this._sprite[sprite!][0] / 1000; + const stop = (this._sprite[sprite!][0] + this._sprite[sprite!][1]) / 1000; sound._sprite = sprite!; sound._ended = false; @@ -869,23 +839,23 @@ class Howl { sound._seek = seek; sound._start = start; sound._stop = stop; - sound._loop = !!(sound._loop || self._sprite[sprite!][2]); + sound._loop = !!(sound._loop || this._sprite[sprite!][2]); }; if (seek >= stop) { - self._ended(sound); + this._ended(sound); return sound._id; } const node = sound._node; - if (self._webAudio) { + if (this._webAudio) { const playWebAudio = () => { - self._playLock = false; + this._playLock = false; setParams(); - self._refreshBuffer(sound); + this._refreshBuffer(sound); - const vol = sound._muted || self._muted ? 0 : sound._volume; + const vol = sound._muted || this._muted ? 0 : sound._volume; node.gain.setValueAtTime(vol, Howler.ctx!.currentTime); sound._playStart = Howler.ctx!.currentTime; @@ -896,13 +866,13 @@ class Howl { } if (timeout !== Infinity) { - self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout); + this._endTimers[sound._id] = setTimeout(this._ended.bind(this, sound), timeout); } if (!internal) { setTimeout(() => { - self._emit('play', sound._id); - self._loadQueue(); + this._emit('play', sound._id); + this._loadQueue(); }, 0); } }; @@ -910,14 +880,14 @@ class Howl { if (Howler.state === 'running' && Howler.ctx!.state !== 'interrupted') { playWebAudio(); } else { - self._playLock = true; - self.once('resume', playWebAudio); - self._clearTimer(sound._id); + this._playLock = true; + this.once('resume', playWebAudio); + this._clearTimer(sound._id); } } else { const playHtml5 = () => { node.currentTime = seek; - node.muted = sound._muted || self._muted || Howler._muted || node.muted; + node.muted = sound._muted || this._muted || Howler._muted || node.muted; const volume = Howler.volume(); node.volume = sound._volume * (typeof volume === 'number' ? volume : 1); node.playbackRate = sound._rate; @@ -926,55 +896,55 @@ class Howl { const play = node.play(); if (play && typeof Promise !== 'undefined' && (play instanceof Promise || typeof (play as any).then === 'function')) { - self._playLock = true; + this._playLock = true; setParams(); (play as any) .then(() => { - self._playLock = false; + this._playLock = false; (node as any)._unlocked = true; if (!internal) { - self._emit('play', sound._id); + this._emit('play', sound._id); } else { - self._loadQueue(); + this._loadQueue(); } }) .catch(() => { - 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.'); + 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) { - self._playLock = false; + this._playLock = false; setParams(); - self._emit('play', sound._id); + this._emit('play', sound._id); } node.playbackRate = sound._rate; 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.'); + 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) { - self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout); + this._endTimers[sound._id] = setTimeout(this._ended.bind(this, sound), timeout); } else { - self._endTimers[sound._id] = () => { - self._ended(sound); - node.removeEventListener('ended', self._endTimers[sound._id], false); + this._endTimers[sound._id] = () => { + this._ended(sound); + node.removeEventListener('ended', this._endTimers[sound._id], false); }; - node.addEventListener('ended', self._endTimers[sound._id], false); + node.addEventListener('ended', this._endTimers[sound._id], false); } } catch (err: unknown) { - self._emit('playerror', sound._id, err instanceof Error ? err.message : String(err)); + this._emit('playerror', sound._id, err instanceof Error ? err.message : String(err)); } }; if (node.src === 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA') { - node.src = self._src; + node.src = this._src; node.load(); } @@ -982,17 +952,17 @@ class Howl { if (node.readyState >= 3 || loadedNoReadyState) { playHtml5(); } else { - self._playLock = true; - self._state = 'loading'; + this._playLock = true; + this._state = 'loading'; const listener = () => { - self._state = 'loaded'; + this._state = 'loaded'; playHtml5(); node.removeEventListener(Howler._canPlayEvent, listener, false); }; node.addEventListener(Howler._canPlayEvent, listener as any, false); - self._clearTimer(sound._id); + this._clearTimer(sound._id); } } @@ -1000,35 +970,35 @@ class Howl { } pause(id?: number): Howl { - const self = this; + - if (self._state !== 'loaded' || self._playLock) { - self._queue.push({ + if (this._state !== 'loaded' || this._playLock) { + this._queue.push({ event: 'pause', action: () => { - self.pause(id); + this.pause(id); } }); - return self; + return this; } - const ids = self._getSoundIds(id); + const ids = this._getSoundIds(id); for (let i = 0; i < ids.length; i++) { - self._clearTimer(ids[i]); + this._clearTimer(ids[i]); - const sound = self._soundById(ids[i]); + const sound = this._soundById(ids[i]); if (sound && !sound._paused) { - sound._seek = self.seek(ids[i]) as number; + sound._seek = this.seek(ids[i]) as number; sound._rateSeek = 0; sound._paused = true; - self._stopFade(ids[i]); + this._stopFade(ids[i]); if (sound._node) { - if (self._webAudio) { + if (this._webAudio) { if (!sound._node.bufferSource) { continue; } @@ -1039,7 +1009,7 @@ class Howl { (sound._node.bufferSource as any).stop(0); } - self._cleanBuffer(sound._node); + this._cleanBuffer(sound._node); } else if (!isNaN(sound._node.duration) || sound._node.duration === Infinity) { sound._node.pause(); } @@ -1047,33 +1017,33 @@ class Howl { } if (!arguments[1]) { - self._emit('pause', sound ? sound._id : null); + this._emit('pause', sound ? sound._id : null); } } - return self; + return this; } stop(id?: number, internal?: boolean): Howl { - const self = this; + - if (self._state !== 'loaded' || self._playLock) { - self._queue.push({ + if (this._state !== 'loaded' || this._playLock) { + this._queue.push({ event: 'stop', action: () => { - self.stop(id); + this.stop(id); } }); - return self; + return this; } - const ids = self._getSoundIds(id); + const ids = this._getSoundIds(id); for (let i = 0; i < ids.length; i++) { - self._clearTimer(ids[i]); + this._clearTimer(ids[i]); - const sound = self._soundById(ids[i]); + const sound = this._soundById(ids[i]); if (sound) { sound._seek = sound._start || 0; @@ -1081,10 +1051,10 @@ class Howl { sound._paused = true; sound._ended = true; - self._stopFade(ids[i]); + this._stopFade(ids[i]); if (sound._node) { - if (self._webAudio) { + if (this._webAudio) { if (sound._node.bufferSource) { if (typeof (sound._node.bufferSource as any).stop === 'undefined') { (sound._node.bufferSource as any).noteOff(0); @@ -1092,86 +1062,86 @@ class Howl { (sound._node.bufferSource as any).stop(0); } - self._cleanBuffer(sound._node); + 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) { - self._clearSound(sound._node); + this._clearSound(sound._node); } } } if (!internal) { - self._emit('stop', sound._id); + this._emit('stop', sound._id); } } } - return self; + return this; } mute(muted: boolean, id?: number): boolean | Howl { - const self = this; + - if (self._state !== 'loaded' || self._playLock) { - self._queue.push({ + if (this._state !== 'loaded' || this._playLock) { + this._queue.push({ event: 'mute', action: () => { - self.mute(muted, id); + this.mute(muted, id); } }); - return self; + return this; } if (typeof id === 'undefined') { if (typeof muted === 'boolean') { - self._muted = muted; + this._muted = muted; } else { - return self._muted; + return this._muted; } } - const ids = self._getSoundIds(id); + const ids = this._getSoundIds(id); for (let i = 0; i < ids.length; i++) { - const sound = self._soundById(ids[i]); + const sound = this._soundById(ids[i]); if (sound) { sound._muted = muted; if (sound._interval) { - self._stopFade(sound._id); + this._stopFade(sound._id); } - if (self._webAudio && sound._node) { + if (this._webAudio && sound._node) { sound._node.gain.setValueAtTime(muted ? 0 : sound._volume, Howler.ctx!.currentTime); } else if (sound._node) { sound._node.muted = Howler._muted ? true : muted; } - self._emit('mute', sound._id); + this._emit('mute', sound._id); } } - return self; + return this; } volume(): number; volume(vol: number): Howl; volume(vol?: number): number | Howl { - const self = this; + const args = arguments; let volume: number | undefined; let id: number | undefined; if (args.length === 0) { - return self._volume; + return this._volume; } else if (args.length === 1 || (args.length === 2 && typeof args[1] === 'undefined')) { - const ids = self._getSoundIds(); + const ids = this._getSoundIds(); const index = ids.indexOf(args[0] as any); if (index >= 0) { id = parseInt(args[0] as any, 10); @@ -1185,33 +1155,33 @@ class Howl { let sound; if (typeof volume !== 'undefined' && volume >= 0 && volume <= 1) { - if (self._state !== 'loaded' || self._playLock) { - self._queue.push({ + if (this._state !== 'loaded' || this._playLock) { + this._queue.push({ event: 'volume', action: () => { - self.volume.apply(self, args as any); + this.volume.apply(this, args as any); } }); - return self; + return this; } if (typeof id === 'undefined') { - self._volume = volume; + this._volume = volume; } - id = self._getSoundIds(id); + id = this._getSoundIds(id); for (let i = 0; i < (id as any).length; i++) { - sound = self._soundById((id as any)[i]); + sound = this._soundById((id as any)[i]); if (sound) { sound._volume = volume; if (!(args as any)[2]) { - self._stopFade((id as any)[i]); + this._stopFade((id as any)[i]); } - if (self._webAudio && sound._node && !sound._muted) { + if (this._webAudio && sound._node && !sound._muted) { sound._node.gain.setValueAtTime(volume, Howler.ctx!.currentTime); } else if (sound._node && !sound._muted) { const volumeMultiplierOrGlobal = Howler.volume(); @@ -1220,47 +1190,47 @@ class Howl { } } - self._emit('volume', sound._id); + this._emit('volume', sound._id); } } } else { - sound = id ? self._soundById(id) : self._sounds[0]; + sound = id ? this._soundById(id) : this._sounds[0]; return sound ? sound._volume : 0; } - return self; + return this; } fade(from: number, to: number, len: number, id?: number): Howl { - const self = this; + - if (self._state !== 'loaded' || self._playLock) { - self._queue.push({ + if (this._state !== 'loaded' || this._playLock) { + this._queue.push({ event: 'fade', action: () => { - self.fade(from, to, len, id); + this.fade(from, to, len, id); } }); - return self; + return this; } from = Math.min(Math.max(0, parseFloat(from as any)), 1); to = Math.min(Math.max(0, parseFloat(to as any)), 1); len = parseFloat(len as any); - self.volume(from, id); + this.volume(from, id); - const ids = self._getSoundIds(id); + const ids = this._getSoundIds(id); for (let i = 0; i < ids.length; i++) { - const sound = self._soundById(ids[i]); + const sound = this._soundById(ids[i]); if (sound) { if (!id) { - self._stopFade(ids[i]); + this._stopFade(ids[i]); } - if (self._webAudio && !sound._muted) { + if (this._webAudio && !sound._muted) { const currentTime = Howler.ctx!.currentTime; const end = currentTime + len / 1000; sound._volume = from; @@ -1268,15 +1238,15 @@ class Howl { sound._node.gain.linearRampToValueAtTime(to, end); } - self._startFadeInterval(sound, from, to, len, ids[i], typeof id === 'undefined'); + this._startFadeInterval(sound, from, to, len, ids[i], typeof id === 'undefined'); } } - return self; + return this; } _startFadeInterval(sound: Sound, from: number, to: number, len: number, id: number, isGroup: boolean): void { - const self = this; + let vol = from; const diff = to - from; const steps = Math.abs(diff / 0.01); @@ -1298,62 +1268,62 @@ class Howl { vol = Math.min(to, vol); } - if (self._webAudio) { + if (this._webAudio) { sound._volume = vol; } else { - self.volume(vol, sound._id, true); + this.volume(vol, sound._id, true); } if (isGroup) { - self._volume = vol; + this._volume = vol; } if ((to < from && vol <= to) || (to > from && vol >= to)) { clearInterval(sound._interval as any); sound._interval = undefined; sound._fadeTo = undefined; - self.volume(to, sound._id); - self._emit('fade', sound._id); + this.volume(to, sound._id); + this._emit('fade', sound._id); } }, stepLen); } _stopFade(id: number): Howl { - const self = this; - const sound = self._soundById(id); + + const sound = this._soundById(id); if (sound && sound._interval) { - if (self._webAudio) { + if (this._webAudio) { sound._node.gain.cancelScheduledValues(Howler.ctx!.currentTime); } clearInterval(sound._interval as any); sound._interval = undefined; - self.volume(sound._fadeTo as number, id); + this.volume(sound._fadeTo as number, id); sound._fadeTo = undefined; - self._emit('fade', id); + this._emit('fade', id); } - return self; + return this; } loop(): boolean; loop(loop: boolean): Howl; loop(loop?: boolean): boolean | Howl { - const self = this; + const args = arguments; let loopVal: boolean | undefined; let id: number | undefined; let sound: Sound | null = null; if (args.length === 0) { - return self._loop; + return this._loop; } else if (args.length === 1) { if (typeof args[0] === 'boolean') { loopVal = args[0] as boolean; - self._loop = loopVal; + this._loop = loopVal; } else { - sound = self._soundById(parseInt(args[0] as any, 10)); + sound = this._soundById(parseInt(args[0] as any, 10)); return sound ? sound._loop : false; } } else if (args.length === 2) { @@ -1361,42 +1331,42 @@ class Howl { id = parseInt(args[1] as any, 10); } - const ids = self._getSoundIds(id); + const ids = this._getSoundIds(id); for (let i = 0; i < ids.length; i++) { - sound = self._soundById(ids[i]); + sound = this._soundById(ids[i]); if (sound) { sound._loop = loopVal as boolean; - if (self._webAudio && sound._node && (sound._node.bufferSource as any)) { + if (this._webAudio && sound._node && (sound._node.bufferSource as any)) { (sound._node.bufferSource as any).loop = loopVal; if (loopVal) { (sound._node.bufferSource as any).loopStart = sound._start || 0; (sound._node.bufferSource as any).loopEnd = sound._stop; - if (self.playing(ids[i])) { - self.pause(ids[i], true); - self.play(ids[i], true); + if (this.playing(ids[i])) { + this.pause(ids[i], true); + this.play(ids[i], true); } } } } } - return self; + return this; } rate(): number; rate(rate: number): Howl; rate(rate?: number): number | Howl { - const self = this; + const args = arguments; let rateVal: number | undefined; let id: number | undefined; if (args.length === 0) { - id = self._sounds[0]._id; + id = this._sounds[0]._id; } else if (args.length === 1) { - const ids = self._getSoundIds(); + const ids = this._getSoundIds(); const index = ids.indexOf(args[0] as any); if (index >= 0) { id = parseInt(args[0] as any, 10); @@ -1410,77 +1380,77 @@ class Howl { let sound; if (typeof rateVal === 'number') { - if (self._state !== 'loaded' || self._playLock) { - self._queue.push({ + if (this._state !== 'loaded' || this._playLock) { + this._queue.push({ event: 'rate', action: () => { - self.rate.apply(self, args as any); + this.rate.apply(this, args as any); } }); - return self; + return this; } if (typeof id === 'undefined') { - self._rate = rateVal; + this._rate = rateVal; } - id = self._getSoundIds(id); + id = this._getSoundIds(id); for (let i = 0; i < (id as any).length; i++) { - sound = self._soundById((id as any)[i]); + sound = this._soundById((id as any)[i]); if (sound) { - if (self.playing((id as any)[i])) { - sound._rateSeek = self.seek((id as any)[i]) as number; - sound._playStart = self._webAudio ? Howler.ctx!.currentTime : sound._playStart; + if (this.playing((id as any)[i])) { + sound._rateSeek = this.seek((id as any)[i]) as number; + sound._playStart = this._webAudio ? Howler.ctx!.currentTime : sound._playStart; } sound._rate = rateVal; - if (self._webAudio && sound._node && (sound._node.bufferSource as any)) { + if (this._webAudio && sound._node && (sound._node.bufferSource as any)) { (sound._node.bufferSource as any).playbackRate.setValueAtTime(rateVal, Howler.ctx!.currentTime); } else if (sound._node) { sound._node.playbackRate = rateVal; } - const seek = self.seek((id as any)[i]) as number; - const duration = (self._sprite[sound._sprite][0] + self._sprite[sound._sprite][1]) / 1000 - seek; + const seek = this.seek((id as any)[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); - if (self._endTimers[(id as any)[i]] || !sound._paused) { - self._clearTimer((id as any)[i]); - self._endTimers[(id as any)[i]] = setTimeout(self._ended.bind(self, sound), timeout); + if (this._endTimers[(id as any)[i]] || !sound._paused) { + this._clearTimer((id as any)[i]); + this._endTimers[(id as any)[i]] = setTimeout(this._ended.bind(this, sound), timeout); } - self._emit('rate', sound._id); + this._emit('rate', sound._id); } } } else { - sound = self._soundById(id); - return sound ? sound._rate : self._rate; + sound = this._soundById(id); + return sound ? sound._rate : this._rate; } - return self; + return this; } seek(): number; seek(seek: number): Howl; seek(seek?: number): number | Howl { - const self = this; + const args = arguments; let seekVal: number | undefined; let id: number | undefined; if (args.length === 0) { - if (self._sounds.length) { - id = self._sounds[0]._id; + if (this._sounds.length) { + id = this._sounds[0]._id; } } else if (args.length === 1) { - const ids = self._getSoundIds(); + const ids = this._getSoundIds(); const index = ids.indexOf(args[0] as any); if (index >= 0) { id = parseInt(args[0] as any, 10); - } else if (self._sounds.length) { - id = self._sounds[0]._id; + } else if (this._sounds.length) { + id = this._sounds[0]._id; seekVal = parseFloat(args[0] as any); } } else if (args.length === 2) { @@ -1492,45 +1462,45 @@ class Howl { return 0; } - if (typeof seekVal === 'number' && (self._state !== 'loaded' || self._playLock)) { - self._queue.push({ + if (typeof seekVal === 'number' && (this._state !== 'loaded' || this._playLock)) { + this._queue.push({ event: 'seek', action: () => { - self.seek.apply(self, args as any); + this.seek.apply(this, args as any); } }); - return self; + return this; } - const sound = self._soundById(id); + const sound = this._soundById(id); if (sound) { if (typeof seekVal === 'number' && seekVal >= 0) { - const playing = self.playing(id); + const playing = this.playing(id); if (playing) { - self.pause(id, true); + this.pause(id, true); } sound._seek = seekVal; sound._ended = false; - self._clearTimer(id); + this._clearTimer(id); - if (!self._webAudio && sound._node && !isNaN(sound._node.duration)) { + if (!this._webAudio && sound._node && !isNaN(sound._node.duration)) { sound._node.currentTime = seekVal; } const seekAndEmit = () => { if (playing) { - self.play(id, true); + this.play(id, true); } - self._emit('seek', id); + this._emit('seek', id); }; - if (playing && !self._webAudio) { + if (playing && !this._webAudio) { const emitSeek = () => { - if (!self._playLock) { + if (!this._playLock) { seekAndEmit(); } else { setTimeout(emitSeek, 0); @@ -1541,8 +1511,8 @@ class Howl { seekAndEmit(); } } else { - if (self._webAudio) { - const realTime = self.playing(id) ? Howler.ctx!.currentTime - sound._playStart : 0; + if (this._webAudio) { + 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 { @@ -1551,19 +1521,19 @@ class Howl { } } - return self; + return this; } playing(id?: number): boolean { - const self = this; + if (typeof id === 'number') { - const sound = self._soundById(id); + const sound = this._soundById(id); return sound ? !sound._paused : false; } - for (let i = 0; i < self._sounds.length; i++) { - if (!self._sounds[i]._paused) { + for (let i = 0; i < this._sounds.length; i++) { + if (!this._sounds[i]._paused) { return true; } } @@ -1572,12 +1542,12 @@ class Howl { } duration(id?: number): number { - const self = this; - let duration = self._duration; + + let duration = this._duration; - const sound = self._soundById(id); + const sound = this._soundById(id); if (sound) { - duration = self._sprite[sound._sprite][1] / 1000; + duration = this._sprite[sound._sprite][1] / 1000; } return duration; @@ -1588,19 +1558,19 @@ class Howl { } unload(): null { - const self = this; + // Execute plugin hooks before destruction - globalPluginManager.executeHowlDestroy(self); + globalPluginManager.executeHowlDestroy(this); - const sounds = self._sounds; + const sounds = this._sounds; for (let i = 0; i < sounds.length; i++) { if (!sounds[i]._paused) { - self.stop(sounds[i]._id); + this.stop(sounds[i]._id); } - if (!self._webAudio) { - self._clearSound(sounds[i]._node); + if (!this._webAudio) { + this._clearSound(sounds[i]._node); sounds[i]._node.removeEventListener('error', sounds[i]._errorFn, false); sounds[i]._node.removeEventListener(Howler._canPlayEvent, sounds[i]._loadFn, false); @@ -1611,48 +1581,48 @@ class Howl { delete sounds[i]._node; - self._clearTimer(sounds[i]._id); + this._clearTimer(sounds[i]._id); } - const index = Howler._howls.indexOf(self); + const index = Howler._howls.indexOf(this); if (index >= 0) { Howler._howls.splice(index, 1); } let remCache = true; for (let i = 0; i < Howler._howls.length; i++) { - if (Howler._howls[i]._src === self._src || (self._src as string).indexOf(Howler._howls[i]._src as string) >= 0) { + if (Howler._howls[i]._src === this._src || (this._src as string).indexOf(Howler._howls[i]._src as string) >= 0) { remCache = false; break; } } if (cache && remCache) { - delete cache[self._src as string]; + delete cache[this._src as string]; } Howler.noAudio = false; - self._state = 'unloaded'; - self._sounds = []; + this._state = 'unloaded'; + this._sounds = []; return null; } on(event: string, fn: (...args: any[]) => void, id?: number, once?: boolean): Howl { - const self = this; - const events = (self as any)['_on' + event]; + + const events = (this as any)['_on' + event]; if (typeof fn === 'function') { events.push(once ? { id, fn, once } : { id, fn }); } - return self; + return this; } off(event: string, fn?: (...args: any[]) => void, id?: number): Howl { - const self = this; - const events = (self as any)['_on' + event]; + + const events = (this as any)['_on' + event]; let i = 0; if (typeof fn === 'number') { @@ -1669,60 +1639,57 @@ class Howl { } } } else if (event) { - (self as any)['_on' + event] = []; + (this as any)['_on' + event] = []; } else { - const keys = Object.keys(self); + const keys = Object.keys(this); for (i = 0; i < keys.length; i++) { - if (keys[i].indexOf('_on') === 0 && Array.isArray((self as any)[keys[i]])) { - (self as any)[keys[i]] = []; + if (keys[i].indexOf('_on') === 0 && Array.isArray((this as any)[keys[i]])) { + (this as any)[keys[i]] = []; } } } - return self; + return this; } once(event: string, fn: (...args: any[]) => void, id?: number): Howl { - const self = this; + - self.on(event, fn, id, true); + this.on(event, fn, id, true); - return self; + return this; } _emit(event: string, id?: number | null, msg?: string): Howl { - const self = this; - const events = (self as any)['_on' + event]; + const events = (this as any)['_on' + event]; for (let i = events.length - 1; i >= 0; i--) { if (!events[i].id || events[i].id === id || event === 'load') { - setTimeout( - function(fn) { - fn.call(this, id, msg); - }.bind(self, events[i].fn), - 0 - ); + const fn = events[i].fn; + setTimeout(() => { + fn(id, msg); + }, 0); if (events[i].once) { - self.off(event, events[i].fn, events[i].id); + this.off(event, events[i].fn, events[i].id); } } } - self._loadQueue(event); + this._loadQueue(event); - return self; + return this; } _loadQueue(event?: string): Howl { - const self = this; + - if (self._queue.length > 0) { - const task = self._queue[0]; + if (this._queue.length > 0) { + const task = this._queue[0]; if (task.event === event) { - self._queue.shift(); - self._loadQueue(); + this._queue.shift(); + this._loadQueue(); } if (!event) { @@ -1730,80 +1697,80 @@ class Howl { } } - return self; + return this; } _ended(sound: Sound): Howl { - const self = this; + const sprite = sound._sprite; - 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; + 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; } - const loop = !!(sound._loop || self._sprite[sprite][2]); + const loop = !!(sound._loop || this._sprite[sprite][2]); - self._emit('end', sound._id); + this._emit('end', sound._id); - if (!self._webAudio && loop) { - self.stop(sound._id, true).play(sound._id); + if (!this._webAudio && loop) { + this.stop(sound._id, true).play(sound._id); } - if (self._webAudio && loop) { - self._emit('play', sound._id); + if (this._webAudio && loop) { + this._emit('play', sound._id); sound._seek = sound._start || 0; sound._rateSeek = 0; sound._playStart = Howler.ctx!.currentTime; const timeout = ((sound._stop! - (sound._start || 0)) * 1000) / Math.abs(sound._rate); - self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout); + this._endTimers[sound._id] = setTimeout(this._ended.bind(this, sound), timeout); } - if (self._webAudio && !loop) { + if (this._webAudio && !loop) { sound._paused = true; sound._ended = true; sound._seek = sound._start || 0; sound._rateSeek = 0; - self._clearTimer(sound._id); + this._clearTimer(sound._id); - self._cleanBuffer(sound._node); + this._cleanBuffer(sound._node); Howler._autoSuspend(); } - if (!self._webAudio && !loop) { - self.stop(sound._id, true); + if (!this._webAudio && !loop) { + this.stop(sound._id, true); } - return self; + return this; } _clearTimer(id: number): Howl { - const self = this; + - if (self._endTimers[id]) { - if (typeof self._endTimers[id] !== 'function') { - clearTimeout(self._endTimers[id]); + if (this._endTimers[id]) { + if (typeof this._endTimers[id] !== 'function') { + clearTimeout(this._endTimers[id]); } else { - const sound = self._soundById(id); + const sound = this._soundById(id); if (sound && sound._node) { - sound._node.removeEventListener('ended', self._endTimers[id], false); + sound._node.removeEventListener('ended', this._endTimers[id], false); } } - delete self._endTimers[id]; + delete this._endTimers[id]; } - return self; + return this; } _soundById(id: number): Sound | null { - const self = this; + - for (let i = 0; i < self._sounds.length; i++) { - if (id === self._sounds[i]._id) { - return self._sounds[i]; + for (let i = 0; i < this._sounds.length; i++) { + if (id === this._sounds[i]._id) { + return this._sounds[i]; } } @@ -1811,57 +1778,57 @@ class Howl { } _inactiveSound(): Sound { - const self = this; + - self._drain(); + this._drain(); - for (let i = 0; i < self._sounds.length; i++) { - if (self._sounds[i]._ended) { - return self._sounds[i].reset(); + for (let i = 0; i < this._sounds.length; i++) { + if (this._sounds[i]._ended) { + return this._sounds[i].reset(); } } - return new Sound(self); + return new Sound(this); } _drain(): void { - const self = this; - const limit = self._pool; + + const limit = this._pool; let cnt = 0; - if (self._sounds.length < limit) { + if (this._sounds.length < limit) { return; } - for (let i = 0; i < self._sounds.length; i++) { - if (self._sounds[i]._ended) { + for (let i = 0; i < this._sounds.length; i++) { + if (this._sounds[i]._ended) { cnt++; } } - for (let i = self._sounds.length - 1; i >= 0; i--) { + for (let i = this._sounds.length - 1; i >= 0; i--) { if (cnt <= limit) { return; } - if (self._sounds[i]._ended) { - if (self._webAudio && self._sounds[i]._node) { - self._sounds[i]._node.disconnect(0); + if (this._sounds[i]._ended) { + if (this._webAudio && this._sounds[i]._node) { + this._sounds[i]._node.disconnect(0); } - self._sounds.splice(i, 1); + this._sounds.splice(i, 1); cnt--; } } } _getSoundIds(id?: number): number[] { - const self = this; + if (typeof id === 'undefined') { const ids: number[] = []; - for (let i = 0; i < self._sounds.length; i++) { - ids.push(self._sounds[i]._id); + for (let i = 0; i < this._sounds.length; i++) { + ids.push(this._sounds[i]._id); } return ids; @@ -1871,10 +1838,10 @@ class Howl { } _refreshBuffer(sound: Sound): Howl { - const self = this; + sound._node.bufferSource = Howler.ctx!.createBufferSource(); - sound._node.bufferSource.buffer = cache[self._src as string]; + sound._node.bufferSource.buffer = cache[this._src as string]; if (sound._panner) { sound._node.bufferSource.connect(sound._panner); @@ -1889,15 +1856,15 @@ class Howl { } sound._node.bufferSource.playbackRate.setValueAtTime(sound._rate, Howler.ctx!.currentTime); - return self; + return this; } _cleanBuffer(node: any): Howl { - const self = this; + const isIOS = isAppleVendor(Howler._navigator); if (!node.bufferSource) { - return self; + return this; } if (Howler._scratchBuffer && node.bufferSource) { @@ -1911,7 +1878,7 @@ class Howl { } node.bufferSource = null; - return self; + return this; } _clearSound(node: HTMLAudioElement): void { diff --git a/src/plugins/plugin.ts b/src/plugins/plugin.ts index eb658ac8..e710cb54 100644 --- a/src/plugins/plugin.ts +++ b/src/plugins/plugin.ts @@ -15,13 +15,11 @@ import type { HowlOptions } from '../types'; * Plugin hook lifecycle events */ export interface PluginHooks { - /** - * Called when the plugin is registered with the PluginManager - */ - onRegister?: () => void; - /** * Called when Howler global instance is initialized + * This is called either: + * - When Howler initializes (if plugin was registered before) + * - Immediately during registration (if Howler is already initialized) */ onHowlerInit?: (howler: HowlerGlobal) => void; @@ -106,17 +104,6 @@ export class PluginManager { this.plugins.set(plugin.name, registered); - // Execute onRegister hook if provided - // Plugins can use this hook to initialize themselves, even if Howler is - // already initialized. The howlerInstance is available via getHowlerInstance() - if (hooks.onRegister) { - try { - hooks.onRegister(); - } catch (error: unknown) { - console.error(`Error during onRegister for plugin "${plugin.name}":`, error); - } - } - // If Howler is already initialized, execute onHowlerInit hook for this plugin if (this.howlerInstance && hooks.onHowlerInit) { try { @@ -175,7 +162,7 @@ export class PluginManager { /** * Get the Howler instance (if initialized) - * This can be used by plugins in their onRegister hook to apply initialization + * This can be used by plugins to access the Howler instance if needed */ getHowlerInstance(): HowlerGlobal | null { return this.howlerInstance; From 157be9621b58bcbe06cdcbce3dec7518f72552b4 Mon Sep 17 00:00:00 2001 From: eatsjobs Date: Sun, 23 Nov 2025 00:39:04 +0100 Subject: [PATCH 18/25] Enhance Howler.js TypeScript types and improve audio handling - Expanded TypeScript types in `types.ts` to include custom interfaces for HTML5 audio elements and audio nodes, improving type safety and clarity. - Updated `HowlerGlobal` and `Sound` classes in `howler.core.ts` to utilize new types, ensuring better type checking and reducing potential runtime errors. - Refined audio handling logic to incorporate type guards for audio nodes, enhancing the robustness of audio playback and manipulation. - Improved error handling in `audio-loader.ts` by creating a more descriptive error event for XMLHttpRequest failures. - Adjusted spatial plugin connections to ensure compatibility with the new type definitions. These changes aim to enhance the overall developer experience and maintainability of the codebase while ensuring existing functionality remains intact. --- src/helpers/audio-loader.ts | 8 +- src/howler.core.ts | 413 +++++++++++++++++++++------------- src/plugins/spatial-plugin.ts | 5 +- src/types.ts | 48 +++- 4 files changed, 312 insertions(+), 162 deletions(-) diff --git a/src/helpers/audio-loader.ts b/src/helpers/audio-loader.ts index 72239a9d..1345322b 100644 --- a/src/helpers/audio-loader.ts +++ b/src/helpers/audio-loader.ts @@ -69,7 +69,13 @@ export const safeXhrSend = (xhr: XMLHttpRequest) => { xhr.send(); } catch (e) { if (xhr.onerror) { - xhr.onerror(new Event('error')); + // Create a ProgressEvent-like object for the error handler + const errorEvent = new ProgressEvent('error', { + lengthComputable: false, + loaded: 0, + total: 0 + }); + xhr.onerror(errorEvent); } } }; diff --git a/src/howler.core.ts b/src/howler.core.ts index 244bfa57..67cec6c0 100644 --- a/src/howler.core.ts +++ b/src/howler.core.ts @@ -8,7 +8,7 @@ * MIT License */ // Import shared types -import { cache, EventListener, HowlOptions, QueueItem } from './types'; +import { AudioBufferSourceNodeWithLegacy, cache, EventListener, GainNodeWithBufferSource, HowlOptions, HTMLAudioElementWithUnlocked, isGainNode, isHTMLAudioElement, NavigatorWithCocoonJS, QueueItem, WindowWithAudio } from './types'; // Import helper functions import { isAppleVendor, isIE, isOldOpera, isOldSafari, loadBuffer, setupAudioContext } from './helpers'; @@ -25,7 +25,7 @@ export class HowlerGlobal { _muted: boolean = false; _volume: number = 1; _canPlayEvent: string = 'canplaythrough'; - _navigator: Navigator | null = null; + _navigator: NavigatorWithCocoonJS | null = null; masterGain: GainNode | null = null; noAudio: boolean = false; usingWebAudio: boolean = true; @@ -67,7 +67,7 @@ export class HowlerGlobal { volume(vol?: number): number | HowlerGlobal { if (vol !== undefined) { - vol = parseFloat(vol as any); + vol = parseFloat(String(vol)); if (!this.ctx) { setupAudioContext(); @@ -89,7 +89,7 @@ export class HowlerGlobal { const ids = this._howls[i]._getSoundIds(); for (let j = 0; j < ids.length; j++) { const sound = this._howls[i]._soundById(ids[j]); - if (sound && sound._node) { + if (sound && sound._node && isHTMLAudioElement(sound._node)) { sound._node.volume = sound._volume * vol; } } @@ -119,7 +119,7 @@ export class HowlerGlobal { const ids = this._howls[i]._getSoundIds(); for (let j = 0; j < ids.length; j++) { const sound = this._howls[i]._soundById(ids[j]); - if (sound && sound._node) { + if (sound && sound._node && isHTMLAudioElement(sound._node)) { sound._node.muted = muted ? true : sound._muted; } } @@ -220,7 +220,7 @@ export class HowlerGlobal { } _setupCodecs(): HowlerGlobal { - let audioTest: any = null; + let audioTest: HTMLAudioElement | null = null; try { audioTest = typeof window.Audio !== 'undefined' ? new window.Audio() : null; @@ -275,7 +275,7 @@ export class HowlerGlobal { const unlock = () => { while (this._html5AudioPool.length < this.html5PoolSize) { try { - const audioNode = new (window as any).Audio(); + const audioNode = new (window as WindowWithAudio).Audio() as HTMLAudioElementWithUnlocked; audioNode._unlocked = true; this._releaseHtml5Audio(audioNode); } catch (e) { @@ -289,8 +289,8 @@ export class HowlerGlobal { const ids = this._howls[i]._getSoundIds(); for (let j = 0; j < ids.length; j++) { const sound = this._howls[i]._soundById(ids[j]); - if (sound && sound._node && !(sound._node as any)._unlocked) { - (sound._node as any)._unlocked = true; + if (sound && sound._node && isHTMLAudioElement(sound._node) && !sound._node._unlocked) { + sound._node._unlocked = true; sound._node.load(); } } @@ -304,7 +304,7 @@ export class HowlerGlobal { source.connect(this.ctx!.destination); if (typeof source.start === 'undefined') { - (source as any).noteOn(0); + (source as AudioBufferSourceNodeWithLegacy).noteOn?.(0); } else { source.start(0); } @@ -328,28 +328,35 @@ export class HowlerGlobal { }; }; - document.addEventListener('touchstart', unlock as any, true); - document.addEventListener('touchend', unlock as any, true); - document.addEventListener('click', unlock as any, true); - document.addEventListener('keydown', unlock as any, true); + document.addEventListener('touchstart', unlock, true); + document.addEventListener('touchend', unlock, true); + document.addEventListener('click', unlock, true); + document.addEventListener('keydown', unlock, true); } - _obtainHtml5Audio(): HTMLAudioElement { + _obtainHtml5Audio(): HTMLAudioElementWithUnlocked { if (this._html5AudioPool.length) { return this._html5AudioPool.pop()!; } - const testPlay = new (window as any).Audio().play(); - if (testPlay && typeof Promise !== 'undefined' && (testPlay instanceof Promise || typeof (testPlay as any).then === 'function')) { - (testPlay as any).catch(() => { - console.warn('HTML5 Audio pool exhausted, returning potentially locked audio object.'); - }); + const testPlay = new (window as WindowWithAudio).Audio().play(); + if (testPlay && typeof Promise !== 'undefined') { + if (testPlay instanceof Promise) { + testPlay.catch(() => { + console.warn('HTML5 Audio pool exhausted, returning potentially locked audio object.'); + }); + } else if (typeof testPlay === 'object' && testPlay !== null && 'then' in testPlay && typeof (testPlay as { then?: unknown }).then === 'function') { + // Handle thenable objects + (testPlay as { catch: (onRejected: () => void) => void }).catch(() => { + console.warn('HTML5 Audio pool exhausted, returning potentially locked audio object.'); + }); + } } - return new (window as any).Audio(); + return new (window as WindowWithAudio).Audio() as HTMLAudioElementWithUnlocked; } - _releaseHtml5Audio(audio: any): HowlerGlobal { + _releaseHtml5Audio(audio: HTMLAudioElementWithUnlocked): HowlerGlobal { if (audio._unlocked) { this._html5AudioPool.push(audio); } @@ -427,6 +434,8 @@ export class HowlerGlobal { // Setup the global audio controller const Howler = new HowlerGlobal(); +// Type guards for Sound._node + class Sound { _parent: Howl; _muted: boolean = false; @@ -438,12 +447,12 @@ class Sound { _ended: boolean = true; _sprite: string = '__default'; _id: number = 0; - _node: HTMLAudioElement | any = null; + _node: HTMLAudioElementWithUnlocked | GainNodeWithBufferSource | null = null; _playStart: number = 0; _rateSeek: number = 0; - _errorFn?: any; - _loadFn?: any; - _endFn?: any; + _errorFn?: (event: Event) => void; + _loadFn?: (event: Event) => void; + _endFn?: (event: Event) => void; _start?: number; _stop?: number; _panner?: PannerNode | StereoPannerNode; @@ -481,15 +490,21 @@ class Sound { } create(): Sound { - const parent = this._parent; const volume = Howler._muted || this._muted || parent._muted ? 0 : this._volume; - if (parent._webAudio) { - this._node = typeof Howler.ctx!.createGain === 'undefined' ? Howler.ctx!.createGainNode() : Howler.ctx!.createGain(); - this._node.gain.setValueAtTime(volume, Howler.ctx!.currentTime); - this._node.paused = true; - this._node.connect(Howler.masterGain); + this._errorFn = this._errorListener.bind(this); + this._loadFn = this._loadListener.bind(this); + this._endFn = this._endListener.bind(this); + + if (parent._webAudio && Howler.ctx) { + const gainNode = typeof Howler.ctx.createGain === 'undefined' ? (Howler.ctx as { createGainNode?: () => GainNode }).createGainNode?.() : Howler.ctx.createGain(); + if (gainNode) { + this._node = gainNode as GainNodeWithBufferSource; + this._node.gain.setValueAtTime(volume, Howler.ctx.currentTime); + (this._node as { paused?: boolean }).paused = true; + this._node.connect(Howler.masterGain!); + } } else if (!Howler.noAudio) { this._node = Howler._obtainHtml5Audio(); @@ -502,8 +517,10 @@ class Sound { 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; + const src = typeof parent._src === 'string' ? parent._src : (Array.isArray(parent._src) && parent._src.length > 0 ? parent._src[0] : ''); + this._node.src = src; + const preloadValue = parent._preload === true ? 'auto' : (parent._preload === false ? 'none' : (parent._preload === 'metadata' ? 'metadata' : 'auto')); + this._node.preload = preloadValue; const volumeOrHowler = Howler.volume(); if (typeof volumeOrHowler === 'number') { this._node.volume = volume * volumeOrHowler; @@ -535,13 +552,20 @@ class Sound { } _errorListener(): void { - - this._parent._emit('loaderror', this._id, this._node.error ? this._node.error.code : 0); - this._node.removeEventListener('error', this._errorFn, false); + if (this._node && isHTMLAudioElement(this._node)) { + const errorCode = this._node.error ? this._node.error.code : 0; + this._parent._emit('loaderror', this._id, String(errorCode)); + if (this._errorFn) { + this._node.removeEventListener('error', this._errorFn, false); + } + } } _loadListener(): void { - + if (!this._node || !isHTMLAudioElement(this._node)) { + return; + } + const parent = this._parent; parent._duration = Math.ceil(this._node.duration * 10) / 10; @@ -559,14 +583,16 @@ class Sound { globalPluginManager.executeHowlLoad(parent); } - this._node.removeEventListener(Howler._canPlayEvent, this._loadFn, false); + if (this._loadFn) { + this._node.removeEventListener(Howler._canPlayEvent, this._loadFn, false); + } } _endListener(): void { const parent = this._parent; - if (parent._duration === Infinity) { + if (parent._duration === Infinity && this._node && isHTMLAudioElement(this._node)) { parent._duration = Math.ceil(this._node.duration * 10) / 10; if (parent._sprite.__default[1] === Infinity) { @@ -576,7 +602,9 @@ class Sound { parent._ended(this); } - this._node.removeEventListener('ended', this._endFn, false); + if (this._endFn && this._node) { + this._node.removeEventListener('ended', this._endFn, false); + } } } @@ -596,7 +624,7 @@ class Howl { _duration: number = 0; _state: string = 'unloaded'; _sounds: Sound[] = []; - _endTimers: Record = {}; + _endTimers: Record> = {}; _queue: QueueItem[] = []; _playLock: boolean = false; _webAudio: boolean = false; @@ -656,8 +684,16 @@ class Howl { 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._onloaderror = o.onloaderror ? [{ fn: (...args: unknown[]) => { + if (o.onloaderror && typeof args[0] === 'number' && typeof args[1] === 'string') { + o.onloaderror(args[0], args[1]); + } + } }] : []; + this._onplayerror = o.onplayerror ? [{ fn: (...args: unknown[]) => { + if (o.onplayerror && typeof args[0] === 'number' && typeof args[1] === 'string') { + o.onplayerror(args[0], args[1]); + } + } }] : []; this._onpause = o.onpause ? [{ fn: o.onpause }] : []; this._onplay = o.onplay ? [{ fn: o.onplay }] : []; this._onstop = o.onstop ? [{ fn: o.onstop }] : []; @@ -688,7 +724,7 @@ class Howl { }); } - if (this._preload && this._preload !== 'none') { + if (this._preload === true || this._preload === 'metadata') { this.load(); } @@ -799,7 +835,7 @@ class Howl { } if (this._state !== 'loaded') { - sound._sprite = sprite; + sound._sprite = sprite || '__default'; sound._ended = false; const soundId = sound._id; @@ -849,7 +885,7 @@ class Howl { const node = sound._node; - if (this._webAudio) { + if (this._webAudio && node && isGainNode(node)) { const playWebAudio = () => { this._playLock = false; setParams(); @@ -859,10 +895,12 @@ class Howl { node.gain.setValueAtTime(vol, Howler.ctx!.currentTime); sound._playStart = Howler.ctx!.currentTime; - if (typeof (node.bufferSource as any).start === 'undefined') { - (node.bufferSource as any).noteGrainOn(0, seek, sound._loop ? 86400 : duration); - } else { - (node.bufferSource as any).start(0, seek, sound._loop ? 86400 : duration); + if (node.bufferSource) { + if (typeof node.bufferSource.start === 'undefined') { + node.bufferSource.noteGrainOn?.(0, seek, sound._loop ? 86400 : duration); + } else { + node.bufferSource.start(0, seek, sound._loop ? 86400 : duration); + } } if (timeout !== Infinity) { @@ -884,7 +922,7 @@ class Howl { this.once('resume', playWebAudio); this._clearTimer(sound._id); } - } else { + } else if (node && isHTMLAudioElement(node)) { const playHtml5 = () => { node.currentTime = seek; node.muted = sound._muted || this._muted || Howler._muted || node.muted; @@ -903,7 +941,9 @@ class Howl { (play as any) .then(() => { this._playLock = false; - (node as any)._unlocked = true; + if ('_unlocked' in node) { + (node as HTMLAudioElementWithUnlocked)._unlocked = true; + } if (!internal) { this._emit('play', sound._id); } else { @@ -932,11 +972,12 @@ class Howl { if (sprite !== '__default' || sound._loop) { this._endTimers[sound._id] = setTimeout(this._ended.bind(this, sound), timeout); } else { - this._endTimers[sound._id] = () => { + const endHandler = () => { this._ended(sound); - node.removeEventListener('ended', this._endTimers[sound._id], false); + node.removeEventListener('ended', endHandler, false); }; - node.addEventListener('ended', this._endTimers[sound._id], false); + this._endTimers[sound._id] = setTimeout(endHandler, timeout); + node.addEventListener('ended', endHandler, false); } } catch (err: unknown) { this._emit('playerror', sound._id, err instanceof Error ? err.message : String(err)); @@ -944,11 +985,12 @@ class Howl { }; if (node.src === 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA') { - node.src = this._src; + const src = typeof this._src === 'string' ? this._src : (Array.isArray(this._src) && this._src.length > 0 ? this._src[0] : ''); + node.src = src; node.load(); } - const loadedNoReadyState = (typeof (window as any).ejecta !== 'undefined') || (!node.readyState && Howler._navigator && (Howler._navigator as any).isCocoonJS); + const loadedNoReadyState = (typeof (window as WindowWithAudio).ejecta !== 'undefined') || (!node.readyState && Howler._navigator && Howler._navigator.isCocoonJS); if (node.readyState >= 3 || loadedNoReadyState) { playHtml5(); } else { @@ -960,7 +1002,7 @@ class Howl { playHtml5(); node.removeEventListener(Howler._canPlayEvent, listener, false); }; - node.addEventListener(Howler._canPlayEvent, listener as any, false); + node.addEventListener(Howler._canPlayEvent, listener, false); this._clearTimer(sound._id); } @@ -969,7 +1011,7 @@ class Howl { return sound._id; } - pause(id?: number): Howl { + pause(id?: number, internal?: boolean): Howl { if (this._state !== 'loaded' || this._playLock) { @@ -991,26 +1033,27 @@ class Howl { const sound = this._soundById(ids[i]); if (sound && !sound._paused) { - sound._seek = this.seek(ids[i]) as number; + const seekResult = this.seek(ids[i]); + sound._seek = typeof seekResult === 'number' ? seekResult : 0; sound._rateSeek = 0; sound._paused = true; this._stopFade(ids[i]); if (sound._node) { - if (this._webAudio) { + if (this._webAudio && isGainNode(sound._node)) { if (!sound._node.bufferSource) { continue; } - if (typeof (sound._node.bufferSource as any).stop === 'undefined') { - (sound._node.bufferSource as any).noteOff(0); + if (typeof sound._node.bufferSource.stop === 'undefined') { + sound._node.bufferSource.noteOff?.(0); } else { - (sound._node.bufferSource as any).stop(0); + sound._node.bufferSource.stop(0); } this._cleanBuffer(sound._node); - } else if (!isNaN(sound._node.duration) || sound._node.duration === Infinity) { + } else if (isHTMLAudioElement(sound._node) && (!isNaN(sound._node.duration) || sound._node.duration === Infinity)) { sound._node.pause(); } } @@ -1054,17 +1097,17 @@ class Howl { this._stopFade(ids[i]); if (sound._node) { - if (this._webAudio) { + if (this._webAudio && isGainNode(sound._node)) { if (sound._node.bufferSource) { - if (typeof (sound._node.bufferSource as any).stop === 'undefined') { - (sound._node.bufferSource as any).noteOff(0); + if (typeof sound._node.bufferSource.stop === 'undefined') { + sound._node.bufferSource.noteOff?.(0); } else { - (sound._node.bufferSource as any).stop(0); + sound._node.bufferSource.stop(0); } this._cleanBuffer(sound._node); } - } else if (!isNaN(sound._node.duration) || sound._node.duration === Infinity) { + } else if (isHTMLAudioElement(sound._node) && (!isNaN(sound._node.duration) || sound._node.duration === Infinity)) { sound._node.currentTime = sound._start || 0; sound._node.pause(); @@ -1117,9 +1160,9 @@ class Howl { this._stopFade(sound._id); } - if (this._webAudio && sound._node) { + if (this._webAudio && sound._node && isGainNode(sound._node)) { sound._node.gain.setValueAtTime(muted ? 0 : sound._volume, Howler.ctx!.currentTime); - } else if (sound._node) { + } else if (sound._node && isHTMLAudioElement(sound._node)) { sound._node.muted = Howler._muted ? true : muted; } @@ -1132,6 +1175,8 @@ class Howl { volume(): number; volume(vol: number): Howl; + volume(vol: number, id: number): Howl; + volume(vol: number, id: number, internal: boolean): Howl; volume(vol?: number): number | Howl { const args = arguments; @@ -1142,15 +1187,15 @@ class Howl { return this._volume; } else if (args.length === 1 || (args.length === 2 && typeof args[1] === 'undefined')) { const ids = this._getSoundIds(); - const index = ids.indexOf(args[0] as any); + const index = ids.indexOf(args[0] as number); if (index >= 0) { - id = parseInt(args[0] as any, 10); + id = parseInt(String(args[0]), 10); } else { - volume = parseFloat(args[0] as any); + volume = parseFloat(String(args[0])); } } else if (args.length >= 2) { - volume = parseFloat(args[0] as any); - id = parseInt(args[1] as any, 10); + volume = parseFloat(String(args[0])); + id = parseInt(String(args[1]), 10); } let sound; @@ -1159,7 +1204,13 @@ class Howl { this._queue.push({ event: 'volume', action: () => { - this.volume.apply(this, args as any); + if (args.length >= 1 && typeof args[0] === 'number') { + if (args.length >= 2 && typeof args[1] === 'number') { + this.volume(args[0], args[1]); + } else { + this.volume(args[0]); + } + } } }); @@ -1170,20 +1221,20 @@ class Howl { this._volume = volume; } - id = this._getSoundIds(id); - for (let i = 0; i < (id as any).length; i++) { - sound = this._soundById((id as any)[i]); + const soundIds = this._getSoundIds(id); + for (let i = 0; i < soundIds.length; i++) { + sound = this._soundById(soundIds[i]); if (sound) { sound._volume = volume; - if (!(args as any)[2]) { - this._stopFade((id as any)[i]); + if (!args[2]) { + this._stopFade(soundIds[i]); } - if (this._webAudio && sound._node && !sound._muted) { + if (this._webAudio && sound._node && isGainNode(sound._node) && !sound._muted) { sound._node.gain.setValueAtTime(volume, Howler.ctx!.currentTime); - } else if (sound._node && !sound._muted) { + } else if (sound._node && isHTMLAudioElement(sound._node) && !sound._muted) { const volumeMultiplierOrGlobal = Howler.volume(); if (typeof volumeMultiplierOrGlobal === 'number') { sound._node.volume = volume * volumeMultiplierOrGlobal; @@ -1215,11 +1266,15 @@ class Howl { return this; } - from = Math.min(Math.max(0, parseFloat(from as any)), 1); - to = Math.min(Math.max(0, parseFloat(to as any)), 1); - len = parseFloat(len as any); + from = Math.min(Math.max(0, parseFloat(String(from))), 1); + to = Math.min(Math.max(0, parseFloat(String(to))), 1); + len = parseFloat(String(len)); - this.volume(from, id); + if (typeof id !== 'undefined') { + this.volume(from, id); + } else { + this.volume(from); + } const ids = this._getSoundIds(id); for (let i = 0; i < ids.length; i++) { @@ -1234,8 +1289,10 @@ class Howl { const currentTime = Howler.ctx!.currentTime; const end = currentTime + len / 1000; sound._volume = from; - sound._node.gain.setValueAtTime(from, currentTime); - sound._node.gain.linearRampToValueAtTime(to, end); + if (sound._node && isGainNode(sound._node)) { + sound._node.gain.setValueAtTime(from, currentTime); + sound._node.gain.linearRampToValueAtTime(to, end); + } } this._startFadeInterval(sound, from, to, len, ids[i], typeof id === 'undefined'); @@ -1279,7 +1336,9 @@ class Howl { } if ((to < from && vol <= to) || (to > from && vol >= to)) { - clearInterval(sound._interval as any); + if (sound._interval) { + clearInterval(sound._interval); + } sound._interval = undefined; sound._fadeTo = undefined; this.volume(to, sound._id); @@ -1293,12 +1352,14 @@ class Howl { const sound = this._soundById(id); if (sound && sound._interval) { - if (this._webAudio) { + if (this._webAudio && sound._node && isGainNode(sound._node)) { sound._node.gain.cancelScheduledValues(Howler.ctx!.currentTime); } - clearInterval(sound._interval as any); - sound._interval = undefined; + if (sound._interval) { + clearInterval(sound._interval); + sound._interval = undefined; + } this.volume(sound._fadeTo as number, id); sound._fadeTo = undefined; this._emit('fade', id); @@ -1323,12 +1384,12 @@ class Howl { loopVal = args[0] as boolean; this._loop = loopVal; } else { - sound = this._soundById(parseInt(args[0] as any, 10)); + sound = this._soundById(parseInt(String(args[0]), 10)); return sound ? sound._loop : false; } } else if (args.length === 2) { loopVal = args[0] as boolean; - id = parseInt(args[1] as any, 10); + id = parseInt(String(args[1]), 10); } const ids = this._getSoundIds(id); @@ -1337,11 +1398,11 @@ class Howl { if (sound) { sound._loop = loopVal as boolean; - if (this._webAudio && sound._node && (sound._node.bufferSource as any)) { - (sound._node.bufferSource as any).loop = loopVal; + if (this._webAudio && sound._node && isGainNode(sound._node) && sound._node.bufferSource) { + sound._node.bufferSource.loop = loopVal; if (loopVal) { - (sound._node.bufferSource as any).loopStart = sound._start || 0; - (sound._node.bufferSource as any).loopEnd = sound._stop; + sound._node.bufferSource.loopStart = sound._start || 0; + sound._node.bufferSource.loopEnd = sound._stop; if (this.playing(ids[i])) { this.pause(ids[i], true); @@ -1357,6 +1418,7 @@ class Howl { rate(): number; rate(rate: number): Howl; + rate(rate: number, id: number): Howl; rate(rate?: number): number | Howl { const args = arguments; @@ -1367,15 +1429,15 @@ class Howl { id = this._sounds[0]._id; } else if (args.length === 1) { const ids = this._getSoundIds(); - const index = ids.indexOf(args[0] as any); + const index = ids.indexOf(args[0] as number); if (index >= 0) { - id = parseInt(args[0] as any, 10); + id = parseInt(String(args[0]), 10); } else { - rateVal = parseFloat(args[0] as any); + rateVal = parseFloat(String(args[0])); } } else if (args.length === 2) { - rateVal = parseFloat(args[0] as any); - id = parseInt(args[1] as any, 10); + rateVal = parseFloat(String(args[0])); + id = parseInt(String(args[1]), 10); } let sound; @@ -1384,7 +1446,13 @@ class Howl { this._queue.push({ event: 'rate', action: () => { - this.rate.apply(this, args as any); + if (args.length >= 1 && typeof args[0] === 'number') { + if (args.length >= 2 && typeof args[1] === 'number') { + this.rate(args[0], args[1]); + } else { + this.rate(args[0]); + } + } } }); @@ -1395,38 +1463,43 @@ class Howl { this._rate = rateVal; } - id = this._getSoundIds(id); - for (let i = 0; i < (id as any).length; i++) { - sound = this._soundById((id as any)[i]); + const soundIds = this._getSoundIds(id); + for (let i = 0; i < soundIds.length; i++) { + sound = this._soundById(soundIds[i]); if (sound) { - if (this.playing((id as any)[i])) { - sound._rateSeek = this.seek((id as any)[i]) as number; + if (this.playing(soundIds[i])) { + const seekResult = this.seek(soundIds[i]); + sound._rateSeek = typeof seekResult === 'number' ? seekResult : 0; sound._playStart = this._webAudio ? Howler.ctx!.currentTime : sound._playStart; } sound._rate = rateVal; - if (this._webAudio && sound._node && (sound._node.bufferSource as any)) { - (sound._node.bufferSource as any).playbackRate.setValueAtTime(rateVal, Howler.ctx!.currentTime); - } else if (sound._node) { + if (this._webAudio && sound._node && isGainNode(sound._node) && sound._node.bufferSource) { + sound._node.bufferSource.playbackRate.setValueAtTime(rateVal, Howler.ctx!.currentTime); + } else if (sound._node && isHTMLAudioElement(sound._node)) { sound._node.playbackRate = rateVal; } - const seek = this.seek((id as any)[i]) as number; + const seekResult = this.seek(soundIds[i]); + const seek = typeof seekResult === 'number' ? seekResult : 0; const duration = (this._sprite[sound._sprite][0] + this._sprite[sound._sprite][1]) / 1000 - seek; const timeout = (duration * 1000) / Math.abs(sound._rate); - if (this._endTimers[(id as any)[i]] || !sound._paused) { - this._clearTimer((id as any)[i]); - this._endTimers[(id as any)[i]] = setTimeout(this._ended.bind(this, sound), timeout); + if (this._endTimers[soundIds[i]] || !sound._paused) { + this._clearTimer(soundIds[i]); + this._endTimers[soundIds[i]] = setTimeout(this._ended.bind(this, sound), timeout); } this._emit('rate', sound._id); } } } else { - sound = this._soundById(id); - return sound ? sound._rate : this._rate; + if (typeof id !== 'undefined') { + sound = this._soundById(id); + return sound ? sound._rate : this._rate; + } + return this._rate; } return this; @@ -1434,6 +1507,7 @@ class Howl { seek(): number; seek(seek: number): Howl; + seek(seek: number, id: number): Howl; seek(seek?: number): number | Howl { const args = arguments; @@ -1446,16 +1520,16 @@ class Howl { } } else if (args.length === 1) { const ids = this._getSoundIds(); - const index = ids.indexOf(args[0] as any); + const index = ids.indexOf(args[0] as number); if (index >= 0) { - id = parseInt(args[0] as any, 10); + id = parseInt(String(args[0]), 10); } else if (this._sounds.length) { id = this._sounds[0]._id; - seekVal = parseFloat(args[0] as any); + seekVal = parseFloat(String(args[0])); } } else if (args.length === 2) { - seekVal = parseFloat(args[0] as any); - id = parseInt(args[1] as any, 10); + seekVal = parseFloat(String(args[0])); + id = parseInt(String(args[1]), 10); } if (typeof id === 'undefined') { @@ -1466,7 +1540,13 @@ class Howl { this._queue.push({ event: 'seek', action: () => { - this.seek.apply(this, args as any); + if (args.length >= 1 && typeof args[0] === 'number') { + if (args.length >= 2 && typeof args[1] === 'number') { + this.seek(args[0], args[1]); + } else { + this.seek(args[0]); + } + } } }); @@ -1486,7 +1566,7 @@ class Howl { sound._ended = false; this._clearTimer(id); - if (!this._webAudio && sound._node && !isNaN(sound._node.duration)) { + if (!this._webAudio && sound._node && isHTMLAudioElement(sound._node) && !isNaN(sound._node.duration)) { sound._node.currentTime = seekVal; } @@ -1515,9 +1595,10 @@ class Howl { 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 { + } else if (sound._node && isHTMLAudioElement(sound._node)) { return sound._node.currentTime; } + return 0; } } @@ -1545,9 +1626,11 @@ class Howl { let duration = this._duration; - const sound = this._soundById(id); - if (sound) { - duration = this._sprite[sound._sprite][1] / 1000; + if (typeof id !== 'undefined') { + const sound = this._soundById(id); + if (sound) { + duration = this._sprite[sound._sprite][1] / 1000; + } } return duration; @@ -1569,17 +1652,27 @@ class Howl { this.stop(sounds[i]._id); } - if (!this._webAudio) { - this._clearSound(sounds[i]._node); + const node = sounds[i]._node; + if (!this._webAudio && node && isHTMLAudioElement(node)) { + this._clearSound(node); - 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); + const errorFn = sounds[i]._errorFn; + if (errorFn) { + node.removeEventListener('error', errorFn, false); + } + const loadFn = sounds[i]._loadFn; + if (loadFn) { + node.removeEventListener(Howler._canPlayEvent as string, loadFn, false); + } + const endFn = sounds[i]._endFn; + if (endFn) { + node.removeEventListener('ended', endFn, false); + } - Howler._releaseHtml5Audio(sounds[i]._node); + Howler._releaseHtml5Audio(node); } - delete sounds[i]._node; + sounds[i]._node = null; this._clearTimer(sounds[i]._id); } @@ -1609,9 +1702,8 @@ class Howl { return null; } - on(event: string, fn: (...args: any[]) => void, id?: number, once?: boolean): Howl { - - const events = (this as any)['_on' + event]; + on(event: string, fn: (...args: unknown[]) => void, id?: number, once?: boolean): Howl { + const events = (this as unknown as Record)[`_on${event}`]; if (typeof fn === 'function') { events.push(once ? { id, fn, once } : { id, fn }); @@ -1620,9 +1712,8 @@ class Howl { return this; } - off(event: string, fn?: (...args: any[]) => void, id?: number): Howl { - - const events = (this as any)['_on' + event]; + off(event: string, fn?: (...args: unknown[]) => void, id?: number): Howl { + const events = (this as unknown as Record)[`_on${event}`]; let i = 0; if (typeof fn === 'number') { @@ -1639,12 +1730,12 @@ class Howl { } } } else if (event) { - (this as any)['_on' + event] = []; + (this as unknown as Record)[`_on${event}`] = []; } else { const keys = Object.keys(this); for (i = 0; i < keys.length; i++) { - if (keys[i].indexOf('_on') === 0 && Array.isArray((this as any)[keys[i]])) { - (this as any)[keys[i]] = []; + if (keys[i].indexOf('_on') === 0 && Array.isArray((this as unknown as Record)[keys[i]])) { + (this as unknown as Record)[keys[i]] = []; } } } @@ -1652,7 +1743,7 @@ class Howl { return this; } - once(event: string, fn: (...args: any[]) => void, id?: number): Howl { + once(event: string, fn: (...args: unknown[]) => void, id?: number): Howl { this.on(event, fn, id, true); @@ -1661,7 +1752,7 @@ class Howl { } _emit(event: string, id?: number | null, msg?: string): Howl { - const events = (this as any)['_on' + event]; + const events = (this as unknown as Record)[`_on${event}`]; for (let i = events.length - 1; i >= 0; i--) { if (!events[i].id || events[i].id === id || event === 'load') { @@ -1704,7 +1795,7 @@ class Howl { const sprite = sound._sprite; - if (!this._webAudio && sound._node && !sound._node.paused && !sound._node.ended && sound._node.currentTime < sound._stop!) { + if (!this._webAudio && sound._node && isHTMLAudioElement(sound._node) && !sound._node.paused && !sound._node.ended && sound._node.currentTime < sound._stop!) { setTimeout(this._ended.bind(this, sound), 100); return this; } @@ -1812,8 +1903,9 @@ class Howl { } if (this._sounds[i]._ended) { - if (this._webAudio && this._sounds[i]._node) { - this._sounds[i]._node.disconnect(0); + const node = this._sounds[i]._node; + if (this._webAudio && node && isGainNode(node)) { + node.disconnect(0); } this._sounds.splice(i, 1); @@ -1838,10 +1930,13 @@ class Howl { } _refreshBuffer(sound: Sound): Howl { - + if (!sound._node || !isGainNode(sound._node) || !Howler.ctx) { + return this; + } - sound._node.bufferSource = Howler.ctx!.createBufferSource(); - sound._node.bufferSource.buffer = cache[this._src as string]; + sound._node.bufferSource = Howler.ctx.createBufferSource() as AudioBufferSourceNodeWithLegacy; + const src = typeof this._src === 'string' ? this._src : (Array.isArray(this._src) && this._src.length > 0 ? this._src[0] : ''); + sound._node.bufferSource.buffer = cache[src]; if (sound._panner) { sound._node.bufferSource.connect(sound._panner); @@ -1854,7 +1949,7 @@ class Howl { sound._node.bufferSource.loopStart = sound._start || 0; sound._node.bufferSource.loopEnd = sound._stop || 0; } - sound._node.bufferSource.playbackRate.setValueAtTime(sound._rate, Howler.ctx!.currentTime); + sound._node.bufferSource.playbackRate.setValueAtTime(sound._rate, Howler.ctx.currentTime); return this; } @@ -1881,7 +1976,7 @@ class Howl { return this; } - _clearSound(node: HTMLAudioElement): void { + _clearSound(node: HTMLAudioElementWithUnlocked): void { if (!isIE(Howler._navigator)) { node.src = 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA'; } diff --git a/src/plugins/spatial-plugin.ts b/src/plugins/spatial-plugin.ts index 4ba13a98..6c161d5f 100644 --- a/src/plugins/spatial-plugin.ts +++ b/src/plugins/spatial-plugin.ts @@ -11,6 +11,7 @@ import type { HowlOptions } from '../howler.core'; import { Howl, Howler, HowlerGlobal, Sound } from '../howler.core'; +import { isGainNode } from '../types'; import { HowlerPlugin, type PluginHooks, globalPluginManager } from './plugin'; /** @@ -177,7 +178,9 @@ function setupPanner(sound: Sound & SpatialSoundState, type: 'stereo' | 'spatial } // Connect panner to the sound's node - sound._panner.connect(sound._node); + if (sound._node && isGainNode(sound._node)) { + sound._panner.connect(sound._node); + } // Update connections if sound is playing if (!sound._paused) { diff --git a/src/types.ts b/src/types.ts index 043b38ad..bd46681b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -42,7 +42,7 @@ export interface HowlOptions { export interface EventListener { id?: number; - fn: (...args: any[]) => void; + fn: (...args: unknown[]) => void; once?: boolean; } @@ -53,3 +53,49 @@ export interface QueueItem { // Global audio context cache export const cache: Record = {}; + +// Type for HTML5 Audio element with custom properties +export interface HTMLAudioElementWithUnlocked extends HTMLAudioElement { + _unlocked?: boolean; +} + +// Type for AudioBufferSourceNode with legacy methods +export interface AudioBufferSourceNodeWithLegacy extends Omit { + noteOn?: (when: number) => void; + noteOff?: (when: number) => void; + noteGrainOn?: (when: number, grainOffset: number, grainDuration: number) => void; + loop?: boolean; + loopStart?: number | undefined; + loopEnd?: number | undefined; +} + +// Type for window with Audio constructor +export interface WindowWithAudio extends Window { + Audio: { + new (): HTMLAudioElement; + }; + ejecta?: unknown; +} + +// Type for Navigator with CocoonJS +export interface NavigatorWithCocoonJS extends Navigator { + isCocoonJS?: boolean; +} + +// Type for GainNode with bufferSource property +export interface GainNodeWithBufferSource extends GainNode { + bufferSource?: AudioBufferSourceNodeWithLegacy; +} + +// Type guards for audio node types +export function isHTMLAudioElement(node: HTMLAudioElementWithUnlocked | GainNodeWithBufferSource | null): node is HTMLAudioElementWithUnlocked { + return node !== null && + node instanceof HTMLAudioElement && + 'src' in node && + 'play' in node && + !('videoWidth' in node); +} + +export function isGainNode(node: HTMLAudioElementWithUnlocked | GainNodeWithBufferSource | null): node is GainNodeWithBufferSource { + return node !== null && 'gain' in node && 'connect' in node; +} From cea130e67ead6e47237879cd319d82093a8e41b5 Mon Sep 17 00:00:00 2001 From: eatsjobs Date: Sun, 23 Nov 2025 01:00:11 +0100 Subject: [PATCH 19/25] Add biome configuration and update dependencies - Introduced a new `biome.json` configuration file to manage project settings, including VCS, file includes, formatter, and linter options. - Updated `package.json` to include `@biomejs/biome` as a development dependency, ensuring compatibility with the new configuration. - Modified `package-lock.json` to reflect the addition of the new dependency and its associated metadata. - Ensured consistent formatting across various files by standardizing indentation and style. These changes aim to enhance project configuration management and maintain consistency in code formatting. --- biome.json | 34 + examples/3d/js/camera.js | 4 +- examples/3d/js/game.js | 3 +- examples/3d/js/howlerConfig.js | 5 +- examples/3d/js/sound.js | 158 +- examples/3d/js/texture.js | 10 +- examples/3d/styles.css | 36 +- examples/player/player.js | 685 +++--- examples/player/siriwave.js | 300 +-- examples/radio/radio.js | 292 ++- examples/sprite/sprite.js | 269 +- package-lock.json | 164 ++ package.json | 101 +- src/helpers/audio-context.ts | 76 +- src/helpers/audio-loader.ts | 207 +- src/helpers/index.ts | 24 +- src/helpers/light-ua-parser.ts | 69 +- src/howler.core.ts | 4240 +++++++++++++++++--------------- src/index.ts | 8 +- src/plugins/index.ts | 4 +- src/plugins/plugin.ts | 410 +-- src/plugins/spatial-plugin.ts | 1553 +++++++----- src/plugins/spatial.ts | 16 +- src/types.ts | 123 +- tests/css/styles.css | 148 +- tests/index.html | 6 +- tests/js/core.html5audio.js | 437 ++-- tests/js/core.webaudio.js | 467 ++-- tests/js/spatial.js | 199 +- tsconfig.json | 38 +- tsdown.config.ts | 26 +- 31 files changed, 5475 insertions(+), 4637 deletions(-) create mode 100644 biome.json diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..45ef3e26 --- /dev/null +++ b/biome.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.3.7/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "includes": ["src/**", "!!**/dist"] + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/examples/3d/js/camera.js b/examples/3d/js/camera.js index 84e9d6f6..8c95758d 100644 --- a/examples/3d/js/camera.js +++ b/examples/3d/js/camera.js @@ -43,7 +43,6 @@ export class Camera { ctx.fillRect(0, this.height * 0.5, this.width, this.height * 0.5); } ctx.restore(); - } /** @@ -64,7 +63,6 @@ export class Camera { ctx.restore(); } - /** * Draw a single column of the scene. @@ -171,4 +169,4 @@ export class Camera { this.drawCols(); this.drawHand(); } -} \ No newline at end of file +} diff --git a/examples/3d/js/game.js b/examples/3d/js/game.js index e1242648..599d1980 100644 --- a/examples/3d/js/game.js +++ b/examples/3d/js/game.js @@ -8,15 +8,14 @@ * MIT License */ -import { Howler } from "./howlerConfig.js"; import { Camera } from "./camera.js"; import { Controls } from "./controls.js"; +import { Howler } from "./howlerConfig.js"; import { Map } from "./map.js"; import { Player } from "./player.js"; import { Sound } from "./sound.js"; import { isMobile } from "./utils.js"; - /** * Main game class that runs the tick and sets up all other components. */ diff --git a/examples/3d/js/howlerConfig.js b/examples/3d/js/howlerConfig.js index d3aa553a..99a7c4f7 100644 --- a/examples/3d/js/howlerConfig.js +++ b/examples/3d/js/howlerConfig.js @@ -8,8 +8,8 @@ * MIT License */ -import { Howl, Howler } from 'howler'; -import { SpatialAudioPlugin } from 'howler/plugins/spatial'; +import { Howl, Howler } from "howler"; +import { SpatialAudioPlugin } from "howler/plugins/spatial"; const spatialAudioPlugin = new SpatialAudioPlugin(); // Register the spatial audio plugin on module initialization @@ -26,4 +26,3 @@ export { Howler }; * @type {new (...args: any[]) => import('howler/plugins/spatial').SpatialHowl} */ export { Howl }; - diff --git a/examples/3d/js/sound.js b/examples/3d/js/sound.js index fda379ac..12ed2853 100644 --- a/examples/3d/js/sound.js +++ b/examples/3d/js/sound.js @@ -8,91 +8,101 @@ * MIT License */ -import { Howl } from './howlerConfig.js'; -import { game } from './game.js'; +import { game } from "./game.js"; +import { Howl } from "./howlerConfig.js"; /** * Setup and control all of the game's audio. */ export class Sound { - constructor() { - // Setup the shared Howl with spatial audio capabilities (via plugin). - this.sound = new Howl({ - src: ['./assets/sprite.webm', './assets/sprite.mp3'], - sprite: { - lightning: [2000, 4147], - rain: [8000, 9962, true], - thunder: [19000, 13858], - music: [34000, 31994, true] - }, - volume: 0 - }); + constructor() { + // Setup the shared Howl with spatial audio capabilities (via plugin). + this.sound = new Howl({ + src: ["./assets/sprite.webm", "./assets/sprite.mp3"], + sprite: { + lightning: [2000, 4147], + rain: [8000, 9962, true], + thunder: [19000, 13858], + music: [34000, 31994, true], + }, + volume: 0, + }); - // Begin playing background sounds. - this.rain(); - this.thunder(); - } + // Begin playing background sounds. + this.rain(); + this.thunder(); + } - /** - * Play a rain loop in the background. - */ - rain() { - this._rain = this.sound.play('rain'); - this.sound.volume(0.2, this._rain); - } + /** + * Play a rain loop in the background. + */ + rain() { + this._rain = this.sound.play("rain"); + this.sound.volume(0.2, this._rain); + } - /** - * Randomly play thunder sounds periodically. - */ - thunder() { - setTimeout(() => { - // Play the thunder sound in a random position. - const x = Math.round(100 * (2 - (Math.random() * 4))) / 100; - const y = Math.round(100 * (2 - (Math.random() * 4))) / 100; - this._thunder = this.sound.play('thunder'); - this.sound.pos(x, y, -0.5, this._thunder); - this.sound.volume(1, this._thunder); + /** + * Randomly play thunder sounds periodically. + */ + thunder() { + setTimeout( + () => { + // Play the thunder sound in a random position. + const x = Math.round(100 * (2 - Math.random() * 4)) / 100; + const y = Math.round(100 * (2 - Math.random() * 4)) / 100; + this._thunder = this.sound.play("thunder"); + this.sound.pos(x, y, -0.5, this._thunder); + this.sound.volume(1, this._thunder); - // Schedule the next clap. - this.thunder(); - }, 5000 + Math.round(Math.random() * 15000)); - } + // Schedule the next clap. + this.thunder(); + }, + 5000 + Math.round(Math.random() * 15000), + ); + } - /** - * Play lightning in a random location with a random rate/pitch. - */ - lightning() { - const x = Math.round(100 * (2.5 - (Math.random() * 5))) / 100; - const y = Math.round(100 * (2.5 - (Math.random() * 5))) / 100; - const rate = Math.round(100 * (0.4 + (Math.random() * 1.25))) / 100; + /** + * Play lightning in a random location with a random rate/pitch. + */ + lightning() { + const x = Math.round(100 * (2.5 - Math.random() * 5)) / 100; + const y = Math.round(100 * (2.5 - Math.random() * 5)) / 100; + const rate = Math.round(100 * (0.4 + Math.random() * 1.25)) / 100; - // Play the lightning sound. - const id = this.sound.play('lightning'); + // Play the lightning sound. + const id = this.sound.play("lightning"); - // Change the position and rate. - this.sound.pos(x, y, -0.5, id); - this.sound.rate(rate, id); - this.sound.volume(1, id); - } + // Change the position and rate. + this.sound.pos(x, y, -0.5, id); + this.sound.rate(rate, id); + this.sound.volume(1, id); + } - /** - * Setup a speaker in 3D space to play music from. - * @param {Number} x x-tile position of speaker. - * @param {Number} y y-tile position of speaker. - */ - speaker(x, y) { - const soundId = game.audio.sound.play('music'); - this.sound.once('play', () => { - // Set the position of the speaker in 3D space. - this.sound.pos(x + 0.5, y + 0.5, -0.5, soundId); - this.sound.volume(1, soundId); + /** + * Setup a speaker in 3D space to play music from. + * @param {Number} x x-tile position of speaker. + * @param {Number} y y-tile position of speaker. + */ + speaker(x, y) { + const soundId = game.audio.sound.play("music"); + this.sound.once( + "play", + () => { + // Set the position of the speaker in 3D space. + this.sound.pos(x + 0.5, y + 0.5, -0.5, soundId); + this.sound.volume(1, soundId); - // Tweak the attributes to get the desired effect. - this.sound.pannerAttr({ - panningModel: 'HRTF', - refDistance: 0.8, - rolloffFactor: 2.5, - distanceModel: 'exponential' - }, soundId); - }, soundId); - } + // Tweak the attributes to get the desired effect. + this.sound.pannerAttr( + { + panningModel: "HRTF", + refDistance: 0.8, + rolloffFactor: 2.5, + distanceModel: "exponential", + }, + soundId, + ); + }, + soundId, + ); + } } diff --git a/examples/3d/js/texture.js b/examples/3d/js/texture.js index 69383a74..375e4dee 100644 --- a/examples/3d/js/texture.js +++ b/examples/3d/js/texture.js @@ -16,9 +16,9 @@ */ export class Texture { constructor(src, w, h) { - this.image = new Image(); - this.image.src = src; - this.width = w; - this.height = h; + this.image = new Image(); + this.image.src = src; + this.width = w; + this.height = h; } -} \ No newline at end of file +} diff --git a/examples/3d/styles.css b/examples/3d/styles.css index c65eaf27..0a920e68 100644 --- a/examples/3d/styles.css +++ b/examples/3d/styles.css @@ -1,25 +1,25 @@ html { - width: 100%; - height: 100%; - overflow: hidden; - padding: 0; - margin: 0; - outline: 0; + width: 100%; + height: 100%; + overflow: hidden; + padding: 0; + margin: 0; + outline: 0; } body { - width: 100%; - height: 100%; - padding: 0; - margin: 0; - overflow: hidden; - background: #000; - -webkit-user-select: none; - user-select: none; - -webkit-tap-highlight-color: rgba(255, 255, 255, 0); + width: 100%; + height: 100%; + padding: 0; + margin: 0; + overflow: hidden; + background: #000; + -webkit-user-select: none; + user-select: none; + -webkit-tap-highlight-color: rgba(255, 255, 255, 0); } #canvas { - width: 100%; - height: 100%; -} \ No newline at end of file + width: 100%; + height: 100%; +} diff --git a/examples/player/player.js b/examples/player/player.js index c686f2e3..2d756cdc 100644 --- a/examples/player/player.js +++ b/examples/player/player.js @@ -8,13 +8,33 @@ * MIT License */ -import { Howl, Howler } from 'howler'; -import { SiriWave } from './siriwave.js'; +import { Howl, Howler } from "howler"; +import { SiriWave } from "./siriwave.js"; // Cache references to DOM elements. -const elms = ['track', 'timer', 'duration', 'playBtn', 'pauseBtn', 'prevBtn', 'nextBtn', 'playlistBtn', 'volumeBtn', 'progress', 'bar', 'wave', 'loading', 'playlist', 'list', 'volume', 'barEmpty', 'barFull', 'sliderBtn']; -elms.forEach(function(elm) { - window[elm] = document.getElementById(elm); +const elms = [ + "track", + "timer", + "duration", + "playBtn", + "pauseBtn", + "prevBtn", + "nextBtn", + "playlistBtn", + "volumeBtn", + "progress", + "bar", + "wave", + "loading", + "playlist", + "list", + "volume", + "barEmpty", + "barFull", + "sliderBtn", +]; +elms.forEach((elm) => { + window[elm] = document.getElementById(elm); }); /** @@ -22,369 +42,368 @@ elms.forEach(function(elm) { * Includes all methods for playing, skipping, updating the display, etc. * @param {Array} playlist Array of objects with playlist song details ({title, file, howl}). */ -var Player = function(playlist) { - this.playlist = playlist; - this.index = 0; - - // Display the title of the first track. - track.innerHTML = '1. ' + playlist[0].title; - - // Setup the playlist display. - playlist.forEach(function(song) { - var div = document.createElement('div'); - div.className = 'list-song'; - div.innerHTML = song.title; - div.onclick = function() { - player.skipTo(playlist.indexOf(song)); - }; - list.appendChild(div); - }); +var Player = function (playlist) { + this.playlist = playlist; + this.index = 0; + + // Display the title of the first track. + track.innerHTML = "1. " + playlist[0].title; + + // Setup the playlist display. + playlist.forEach((song) => { + var div = document.createElement("div"); + div.className = "list-song"; + div.innerHTML = song.title; + div.onclick = () => { + player.skipTo(playlist.indexOf(song)); + }; + list.appendChild(div); + }); }; Player.prototype = { - /** - * Play a song in the playlist. - * @param {Number} index Index of the song in the playlist (leave empty to play the first or current). - */ - play: function(index) { - var self = this; - var sound; - - index = typeof index === 'number' ? index : self.index; - var data = self.playlist[index]; - - // If we already loaded this track, use the current one. - // Otherwise, setup and load a new Howl. - if (data.howl) { - sound = data.howl; - } else { - sound = data.howl = new Howl({ - src: ['./audio/' + data.file + '.webm', './audio/' + data.file + '.mp3'], - html5: true, // Force to HTML5 so that the audio can stream in (best for large files). - onplay: function() { - // Display the duration. - duration.innerHTML = self.formatTime(Math.round(sound.duration())); - - // Start updating the progress of the track. - requestAnimationFrame(self.step.bind(self)); - - // Start the wave animation if we have already loaded - wave.container.style.display = 'block'; - bar.style.display = 'none'; - pauseBtn.style.display = 'block'; - }, - onload: function() { - // Start the wave animation. - wave.container.style.display = 'block'; - bar.style.display = 'none'; - loading.style.display = 'none'; - }, - onend: function() { - // Stop the wave animation. - wave.container.style.display = 'none'; - bar.style.display = 'block'; - self.skip('next'); - }, - onpause: function() { - // Stop the wave animation. - wave.container.style.display = 'none'; - bar.style.display = 'block'; - }, - onstop: function() { - // Stop the wave animation. - wave.container.style.display = 'none'; - bar.style.display = 'block'; - }, - onseek: function() { - // Start updating the progress of the track. - requestAnimationFrame(self.step.bind(self)); - } - }); - } - - // Begin playing the sound. - sound.play(); - - // Update the track display. - track.innerHTML = (index + 1) + '. ' + data.title; - - // Show the pause button. - if (sound.state() === 'loaded') { - playBtn.style.display = 'none'; - pauseBtn.style.display = 'block'; - } else { - loading.style.display = 'block'; - playBtn.style.display = 'none'; - pauseBtn.style.display = 'none'; - } - - // Keep track of the index we are currently playing. - self.index = index; - }, - - /** - * Pause the currently playing track. - */ - pause: function() { - var self = this; - - // Get the Howl we want to manipulate. - var sound = self.playlist[self.index].howl; - - // Puase the sound. - sound.pause(); - - // Show the play button. - playBtn.style.display = 'block'; - pauseBtn.style.display = 'none'; - }, - - /** - * Skip to the next or previous track. - * @param {String} direction 'next' or 'prev'. - */ - skip: function(direction) { - var self = this; - - // Get the next track based on the direction of the track. - var index = 0; - if (direction === 'prev') { - index = self.index - 1; - if (index < 0) { - index = self.playlist.length - 1; - } - } else { - index = self.index + 1; - if (index >= self.playlist.length) { - index = 0; - } - } - - self.skipTo(index); - }, - - /** - * Skip to a specific track based on its playlist index. - * @param {Number} index Index in the playlist. - */ - skipTo: function(index) { - var self = this; - - // Stop the current track. - if (self.playlist[self.index].howl) { - self.playlist[self.index].howl.stop(); - } - - // Reset progress. - progress.style.width = '0%'; - - // Play the new track. - self.play(index); - }, - - /** - * Set the volume and update the volume slider display. - * @param {Number} val Volume between 0 and 1. - */ - volume: function(val) { - var self = this; - - // Update the global volume (affecting all Howls). - Howler.volume(val); - - // Update the display on the slider. - var barWidth = (val * 90) / 100; - barFull.style.width = (barWidth * 100) + '%'; - sliderBtn.style.left = (window.innerWidth * barWidth + window.innerWidth * 0.05 - 25) + 'px'; - }, - - /** - * Seek to a new position in the currently playing track. - * @param {Number} per Percentage through the song to skip. - */ - seek: function(per) { - var self = this; - - // Get the Howl we want to manipulate. - var sound = self.playlist[self.index].howl; - - // Convert the percent into a seek position. - if (sound.playing()) { - sound.seek(sound.duration() * per); - } - }, - - /** - * The step called within requestAnimationFrame to update the playback position. - */ - step: function() { - var self = this; - - // Get the Howl we want to manipulate. - var sound = self.playlist[self.index].howl; - - // Determine our current seek position. - var seek = sound.seek() || 0; - timer.innerHTML = self.formatTime(Math.round(seek)); - progress.style.width = (((seek / sound.duration()) * 100) || 0) + '%'; - - // If the sound is still playing, continue stepping. - if (sound.playing()) { - requestAnimationFrame(self.step.bind(self)); - } - }, - - /** - * Toggle the playlist display on/off. - */ - togglePlaylist: function() { - var self = this; - var display = (playlist.style.display === 'block') ? 'none' : 'block'; - - setTimeout(function() { - playlist.style.display = display; - }, (display === 'block') ? 0 : 500); - playlist.className = (display === 'block') ? 'fadein' : 'fadeout'; - }, - - /** - * Toggle the volume display on/off. - */ - toggleVolume: function() { - var self = this; - var display = (volume.style.display === 'block') ? 'none' : 'block'; - - setTimeout(function() { - volume.style.display = display; - }, (display === 'block') ? 0 : 500); - volume.className = (display === 'block') ? 'fadein' : 'fadeout'; - }, - - /** - * Format the time from seconds to M:SS. - * @param {Number} secs Seconds to format. - * @return {String} Formatted time. - */ - formatTime: function(secs) { - var minutes = Math.floor(secs / 60) || 0; - var seconds = (secs - minutes * 60) || 0; - - return minutes + ':' + (seconds < 10 ? '0' : '') + seconds; - } + /** + * Play a song in the playlist. + * @param {Number} index Index of the song in the playlist (leave empty to play the first or current). + */ + play: function (index) { + var sound; + + index = typeof index === "number" ? index : this.index; + var data = this.playlist[index]; + + // If we already loaded this track, use the current one. + // Otherwise, setup and load a new Howl. + if (data.howl) { + sound = data.howl; + } else { + sound = data.howl = new Howl({ + src: [ + "./audio/" + data.file + ".webm", + "./audio/" + data.file + ".mp3", + ], + html5: true, // Force to HTML5 so that the audio can stream in (best for large files). + onplay: () => { + // Display the duration. + duration.innerHTML = this.formatTime(Math.round(sound.duration())); + + // Start updating the progress of the track. + requestAnimationFrame(this.step.bind(this)); + + // Start the wave animation if we have already loaded + wave.container.style.display = "block"; + bar.style.display = "none"; + pauseBtn.style.display = "block"; + }, + onload: () => { + // Start the wave animation. + wave.container.style.display = "block"; + bar.style.display = "none"; + loading.style.display = "none"; + }, + onend: () => { + // Stop the wave animation. + wave.container.style.display = "none"; + bar.style.display = "block"; + this.skip("next"); + }, + onpause: () => { + // Stop the wave animation. + wave.container.style.display = "none"; + bar.style.display = "block"; + }, + onstop: () => { + // Stop the wave animation. + wave.container.style.display = "none"; + bar.style.display = "block"; + }, + onseek: () => { + // Start updating the progress of the track. + requestAnimationFrame(this.step.bind(this)); + }, + }); + } + + // Begin playing the sound. + sound.play(); + + // Update the track display. + track.innerHTML = index + 1 + ". " + data.title; + + // Show the pause button. + if (sound.state() === "loaded") { + playBtn.style.display = "none"; + pauseBtn.style.display = "block"; + } else { + loading.style.display = "block"; + playBtn.style.display = "none"; + pauseBtn.style.display = "none"; + } + + // Keep track of the index we are currently playing. + this.index = index; + }, + + /** + * Pause the currently playing track. + */ + pause: function () { + // Get the Howl we want to manipulate. + var sound = this.playlist[this.index].howl; + + // Puase the sound. + sound.pause(); + + // Show the play button. + playBtn.style.display = "block"; + pauseBtn.style.display = "none"; + }, + + /** + * Skip to the next or previous track. + * @param {String} direction 'next' or 'prev'. + */ + skip: function (direction) { + // Get the next track based on the direction of the track. + var index = 0; + if (direction === "prev") { + index = this.index - 1; + if (index < 0) { + index = this.playlist.length - 1; + } + } else { + index = this.index + 1; + if (index >= this.playlist.length) { + index = 0; + } + } + + this.skipTo(index); + }, + + /** + * Skip to a specific track based on its playlist index. + * @param {Number} index Index in the playlist. + */ + skipTo: function (index) { + // Stop the current track. + if (this.playlist[this.index].howl) { + this.playlist[this.index].howl.stop(); + } + + // Reset progress. + progress.style.width = "0%"; + + // Play the new track. + this.play(index); + }, + + /** + * Set the volume and update the volume slider display. + * @param {Number} val Volume between 0 and 1. + */ + volume: (val) => { + // Update the global volume (affecting all Howls). + Howler.volume(val); + + // Update the display on the slider. + var barWidth = (val * 90) / 100; + barFull.style.width = barWidth * 100 + "%"; + sliderBtn.style.left = + window.innerWidth * barWidth + window.innerWidth * 0.05 - 25 + "px"; + }, + + /** + * Seek to a new position in the currently playing track. + * @param {Number} per Percentage through the song to skip. + */ + seek: function (per) { + // Get the Howl we want to manipulate. + var sound = this.playlist[this.index].howl; + + // Convert the percent into a seek position. + if (sound.playing()) { + sound.seek(sound.duration() * per); + } + }, + + /** + * The step called within requestAnimationFrame to update the playback position. + */ + step: function () { + // Get the Howl we want to manipulate. + var sound = this.playlist[this.index].howl; + + // Determine our current seek position. + var seek = sound.seek() || 0; + timer.innerHTML = this.formatTime(Math.round(seek)); + progress.style.width = ((seek / sound.duration()) * 100 || 0) + "%"; + + // If the sound is still playing, continue stepping. + if (sound.playing()) { + requestAnimationFrame(this.step.bind(this)); + } + }, + + /** + * Toggle the playlist display on/off. + */ + togglePlaylist: () => { + var display = playlist.style.display === "block" ? "none" : "block"; + + setTimeout( + () => { + playlist.style.display = display; + }, + display === "block" ? 0 : 500, + ); + playlist.className = display === "block" ? "fadein" : "fadeout"; + }, + + /** + * Toggle the volume display on/off. + */ + toggleVolume: () => { + var display = volume.style.display === "block" ? "none" : "block"; + + setTimeout( + () => { + volume.style.display = display; + }, + display === "block" ? 0 : 500, + ); + volume.className = display === "block" ? "fadein" : "fadeout"; + }, + + /** + * Format the time from seconds to M:SS. + * @param {Number} secs Seconds to format. + * @return {String} Formatted time. + */ + formatTime: (secs) => { + var minutes = Math.floor(secs / 60) || 0; + var seconds = secs - minutes * 60 || 0; + + return minutes + ":" + (seconds < 10 ? "0" : "") + seconds; + }, }; // Setup our new audio player class and pass it the playlist. var player = new Player([ - { - title: 'Rave Digger', - file: 'rave_digger', - howl: null - }, - { - title: '80s Vibe', - file: '80s_vibe', - howl: null - }, - { - title: 'Running Out', - file: 'running_out', - howl: null - } + { + title: "Rave Digger", + file: "rave_digger", + howl: null, + }, + { + title: "80s Vibe", + file: "80s_vibe", + howl: null, + }, + { + title: "Running Out", + file: "running_out", + howl: null, + }, ]); // Bind our player controls. -playBtn.addEventListener('click', function() { - player.play(); +playBtn.addEventListener("click", () => { + player.play(); }); -pauseBtn.addEventListener('click', function() { - player.pause(); +pauseBtn.addEventListener("click", () => { + player.pause(); }); -prevBtn.addEventListener('click', function() { - player.skip('prev'); +prevBtn.addEventListener("click", () => { + player.skip("prev"); }); -nextBtn.addEventListener('click', function() { - player.skip('next'); +nextBtn.addEventListener("click", () => { + player.skip("next"); }); -waveform.addEventListener('click', function(event) { - player.seek(event.clientX / window.innerWidth); +waveform.addEventListener("click", (event) => { + player.seek(event.clientX / window.innerWidth); }); -playlistBtn.addEventListener('click', function() { - player.togglePlaylist(); +playlistBtn.addEventListener("click", () => { + player.togglePlaylist(); }); -playlist.addEventListener('click', function() { - player.togglePlaylist(); +playlist.addEventListener("click", () => { + player.togglePlaylist(); }); -volumeBtn.addEventListener('click', function() { - player.toggleVolume(); +volumeBtn.addEventListener("click", () => { + player.toggleVolume(); }); -volume.addEventListener('click', function() { - player.toggleVolume(); +volume.addEventListener("click", () => { + player.toggleVolume(); }); // Setup the event listeners to enable dragging of volume slider. -barEmpty.addEventListener('click', function(event) { - var per = event.layerX / parseFloat(barEmpty.scrollWidth); - player.volume(per); +barEmpty.addEventListener("click", (event) => { + var per = event.layerX / parseFloat(barEmpty.scrollWidth); + player.volume(per); }); -sliderBtn.addEventListener('mousedown', function() { - window.sliderDown = true; +sliderBtn.addEventListener("mousedown", () => { + window.sliderDown = true; }); -sliderBtn.addEventListener('touchstart', function() { - window.sliderDown = true; +sliderBtn.addEventListener("touchstart", () => { + window.sliderDown = true; }); -volume.addEventListener('mouseup', function() { - window.sliderDown = false; +volume.addEventListener("mouseup", () => { + window.sliderDown = false; }); -volume.addEventListener('touchend', function() { - window.sliderDown = false; +volume.addEventListener("touchend", () => { + window.sliderDown = false; }); -var move = function(event) { - if (window.sliderDown) { - var x = event.clientX || event.touches[0].clientX; - var startX = window.innerWidth * 0.05; - var layerX = x - startX; - var per = Math.min(1, Math.max(0, layerX / parseFloat(barEmpty.scrollWidth))); - player.volume(per); - } +var move = (event) => { + if (window.sliderDown) { + var x = event.clientX || event.touches[0].clientX; + var startX = window.innerWidth * 0.05; + var layerX = x - startX; + var per = Math.min( + 1, + Math.max(0, layerX / parseFloat(barEmpty.scrollWidth)), + ); + player.volume(per); + } }; -volume.addEventListener('mousemove', move); -volume.addEventListener('touchmove', move); +volume.addEventListener("mousemove", move); +volume.addEventListener("touchmove", move); // Setup the "waveform" animation. var wave = new SiriWave({ - container: waveform, - width: window.innerWidth, - height: window.innerHeight * 0.3, - cover: true, - speed: 0.03, - amplitude: 0.7, - frequency: 2 + container: waveform, + width: window.innerWidth, + height: window.innerHeight * 0.3, + cover: true, + speed: 0.03, + amplitude: 0.7, + frequency: 2, }); wave.start(); // Update the height of the wave animation. // These are basically some hacks to get SiriWave.js to do what we want. -var resize = function() { - var height = window.innerHeight * 0.3; - var width = window.innerWidth; - wave.height = height; - wave.height_2 = height / 2; - wave.MAX = wave.height_2 - 4; - wave.width = width; - wave.width_2 = width / 2; - wave.width_4 = width / 4; - wave.canvas.height = height; - wave.canvas.width = width; - wave.container.style.margin = -(height / 2) + 'px auto'; - - // Update the position of the slider. - var sound = player.playlist[player.index].howl; - if (sound) { - var vol = sound.volume(); - var barWidth = (vol * 0.9); - sliderBtn.style.left = (window.innerWidth * barWidth + window.innerWidth * 0.05 - 25) + 'px'; - } +var resize = () => { + var height = window.innerHeight * 0.3; + var width = window.innerWidth; + wave.height = height; + wave.height_2 = height / 2; + wave.MAX = wave.height_2 - 4; + wave.width = width; + wave.width_2 = width / 2; + wave.width_4 = width / 4; + wave.canvas.height = height; + wave.canvas.width = width; + wave.container.style.margin = -(height / 2) + "px auto"; + + // Update the position of the slider. + var sound = player.playlist[player.index].howl; + if (sound) { + var vol = sound.volume(); + var barWidth = vol * 0.9; + sliderBtn.style.left = + window.innerWidth * barWidth + window.innerWidth * 0.05 - 25 + "px"; + } }; -window.addEventListener('resize', resize); +window.addEventListener("resize", resize); resize(); diff --git a/examples/player/siriwave.js b/examples/player/siriwave.js index 22d6c226..e30ae19a 100644 --- a/examples/player/siriwave.js +++ b/examples/player/siriwave.js @@ -1,149 +1,159 @@ /* Modified from https://github.com/CaffeinaLab/SiriWaveJS */ export class SiriWave { - constructor(opt) { - opt = opt || {}; - - this.phase = 0; - this.run = false; - - // UI vars - - this.ratio = opt.ratio || window.devicePixelRatio || 1; - - this.width = this.ratio * (opt.width || 320); - this.width_2 = this.width / 2; - this.width_4 = this.width / 4; - - this.height = this.ratio * (opt.height || 100); - this.height_2 = this.height / 2; - - this.MAX = (this.height_2) - 4; - - // Constructor opt - - this.amplitude = opt.amplitude || 1; - this.speed = opt.speed || 0.2; - this.frequency = opt.frequency || 6; - - this._draw = this._draw.bind(this); - this.color = (() => { - const hex2rgb = (hex) => { - const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; - hex = hex.replace(shorthandRegex, (m,r,g,b) => r + r + g + g + b + b); - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result ? - parseInt(result[1],16).toString()+','+parseInt(result[2], 16).toString()+','+parseInt(result[3], 16).toString() - : null; - }; - return hex2rgb(opt.color || '#fff') || '255,255,255'; - })(); - - // Canvas - - this.canvas = document.createElement('canvas'); - this.canvas.width = this.width; - this.canvas.height = this.height; - if (opt.cover) { - this.canvas.style.width = this.canvas.style.height = '100%'; - } else { - this.canvas.style.width = (this.width / this.ratio) + 'px'; - this.canvas.style.height = (this.height / this.ratio) + 'px'; - } - - this.container = opt.container || document.body; - this.container.appendChild(this.canvas); - - this.ctx = this.canvas.getContext('2d'); - - // Start - - if (opt.autostart) { - this.start(); - } - } - - _GATF_cache = {}; - - _globAttFunc(x) { - if (this._GATF_cache[x] == null) { - this._GATF_cache[x] = Math.pow(4/(4+Math.pow(x,4)), 4); - } - return this._GATF_cache[x]; - } - - _xpos(i) { - return this.width_2 + i * this.width_4; - } - - _ypos(i, attenuation) { - const att = (this.MAX * this.amplitude) / attenuation; - return this.height_2 + this._globAttFunc(i) * att * Math.sin(this.frequency * i - this.phase); - } - - _drawLine(attenuation, color, width) { - this.ctx.moveTo(0,0); - this.ctx.beginPath(); - this.ctx.strokeStyle = color; - this.ctx.lineWidth = width || 1; - - let i = -2; - while ((i += 0.01) <= 2) { - let y = this._ypos(i, attenuation); - if (Math.abs(i) >= 1.90) y = this.height_2; - this.ctx.lineTo(this._xpos(i), y); - } - - this.ctx.stroke(); - } - - _clear() { - this.ctx.globalCompositeOperation = 'destination-out'; - this.ctx.fillRect(0, 0, this.width, this.height); - this.ctx.globalCompositeOperation = 'source-over'; - } - - _draw() { - if (this.run === false) return; - - this.phase = (this.phase + Math.PI*this.speed) % (2*Math.PI); - - this._clear(); - this._drawLine(-2, 'rgba(' + this.color + ',0.1)'); - this._drawLine(-6, 'rgba(' + this.color + ',0.2)'); - this._drawLine(4, 'rgba(' + this.color + ',0.4)'); - this._drawLine(2, 'rgba(' + this.color + ',0.6)'); - this._drawLine(1, 'rgba(' + this.color + ',1)', 1.5); - - if (window.requestAnimationFrame) { - requestAnimationFrame(this._draw); - return; - } - setTimeout(this._draw, 20); - } - - /* API */ - - start() { - this.phase = 0; - this.run = true; - this._draw(); - } - - stop() { - this.phase = 0; - this.run = false; - } - - setSpeed(v) { - this.speed = v; - } - - setNoise(v) { - this.amplitude = Math.max(Math.min(v, 1), 0); - } - - setAmplitude(v) { - this.setNoise(v); - } + constructor(opt) { + opt = opt || {}; + + this.phase = 0; + this.run = false; + + // UI vars + + this.ratio = opt.ratio || window.devicePixelRatio || 1; + + this.width = this.ratio * (opt.width || 320); + this.width_2 = this.width / 2; + this.width_4 = this.width / 4; + + this.height = this.ratio * (opt.height || 100); + this.height_2 = this.height / 2; + + this.MAX = this.height_2 - 4; + + // Constructor opt + + this.amplitude = opt.amplitude || 1; + this.speed = opt.speed || 0.2; + this.frequency = opt.frequency || 6; + + this._draw = this._draw.bind(this); + this.color = (() => { + const hex2rgb = (hex) => { + const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; + hex = hex.replace( + shorthandRegex, + (m, r, g, b) => r + r + g + g + b + b, + ); + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result + ? parseInt(result[1], 16).toString() + + "," + + parseInt(result[2], 16).toString() + + "," + + parseInt(result[3], 16).toString() + : null; + }; + return hex2rgb(opt.color || "#fff") || "255,255,255"; + })(); + + // Canvas + + this.canvas = document.createElement("canvas"); + this.canvas.width = this.width; + this.canvas.height = this.height; + if (opt.cover) { + this.canvas.style.width = this.canvas.style.height = "100%"; + } else { + this.canvas.style.width = this.width / this.ratio + "px"; + this.canvas.style.height = this.height / this.ratio + "px"; + } + + this.container = opt.container || document.body; + this.container.appendChild(this.canvas); + + this.ctx = this.canvas.getContext("2d"); + + // Start + + if (opt.autostart) { + this.start(); + } + } + + _GATF_cache = {}; + + _globAttFunc(x) { + if (this._GATF_cache[x] == null) { + this._GATF_cache[x] = (4 / (4 + x ** 4)) ** 4; + } + return this._GATF_cache[x]; + } + + _xpos(i) { + return this.width_2 + i * this.width_4; + } + + _ypos(i, attenuation) { + const att = (this.MAX * this.amplitude) / attenuation; + return ( + this.height_2 + + this._globAttFunc(i) * att * Math.sin(this.frequency * i - this.phase) + ); + } + + _drawLine(attenuation, color, width) { + this.ctx.moveTo(0, 0); + this.ctx.beginPath(); + this.ctx.strokeStyle = color; + this.ctx.lineWidth = width || 1; + + let i = -2; + while ((i += 0.01) <= 2) { + let y = this._ypos(i, attenuation); + if (Math.abs(i) >= 1.9) y = this.height_2; + this.ctx.lineTo(this._xpos(i), y); + } + + this.ctx.stroke(); + } + + _clear() { + this.ctx.globalCompositeOperation = "destination-out"; + this.ctx.fillRect(0, 0, this.width, this.height); + this.ctx.globalCompositeOperation = "source-over"; + } + + _draw() { + if (this.run === false) return; + + this.phase = (this.phase + Math.PI * this.speed) % (2 * Math.PI); + + this._clear(); + this._drawLine(-2, "rgba(" + this.color + ",0.1)"); + this._drawLine(-6, "rgba(" + this.color + ",0.2)"); + this._drawLine(4, "rgba(" + this.color + ",0.4)"); + this._drawLine(2, "rgba(" + this.color + ",0.6)"); + this._drawLine(1, "rgba(" + this.color + ",1)", 1.5); + + if (window.requestAnimationFrame) { + requestAnimationFrame(this._draw); + return; + } + setTimeout(this._draw, 20); + } + + /* API */ + + start() { + this.phase = 0; + this.run = true; + this._draw(); + } + + stop() { + this.phase = 0; + this.run = false; + } + + setSpeed(v) { + this.speed = v; + } + + setNoise(v) { + this.amplitude = Math.max(Math.min(v, 1), 0); + } + + setAmplitude(v) { + this.setNoise(v); + } } diff --git a/examples/radio/radio.js b/examples/radio/radio.js index 2ee6e65a..b6c9f1fd 100644 --- a/examples/radio/radio.js +++ b/examples/radio/radio.js @@ -8,23 +8,44 @@ * MIT License */ -import { Howl, Howler } from 'howler'; +import { Howl, Howler } from "howler"; // Patch Howler's HTML5 audio element creation to set crossOrigin for CORS support const originalObtainHtml5Audio = Howler._obtainHtml5Audio; -Howler._obtainHtml5Audio = function() { - const audio = originalObtainHtml5Audio.call(this); - // Set crossOrigin before src is set to enable CORS - if (audio && audio.crossOrigin !== undefined) { - audio.crossOrigin = 'anonymous'; - } - return audio; +Howler._obtainHtml5Audio = function () { + const audio = originalObtainHtml5Audio.call(this); + // Set crossOrigin before src is set to enable CORS + if (audio && audio.crossOrigin !== undefined) { + audio.crossOrigin = "anonymous"; + } + return audio; }; // Cache references to DOM elements. -const elms = ['station0', 'title0', 'live0', 'playing0', 'station1', 'title1', 'live1', 'playing1', 'station2', 'title2', 'live2', 'playing2', 'station3', 'title3', 'live3', 'playing3', 'station4', 'title4', 'live4', 'playing4']; -elms.forEach(function(elm) { - window[elm] = document.getElementById(elm); +const elms = [ + "station0", + "title0", + "live0", + "playing0", + "station1", + "title1", + "live1", + "playing1", + "station2", + "title2", + "live2", + "playing2", + "station3", + "title3", + "live3", + "playing3", + "station4", + "title4", + "live4", + "playing4", +]; +elms.forEach((elm) => { + window[elm] = document.getElementById(elm); }); /** @@ -33,132 +54,135 @@ elms.forEach(function(elm) { * @param {Array} stations Array of objects with station details ({title, src, howl, ...}). */ class Radio { - constructor(stations) { - this.stations = stations; - this.index = 0; - - // Setup the display for each station. - for (let i = 0; i < this.stations.length; i++) { - window['title' + i].innerHTML = '' + this.stations[i].freq + ' ' + this.stations[i].title; - window['station' + i].addEventListener('click', () => { - const isNotPlaying = (this.stations[i].howl && !this.stations[i].howl.playing()); - - // Stop other sounds or the current one. - radio.stop(); - - // If the station isn't already playing or it doesn't exist, play it. - if (isNotPlaying || !this.stations[i].howl) { - radio.play(i); - } - }); - } - } - - /** - * Play a station with a specific index. - * @param {Number} index Index in the array of stations. - */ - play(index) { - let sound; - - index = typeof index === 'number' ? index : this.index; - const data = this.stations[index]; - - // If we already loaded this track, use the current one. - // Otherwise, setup and load a new Howl. - if (data.howl) { - sound = data.howl; - } else { - sound = data.howl = new Howl({ - src: data.src, - html5: true, // A live stream can only be played through HTML5 Audio. - format: ['mp3', 'aac'], - onloaderror: function(id, error) { - console.error('Error loading stream:', error); - alert('Failed to load radio stream. Please try another station.'); - }, - onplayerror: function(id, error) { - console.error('Error playing stream:', error); - alert('Failed to play radio stream. Please try another station.'); - } - }); - - } - - // Begin playing the sound. - sound.play(); - - // Toggle the display. - this.toggleStationDisplay(index, true); - - // Keep track of the index we are currently playing. - this.index = index; - } - - /** - * Stop a station's live stream. - */ - stop() { - // Get the Howl we want to manipulate. - const sound = this.stations[this.index].howl; - - // Toggle the display. - this.toggleStationDisplay(this.index, false); - - // Stop the sound. - if (sound) { - sound.unload(); - } - } - - /** - * Toggle the display of a station to off/on. - * @param {Number} index Index of the station to toggle. - * @param {Boolean} state true is on and false is off. - */ - toggleStationDisplay(index, state) { - // Highlight/un-highlight the row. - window['station' + index].style.backgroundColor = state ? 'rgba(255, 255, 255, 0.33)' : ''; - - // Show/hide the "live" marker. - window['live' + index].style.opacity = state ? 1 : 0; - - // Show/hide the "playing" animation. - window['playing' + index].style.display = state ? 'block' : 'none'; - } + constructor(stations) { + this.stations = stations; + this.index = 0; + + // Setup the display for each station. + for (let i = 0; i < this.stations.length; i++) { + window["title" + i].innerHTML = + "" + this.stations[i].freq + " " + this.stations[i].title; + window["station" + i].addEventListener("click", () => { + const isNotPlaying = + this.stations[i].howl && !this.stations[i].howl.playing(); + + // Stop other sounds or the current one. + radio.stop(); + + // If the station isn't already playing or it doesn't exist, play it. + if (isNotPlaying || !this.stations[i].howl) { + radio.play(i); + } + }); + } + } + + /** + * Play a station with a specific index. + * @param {Number} index Index in the array of stations. + */ + play(index) { + let sound; + + index = typeof index === "number" ? index : this.index; + const data = this.stations[index]; + + // If we already loaded this track, use the current one. + // Otherwise, setup and load a new Howl. + if (data.howl) { + sound = data.howl; + } else { + sound = data.howl = new Howl({ + src: data.src, + html5: true, // A live stream can only be played through HTML5 Audio. + format: ["mp3", "aac"], + onloaderror: (id, error) => { + console.error("Error loading stream:", error); + alert("Failed to load radio stream. Please try another station."); + }, + onplayerror: (id, error) => { + console.error("Error playing stream:", error); + alert("Failed to play radio stream. Please try another station."); + }, + }); + } + + // Begin playing the sound. + sound.play(); + + // Toggle the display. + this.toggleStationDisplay(index, true); + + // Keep track of the index we are currently playing. + this.index = index; + } + + /** + * Stop a station's live stream. + */ + stop() { + // Get the Howl we want to manipulate. + const sound = this.stations[this.index].howl; + + // Toggle the display. + this.toggleStationDisplay(this.index, false); + + // Stop the sound. + if (sound) { + sound.unload(); + } + } + + /** + * Toggle the display of a station to off/on. + * @param {Number} index Index of the station to toggle. + * @param {Boolean} state true is on and false is off. + */ + toggleStationDisplay(index, state) { + // Highlight/un-highlight the row. + window["station" + index].style.backgroundColor = state + ? "rgba(255, 255, 255, 0.33)" + : ""; + + // Show/hide the "live" marker. + window["live" + index].style.opacity = state ? 1 : 0; + + // Show/hide the "playing" animation. + window["playing" + index].style.display = state ? "block" : "none"; + } } // Setup our new radio and pass in the stations. // Using streams that are known to work with browser-based players const radio = new Radio([ - { - freq: '81.4', - title: "Chill Out Zone", - src: 'https://streams.fluxfm.de/Chillout/mp3-320/streams.fluxfm.de/', - howl: null - }, - { - freq: '89.9', - title: "Radio Paradise", - src: 'https://stream.radioparadise.com/mp3-128', - howl: null - }, - { - freq: '98.9', - title: "Smooth Jazz", - src: 'https://streams.fluxfm.de/SmoothJazz/mp3-320/streams.fluxfm.de/', - howl: null - }, - { - freq: '103.3', - title: "Classic Rock", - src: 'https://streams.fluxfm.de/ClassicRock/mp3-320/streams.fluxfm.de/', - howl: null - }, - { - freq: '107.7', - title: "Radio Paradise (Alternative)", - src: 'https://stream.radioparadise.com/aac-128', - howl: null - } + { + freq: "81.4", + title: "Chill Out Zone", + src: "https://streams.fluxfm.de/Chillout/mp3-320/streams.fluxfm.de/", + howl: null, + }, + { + freq: "89.9", + title: "Radio Paradise", + src: "https://stream.radioparadise.com/mp3-128", + howl: null, + }, + { + freq: "98.9", + title: "Smooth Jazz", + src: "https://streams.fluxfm.de/SmoothJazz/mp3-320/streams.fluxfm.de/", + howl: null, + }, + { + freq: "103.3", + title: "Classic Rock", + src: "https://streams.fluxfm.de/ClassicRock/mp3-320/streams.fluxfm.de/", + howl: null, + }, + { + freq: "107.7", + title: "Radio Paradise (Alternative)", + src: "https://stream.radioparadise.com/aac-128", + howl: null, + }, ]); diff --git a/examples/sprite/sprite.js b/examples/sprite/sprite.js index ea5c40a4..eb5ea802 100644 --- a/examples/sprite/sprite.js +++ b/examples/sprite/sprite.js @@ -8,146 +8,159 @@ * MIT License */ -import { Howl } from 'howler'; +import { Howl } from "howler"; // Cache references to DOM elements. -const elms = ['waveform', 'sprite0', 'sprite1', 'sprite2', 'sprite3', 'sprite4', 'sprite5']; -elms.forEach(function(elm) { - window[elm] = document.getElementById(elm); +const elms = [ + "waveform", + "sprite0", + "sprite1", + "sprite2", + "sprite3", + "sprite4", + "sprite5", +]; +elms.forEach((elm) => { + window[elm] = document.getElementById(elm); }); /** * Sprite class containing the state of our sprites to play and their progress. * @param {Object} options Settings to pass into and setup the sound and visuals. */ -var Sprite = function(options) { - var self = this; - - self.sounds = []; - - // Setup the options to define this sprite display. - self._width = options.width; - self._left = options.left; - self._spriteMap = options.spriteMap; - self._sprite = options.sprite; - self.setupListeners(); - - // Create our audio sprite definition. - self.sound = new Howl({ - src: options.src, - sprite: options.sprite - }); - - // Setup a resize event and fire it to setup our sprite overlays. - window.addEventListener('resize', function() { - self.resize(); - }, false); - self.resize(); - - // Begin the progress step tick. - requestAnimationFrame(self.step.bind(self)); +var Sprite = function (options) { + this.sounds = []; + + // Setup the options to define this sprite display. + this._width = options.width; + this._left = options.left; + this._spriteMap = options.spriteMap; + this._sprite = options.sprite; + this.setupListeners(); + + // Create our audio sprite definition. + this.sound = new Howl({ + src: options.src, + sprite: options.sprite, + }); + + // Setup a resize event and fire it to setup our sprite overlays. + window.addEventListener( + "resize", + () => { + this.resize(); + }, + false, + ); + this.resize(); + + // Begin the progress step tick. + requestAnimationFrame(this.step.bind(this)); }; Sprite.prototype = { - /** - * Setup the listeners for each sprite click area. - */ - setupListeners: function() { - var self = this; - var keys = Object.keys(self._spriteMap); - - keys.forEach(function(key) { - window[key].addEventListener('click', function() { - self.play(key); - }, false); - }); - }, - - /** - * Play a sprite when clicked and track the progress. - * @param {String} key Key in the sprite map object. - */ - play: function(key) { - var self = this; - var sprite = self._spriteMap[key]; - - // Play the sprite sound and capture the ID. - var id = self.sound.play(sprite); - - // Create a progress element and begin visually tracking it. - var elm = document.createElement('div'); - elm.className = 'progress'; - elm.id = id; - elm.dataset.sprite = sprite; - window[key].appendChild(elm); - self.sounds.push(elm); - - // When this sound is finished, remove the progress element. - self.sound.once('end', function() { - var index = self.sounds.indexOf(elm); - if (index >= 0) { - self.sounds.splice(index, 1); - window[key].removeChild(elm); - } - }, id); - }, - - /** - * Called on window resize to correctly position and size the click overlays. - */ - resize: function() { - var self = this; - - // Calculate the scale of our window from "full" size. - var scale = window.innerWidth / 3600; - - // Resize and reposition the sprite overlays. - var keys = Object.keys(self._spriteMap); - for (var i=0; i { + window[key].addEventListener( + "click", + () => { + this.play(key); + }, + false, + ); + }); + }, + + /** + * Play a sprite when clicked and track the progress. + * @param {String} key Key in the sprite map object. + */ + play: function (key) { + var sprite = this._spriteMap[key]; + + // Play the sprite sound and capture the ID. + var id = this.sound.play(sprite); + + // Create a progress element and begin visually tracking it. + var elm = document.createElement("div"); + elm.className = "progress"; + elm.id = id; + elm.dataset.sprite = sprite; + window[key].appendChild(elm); + this.sounds.push(elm); + + // When this sound is finished, remove the progress element. + this.sound.once( + "end", + () => { + var index = this.sounds.indexOf(elm); + if (index >= 0) { + this.sounds.splice(index, 1); + window[key].removeChild(elm); + } + }, + id, + ); + }, + + /** + * Called on window resize to correctly position and size the click overlays. + */ + resize: function () { + // Calculate the scale of our window from "full" size. + var scale = window.innerWidth / 3600; + + // Resize and reposition the sprite overlays. + var keys = Object.keys(this._spriteMap); + for (var i = 0; i < keys.length; i++) { + var sprite = window[keys[i]]; + sprite.style.width = Math.round(this._width[i] * scale) + "px"; + if (this._left[i]) { + sprite.style.left = Math.round(this._left[i] * scale) + "px"; + } + } + }, + + /** + * The step called within requestAnimationFrame to update the playback positions. + */ + step: function () { + // Loop through all active sounds and update their progress bar. + for (var i = 0; i < this.sounds.length; i++) { + var id = parseInt(this.sounds[i].id, 10); + var offset = this._sprite[this.sounds[i].dataset.sprite][0]; + var seek = (this.sound.seek(id) || 0) - offset / 1000; + this.sounds[i].style.width = + ((seek / this.sound.duration(id)) * 100 || 0) + "%"; + } + + requestAnimationFrame(this.step.bind(this)); + }, }; // Setup our new sprite class and pass in the options. var sprite = new Sprite({ - width: [78, 60, 62, 70, 62, 1895], - left: [0, 342, 680, 1022, 1361], - src: ['../../tests/audio/sound2.webm', '../../tests/audio/sound2.mp3'], - sprite: { - one: [0, 450], - two: [2000, 250], - three: [4000, 350], - four: [6000, 380], - five: [8000, 340], - beat: [10000, 11163] - }, - spriteMap: { - sprite0: 'one', - sprite1: 'two', - sprite2: 'three', - sprite3: 'four', - sprite4: 'five', - sprite5: 'beat' - } + width: [78, 60, 62, 70, 62, 1895], + left: [0, 342, 680, 1022, 1361], + src: ["../../tests/audio/sound2.webm", "../../tests/audio/sound2.mp3"], + sprite: { + one: [0, 450], + two: [2000, 250], + three: [4000, 350], + four: [6000, 380], + five: [8000, 340], + beat: [10000, 11163], + }, + spriteMap: { + sprite0: "one", + sprite1: "two", + sprite2: "three", + sprite3: "four", + sprite4: "five", + sprite5: "beat", + }, }); diff --git a/package-lock.json b/package-lock.json index e0f64ebc..9e1652b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "3.0.0-alpha.1", "license": "MIT", "devDependencies": { + "@biomejs/biome": "2.3.7", "tsdown": "~0.16.6", "typescript": "~5.9.3" } @@ -80,6 +81,169 @@ "node": ">=6.9.0" } }, + "node_modules/@biomejs/biome": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.7.tgz", + "integrity": "sha512-CTbAS/jNAiUc6rcq94BrTB8z83O9+BsgWj2sBCQg9rD6Wkh2gjfR87usjx0Ncx0zGXP1NKgT7JNglay5Zfs9jw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.3.7", + "@biomejs/cli-darwin-x64": "2.3.7", + "@biomejs/cli-linux-arm64": "2.3.7", + "@biomejs/cli-linux-arm64-musl": "2.3.7", + "@biomejs/cli-linux-x64": "2.3.7", + "@biomejs/cli-linux-x64-musl": "2.3.7", + "@biomejs/cli-win32-arm64": "2.3.7", + "@biomejs/cli-win32-x64": "2.3.7" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.7.tgz", + "integrity": "sha512-LirkamEwzIUULhXcf2D5b+NatXKeqhOwilM+5eRkbrnr6daKz9rsBL0kNZ16Hcy4b8RFq22SG4tcLwM+yx/wFA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.7.tgz", + "integrity": "sha512-Q4TO633kvrMQkKIV7wmf8HXwF0dhdTD9S458LGE24TYgBjSRbuhvio4D5eOQzirEYg6eqxfs53ga/rbdd8nBKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.7.tgz", + "integrity": "sha512-inHOTdlstUBzgjDcx0ge71U4SVTbwAljmkfi3MC5WzsYCRhancqfeL+sa4Ke6v2ND53WIwCFD5hGsYExoI3EZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.7.tgz", + "integrity": "sha512-/afy8lto4CB8scWfMdt+NoCZtatBUF62Tk3ilWH2w8ENd5spLhM77zKlFZEvsKJv9AFNHknMl03zO67CiklL2Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.7.tgz", + "integrity": "sha512-fJMc3ZEuo/NaMYo5rvoWjdSS5/uVSW+HPRQujucpZqm2ZCq71b8MKJ9U4th9yrv2L5+5NjPF0nqqILCl8HY/fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.7.tgz", + "integrity": "sha512-CQUtgH1tIN6e5wiYSJqzSwJumHYolNtaj1dwZGCnZXm2PZU1jOJof9TsyiP3bXNDb+VOR7oo7ZvY01If0W3iFQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.7.tgz", + "integrity": "sha512-aJAE8eCNyRpcfx2JJAtsPtISnELJ0H4xVVSwnxm13bzI8RwbXMyVtxy2r5DV1xT3WiSP+7LxORcApWw0LM8HiA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.7.tgz", + "integrity": "sha512-pulzUshqv9Ed//MiE8MOUeeEkbkSHVDVY5Cz5wVAnH1DUqliCQG3j6s1POaITTFqFfo7AVIx2sWdKpx/GS+Nqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@emnapi/core": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", diff --git a/package.json b/package.json index a5810444..cb3c9409 100644 --- a/package.json +++ b/package.json @@ -1,52 +1,53 @@ { - "name": "howler", - "version": "3.0.0-alpha.1", - "description": "Javascript audio library for the modern web.", - "homepage": "https://howlerjs.com", - "type": "module", - "keywords": [ - "howler", - "howler.js", - "audio", - "sound", - "web audio", - "webaudio", - "browser", - "html5", - "html5 audio", - "audio sprite", - "audiosprite" - ], - "author": "James Simpson (http://goldfirestudios.com)", - "repository": { - "type": "git", - "url": "git://github.com/goldfire/howler.js.git" - }, - "scripts": { - "build": "tsdown", - "release": "npm run build && git add dist && git commit -m 'build: update dist files' && npm publish" - }, - "devDependencies": { - "tsdown": "~0.16.6", - "typescript": "~5.9.3" - }, - "main": "dist/index.js", - "module": "dist/index.js", - "types": "dist/index.d.ts", - "exports": { - ".": { - "import": "./dist/index.js", - "types": "./dist/index.d.ts" - }, - "./plugins/spatial": { - "import": "./dist/plugins/spatial.js", - "types": "./dist/plugins/spatial.d.ts" - } - }, - "license": "MIT", - "files": [ - "src", - "dist", - "LICENSE.md" - ] + "name": "howler", + "version": "3.0.0-alpha.1", + "description": "Javascript audio library for the modern web.", + "homepage": "https://howlerjs.com", + "type": "module", + "keywords": [ + "howler", + "howler.js", + "audio", + "sound", + "web audio", + "webaudio", + "browser", + "html5", + "html5 audio", + "audio sprite", + "audiosprite" + ], + "author": "James Simpson (http://goldfirestudios.com)", + "repository": { + "type": "git", + "url": "git://github.com/goldfire/howler.js.git" + }, + "scripts": { + "build": "tsdown", + "release": "npm run build && git add dist && git commit -m 'build: update dist files' && npm publish" + }, + "devDependencies": { + "@biomejs/biome": "2.3.7", + "tsdown": "~0.16.6", + "typescript": "~5.9.3" + }, + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./plugins/spatial": { + "import": "./dist/plugins/spatial.js", + "types": "./dist/plugins/spatial.d.ts" + } + }, + "license": "MIT", + "files": [ + "src", + "dist", + "LICENSE.md" + ] } diff --git a/src/helpers/audio-context.ts b/src/helpers/audio-context.ts index 41c06241..aaf14899 100644 --- a/src/helpers/audio-context.ts +++ b/src/helpers/audio-context.ts @@ -8,46 +8,52 @@ * MIT License */ -import { Howler } from '../howler.core'; -import { isIOS, getIOSVersion, isSafari } from './light-ua-parser'; +import { Howler } from "../howler.core"; +import { getIOSVersion, isIOS, isSafari } from "./light-ua-parser"; export const setupAudioContext = () => { - if (!Howler.usingWebAudio) { - return; - } + if (!Howler.usingWebAudio) { + return; + } - try { - if (typeof window.AudioContext !== 'undefined') { - Howler.ctx = new window.AudioContext(); - } else if (typeof (window as any).webkitAudioContext !== 'undefined') { - Howler.ctx = new (window as any).webkitAudioContext(); - } else { - Howler.usingWebAudio = false; - } - } catch (e) { - Howler.usingWebAudio = false; - } + try { + if (typeof window.AudioContext !== "undefined") { + Howler.ctx = new window.AudioContext(); + } else if (typeof (window as any).webkitAudioContext !== "undefined") { + Howler.ctx = new (window as any).webkitAudioContext(); + } else { + Howler.usingWebAudio = false; + } + } catch (e) { + Howler.usingWebAudio = false; + } - if (!Howler.ctx) { - Howler.usingWebAudio = false; - } + if (!Howler.ctx) { + Howler.usingWebAudio = false; + } - const iOS = isIOS(Howler._navigator); - const version = getIOSVersion(Howler._navigator); - if (iOS && version && version < 9) { - const safari = isSafari(Howler._navigator); - if (Howler._navigator && !safari) { - Howler.usingWebAudio = false; - } - } + const iOS = isIOS(Howler._navigator); + const version = getIOSVersion(Howler._navigator); + if (iOS && version && version < 9) { + const safari = isSafari(Howler._navigator); + if (Howler._navigator && !safari) { + Howler.usingWebAudio = false; + } + } - if (Howler.usingWebAudio && Howler.ctx) { - Howler.masterGain = typeof Howler.ctx.createGain === 'undefined' ? (Howler.ctx as any).createGainNode() : Howler.ctx.createGain(); - if (Howler.masterGain) { - Howler.masterGain.gain.setValueAtTime(Howler._muted ? 0 : Howler._volume, Howler.ctx.currentTime); - Howler.masterGain.connect(Howler.ctx.destination); - } - } + if (Howler.usingWebAudio && Howler.ctx) { + Howler.masterGain = + typeof Howler.ctx.createGain === "undefined" + ? (Howler.ctx as any).createGainNode() + : Howler.ctx.createGain(); + if (Howler.masterGain) { + Howler.masterGain.gain.setValueAtTime( + Howler._muted ? 0 : Howler._volume, + Howler.ctx.currentTime, + ); + Howler.masterGain.connect(Howler.ctx.destination); + } + } - Howler._setup(); + Howler._setup(); }; diff --git a/src/helpers/audio-loader.ts b/src/helpers/audio-loader.ts index 1345322b..879cc93b 100644 --- a/src/helpers/audio-loader.ts +++ b/src/helpers/audio-loader.ts @@ -8,114 +8,123 @@ * MIT License */ -import type { Howl } from '../howler.core'; -import { Howler } from '../howler.core'; -import { globalPluginManager } from '../plugins'; -import { cache } from '../types'; +import type { Howl } from "../howler.core"; +import { Howler } from "../howler.core"; +import { globalPluginManager } from "../plugins"; +import { cache } from "../types"; export const loadBuffer = (self: Howl) => { - const url = self._src as string; - - if (cache[url]) { - self._duration = cache[url].duration; - loadSound(self); - return; - } - - if (/^data:[^;]+;base64,/.test(url)) { - const data = atob(url.split(',')[1]); - const dataView = new Uint8Array(data.length); - for (let i = 0; i < data.length; ++i) { - dataView[i] = data.charCodeAt(i); - } - - decodeAudioData(dataView.buffer, self); - } else { - const xhr = new XMLHttpRequest(); - xhr.open(self._xhr.method, url, true); - xhr.withCredentials = self._xhr.withCredentials; - xhr.responseType = 'arraybuffer'; - - if (self._xhr.headers) { - Object.keys(self._xhr.headers).forEach((key) => { - xhr.setRequestHeader(key, self._xhr.headers![key]); - }); - } - - xhr.onload = () => { - const 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); - } + const url = self._src as string; + + if (cache[url]) { + self._duration = cache[url].duration; + loadSound(self); + return; + } + + if (/^data:[^;]+;base64,/.test(url)) { + const data = atob(url.split(",")[1]); + const dataView = new Uint8Array(data.length); + for (let i = 0; i < data.length; ++i) { + dataView[i] = data.charCodeAt(i); + } + + decodeAudioData(dataView.buffer, self); + } else { + const xhr = new XMLHttpRequest(); + xhr.open(self._xhr.method, url, true); + xhr.withCredentials = self._xhr.withCredentials; + xhr.responseType = "arraybuffer"; + + if (self._xhr.headers) { + Object.keys(self._xhr.headers).forEach((key) => { + xhr.setRequestHeader(key, self._xhr.headers![key]); + }); + } + + xhr.onload = () => { + const 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); + } }; export const safeXhrSend = (xhr: XMLHttpRequest) => { - try { - xhr.send(); - } catch (e) { - if (xhr.onerror) { - // Create a ProgressEvent-like object for the error handler - const errorEvent = new ProgressEvent('error', { - lengthComputable: false, - loaded: 0, - total: 0 - }); - xhr.onerror(errorEvent); - } - } + try { + xhr.send(); + } catch (e) { + if (xhr.onerror) { + // Create a ProgressEvent-like object for the error handler + const errorEvent = new ProgressEvent("error", { + lengthComputable: false, + loaded: 0, + total: 0, + }); + xhr.onerror(errorEvent); + } + } }; export const decodeAudioData = (arraybuffer: ArrayBuffer, self: Howl) => { - const error = () => { - self._emit('loaderror', null, 'Decoding audio data failed.'); - }; - - const success = (buffer: AudioBuffer) => { - if (buffer && self._sounds.length > 0) { - cache[self._src as string] = buffer; - loadSound(self, buffer); - } else { - error(); - } - }; - - if (typeof Promise !== 'undefined' && Howler.ctx!.decodeAudioData.length === 1) { - (Howler.ctx!.decodeAudioData(arraybuffer) as Promise).then(success).catch(error); - } else { - Howler.ctx!.decodeAudioData(arraybuffer, success, error); - } + const error = () => { + self._emit("loaderror", null, "Decoding audio data failed."); + }; + + const success = (buffer: AudioBuffer) => { + if (buffer && self._sounds.length > 0) { + cache[self._src as string] = buffer; + loadSound(self, buffer); + } else { + error(); + } + }; + + if ( + typeof Promise !== "undefined" && + Howler.ctx!.decodeAudioData.length === 1 + ) { + (Howler.ctx!.decodeAudioData(arraybuffer) as Promise) + .then(success) + .catch(error); + } else { + Howler.ctx!.decodeAudioData(arraybuffer, success, error); + } }; export const loadSound = (self: Howl, buffer?: AudioBuffer) => { - if (buffer && !self._duration) { - self._duration = buffer.duration; - } - - if (Object.keys(self._sprite).length === 0) { - self._sprite = { __default: [0, self._duration * 1000] }; - } - - if (self._state !== 'loaded') { - self._state = 'loaded'; - self._emit('load'); - self._loadQueue(); - - // Execute plugin hooks - globalPluginManager.executeHowlLoad(self); - } + if (buffer && !self._duration) { + self._duration = buffer.duration; + } + + if (Object.keys(self._sprite).length === 0) { + self._sprite = { __default: [0, self._duration * 1000] }; + } + + if (self._state !== "loaded") { + self._state = "loaded"; + self._emit("load"); + self._loadQueue(); + + // Execute plugin hooks + globalPluginManager.executeHowlLoad(self); + } }; diff --git a/src/helpers/index.ts b/src/helpers/index.ts index a4538e9d..23d0b1d7 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -8,6 +8,24 @@ * MIT License */ -export { loadBuffer, safeXhrSend, decodeAudioData, loadSound } from './audio-loader'; -export { setupAudioContext } from './audio-context'; -export { isIOS, getIOSVersion, isSafari, getSafariVersion, isOldSafari, isOpera, getOperaVersion, isOldOpera, isIE, isAppleVendor, isChromeBased, isCocoonJS } from './light-ua-parser'; +export { setupAudioContext } from "./audio-context"; +export { + decodeAudioData, + loadBuffer, + loadSound, + safeXhrSend, +} from "./audio-loader"; +export { + getIOSVersion, + getOperaVersion, + getSafariVersion, + isAppleVendor, + isChromeBased, + isCocoonJS, + isIE, + isIOS, + isOldOpera, + isOldSafari, + isOpera, + isSafari, +} from "./light-ua-parser"; diff --git a/src/helpers/light-ua-parser.ts b/src/helpers/light-ua-parser.ts index 93db0213..56b6a443 100644 --- a/src/helpers/light-ua-parser.ts +++ b/src/helpers/light-ua-parser.ts @@ -13,21 +13,21 @@ * Get the user agent string from navigator */ function getUserAgent(navigator: Navigator | null): string { - return navigator?.userAgent || ''; + return navigator?.userAgent || ""; } /** * Get the platform string from navigator */ function getPlatform(navigator: Navigator | null): string { - return navigator?.platform || ''; + return navigator?.platform || ""; } /** * Check if the device is iOS (iPhone, iPad, iPod) */ export function isIOS(navigator: Navigator | null): boolean { - return /iP(hone|od|ad)/.test(getPlatform(navigator)); + return /iP(hone|od|ad)/.test(getPlatform(navigator)); } /** @@ -35,20 +35,20 @@ export function isIOS(navigator: Navigator | null): boolean { * Returns the major version number or null if not iOS or version can't be determined */ export function getIOSVersion(navigator: Navigator | null): number | null { - if (!isIOS(navigator)) { - return null; - } + if (!isIOS(navigator)) { + return null; + } - const appVersion = navigator?.appVersion?.match(/OS (\d+)_(\d+)_?(\d+)?/); - return appVersion ? parseInt(appVersion[1], 10) : null; + const appVersion = navigator?.appVersion?.match(/OS (\d+)_(\d+)_?(\d+)?/); + return appVersion ? parseInt(appVersion[1], 10) : null; } /** * Check if the browser is Safari (not Chrome-based) */ export function isSafari(navigator: Navigator | null): boolean { - const ua = getUserAgent(navigator); - return ua.indexOf('Safari') !== -1 && ua.indexOf('Chrome') === -1; + const ua = getUserAgent(navigator); + return ua.indexOf("Safari") !== -1 && ua.indexOf("Chrome") === -1; } /** @@ -56,29 +56,29 @@ export function isSafari(navigator: Navigator | null): boolean { * Returns the major version number or null if not Safari or version can't be determined */ export function getSafariVersion(navigator: Navigator | null): number | null { - if (!isSafari(navigator)) { - return null; - } + if (!isSafari(navigator)) { + return null; + } - const ua = getUserAgent(navigator); - const versionMatch = ua.match(/Version\/(.*?) /); - return versionMatch ? parseInt(versionMatch[1], 10) : null; + const ua = getUserAgent(navigator); + const versionMatch = ua.match(/Version\/(.*?) /); + return versionMatch ? parseInt(versionMatch[1], 10) : null; } /** * Check if Safari is an old version (before version 15) */ export function isOldSafari(navigator: Navigator | null): boolean { - const version = getSafariVersion(navigator); - return version !== null && version < 15; + const version = getSafariVersion(navigator); + return version !== null && version < 15; } /** * Check if the browser is Opera */ export function isOpera(navigator: Navigator | null): boolean { - const ua = getUserAgent(navigator); - return /OPR\//.test(ua); + const ua = getUserAgent(navigator); + return /OPR\//.test(ua); } /** @@ -86,50 +86,49 @@ export function isOpera(navigator: Navigator | null): boolean { * Returns the major version number or null if not Opera or version can't be determined */ export function getOperaVersion(navigator: Navigator | null): number | null { - if (!isOpera(navigator)) { - return null; - } + if (!isOpera(navigator)) { + return null; + } - const ua = getUserAgent(navigator); - const versionMatch = ua.match(/OPR\/(\d+)/); - return versionMatch ? parseInt(versionMatch[1], 10) : null; + const ua = getUserAgent(navigator); + const versionMatch = ua.match(/OPR\/(\d+)/); + return versionMatch ? parseInt(versionMatch[1], 10) : null; } /** * Check if Opera is an old version (before version 33) */ export function isOldOpera(navigator: Navigator | null): boolean { - const version = getOperaVersion(navigator); - return version !== null && version < 33; + const version = getOperaVersion(navigator); + return version !== null && version < 33; } /** * Check if the browser is Internet Explorer (MSIE or Trident) */ export function isIE(navigator: Navigator | null): boolean { - const ua = getUserAgent(navigator); - return /MSIE |Trident\//.test(ua); + const ua = getUserAgent(navigator); + return /MSIE |Trident\//.test(ua); } /** * Check if the browser vendor is Apple */ export function isAppleVendor(navigator: Navigator | null): boolean { - return (navigator?.vendor?.indexOf('Apple') ?? -1) >= 0; + return (navigator?.vendor?.indexOf("Apple") ?? -1) >= 0; } /** * Check if the browser is Chrome-based (Chrome, Edge, etc.) */ export function isChromeBased(navigator: Navigator | null): boolean { - const ua = getUserAgent(navigator); - return ua.indexOf('Chrome') !== -1; + const ua = getUserAgent(navigator); + return ua.indexOf("Chrome") !== -1; } /** * Check if the browser is CocoonJS (for game engines) */ export function isCocoonJS(navigator: Navigator | null): boolean { - return !!(navigator as any)?.isCocoonJS; + return !!(navigator as any)?.isCocoonJS; } - diff --git a/src/howler.core.ts b/src/howler.core.ts index 67cec6c0..79c2bdd0 100644 --- a/src/howler.core.ts +++ b/src/howler.core.ts @@ -8,427 +8,525 @@ * MIT License */ // Import shared types -import { AudioBufferSourceNodeWithLegacy, cache, EventListener, GainNodeWithBufferSource, HowlOptions, HTMLAudioElementWithUnlocked, isGainNode, isHTMLAudioElement, NavigatorWithCocoonJS, QueueItem, WindowWithAudio } from './types'; // Import helper functions -import { isAppleVendor, isIE, isOldOpera, isOldSafari, loadBuffer, setupAudioContext } from './helpers'; - +import { + isAppleVendor, + isIE, + isOldOpera, + isOldSafari, + loadBuffer, + setupAudioContext, +} from "./helpers"; // Import plugin manager -import { globalPluginManager, HowlerPlugin } from './plugins'; +import { globalPluginManager, type HowlerPlugin } from "./plugins"; +import { + type AudioBufferSourceNodeWithLegacy, + cache, + type EventListener, + type GainNodeWithBufferSource, + type HowlOptions, + type HTMLAudioElementWithUnlocked, + isGainNode, + isHTMLAudioElement, + type NavigatorWithCocoonJS, + type QueueItem, + type WindowWithAudio, +} from "./types"; export class HowlerGlobal { - _counter: number = 1000; - _html5AudioPool: HTMLAudioElement[] = []; - html5PoolSize: number = 10; - _codecs: Record = {}; - _howls: Howl[] = []; - _muted: boolean = false; - _volume: number = 1; - _canPlayEvent: string = 'canplaythrough'; - _navigator: NavigatorWithCocoonJS | null = null; - masterGain: GainNode | null = null; - noAudio: boolean = false; - usingWebAudio: boolean = true; - autoSuspend: boolean = true; - ctx: AudioContext | null = null; - autoUnlock: boolean = true; - state: string = 'suspended'; - _audioUnlocked: boolean = false; - _scratchBuffer: AudioBuffer | null = null; - _suspendTimer: ReturnType | null = null; - _resumeAfterSuspend?: boolean; - _mobileUnloaded?: boolean; - - constructor() { - // Initialize all properties (explicit initialization ensures correct values) - this._counter = 1000; - this._html5AudioPool = []; - this.html5PoolSize = 10; - this._codecs = {}; - this._howls = []; - this._muted = false; - this._volume = 1; - this._canPlayEvent = 'canplaythrough'; - this._navigator = typeof window !== 'undefined' && window.navigator ? window.navigator : null; - this.masterGain = null; - this.noAudio = false; - this.usingWebAudio = true; - this.autoSuspend = true; - this.ctx = null; - this.autoUnlock = true; - - // Setup Howler (codecs, audio context, etc.) - this._setup(); - - // Register the Howler instance with the plugin manager - // This triggers onHowlerInit hooks for any plugins already registered - globalPluginManager.setHowlerInstance(this); - } - - volume(vol?: number): number | HowlerGlobal { - if (vol !== undefined) { - vol = parseFloat(String(vol)); - - if (!this.ctx) { - setupAudioContext(); - } - - if (typeof vol === 'number' && vol >= 0 && vol <= 1) { - this._volume = vol; - - if (this._muted) { - return this; - } - - if (this.usingWebAudio) { - this.masterGain!.gain.setValueAtTime(vol, Howler.ctx!.currentTime); - } - - for (let i = 0; i < this._howls.length; i++) { - if (!this._howls[i]._webAudio) { - const ids = this._howls[i]._getSoundIds(); - for (let j = 0; j < ids.length; j++) { - const sound = this._howls[i]._soundById(ids[j]); - if (sound && sound._node && isHTMLAudioElement(sound._node)) { - sound._node.volume = sound._volume * vol; - } - } - } - } - - return this; - } - } - - return this._volume; - } - - mute(muted: boolean): HowlerGlobal { - if (!this.ctx) { - setupAudioContext(); - } - - this._muted = muted; - - if (this.usingWebAudio) { - this.masterGain!.gain.setValueAtTime(muted ? 0 : this._volume, Howler.ctx!.currentTime); - } - - for (let i = 0; i < this._howls.length; i++) { - if (!this._howls[i]._webAudio) { - const ids = this._howls[i]._getSoundIds(); - for (let j = 0; j < ids.length; j++) { - const sound = this._howls[i]._soundById(ids[j]); - if (sound && sound._node && isHTMLAudioElement(sound._node)) { - sound._node.muted = muted ? true : sound._muted; - } - } - } - } - - return this; - } - - stop(): HowlerGlobal { - for (let i = 0; i < this._howls.length; i++) { - this._howls[i].stop(); - } - - return this; - } - - unload(): HowlerGlobal { - for (let 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; - setupAudioContext(); - } - - return this; - } - - codecs(ext: string): boolean { - return (this || Howler)._codecs[ext.replace(/^x-/, '')]; - } - - /** - * Register a plugin with Howler - * @param plugin - The plugin to register - * @returns this for chaining - * @throws Error if a plugin with the same name is already registered - */ - addPlugin(plugin: HowlerPlugin): HowlerGlobal { - globalPluginManager.register(plugin); - return this; - } - - /** - * Unregister a plugin from Howler - * @param plugin - The plugin instance to unregister - * @returns this for chaining - * @throws Error if the plugin is not registered - */ - removePlugin(plugin: HowlerPlugin): HowlerGlobal { - globalPluginManager.unregister(plugin.name); - return this; - } - - /** - * Check if a plugin is registered - * @param pluginName - The name of the plugin to check - * @returns true if the plugin is registered, false otherwise - */ - hasPlugin(pluginName: string): boolean { - return globalPluginManager.isRegistered(pluginName); - } - - _setup(): HowlerGlobal { - this.state = this.ctx ? this.ctx.state || 'suspended' : 'suspended'; - this._autoSuspend(); - - if (!this.usingWebAudio) { - if (typeof window.Audio !== 'undefined') { - try { - const test = new window.Audio(); - if (typeof test.oncanplaythrough === 'undefined') { - this._canPlayEvent = 'canplay'; - } - } catch (e) { - this.noAudio = true; - } - } else { - this.noAudio = true; - } - } - - try { - const test = new window.Audio(); - if (test.muted) { - this.noAudio = true; - } - } catch (e) {} - - if (!this.noAudio) { - this._setupCodecs(); - } - - return this; - } - - _setupCodecs(): HowlerGlobal { - let audioTest: HTMLAudioElement | null = null; - - try { - audioTest = typeof window.Audio !== 'undefined' ? new window.Audio() : null; - } catch (err) { - return this; - } - - if (!audioTest || typeof audioTest.canPlayType !== 'function') { - return this; - } - - const mpegTest = audioTest.canPlayType('audio/mpeg;').replace(/^no$/, ''); - const oldOpera = isOldOpera(this._navigator); - const oldSafari = isOldSafari(this._navigator); - - this._codecs = { - mp3: !!(!oldOpera && (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: !!(!oldSafari && audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, '')), - webm: !!(!oldSafari && 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(): void { - if (this._audioUnlocked || !this.ctx) { - return; - } - - this._audioUnlocked = false; - 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 { - const audioNode = new (window as WindowWithAudio).Audio() as HTMLAudioElementWithUnlocked; - audioNode._unlocked = true; - this._releaseHtml5Audio(audioNode); - } catch (e) { - this.noAudio = true; - break; - } - } - - for (let i = 0; i < this._howls.length; i++) { - if (!this._howls[i]._webAudio) { - const ids = this._howls[i]._getSoundIds(); - for (let j = 0; j < ids.length; j++) { - const sound = this._howls[i]._soundById(ids[j]); - if (sound && sound._node && isHTMLAudioElement(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 as AudioBufferSourceNodeWithLegacy).noteOn?.(0); - } else { - source.start(0); - } - - if (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 (let i = 0; i < this._howls.length; i++) { - this._howls[i]._emit('unlock'); - } - }; - }; - - document.addEventListener('touchstart', unlock, true); - document.addEventListener('touchend', unlock, true); - document.addEventListener('click', unlock, true); - document.addEventListener('keydown', unlock, true); - } - - _obtainHtml5Audio(): HTMLAudioElementWithUnlocked { - if (this._html5AudioPool.length) { - return this._html5AudioPool.pop()!; - } - - const testPlay = new (window as WindowWithAudio).Audio().play(); - if (testPlay && typeof Promise !== 'undefined') { - if (testPlay instanceof Promise) { - testPlay.catch(() => { - console.warn('HTML5 Audio pool exhausted, returning potentially locked audio object.'); - }); - } else if (typeof testPlay === 'object' && testPlay !== null && 'then' in testPlay && typeof (testPlay as { then?: unknown }).then === 'function') { - // Handle thenable objects - (testPlay as { catch: (onRejected: () => void) => void }).catch(() => { - console.warn('HTML5 Audio pool exhausted, returning potentially locked audio object.'); - }); - } - } - - return new (window as WindowWithAudio).Audio() as HTMLAudioElementWithUnlocked; - } - - _releaseHtml5Audio(audio: HTMLAudioElementWithUnlocked): HowlerGlobal { - if (audio._unlocked) { - this._html5AudioPool.push(audio); - } - - return this; - } - - _autoSuspend(): void { - if (!this.autoSuspend || !this.ctx || typeof this.ctx.suspend === 'undefined' || !Howler.usingWebAudio) { - return; - } - - for (let i = 0; i < this._howls.length; i++) { - if (this._howls[i]._webAudio) { - for (let j = 0; j < this._howls[i]._sounds.length; j++) { - if (!this._howls[i]._sounds[j]._paused) { - return; - } - } - } - } - - 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); - }, 30000); - } - - _autoResume(): void { - if (!this.ctx || typeof this.ctx.resume === 'undefined' || !Howler.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 (let 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; - } - } + _counter: number = 1000; + _html5AudioPool: HTMLAudioElement[] = []; + html5PoolSize: number = 10; + _codecs: Record = {}; + _howls: Howl[] = []; + _muted: boolean = false; + _volume: number = 1; + _canPlayEvent: string = "canplaythrough"; + _navigator: NavigatorWithCocoonJS | null = null; + masterGain: GainNode | null = null; + noAudio: boolean = false; + usingWebAudio: boolean = true; + autoSuspend: boolean = true; + ctx: AudioContext | null = null; + autoUnlock: boolean = true; + state: string = "suspended"; + _audioUnlocked: boolean = false; + _scratchBuffer: AudioBuffer | null = null; + _suspendTimer: ReturnType | null = null; + _resumeAfterSuspend?: boolean; + _mobileUnloaded?: boolean; + + constructor() { + // Initialize all properties (explicit initialization ensures correct values) + this._counter = 1000; + this._html5AudioPool = []; + this.html5PoolSize = 10; + this._codecs = {}; + this._howls = []; + this._muted = false; + this._volume = 1; + this._canPlayEvent = "canplaythrough"; + this._navigator = + typeof window !== "undefined" && window.navigator + ? window.navigator + : null; + this.masterGain = null; + this.noAudio = false; + this.usingWebAudio = true; + this.autoSuspend = true; + this.ctx = null; + this.autoUnlock = true; + + // Setup Howler (codecs, audio context, etc.) + this._setup(); + + // Register the Howler instance with the plugin manager + // This triggers onHowlerInit hooks for any plugins already registered + globalPluginManager.setHowlerInstance(this); + } + + volume(vol?: number): number | HowlerGlobal { + if (vol !== undefined) { + vol = parseFloat(String(vol)); + + if (!this.ctx) { + setupAudioContext(); + } + + if (typeof vol === "number" && vol >= 0 && vol <= 1) { + this._volume = vol; + + if (this._muted) { + return this; + } + + if (this.usingWebAudio) { + this.masterGain!.gain.setValueAtTime(vol, Howler.ctx!.currentTime); + } + + for (let i = 0; i < this._howls.length; i++) { + if (!this._howls[i]._webAudio) { + const ids = this._howls[i]._getSoundIds(); + for (let j = 0; j < ids.length; j++) { + const sound = this._howls[i]._soundById(ids[j]); + if (sound && sound._node && isHTMLAudioElement(sound._node)) { + sound._node.volume = sound._volume * vol; + } + } + } + } + + return this; + } + } + + return this._volume; + } + + mute(muted: boolean): HowlerGlobal { + if (!this.ctx) { + setupAudioContext(); + } + + this._muted = muted; + + if (this.usingWebAudio) { + this.masterGain!.gain.setValueAtTime( + muted ? 0 : this._volume, + Howler.ctx!.currentTime, + ); + } + + for (let i = 0; i < this._howls.length; i++) { + if (!this._howls[i]._webAudio) { + const ids = this._howls[i]._getSoundIds(); + for (let j = 0; j < ids.length; j++) { + const sound = this._howls[i]._soundById(ids[j]); + if (sound && sound._node && isHTMLAudioElement(sound._node)) { + sound._node.muted = muted ? true : sound._muted; + } + } + } + } + + return this; + } + + stop(): HowlerGlobal { + for (let i = 0; i < this._howls.length; i++) { + this._howls[i].stop(); + } + + return this; + } + + unload(): HowlerGlobal { + for (let 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; + setupAudioContext(); + } + + return this; + } + + codecs(ext: string): boolean { + return (this || Howler)._codecs[ext.replace(/^x-/, "")]; + } + + /** + * Register a plugin with Howler + * @param plugin - The plugin to register + * @returns this for chaining + * @throws Error if a plugin with the same name is already registered + */ + addPlugin(plugin: HowlerPlugin): HowlerGlobal { + globalPluginManager.register(plugin); + return this; + } + + /** + * Unregister a plugin from Howler + * @param plugin - The plugin instance to unregister + * @returns this for chaining + * @throws Error if the plugin is not registered + */ + removePlugin(plugin: HowlerPlugin): HowlerGlobal { + globalPluginManager.unregister(plugin.name); + return this; + } + + /** + * Check if a plugin is registered + * @param pluginName - The name of the plugin to check + * @returns true if the plugin is registered, false otherwise + */ + hasPlugin(pluginName: string): boolean { + return globalPluginManager.isRegistered(pluginName); + } + + _setup(): HowlerGlobal { + this.state = this.ctx ? this.ctx.state || "suspended" : "suspended"; + this._autoSuspend(); + + if (!this.usingWebAudio) { + if (typeof window.Audio !== "undefined") { + try { + const test = new window.Audio(); + if (typeof test.oncanplaythrough === "undefined") { + this._canPlayEvent = "canplay"; + } + } catch (e) { + this.noAudio = true; + } + } else { + this.noAudio = true; + } + } + + try { + const test = new window.Audio(); + if (test.muted) { + this.noAudio = true; + } + } catch (e) {} + + if (!this.noAudio) { + this._setupCodecs(); + } + + return this; + } + + _setupCodecs(): HowlerGlobal { + let audioTest: HTMLAudioElement | null = null; + + try { + audioTest = + typeof window.Audio !== "undefined" ? new window.Audio() : null; + } catch (err) { + return this; + } + + if (!audioTest || typeof audioTest.canPlayType !== "function") { + return this; + } + + const mpegTest = audioTest.canPlayType("audio/mpeg;").replace(/^no$/, ""); + const oldOpera = isOldOpera(this._navigator); + const oldSafari = isOldSafari(this._navigator); + + this._codecs = { + mp3: !!( + !oldOpera && + (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: !!( + !oldSafari && + audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, "") + ), + webm: !!( + !oldSafari && + 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(): void { + if (this._audioUnlocked || !this.ctx) { + return; + } + + this._audioUnlocked = false; + 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 { + const audioNode = new ( + window as WindowWithAudio + ).Audio() as HTMLAudioElementWithUnlocked; + audioNode._unlocked = true; + this._releaseHtml5Audio(audioNode); + } catch (e) { + this.noAudio = true; + break; + } + } + + for (let i = 0; i < this._howls.length; i++) { + if (!this._howls[i]._webAudio) { + const ids = this._howls[i]._getSoundIds(); + for (let j = 0; j < ids.length; j++) { + const sound = this._howls[i]._soundById(ids[j]); + if ( + sound && + sound._node && + isHTMLAudioElement(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 as AudioBufferSourceNodeWithLegacy).noteOn?.(0); + } else { + source.start(0); + } + + if (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 (let i = 0; i < this._howls.length; i++) { + this._howls[i]._emit("unlock"); + } + }; + }; + + document.addEventListener("touchstart", unlock, true); + document.addEventListener("touchend", unlock, true); + document.addEventListener("click", unlock, true); + document.addEventListener("keydown", unlock, true); + } + + _obtainHtml5Audio(): HTMLAudioElementWithUnlocked { + if (this._html5AudioPool.length) { + return this._html5AudioPool.pop()!; + } + + const testPlay = new (window as WindowWithAudio).Audio().play(); + if (testPlay && typeof Promise !== "undefined") { + if (testPlay instanceof Promise) { + testPlay.catch(() => { + console.warn( + "HTML5 Audio pool exhausted, returning potentially locked audio object.", + ); + }); + } else if ( + typeof testPlay === "object" && + testPlay !== null && + "then" in testPlay && + typeof (testPlay as { then?: unknown }).then === "function" + ) { + // Handle thenable objects + (testPlay as { catch: (onRejected: () => void) => void }).catch(() => { + console.warn( + "HTML5 Audio pool exhausted, returning potentially locked audio object.", + ); + }); + } + } + + return new ( + window as WindowWithAudio + ).Audio() as HTMLAudioElementWithUnlocked; + } + + _releaseHtml5Audio(audio: HTMLAudioElementWithUnlocked): HowlerGlobal { + if (audio._unlocked) { + this._html5AudioPool.push(audio); + } + + return this; + } + + _autoSuspend(): void { + if ( + !this.autoSuspend || + !this.ctx || + typeof this.ctx.suspend === "undefined" || + !Howler.usingWebAudio + ) { + return; + } + + for (let i = 0; i < this._howls.length; i++) { + if (this._howls[i]._webAudio) { + for (let j = 0; j < this._howls[i]._sounds.length; j++) { + if (!this._howls[i]._sounds[j]._paused) { + return; + } + } + } + } + + 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); + }, 30000); + } + + _autoResume(): void { + if ( + !this.ctx || + typeof this.ctx.resume === "undefined" || + !Howler.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 (let 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; + } + } } // Setup the global audio controller @@ -437,1550 +535,1735 @@ const Howler = new HowlerGlobal(); // Type guards for Sound._node class Sound { - _parent: Howl; - _muted: boolean = false; - _loop: boolean = false; - _volume: number = 1; - _rate: number = 1; - _seek: number = 0; - _paused: boolean = true; - _ended: boolean = true; - _sprite: string = '__default'; - _id: number = 0; - _node: HTMLAudioElementWithUnlocked | GainNodeWithBufferSource | null = null; - _playStart: number = 0; - _rateSeek: number = 0; - _errorFn?: (event: Event) => void; - _loadFn?: (event: Event) => void; - _endFn?: (event: Event) => void; - _start?: number; - _stop?: number; - _panner?: PannerNode | StereoPannerNode; - _fadeTo?: number; - _interval?: ReturnType; - - constructor(howl: Howl) { - this._parent = howl; - this.init(); - } - - init(): Sound { - - const parent = this._parent; - - this._muted = parent._muted; - this._loop = parent._loop; - this._volume = parent._volume; - this._rate = parent._rate; - this._seek = 0; - this._paused = true; - this._ended = true; - this._sprite = '__default'; - - this._id = ++Howler._counter; - - parent._sounds.push(this); - - this.create(); - - // Execute plugin hooks - globalPluginManager.executeSoundCreate(this, parent); - - return this; - } - - create(): Sound { - const parent = this._parent; - const volume = Howler._muted || this._muted || parent._muted ? 0 : this._volume; - - this._errorFn = this._errorListener.bind(this); - this._loadFn = this._loadListener.bind(this); - this._endFn = this._endListener.bind(this); - - if (parent._webAudio && Howler.ctx) { - const gainNode = typeof Howler.ctx.createGain === 'undefined' ? (Howler.ctx as { createGainNode?: () => GainNode }).createGainNode?.() : Howler.ctx.createGain(); - if (gainNode) { - this._node = gainNode as GainNodeWithBufferSource; - this._node.gain.setValueAtTime(volume, Howler.ctx.currentTime); - (this._node as { paused?: boolean }).paused = true; - this._node.connect(Howler.masterGain!); - } - } else if (!Howler.noAudio) { - this._node = Howler._obtainHtml5Audio(); - - this._errorFn = this._errorListener.bind(this); - this._node.addEventListener('error', this._errorFn, false); - - this._loadFn = this._loadListener.bind(this); - this._node.addEventListener(Howler._canPlayEvent, this._loadFn, false); - - this._endFn = this._endListener.bind(this); - this._node.addEventListener('ended', this._endFn, false); - - const src = typeof parent._src === 'string' ? parent._src : (Array.isArray(parent._src) && parent._src.length > 0 ? parent._src[0] : ''); - this._node.src = src; - const preloadValue = parent._preload === true ? 'auto' : (parent._preload === false ? 'none' : (parent._preload === 'metadata' ? 'metadata' : 'auto')); - this._node.preload = preloadValue; - const volumeOrHowler = Howler.volume(); - if (typeof volumeOrHowler === 'number') { - this._node.volume = volume * volumeOrHowler; - } - - this._node.load(); - } - - return this; - } - - reset(): Sound { - - const 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._counter; - - return this; - } - - _errorListener(): void { - if (this._node && isHTMLAudioElement(this._node)) { - const errorCode = this._node.error ? this._node.error.code : 0; - this._parent._emit('loaderror', this._id, String(errorCode)); - if (this._errorFn) { - this._node.removeEventListener('error', this._errorFn, false); - } - } - } - - _loadListener(): void { - if (!this._node || !isHTMLAudioElement(this._node)) { - return; - } - - 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 * 1000] }; - } - - if (parent._state !== 'loaded') { - parent._state = 'loaded'; - parent._emit('load'); - parent._loadQueue(); - - // Execute plugin hooks - globalPluginManager.executeHowlLoad(parent); - } - - if (this._loadFn) { - this._node.removeEventListener(Howler._canPlayEvent, this._loadFn, false); - } - } - - _endListener(): void { - - const parent = this._parent; - - if (parent._duration === Infinity && this._node && isHTMLAudioElement(this._node)) { - parent._duration = Math.ceil(this._node.duration * 10) / 10; - - if (parent._sprite.__default[1] === Infinity) { - parent._sprite.__default[1] = parent._duration * 1000; - } - - parent._ended(this); - } - - if (this._endFn && this._node) { - this._node.removeEventListener('ended', this._endFn, false); - } - } + _parent: Howl; + _muted: boolean = false; + _loop: boolean = false; + _volume: number = 1; + _rate: number = 1; + _seek: number = 0; + _paused: boolean = true; + _ended: boolean = true; + _sprite: string = "__default"; + _id: number = 0; + _node: HTMLAudioElementWithUnlocked | GainNodeWithBufferSource | null = null; + _playStart: number = 0; + _rateSeek: number = 0; + _errorFn?: (event: Event) => void; + _loadFn?: (event: Event) => void; + _endFn?: (event: Event) => void; + _start?: number; + _stop?: number; + _panner?: PannerNode | StereoPannerNode; + _fadeTo?: number; + _interval?: ReturnType; + + constructor(howl: Howl) { + this._parent = howl; + this.init(); + } + + init(): Sound { + const parent = this._parent; + + this._muted = parent._muted; + this._loop = parent._loop; + this._volume = parent._volume; + this._rate = parent._rate; + this._seek = 0; + this._paused = true; + this._ended = true; + this._sprite = "__default"; + + this._id = ++Howler._counter; + + parent._sounds.push(this); + + this.create(); + + // Execute plugin hooks + globalPluginManager.executeSoundCreate(this, parent); + + return this; + } + + create(): Sound { + const parent = this._parent; + const volume = + Howler._muted || this._muted || parent._muted ? 0 : this._volume; + + this._errorFn = this._errorListener.bind(this); + this._loadFn = this._loadListener.bind(this); + this._endFn = this._endListener.bind(this); + + if (parent._webAudio && Howler.ctx) { + const gainNode = + typeof Howler.ctx.createGain === "undefined" + ? ( + Howler.ctx as { createGainNode?: () => GainNode } + ).createGainNode?.() + : Howler.ctx.createGain(); + if (gainNode) { + this._node = gainNode as GainNodeWithBufferSource; + this._node.gain.setValueAtTime(volume, Howler.ctx.currentTime); + (this._node as { paused?: boolean }).paused = true; + this._node.connect(Howler.masterGain!); + } + } else if (!Howler.noAudio) { + this._node = Howler._obtainHtml5Audio(); + + this._errorFn = this._errorListener.bind(this); + this._node.addEventListener("error", this._errorFn, false); + + this._loadFn = this._loadListener.bind(this); + this._node.addEventListener(Howler._canPlayEvent, this._loadFn, false); + + this._endFn = this._endListener.bind(this); + this._node.addEventListener("ended", this._endFn, false); + + const src = + typeof parent._src === "string" + ? parent._src + : Array.isArray(parent._src) && parent._src.length > 0 + ? parent._src[0] + : ""; + this._node.src = src; + const preloadValue = + parent._preload === true + ? "auto" + : parent._preload === false + ? "none" + : parent._preload === "metadata" + ? "metadata" + : "auto"; + this._node.preload = preloadValue; + const volumeOrHowler = Howler.volume(); + if (typeof volumeOrHowler === "number") { + this._node.volume = volume * volumeOrHowler; + } + + this._node.load(); + } + + return this; + } + + reset(): Sound { + const 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._counter; + + return this; + } + + _errorListener(): void { + if (this._node && isHTMLAudioElement(this._node)) { + const errorCode = this._node.error ? this._node.error.code : 0; + this._parent._emit("loaderror", this._id, String(errorCode)); + if (this._errorFn) { + this._node.removeEventListener("error", this._errorFn, false); + } + } + } + + _loadListener(): void { + if (!this._node || !isHTMLAudioElement(this._node)) { + return; + } + + 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 * 1000] }; + } + + if (parent._state !== "loaded") { + parent._state = "loaded"; + parent._emit("load"); + parent._loadQueue(); + + // Execute plugin hooks + globalPluginManager.executeHowlLoad(parent); + } + + if (this._loadFn) { + this._node.removeEventListener(Howler._canPlayEvent, this._loadFn, false); + } + } + + _endListener(): void { + const parent = this._parent; + + if ( + parent._duration === Infinity && + this._node && + isHTMLAudioElement(this._node) + ) { + parent._duration = Math.ceil(this._node.duration * 10) / 10; + + if (parent._sprite.__default[1] === Infinity) { + parent._sprite.__default[1] = parent._duration * 1000; + } + + parent._ended(this); + } + + if (this._endFn && this._node) { + this._node.removeEventListener("ended", this._endFn, false); + } + } } class Howl { - _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: Record = {}; - _src: string | string[] = []; - _volume: number = 1; - _xhr: { method: string; headers?: Record; withCredentials: boolean } = { method: 'GET', withCredentials: false }; - _duration: number = 0; - _state: string = 'unloaded'; - _sounds: Sound[] = []; - _endTimers: Record> = {}; - _queue: QueueItem[] = []; - _playLock: boolean = false; - _webAudio: boolean = false; - _onend: EventListener[] = []; - _onfade: EventListener[] = []; - _onload: EventListener[] = []; - _onloaderror: EventListener[] = []; - _onplayerror: EventListener[] = []; - _onpause: EventListener[] = []; - _onplay: EventListener[] = []; - _onstop: EventListener[] = []; - _onmute: EventListener[] = []; - _onvolume: EventListener[] = []; - _onrate: EventListener[] = []; - _onseek: EventListener[] = []; - _onunlock: EventListener[] = []; - _onresume: EventListener[] = []; - - constructor(o: HowlOptions) { - if (!o.src || o.src.length === 0) { - console.error('An array of source files must be passed with any new Howl.'); - return; - } - - this.init(o); - } - - init(o: HowlOptions): Howl { - if (!Howler.ctx) { - setupAudioContext(); - } - - this._autoplay = o.autoplay || false; - this._format = 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 - }; - - this._duration = 0; - this._state = 'unloaded'; - this._sounds = []; - this._endTimers = {}; - this._queue = []; - this._playLock = 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: (...args: unknown[]) => { - if (o.onloaderror && typeof args[0] === 'number' && typeof args[1] === 'string') { - o.onloaderror(args[0], args[1]); - } - } }] : []; - this._onplayerror = o.onplayerror ? [{ fn: (...args: unknown[]) => { - if (o.onplayerror && typeof args[0] === 'number' && typeof args[1] === 'string') { - o.onplayerror(args[0], args[1]); - } - } }] : []; - 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.usingWebAudio && !this._html5; - - if (typeof Howler.ctx !== 'undefined' && Howler.ctx && Howler.autoUnlock) { - Howler._unlockAudio(); - } - - Howler._howls.push(this); - - // Execute plugin hooks - globalPluginManager.executeHowlCreate(this, o); - - if (this._autoplay) { - this._queue.push({ - event: 'play', - action: () => { - this.play(); - } - }); - } - - if (this._preload === true || this._preload === 'metadata') { - this.load(); - } - - return this; - } - - load(): Howl { - let url: string | null = null; - - if (Howler.noAudio) { - this._emit('loaderror', null, 'No audio support.'); - return this; - } - - if (typeof this._src === 'string') { - this._src = [this._src]; - } - - for (let i = 0; i < (this._src as string[]).length; i++) { - let ext: string | null; - const str = (this._src as string[])[i]; - - if (this._format && this._format[i]) { - ext = this._format[i]; - } else { - if (typeof str !== 'string') { - this._emit('loaderror', null, 'Non-string found in selected audio sources - ignoring.'); - continue; - } - - let extMatch = /^data:audio\/([^;,]+);/i.exec(str); - if (!extMatch) { - extMatch = /\.([^.]+)$/.exec(str.split('?', 1)[0]); - } - - ext = extMatch ? extMatch[1].toLowerCase() : null; - } - - if (!ext) { - console.warn('No file extension was found. Consider using the "format" property or specify an extension.'); - } - - if (ext && Howler.codecs(ext)) { - url = (this._src as string[])[i]; - break; - } - } - - if (!url) { - this._emit('loaderror', null, 'No codec support for selected audio sources.'); - return this; - } - - this._src = url; - this._state = 'loading'; - - if (typeof window !== 'undefined' && window.location.protocol === 'https:' && url.slice(0, 5) === 'http:') { - this._html5 = true; - this._webAudio = false; - } - - new Sound(this); - - if (this._webAudio) { - loadBuffer(this); - } - - return this; - } - - play(sprite?: string | number, internal?: boolean): number | null { - - let id: number | null = null; - - if (typeof sprite === 'number') { - id = sprite; - sprite = undefined; - } else if (typeof sprite === 'string' && this._state === 'loaded' && !this._sprite[sprite]) { - return null; - } else if (typeof sprite === 'undefined') { - sprite = '__default'; - - if (!this._playLock) { - let num = 0; - for (let 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 = undefined; - } 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 || '__default'; - sound._ended = false; - - const 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._autoResume(); - } - - const seek = Math.max(0, sound._seek > 0 ? sound._seek : this._sprite[sprite!][0] / 1000); - const duration = Math.max(0, (this._sprite[sprite!][0] + this._sprite[sprite!][1]) / 1000 - seek); - const timeout = (duration * 1000) / Math.abs(sound._rate); - const start = this._sprite[sprite!][0] / 1000; - const stop = (this._sprite[sprite!][0] + this._sprite[sprite!][1]) / 1000; - 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 sound._id; - } - - const node = sound._node; - - if (this._webAudio && node && isGainNode(node)) { - const playWebAudio = () => { - this._playLock = false; - setParams(); - this._refreshBuffer(sound); - - const vol = sound._muted || this._muted ? 0 : sound._volume; - node.gain.setValueAtTime(vol, Howler.ctx!.currentTime); - sound._playStart = Howler.ctx!.currentTime; - - if (node.bufferSource) { - if (typeof node.bufferSource.start === 'undefined') { - node.bufferSource.noteGrainOn?.(0, seek, sound._loop ? 86400 : duration); - } else { - node.bufferSource.start(0, seek, sound._loop ? 86400 : 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.state === 'running' && Howler.ctx!.state !== 'interrupted') { - playWebAudio(); - } else { - this._playLock = true; - this.once('resume', playWebAudio); - this._clearTimer(sound._id); - } - } else if (node && isHTMLAudioElement(node)) { - const playHtml5 = () => { - node.currentTime = seek; - node.muted = sound._muted || this._muted || Howler._muted || node.muted; - const volume = Howler.volume(); - node.volume = sound._volume * (typeof volume === 'number' ? volume : 1); - node.playbackRate = sound._rate; - - try { - const play = node.play(); - - if (play && typeof Promise !== 'undefined' && (play instanceof Promise || typeof (play as any).then === 'function')) { - this._playLock = true; - - setParams(); - - (play as any) - .then(() => { - this._playLock = false; - if ('_unlocked' in node) { - (node as HTMLAudioElementWithUnlocked)._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 { - const endHandler = () => { - this._ended(sound); - node.removeEventListener('ended', endHandler, false); - }; - this._endTimers[sound._id] = setTimeout(endHandler, timeout); - node.addEventListener('ended', endHandler, false); - } - } catch (err: unknown) { - this._emit('playerror', sound._id, err instanceof Error ? err.message : String(err)); - } - }; - - if (node.src === 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA') { - const src = typeof this._src === 'string' ? this._src : (Array.isArray(this._src) && this._src.length > 0 ? this._src[0] : ''); - node.src = src; - node.load(); - } - - const loadedNoReadyState = (typeof (window as WindowWithAudio).ejecta !== 'undefined') || (!node.readyState && Howler._navigator && Howler._navigator.isCocoonJS); - if (node.readyState >= 3 || loadedNoReadyState) { - playHtml5(); - } else { - this._playLock = true; - this._state = 'loading'; - - const listener = () => { - this._state = 'loaded'; - playHtml5(); - node.removeEventListener(Howler._canPlayEvent, listener, false); - }; - node.addEventListener(Howler._canPlayEvent, listener, false); - - this._clearTimer(sound._id); - } - } - - return sound._id; - } - - pause(id?: number, internal?: boolean): Howl { - - - if (this._state !== 'loaded' || this._playLock) { - this._queue.push({ - event: 'pause', - action: () => { - this.pause(id); - } - }); - - return this; - } - - const ids = this._getSoundIds(id); - - for (let i = 0; i < ids.length; i++) { - this._clearTimer(ids[i]); - - const sound = this._soundById(ids[i]); - - if (sound && !sound._paused) { - const seekResult = this.seek(ids[i]); - sound._seek = typeof seekResult === 'number' ? seekResult : 0; - sound._rateSeek = 0; - sound._paused = true; - - this._stopFade(ids[i]); - - if (sound._node) { - if (this._webAudio && isGainNode(sound._node)) { - 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 (isHTMLAudioElement(sound._node) && (!isNaN(sound._node.duration) || sound._node.duration === Infinity)) { - sound._node.pause(); - } - } - } - - if (!arguments[1]) { - this._emit('pause', sound ? sound._id : null); - } - } - - return this; - } - - stop(id?: number, internal?: boolean): Howl { - - - if (this._state !== 'loaded' || this._playLock) { - this._queue.push({ - event: 'stop', - action: () => { - this.stop(id); - } - }); - - return this; - } - - const ids = this._getSoundIds(id); - - for (let i = 0; i < ids.length; i++) { - this._clearTimer(ids[i]); - - const 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 && isGainNode(sound._node)) { - 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 (isHTMLAudioElement(sound._node) && (!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: boolean, id?: number): boolean | Howl { - - - 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; - } - } - - const ids = this._getSoundIds(id); - - for (let i = 0; i < ids.length; i++) { - const sound = this._soundById(ids[i]); - - if (sound) { - sound._muted = muted; - - if (sound._interval) { - this._stopFade(sound._id); - } - - if (this._webAudio && sound._node && isGainNode(sound._node)) { - sound._node.gain.setValueAtTime(muted ? 0 : sound._volume, Howler.ctx!.currentTime); - } else if (sound._node && isHTMLAudioElement(sound._node)) { - sound._node.muted = Howler._muted ? true : muted; - } - - this._emit('mute', sound._id); - } - } - - return this; - } - - volume(): number; - volume(vol: number): Howl; - volume(vol: number, id: number): Howl; - volume(vol: number, id: number, internal: boolean): Howl; - volume(vol?: number): number | Howl { - - const args = arguments; - let volume: number | undefined; - let id: number | undefined; - - if (args.length === 0) { - return this._volume; - } else if (args.length === 1 || (args.length === 2 && typeof args[1] === 'undefined')) { - const ids = this._getSoundIds(); - const index = ids.indexOf(args[0] as number); - if (index >= 0) { - id = parseInt(String(args[0]), 10); - } else { - volume = parseFloat(String(args[0])); - } - } else if (args.length >= 2) { - volume = parseFloat(String(args[0])); - id = parseInt(String(args[1]), 10); - } - - let sound; - if (typeof volume !== 'undefined' && volume >= 0 && volume <= 1) { - if (this._state !== 'loaded' || this._playLock) { - this._queue.push({ - event: 'volume', - action: () => { - if (args.length >= 1 && typeof args[0] === 'number') { - if (args.length >= 2 && typeof args[1] === 'number') { - this.volume(args[0], args[1]); - } else { - this.volume(args[0]); - } - } - } - }); - - return this; - } - - if (typeof id === 'undefined') { - this._volume = volume; - } - - const soundIds = this._getSoundIds(id); - for (let i = 0; i < soundIds.length; i++) { - sound = this._soundById(soundIds[i]); - - if (sound) { - sound._volume = volume; - - if (!args[2]) { - this._stopFade(soundIds[i]); - } - - if (this._webAudio && sound._node && isGainNode(sound._node) && !sound._muted) { - sound._node.gain.setValueAtTime(volume, Howler.ctx!.currentTime); - } else if (sound._node && isHTMLAudioElement(sound._node) && !sound._muted) { - const volumeMultiplierOrGlobal = Howler.volume(); - if (typeof volumeMultiplierOrGlobal === 'number') { - sound._node.volume = volume * volumeMultiplierOrGlobal; - } - } - - this._emit('volume', sound._id); - } - } - } else { - sound = id ? this._soundById(id) : this._sounds[0]; - return sound ? sound._volume : 0; - } - - return this; - } - - fade(from: number, to: number, len: number, id?: number): Howl { - - - 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(String(from))), 1); - to = Math.min(Math.max(0, parseFloat(String(to))), 1); - len = parseFloat(String(len)); - - if (typeof id !== 'undefined') { - this.volume(from, id); - } else { - this.volume(from); - } - - const ids = this._getSoundIds(id); - for (let i = 0; i < ids.length; i++) { - const sound = this._soundById(ids[i]); - - if (sound) { - if (!id) { - this._stopFade(ids[i]); - } - - if (this._webAudio && !sound._muted) { - const currentTime = Howler.ctx!.currentTime; - const end = currentTime + len / 1000; - sound._volume = from; - if (sound._node && isGainNode(sound._node)) { - 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: Sound, from: number, to: number, len: number, id: number, isGroup: boolean): void { - - let vol = from; - const diff = to - from; - const steps = Math.abs(diff / 0.01); - const stepLen = Math.max(4, steps > 0 ? len / steps : len); - let lastTick = Date.now(); - - sound._fadeTo = to; - - sound._interval = setInterval(() => { - const 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 (sound._interval) { - clearInterval(sound._interval); - } - sound._interval = undefined; - sound._fadeTo = undefined; - this.volume(to, sound._id); - this._emit('fade', sound._id); - } - }, stepLen); - } - - _stopFade(id: number): Howl { - - const sound = this._soundById(id); - - if (sound && sound._interval) { - if (this._webAudio && sound._node && isGainNode(sound._node)) { - sound._node.gain.cancelScheduledValues(Howler.ctx!.currentTime); - } - - if (sound._interval) { - clearInterval(sound._interval); - sound._interval = undefined; - } - this.volume(sound._fadeTo as number, id); - sound._fadeTo = undefined; - this._emit('fade', id); - } - - return this; - } - - loop(): boolean; - loop(loop: boolean): Howl; - loop(loop?: boolean): boolean | Howl { - - const args = arguments; - let loopVal: boolean | undefined; - let id: number | undefined; - let sound: Sound | null = null; - - if (args.length === 0) { - return this._loop; - } else if (args.length === 1) { - if (typeof args[0] === 'boolean') { - loopVal = args[0] as boolean; - this._loop = loopVal; - } else { - sound = this._soundById(parseInt(String(args[0]), 10)); - return sound ? sound._loop : false; - } - } else if (args.length === 2) { - loopVal = args[0] as boolean; - id = parseInt(String(args[1]), 10); - } - - const ids = this._getSoundIds(id); - for (let i = 0; i < ids.length; i++) { - sound = this._soundById(ids[i]); - - if (sound) { - sound._loop = loopVal as boolean; - if (this._webAudio && sound._node && isGainNode(sound._node) && sound._node.bufferSource) { - sound._node.bufferSource.loop = loopVal; - if (loopVal) { - 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(): number; - rate(rate: number): Howl; - rate(rate: number, id: number): Howl; - rate(rate?: number): number | Howl { - - const args = arguments; - let rateVal: number | undefined; - let id: number | undefined; - - if (args.length === 0) { - id = this._sounds[0]._id; - } else if (args.length === 1) { - const ids = this._getSoundIds(); - const index = ids.indexOf(args[0] as number); - if (index >= 0) { - id = parseInt(String(args[0]), 10); - } else { - rateVal = parseFloat(String(args[0])); - } - } else if (args.length === 2) { - rateVal = parseFloat(String(args[0])); - id = parseInt(String(args[1]), 10); - } - - let sound; - if (typeof rateVal === 'number') { - if (this._state !== 'loaded' || this._playLock) { - this._queue.push({ - event: 'rate', - action: () => { - if (args.length >= 1 && typeof args[0] === 'number') { - if (args.length >= 2 && typeof args[1] === 'number') { - this.rate(args[0], args[1]); - } else { - this.rate(args[0]); - } - } - } - }); - - return this; - } - - if (typeof id === 'undefined') { - this._rate = rateVal; - } - - const soundIds = this._getSoundIds(id); - for (let i = 0; i < soundIds.length; i++) { - sound = this._soundById(soundIds[i]); - - if (sound) { - if (this.playing(soundIds[i])) { - const seekResult = this.seek(soundIds[i]); - sound._rateSeek = typeof seekResult === 'number' ? seekResult : 0; - sound._playStart = this._webAudio ? Howler.ctx!.currentTime : sound._playStart; - } - sound._rate = rateVal; - - if (this._webAudio && sound._node && isGainNode(sound._node) && sound._node.bufferSource) { - sound._node.bufferSource.playbackRate.setValueAtTime(rateVal, Howler.ctx!.currentTime); - } else if (sound._node && isHTMLAudioElement(sound._node)) { - sound._node.playbackRate = rateVal; - } - - const seekResult = this.seek(soundIds[i]); - const seek = typeof seekResult === 'number' ? seekResult : 0; - const duration = (this._sprite[sound._sprite][0] + this._sprite[sound._sprite][1]) / 1000 - seek; - const timeout = (duration * 1000) / Math.abs(sound._rate); - - if (this._endTimers[soundIds[i]] || !sound._paused) { - this._clearTimer(soundIds[i]); - this._endTimers[soundIds[i]] = setTimeout(this._ended.bind(this, sound), timeout); - } - - this._emit('rate', sound._id); - } - } - } else { - if (typeof id !== 'undefined') { - sound = this._soundById(id); - return sound ? sound._rate : this._rate; - } - return this._rate; - } - - return this; - } - - seek(): number; - seek(seek: number): Howl; - seek(seek: number, id: number): Howl; - seek(seek?: number): number | Howl { - - const args = arguments; - let seekVal: number | undefined; - let id: number | undefined; - - if (args.length === 0) { - if (this._sounds.length) { - id = this._sounds[0]._id; - } - } else if (args.length === 1) { - const ids = this._getSoundIds(); - const index = ids.indexOf(args[0] as number); - if (index >= 0) { - id = parseInt(String(args[0]), 10); - } else if (this._sounds.length) { - id = this._sounds[0]._id; - seekVal = parseFloat(String(args[0])); - } - } else if (args.length === 2) { - seekVal = parseFloat(String(args[0])); - id = parseInt(String(args[1]), 10); - } - - if (typeof id === 'undefined') { - return 0; - } - - if (typeof seekVal === 'number' && (this._state !== 'loaded' || this._playLock)) { - this._queue.push({ - event: 'seek', - action: () => { - if (args.length >= 1 && typeof args[0] === 'number') { - if (args.length >= 2 && typeof args[1] === 'number') { - this.seek(args[0], args[1]); - } else { - this.seek(args[0]); - } - } - } - }); - - return this; - } - - const sound = this._soundById(id); - - if (sound) { - if (typeof seekVal === 'number' && seekVal >= 0) { - const playing = this.playing(id); - if (playing) { - this.pause(id, true); - } - - sound._seek = seekVal; - sound._ended = false; - this._clearTimer(id); - - if (!this._webAudio && sound._node && isHTMLAudioElement(sound._node) && !isNaN(sound._node.duration)) { - sound._node.currentTime = seekVal; - } - - 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) { - 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 if (sound._node && isHTMLAudioElement(sound._node)) { - return sound._node.currentTime; - } - return 0; - } - } - - return this; - } - - playing(id?: number): boolean { - - - if (typeof id === 'number') { - const sound = this._soundById(id); - return sound ? !sound._paused : false; - } - - for (let i = 0; i < this._sounds.length; i++) { - if (!this._sounds[i]._paused) { - return true; - } - } - - return false; - } - - duration(id?: number): number { - - let duration = this._duration; - - if (typeof id !== 'undefined') { - const sound = this._soundById(id); - if (sound) { - duration = this._sprite[sound._sprite][1] / 1000; - } - } - - return duration; - } - - state(): string { - return this._state; - } - - unload(): null { - - - // Execute plugin hooks before destruction - globalPluginManager.executeHowlDestroy(this); - - const sounds = this._sounds; - for (let i = 0; i < sounds.length; i++) { - if (!sounds[i]._paused) { - this.stop(sounds[i]._id); - } - - const node = sounds[i]._node; - if (!this._webAudio && node && isHTMLAudioElement(node)) { - this._clearSound(node); - - const errorFn = sounds[i]._errorFn; - if (errorFn) { - node.removeEventListener('error', errorFn, false); - } - const loadFn = sounds[i]._loadFn; - if (loadFn) { - node.removeEventListener(Howler._canPlayEvent as string, loadFn, false); - } - const endFn = sounds[i]._endFn; - if (endFn) { - node.removeEventListener('ended', endFn, false); - } - - Howler._releaseHtml5Audio(node); - } - - sounds[i]._node = null; - - this._clearTimer(sounds[i]._id); - } - - const index = Howler._howls.indexOf(this); - if (index >= 0) { - Howler._howls.splice(index, 1); - } - - let remCache = true; - for (let i = 0; i < Howler._howls.length; i++) { - if (Howler._howls[i]._src === this._src || (this._src as string).indexOf(Howler._howls[i]._src as string) >= 0) { - remCache = false; - break; - } - } - - if (cache && remCache) { - delete cache[this._src as string]; - } - - Howler.noAudio = false; - - this._state = 'unloaded'; - this._sounds = []; - - return null; - } - - on(event: string, fn: (...args: unknown[]) => void, id?: number, once?: boolean): Howl { - const events = (this as unknown as Record)[`_on${event}`]; - - if (typeof fn === 'function') { - events.push(once ? { id, fn, once } : { id, fn }); - } - - return this; - } - - off(event: string, fn?: (...args: unknown[]) => void, id?: number): Howl { - const events = (this as unknown as Record)[`_on${event}`]; - let i = 0; - - if (typeof fn === 'number') { - id = fn; - fn = undefined; - } - - if (fn || id) { - for (i = 0; i < events.length; i++) { - const isId = id === events[i].id; - if ((fn === events[i].fn && isId) || (!fn && isId)) { - events.splice(i, 1); - break; - } - } - } else if (event) { - (this as unknown as Record)[`_on${event}`] = []; - } else { - const keys = Object.keys(this); - for (i = 0; i < keys.length; i++) { - if (keys[i].indexOf('_on') === 0 && Array.isArray((this as unknown as Record)[keys[i]])) { - (this as unknown as Record)[keys[i]] = []; - } - } - } - - return this; - } - - once(event: string, fn: (...args: unknown[]) => void, id?: number): Howl { - - - this.on(event, fn, id, true); - - return this; - } - - _emit(event: string, id?: number | null, msg?: string): Howl { - const events = (this as unknown as Record)[`_on${event}`]; - - for (let i = events.length - 1; i >= 0; i--) { - if (!events[i].id || events[i].id === id || event === 'load') { - const fn = events[i].fn; - setTimeout(() => { - fn(id, msg); - }, 0); - - if (events[i].once) { - this.off(event, events[i].fn, events[i].id); - } - } - } - - this._loadQueue(event); - - return this; - } - - _loadQueue(event?: string): Howl { - - - if (this._queue.length > 0) { - const task = this._queue[0]; - - if (task.event === event) { - this._queue.shift(); - this._loadQueue(); - } - - if (!event) { - task.action(); - } - } - - return this; - } - - _ended(sound: Sound): Howl { - - const sprite = sound._sprite; - - if (!this._webAudio && sound._node && isHTMLAudioElement(sound._node) && !sound._node.paused && !sound._node.ended && sound._node.currentTime < sound._stop!) { - setTimeout(this._ended.bind(this, sound), 100); - return this; - } - - const 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) { - this._emit('play', sound._id); - sound._seek = sound._start || 0; - sound._rateSeek = 0; - sound._playStart = Howler.ctx!.currentTime; - - const timeout = ((sound._stop! - (sound._start || 0)) * 1000) / 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._autoSuspend(); - } - - if (!this._webAudio && !loop) { - this.stop(sound._id, true); - } - - return this; - } - - _clearTimer(id: number): Howl { - - - if (this._endTimers[id]) { - if (typeof this._endTimers[id] !== 'function') { - clearTimeout(this._endTimers[id]); - } else { - const sound = this._soundById(id); - if (sound && sound._node) { - sound._node.removeEventListener('ended', this._endTimers[id], false); - } - } - - delete this._endTimers[id]; - } - - return this; - } - - _soundById(id: number): Sound | null { - - - for (let i = 0; i < this._sounds.length; i++) { - if (id === this._sounds[i]._id) { - return this._sounds[i]; - } - } - - return null; - } - - _inactiveSound(): Sound { - - - this._drain(); - - for (let i = 0; i < this._sounds.length; i++) { - if (this._sounds[i]._ended) { - return this._sounds[i].reset(); - } - } - - return new Sound(this); - } - - _drain(): void { - - const limit = this._pool; - let cnt = 0; - - if (this._sounds.length < limit) { - return; - } - - for (let i = 0; i < this._sounds.length; i++) { - if (this._sounds[i]._ended) { - cnt++; - } - } - - for (let i = this._sounds.length - 1; i >= 0; i--) { - if (cnt <= limit) { - return; - } - - if (this._sounds[i]._ended) { - const node = this._sounds[i]._node; - if (this._webAudio && node && isGainNode(node)) { - node.disconnect(0); - } - - this._sounds.splice(i, 1); - cnt--; - } - } - } - - _getSoundIds(id?: number): number[] { - - - if (typeof id === 'undefined') { - const ids: number[] = []; - for (let i = 0; i < this._sounds.length; i++) { - ids.push(this._sounds[i]._id); - } - - return ids; - } else { - return [id]; - } - } - - _refreshBuffer(sound: Sound): Howl { - if (!sound._node || !isGainNode(sound._node) || !Howler.ctx) { - return this; - } - - sound._node.bufferSource = Howler.ctx.createBufferSource() as AudioBufferSourceNodeWithLegacy; - const src = typeof this._src === 'string' ? this._src : (Array.isArray(this._src) && this._src.length > 0 ? this._src[0] : ''); - sound._node.bufferSource.buffer = cache[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.ctx.currentTime); - - return this; - } - - _cleanBuffer(node: any): Howl { - - const isIOS = isAppleVendor(Howler._navigator); - - if (!node.bufferSource) { - return this; - } - - 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 this; - } - - _clearSound(node: HTMLAudioElementWithUnlocked): void { - if (!isIE(Howler._navigator)) { - node.src = 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA'; - } - } + _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: Record = {}; + _src: string | string[] = []; + _volume: number = 1; + _xhr: { + method: string; + headers?: Record; + withCredentials: boolean; + } = { method: "GET", withCredentials: false }; + _duration: number = 0; + _state: string = "unloaded"; + _sounds: Sound[] = []; + _endTimers: Record> = {}; + _queue: QueueItem[] = []; + _playLock: boolean = false; + _webAudio: boolean = false; + _onend: EventListener[] = []; + _onfade: EventListener[] = []; + _onload: EventListener[] = []; + _onloaderror: EventListener[] = []; + _onplayerror: EventListener[] = []; + _onpause: EventListener[] = []; + _onplay: EventListener[] = []; + _onstop: EventListener[] = []; + _onmute: EventListener[] = []; + _onvolume: EventListener[] = []; + _onrate: EventListener[] = []; + _onseek: EventListener[] = []; + _onunlock: EventListener[] = []; + _onresume: EventListener[] = []; + + constructor(o: HowlOptions) { + if (!o.src || o.src.length === 0) { + console.error( + "An array of source files must be passed with any new Howl.", + ); + return; + } + + this.init(o); + } + + init(o: HowlOptions): Howl { + if (!Howler.ctx) { + setupAudioContext(); + } + + this._autoplay = o.autoplay || false; + this._format = 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, + }; + + this._duration = 0; + this._state = "unloaded"; + this._sounds = []; + this._endTimers = {}; + this._queue = []; + this._playLock = 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: (...args: unknown[]) => { + if ( + o.onloaderror && + typeof args[0] === "number" && + typeof args[1] === "string" + ) { + o.onloaderror(args[0], args[1]); + } + }, + }, + ] + : []; + this._onplayerror = o.onplayerror + ? [ + { + fn: (...args: unknown[]) => { + if ( + o.onplayerror && + typeof args[0] === "number" && + typeof args[1] === "string" + ) { + o.onplayerror(args[0], args[1]); + } + }, + }, + ] + : []; + 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.usingWebAudio && !this._html5; + + if (typeof Howler.ctx !== "undefined" && Howler.ctx && Howler.autoUnlock) { + Howler._unlockAudio(); + } + + Howler._howls.push(this); + + // Execute plugin hooks + globalPluginManager.executeHowlCreate(this, o); + + if (this._autoplay) { + this._queue.push({ + event: "play", + action: () => { + this.play(); + }, + }); + } + + if (this._preload === true || this._preload === "metadata") { + this.load(); + } + + return this; + } + + load(): Howl { + let url: string | null = null; + + if (Howler.noAudio) { + this._emit("loaderror", null, "No audio support."); + return this; + } + + if (typeof this._src === "string") { + this._src = [this._src]; + } + + for (let i = 0; i < (this._src as string[]).length; i++) { + let ext: string | null; + const str = (this._src as string[])[i]; + + if (this._format && this._format[i]) { + ext = this._format[i]; + } else { + if (typeof str !== "string") { + this._emit( + "loaderror", + null, + "Non-string found in selected audio sources - ignoring.", + ); + continue; + } + + let extMatch = /^data:audio\/([^;,]+);/i.exec(str); + if (!extMatch) { + extMatch = /\.([^.]+)$/.exec(str.split("?", 1)[0]); + } + + ext = extMatch ? extMatch[1].toLowerCase() : null; + } + + if (!ext) { + console.warn( + 'No file extension was found. Consider using the "format" property or specify an extension.', + ); + } + + if (ext && Howler.codecs(ext)) { + url = (this._src as string[])[i]; + break; + } + } + + if (!url) { + this._emit( + "loaderror", + null, + "No codec support for selected audio sources.", + ); + return this; + } + + this._src = url; + this._state = "loading"; + + if ( + typeof window !== "undefined" && + window.location.protocol === "https:" && + url.slice(0, 5) === "http:" + ) { + this._html5 = true; + this._webAudio = false; + } + + new Sound(this); + + if (this._webAudio) { + loadBuffer(this); + } + + return this; + } + + play(sprite?: string | number, internal?: boolean): number | null { + let id: number | null = null; + + if (typeof sprite === "number") { + id = sprite; + sprite = undefined; + } else if ( + typeof sprite === "string" && + this._state === "loaded" && + !this._sprite[sprite] + ) { + return null; + } else if (typeof sprite === "undefined") { + sprite = "__default"; + + if (!this._playLock) { + let num = 0; + for (let 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 = undefined; + } 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 || "__default"; + sound._ended = false; + + const 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._autoResume(); + } + + const seek = Math.max( + 0, + sound._seek > 0 ? sound._seek : this._sprite[sprite!][0] / 1000, + ); + const duration = Math.max( + 0, + (this._sprite[sprite!][0] + this._sprite[sprite!][1]) / 1000 - seek, + ); + const timeout = (duration * 1000) / Math.abs(sound._rate); + const start = this._sprite[sprite!][0] / 1000; + const stop = (this._sprite[sprite!][0] + this._sprite[sprite!][1]) / 1000; + 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 sound._id; + } + + const node = sound._node; + + if (this._webAudio && node && isGainNode(node)) { + const playWebAudio = () => { + this._playLock = false; + setParams(); + this._refreshBuffer(sound); + + const vol = sound._muted || this._muted ? 0 : sound._volume; + node.gain.setValueAtTime(vol, Howler.ctx!.currentTime); + sound._playStart = Howler.ctx!.currentTime; + + if (node.bufferSource) { + if (typeof node.bufferSource.start === "undefined") { + node.bufferSource.noteGrainOn?.( + 0, + seek, + sound._loop ? 86400 : duration, + ); + } else { + node.bufferSource.start(0, seek, sound._loop ? 86400 : 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.state === "running" && Howler.ctx!.state !== "interrupted") { + playWebAudio(); + } else { + this._playLock = true; + this.once("resume", playWebAudio); + this._clearTimer(sound._id); + } + } else if (node && isHTMLAudioElement(node)) { + const playHtml5 = () => { + node.currentTime = seek; + node.muted = sound._muted || this._muted || Howler._muted || node.muted; + const volume = Howler.volume(); + node.volume = sound._volume * (typeof volume === "number" ? volume : 1); + node.playbackRate = sound._rate; + + try { + const play = node.play(); + + if ( + play && + typeof Promise !== "undefined" && + (play instanceof Promise || + typeof (play as any).then === "function") + ) { + this._playLock = true; + + setParams(); + + (play as any) + .then(() => { + this._playLock = false; + if ("_unlocked" in node) { + (node as HTMLAudioElementWithUnlocked)._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 { + const endHandler = () => { + this._ended(sound); + node.removeEventListener("ended", endHandler, false); + }; + this._endTimers[sound._id] = setTimeout(endHandler, timeout); + node.addEventListener("ended", endHandler, false); + } + } catch (err: unknown) { + this._emit( + "playerror", + sound._id, + err instanceof Error ? err.message : String(err), + ); + } + }; + + if ( + node.src === + "data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA" + ) { + const src = + typeof this._src === "string" + ? this._src + : Array.isArray(this._src) && this._src.length > 0 + ? this._src[0] + : ""; + node.src = src; + node.load(); + } + + const loadedNoReadyState = + typeof (window as WindowWithAudio).ejecta !== "undefined" || + (!node.readyState && Howler._navigator && Howler._navigator.isCocoonJS); + if (node.readyState >= 3 || loadedNoReadyState) { + playHtml5(); + } else { + this._playLock = true; + this._state = "loading"; + + const listener = () => { + this._state = "loaded"; + playHtml5(); + node.removeEventListener(Howler._canPlayEvent, listener, false); + }; + node.addEventListener(Howler._canPlayEvent, listener, false); + + this._clearTimer(sound._id); + } + } + + return sound._id; + } + + pause(id?: number, internal?: boolean): Howl { + if (this._state !== "loaded" || this._playLock) { + this._queue.push({ + event: "pause", + action: () => { + this.pause(id); + }, + }); + + return this; + } + + const ids = this._getSoundIds(id); + + for (let i = 0; i < ids.length; i++) { + this._clearTimer(ids[i]); + + const sound = this._soundById(ids[i]); + + if (sound && !sound._paused) { + const seekResult = this.seek(ids[i]); + sound._seek = typeof seekResult === "number" ? seekResult : 0; + sound._rateSeek = 0; + sound._paused = true; + + this._stopFade(ids[i]); + + if (sound._node) { + if (this._webAudio && isGainNode(sound._node)) { + 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 ( + isHTMLAudioElement(sound._node) && + (!isNaN(sound._node.duration) || sound._node.duration === Infinity) + ) { + sound._node.pause(); + } + } + } + + if (!arguments[1]) { + this._emit("pause", sound ? sound._id : null); + } + } + + return this; + } + + stop(id?: number, internal?: boolean): Howl { + if (this._state !== "loaded" || this._playLock) { + this._queue.push({ + event: "stop", + action: () => { + this.stop(id); + }, + }); + + return this; + } + + const ids = this._getSoundIds(id); + + for (let i = 0; i < ids.length; i++) { + this._clearTimer(ids[i]); + + const 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 && isGainNode(sound._node)) { + 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 ( + isHTMLAudioElement(sound._node) && + (!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: boolean, id?: number): boolean | Howl { + 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; + } + } + + const ids = this._getSoundIds(id); + + for (let i = 0; i < ids.length; i++) { + const sound = this._soundById(ids[i]); + + if (sound) { + sound._muted = muted; + + if (sound._interval) { + this._stopFade(sound._id); + } + + if (this._webAudio && sound._node && isGainNode(sound._node)) { + sound._node.gain.setValueAtTime( + muted ? 0 : sound._volume, + Howler.ctx!.currentTime, + ); + } else if (sound._node && isHTMLAudioElement(sound._node)) { + sound._node.muted = Howler._muted ? true : muted; + } + + this._emit("mute", sound._id); + } + } + + return this; + } + + volume(): number; + volume(vol: number): Howl; + volume(vol: number, id: number): Howl; + volume(vol: number, id: number, internal: boolean): Howl; + volume(vol?: number): number | Howl { + const args = arguments; + let volume: number | undefined; + let id: number | undefined; + + if (args.length === 0) { + return this._volume; + } else if ( + args.length === 1 || + (args.length === 2 && typeof args[1] === "undefined") + ) { + const ids = this._getSoundIds(); + const index = ids.indexOf(args[0] as number); + if (index >= 0) { + id = parseInt(String(args[0]), 10); + } else { + volume = parseFloat(String(args[0])); + } + } else if (args.length >= 2) { + volume = parseFloat(String(args[0])); + id = parseInt(String(args[1]), 10); + } + + let sound: Sound | null = null; + if (typeof volume !== "undefined" && volume >= 0 && volume <= 1) { + if (this._state !== "loaded" || this._playLock) { + this._queue.push({ + event: "volume", + action: () => { + if (args.length >= 1 && typeof args[0] === "number") { + if (args.length >= 2 && typeof args[1] === "number") { + this.volume(args[0], args[1]); + } else { + this.volume(args[0]); + } + } + }, + }); + + return this; + } + + if (typeof id === "undefined") { + this._volume = volume; + } + + const soundIds = this._getSoundIds(id); + for (let i = 0; i < soundIds.length; i++) { + sound = this._soundById(soundIds[i]); + + if (sound) { + sound._volume = volume; + + if (!args[2]) { + this._stopFade(soundIds[i]); + } + + if ( + this._webAudio && + sound._node && + isGainNode(sound._node) && + !sound._muted + ) { + sound._node.gain.setValueAtTime(volume, Howler.ctx!.currentTime); + } else if ( + sound._node && + isHTMLAudioElement(sound._node) && + !sound._muted + ) { + const volumeMultiplierOrGlobal = Howler.volume(); + if (typeof volumeMultiplierOrGlobal === "number") { + sound._node.volume = volume * volumeMultiplierOrGlobal; + } + } + + this._emit("volume", sound._id); + } + } + } else { + sound = id ? this._soundById(id) : this._sounds[0]; + return sound ? sound._volume : 0; + } + + return this; + } + + fade(from: number, to: number, len: number, id?: number): Howl { + 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(String(from))), 1); + to = Math.min(Math.max(0, parseFloat(String(to))), 1); + len = parseFloat(String(len)); + + if (typeof id !== "undefined") { + this.volume(from, id); + } else { + this.volume(from); + } + + const ids = this._getSoundIds(id); + for (let i = 0; i < ids.length; i++) { + const sound = this._soundById(ids[i]); + + if (sound) { + if (!id) { + this._stopFade(ids[i]); + } + + if (this._webAudio && !sound._muted) { + const currentTime = Howler.ctx!.currentTime; + const end = currentTime + len / 1000; + sound._volume = from; + if (sound._node && isGainNode(sound._node)) { + 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: Sound, + from: number, + to: number, + len: number, + id: number, + isGroup: boolean, + ): void { + let vol = from; + const diff = to - from; + const steps = Math.abs(diff / 0.01); + const stepLen = Math.max(4, steps > 0 ? len / steps : len); + let lastTick = Date.now(); + + sound._fadeTo = to; + + sound._interval = setInterval(() => { + const 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 (sound._interval) { + clearInterval(sound._interval); + } + sound._interval = undefined; + sound._fadeTo = undefined; + this.volume(to, sound._id); + this._emit("fade", sound._id); + } + }, stepLen); + } + + _stopFade(id: number): Howl { + const sound = this._soundById(id); + + if (sound && sound._interval) { + if (this._webAudio && sound._node && isGainNode(sound._node)) { + sound._node.gain.cancelScheduledValues(Howler.ctx!.currentTime); + } + + if (sound._interval) { + clearInterval(sound._interval); + sound._interval = undefined; + } + this.volume(sound._fadeTo as number, id); + sound._fadeTo = undefined; + this._emit("fade", id); + } + + return this; + } + + loop(): boolean; + loop(loop: boolean): Howl; + loop(loop?: boolean): boolean | Howl { + const args = arguments; + let loopVal: boolean | undefined; + let id: number | undefined; + let sound: Sound | null = null; + + if (args.length === 0) { + return this._loop; + } else if (args.length === 1) { + if (typeof args[0] === "boolean") { + loopVal = args[0] as boolean; + this._loop = loopVal; + } else { + sound = this._soundById(parseInt(String(args[0]), 10)); + return sound ? sound._loop : false; + } + } else if (args.length === 2) { + loopVal = args[0] as boolean; + id = parseInt(String(args[1]), 10); + } + + const ids = this._getSoundIds(id); + for (let i = 0; i < ids.length; i++) { + sound = this._soundById(ids[i]); + + if (sound) { + sound._loop = loopVal as boolean; + if ( + this._webAudio && + sound._node && + isGainNode(sound._node) && + sound._node.bufferSource + ) { + sound._node.bufferSource.loop = loopVal; + if (loopVal) { + 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(): number; + rate(rate: number): Howl; + rate(rate: number, id: number): Howl; + rate(rate?: number): number | Howl { + const args = arguments; + let rateVal: number | undefined; + let id: number | undefined; + + if (args.length === 0) { + id = this._sounds[0]._id; + } else if (args.length === 1) { + const ids = this._getSoundIds(); + const index = ids.indexOf(args[0] as number); + if (index >= 0) { + id = parseInt(String(args[0]), 10); + } else { + rateVal = parseFloat(String(args[0])); + } + } else if (args.length === 2) { + rateVal = parseFloat(String(args[0])); + id = parseInt(String(args[1]), 10); + } + + let sound: Sound | null = null; + if (typeof rateVal === "number") { + if (this._state !== "loaded" || this._playLock) { + this._queue.push({ + event: "rate", + action: () => { + if (args.length >= 1 && typeof args[0] === "number") { + if (args.length >= 2 && typeof args[1] === "number") { + this.rate(args[0], args[1]); + } else { + this.rate(args[0]); + } + } + }, + }); + + return this; + } + + if (typeof id === "undefined") { + this._rate = rateVal; + } + + const soundIds = this._getSoundIds(id); + for (let i = 0; i < soundIds.length; i++) { + sound = this._soundById(soundIds[i]); + + if (sound) { + if (this.playing(soundIds[i])) { + const seekResult = this.seek(soundIds[i]); + sound._rateSeek = typeof seekResult === "number" ? seekResult : 0; + sound._playStart = this._webAudio + ? Howler.ctx!.currentTime + : sound._playStart; + } + sound._rate = rateVal; + + if ( + this._webAudio && + sound._node && + isGainNode(sound._node) && + sound._node.bufferSource + ) { + sound._node.bufferSource.playbackRate.setValueAtTime( + rateVal, + Howler.ctx!.currentTime, + ); + } else if (sound._node && isHTMLAudioElement(sound._node)) { + sound._node.playbackRate = rateVal; + } + + const seekResult = this.seek(soundIds[i]); + const seek = typeof seekResult === "number" ? seekResult : 0; + const duration = + (this._sprite[sound._sprite][0] + this._sprite[sound._sprite][1]) / + 1000 - + seek; + const timeout = (duration * 1000) / Math.abs(sound._rate); + + if (this._endTimers[soundIds[i]] || !sound._paused) { + this._clearTimer(soundIds[i]); + this._endTimers[soundIds[i]] = setTimeout( + this._ended.bind(this, sound), + timeout, + ); + } + + this._emit("rate", sound._id); + } + } + } else { + if (typeof id !== "undefined") { + sound = this._soundById(id); + return sound ? sound._rate : this._rate; + } + return this._rate; + } + + return this; + } + + seek(): number; + seek(seek: number): Howl; + seek(seek: number, id: number): Howl; + seek(seek?: number): number | Howl { + const args = arguments; + let seekVal: number | undefined; + let id: number | undefined; + + if (args.length === 0) { + if (this._sounds.length) { + id = this._sounds[0]._id; + } + } else if (args.length === 1) { + const ids = this._getSoundIds(); + const index = ids.indexOf(args[0] as number); + if (index >= 0) { + id = parseInt(String(args[0]), 10); + } else if (this._sounds.length) { + id = this._sounds[0]._id; + seekVal = parseFloat(String(args[0])); + } + } else if (args.length === 2) { + seekVal = parseFloat(String(args[0])); + id = parseInt(String(args[1]), 10); + } + + if (typeof id === "undefined") { + return 0; + } + + if ( + typeof seekVal === "number" && + (this._state !== "loaded" || this._playLock) + ) { + this._queue.push({ + event: "seek", + action: () => { + if (args.length >= 1 && typeof args[0] === "number") { + if (args.length >= 2 && typeof args[1] === "number") { + this.seek(args[0], args[1]); + } else { + this.seek(args[0]); + } + } + }, + }); + + return this; + } + + const sound = this._soundById(id); + + if (sound) { + if (typeof seekVal === "number" && seekVal >= 0) { + const playing = this.playing(id); + if (playing) { + this.pause(id, true); + } + + sound._seek = seekVal; + sound._ended = false; + this._clearTimer(id); + + if ( + !this._webAudio && + sound._node && + isHTMLAudioElement(sound._node) && + !isNaN(sound._node.duration) + ) { + sound._node.currentTime = seekVal; + } + + 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) { + 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 if (sound._node && isHTMLAudioElement(sound._node)) { + return sound._node.currentTime; + } + return 0; + } + } + + return this; + } + + playing(id?: number): boolean { + if (typeof id === "number") { + const sound = this._soundById(id); + return sound ? !sound._paused : false; + } + + for (let i = 0; i < this._sounds.length; i++) { + if (!this._sounds[i]._paused) { + return true; + } + } + + return false; + } + + duration(id?: number): number { + let duration = this._duration; + + if (typeof id !== "undefined") { + const sound = this._soundById(id); + if (sound) { + duration = this._sprite[sound._sprite][1] / 1000; + } + } + + return duration; + } + + state(): string { + return this._state; + } + + unload(): null { + // Execute plugin hooks before destruction + globalPluginManager.executeHowlDestroy(this); + + const sounds = this._sounds; + for (let i = 0; i < sounds.length; i++) { + if (!sounds[i]._paused) { + this.stop(sounds[i]._id); + } + + const node = sounds[i]._node; + if (!this._webAudio && node && isHTMLAudioElement(node)) { + this._clearSound(node); + + const errorFn = sounds[i]._errorFn; + if (errorFn) { + node.removeEventListener("error", errorFn, false); + } + const loadFn = sounds[i]._loadFn; + if (loadFn) { + node.removeEventListener( + Howler._canPlayEvent as string, + loadFn, + false, + ); + } + const endFn = sounds[i]._endFn; + if (endFn) { + node.removeEventListener("ended", endFn, false); + } + + Howler._releaseHtml5Audio(node); + } + + sounds[i]._node = null; + + this._clearTimer(sounds[i]._id); + } + + const index = Howler._howls.indexOf(this); + if (index >= 0) { + Howler._howls.splice(index, 1); + } + + let remCache = true; + for (let i = 0; i < Howler._howls.length; i++) { + if ( + Howler._howls[i]._src === this._src || + (this._src as string).indexOf(Howler._howls[i]._src as string) >= 0 + ) { + remCache = false; + break; + } + } + + if (cache && remCache) { + delete cache[this._src as string]; + } + + Howler.noAudio = false; + + this._state = "unloaded"; + this._sounds = []; + + return null; + } + + on( + event: string, + fn: (...args: unknown[]) => void, + id?: number, + once?: boolean, + ): Howl { + const events = (this as unknown as Record)[ + `_on${event}` + ]; + + if (typeof fn === "function") { + events.push(once ? { id, fn, once } : { id, fn }); + } + + return this; + } + + off(event: string, fn?: (...args: unknown[]) => void, id?: number): Howl { + const events = (this as unknown as Record)[ + `_on${event}` + ]; + let i = 0; + + if (typeof fn === "number") { + id = fn; + fn = undefined; + } + + if (fn || id) { + for (i = 0; i < events.length; i++) { + const isId = id === events[i].id; + if ((fn === events[i].fn && isId) || (!fn && isId)) { + events.splice(i, 1); + break; + } + } + } else if (event) { + (this as unknown as Record)[`_on${event}`] = []; + } else { + const keys = Object.keys(this); + for (i = 0; i < keys.length; i++) { + if ( + keys[i].indexOf("_on") === 0 && + Array.isArray( + (this as unknown as Record)[keys[i]], + ) + ) { + (this as unknown as Record)[keys[i]] = []; + } + } + } + + return this; + } + + once(event: string, fn: (...args: unknown[]) => void, id?: number): Howl { + this.on(event, fn, id, true); + + return this; + } + + _emit(event: string, id?: number | null, msg?: string): Howl { + const events = (this as unknown as Record)[ + `_on${event}` + ]; + + for (let i = events.length - 1; i >= 0; i--) { + if (!events[i].id || events[i].id === id || event === "load") { + const fn = events[i].fn; + setTimeout(() => { + fn(id, msg); + }, 0); + + if (events[i].once) { + this.off(event, events[i].fn, events[i].id); + } + } + } + + this._loadQueue(event); + + return this; + } + + _loadQueue(event?: string): Howl { + if (this._queue.length > 0) { + const task = this._queue[0]; + + if (task.event === event) { + this._queue.shift(); + this._loadQueue(); + } + + if (!event) { + task.action(); + } + } + + return this; + } + + _ended(sound: Sound): Howl { + const sprite = sound._sprite; + + if ( + !this._webAudio && + sound._node && + isHTMLAudioElement(sound._node) && + !sound._node.paused && + !sound._node.ended && + sound._node.currentTime < sound._stop! + ) { + setTimeout(this._ended.bind(this, sound), 100); + return this; + } + + const 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) { + this._emit("play", sound._id); + sound._seek = sound._start || 0; + sound._rateSeek = 0; + sound._playStart = Howler.ctx!.currentTime; + + const timeout = + ((sound._stop! - (sound._start || 0)) * 1000) / 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._autoSuspend(); + } + + if (!this._webAudio && !loop) { + this.stop(sound._id, true); + } + + return this; + } + + _clearTimer(id: number): Howl { + if (this._endTimers[id]) { + if (typeof this._endTimers[id] !== "function") { + clearTimeout(this._endTimers[id]); + } else { + const sound = this._soundById(id); + if (sound && sound._node) { + sound._node.removeEventListener("ended", this._endTimers[id], false); + } + } + + delete this._endTimers[id]; + } + + return this; + } + + _soundById(id: number): Sound | null { + for (let i = 0; i < this._sounds.length; i++) { + if (id === this._sounds[i]._id) { + return this._sounds[i]; + } + } + + return null; + } + + _inactiveSound(): Sound { + this._drain(); + + for (let i = 0; i < this._sounds.length; i++) { + if (this._sounds[i]._ended) { + return this._sounds[i].reset(); + } + } + + return new Sound(this); + } + + _drain(): void { + const limit = this._pool; + let cnt = 0; + + if (this._sounds.length < limit) { + return; + } + + for (let i = 0; i < this._sounds.length; i++) { + if (this._sounds[i]._ended) { + cnt++; + } + } + + for (let i = this._sounds.length - 1; i >= 0; i--) { + if (cnt <= limit) { + return; + } + + if (this._sounds[i]._ended) { + const node = this._sounds[i]._node; + if (this._webAudio && node && isGainNode(node)) { + node.disconnect(0); + } + + this._sounds.splice(i, 1); + cnt--; + } + } + } + + _getSoundIds(id?: number): number[] { + if (typeof id === "undefined") { + const ids: number[] = []; + for (let i = 0; i < this._sounds.length; i++) { + ids.push(this._sounds[i]._id); + } + + return ids; + } else { + return [id]; + } + } + + _refreshBuffer(sound: Sound): Howl { + if (!sound._node || !isGainNode(sound._node) || !Howler.ctx) { + return this; + } + + sound._node.bufferSource = + Howler.ctx.createBufferSource() as AudioBufferSourceNodeWithLegacy; + const src = + typeof this._src === "string" + ? this._src + : Array.isArray(this._src) && this._src.length > 0 + ? this._src[0] + : ""; + sound._node.bufferSource.buffer = cache[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.ctx.currentTime, + ); + + return this; + } + + _cleanBuffer(node: any): Howl { + const isIOS = isAppleVendor(Howler._navigator); + + if (!node.bufferSource) { + return this; + } + + 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 this; + } + + _clearSound(node: HTMLAudioElementWithUnlocked): void { + if (!isIE(Howler._navigator)) { + node.src = + "data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA"; + } + } } /** @@ -1988,12 +2271,19 @@ class Howl { * These methods are added dynamically by the SpatialAudioPlugin at runtime. */ export interface HowlerInstance extends HowlerGlobal { - // Optional spatial audio methods added by plugin - _pos?: [number, number, number]; - _orientation?: [number, number, number, number, number, number]; - pos?(x?: number, y?: number, z?: number): any; - orientation?(x?: number, y?: number, z?: number, xUp?: number, yUp?: number, zUp?: number): any; - stereo?(pan?: number): any; + // Optional spatial audio methods added by plugin + _pos?: [number, number, number]; + _orientation?: [number, number, number, number, number, number]; + pos?(x?: number, y?: number, z?: number): any; + orientation?( + x?: number, + y?: number, + z?: number, + xUp?: number, + yUp?: number, + zUp?: number, + ): any; + stereo?(pan?: number): any; } /** @@ -2001,20 +2291,20 @@ export interface HowlerInstance extends HowlerGlobal { * These methods are added dynamically by the SpatialAudioPlugin at runtime. */ export interface HowlInstance extends Howl { - // Optional spatial audio properties added by plugin - _pos?: [number, number, number] | null; - _orientation?: [number, number, number]; - _stereo?: number | null; - _pannerAttr?: any; - - // Optional spatial audio methods added by plugin - pos?(x?: number, y?: number, z?: number, id?: number): any; - orientation?(x?: number, y?: number, z?: number, id?: number): any; - stereo?(pan?: number, id?: number): any; - pannerAttr?(o?: any, id?: number): any; + // Optional spatial audio properties added by plugin + _pos?: [number, number, number] | null; + _orientation?: [number, number, number]; + _stereo?: number | null; + _pannerAttr?: any; + + // Optional spatial audio methods added by plugin + pos?(x?: number, y?: number, z?: number, id?: number): any; + orientation?(x?: number, y?: number, z?: number, id?: number): any; + stereo?(pan?: number, id?: number): any; + pannerAttr?(o?: any, id?: number): any; } // Export for ESM -export * from './types'; +export * from "./types"; export { Howl, Howler, Sound }; export default { Howler, Howl, Sound }; diff --git a/src/index.ts b/src/index.ts index 11abd614..0421ce63 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,9 +4,9 @@ */ // Core library exports -export { Howl, Howler, Sound } from './howler.core'; -export type { HowlOptions } from './types'; +export { Howl, Howler, Sound } from "./howler.core"; +export type { PluginHooks } from "./plugins"; // Plugin system exports -export { globalPluginManager, HowlerPlugin, PluginManager } from './plugins'; -export type { PluginHooks } from './plugins'; +export { globalPluginManager, HowlerPlugin, PluginManager } from "./plugins"; +export type { HowlOptions } from "./types"; diff --git a/src/plugins/index.ts b/src/plugins/index.ts index bc48bae0..bf3f7e81 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -8,6 +8,6 @@ * MIT License */ +export type { PluginHooks, RegisteredPlugin } from "./plugin"; // Core plugin infrastructure -export { HowlerPlugin, PluginManager, globalPluginManager } from './plugin'; -export type { PluginHooks, RegisteredPlugin } from './plugin'; +export { globalPluginManager, HowlerPlugin, PluginManager } from "./plugin"; diff --git a/src/plugins/plugin.ts b/src/plugins/plugin.ts index e710cb54..e2c7d957 100644 --- a/src/plugins/plugin.ts +++ b/src/plugins/plugin.ts @@ -8,40 +8,40 @@ * MIT License */ -import type { Howl, HowlerGlobal, Sound } from '../howler.core'; -import type { HowlOptions } from '../types'; +import type { Howl, HowlerGlobal, Sound } from "../howler.core"; +import type { HowlOptions } from "../types"; /** * Plugin hook lifecycle events */ export interface PluginHooks { - /** - * Called when Howler global instance is initialized - * This is called either: - * - When Howler initializes (if plugin was registered before) - * - Immediately during registration (if Howler is already initialized) - */ - onHowlerInit?: (howler: HowlerGlobal) => void; - - /** - * Called when a new Howl instance is created - */ - onHowlCreate?: (howl: Howl, options: HowlOptions) => void; - - /** - * Called when a Sound instance is created - */ - onSoundCreate?: (sound: Sound, parent: Howl) => void; - - /** - * Called when a Howl instance is loaded - */ - onHowlLoad?: (howl: Howl) => void; - - /** - * Called when a Howl instance is destroyed - */ - onHowlDestroy?: (howl: Howl) => void; + /** + * Called when Howler global instance is initialized + * This is called either: + * - When Howler initializes (if plugin was registered before) + * - Immediately during registration (if Howler is already initialized) + */ + onHowlerInit?: (howler: HowlerGlobal) => void; + + /** + * Called when a new Howl instance is created + */ + onHowlCreate?: (howl: Howl, options: HowlOptions) => void; + + /** + * Called when a Sound instance is created + */ + onSoundCreate?: (sound: Sound, parent: Howl) => void; + + /** + * Called when a Howl instance is loaded + */ + onHowlLoad?: (howl: Howl) => void; + + /** + * Called when a Howl instance is destroyed + */ + onHowlDestroy?: (howl: Howl) => void; } /** @@ -49,188 +49,200 @@ export interface PluginHooks { * Extend this class to create a custom Howler plugin */ export abstract class HowlerPlugin { - /** - * Unique plugin name (must be unique across all plugins) - */ - abstract readonly name: string; - - /** - * Plugin version (optional) - */ - readonly version?: string; - - /** - * Get the hooks provided by this plugin - */ - abstract getHooks(): PluginHooks; - - /** - * Called when plugin is about to be unregistered - * Use this to clean up resources - */ - onUnregister?(): void; + /** + * Unique plugin name (must be unique across all plugins) + */ + abstract readonly name: string; + + /** + * Plugin version (optional) + */ + readonly version?: string; + + /** + * Get the hooks provided by this plugin + */ + abstract getHooks(): PluginHooks; + + /** + * Called when plugin is about to be unregistered + * Use this to clean up resources + */ + onUnregister?(): void; } /** * Plugin registration metadata */ export interface RegisteredPlugin { - plugin: HowlerPlugin; - hooks: PluginHooks; + plugin: HowlerPlugin; + hooks: PluginHooks; } /** * Plugin manager for registering and executing plugins */ export class PluginManager { - private plugins: Map = new Map(); - private howlerInstance: HowlerGlobal | null = null; - - /** - * Register a plugin - * @param plugin - The plugin to register - * @throws Error if plugin name already exists - */ - register(plugin: HowlerPlugin): void { - if (this.plugins.has(plugin.name)) { - throw new Error(`Plugin "${plugin.name}" is already registered`); - } - - const hooks = plugin.getHooks(); - const registered: RegisteredPlugin = { - plugin, - hooks - }; - - this.plugins.set(plugin.name, registered); - - // If Howler is already initialized, execute onHowlerInit hook for this plugin - if (this.howlerInstance && hooks.onHowlerInit) { - try { - hooks.onHowlerInit(this.howlerInstance); - } catch (error: unknown) { - console.error(`Error during onHowlerInit for plugin "${plugin.name}":`, error); - } - } - } - - /** - * Unregister a plugin - * @param pluginName - The name of the plugin to unregister - */ - unregister(pluginName: string): void { - const registered = this.plugins.get(pluginName); - if (!registered) { - throw new Error(`Plugin "${pluginName}" is not registered`); - } - - // Call cleanup hook - if (registered.plugin.onUnregister) { - try { - registered.plugin.onUnregister(); - } catch (error) { - console.error(`Error during onUnregister for plugin "${pluginName}":`, error); - } - } - - this.plugins.delete(pluginName); - } - - /** - * Check if a plugin is registered - * @param pluginName - The name of the plugin - */ - isRegistered(pluginName: string): boolean { - return this.plugins.has(pluginName); - } - - /** - * Set the Howler instance reference for late-registered plugins - * Also executes onHowlerInit hooks for any plugins already registered - * @internal - */ - setHowlerInstance(howler: HowlerGlobal): void { - this.howlerInstance = howler; - - // Execute onHowlerInit hooks for all registered plugins - this._executeHooks('onHowlerInit', (hooks) => { - if (hooks.onHowlerInit) { - hooks.onHowlerInit(howler); - } - }); - } - - /** - * Get the Howler instance (if initialized) - * This can be used by plugins to access the Howler instance if needed - */ - getHowlerInstance(): HowlerGlobal | null { - return this.howlerInstance; - } - - /** - * Get all registered plugins - */ - getPlugins(): ReadonlyMap { - return new Map(this.plugins); - } - - /** - * Execute onHowlCreate hooks - */ - executeHowlCreate(howl: Howl, options: HowlOptions): void { - this._executeHooks('onHowlCreate', (hooks) => { - if (hooks.onHowlCreate) { - hooks.onHowlCreate(howl, options); - } - }); - } - - /** - * Execute onSoundCreate hooks - */ - executeSoundCreate(sound: Sound, parent: Howl): void { - this._executeHooks('onSoundCreate', (hooks) => { - if (hooks.onSoundCreate) { - hooks.onSoundCreate(sound, parent); - } - }); - } - - /** - * Execute onHowlLoad hooks - */ - executeHowlLoad(howl: Howl): void { - this._executeHooks('onHowlLoad', (hooks) => { - if (hooks.onHowlLoad) { - hooks.onHowlLoad(howl); - } - }); - } - - /** - * Execute onHowlDestroy hooks - */ - executeHowlDestroy(howl: Howl): void { - this._executeHooks('onHowlDestroy', (hooks) => { - if (hooks.onHowlDestroy) { - hooks.onHowlDestroy(howl); - } - }); - } - - /** - * Internal hook execution with error handling - */ - private _executeHooks(hookName: string, callback: (hooks: PluginHooks) => void): void { - for (const [pluginName, registered] of this.plugins) { - try { - callback(registered.hooks); - } catch (error) { - console.error(`Error in hook "${hookName}" for plugin "${pluginName}":`, error); - } - } - } + private plugins: Map = new Map(); + private howlerInstance: HowlerGlobal | null = null; + + /** + * Register a plugin + * @param plugin - The plugin to register + * @throws Error if plugin name already exists + */ + register(plugin: HowlerPlugin): void { + if (this.plugins.has(plugin.name)) { + throw new Error(`Plugin "${plugin.name}" is already registered`); + } + + const hooks = plugin.getHooks(); + const registered: RegisteredPlugin = { + plugin, + hooks, + }; + + this.plugins.set(plugin.name, registered); + + // If Howler is already initialized, execute onHowlerInit hook for this plugin + if (this.howlerInstance && hooks.onHowlerInit) { + try { + hooks.onHowlerInit(this.howlerInstance); + } catch (error: unknown) { + console.error( + `Error during onHowlerInit for plugin "${plugin.name}":`, + error, + ); + } + } + } + + /** + * Unregister a plugin + * @param pluginName - The name of the plugin to unregister + */ + unregister(pluginName: string): void { + const registered = this.plugins.get(pluginName); + if (!registered) { + throw new Error(`Plugin "${pluginName}" is not registered`); + } + + // Call cleanup hook + if (registered.plugin.onUnregister) { + try { + registered.plugin.onUnregister(); + } catch (error) { + console.error( + `Error during onUnregister for plugin "${pluginName}":`, + error, + ); + } + } + + this.plugins.delete(pluginName); + } + + /** + * Check if a plugin is registered + * @param pluginName - The name of the plugin + */ + isRegistered(pluginName: string): boolean { + return this.plugins.has(pluginName); + } + + /** + * Set the Howler instance reference for late-registered plugins + * Also executes onHowlerInit hooks for any plugins already registered + * @internal + */ + setHowlerInstance(howler: HowlerGlobal): void { + this.howlerInstance = howler; + + // Execute onHowlerInit hooks for all registered plugins + this._executeHooks("onHowlerInit", (hooks) => { + if (hooks.onHowlerInit) { + hooks.onHowlerInit(howler); + } + }); + } + + /** + * Get the Howler instance (if initialized) + * This can be used by plugins to access the Howler instance if needed + */ + getHowlerInstance(): HowlerGlobal | null { + return this.howlerInstance; + } + + /** + * Get all registered plugins + */ + getPlugins(): ReadonlyMap { + return new Map(this.plugins); + } + + /** + * Execute onHowlCreate hooks + */ + executeHowlCreate(howl: Howl, options: HowlOptions): void { + this._executeHooks("onHowlCreate", (hooks) => { + if (hooks.onHowlCreate) { + hooks.onHowlCreate(howl, options); + } + }); + } + + /** + * Execute onSoundCreate hooks + */ + executeSoundCreate(sound: Sound, parent: Howl): void { + this._executeHooks("onSoundCreate", (hooks) => { + if (hooks.onSoundCreate) { + hooks.onSoundCreate(sound, parent); + } + }); + } + + /** + * Execute onHowlLoad hooks + */ + executeHowlLoad(howl: Howl): void { + this._executeHooks("onHowlLoad", (hooks) => { + if (hooks.onHowlLoad) { + hooks.onHowlLoad(howl); + } + }); + } + + /** + * Execute onHowlDestroy hooks + */ + executeHowlDestroy(howl: Howl): void { + this._executeHooks("onHowlDestroy", (hooks) => { + if (hooks.onHowlDestroy) { + hooks.onHowlDestroy(howl); + } + }); + } + + /** + * Internal hook execution with error handling + */ + private _executeHooks( + hookName: string, + callback: (hooks: PluginHooks) => void, + ): void { + for (const [pluginName, registered] of this.plugins) { + try { + callback(registered.hooks); + } catch (error) { + console.error( + `Error in hook "${hookName}" for plugin "${pluginName}":`, + error, + ); + } + } + } } /** diff --git a/src/plugins/spatial-plugin.ts b/src/plugins/spatial-plugin.ts index 6c161d5f..04c91949 100644 --- a/src/plugins/spatial-plugin.ts +++ b/src/plugins/spatial-plugin.ts @@ -9,283 +9,383 @@ * MIT License */ -import type { HowlOptions } from '../howler.core'; -import { Howl, Howler, HowlerGlobal, Sound } from '../howler.core'; -import { isGainNode } from '../types'; -import { HowlerPlugin, type PluginHooks, globalPluginManager } from './plugin'; +import type { HowlOptions } from "../howler.core"; +import { + type Howl, + Howler, + type HowlerGlobal, + type Sound, +} from "../howler.core"; +import { isGainNode } from "../types"; +import { globalPluginManager, HowlerPlugin, type PluginHooks } from "./plugin"; /** * Extended HowlOptions with spatial audio properties */ export interface SpatialHowlOptions extends HowlOptions { - pos?: [number, number, number]; - orientation?: [number, number, number]; - stereo?: number; - coneInnerAngle?: number; - coneOuterAngle?: number; - coneOuterGain?: number; - distanceModel?: 'linear' | 'inverse' | 'exponential'; - maxDistance?: number; - panningModel?: 'equalpower' | 'HRTF'; - refDistance?: number; - rolloffFactor?: number; - onstereo?: () => void; - onpos?: () => void; - onorientation?: () => void; + pos?: [number, number, number]; + orientation?: [number, number, number]; + stereo?: number; + coneInnerAngle?: number; + coneOuterAngle?: number; + coneOuterGain?: number; + distanceModel?: "linear" | "inverse" | "exponential"; + maxDistance?: number; + panningModel?: "equalpower" | "HRTF"; + refDistance?: number; + rolloffFactor?: number; + onstereo?: () => void; + onpos?: () => void; + onorientation?: () => void; } /** * Spatial audio properties for HowlerGlobal */ export interface SpatialAudioState { - _pos: [number, number, number]; - _orientation: [number, number, number, number, number, number]; + _pos: [number, number, number]; + _orientation: [number, number, number, number, number, number]; } /** * Spatial audio properties for Howl */ export interface SpatialHowlState { - _pos: [number, number, number] | null; - _orientation: [number, number, number]; - _stereo: number | null; - _pannerAttr: { - coneInnerAngle: number; - coneOuterAngle: number; - coneOuterGain: number; - distanceModel: 'linear' | 'inverse' | 'exponential'; - maxDistance: number; - panningModel: 'equalpower' | 'HRTF'; - refDistance: number; - rolloffFactor: number; - }; - _onstereo: Array<{ fn: () => void }>; - _onpos: Array<{ fn: () => void }>; - _onorientation: Array<{ fn: () => void }>; + _pos: [number, number, number] | null; + _orientation: [number, number, number]; + _stereo: number | null; + _pannerAttr: { + coneInnerAngle: number; + coneOuterAngle: number; + coneOuterGain: number; + distanceModel: "linear" | "inverse" | "exponential"; + maxDistance: number; + panningModel: "equalpower" | "HRTF"; + refDistance: number; + rolloffFactor: number; + }; + _onstereo: Array<{ fn: () => void }>; + _onpos: Array<{ fn: () => void }>; + _onorientation: Array<{ fn: () => void }>; } /** * Spatial audio properties for Sound */ export interface SpatialSoundState { - _pos: [number, number, number] | null; - _orientation: [number, number, number]; - _stereo: number | null; - _pannerAttr: { - coneInnerAngle: number; - coneOuterAngle: number; - coneOuterGain: number; - distanceModel: 'linear' | 'inverse' | 'exponential'; - maxDistance: number; - panningModel: 'equalpower' | 'HRTF'; - refDistance: number; - rolloffFactor: number; - }; + _pos: [number, number, number] | null; + _orientation: [number, number, number]; + _stereo: number | null; + _pannerAttr: { + coneInnerAngle: number; + coneOuterAngle: number; + coneOuterGain: number; + distanceModel: "linear" | "inverse" | "exponential"; + maxDistance: number; + panningModel: "equalpower" | "HRTF"; + refDistance: number; + rolloffFactor: number; + }; } /** * Howler instance with spatial audio capabilities * Use this type when the spatial plugin is registered - * + * * @example * ```typescript * import { Howler } from 'howler'; * import { SpatialAudioPlugin, type SpatialHowler } from 'howler/plugins/spatial'; - * + * * Howler.addPlugin(new SpatialAudioPlugin()); - * + * * const howler: SpatialHowler = Howler as SpatialHowler; * howler.pos(10, 20, 30); * ``` */ -export type SpatialHowler = HowlerGlobal & SpatialAudioState & { - pos(x?: number, y?: number, z?: number): SpatialHowler | [number, number, number]; - orientation(x?: number, y?: number, z?: number, xUp?: number, yUp?: number, zUp?: number): SpatialHowler | [number, number, number, number, number, number]; - stereo(pan?: number): SpatialHowler; -}; +export type SpatialHowler = HowlerGlobal & + SpatialAudioState & { + pos( + x?: number, + y?: number, + z?: number, + ): SpatialHowler | [number, number, number]; + orientation( + x?: number, + y?: number, + z?: number, + xUp?: number, + yUp?: number, + zUp?: number, + ): SpatialHowler | [number, number, number, number, number, number]; + stereo(pan?: number): SpatialHowler; + }; /** * Howl instance with spatial audio capabilities * Use this type when the spatial plugin is registered - * + * * @example * ```typescript * import { Howl } from 'howler'; * import { SpatialAudioPlugin, type SpatialHowl, type SpatialHowlOptions } from 'howler/plugins/spatial'; - * + * * Howler.addPlugin(new SpatialAudioPlugin()); - * + * * const sound: SpatialHowl = new Howl({ * src: ['sound.mp3'], * pos: [10, 20, 30] * } as SpatialHowlOptions) as SpatialHowl; - * + * * sound.pos(5, 10, 15); * sound.stereo(0.5); * ``` */ -export type SpatialHowl = Howl & SpatialHowlState & { - pos(x?: number, y?: number, z?: number, id?: number): SpatialHowl | [number, number, number]; - orientation(x?: number, y?: number, z?: number, id?: number): SpatialHowl | [number, number, number]; - stereo(pan?: number, id?: number): SpatialHowl | number; - pannerAttr(o?: any, id?: number): SpatialHowl | any; -}; +export type SpatialHowl = Howl & + SpatialHowlState & { + pos( + x?: number, + y?: number, + z?: number, + id?: number, + ): SpatialHowl | [number, number, number]; + orientation( + x?: number, + y?: number, + z?: number, + id?: number, + ): SpatialHowl | [number, number, number]; + stereo(pan?: number, id?: number): SpatialHowl | number; + pannerAttr(o?: any, id?: number): SpatialHowl | any; + }; /** * Setup a panner node for a sound */ -function setupPanner(sound: Sound & SpatialSoundState, type: 'stereo' | 'spatial' = 'spatial'): void { - if (!Howler.ctx) { - return; - } - - // Create the new panner node - if (type === 'spatial') { - sound._panner = Howler.ctx.createPanner(); - const panner = sound._panner as PannerNode; - panner.coneInnerAngle = sound._pannerAttr.coneInnerAngle; - panner.coneOuterAngle = sound._pannerAttr.coneOuterAngle; - panner.coneOuterGain = sound._pannerAttr.coneOuterGain; - panner.distanceModel = sound._pannerAttr.distanceModel; - panner.maxDistance = sound._pannerAttr.maxDistance; - panner.refDistance = sound._pannerAttr.refDistance; - panner.rolloffFactor = sound._pannerAttr.rolloffFactor; - panner.panningModel = sound._pannerAttr.panningModel; - - if (sound._pos) { - if (typeof (panner as any).positionX !== 'undefined') { - (panner as any).positionX.setValueAtTime(sound._pos[0], Howler.ctx!.currentTime); - (panner as any).positionY.setValueAtTime(sound._pos[1], Howler.ctx!.currentTime); - (panner as any).positionZ.setValueAtTime(sound._pos[2], Howler.ctx!.currentTime); - } else { - panner.setPosition(sound._pos[0], sound._pos[1], sound._pos[2]); - } - } - - if (typeof (panner as any).orientationX !== 'undefined') { - (panner as any).orientationX.setValueAtTime(sound._orientation[0], Howler.ctx!.currentTime); - (panner as any).orientationY.setValueAtTime(sound._orientation[1], Howler.ctx!.currentTime); - (panner as any).orientationZ.setValueAtTime(sound._orientation[2], Howler.ctx!.currentTime); - } else { - panner.setOrientation(sound._orientation[0], sound._orientation[1], sound._orientation[2]); - } - } else { - sound._panner = Howler.ctx.createStereoPanner(); - const stereoPanner = sound._panner as StereoPannerNode; - if (sound._stereo !== null) { - stereoPanner.pan.setValueAtTime(sound._stereo, Howler.ctx!.currentTime); - } - } - - // Connect panner to the sound's node - if (sound._node && isGainNode(sound._node)) { - sound._panner.connect(sound._node); - } - - // Update connections if sound is playing - if (!sound._paused) { - (sound._parent as any).pause(sound._id, true); - (sound._parent as any).play(sound._id, true); - } +function setupPanner( + sound: Sound & SpatialSoundState, + type: "stereo" | "spatial" = "spatial", +): void { + if (!Howler.ctx) { + return; + } + + // Create the new panner node + if (type === "spatial") { + sound._panner = Howler.ctx.createPanner(); + const panner = sound._panner as PannerNode; + panner.coneInnerAngle = sound._pannerAttr.coneInnerAngle; + panner.coneOuterAngle = sound._pannerAttr.coneOuterAngle; + panner.coneOuterGain = sound._pannerAttr.coneOuterGain; + panner.distanceModel = sound._pannerAttr.distanceModel; + panner.maxDistance = sound._pannerAttr.maxDistance; + panner.refDistance = sound._pannerAttr.refDistance; + panner.rolloffFactor = sound._pannerAttr.rolloffFactor; + panner.panningModel = sound._pannerAttr.panningModel; + + if (sound._pos) { + if (typeof panner.positionX !== "undefined") { + panner.positionX.setValueAtTime( + sound._pos[0], + Howler.ctx?.currentTime ?? 0, + ); + panner.positionY.setValueAtTime( + sound._pos[1], + Howler.ctx?.currentTime ?? 0, + ); + panner.positionZ.setValueAtTime( + sound._pos[2], + Howler.ctx?.currentTime ?? 0, + ); + } else { + panner.setPosition(sound._pos[0], sound._pos[1], sound._pos[2]); + } + } + + if (typeof panner.orientationX !== "undefined") { + panner.orientationX.setValueAtTime( + sound._orientation[0], + Howler.ctx?.currentTime ?? 0, + ); + panner.orientationY.setValueAtTime( + sound._orientation[1], + Howler.ctx?.currentTime ?? 0, + ); + panner.orientationZ.setValueAtTime( + sound._orientation[2], + Howler.ctx?.currentTime ?? 0, + ); + } else { + panner.setOrientation( + sound._orientation[0], + sound._orientation[1], + sound._orientation[2], + ); + } + } else { + sound._panner = Howler.ctx.createStereoPanner(); + const stereoPanner = sound._panner as StereoPannerNode; + if (sound._stereo !== null) { + stereoPanner.pan.setValueAtTime( + sound._stereo, + Howler.ctx?.currentTime ?? 0, + ); + } + } + + // Connect panner to the sound's node + if (sound._node && isGainNode(sound._node)) { + sound._panner.connect(sound._node); + } + + // Update connections if sound is playing + if (!sound._paused) { + (sound._parent as any).pause(sound._id, true); + (sound._parent as any).play(sound._id, true); + } } /** * Mixin function to add spatial audio to HowlerGlobal (listener) */ -function withSpatialListener(instance: HowlerGlobal): HowlerGlobal & SpatialAudioState & { - pos(x?: number, y?: number, z?: number): any; - orientation(x?: number, y?: number, z?: number, xUp?: number, yUp?: number, zUp?: number): any; - stereo(pan?: number): any; -} { - const spatial = instance as any; - - // Initialize spatial properties - spatial._pos = [0, 0, 0]; - spatial._orientation = [0, 0, -1, 0, 1, 0]; - - // Add pos method to set listener position - spatial.pos = function (x?: number, y?: number, z?: number) { - if (!this.ctx || !this.ctx.listener) { - return this; - } - - // Set the defaults for optional 'y' & 'z' - y = typeof y !== 'number' ? this._pos[1] : y; - z = typeof z !== 'number' ? this._pos[2] : z; - - if (typeof x === 'number') { - this._pos = [x, y, z]; - - if (typeof this.ctx.listener.positionX !== 'undefined') { - (this.ctx.listener.positionX as any).setTargetAtTime(this._pos[0], Howler.ctx!.currentTime, 0.1); - (this.ctx.listener.positionY as any).setTargetAtTime(this._pos[1], Howler.ctx!.currentTime, 0.1); - (this.ctx.listener.positionZ as any).setTargetAtTime(this._pos[2], Howler.ctx!.currentTime, 0.1); - } else { - (this.ctx.listener as any).setPosition(this._pos[0], this._pos[1], this._pos[2]); - } - } else { - return this._pos; - } - - return this; - }; - - // Add orientation method to set listener orientation - spatial.orientation = function ( - x?: number, - y?: number, - z?: number, - xUp?: number, - yUp?: number, - zUp?: number - ) { - if (!this.ctx || !this.ctx.listener) { - return this; - } - - // Set the defaults for optional parameters - const or = this._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') { - this._orientation = [x, y, z, xUp, yUp, zUp]; - - if (typeof this.ctx.listener.forwardX !== 'undefined') { - (this.ctx.listener.forwardX as any).setTargetAtTime(x, Howler.ctx!.currentTime, 0.1); - (this.ctx.listener.forwardY as any).setTargetAtTime(y, Howler.ctx!.currentTime, 0.1); - (this.ctx.listener.forwardZ as any).setTargetAtTime(z, Howler.ctx!.currentTime, 0.1); - (this.ctx.listener.upX as any).setTargetAtTime(xUp, Howler.ctx!.currentTime, 0.1); - (this.ctx.listener.upY as any).setTargetAtTime(yUp, Howler.ctx!.currentTime, 0.1); - (this.ctx.listener.upZ as any).setTargetAtTime(zUp, Howler.ctx!.currentTime, 0.1); - } else { - (this.ctx.listener as any).setOrientation(x, y, z, xUp, yUp, zUp); - } - } else { - return or; - } - - return this; - }; - - // Add stereo method - spatial.stereo = function (pan?: number) { - if (!this.ctx || !this.ctx.listener) { - return this; - } - - // Loop through all Howls and update their stereo panning - for (let i = this._howls.length - 1; i >= 0; i--) { - (this._howls[i] as any).stereo?.(pan); - } - - return this; - }; - - return spatial; +function withSpatialListener(instance: HowlerGlobal): HowlerGlobal & + SpatialAudioState & { + pos(x?: number, y?: number, z?: number): SpatialHowler; + orientation( + x?: number, + y?: number, + z?: number, + xUp?: number, + yUp?: number, + zUp?: number, + ): SpatialHowler; + stereo(pan?: number): SpatialHowler; + } { + const spatial = instance as SpatialHowler; + + // Initialize spatial properties + spatial._pos = [0, 0, 0]; + spatial._orientation = [0, 0, -1, 0, 1, 0]; + + // Add pos method to set listener position + spatial.pos = function (x?: number, y?: number, z?: number) { + if (!this.ctx || !this.ctx.listener) { + return this; + } + + // Set the defaults for optional 'y' & 'z' + y = typeof y !== "number" ? this._pos[1] : y; + z = typeof z !== "number" ? this._pos[2] : z; + + if (typeof x === "number") { + this._pos = [x, y, z]; + + if (typeof this.ctx.listener.positionX !== "undefined") { + this.ctx.listener.positionX.setTargetAtTime( + this._pos[0], + Howler.ctx?.currentTime ?? 0, + 0.1, + ); + this.ctx.listener.positionY.setTargetAtTime( + this._pos[1], + Howler.ctx?.currentTime ?? 0, + 0.1, + ); + this.ctx.listener.positionZ.setTargetAtTime( + this._pos[2], + Howler.ctx?.currentTime ?? 0, + 0.1, + ); + } else { + this.ctx.listener.setPosition(this._pos[0], this._pos[1], this._pos[2]); + } + } else { + return this._pos; + } + + return this; + }; + + // Add orientation method to set listener orientation + spatial.orientation = function ( + x?: number, + y?: number, + z?: number, + xUp?: number, + yUp?: number, + zUp?: number, + ) { + if (!this.ctx || !this.ctx.listener) { + return this; + } + + // Set the defaults for optional parameters + const or = this._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") { + this._orientation = [x, y, z, xUp, yUp, zUp]; + + if (typeof this.ctx.listener.forwardX !== "undefined") { + this.ctx.listener.forwardX.setTargetAtTime( + x, + Howler.ctx?.currentTime ?? 0, + 0.1, + ); + this.ctx.listener.forwardY.setTargetAtTime( + y, + Howler.ctx?.currentTime ?? 0, + 0.1, + ); + this.ctx.listener.forwardZ.setTargetAtTime( + z, + Howler.ctx?.currentTime ?? 0, + 0.1, + ); + this.ctx.listener.upX.setTargetAtTime( + xUp, + Howler.ctx?.currentTime ?? 0, + 0.1, + ); + this.ctx.listener.upY.setTargetAtTime( + yUp, + Howler.ctx?.currentTime ?? 0, + 0.1, + ); + this.ctx.listener.upZ.setTargetAtTime( + zUp, + Howler.ctx?.currentTime ?? 0, + 0.1, + ); + } else { + this.ctx.listener.setOrientation(x, y, z, xUp, yUp, zUp); + } + } else { + return or; + } + + return this; + }; + + // Add stereo method + spatial.stereo = function (pan?: number) { + if (!this.ctx || !this.ctx.listener) { + return this; + } + + // Loop through all Howls and update their stereo panning + for (let i = this._howls.length - 1; i >= 0; i--) { + (this._howls[i] as SpatialHowl).stereo?.(pan); + } + + return this; + }; + + return spatial; } /** @@ -293,464 +393,569 @@ function withSpatialListener(instance: HowlerGlobal): HowlerGlobal & SpatialAudi * Adds 3D spatial audio and stereo panning capabilities to Howler and Howl instances */ export class SpatialAudioPlugin extends HowlerPlugin { - readonly name = 'spatial-audio'; - readonly version = '1.0.0'; - - getHooks(): PluginHooks { - return { - onHowlerInit: this.onHowlerInit.bind(this), - onHowlCreate: this.onHowlCreate.bind(this), - onSoundCreate: this.onSoundCreate.bind(this), - onHowlLoad: this.onHowlLoad.bind(this), - }; - } - - /** - * Initialize spatial audio when Howler is initialized - * This is called either: - * - When Howler initializes (if plugin was registered before) - * - Immediately during registration (if Howler is already initialized) - */ - private onHowlerInit(howler: HowlerGlobal): void { - withSpatialListener(howler); - } - - /** - * Extend Howl instances with spatial audio methods - */ - private onHowlCreate(howl: Howl, options: HowlOptions): void { - const spatialOptions = options as SpatialHowlOptions; - const spatial = howl as any; - - // Setup user-defined default properties - spatial._orientation = spatialOptions.orientation || [1, 0, 0]; - spatial._stereo = spatialOptions.stereo !== undefined ? spatialOptions.stereo : null; - spatial._pos = spatialOptions.pos || null; - spatial._pannerAttr = { - coneInnerAngle: typeof spatialOptions.coneInnerAngle !== 'undefined' ? spatialOptions.coneInnerAngle : 360, - coneOuterAngle: typeof spatialOptions.coneOuterAngle !== 'undefined' ? spatialOptions.coneOuterAngle : 360, - coneOuterGain: typeof spatialOptions.coneOuterGain !== 'undefined' ? spatialOptions.coneOuterGain : 0, - distanceModel: typeof spatialOptions.distanceModel !== 'undefined' ? spatialOptions.distanceModel : 'inverse', - maxDistance: typeof spatialOptions.maxDistance !== 'undefined' ? spatialOptions.maxDistance : 10000, - panningModel: typeof spatialOptions.panningModel !== 'undefined' ? spatialOptions.panningModel : 'HRTF', - refDistance: typeof spatialOptions.refDistance !== 'undefined' ? spatialOptions.refDistance : 1, - rolloffFactor: typeof spatialOptions.rolloffFactor !== 'undefined' ? spatialOptions.rolloffFactor : 1, - }; - - // Setup event listeners - spatial._onstereo = spatialOptions.onstereo ? [{ fn: spatialOptions.onstereo }] : []; - spatial._onpos = spatialOptions.onpos ? [{ fn: spatialOptions.onpos }] : []; - spatial._onorientation = spatialOptions.onorientation ? [{ fn: spatialOptions.onorientation }] : []; - - // Add stereo method - spatial.stereo = function (pan?: number, id?: number) { - const self = this as any; - - // 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 - 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 - const pannerType = - typeof Howler.ctx!.createStereoPanner !== 'undefined' ? 'stereo' : 'spatial'; - - const ids = self._getSoundIds(id); - for (let i = 0; i < ids.length; i++) { - const sound = self._soundById(ids[i]) as any; - if (sound) { - sound._stereo = pan; - - // Create a new panner node if one doesn't already exist - if (!sound._panner) { - // Make sure we have a position to setup the node with - if (!sound._pos) { - sound._pos = self._pos || [0, 0, -0.5]; - } - setupPanner(sound, pannerType); - } else if (pannerType === 'stereo' && sound._panner instanceof StereoPannerNode) { - sound._panner.pan.setValueAtTime(pan, Howler.ctx!.currentTime); - } - } - } - - // Fire event - self._emit('stereo', id); - - return self; - }; - - // Add pos method - spatial.pos = function (x?: number, y?: number, z?: number, id?: number) { - const self = this as any; - - // 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 - if (self._state !== 'loaded') { - self._queue.push({ - event: 'pos', - action: function () { - self.pos(x, y, z, id); - }, - }); - return self; - } - - // Set the defaults for optional 'y' & 'z' - y = typeof y !== 'number' ? (self._pos ? self._pos[1] : 0) : y; - z = typeof z !== 'number' ? (self._pos ? self._pos[2] : 0) : z; - - if (typeof x === 'number') { - const ids = self._getSoundIds(id); - for (let i = 0; i < ids.length; i++) { - const sound = self._soundById(ids[i]) as any; - if (sound) { - sound._pos = [x, y, z]; - - // Create a new panner node if one doesn't already exist - if (!sound._panner) { - setupPanner(sound, 'spatial'); - } else if (sound._panner instanceof PannerNode) { - // Update position - if (typeof (sound._panner as any).positionX !== 'undefined') { - (sound._panner as any).positionX.setValueAtTime(x, Howler.ctx!.currentTime); - (sound._panner as any).positionY.setValueAtTime(y, Howler.ctx!.currentTime); - (sound._panner as any).positionZ.setValueAtTime(z, Howler.ctx!.currentTime); - } else { - sound._panner.setPosition(x, y, z); - } - } - } - } - - // Fire event - self._emit('pos', id); - - return self; - } else { - // Return the position of the first sound or the group's position - if (typeof id === 'number') { - const sound = self._soundById(id) as any; - return sound ? sound._pos || [0, 0, 0] : [0, 0, 0]; - } - return self._pos || [0, 0, 0]; - } - }; - - // Add orientation method - spatial.orientation = function (x?: number, y?: number, z?: number, id?: number) { - const self = this as any; - - // 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 - if (self._state !== 'loaded') { - self._queue.push({ - event: 'orientation', - action: function () { - self.orientation(x, y, z, id); - }, - }); - return self; - } - - // Set the defaults for optional 'y' & 'z' - y = typeof y !== 'number' ? self._orientation[1] : y; - z = typeof z !== 'number' ? self._orientation[2] : z; - - if (typeof x === 'number') { - const ids = self._getSoundIds(id); - for (let i = 0; i < ids.length; i++) { - const sound = self._soundById(ids[i]) as any; - if (sound) { - sound._orientation = [x, y, z]; - - // Create a new panner node if one doesn't already exist - if (!sound._panner) { - if (!sound._pos) { - sound._pos = self._pos || [0, 0, -0.5]; - } - setupPanner(sound, 'spatial'); - } else if (sound._panner instanceof PannerNode) { - // Update orientation - if (typeof (sound._panner as any).orientationX !== 'undefined') { - (sound._panner as any).orientationX.setValueAtTime(x, Howler.ctx!.currentTime); - (sound._panner as any).orientationY.setValueAtTime(y, Howler.ctx!.currentTime); - (sound._panner as any).orientationZ.setValueAtTime(z, Howler.ctx!.currentTime); - } else { - sound._panner.setOrientation(x, y, z); - } - } - } - } - - // Fire event - self._emit('orientation', id); - - return self; - } else { - // Return the orientation of the first sound or the group's orientation - if (typeof id === 'number') { - const sound = self._soundById(id) as any; - return sound ? sound._orientation : [1, 0, 0]; - } - return self._orientation; - } - }; - - // Add pannerAttr method - spatial.pannerAttr = function (o?: any, id?: number) { - const self = this as any; - const args = arguments; - let sound: any = null; - - if (args.length === 0) { - // Return this sound's panner attribute values - return self._pannerAttr; - } else if (args.length === 1) { - if (typeof args[0] === 'number') { - // Return this sound's panner attribute values - sound = self._soundById(parseInt(args[0] as any, 10)); - return sound ? sound._pannerAttr : self._pannerAttr; - } else { - // Update all sounds in the group - o = args[0]; - } - } else if (args.length === 2) { - o = args[0]; - id = parseInt(args[1] as any, 10); - } - - // Update the values of the specified sounds - const ids = self._getSoundIds(id); - for (let i = 0; i < ids.length; i++) { - sound = self._soundById(ids[i]) as any; - - if (sound) { - // Merge the new values into the sound - const pa = sound._pannerAttr; - sound._pannerAttr = { - coneInnerAngle: typeof o.coneInnerAngle !== 'undefined' ? o.coneInnerAngle : pa.coneInnerAngle, - coneOuterAngle: typeof o.coneOuterAngle !== 'undefined' ? o.coneOuterAngle : pa.coneOuterAngle, - coneOuterGain: typeof o.coneOuterGain !== 'undefined' ? o.coneOuterGain : pa.coneOuterGain, - distanceModel: typeof o.distanceModel !== 'undefined' ? o.distanceModel : pa.distanceModel, - maxDistance: typeof o.maxDistance !== 'undefined' ? o.maxDistance : pa.maxDistance, - refDistance: typeof o.refDistance !== 'undefined' ? o.refDistance : pa.refDistance, - rolloffFactor: typeof o.rolloffFactor !== 'undefined' ? o.rolloffFactor : pa.rolloffFactor, - panningModel: typeof o.panningModel !== 'undefined' ? o.panningModel : pa.panningModel, - }; - - // Create a new panner node if one doesn't already exist - let panner = sound._panner; - if (!panner) { - // Make sure we have a position to setup the node with - if (!sound._pos) { - sound._pos = self._pos || [0, 0, -0.5]; - } - - // Create a new panner node - setupPanner(sound, 'spatial'); - panner = sound._panner; - } - - // Update the panner values - if (panner instanceof PannerNode) { - panner.coneInnerAngle = sound._pannerAttr.coneInnerAngle; - panner.coneOuterAngle = sound._pannerAttr.coneOuterAngle; - panner.coneOuterGain = sound._pannerAttr.coneOuterGain; - panner.distanceModel = sound._pannerAttr.distanceModel; - panner.maxDistance = sound._pannerAttr.maxDistance; - panner.refDistance = sound._pannerAttr.refDistance; - panner.rolloffFactor = sound._pannerAttr.rolloffFactor; - panner.panningModel = sound._pannerAttr.panningModel; - } - } - } - - return self; - }; - } - - /** - * Extend Sound instances with spatial audio properties - */ - private onSoundCreate(sound: Sound, parent: Howl): void { - const spatialParent = parent as any; - const spatialSound = sound as any; - - // Setup user-defined default properties - spatialSound._orientation = spatialParent._orientation; - spatialSound._stereo = spatialParent._stereo; - spatialSound._pos = spatialParent._pos; - spatialSound._pannerAttr = spatialParent._pannerAttr; - - // Wrap the reset method to handle spatial cleanup - if (!spatialSound._originalReset) { - spatialSound._originalReset = spatialSound.reset.bind(spatialSound); - spatialSound.reset = function () { - const self = this as any; - const parent = self._parent as any; - - // Reset all spatial plugin properties on this sound - self._orientation = parent._orientation; - self._stereo = parent._stereo; - self._pos = parent._pos; - self._pannerAttr = parent._pannerAttr; - - // If a stereo or position was specified, set it up - if (self._stereo !== null && self._stereo !== undefined) { - parent.stereo(self._stereo, self._id); - } else if (self._pos) { - parent.pos(self._pos[0], self._pos[1], self._pos[2], self._id); - } else if (self._panner) { - // Disconnect the panner - self._panner.disconnect(0); - self._panner = undefined; - parent._refreshBuffer(self); - } - - // Complete resetting of the sound - return self._originalReset(); - }; - } - - // If a stereo or position was specified, set it up - if (spatialSound._stereo !== null && spatialSound._stereo !== undefined) { - spatialParent.stereo(spatialSound._stereo, spatialSound._id); - } else if (spatialSound._pos) { - spatialParent.pos(spatialSound._pos[0], spatialSound._pos[1], spatialSound._pos[2], spatialSound._id); - } - } - - /** - * Handle load queue for spatial audio - */ - private onHowlLoad(howl: Howl): void { - const spatial = howl as any; - - // Process any queued spatial audio actions - if (spatial._queue) { - for (let i = 0; i < spatial._queue.length; i++) { - const task = spatial._queue[i]; - if (task.event === 'stereo' || task.event === 'pos' || task.event === 'orientation') { - task.action(); - } - } - } - } - - onUnregister(): void { - const howler = globalPluginManager.getHowlerInstance(); - if (!howler) { - return; - } - - // Clean up Howler instance - delete (howler as any)._pos; - delete (howler as any)._orientation; - delete (howler as any).pos; - delete (howler as any).orientation; - delete (howler as any).stereo; - - // Clean up all Howl instances - for (let i = 0; i < howler._howls.length; i++) { - const howl = howler._howls[i] as any; - - // Remove spatial methods from Howl - delete howl.stereo; - delete howl.pos; - delete howl.orientation; - delete howl.pannerAttr; - - // Remove spatial properties from Howl - delete howl._pos; - delete howl._orientation; - delete howl._stereo; - delete howl._pannerAttr; - delete howl._onstereo; - delete howl._onpos; - delete howl._onorientation; - - // Clean up all Sound instances in this Howl - if (howl._sounds) { - for (let j = 0; j < howl._sounds.length; j++) { - const sound = howl._sounds[j] as any; - - // Disconnect and remove panner nodes - if (sound._panner) { - const wasPlaying = !sound._paused; - - try { - // Disconnect panner from the audio graph - sound._panner.disconnect(0); - } catch (e) { - // Panner may already be disconnected - } - - // Remove panner reference - sound._panner = undefined; - - // If the sound was playing, we need to stop it and clean up the buffer source - // The user will need to call play() again to resume without spatial audio - if (wasPlaying && howl._webAudio && sound._node && sound._node.bufferSource) { - try { - // Stop the current buffer source - if (typeof (sound._node.bufferSource as any).stop === 'undefined') { - (sound._node.bufferSource as any).noteOff(0); - } else { - (sound._node.bufferSource as any).stop(0); - } - sound._node.bufferSource.disconnect(0); - } catch (e) { - // Buffer source may already be stopped or disconnected - } - - // Mark as paused so the audio graph can be reconnected on next play - sound._paused = true; - - // Clean up the buffer source - it will be recreated on next play - howl._cleanBuffer(sound._node); - } - } - - // Remove spatial properties from Sound - delete sound._pos; - delete sound._orientation; - delete sound._stereo; - delete sound._pannerAttr; - - // Restore original reset method if it was wrapped - if (sound._originalReset) { - sound.reset = sound._originalReset; - delete sound._originalReset; - } - } - } - - // Remove spatial-related queue items - if (howl._queue) { - howl._queue = howl._queue.filter((item: any) => { - return item.event !== 'stereo' && item.event !== 'pos' && item.event !== 'orientation'; - }); - } - } - } + readonly name = "spatial-audio"; + readonly version = "1.0.0"; + + getHooks(): PluginHooks { + return { + onHowlerInit: this.onHowlerInit.bind(this), + onHowlCreate: this.onHowlCreate.bind(this), + onSoundCreate: this.onSoundCreate.bind(this), + onHowlLoad: this.onHowlLoad.bind(this), + }; + } + + /** + * Initialize spatial audio when Howler is initialized + * This is called either: + * - When Howler initializes (if plugin was registered before) + * - Immediately during registration (if Howler is already initialized) + */ + private onHowlerInit(howler: HowlerGlobal): void { + withSpatialListener(howler); + } + + /** + * Extend Howl instances with spatial audio methods + */ + private onHowlCreate(howl: Howl, options: HowlOptions): void { + const spatialOptions = options as SpatialHowlOptions; + const spatial = howl as SpatialHowl; + + // Setup user-defined default properties + spatial._orientation = spatialOptions.orientation || [1, 0, 0]; + spatial._stereo = + spatialOptions.stereo !== undefined ? spatialOptions.stereo : null; + spatial._pos = spatialOptions.pos || null; + spatial._pannerAttr = { + coneInnerAngle: + typeof spatialOptions.coneInnerAngle !== "undefined" + ? spatialOptions.coneInnerAngle + : 360, + coneOuterAngle: + typeof spatialOptions.coneOuterAngle !== "undefined" + ? spatialOptions.coneOuterAngle + : 360, + coneOuterGain: + typeof spatialOptions.coneOuterGain !== "undefined" + ? spatialOptions.coneOuterGain + : 0, + distanceModel: + typeof spatialOptions.distanceModel !== "undefined" + ? spatialOptions.distanceModel + : "inverse", + maxDistance: + typeof spatialOptions.maxDistance !== "undefined" + ? spatialOptions.maxDistance + : 10000, + panningModel: + typeof spatialOptions.panningModel !== "undefined" + ? spatialOptions.panningModel + : "HRTF", + refDistance: + typeof spatialOptions.refDistance !== "undefined" + ? spatialOptions.refDistance + : 1, + rolloffFactor: + typeof spatialOptions.rolloffFactor !== "undefined" + ? spatialOptions.rolloffFactor + : 1, + }; + + // Setup event listeners + spatial._onstereo = spatialOptions.onstereo + ? [{ fn: spatialOptions.onstereo }] + : []; + spatial._onpos = spatialOptions.onpos ? [{ fn: spatialOptions.onpos }] : []; + spatial._onorientation = spatialOptions.onorientation + ? [{ fn: spatialOptions.onorientation }] + : []; + + // Add stereo method + spatial.stereo = function (pan?: number, id?: number) { + const self = this as any; + + // 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 + if (self._state !== "loaded") { + self._queue.push({ + event: "stereo", + action: () => { + self.stereo(pan, id); + }, + }); + return self; + } + + // Check for PannerStereoNode support and fallback to PannerNode if it doesn't exist + const pannerType = + typeof Howler.ctx?.createStereoPanner !== "undefined" + ? "stereo" + : "spatial"; + + const ids = self._getSoundIds(id); + for (let i = 0; i < ids.length; i++) { + const sound = self._soundById(ids[i]) as Sound & SpatialSoundState; + if (sound) { + sound._stereo = pan ?? null; + + // Create a new panner node if one doesn't already exist + if (!sound._panner) { + // Make sure we have a position to setup the node with + if (!sound._pos) { + sound._pos = self._pos || [0, 0, -0.5]; + } + setupPanner(sound, pannerType); + } else if ( + pannerType === "stereo" && + sound._panner instanceof StereoPannerNode + ) { + sound._panner.pan.setValueAtTime( + pan ?? 0, + Howler.ctx?.currentTime ?? 0, + ); + } + } + } + + // Fire event + self._emit("stereo", id); + + return self; + }; + + // Add pos method + spatial.pos = function (x?: number, y?: number, z?: number, id?: number) { + const self = this as SpatialHowl; + + // 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 + if (self._state !== "loaded") { + self._queue.push({ + event: "pos", + action: () => { + self.pos(x, y, z, id); + }, + }); + return self; + } + + // Set the defaults for optional 'y' & 'z' + y = typeof y !== "number" ? (self._pos ? self._pos[1] : 0) : y; + z = typeof z !== "number" ? (self._pos ? self._pos[2] : 0) : z; + + if (typeof x === "number") { + const ids = self._getSoundIds(id); + for (let i = 0; i < ids.length; i++) { + const sound = self._soundById(ids[i]) as Sound & SpatialSoundState; + if (sound) { + sound._pos = [x, y, z]; + + // Create a new panner node if one doesn't already exist + if (!sound._panner) { + setupPanner(sound, "spatial"); + } else if (sound._panner instanceof PannerNode) { + // Update position + if (typeof sound._panner.positionX !== "undefined") { + sound._panner.positionX.setValueAtTime( + x, + Howler.ctx?.currentTime ?? 0, + ); + sound._panner.positionY.setValueAtTime( + y, + Howler.ctx?.currentTime ?? 0, + ); + sound._panner.positionZ.setValueAtTime( + z, + Howler.ctx?.currentTime ?? 0, + ); + } else { + sound._panner.setPosition(x, y, z); + } + } + } + } + + // Fire event + self._emit("pos", id); + + return self; + } else { + // Return the position of the first sound or the group's position + if (typeof id === "number") { + const sound = self._soundById(id) as Sound & SpatialSoundState; + return sound ? sound._pos || [0, 0, 0] : [0, 0, 0]; + } + return self._pos || [0, 0, 0]; + } + }; + + // Add orientation method + spatial.orientation = function ( + x?: number, + y?: number, + z?: number, + id?: number, + ) { + const self = this as SpatialHowl; + + // 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 + if (self._state !== "loaded") { + self._queue.push({ + event: "orientation", + action: () => { + self.orientation(x, y, z, id); + }, + }); + return self; + } + + // Set the defaults for optional 'y' & 'z' + y = typeof y !== "number" ? self._orientation[1] : y; + z = typeof z !== "number" ? self._orientation[2] : z; + + if (typeof x === "number") { + const ids = self._getSoundIds(id); + for (let i = 0; i < ids.length; i++) { + const sound = self._soundById(ids[i]) as Sound & SpatialSoundState; + if (sound) { + sound._orientation = [x, y, z]; + + // Create a new panner node if one doesn't already exist + if (!sound._panner) { + if (!sound._pos) { + sound._pos = self._pos || [0, 0, -0.5]; + } + setupPanner(sound, "spatial"); + } else if (sound._panner instanceof PannerNode) { + // Update orientation + if (typeof sound._panner.orientationX !== "undefined") { + sound._panner.orientationX.setValueAtTime( + x, + Howler.ctx?.currentTime ?? 0, + ); + sound._panner.orientationY.setValueAtTime( + y, + Howler.ctx?.currentTime ?? 0, + ); + sound._panner.orientationZ.setValueAtTime( + z, + Howler.ctx?.currentTime ?? 0, + ); + } else { + sound._panner.setOrientation(x, y, z); + } + } + } + } + + // Fire event + self._emit("orientation", id); + + return self; + } else { + // Return the orientation of the first sound or the group's orientation + if (typeof id === "number") { + const sound = self._soundById(id) as Sound & SpatialSoundState; + return sound ? sound._orientation : [1, 0, 0]; + } + return self._orientation; + } + }; + + // Add pannerAttr method + spatial.pannerAttr = function (o?: any, id?: number) { + const self = this as SpatialHowl; + const args = arguments; + let sound: (Sound & SpatialSoundState) | null = null; + + if (args.length === 0) { + // Return this sound's panner attribute values + return self._pannerAttr; + } else if (args.length === 1) { + if (typeof args[0] === "number") { + // Return this sound's panner attribute values + sound = self._soundById(parseInt(String(args[0]), 10)) as Sound & + SpatialSoundState; + return sound ? sound._pannerAttr : self._pannerAttr; + } else { + // Update all sounds in the group + o = args[0]; + } + } else if (args.length === 2) { + o = args[0]; + id = parseInt(args[1] as any, 10); + } + + // Update the values of the specified sounds + const ids = self._getSoundIds(id); + for (let i = 0; i < ids.length; i++) { + sound = self._soundById(ids[i]) as any; + + if (sound) { + // Merge the new values into the sound + const pa = sound._pannerAttr; + sound._pannerAttr = { + coneInnerAngle: + typeof o.coneInnerAngle !== "undefined" + ? o.coneInnerAngle + : pa.coneInnerAngle, + coneOuterAngle: + typeof o.coneOuterAngle !== "undefined" + ? o.coneOuterAngle + : pa.coneOuterAngle, + coneOuterGain: + typeof o.coneOuterGain !== "undefined" + ? o.coneOuterGain + : pa.coneOuterGain, + distanceModel: + typeof o.distanceModel !== "undefined" + ? o.distanceModel + : pa.distanceModel, + maxDistance: + typeof o.maxDistance !== "undefined" + ? o.maxDistance + : pa.maxDistance, + refDistance: + typeof o.refDistance !== "undefined" + ? o.refDistance + : pa.refDistance, + rolloffFactor: + typeof o.rolloffFactor !== "undefined" + ? o.rolloffFactor + : pa.rolloffFactor, + panningModel: + typeof o.panningModel !== "undefined" + ? o.panningModel + : pa.panningModel, + }; + + // Create a new panner node if one doesn't already exist + let panner = sound._panner; + if (!panner) { + // Make sure we have a position to setup the node with + if (!sound._pos) { + sound._pos = self._pos || [0, 0, -0.5]; + } + + // Create a new panner node + setupPanner(sound, "spatial"); + panner = sound._panner; + } + + // Update the panner values + if (panner instanceof PannerNode) { + panner.coneInnerAngle = sound._pannerAttr.coneInnerAngle; + panner.coneOuterAngle = sound._pannerAttr.coneOuterAngle; + panner.coneOuterGain = sound._pannerAttr.coneOuterGain; + panner.distanceModel = sound._pannerAttr.distanceModel; + panner.maxDistance = sound._pannerAttr.maxDistance; + panner.refDistance = sound._pannerAttr.refDistance; + panner.rolloffFactor = sound._pannerAttr.rolloffFactor; + panner.panningModel = sound._pannerAttr.panningModel; + } + } + } + + return self; + }; + } + + /** + * Extend Sound instances with spatial audio properties + */ + private onSoundCreate(sound: Sound, parent: Howl): void { + const spatialParent = parent as any; + const spatialSound = sound as any; + + // Setup user-defined default properties + spatialSound._orientation = spatialParent._orientation; + spatialSound._stereo = spatialParent._stereo; + spatialSound._pos = spatialParent._pos; + spatialSound._pannerAttr = spatialParent._pannerAttr; + + // Wrap the reset method to handle spatial cleanup + if (!spatialSound._originalReset) { + spatialSound._originalReset = spatialSound.reset.bind(spatialSound); + spatialSound.reset = function () { + const self = this as any; + const parent = self._parent as any; + + // Reset all spatial plugin properties on this sound + self._orientation = parent._orientation; + self._stereo = parent._stereo; + self._pos = parent._pos; + self._pannerAttr = parent._pannerAttr; + + // If a stereo or position was specified, set it up + if (self._stereo !== null && self._stereo !== undefined) { + parent.stereo(self._stereo, self._id); + } else if (self._pos) { + parent.pos(self._pos[0], self._pos[1], self._pos[2], self._id); + } else if (self._panner) { + // Disconnect the panner + self._panner.disconnect(0); + self._panner = undefined; + parent._refreshBuffer(self); + } + + // Complete resetting of the sound + return self._originalReset(); + }; + } + + // If a stereo or position was specified, set it up + if (spatialSound._stereo !== null && spatialSound._stereo !== undefined) { + spatialParent.stereo(spatialSound._stereo, spatialSound._id); + } else if (spatialSound._pos) { + spatialParent.pos( + spatialSound._pos[0], + spatialSound._pos[1], + spatialSound._pos[2], + spatialSound._id, + ); + } + } + + /** + * Handle load queue for spatial audio + */ + private onHowlLoad(howl: Howl): void { + const spatial = howl as any; + + // Process any queued spatial audio actions + if (spatial._queue) { + for (let i = 0; i < spatial._queue.length; i++) { + const task = spatial._queue[i]; + if ( + task.event === "stereo" || + task.event === "pos" || + task.event === "orientation" + ) { + task.action(); + } + } + } + } + + onUnregister(): void { + const howler = globalPluginManager.getHowlerInstance(); + if (!howler) { + return; + } + + // Clean up Howler instance + delete (howler as any)._pos; + delete (howler as any)._orientation; + delete (howler as any).pos; + delete (howler as any).orientation; + delete (howler as any).stereo; + + // Clean up all Howl instances + for (let i = 0; i < howler._howls.length; i++) { + const howl = howler._howls[i] as any; + + // Remove spatial methods from Howl + delete howl.stereo; + delete howl.pos; + delete howl.orientation; + delete howl.pannerAttr; + + // Remove spatial properties from Howl + delete howl._pos; + delete howl._orientation; + delete howl._stereo; + delete howl._pannerAttr; + delete howl._onstereo; + delete howl._onpos; + delete howl._onorientation; + + // Clean up all Sound instances in this Howl + if (howl._sounds) { + for (let j = 0; j < howl._sounds.length; j++) { + const sound = howl._sounds[j] as any; + + // Disconnect and remove panner nodes + if (sound._panner) { + const wasPlaying = !sound._paused; + + try { + // Disconnect panner from the audio graph + sound._panner.disconnect(0); + } catch (e) { + // Panner may already be disconnected + } + + // Remove panner reference + sound._panner = undefined; + + // If the sound was playing, we need to stop it and clean up the buffer source + // The user will need to call play() again to resume without spatial audio + if ( + wasPlaying && + howl._webAudio && + sound._node && + sound._node.bufferSource + ) { + try { + // Stop the current buffer source + if ( + typeof (sound._node.bufferSource as any).stop === "undefined" + ) { + (sound._node.bufferSource as any).noteOff(0); + } else { + (sound._node.bufferSource as any).stop(0); + } + sound._node.bufferSource.disconnect(0); + } catch (e) { + // Buffer source may already be stopped or disconnected + } + + // Mark as paused so the audio graph can be reconnected on next play + sound._paused = true; + + // Clean up the buffer source - it will be recreated on next play + howl._cleanBuffer(sound._node); + } + } + + // Remove spatial properties from Sound + delete sound._pos; + delete sound._orientation; + delete sound._stereo; + delete sound._pannerAttr; + + // Restore original reset method if it was wrapped + if (sound._originalReset) { + sound.reset = sound._originalReset; + delete sound._originalReset; + } + } + } + + // Remove spatial-related queue items + if (howl._queue) { + howl._queue = howl._queue.filter((item: any) => { + return ( + item.event !== "stereo" && + item.event !== "pos" && + item.event !== "orientation" + ); + }); + } + } + } } diff --git a/src/plugins/spatial.ts b/src/plugins/spatial.ts index 4c8fa634..983977b2 100644 --- a/src/plugins/spatial.ts +++ b/src/plugins/spatial.ts @@ -2,12 +2,14 @@ * Howler.js - Spatial Plugin Entry Point */ -// Export the plugin for use with Howler.addPlugin() -export { SpatialAudioPlugin } from './spatial-plugin'; - // Export spatial types for TypeScript support export type { - SpatialAudioState, SpatialHowl, SpatialHowler, SpatialHowlOptions, SpatialHowlState, - SpatialSoundState -} from './spatial-plugin'; - + SpatialAudioState, + SpatialHowl, + SpatialHowler, + SpatialHowlOptions, + SpatialHowlState, + SpatialSoundState, +} from "./spatial-plugin"; +// Export the plugin for use with Howler.addPlugin() +export { SpatialAudioPlugin } from "./spatial-plugin"; diff --git a/src/types.ts b/src/types.ts index bd46681b..c31e6930 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,46 +9,46 @@ */ export interface HowlOptions { - src: string | string[]; - autoplay?: boolean; - format?: string | string[]; - html5?: boolean; - mute?: boolean; - loop?: boolean; - pool?: number; - preload?: boolean | 'metadata'; - rate?: number; - sprite?: Record; - volume?: number; - xhr?: { - method?: string; - headers?: Record; - withCredentials?: boolean; - }; - onend?: () => void; - onfade?: () => void; - onload?: () => void; - onloaderror?: (id: number, msg: string) => void; - onplayerror?: (id: number, msg: string) => void; - onpause?: () => void; - onplay?: () => void; - onstop?: () => void; - onmute?: () => void; - onvolume?: () => void; - onrate?: () => void; - onseek?: () => void; - onunlock?: () => void; + src: string | string[]; + autoplay?: boolean; + format?: string | string[]; + html5?: boolean; + mute?: boolean; + loop?: boolean; + pool?: number; + preload?: boolean | "metadata"; + rate?: number; + sprite?: Record; + volume?: number; + xhr?: { + method?: string; + headers?: Record; + withCredentials?: boolean; + }; + onend?: () => void; + onfade?: () => void; + onload?: () => void; + onloaderror?: (id: number, msg: string) => void; + onplayerror?: (id: number, msg: string) => void; + onpause?: () => void; + onplay?: () => void; + onstop?: () => void; + onmute?: () => void; + onvolume?: () => void; + onrate?: () => void; + onseek?: () => void; + onunlock?: () => void; } export interface EventListener { - id?: number; - fn: (...args: unknown[]) => void; - once?: boolean; + id?: number; + fn: (...args: unknown[]) => void; + once?: boolean; } export interface QueueItem { - event: string; - action: () => void; + event: string; + action: () => void; } // Global audio context cache @@ -56,46 +56,57 @@ export const cache: Record = {}; // Type for HTML5 Audio element with custom properties export interface HTMLAudioElementWithUnlocked extends HTMLAudioElement { - _unlocked?: boolean; + _unlocked?: boolean; } // Type for AudioBufferSourceNode with legacy methods -export interface AudioBufferSourceNodeWithLegacy extends Omit { - noteOn?: (when: number) => void; - noteOff?: (when: number) => void; - noteGrainOn?: (when: number, grainOffset: number, grainDuration: number) => void; - loop?: boolean; - loopStart?: number | undefined; - loopEnd?: number | undefined; +export interface AudioBufferSourceNodeWithLegacy + extends Omit { + noteOn?: (when: number) => void; + noteOff?: (when: number) => void; + noteGrainOn?: ( + when: number, + grainOffset: number, + grainDuration: number, + ) => void; + loop?: boolean; + loopStart?: number | undefined; + loopEnd?: number | undefined; } // Type for window with Audio constructor export interface WindowWithAudio extends Window { - Audio: { - new (): HTMLAudioElement; - }; - ejecta?: unknown; + Audio: { + new (): HTMLAudioElement; + }; + ejecta?: unknown; } // Type for Navigator with CocoonJS export interface NavigatorWithCocoonJS extends Navigator { - isCocoonJS?: boolean; + isCocoonJS?: boolean; } // Type for GainNode with bufferSource property export interface GainNodeWithBufferSource extends GainNode { - bufferSource?: AudioBufferSourceNodeWithLegacy; + bufferSource?: AudioBufferSourceNodeWithLegacy; } // Type guards for audio node types -export function isHTMLAudioElement(node: HTMLAudioElementWithUnlocked | GainNodeWithBufferSource | null): node is HTMLAudioElementWithUnlocked { - return node !== null && - node instanceof HTMLAudioElement && - 'src' in node && - 'play' in node && - !('videoWidth' in node); +export function isHTMLAudioElement( + node: HTMLAudioElementWithUnlocked | GainNodeWithBufferSource | null, +): node is HTMLAudioElementWithUnlocked { + return ( + node !== null && + node instanceof HTMLAudioElement && + "src" in node && + "play" in node && + !("videoWidth" in node) + ); } -export function isGainNode(node: HTMLAudioElementWithUnlocked | GainNodeWithBufferSource | null): node is GainNodeWithBufferSource { - return node !== null && 'gain' in node && 'connect' in node; +export function isGainNode( + node: HTMLAudioElementWithUnlocked | GainNodeWithBufferSource | null, +): node is GainNodeWithBufferSource { + return node !== null && "gain" in node && "connect" in node; } diff --git a/tests/css/styles.css b/tests/css/styles.css index 501b568e..e4d399a0 100644 --- a/tests/css/styles.css +++ b/tests/css/styles.css @@ -1,96 +1,96 @@ html { - width: 100%; - height: 100%; - overflow: hidden; - padding: 0; - margin: 0; - outline: 0; + width: 100%; + height: 100%; + overflow: hidden; + padding: 0; + margin: 0; + outline: 0; } body { - width: 100%; - height: 100%; - overflow: hidden; - background: #e0dad2; - border: 30px solid #a2998c; - margin: 0; - padding: 0; - outline: 0; - font-family: 'Lucida Grande', Arial; - font-weight: bold; - font-size: 50px; - font-size: 7vw; - color: #a2998c; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; - -moz-user-select: none; - -webkit-user-select: none; - user-select: none; - cursor: default; + width: 100%; + height: 100%; + overflow: hidden; + background: #e0dad2; + border: 30px solid #a2998c; + margin: 0; + padding: 0; + outline: 0; + font-family: "Lucida Grande", Arial; + font-weight: bold; + font-size: 50px; + font-size: 7vw; + color: #a2998c; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; + cursor: default; } /* Main Components */ #container { - width: 100%; - height: 100%; - text-align: center; + width: 100%; + height: 100%; + text-align: center; } -.button{ - background: #a2998c; - border: none; - outline: 0; - padding: 20px 50px; - margin: 0; - font-family: 'Lucida Grande', Arial; - font-weight: bold; - font-size: 40px; - font-size: 5vw; - color: #e0dad2; - border-radius: 5px; - width: 92%; - margin: 20px 0; +.button { + background: #a2998c; + border: none; + outline: 0; + padding: 20px 50px; + margin: 0; + font-family: "Lucida Grande", Arial; + font-weight: bold; + font-size: 40px; + font-size: 5vw; + color: #e0dad2; + border-radius: 5px; + width: 92%; + margin: 20px 0; } .button:hover { - background: #91887b; - cursor: pointer; + background: #91887b; + cursor: pointer; } .button[disabled] { - pointer-events: none; - opacity: 0.5; + pointer-events: none; + opacity: 0.5; } #logo { - background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6Mjg5MUYwNDBCRUY0MTFFNUJCQ0JDMjc0MDg4QjEwMTIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6Mjg5MUYwNDFCRUY0MTFFNUJCQ0JDMjc0MDg4QjEwMTIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDoyODkxRjAzRUJFRjQxMUU1QkJDQkMyNzQwODhCMTAxMiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoyODkxRjAzRkJFRjQxMUU1QkJDQkMyNzQwODhCMTAxMiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pv3J1C0AABEnSURBVHja7F1LkxvVFT6SWo8Zad7j8Xg8GGyeoUh4BEIIJCmgkg1VqUpl5U0WziKr+KdkwTILFqlKsaKcbBMoCHGKAGUXBEPAxoztseclaawZvUdSd+53+rZGnlE/bqu7Z+SaSx1kW6/u893z+M499ypmGAbtHW+/9eafxcNvhBSFNITE6GgEMaDsjJAZIe+cPXf+t3tfoNm8sSlkVMrRCGc0+/1j3ObFXwpZP9JZaGNd6tgzIMtHgIQOyLIKIN/aveFoBDKWpY49AwIEC0d6C20U7DyQXVCvDxsgBhmH6npizolpQerYMyDNYQMkqSWFFoQSjMMBTKfTIcP+Wgp2WZYdIDtC7g6LZWA2JjRN0iXjwG0D19But7vX1meUpI69xRBBWHRJCofHZfFsNPjxIAXXoOu6m9sqSB17DuoY20LaR/E38JgGnW7ZPekECFxW60i1PsDQdaen29JlKQOyNixx5PBZh+PYlLpVBqR4xNZDSHhNnRb9AFKXaB6NYBlR0Y6DOAIisgDDKfgcDbeMz3ZsSd2SCg/pDeyHmzkj3SSjqwQjImIYi8X8AlJyetINkIKfC43F41SplKlarVE8Hg99NuI7c9lRfgwbkI7IoEZHRikjRNc7/UFyvoZCpIDgIpv1Gj3x5NN0fOEBard2IgEkHk9EEh3S6Qyt3Vmm61evUDoz4mgpNiMfKSDMKMsVmp6do0cef8otJw+sdBIL2RJ7R1VYf7lSZUD6TRAXKy0OAsiWusuKU7vdoVq1whdWLGxQPBGesgzdIE3TaHxy2s9s9TWajbptHPHgMqONIZixiBu4aFxwMpUyfWpIyjJiugA8QVGOnWbTBvyYBEV3ul9HnbpNXfi7hiotisdj1JCzKBGPVllRjHq9SnEkL3ZKj8Xs6GFjUECqZBYZlTOthgjsiB+x+P3VQQSXVK9Vbd2wS8yEu6oMAkjNLSvom/ZKQDoi40L2YxjGfQNIp92mlsgcE32TCMm+nBem6oMA0vRjIbjYdrslUt5W6Dwk6oH7AiiJvnHL1Rtsu4UAN235qmdh9Q78Y2enGQk/iHI0mw2+L7v44eIN7g4EyNlz5zt+Ut9UUhMXvWNmIx5jiOXqfEmEna47ApCWACSR0Pq7LOeMsiR16jvt9cVF4KbqjaYApeHJZXWEG8iNT1IqlT70FtISE63Z3BHpfNpP2aTk9vleAFn3AwgW+U0uEnckbHguOzZBS9e+opXbN7lGpJr1xMV3aMlk6GCAU5U2C5TNZpmM+khW8kEAsuoHEFwsMi1Wuvx7H21yVoL60Mb6Cl24cIEeOrWoxLitWtbo6AhzAz3EjK4lkpSxXJbGJ6aY/PW7J91wTHtXggBkQ5mHSADqEhAH8+hWTI/NnaDHHjlNszMzSnUpCxC4uyiqvfj8vVXeLhjgXeI/h6WHjSAA2SJzmTim4kKsmo8VeN0UBZ+M13Q6bYob3jMzq7gIJUUBiJfrsX3KA4XwMhVrUtQtpFbtllOcBpQJtwW/rIdcHT7AUZUyMCAlVXLIViILjJjxbqmv3tHZQpJDDUjMLJsYtt2K214yVi+AbKqSQ7iOhAQEynYL0giEyK5SqaTph2P35Q46kMJiEICU3QpifT84kWBGyzEBbN3Bt+udDiVF2poSbqvTGV6XZbjrsTwwIIJZNvwAAgtB2sv1LIf1Cisgg/lmMiPU7nSGFg4Xll4TumwGYSFEPhqvk0mNGe1Oq9nNuuzcG9waAIGFwFpoSF0WA+Jc6XUdmsfvKqheGKqhXD5pNlyXcBFDwLRBtpZu3mZy5zW4WzxkJJMJNe2F5Y6P5SibG2c3vL9s4ro3JR8kIOuKU2Vf+cSJHCI7gWtbWDxNL/24QbmxcYV9N5IYpjO7szSEgQnTEGn8ZjHPk21/4mF072cQHYZiIWb5JMHNDg1JDq2ZbAdKuVyiJ59+np790Su2TNiJiEbRdfLtN1fo2jfv0PH5+X3W6KHbJFCXVVJ1WVAQ3E63nuUSF6DUVrMpYkhbiYtABwnhEkdGc6EDUq9WOEHBZNs7aQzZQelS8QgMEHxYW+H1DICmJdhlGV66TsTz8M1YkVPhIYg/ui4yNCcLDAqQepU0fx0ubfLYlutVwSCGSH9zKoBYbJ2kslz9u1Mnh91bjFhkRBKlIEwyH9/X8AqIV8fruvTYN9MCFxGAILgP+zCXE+q2C27WZHNo/ykFCcimKiBsIWDrIu2FW3HiIsMBiM4rm3GbtR0XDtLwyuW8ammLFLcmWBaCJU80PHCBcYjbgWDl3NaUSNgD4jyhtwIDRFL+oiog8LetbvfJcFsIYiGaG0xLN/YXgBBX7GNLXujQ0zYATeGaCoo2zgQKN8H1LJGWYoYF3Vrau1knzIHmcXTS9LcQ18qC52q5CiBbqhbCWZbgFvgzWvdRrwq6tdT6nrAzLcRCuN5+WxCC1F14FkJmswPK6au3b/Cf0acVeAsVKI4A2eqT8mItAA+xLTc+QROT056+Bq+3W6txCehKulMBRLkdCDWsXC5Hlz+5SB9dfD+UWdztOhnxVsuy1viXbizTG7/6NT334s+8WchOg1p90vdefuWwVL0WBiC+t0hrmuapfDIIIJqW9EQ+udwvUli0DY2NTyqQwhovE9ilvUHpTgWQkl+FJZMps9MvhODbbQNKp8nLaUC8w0vEgsnJcXZZnql2oyZ7vvZ/B7cGGbqThWyHBYiuwF3uqTeJ/3leeFLp1eWCnmEB4yWGGHyWFThSWpbsPfGQ7uZV5UnVCctCsMCCpdxxpTgCE++0eSOom8ti7iLSSsQdrwFaPa6ZLguup3/DtA0PEVlW/z0hsd2g3v/+KmEF9bz84HGVmy9vb9Pc/Al65bU3uJqLTZp2I5XJ0HZpkz6++B4rDClmGLt4sb8DC1oq/cA7jQaX+fvaqHuGFQogVfnBZ5QYrkgXccGnTj/Kyu3IDsN+AzO2Oj1Llz/+kCrVGrcGhUH5kL7CXSU9dtsj5UUM8blJZ4MUmkRUAGmRj6M20qmUYLll2cVoiD/3358H0EYEAFj8gbKwD9wIKQlgQJioeqsacMrbatm83tVCrLUkb9zN6wvPnjvvCxCLhKGmpWkpBgNK3ytWSQXpa5L7s8JtB0IfmNfdXVZ/Wcyl9G6XDEndBQuIXy5iFhjbXHpwbSnVdQYMFhJefcrg9FXlWAzED3O/pI3LGuDkhkEBUbYQBOcWd5+476ZCwEc1dWQ0a65RhxDQzc81KDemxkFgsXtJYRdQ50pvKUxAfPVnYS0BN8W7qci+80QetESZzGho5Xpd7p3P5sYUCotN7qDpb1GuE2fjUFkIbgI3Y+0VcQMQbgFbx2p1c4ESO3qDODqD45Rmprm1Wl1pdzDih9POLJduEyUL0RTvK0/qiHD+brUD8XqC3v8sEGumoVFua2ub/44tZDkh4A0gZjivyqsrg9K5HUkoFN8PcgeCipQ6pcDS8V67ozQ8xLp8mIAo9/jG5Goa0l4oEjGiQ7oteOXtEp089TD97vd/oLU7N2lVSD6fp0LxLvcLoyiYSae7s51kSdxq57TKJ/D5UCSsAW2gAHZ2bp5+8NxDdPzEA3R8YdE7AauUe75jvws8SEBAJjDVlbbK9m4AdatnoS8rmUzTmUe/x4LsZntrk9ZWblN+7Q4V8qtUKBSoIngK4gwAwmlynXaCO1yqNfPkCvThYnPm6UefFAAs0tz8SWFp4746HMGjEHecqsk2sRH6qocJSEX6xFHvHkuefcLVUp1PCuq4uBl0d2zdLTJ41llY07PHyXjqWapWK1S6W6DC+oqwnluUF48b+SKDMjk1RYsPPkyzx+bp2PwCgzDoSRKIH+BRcX9LB66HzQRhIQjsCypvSsjVQvtcfr/r2rWYNrXL2932VAA0L1zO4qkz9MwLWKeo0J1bSxz8F4Wr4/O5gszKOrpJCq0O9z6ld4fCYjEKQNTWRaz+LDQ7wB35Oa2hx38DVARnk22nODg//PhToa2pt1pNdoWOdayY4zqI0oZZJYd69tz5Gvk4HUiTDXNsIYM2zMXM9XMINvdUy1sixoR3Ijosu2kLiGu3ybbQWT00QOQoq76B014++6QZrLbkaaQ+l1U9WsgOu02fsUhZV34A8XWWb0O2A2ENghetLD5xyLsZYYUgtnvdUjfjCqjbxG8MwbihWDyilAi0mtakm999Q2PjEzSazXFVl8/4ReFOxBa95yeCYoekyxEWfXPpKvMfs1HD6A+K/bgeBSCraniY5ZCpyUm6/OlHdPWrzwVBWxDc4CSdOPkgTc4cozFZTDRTzCaDxEdlxOORHoCG9HartEkrt5doffU2lYp5TiDGZDOED7e4HgUgykHdupExQdZw09evfU1XvvicZ96x2VmaP3mKFh54iAEaG5vg3VCm9dQ5qFodHS79s+TnulAZKGys0Z3l72jt9k0qlUrM7NHnhWUAXrW0AcIDQOUoAMEKGKKzcv4KpWIdYlRYBIp1yLxKW1uC2F2i/352iWtWM7NztLD4oADoNE3DeiYm2UoAJFbuur9+5jP2oGKwIRg/ZH11mc+/wuqkuZM3TRPCkpFOWzUwJ6W7bGNrko/D3/wCUvMDiFVAbMtUERxiWi5GgZ3X6w26dfMGXbt2jTLpFFsP2DYE58hPTM1QVvIYk4/Uuy2eTkwbrB+sHiBsFtZFmlzi70JsGxGWMDM9zbUxK1vj/YOuG093g7rNLmNfR+z6dVkAZCqIDGY3Nda4/QdixZNKpUL5K58L9/YZFwfRhzsvgIF7QxzKiuQglov3MGlz1qL2tFnYYAa/JqygVtnmIiPqUdjPnsuN0biwvF72zRtygj3Wo0Y+mgv9ALLpxzd6jTP3ApQUAXWcn0OjWj6/QUtLS8Ld/ZOOzUyx5cC9nTrzGL9nZfkGg1DYWOHyPSoE2dERdpPTM1mZFel7fmvEGOya7WNamfwsefu0kDpFMNhHSyvShF+fmsoImeLYg6ru9W+v0rWrX1Puk4vc3QI3BKWn4e7m5rplGjOl1gOmPIYXC1GeuDE/DPftt958Tzy8dlD8oJenIPBiDwoOrkF/r1ldjtueiRhckSDGrUEd3sbdN4b84+y587+MwkJ85deBzs3eII7sCGdtpdO7VmV0QgWiu4XNubCY9/P5fgEp0SEa1rEWYVV895beYX0op8AVOjSG340SkPv2t9Z3QY11YwWnwrLRgeMR7a6BOAASqYXcvX9B2AXAkAnB3rN4GQR3ayxFCUjx/gBh1wpALlksAIzd45Z8ni2/GSUgYOu+Nu8cvBXI7MwCQC4DGPsquQPdWidqC1mTXzg9LG7ILKN0TABkm6qVGltpdIC/slDym4nG/Obqgou8KB5+IeQlIc8JmT+QDEuebG1ykHtPWbD6s+6xAvdg7HfgXPdLQv4t5F3BQS5FCkgPMHg4JeSHQl4X8hMhT0flzixA0hmzExG7o3RpAd1gHA4I2GLwmQTgfYAhQLgz6IdqAV3cLSkXhGTJ3GUFy8Em8J8LWQzNNUmSZq2b9A3GwfGTG0I+EPIvIR/h76pNDFEB0jtQdv5Cyp+EzAp5WchPpfU8QQFUivcRQ+mqYsEeA4Vs8itpBR8K+U/YKb8WgVcBifybFOqxntclUI8FZSkBDPi4r4VcFPKeBOBWlDFRo+jHd1L+ImRSWszL0npgRccivp5V6YIgHwu5Sgf4O/IaHewoyVkI+aPM1JC9vSbjD6xnJODvhEv9n4wFkE/Jx4/W3K+A3JswmbP1r1LQpPt9aT0A53mZzan6prYMxp9KK4A7+lK6p0M3NDq8Y0fm9ZA3ZXLwjJBXZeb2LNl34WMR7bJMRz+QCcZQ1N80Gp6B5OBdKWmZSr8gwXld8oK/Syu4LInaDg3Z+L8AAwC5zGYnR8jzIwAAAABJRU5ErkJggg=='); - width: 100px; - height: 100px; - position: absolute; - bottom: 60px; - left: 50%; - margin: 0 -50px 0 -50px; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6Mjg5MUYwNDBCRUY0MTFFNUJCQ0JDMjc0MDg4QjEwMTIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6Mjg5MUYwNDFCRUY0MTFFNUJCQ0JDMjc0MDg4QjEwMTIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDoyODkxRjAzRUJFRjQxMUU1QkJDQkMyNzQwODhCMTAxMiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoyODkxRjAzRkJFRjQxMUU1QkJDQkMyNzQwODhCMTAxMiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pv3J1C0AABEnSURBVHja7F1LkxvVFT6SWo8Zad7j8Xg8GGyeoUh4BEIIJCmgkg1VqUpl5U0WziKr+KdkwTILFqlKsaKcbBMoCHGKAGUXBEPAxoztseclaawZvUdSd+53+rZGnlE/bqu7Z+SaSx1kW6/u893z+M499ypmGAbtHW+/9eafxcNvhBSFNITE6GgEMaDsjJAZIe+cPXf+t3tfoNm8sSlkVMrRCGc0+/1j3ObFXwpZP9JZaGNd6tgzIMtHgIQOyLIKIN/aveFoBDKWpY49AwIEC0d6C20U7DyQXVCvDxsgBhmH6npizolpQerYMyDNYQMkqSWFFoQSjMMBTKfTIcP+Wgp2WZYdIDtC7g6LZWA2JjRN0iXjwG0D19But7vX1meUpI69xRBBWHRJCofHZfFsNPjxIAXXoOu6m9sqSB17DuoY20LaR/E38JgGnW7ZPekECFxW60i1PsDQdaen29JlKQOyNixx5PBZh+PYlLpVBqR4xNZDSHhNnRb9AFKXaB6NYBlR0Y6DOAIisgDDKfgcDbeMz3ZsSd2SCg/pDeyHmzkj3SSjqwQjImIYi8X8AlJyetINkIKfC43F41SplKlarVE8Hg99NuI7c9lRfgwbkI7IoEZHRikjRNc7/UFyvoZCpIDgIpv1Gj3x5NN0fOEBard2IgEkHk9EEh3S6Qyt3Vmm61evUDoz4mgpNiMfKSDMKMsVmp6do0cef8otJw+sdBIL2RJ7R1VYf7lSZUD6TRAXKy0OAsiWusuKU7vdoVq1whdWLGxQPBGesgzdIE3TaHxy2s9s9TWajbptHPHgMqONIZixiBu4aFxwMpUyfWpIyjJiugA8QVGOnWbTBvyYBEV3ul9HnbpNXfi7hiotisdj1JCzKBGPVllRjHq9SnEkL3ZKj8Xs6GFjUECqZBYZlTOthgjsiB+x+P3VQQSXVK9Vbd2wS8yEu6oMAkjNLSvom/ZKQDoi40L2YxjGfQNIp92mlsgcE32TCMm+nBem6oMA0vRjIbjYdrslUt5W6Dwk6oH7AiiJvnHL1Rtsu4UAN235qmdh9Q78Y2enGQk/iHI0mw2+L7v44eIN7g4EyNlz5zt+Ut9UUhMXvWNmIx5jiOXqfEmEna47ApCWACSR0Pq7LOeMsiR16jvt9cVF4KbqjaYApeHJZXWEG8iNT1IqlT70FtISE63Z3BHpfNpP2aTk9vleAFn3AwgW+U0uEnckbHguOzZBS9e+opXbN7lGpJr1xMV3aMlk6GCAU5U2C5TNZpmM+khW8kEAsuoHEFwsMi1Wuvx7H21yVoL60Mb6Cl24cIEeOrWoxLitWtbo6AhzAz3EjK4lkpSxXJbGJ6aY/PW7J91wTHtXggBkQ5mHSADqEhAH8+hWTI/NnaDHHjlNszMzSnUpCxC4uyiqvfj8vVXeLhjgXeI/h6WHjSAA2SJzmTim4kKsmo8VeN0UBZ+M13Q6bYob3jMzq7gIJUUBiJfrsX3KA4XwMhVrUtQtpFbtllOcBpQJtwW/rIdcHT7AUZUyMCAlVXLIViILjJjxbqmv3tHZQpJDDUjMLJsYtt2K214yVi+AbKqSQ7iOhAQEynYL0giEyK5SqaTph2P35Q46kMJiEICU3QpifT84kWBGyzEBbN3Bt+udDiVF2poSbqvTGV6XZbjrsTwwIIJZNvwAAgtB2sv1LIf1Cisgg/lmMiPU7nSGFg4Xll4TumwGYSFEPhqvk0mNGe1Oq9nNuuzcG9waAIGFwFpoSF0WA+Jc6XUdmsfvKqheGKqhXD5pNlyXcBFDwLRBtpZu3mZy5zW4WzxkJJMJNe2F5Y6P5SibG2c3vL9s4ro3JR8kIOuKU2Vf+cSJHCI7gWtbWDxNL/24QbmxcYV9N5IYpjO7szSEgQnTEGn8ZjHPk21/4mF072cQHYZiIWb5JMHNDg1JDq2ZbAdKuVyiJ59+np790Su2TNiJiEbRdfLtN1fo2jfv0PH5+X3W6KHbJFCXVVJ1WVAQ3E63nuUSF6DUVrMpYkhbiYtABwnhEkdGc6EDUq9WOEHBZNs7aQzZQelS8QgMEHxYW+H1DICmJdhlGV66TsTz8M1YkVPhIYg/ui4yNCcLDAqQepU0fx0ubfLYlutVwSCGSH9zKoBYbJ2kslz9u1Mnh91bjFhkRBKlIEwyH9/X8AqIV8fruvTYN9MCFxGAILgP+zCXE+q2C27WZHNo/ykFCcimKiBsIWDrIu2FW3HiIsMBiM4rm3GbtR0XDtLwyuW8ammLFLcmWBaCJU80PHCBcYjbgWDl3NaUSNgD4jyhtwIDRFL+oiog8LetbvfJcFsIYiGaG0xLN/YXgBBX7GNLXujQ0zYATeGaCoo2zgQKN8H1LJGWYoYF3Vrau1knzIHmcXTS9LcQ18qC52q5CiBbqhbCWZbgFvgzWvdRrwq6tdT6nrAzLcRCuN5+WxCC1F14FkJmswPK6au3b/Cf0acVeAsVKI4A2eqT8mItAA+xLTc+QROT056+Bq+3W6txCehKulMBRLkdCDWsXC5Hlz+5SB9dfD+UWdztOhnxVsuy1viXbizTG7/6NT334s+8WchOg1p90vdefuWwVL0WBiC+t0hrmuapfDIIIJqW9EQ+udwvUli0DY2NTyqQwhovE9ilvUHpTgWQkl+FJZMps9MvhODbbQNKp8nLaUC8w0vEgsnJcXZZnql2oyZ7vvZ/B7cGGbqThWyHBYiuwF3uqTeJ/3leeFLp1eWCnmEB4yWGGHyWFThSWpbsPfGQ7uZV5UnVCctCsMCCpdxxpTgCE++0eSOom8ti7iLSSsQdrwFaPa6ZLguup3/DtA0PEVlW/z0hsd2g3v/+KmEF9bz84HGVmy9vb9Pc/Al65bU3uJqLTZp2I5XJ0HZpkz6++B4rDClmGLt4sb8DC1oq/cA7jQaX+fvaqHuGFQogVfnBZ5QYrkgXccGnTj/Kyu3IDsN+AzO2Oj1Llz/+kCrVGrcGhUH5kL7CXSU9dtsj5UUM8blJZ4MUmkRUAGmRj6M20qmUYLll2cVoiD/3358H0EYEAFj8gbKwD9wIKQlgQJioeqsacMrbatm83tVCrLUkb9zN6wvPnjvvCxCLhKGmpWkpBgNK3ytWSQXpa5L7s8JtB0IfmNfdXVZ/Wcyl9G6XDEndBQuIXy5iFhjbXHpwbSnVdQYMFhJefcrg9FXlWAzED3O/pI3LGuDkhkEBUbYQBOcWd5+476ZCwEc1dWQ0a65RhxDQzc81KDemxkFgsXtJYRdQ50pvKUxAfPVnYS0BN8W7qci+80QetESZzGho5Xpd7p3P5sYUCotN7qDpb1GuE2fjUFkIbgI3Y+0VcQMQbgFbx2p1c4ESO3qDODqD45Rmprm1Wl1pdzDih9POLJduEyUL0RTvK0/qiHD+brUD8XqC3v8sEGumoVFua2ub/44tZDkh4A0gZjivyqsrg9K5HUkoFN8PcgeCipQ6pcDS8V67ozQ8xLp8mIAo9/jG5Goa0l4oEjGiQ7oteOXtEp089TD97vd/oLU7N2lVSD6fp0LxLvcLoyiYSae7s51kSdxq57TKJ/D5UCSsAW2gAHZ2bp5+8NxDdPzEA3R8YdE7AauUe75jvws8SEBAJjDVlbbK9m4AdatnoS8rmUzTmUe/x4LsZntrk9ZWblN+7Q4V8qtUKBSoIngK4gwAwmlynXaCO1yqNfPkCvThYnPm6UefFAAs0tz8SWFp4746HMGjEHecqsk2sRH6qocJSEX6xFHvHkuefcLVUp1PCuq4uBl0d2zdLTJ41llY07PHyXjqWapWK1S6W6DC+oqwnluUF48b+SKDMjk1RYsPPkyzx+bp2PwCgzDoSRKIH+BRcX9LB66HzQRhIQjsCypvSsjVQvtcfr/r2rWYNrXL2932VAA0L1zO4qkz9MwLWKeo0J1bSxz8F4Wr4/O5gszKOrpJCq0O9z6ld4fCYjEKQNTWRaz+LDQ7wB35Oa2hx38DVARnk22nODg//PhToa2pt1pNdoWOdayY4zqI0oZZJYd69tz5Gvk4HUiTDXNsIYM2zMXM9XMINvdUy1sixoR3Ijosu2kLiGu3ybbQWT00QOQoq76B014++6QZrLbkaaQ+l1U9WsgOu02fsUhZV34A8XWWb0O2A2ENghetLD5xyLsZYYUgtnvdUjfjCqjbxG8MwbihWDyilAi0mtakm999Q2PjEzSazXFVl8/4ReFOxBa95yeCYoekyxEWfXPpKvMfs1HD6A+K/bgeBSCraniY5ZCpyUm6/OlHdPWrzwVBWxDc4CSdOPkgTc4cozFZTDRTzCaDxEdlxOORHoCG9HartEkrt5doffU2lYp5TiDGZDOED7e4HgUgykHdupExQdZw09evfU1XvvicZ96x2VmaP3mKFh54iAEaG5vg3VCm9dQ5qFodHS79s+TnulAZKGys0Z3l72jt9k0qlUrM7NHnhWUAXrW0AcIDQOUoAMEKGKKzcv4KpWIdYlRYBIp1yLxKW1uC2F2i/352iWtWM7NztLD4oADoNE3DeiYm2UoAJFbuur9+5jP2oGKwIRg/ZH11mc+/wuqkuZM3TRPCkpFOWzUwJ6W7bGNrko/D3/wCUvMDiFVAbMtUERxiWi5GgZ3X6w26dfMGXbt2jTLpFFsP2DYE58hPTM1QVvIYk4/Uuy2eTkwbrB+sHiBsFtZFmlzi70JsGxGWMDM9zbUxK1vj/YOuG093g7rNLmNfR+z6dVkAZCqIDGY3Nda4/QdixZNKpUL5K58L9/YZFwfRhzsvgIF7QxzKiuQglov3MGlz1qL2tFnYYAa/JqygVtnmIiPqUdjPnsuN0biwvF72zRtygj3Wo0Y+mgv9ALLpxzd6jTP3ApQUAXWcn0OjWj6/QUtLS8Ld/ZOOzUyx5cC9nTrzGL9nZfkGg1DYWOHyPSoE2dERdpPTM1mZFel7fmvEGOya7WNamfwsefu0kDpFMNhHSyvShF+fmsoImeLYg6ru9W+v0rWrX1Puk4vc3QI3BKWn4e7m5rplGjOl1gOmPIYXC1GeuDE/DPftt958Tzy8dlD8oJenIPBiDwoOrkF/r1ldjtueiRhckSDGrUEd3sbdN4b84+y587+MwkJ85deBzs3eII7sCGdtpdO7VmV0QgWiu4XNubCY9/P5fgEp0SEa1rEWYVV895beYX0op8AVOjSG340SkPv2t9Z3QY11YwWnwrLRgeMR7a6BOAASqYXcvX9B2AXAkAnB3rN4GQR3ayxFCUjx/gBh1wpALlksAIzd45Z8ni2/GSUgYOu+Nu8cvBXI7MwCQC4DGPsquQPdWidqC1mTXzg9LG7ILKN0TABkm6qVGltpdIC/slDym4nG/Obqgou8KB5+IeQlIc8JmT+QDEuebG1ykHtPWbD6s+6xAvdg7HfgXPdLQv4t5F3BQS5FCkgPMHg4JeSHQl4X8hMhT0flzixA0hmzExG7o3RpAd1gHA4I2GLwmQTgfYAhQLgz6IdqAV3cLSkXhGTJ3GUFy8Em8J8LWQzNNUmSZq2b9A3GwfGTG0I+EPIvIR/h76pNDFEB0jtQdv5Cyp+EzAp5WchPpfU8QQFUivcRQ+mqYsEeA4Vs8itpBR8K+U/YKb8WgVcBifybFOqxntclUI8FZSkBDPi4r4VcFPKeBOBWlDFRo+jHd1L+ImRSWszL0npgRccivp5V6YIgHwu5Sgf4O/IaHewoyVkI+aPM1JC9vSbjD6xnJODvhEv9n4wFkE/Jx4/W3K+A3JswmbP1r1LQpPt9aT0A53mZzan6prYMxp9KK4A7+lK6p0M3NDq8Y0fm9ZA3ZXLwjJBXZeb2LNl34WMR7bJMRz+QCcZQ1N80Gp6B5OBdKWmZSr8gwXld8oK/Syu4LInaDg3Z+L8AAwC5zGYnR8jzIwAAAABJRU5ErkJggg=="); + width: 100px; + height: 100px; + position: absolute; + bottom: 60px; + left: 50%; + margin: 0 -50px 0 -50px; } /* index.html */ #container { - display: -webkit-box; - display: -ms-flexbox; - display: -webkit-flex; - display: flex; - -webkit-flex-direction: column; - -webkit-box-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-justify-content: center; - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-box-align: center; - -webkit-align-items: center; - -ms-flex-align: center; - align-items: center; + display: -webkit-box; + display: -ms-flexbox; + display: -webkit-flex; + display: flex; + -webkit-flex-direction: column; + -webkit-box-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-justify-content: center; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; } @media screen and (max-height: 400px) { - .button { - padding: 5px; - } -} \ No newline at end of file + .button { + padding: 5px; + } +} diff --git a/tests/index.html b/tests/index.html index 50578f6a..07cd4356 100644 --- a/tests/index.html +++ b/tests/index.html @@ -13,15 +13,15 @@
diff --git a/tests/js/core.html5audio.js b/tests/js/core.html5audio.js index 14db9972..8cab163e 100644 --- a/tests/js/core.html5audio.js +++ b/tests/js/core.html5audio.js @@ -1,270 +1,275 @@ -import { Howl } from 'howler'; +import { Howl } from "howler"; // Cache the label for later use. -const label = document.getElementById('label'); -const start = document.getElementById('start'); +const label = document.getElementById("label"); +const start = document.getElementById("start"); // Setup the sounds to be used. const sound1 = new Howl({ - src: ['audio/sound1.webm', 'audio/sound1.mp3'], - html5: true + src: ["audio/sound1.webm", "audio/sound1.mp3"], + html5: true, }); const sound2 = new Howl({ - src: ['audio/sound2.webm', 'audio/sound2.mp3'], - html5: true, - sprite: { - one: [0, 450], - two: [2000, 250], - three: [4000, 350], - four: [6000, 380], - five: [8000, 340], - beat: [10000, 11163] - } + src: ["audio/sound2.webm", "audio/sound2.mp3"], + html5: true, + sprite: { + one: [0, 450], + two: [2000, 250], + three: [4000, 350], + four: [6000, 380], + five: [8000, 340], + beat: [10000, 11163], + }, }); // Enable the start button when the sounds have loaded. -sound1.once('load', function() { - start.removeAttribute('disabled'); - start.innerHTML = 'BEGIN CORE TESTS'; +sound1.once("load", () => { + start.removeAttribute("disabled"); + start.innerHTML = "BEGIN CORE TESTS"; }); // Define the tests to run. var id; var tests = [ - function(fn) { - id = sound1.play(); + (fn) => { + id = sound1.play(); - label.innerHTML = 'PLAYING'; - setTimeout(fn, 2000); - }, + label.innerHTML = "PLAYING"; + setTimeout(fn, 2000); + }, - function(fn) { - sound1.pause(id); + (fn) => { + sound1.pause(id); - label.innerHTML = 'PAUSED'; - setTimeout(fn, 1500); - }, + label.innerHTML = "PAUSED"; + setTimeout(fn, 1500); + }, - function(fn) { - sound1.play(id); + (fn) => { + sound1.play(id); - label.innerHTML = 'RESUMING'; - setTimeout(fn, 2000); - }, + label.innerHTML = "RESUMING"; + setTimeout(fn, 2000); + }, - function(fn) { - sound1.stop(id); + (fn) => { + sound1.stop(id); - label.innerHTML = 'STOPPED'; - setTimeout(fn, 1500); - }, + label.innerHTML = "STOPPED"; + setTimeout(fn, 1500); + }, - function(fn) { - sound1.play(id); + (fn) => { + sound1.play(id); - label.innerHTML = 'PLAY FROM START'; - setTimeout(fn, 2000); - }, + label.innerHTML = "PLAY FROM START"; + setTimeout(fn, 2000); + }, - function(fn) { - sound1.rate(1.5, id); + (fn) => { + sound1.rate(1.5, id); - label.innerHTML = 'SPEED UP'; - setTimeout(fn, 2000); - }, + label.innerHTML = "SPEED UP"; + setTimeout(fn, 2000); + }, - function(fn) { - sound1.rate(1, id); + (fn) => { + sound1.rate(1, id); - label.innerHTML = 'SLOW DOWN'; - setTimeout(fn, 2000); - }, + label.innerHTML = "SLOW DOWN"; + setTimeout(fn, 2000); + }, - function(fn) { - sound1.fade(1, 0, 2000, id); + (fn) => { + sound1.fade(1, 0, 2000, id); - label.innerHTML = 'FADE OUT'; - sound1.once('fade', function() { - fn(); - }); - }, + label.innerHTML = "FADE OUT"; + sound1.once("fade", () => { + fn(); + }); + }, - function(fn) { - sound1.fade(0, 1, 2000, id); + (fn) => { + sound1.fade(0, 1, 2000, id); - label.innerHTML = 'FADE IN'; - sound1.once('fade', function() { - fn(); - }); - }, + label.innerHTML = "FADE IN"; + sound1.once("fade", () => { + fn(); + }); + }, - function(fn) { - sound1.mute(true, id); + (fn) => { + sound1.mute(true, id); - label.innerHTML = 'MUTE'; - setTimeout(fn, 1500); - }, + label.innerHTML = "MUTE"; + setTimeout(fn, 1500); + }, - function(fn) { - sound1.mute(false, id); + (fn) => { + sound1.mute(false, id); - label.innerHTML = 'UNMUTE'; - setTimeout(fn, 2000); - }, + label.innerHTML = "UNMUTE"; + setTimeout(fn, 2000); + }, - function(fn) { - sound1.volume(0.5, id); + (fn) => { + sound1.volume(0.5, id); - label.innerHTML = 'HALF VOLUME'; - setTimeout(fn, 2000); - }, + label.innerHTML = "HALF VOLUME"; + setTimeout(fn, 2000); + }, - function(fn) { - sound1.volume(1, id); + (fn) => { + sound1.volume(1, id); - label.innerHTML = 'FULL VOLUME'; - setTimeout(fn, 2000); - }, + label.innerHTML = "FULL VOLUME"; + setTimeout(fn, 2000); + }, - function(fn) { - sound1.seek(0, id); + (fn) => { + sound1.seek(0, id); - label.innerHTML = 'SEEK TO START'; - setTimeout(fn, 2000); - }, + label.innerHTML = "SEEK TO START"; + setTimeout(fn, 2000); + }, - function(fn) { - id = sound1.play(); + (fn) => { + id = sound1.play(); - label.innerHTML = 'PLAY 2ND'; - setTimeout(fn, 2000); - }, + label.innerHTML = "PLAY 2ND"; + setTimeout(fn, 2000); + }, - function(fn) { - sound1.mute(true); + (fn) => { + sound1.mute(true); - label.innerHTML = 'MUTE GROUP'; - setTimeout(fn, 1500); - }, + label.innerHTML = "MUTE GROUP"; + setTimeout(fn, 1500); + }, - function(fn) { - sound1.mute(false); - - label.innerHTML = 'UNMUTE GROUP'; - setTimeout(fn, 2000); - }, - - function(fn) { - sound1.volume(0.5); - - label.innerHTML = 'HALF VOLUME GROUP'; - setTimeout(fn, 2000); - }, - - function(fn) { - sound1.fade(0.5, 0, 2000); - - label.innerHTML = 'FADE OUT GROUP'; - sound1.once('fade', function() { - if (sound1._onfade.length === 0) { - fn(); - } - }); - }, - - function(fn) { - sound1.fade(0, 1, 2000); - - label.innerHTML = 'FADE IN GROUP'; - sound1.once('fade', function() { - if (sound1._onfade.length === 0) { - fn(); - } - }); - }, - - function(fn) { - sound1.stop(); - - label.innerHTML = 'STOP GROUP'; - setTimeout(fn, 1500); - }, - - function(fn) { - id = sound2.play('beat'); - - label.innerHTML = 'PLAY SPRITE'; - setTimeout(fn, 2000); - }, - - function(fn) { - sound2.pause(id); - - label.innerHTML = 'PAUSE SPRITE'; - setTimeout(fn, 1000); - }, - - function(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 { + sound1.mute(false); + + label.innerHTML = "UNMUTE GROUP"; + setTimeout(fn, 2000); + }, + + (fn) => { + sound1.volume(0.5); + + label.innerHTML = "HALF VOLUME GROUP"; + setTimeout(fn, 2000); + }, + + (fn) => { + sound1.fade(0.5, 0, 2000); + + label.innerHTML = "FADE OUT GROUP"; + sound1.once("fade", () => { + if (sound1._onfade.length === 0) { + fn(); + } + }); + }, + + (fn) => { + sound1.fade(0, 1, 2000); + + label.innerHTML = "FADE IN GROUP"; + sound1.once("fade", () => { + if (sound1._onfade.length === 0) { + fn(); + } + }); + }, + + (fn) => { + sound1.stop(); + + label.innerHTML = "STOP GROUP"; + setTimeout(fn, 1500); + }, + + (fn) => { + id = sound2.play("beat"); + + label.innerHTML = "PLAY SPRITE"; + setTimeout(fn, 2000); + }, + + (fn) => { + sound2.pause(id); + + label.innerHTML = "PAUSE SPRITE"; + setTimeout(fn, 1000); + }, + + (fn) => { + sound2.play(id); + + label.innerHTML = "RESUME SPRITE"; + setTimeout(fn, 1500); + }, + + (fn) => { + var sounds = ["one", "two", "three", "four", "five"]; + for (var i = 0; i < sounds.length; i++) { + setTimeout( + ((i) => { + sound2.play(sounds[i]); + }).bind(null, i), + i * 500, + ); + } + + label.innerHTML = "MULTIPLE SPRITES"; + setTimeout(fn, 3000); + }, + + (fn) => { + var sprite = sound2.play("one"); + sound2.loop(true, sprite); + + label.innerHTML = "LOOP SPRITE"; + setTimeout(() => { + sound2.loop(false, sprite); + fn(); + }, 3000); + }, + + (fn) => { + sound2.fade(1, 0, 2000, id); + + label.innerHTML = "FADE OUT SPRITE"; + sound2.once("fade", () => { + fn(); + }); + }, ]; // Create a method that will call the next in the series. -var chain = function(i) { - return function() { - if (tests[i]) { - tests[i](chain(++i)); - } else { - document.getElementById('logo').style.display = 'none'; - label.innerHTML = 'COMPLETE!'; - label.style.color = '#74b074'; - - // Wait for 5 seconds and then go back to the tests index. - setTimeout(function() { - window.location = './'; - }, 5000); - } - }; +var chain = (i) => () => { + if (tests[i]) { + tests[i](chain(++i)); + } else { + document.getElementById("logo").style.display = "none"; + label.innerHTML = "COMPLETE!"; + label.style.color = "#74b074"; + + // Wait for 5 seconds and then go back to the tests index. + setTimeout(() => { + window.location = "./"; + }, 5000); + } }; // 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 e4228012..7d304ed0 100644 --- a/tests/js/core.webaudio.js +++ b/tests/js/core.webaudio.js @@ -1,274 +1,287 @@ -import { Howl, Howler } from 'howler'; +import { Howl, Howler } from "howler"; // Cache the label for later use. -const label = document.getElementById('label'); -const start = document.getElementById('start'); +const label = document.getElementById("label"); +const start = document.getElementById("start"); // Setup the sounds to be used. const sound1 = new Howl({ - src: ['audio/sound1.webm', 'audio/sound1.mp3'] + src: ["audio/sound1.webm", "audio/sound1.mp3"], }); const sound2 = new Howl({ - src: ['audio/sound2.webm', 'audio/sound2.mp3'], - sprite: { - one: [0, 450], - two: [2000, 250], - three: [4000, 350], - four: [6000, 380], - five: [8000, 340], - beat: [10000, 11163] - } + src: ["audio/sound2.webm", "audio/sound2.mp3"], + sprite: { + one: [0, 450], + two: [2000, 250], + three: [4000, 350], + four: [6000, 380], + five: [8000, 340], + beat: [10000, 11163], + }, }); // Enable the start button when the sounds have loaded. -sound1.once('load', function() { - start.removeAttribute('disabled'); - start.innerHTML = 'BEGIN CORE TESTS'; +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() { - label.innerHTML = 'PLAYING'; - setTimeout(fn, 2000); - }); - - id = sound1.play(); - }, + (fn) => { + sound1.once("play", () => { + label.innerHTML = "PLAYING"; + setTimeout(fn, 2000); + }); - function(fn) { - sound1.pause(id); + id = sound1.play(); + }, - label.innerHTML = 'PAUSED'; - setTimeout(fn, 1500); - }, + (fn) => { + sound1.pause(id); - function(fn) { - sound1.play(id); + label.innerHTML = "PAUSED"; + setTimeout(fn, 1500); + }, - label.innerHTML = 'RESUMING'; - setTimeout(fn, 2000); - }, + (fn) => { + sound1.play(id); - function(fn) { - sound1.stop(id); + label.innerHTML = "RESUMING"; + setTimeout(fn, 2000); + }, - label.innerHTML = 'STOPPED'; - setTimeout(fn, 1500); - }, + (fn) => { + sound1.stop(id); - function(fn) { - sound1.play(id); + label.innerHTML = "STOPPED"; + setTimeout(fn, 1500); + }, - label.innerHTML = 'PLAY FROM START'; - setTimeout(fn, 2000); - }, + (fn) => { + sound1.play(id); - function(fn) { - sound1.rate(1.5, id); + label.innerHTML = "PLAY FROM START"; + setTimeout(fn, 2000); + }, - label.innerHTML = 'SPEED UP'; - setTimeout(fn, 2000); - }, + (fn) => { + sound1.rate(1.5, id); - function(fn) { - sound1.rate(1, id); + label.innerHTML = "SPEED UP"; + setTimeout(fn, 2000); + }, - label.innerHTML = 'SLOW DOWN'; - setTimeout(fn, 2000); - }, + (fn) => { + sound1.rate(1, id); - function(fn) { - sound1.fade(1, 0, 2000, id); + label.innerHTML = "SLOW DOWN"; + setTimeout(fn, 2000); + }, - label.innerHTML = 'FADE OUT'; - sound1.once('fade', function() { - fn(); - }, id); - }, + (fn) => { + sound1.fade(1, 0, 2000, id); - function(fn) { - sound1.fade(0, 1, 2000, id); + label.innerHTML = "FADE OUT"; + sound1.once( + "fade", + () => { + fn(); + }, + id, + ); + }, + + (fn) => { + sound1.fade(0, 1, 2000, id); - label.innerHTML = 'FADE IN'; - sound1.once('fade', function() { - fn(); - }, id); - }, + label.innerHTML = "FADE IN"; + sound1.once( + "fade", + () => { + fn(); + }, + id, + ); + }, - function(fn) { - sound1.mute(true, id); + (fn) => { + sound1.mute(true, id); - label.innerHTML = 'MUTE'; - setTimeout(fn, 1500); - }, + label.innerHTML = "MUTE"; + setTimeout(fn, 1500); + }, - function(fn) { - sound1.mute(false, id); + (fn) => { + sound1.mute(false, id); - label.innerHTML = 'UNMUTE'; - setTimeout(fn, 2000); - }, + label.innerHTML = "UNMUTE"; + setTimeout(fn, 2000); + }, - function(fn) { - sound1.volume(0.5, id); + (fn) => { + sound1.volume(0.5, id); - label.innerHTML = 'HALF VOLUME'; - setTimeout(fn, 2000); - }, + label.innerHTML = "HALF VOLUME"; + setTimeout(fn, 2000); + }, + + (fn) => { + sound1.volume(1, id); + + label.innerHTML = "FULL VOLUME"; + setTimeout(fn, 2000); + }, - function(fn) { - sound1.volume(1, id); - - label.innerHTML = 'FULL VOLUME'; - setTimeout(fn, 2000); - }, - - function(fn) { - sound1.seek(0, id); - - label.innerHTML = 'SEEK TO START'; - setTimeout(fn, 2000); - }, - - function(fn) { - id = sound1.play(); - - label.innerHTML = 'PLAY 2ND'; - setTimeout(fn, 2000); - }, - - function(fn) { - sound1.mute(true); - - label.innerHTML = 'MUTE GROUP'; - setTimeout(fn, 1500); - }, - - function(fn) { - sound1.mute(false); - - label.innerHTML = 'UNMUTE GROUP'; - setTimeout(fn, 2000); - }, - - function(fn) { - sound1.volume(0.5); - - label.innerHTML = 'HALF VOLUME GROUP'; - setTimeout(fn, 2000); - }, - - function(fn) { - sound1.fade(0.5, 0, 2000); - - label.innerHTML = 'FADE OUT GROUP'; - sound1.once('fade', function() { - if (sound1._onfade.length === 0) { - fn(); - } - }); - }, - - function(fn) { - sound1.fade(0, 1, 2000); - - label.innerHTML = 'FADE IN GROUP'; - sound1.once('fade', function() { - if (sound1._onfade.length === 0) { - fn(); - } - }); - }, - - function(fn) { - sound1.stop(); - - label.innerHTML = 'STOP GROUP'; - setTimeout(fn, 1500); - }, - - function(fn) { - id = sound2.play('beat'); - - label.innerHTML = 'PLAY SPRITE'; - setTimeout(fn, 2000); - }, - - function(fn) { - sound2.pause(id); - - label.innerHTML = 'PAUSE SPRITE'; - setTimeout(fn, 1000); - }, - - function(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 { + sound1.seek(0, id); + + label.innerHTML = "SEEK TO START"; + setTimeout(fn, 2000); + }, + + (fn) => { + id = sound1.play(); + + label.innerHTML = "PLAY 2ND"; + setTimeout(fn, 2000); + }, + + (fn) => { + sound1.mute(true); + + label.innerHTML = "MUTE GROUP"; + setTimeout(fn, 1500); + }, + + (fn) => { + sound1.mute(false); + + label.innerHTML = "UNMUTE GROUP"; + setTimeout(fn, 2000); + }, + + (fn) => { + sound1.volume(0.5); + + label.innerHTML = "HALF VOLUME GROUP"; + setTimeout(fn, 2000); + }, + + (fn) => { + sound1.fade(0.5, 0, 2000); + + label.innerHTML = "FADE OUT GROUP"; + sound1.once("fade", () => { + if (sound1._onfade.length === 0) { + fn(); + } + }); + }, + + (fn) => { + sound1.fade(0, 1, 2000); + + label.innerHTML = "FADE IN GROUP"; + sound1.once("fade", () => { + if (sound1._onfade.length === 0) { + fn(); + } + }); + }, + + (fn) => { + sound1.stop(); + + label.innerHTML = "STOP GROUP"; + setTimeout(fn, 1500); + }, + + (fn) => { + id = sound2.play("beat"); + + label.innerHTML = "PLAY SPRITE"; + setTimeout(fn, 2000); + }, + + (fn) => { + sound2.pause(id); + + label.innerHTML = "PAUSE SPRITE"; + setTimeout(fn, 1000); + }, + + (fn) => { + sound2.play(id); + + label.innerHTML = "RESUME SPRITE"; + setTimeout(fn, 1500); + }, + + (fn) => { + var sounds = ["one", "two", "three", "four", "five"]; + for (var i = 0; i < sounds.length; i++) { + setTimeout( + ((i) => { + sound2.play(sounds[i]); + }).bind(null, i), + i * 500, + ); + } + + label.innerHTML = "MULTIPLE SPRITES"; + setTimeout(fn, 3000); + }, + + (fn) => { + var sprite = sound2.play("one"); + sound2.loop(true, sprite); + + label.innerHTML = "LOOP SPRITE"; + setTimeout(() => { + sound2.loop(false, sprite); + fn(); + }, 3000); + }, + + (fn) => { + sound2.fade(1, 0, 2000, id); + + label.innerHTML = "FADE OUT SPRITE"; + sound2.once("fade", () => { + fn(); + }); + }, ]; // Create a method that will call the next in the series. -var chain = function(i) { - return function() { - if (tests[i]) { - tests[i](chain(++i)); - } else { - label.innerHTML = 'COMPLETE!'; - label.style.color = '#74b074'; - - // Wait for 5 seconds and then go back to the tests index. - setTimeout(function() { - window.location = './'; - }, 5000); - } - }; +var chain = (i) => () => { + if (tests[i]) { + tests[i](chain(++i)); + } else { + label.innerHTML = "COMPLETE!"; + label.style.color = "#74b074"; + + // Wait for 5 seconds and then go back to the tests index. + setTimeout(() => { + window.location = "./"; + }, 5000); + } }; // 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); + // Listen to a click on the button to being the tests. + start.addEventListener( + "click", + () => { + tests[0](chain(1)); + start.style.display = "none"; + }, + false, + ); } else { - window.location = 'core.html5audio.html'; -} \ No newline at end of file + window.location = "core.html5audio.html"; +} diff --git a/tests/js/spatial.js b/tests/js/spatial.js index b5712537..e76d771d 100644 --- a/tests/js/spatial.js +++ b/tests/js/spatial.js @@ -1,125 +1,128 @@ -import { Howl, Howler } from 'howler'; -import { SpatialAudioPlugin } from 'howler/plugins/spatial'; +import { Howl, Howler } from "howler"; +import { SpatialAudioPlugin } from "howler/plugins/spatial"; // Register the Spatial Audio Plugin -Howler.addPlugin(new SpatialAudioPlugin()); +const spatialPlugin = new SpatialAudioPlugin(); +Howler.addPlugin(spatialPlugin); // Cache the label for later use. -const label = document.getElementById('label'); -const start = document.getElementById('start'); +const label = document.getElementById("label"); +const start = document.getElementById("start"); // Setup the sounds to be used. const sound1 = new Howl({ - src: ['audio/sound1.webm', 'audio/sound1.mp3'] + src: ["audio/sound1.webm", "audio/sound1.mp3"], }); const sound2 = new Howl({ - src: ['audio/sound2.webm', 'audio/sound2.mp3'], - sprite: { - one: [0, 450], - two: [2000, 250], - three: [4000, 350], - four: [6000, 380], - five: [8000, 340], - beat: [10000, 11163] - } + src: ["audio/sound2.webm", "audio/sound2.mp3"], + sprite: { + one: [0, 450], + two: [2000, 250], + three: [4000, 350], + four: [6000, 380], + five: [8000, 340], + beat: [10000, 11163], + }, }); // Enable the start button when the sounds have loaded. -sound1.once('load', function() { - start.removeAttribute('disabled'); - start.innerHTML = 'BEGIN SPATIAL TESTS'; +sound1.once("load", () => { + start.removeAttribute("disabled"); + start.innerHTML = "BEGIN SPATIAL TESTS"; }); // Define the tests to run. var id; var tests = [ - function(fn) { - sound1.once('play', function() { - label.innerHTML = 'PLAYING'; - setTimeout(fn, 2000); - }); - - id = sound1.play(); - }, - - function(fn) { - sound1.stereo(-1, id); - - label.innerHTML = 'LEFT STEREO'; - setTimeout(fn, 2000); - }, - - function(fn) { - sound1.stereo(1, id); - - label.innerHTML = 'RIGHT STEREO'; - setTimeout(function() { - fn(); - }, 2000); - }, - - function(fn) { - sound1.pos(-2, 0, -0.5, id); - - label.innerHTML = 'LEFT POSITION'; - setTimeout(fn, 2000); - }, - - function(fn) { - sound1.pos(2, 0, -0.5, id); - - label.innerHTML = 'RIGHT POSITION'; - setTimeout(function() { - sound1.stop(); - fn(); - }, 2000); - }, - - function(fn) { - sound2.pos(-3, 0, -0.5, sound2.play('one')); - sound2.once('end', function() { - sound2.pos(0, 3, -0.5, sound2.play('two')); - sound2.once('end', function() { - sound2.pos(3, 0, -0.5, sound2.play('three')); - sound2.once('end', function() { - sound2.pos(0, -3, -0.5, sound2.play('four')); - sound2.once('end', function() { - sound2.stop(); - fn(); - }); - }); - }); - }); - - label.innerHTML = '3D SURROUND'; - } + (fn) => { + sound1.once("play", () => { + label.innerHTML = "PLAYING"; + setTimeout(fn, 2000); + }); + + id = sound1.play(); + }, + + (fn) => { + sound1.stereo(-1, id); + + label.innerHTML = "LEFT STEREO"; + setTimeout(fn, 2000); + }, + + (fn) => { + sound1.stereo(1, id); + + label.innerHTML = "RIGHT STEREO"; + setTimeout(() => { + fn(); + }, 2000); + }, + + (fn) => { + sound1.pos(-2, 0, -0.5, id); + + label.innerHTML = "LEFT POSITION"; + setTimeout(fn, 2000); + }, + + (fn) => { + sound1.pos(2, 0, -0.5, id); + + label.innerHTML = "RIGHT POSITION"; + setTimeout(() => { + sound1.stop(); + fn(); + }, 2000); + }, + + (fn) => { + sound2.pos(-3, 0, -0.5, sound2.play("one")); + sound2.once("end", () => { + sound2.pos(0, 3, -0.5, sound2.play("two")); + sound2.once("end", () => { + sound2.pos(3, 0, -0.5, sound2.play("three")); + sound2.once("end", () => { + sound2.pos(0, -3, -0.5, sound2.play("four")); + sound2.once("end", () => { + sound2.stop(); + fn(); + }); + }); + }); + }); + + label.innerHTML = "3D SURROUND"; + }, ]; // Create a method that will call the next in the series. -var chain = function(i) { - return function() { - if (tests[i]) { - tests[i](chain(++i)); - } else { - label.innerHTML = 'COMPLETE!'; - label.style.color = '#74b074'; - - // Wait for 5 seconds and then go back to the tests index. - setTimeout(function() { - window.location = './'; - }, 5000); - } - }; +var chain = (i) => () => { + if (tests[i]) { + tests[i](chain(++i)); + } else { + label.innerHTML = "COMPLETE!"; + label.style.color = "#74b074"; + + // Wait for 5 seconds and then go back to the tests index. + setTimeout(() => { + window.location = "./"; + }, 5000); + } }; // 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); + // Listen to a click on the button to being the tests. + start.addEventListener( + "click", + () => { + tests[0](chain(1)); + start.style.display = "none"; + }, + false, + ); } else { - window.location = 'core.html5audio.html'; + window.location = "core.html5audio.html"; } diff --git a/tsconfig.json b/tsconfig.json index 82bd4be1..908adb3c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,21 +1,21 @@ { - "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "lib": ["ES2020", "DOM"], - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "node", - "resolveJsonModule": true, - "allowJs": false - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist", "**/*.test.ts"] + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "allowJs": false + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] } diff --git a/tsdown.config.ts b/tsdown.config.ts index c6c1ed6f..9897c1b4 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -1,19 +1,13 @@ -import { defineConfig } from 'tsdown'; +import { defineConfig } from "tsdown"; export default defineConfig({ - entry: 'src/**/*.ts', - format: 'esm', - platform: 'browser', - unbundle: true, - treeshake: true, - minify: true, - // Target browsers that support ES modules natively - // Chrome 61+, Firefox 60+, Safari 11+, Edge 16+, Opera 48+ - target: [ - 'chrome61', - 'firefox60', - 'safari11', - 'edge16', - 'opera48' - ] + entry: "src/**/*.ts", + format: "esm", + platform: "browser", + unbundle: true, + treeshake: true, + minify: true, + // Target browsers that support ES modules natively + // Chrome 61+, Firefox 60+, Safari 11+, Edge 16+, Opera 48+ + target: ["chrome61", "firefox60", "safari11", "edge16", "opera48"], }); From db3f19df99c0818cdde70a33b286b6295bfdf0db Mon Sep 17 00:00:00 2001 From: eatsjobs Date: Sun, 23 Nov 2025 01:33:35 +0100 Subject: [PATCH 20/25] Enhance Howler.js configuration and add size checking script - Added `sideEffects: false` to `package.json` for better tree-shaking. - Introduced a new `size` script in `package.json` to check the bundle sizes using `check-size.js`. - Created `check-size.js` to calculate and log the sizes of core and plugin files, including gzip sizes. - Added `howl.ts`, `howler-global.ts`, `howler.core.ts`, `sound.ts`, and related type definitions to improve audio handling and maintainability. These changes aim to improve project configuration management and provide developers with tools to monitor bundle sizes effectively. --- package.json | 2 + scripts/check-size.js | 128 ++ src/helpers/audio-context.ts | 7 +- src/helpers/audio-loader.ts | 2 +- src/helpers/index.ts | 9 +- src/helpers/light-ua-parser.ts | 30 - src/howl.ts | 1548 +++++++++++++++++++++ src/howler-global.ts | 506 +++++++ src/howler.core.ts | 2314 +------------------------------- src/plugins/spatial-plugin.ts | 154 ++- src/plugins/spatial.ts | 33 +- src/sound.ts | 205 +++ src/types.ts | 140 +- 13 files changed, 2696 insertions(+), 2382 deletions(-) create mode 100755 scripts/check-size.js create mode 100644 src/howl.ts create mode 100644 src/howler-global.ts create mode 100644 src/sound.ts diff --git a/package.json b/package.json index cb3c9409..6281b0c9 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "Javascript audio library for the modern web.", "homepage": "https://howlerjs.com", "type": "module", + "sideEffects": false, "keywords": [ "howler", "howler.js", @@ -24,6 +25,7 @@ }, "scripts": { "build": "tsdown", + "size": "node scripts/check-size.js", "release": "npm run build && git add dist && git commit -m 'build: update dist files' && npm publish" }, "devDependencies": { diff --git a/scripts/check-size.js b/scripts/check-size.js new file mode 100755 index 00000000..c762eabb --- /dev/null +++ b/scripts/check-size.js @@ -0,0 +1,128 @@ +#!/usr/bin/env node + +import { execSync } from 'child_process'; +import { readFileSync, statSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const rootDir = join(__dirname, '..'); + +// Files to check - core library files +const coreFiles = [ + 'dist/index.js', + 'dist/howler.core.js', + 'dist/howler-global.js', + 'dist/howl.js', + 'dist/sound.js', +]; + +// Plugin files +const pluginFiles = [ + 'dist/plugins/spatial.js', + 'dist/plugins/spatial-plugin.js', +]; + +// All files for total calculation +const files = [...coreFiles, ...pluginFiles]; + +function formatBytes(bytes) { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`; +} + +function getGzipSize(filePath) { + try { + const content = readFileSync(filePath); + const result = execSync('gzip -c', { + input: content, + encoding: null, + stdio: ['pipe', 'pipe', 'ignore'], + }); + return result.length; + } catch (e) { + // gzip not available or error + return null; + } +} + +console.log('\n📦 Bundle Sizes:\n'); + +const sizes = files.map((file) => { + const fullPath = join(rootDir, file); + const stats = statSync(fullPath); + const bytes = stats.size; + const gzipBytes = getGzipSize(fullPath); + + return { + file, + bytes, + gzipBytes, + }; +}); + +// Core library files +console.log(' Core Library:'); +coreFiles.forEach((file) => { + const s = sizes.find((size) => size.file === file); + if (s) { + const gzipInfo = s.gzipBytes + ? ` (${formatBytes(s.gzipBytes)} gzipped)` + : ''; + console.log( + ` ${s.file.padEnd(33)} ${s.bytes.toString().padStart(8)} bytes (${formatBytes(s.bytes)})${gzipInfo}`, + ); + } +}); + +// Plugin files +console.log('\n Plugins:'); +pluginFiles.forEach((file) => { + const s = sizes.find((size) => size.file === file); + if (s) { + const gzipInfo = s.gzipBytes + ? ` (${formatBytes(s.gzipBytes)} gzipped)` + : ''; + console.log( + ` ${s.file.padEnd(33)} ${s.bytes.toString().padStart(8)} bytes (${formatBytes(s.bytes)})${gzipInfo}`, + ); + } +}); + +// Calculate totals +const coreTotal = coreFiles.reduce((sum, file) => { + const s = sizes.find((size) => size.file === file); + return sum + (s ? s.bytes : 0); +}, 0); +const coreTotalGzip = coreFiles.reduce((sum, file) => { + const s = sizes.find((size) => size.file === file); + return sum + (s && s.gzipBytes ? s.gzipBytes : 0); +}, 0); + +const pluginTotal = pluginFiles.reduce((sum, file) => { + const s = sizes.find((size) => size.file === file); + return sum + (s ? s.bytes : 0); +}, 0); +const pluginTotalGzip = pluginFiles.reduce((sum, file) => { + const s = sizes.find((size) => size.file === file); + return sum + (s && s.gzipBytes ? s.gzipBytes : 0); +}, 0); + +const total = sizes.reduce((a, b) => a + b.bytes, 0); +const totalGzip = sizes.reduce((a, b) => a + (b.gzipBytes || 0), 0); + +console.log('\n Summary:'); +console.log( + ` ${'Core Library Total'.padEnd(33)} ${coreTotal.toString().padStart(8)} bytes (${formatBytes(coreTotal)})${coreTotalGzip > 0 ? ` (${formatBytes(coreTotalGzip)} gzipped)` : ''}`, +); +console.log( + ` ${'Plugins Total'.padEnd(33)} ${pluginTotal.toString().padStart(8)} bytes (${formatBytes(pluginTotal)})${pluginTotalGzip > 0 ? ` (${formatBytes(pluginTotalGzip)} gzipped)` : ''}`, +); +console.log( + ` ${'Grand Total'.padEnd(33)} ${total.toString().padStart(8)} bytes (${formatBytes(total)})${totalGzip > 0 ? ` (${formatBytes(totalGzip)} gzipped)` : ''}\n`, +); + diff --git a/src/helpers/audio-context.ts b/src/helpers/audio-context.ts index aaf14899..288a2a2c 100644 --- a/src/helpers/audio-context.ts +++ b/src/helpers/audio-context.ts @@ -19,8 +19,6 @@ export const setupAudioContext = () => { try { if (typeof window.AudioContext !== "undefined") { Howler.ctx = new window.AudioContext(); - } else if (typeof (window as any).webkitAudioContext !== "undefined") { - Howler.ctx = new (window as any).webkitAudioContext(); } else { Howler.usingWebAudio = false; } @@ -42,10 +40,7 @@ export const setupAudioContext = () => { } if (Howler.usingWebAudio && Howler.ctx) { - Howler.masterGain = - typeof Howler.ctx.createGain === "undefined" - ? (Howler.ctx as any).createGainNode() - : Howler.ctx.createGain(); + Howler.masterGain = Howler.ctx.createGain(); if (Howler.masterGain) { Howler.masterGain.gain.setValueAtTime( Howler._muted ? 0 : Howler._volume, diff --git a/src/helpers/audio-loader.ts b/src/helpers/audio-loader.ts index 879cc93b..8e83e359 100644 --- a/src/helpers/audio-loader.ts +++ b/src/helpers/audio-loader.ts @@ -10,7 +10,7 @@ import type { Howl } from "../howler.core"; import { Howler } from "../howler.core"; -import { globalPluginManager } from "../plugins"; +import { globalPluginManager } from "../plugins/plugin"; import { cache } from "../types"; export const loadBuffer = (self: Howl) => { diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 23d0b1d7..b8e17643 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -13,7 +13,7 @@ export { decodeAudioData, loadBuffer, loadSound, - safeXhrSend, + safeXhrSend } from "./audio-loader"; export { getIOSVersion, @@ -21,11 +21,8 @@ export { getSafariVersion, isAppleVendor, isChromeBased, - isCocoonJS, - isIE, isIOS, - isOldOpera, - isOldSafari, isOpera, - isSafari, + isSafari } from "./light-ua-parser"; + diff --git a/src/helpers/light-ua-parser.ts b/src/helpers/light-ua-parser.ts index 56b6a443..f22fac6c 100644 --- a/src/helpers/light-ua-parser.ts +++ b/src/helpers/light-ua-parser.ts @@ -65,14 +65,6 @@ export function getSafariVersion(navigator: Navigator | null): number | null { return versionMatch ? parseInt(versionMatch[1], 10) : null; } -/** - * Check if Safari is an old version (before version 15) - */ -export function isOldSafari(navigator: Navigator | null): boolean { - const version = getSafariVersion(navigator); - return version !== null && version < 15; -} - /** * Check if the browser is Opera */ @@ -95,22 +87,6 @@ export function getOperaVersion(navigator: Navigator | null): number | null { return versionMatch ? parseInt(versionMatch[1], 10) : null; } -/** - * Check if Opera is an old version (before version 33) - */ -export function isOldOpera(navigator: Navigator | null): boolean { - const version = getOperaVersion(navigator); - return version !== null && version < 33; -} - -/** - * Check if the browser is Internet Explorer (MSIE or Trident) - */ -export function isIE(navigator: Navigator | null): boolean { - const ua = getUserAgent(navigator); - return /MSIE |Trident\//.test(ua); -} - /** * Check if the browser vendor is Apple */ @@ -126,9 +102,3 @@ export function isChromeBased(navigator: Navigator | null): boolean { return ua.indexOf("Chrome") !== -1; } -/** - * Check if the browser is CocoonJS (for game engines) - */ -export function isCocoonJS(navigator: Navigator | null): boolean { - return !!(navigator as any)?.isCocoonJS; -} diff --git a/src/howl.ts b/src/howl.ts new file mode 100644 index 00000000..e0aefccb --- /dev/null +++ b/src/howl.ts @@ -0,0 +1,1548 @@ +/*! + * howler.js v2.2.4 + * howlerjs.com + * + * (c) 2013-2020, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */ + +import { setupAudioContext } from "./helpers/audio-context"; +import { loadBuffer } from "./helpers/audio-loader"; +import { isAppleVendor } from "./helpers/light-ua-parser"; +import { Howler } from "./howler.core"; +import { globalPluginManager } from "./plugins/plugin"; +import { Sound } from "./sound"; +import { + type AudioBufferSourceNodeWithLegacy, + cache, + type EventListener, + type HowlOptions, + type HTMLAudioElementWithUnlocked, + isGainNode, + isHTMLAudioElement, + type QueueItem +} from "./types"; + +class Howl { + _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: Record = {}; + _src: string | string[] = []; + _volume: number = 1; + _xhr: { + method: string; + headers?: Record; + withCredentials: boolean; + } = { method: "GET", withCredentials: false }; + _duration: number = 0; + _state: string = "unloaded"; + _sounds: Sound[] = []; + _endTimers: Record> = {}; + _queue: QueueItem[] = []; + _playLock: boolean = false; + _webAudio: boolean = false; + _onend: EventListener[] = []; + _onfade: EventListener[] = []; + _onload: EventListener[] = []; + _onloaderror: EventListener[] = []; + _onplayerror: EventListener[] = []; + _onpause: EventListener[] = []; + _onplay: EventListener[] = []; + _onstop: EventListener[] = []; + _onmute: EventListener[] = []; + _onvolume: EventListener[] = []; + _onrate: EventListener[] = []; + _onseek: EventListener[] = []; + _onunlock: EventListener[] = []; + _onresume: EventListener[] = []; + + constructor(o: HowlOptions) { + if (!o.src || o.src.length === 0) { + console.error( + "An array of source files must be passed with any new Howl.", + ); + return; + } + + this.init(o); + } + + init(o: HowlOptions): Howl { + if (!Howler.ctx) { + setupAudioContext(); + } + + this._autoplay = o.autoplay || false; + this._format = 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, + }; + + this._duration = 0; + this._state = "unloaded"; + this._sounds = []; + this._endTimers = {}; + this._queue = []; + this._playLock = 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: (...args: unknown[]) => { + if ( + o.onloaderror && + typeof args[0] === "number" && + typeof args[1] === "string" + ) { + o.onloaderror(args[0], args[1]); + } + }, + }, + ] + : []; + this._onplayerror = o.onplayerror + ? [ + { + fn: (...args: unknown[]) => { + if ( + o.onplayerror && + typeof args[0] === "number" && + typeof args[1] === "string" + ) { + o.onplayerror(args[0], args[1]); + } + }, + }, + ] + : []; + 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.usingWebAudio && !this._html5; + + if (typeof Howler.ctx !== "undefined" && Howler.ctx && Howler.autoUnlock) { + Howler._unlockAudio(); + } + + Howler._howls.push(this); + + // Execute plugin hooks + globalPluginManager.executeHowlCreate(this, o); + + if (this._autoplay) { + this._queue.push({ + event: "play", + action: () => { + this.play(); + }, + }); + } + + if (this._preload === true || this._preload === "metadata") { + this.load(); + } + + return this; + } + + load(): Howl { + let url: string | null = null; + + if (Howler.noAudio) { + this._emit("loaderror", null, "No audio support."); + return this; + } + + if (typeof this._src === "string") { + this._src = [this._src]; + } + + for (let i = 0; i < (this._src as string[]).length; i++) { + let ext: string | null; + const str = (this._src as string[])[i]; + + if (this._format && this._format[i]) { + ext = this._format[i]; + } else { + if (typeof str !== "string") { + this._emit( + "loaderror", + null, + "Non-string found in selected audio sources - ignoring.", + ); + continue; + } + + let extMatch = /^data:audio\/([^;,]+);/i.exec(str); + if (!extMatch) { + extMatch = /\.([^.]+)$/.exec(str.split("?", 1)[0]); + } + + ext = extMatch ? extMatch[1].toLowerCase() : null; + } + + if (!ext) { + console.warn( + 'No file extension was found. Consider using the "format" property or specify an extension.', + ); + } + + if (ext && Howler.codecs(ext)) { + url = (this._src as string[])[i]; + break; + } + } + + if (!url) { + this._emit( + "loaderror", + null, + "No codec support for selected audio sources.", + ); + return this; + } + + this._src = url; + this._state = "loading"; + + if ( + typeof window !== "undefined" && + window.location.protocol === "https:" && + url.slice(0, 5) === "http:" + ) { + this._html5 = true; + this._webAudio = false; + } + + new Sound(this); + + if (this._webAudio) { + loadBuffer(this); + } + + return this; + } + + play(sprite?: string | number, internal?: boolean): number | null { + let id: number | null = null; + + if (typeof sprite === "number") { + id = sprite; + sprite = undefined; + } else if ( + typeof sprite === "string" && + this._state === "loaded" && + !this._sprite[sprite] + ) { + return null; + } else if (typeof sprite === "undefined") { + sprite = "__default"; + + if (!this._playLock) { + let num = 0; + for (let 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 = undefined; + } 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 || "__default"; + sound._ended = false; + + const 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._autoResume(); + } + + const seek = Math.max( + 0, + sound._seek > 0 ? sound._seek : this._sprite[sprite!][0] / 1000, + ); + const duration = Math.max( + 0, + (this._sprite[sprite!][0] + this._sprite[sprite!][1]) / 1000 - seek, + ); + const timeout = (duration * 1000) / Math.abs(sound._rate); + const start = this._sprite[sprite!][0] / 1000; + const stop = (this._sprite[sprite!][0] + this._sprite[sprite!][1]) / 1000; + 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 sound._id; + } + + const node = sound._node; + + if (this._webAudio && node && isGainNode(node)) { + const playWebAudio = () => { + this._playLock = false; + setParams(); + this._refreshBuffer(sound); + + const vol = sound._muted || this._muted ? 0 : sound._volume; + node.gain.setValueAtTime(vol, Howler.ctx!.currentTime); + sound._playStart = Howler.ctx!.currentTime; + + if (node.bufferSource) { + node.bufferSource.start(0, seek, sound._loop ? 86400 : 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.state === "running" && Howler.ctx!.state !== "interrupted") { + playWebAudio(); + } else { + this._playLock = true; + this.once("resume", playWebAudio); + this._clearTimer(sound._id); + } + } else if (node && isHTMLAudioElement(node)) { + const playHtml5 = () => { + node.currentTime = seek; + node.muted = sound._muted || this._muted || Howler._muted || node.muted; + const volume = Howler.volume(); + node.volume = sound._volume * (typeof volume === "number" ? volume : 1); + node.playbackRate = sound._rate; + + try { + const play = node.play(); + + if ( + play && + typeof Promise !== "undefined" && + (play instanceof Promise || + typeof (play as any).then === "function") + ) { + this._playLock = true; + + setParams(); + + (play as any) + .then(() => { + this._playLock = false; + if ("_unlocked" in node) { + (node as HTMLAudioElementWithUnlocked)._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 { + const endHandler = () => { + this._ended(sound); + node.removeEventListener("ended", endHandler, false); + }; + this._endTimers[sound._id] = setTimeout(endHandler, timeout); + node.addEventListener("ended", endHandler, false); + } + } catch (err: unknown) { + this._emit( + "playerror", + sound._id, + err instanceof Error ? err.message : String(err), + ); + } + }; + + if ( + node.src === + "data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA" + ) { + const src = + typeof this._src === "string" + ? this._src + : Array.isArray(this._src) && this._src.length > 0 + ? this._src[0] + : ""; + node.src = src; + node.load(); + } + + const loadedNoReadyState = false; + if (node.readyState >= 3 || loadedNoReadyState) { + playHtml5(); + } else { + this._playLock = true; + this._state = "loading"; + + const listener = () => { + this._state = "loaded"; + playHtml5(); + node.removeEventListener(Howler._canPlayEvent, listener, false); + }; + node.addEventListener(Howler._canPlayEvent, listener, false); + + this._clearTimer(sound._id); + } + } + + return sound._id; + } + + pause(id?: number, internal?: boolean): Howl { + if (this._state !== "loaded" || this._playLock) { + this._queue.push({ + event: "pause", + action: () => { + this.pause(id); + }, + }); + + return this; + } + + const ids = this._getSoundIds(id); + + for (let i = 0; i < ids.length; i++) { + this._clearTimer(ids[i]); + + const sound = this._soundById(ids[i]); + + if (sound && !sound._paused) { + const seekResult = this.seek(ids[i]); + sound._seek = typeof seekResult === "number" ? seekResult : 0; + sound._rateSeek = 0; + sound._paused = true; + + this._stopFade(ids[i]); + + if (sound._node) { + if (this._webAudio && isGainNode(sound._node)) { + if (!sound._node.bufferSource) { + continue; + } + + sound._node.bufferSource.stop(0); + + this._cleanBuffer(sound._node); + } else if ( + isHTMLAudioElement(sound._node) && + (!isNaN(sound._node.duration) || sound._node.duration === Infinity) + ) { + sound._node.pause(); + } + } + } + + if (!arguments[1]) { + this._emit("pause", sound ? sound._id : null); + } + } + + return this; + } + + stop(id?: number, internal?: boolean): Howl { + if (this._state !== "loaded" || this._playLock) { + this._queue.push({ + event: "stop", + action: () => { + this.stop(id); + }, + }); + + return this; + } + + const ids = this._getSoundIds(id); + + for (let i = 0; i < ids.length; i++) { + this._clearTimer(ids[i]); + + const 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 && isGainNode(sound._node)) { + if (sound._node.bufferSource) { + sound._node.bufferSource.stop(0); + + this._cleanBuffer(sound._node); + } + } else if ( + isHTMLAudioElement(sound._node) && + (!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: boolean, id?: number): boolean | Howl { + 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; + } + } + + const ids = this._getSoundIds(id); + + for (let i = 0; i < ids.length; i++) { + const sound = this._soundById(ids[i]); + + if (sound) { + sound._muted = muted; + + if (sound._interval) { + this._stopFade(sound._id); + } + + if (this._webAudio && sound._node && isGainNode(sound._node)) { + sound._node.gain.setValueAtTime( + muted ? 0 : sound._volume, + Howler.ctx!.currentTime, + ); + } else if (sound._node && isHTMLAudioElement(sound._node)) { + sound._node.muted = Howler._muted ? true : muted; + } + + this._emit("mute", sound._id); + } + } + + return this; + } + + volume(): number; + volume(vol: number): Howl; + volume(vol: number, id: number): Howl; + volume(vol: number, id: number, internal: boolean): Howl; + volume(vol?: number): number | Howl { + const args = arguments; + let volume: number | undefined; + let id: number | undefined; + + if (args.length === 0) { + return this._volume; + } else if ( + args.length === 1 || + (args.length === 2 && typeof args[1] === "undefined") + ) { + const ids = this._getSoundIds(); + const index = ids.indexOf(args[0] as number); + if (index >= 0) { + id = parseInt(String(args[0]), 10); + } else { + volume = parseFloat(String(args[0])); + } + } else if (args.length >= 2) { + volume = parseFloat(String(args[0])); + id = parseInt(String(args[1]), 10); + } + + let sound: Sound | null = null; + if (typeof volume !== "undefined" && volume >= 0 && volume <= 1) { + if (this._state !== "loaded" || this._playLock) { + this._queue.push({ + event: "volume", + action: () => { + if (args.length >= 1 && typeof args[0] === "number") { + if (args.length >= 2 && typeof args[1] === "number") { + this.volume(args[0], args[1]); + } else { + this.volume(args[0]); + } + } + }, + }); + + return this; + } + + if (typeof id === "undefined") { + this._volume = volume; + } + + const soundIds = this._getSoundIds(id); + for (let i = 0; i < soundIds.length; i++) { + sound = this._soundById(soundIds[i]); + + if (sound) { + sound._volume = volume; + + if (!args[2]) { + this._stopFade(soundIds[i]); + } + + if ( + this._webAudio && + sound._node && + isGainNode(sound._node) && + !sound._muted + ) { + sound._node.gain.setValueAtTime(volume, Howler.ctx!.currentTime); + } else if ( + sound._node && + isHTMLAudioElement(sound._node) && + !sound._muted + ) { + const volumeMultiplierOrGlobal = Howler.volume(); + if (typeof volumeMultiplierOrGlobal === "number") { + sound._node.volume = volume * volumeMultiplierOrGlobal; + } + } + + this._emit("volume", sound._id); + } + } + } else { + sound = id ? this._soundById(id) : this._sounds[0]; + return sound ? sound._volume : 0; + } + + return this; + } + + fade(from: number, to: number, len: number, id?: number): Howl { + 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(String(from))), 1); + to = Math.min(Math.max(0, parseFloat(String(to))), 1); + len = parseFloat(String(len)); + + if (typeof id !== "undefined") { + this.volume(from, id); + } else { + this.volume(from); + } + + const ids = this._getSoundIds(id); + for (let i = 0; i < ids.length; i++) { + const sound = this._soundById(ids[i]); + + if (sound) { + if (!id) { + this._stopFade(ids[i]); + } + + if (this._webAudio && !sound._muted) { + const currentTime = Howler.ctx!.currentTime; + const end = currentTime + len / 1000; + sound._volume = from; + if (sound._node && isGainNode(sound._node)) { + 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: Sound, + from: number, + to: number, + len: number, + id: number, + isGroup: boolean, + ): void { + let vol = from; + const diff = to - from; + const steps = Math.abs(diff / 0.01); + const stepLen = Math.max(4, steps > 0 ? len / steps : len); + let lastTick = Date.now(); + + sound._fadeTo = to; + + sound._interval = setInterval(() => { + const 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 (sound._interval) { + clearInterval(sound._interval); + } + sound._interval = undefined; + sound._fadeTo = undefined; + this.volume(to, sound._id); + this._emit("fade", sound._id); + } + }, stepLen); + } + + _stopFade(id: number): Howl { + const sound = this._soundById(id); + + if (sound && sound._interval) { + if (this._webAudio && sound._node && isGainNode(sound._node)) { + sound._node.gain.cancelScheduledValues(Howler.ctx!.currentTime); + } + + if (sound._interval) { + clearInterval(sound._interval); + sound._interval = undefined; + } + this.volume(sound._fadeTo as number, id); + sound._fadeTo = undefined; + this._emit("fade", id); + } + + return this; + } + + loop(): boolean; + loop(loop: boolean): Howl; + loop(loop?: boolean): boolean | Howl { + const args = arguments; + let loopVal: boolean | undefined; + let id: number | undefined; + let sound: Sound | null = null; + + if (args.length === 0) { + return this._loop; + } else if (args.length === 1) { + if (typeof args[0] === "boolean") { + loopVal = args[0] as boolean; + this._loop = loopVal; + } else { + sound = this._soundById(parseInt(String(args[0]), 10)); + return sound ? sound._loop : false; + } + } else if (args.length === 2) { + loopVal = args[0] as boolean; + id = parseInt(String(args[1]), 10); + } + + const ids = this._getSoundIds(id); + for (let i = 0; i < ids.length; i++) { + sound = this._soundById(ids[i]); + + if (sound) { + sound._loop = loopVal as boolean; + if ( + this._webAudio && + sound._node && + isGainNode(sound._node) && + sound._node.bufferSource + ) { + sound._node.bufferSource.loop = loopVal; + if (loopVal) { + 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(): number; + rate(rate: number): Howl; + rate(rate: number, id: number): Howl; + rate(rate?: number): number | Howl { + const args = arguments; + let rateVal: number | undefined; + let id: number | undefined; + + if (args.length === 0) { + id = this._sounds[0]._id; + } else if (args.length === 1) { + const ids = this._getSoundIds(); + const index = ids.indexOf(args[0] as number); + if (index >= 0) { + id = parseInt(String(args[0]), 10); + } else { + rateVal = parseFloat(String(args[0])); + } + } else if (args.length === 2) { + rateVal = parseFloat(String(args[0])); + id = parseInt(String(args[1]), 10); + } + + let sound: Sound | null = null; + if (typeof rateVal === "number") { + if (this._state !== "loaded" || this._playLock) { + this._queue.push({ + event: "rate", + action: () => { + if (args.length >= 1 && typeof args[0] === "number") { + if (args.length >= 2 && typeof args[1] === "number") { + this.rate(args[0], args[1]); + } else { + this.rate(args[0]); + } + } + }, + }); + + return this; + } + + if (typeof id === "undefined") { + this._rate = rateVal; + } + + const soundIds = this._getSoundIds(id); + for (let i = 0; i < soundIds.length; i++) { + sound = this._soundById(soundIds[i]); + + if (sound) { + if (this.playing(soundIds[i])) { + const seekResult = this.seek(soundIds[i]); + sound._rateSeek = typeof seekResult === "number" ? seekResult : 0; + sound._playStart = this._webAudio + ? Howler.ctx!.currentTime + : sound._playStart; + } + sound._rate = rateVal; + + if ( + this._webAudio && + sound._node && + isGainNode(sound._node) && + sound._node.bufferSource + ) { + sound._node.bufferSource.playbackRate.setValueAtTime( + rateVal, + Howler.ctx!.currentTime, + ); + } else if (sound._node && isHTMLAudioElement(sound._node)) { + sound._node.playbackRate = rateVal; + } + + const seekResult = this.seek(soundIds[i]); + const seek = typeof seekResult === "number" ? seekResult : 0; + const duration = + (this._sprite[sound._sprite][0] + this._sprite[sound._sprite][1]) / + 1000 - + seek; + const timeout = (duration * 1000) / Math.abs(sound._rate); + + if (this._endTimers[soundIds[i]] || !sound._paused) { + this._clearTimer(soundIds[i]); + this._endTimers[soundIds[i]] = setTimeout( + this._ended.bind(this, sound), + timeout, + ); + } + + this._emit("rate", sound._id); + } + } + } else { + if (typeof id !== "undefined") { + sound = this._soundById(id); + return sound ? sound._rate : this._rate; + } + return this._rate; + } + + return this; + } + + seek(): number; + seek(seek: number): Howl; + seek(seek: number, id: number): Howl; + seek(seek?: number): number | Howl { + const args = arguments; + let seekVal: number | undefined; + let id: number | undefined; + + if (args.length === 0) { + if (this._sounds.length) { + id = this._sounds[0]._id; + } + } else if (args.length === 1) { + const ids = this._getSoundIds(); + const index = ids.indexOf(args[0] as number); + if (index >= 0) { + id = parseInt(String(args[0]), 10); + } else if (this._sounds.length) { + id = this._sounds[0]._id; + seekVal = parseFloat(String(args[0])); + } + } else if (args.length === 2) { + seekVal = parseFloat(String(args[0])); + id = parseInt(String(args[1]), 10); + } + + if (typeof id === "undefined") { + return 0; + } + + if ( + typeof seekVal === "number" && + (this._state !== "loaded" || this._playLock) + ) { + this._queue.push({ + event: "seek", + action: () => { + if (args.length >= 1 && typeof args[0] === "number") { + if (args.length >= 2 && typeof args[1] === "number") { + this.seek(args[0], args[1]); + } else { + this.seek(args[0]); + } + } + }, + }); + + return this; + } + + const sound = this._soundById(id); + + if (sound) { + if (typeof seekVal === "number" && seekVal >= 0) { + const playing = this.playing(id); + if (playing) { + this.pause(id, true); + } + + sound._seek = seekVal; + sound._ended = false; + this._clearTimer(id); + + if ( + !this._webAudio && + sound._node && + isHTMLAudioElement(sound._node) && + !isNaN(sound._node.duration) + ) { + sound._node.currentTime = seekVal; + } + + 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) { + 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 if (sound._node && isHTMLAudioElement(sound._node)) { + return sound._node.currentTime; + } + return 0; + } + } + + return this; + } + + playing(id?: number): boolean { + if (typeof id === "number") { + const sound = this._soundById(id); + return sound ? !sound._paused : false; + } + + for (let i = 0; i < this._sounds.length; i++) { + if (!this._sounds[i]._paused) { + return true; + } + } + + return false; + } + + duration(id?: number): number { + let duration = this._duration; + + if (typeof id !== "undefined") { + const sound = this._soundById(id); + if (sound) { + duration = this._sprite[sound._sprite][1] / 1000; + } + } + + return duration; + } + + state(): string { + return this._state; + } + + unload(): null { + // Execute plugin hooks before destruction + globalPluginManager.executeHowlDestroy(this); + + const sounds = this._sounds; + for (let i = 0; i < sounds.length; i++) { + if (!sounds[i]._paused) { + this.stop(sounds[i]._id); + } + + const node = sounds[i]._node; + if (!this._webAudio && node && isHTMLAudioElement(node)) { + this._clearSound(node); + + const errorFn = sounds[i]._errorFn; + if (errorFn) { + node.removeEventListener("error", errorFn, false); + } + const loadFn = sounds[i]._loadFn; + if (loadFn) { + node.removeEventListener( + Howler._canPlayEvent as string, + loadFn, + false, + ); + } + const endFn = sounds[i]._endFn; + if (endFn) { + node.removeEventListener("ended", endFn, false); + } + + Howler._releaseHtml5Audio(node); + } + + sounds[i]._node = null; + + this._clearTimer(sounds[i]._id); + } + + const index = Howler._howls.indexOf(this); + if (index >= 0) { + Howler._howls.splice(index, 1); + } + + let remCache = true; + for (let i = 0; i < Howler._howls.length; i++) { + if ( + Howler._howls[i]._src === this._src || + (this._src as string).indexOf(Howler._howls[i]._src as string) >= 0 + ) { + remCache = false; + break; + } + } + + if (cache && remCache) { + delete cache[this._src as string]; + } + + Howler.noAudio = false; + + this._state = "unloaded"; + this._sounds = []; + + return null; + } + + on( + event: string, + fn: (...args: unknown[]) => void, + id?: number, + once?: boolean, + ): Howl { + const events = (this as unknown as Record)[ + `_on${event}` + ]; + + if (typeof fn === "function") { + events.push(once ? { id, fn, once } : { id, fn }); + } + + return this; + } + + off(event: string, fn?: (...args: unknown[]) => void, id?: number): Howl { + const events = (this as unknown as Record)[ + `_on${event}` + ]; + let i = 0; + + if (typeof fn === "number") { + id = fn; + fn = undefined; + } + + if (fn || id) { + for (i = 0; i < events.length; i++) { + const isId = id === events[i].id; + if ((fn === events[i].fn && isId) || (!fn && isId)) { + events.splice(i, 1); + break; + } + } + } else if (event) { + (this as unknown as Record)[`_on${event}`] = []; + } else { + const keys = Object.keys(this); + for (i = 0; i < keys.length; i++) { + if ( + keys[i].indexOf("_on") === 0 && + Array.isArray( + (this as unknown as Record)[keys[i]], + ) + ) { + (this as unknown as Record)[keys[i]] = []; + } + } + } + + return this; + } + + once(event: string, fn: (...args: unknown[]) => void, id?: number): Howl { + this.on(event, fn, id, true); + + return this; + } + + _emit(event: string, id?: number | null, msg?: string): Howl { + const events = (this as unknown as Record)[ + `_on${event}` + ]; + + for (let i = events.length - 1; i >= 0; i--) { + if (!events[i].id || events[i].id === id || event === "load") { + const fn = events[i].fn; + setTimeout(() => { + fn(id, msg); + }, 0); + + if (events[i].once) { + this.off(event, events[i].fn, events[i].id); + } + } + } + + this._loadQueue(event); + + return this; + } + + _loadQueue(event?: string): Howl { + if (this._queue.length > 0) { + const task = this._queue[0]; + + if (task.event === event) { + this._queue.shift(); + this._loadQueue(); + } + + if (!event) { + task.action(); + } + } + + return this; + } + + _ended(sound: Sound): Howl { + const sprite = sound._sprite; + + if ( + !this._webAudio && + sound._node && + isHTMLAudioElement(sound._node) && + !sound._node.paused && + !sound._node.ended && + sound._node.currentTime < sound._stop! + ) { + setTimeout(this._ended.bind(this, sound), 100); + return this; + } + + const 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) { + this._emit("play", sound._id); + sound._seek = sound._start || 0; + sound._rateSeek = 0; + sound._playStart = Howler.ctx!.currentTime; + + const timeout = + ((sound._stop! - (sound._start || 0)) * 1000) / 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._autoSuspend(); + } + + if (!this._webAudio && !loop) { + this.stop(sound._id, true); + } + + return this; + } + + _clearTimer(id: number): Howl { + if (this._endTimers[id]) { + if (typeof this._endTimers[id] !== "function") { + clearTimeout(this._endTimers[id]); + } else { + const sound = this._soundById(id); + if (sound && sound._node) { + sound._node.removeEventListener("ended", this._endTimers[id], false); + } + } + + delete this._endTimers[id]; + } + + return this; + } + + _soundById(id: number): Sound | null { + for (let i = 0; i < this._sounds.length; i++) { + if (id === this._sounds[i]._id) { + return this._sounds[i]; + } + } + + return null; + } + + _inactiveSound(): Sound { + this._drain(); + + for (let i = 0; i < this._sounds.length; i++) { + if (this._sounds[i]._ended) { + return this._sounds[i].reset(); + } + } + + return new Sound(this); + } + + _drain(): void { + const limit = this._pool; + let cnt = 0; + + if (this._sounds.length < limit) { + return; + } + + for (let i = 0; i < this._sounds.length; i++) { + if (this._sounds[i]._ended) { + cnt++; + } + } + + for (let i = this._sounds.length - 1; i >= 0; i--) { + if (cnt <= limit) { + return; + } + + if (this._sounds[i]._ended) { + const node = this._sounds[i]._node; + if (this._webAudio && node && isGainNode(node)) { + node.disconnect(0); + } + + this._sounds.splice(i, 1); + cnt--; + } + } + } + + _getSoundIds(id?: number): number[] { + if (typeof id === "undefined") { + const ids: number[] = []; + for (let i = 0; i < this._sounds.length; i++) { + ids.push(this._sounds[i]._id); + } + + return ids; + } else { + return [id]; + } + } + + _refreshBuffer(sound: Sound): Howl { + if (!sound._node || !isGainNode(sound._node) || !Howler.ctx) { + return this; + } + + sound._node.bufferSource = + Howler.ctx.createBufferSource() as AudioBufferSourceNodeWithLegacy; + const src = + typeof this._src === "string" + ? this._src + : Array.isArray(this._src) && this._src.length > 0 + ? this._src[0] + : ""; + sound._node.bufferSource.buffer = cache[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.ctx.currentTime, + ); + + return this; + } + + _cleanBuffer(node: any): Howl { + const isIOS = isAppleVendor(Howler._navigator); + + if (!node.bufferSource) { + return this; + } + + 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 this; + } + + _clearSound(node: HTMLAudioElementWithUnlocked): void { + node.src = + "data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA"; + } +} +export { Howl }; diff --git a/src/howler-global.ts b/src/howler-global.ts new file mode 100644 index 00000000..5b72e2e7 --- /dev/null +++ b/src/howler-global.ts @@ -0,0 +1,506 @@ +/*! + * howler.js v2.2.4 + * howlerjs.com + * + * (c) 2013-2020, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */ + +// Import helper functions directly for better tree-shaking +import { setupAudioContext } from "./helpers/audio-context"; +import type { Howl } from "./howl"; +// Import plugin manager +import { globalPluginManager, type HowlerPlugin } from "./plugins/plugin"; +import { + type HTMLAudioElementWithUnlocked, + isHTMLAudioElement, + type NavigatorWithCocoonJS, + type WindowWithAudio, +} from "./types"; + +export class HowlerGlobal { + _counter: number = 1000; + _html5AudioPool: HTMLAudioElement[] = []; + html5PoolSize: number = 10; + _codecs: Record = {}; + _howls: Howl[] = []; + _muted: boolean = false; + _volume: number = 1; + _canPlayEvent: string = "canplaythrough"; + _navigator: NavigatorWithCocoonJS | null = null; + masterGain: GainNode | null = null; + noAudio: boolean = false; + usingWebAudio: boolean = true; + autoSuspend: boolean = true; + ctx: AudioContext | null = null; + autoUnlock: boolean = true; + state: string = "suspended"; + _audioUnlocked: boolean = false; + _scratchBuffer: AudioBuffer | null = null; + _suspendTimer: ReturnType | null = null; + _resumeAfterSuspend?: boolean; + _mobileUnloaded?: boolean; + + constructor() { + // Initialize all properties (explicit initialization ensures correct values) + this._counter = 1000; + this._html5AudioPool = []; + this.html5PoolSize = 10; + this._codecs = {}; + this._howls = []; + this._muted = false; + this._volume = 1; + this._canPlayEvent = "canplaythrough"; + this._navigator = + typeof window !== "undefined" && window.navigator + ? window.navigator + : null; + this.masterGain = null; + this.noAudio = false; + this.usingWebAudio = true; + this.autoSuspend = true; + this.ctx = null; + this.autoUnlock = true; + + // Setup Howler (codecs, audio context, etc.) + this._setup(); + + // Register the Howler instance with the plugin manager + // This triggers onHowlerInit hooks for any plugins already registered + globalPluginManager.setHowlerInstance(this); + } + + volume(vol?: number): number | HowlerGlobal { + if (vol !== undefined) { + vol = parseFloat(String(vol)); + + if (!this.ctx) { + setupAudioContext(); + } + + if (typeof vol === "number" && vol >= 0 && vol <= 1) { + this._volume = vol; + + if (this._muted) { + return this; + } + + if (this.usingWebAudio && this.ctx) { + this.masterGain!.gain.setValueAtTime(vol, this.ctx.currentTime); + } + + for (let i = 0; i < this._howls.length; i++) { + if (!this._howls[i]._webAudio) { + const ids = this._howls[i]._getSoundIds(); + for (let j = 0; j < ids.length; j++) { + const sound = this._howls[i]._soundById(ids[j]); + if (sound && sound._node && isHTMLAudioElement(sound._node)) { + sound._node.volume = sound._volume * vol; + } + } + } + } + + return this; + } + } + + return this._volume; + } + + mute(muted: boolean): HowlerGlobal { + if (!this.ctx) { + setupAudioContext(); + } + + this._muted = muted; + + if (this.usingWebAudio && this.ctx) { + this.masterGain!.gain.setValueAtTime( + muted ? 0 : this._volume, + this.ctx.currentTime, + ); + } + + for (let i = 0; i < this._howls.length; i++) { + if (!this._howls[i]._webAudio) { + const ids = this._howls[i]._getSoundIds(); + for (let j = 0; j < ids.length; j++) { + const sound = this._howls[i]._soundById(ids[j]); + if (sound && sound._node && isHTMLAudioElement(sound._node)) { + sound._node.muted = muted ? true : sound._muted; + } + } + } + } + + return this; + } + + stop(): HowlerGlobal { + for (let i = 0; i < this._howls.length; i++) { + this._howls[i].stop(); + } + + return this; + } + + unload(): HowlerGlobal { + for (let 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; + setupAudioContext(); + } + + return this; + } + + codecs(ext: string): boolean { + return this._codecs[ext.replace(/^x-/, "")]; + } + + /** + * Register a plugin with Howler + * @param plugin - The plugin to register + * @returns this for chaining + * @throws Error if a plugin with the same name is already registered + */ + addPlugin(plugin: HowlerPlugin): HowlerGlobal { + globalPluginManager.register(plugin); + return this; + } + + /** + * Unregister a plugin from Howler + * @param plugin - The plugin instance to unregister + * @returns this for chaining + * @throws Error if the plugin is not registered + */ + removePlugin(plugin: HowlerPlugin): HowlerGlobal { + globalPluginManager.unregister(plugin.name); + return this; + } + + /** + * Check if a plugin is registered + * @param pluginName - The name of the plugin to check + * @returns true if the plugin is registered, false otherwise + */ + hasPlugin(pluginName: string): boolean { + return globalPluginManager.isRegistered(pluginName); + } + + _setup(): HowlerGlobal { + this.state = this.ctx ? this.ctx.state || "suspended" : "suspended"; + this._autoSuspend(); + + if (!this.usingWebAudio) { + if (typeof window.Audio !== "undefined") { + try { + const test = new window.Audio(); + if (typeof test.oncanplaythrough === "undefined") { + this._canPlayEvent = "canplay"; + } + } catch (e) { + this.noAudio = true; + } + } else { + this.noAudio = true; + } + } + + try { + const test = new window.Audio(); + if (test.muted) { + this.noAudio = true; + } + } catch (e) {} + + if (!this.noAudio) { + this._setupCodecs(); + } + + return this; + } + + _setupCodecs(): HowlerGlobal { + let audioTest: HTMLAudioElement | null = null; + + try { + audioTest = + typeof window.Audio !== "undefined" ? new window.Audio() : null; + } catch (err) { + return this; + } + + if (!audioTest || typeof audioTest.canPlayType !== "function") { + return this; + } + + const mpegTest = audioTest.canPlayType("audio/mpeg;").replace(/^no$/, ""); + + this._codecs = { + mp3: !!( + 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: !!audioTest + .canPlayType('audio/webm; codecs="vorbis"') + .replace(/^no$/, ""), + webm: !!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(): void { + if (this._audioUnlocked || !this.ctx) { + return; + } + + this._audioUnlocked = false; + 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 { + const audioNode = new ( + window as WindowWithAudio + ).Audio() as HTMLAudioElementWithUnlocked; + audioNode._unlocked = true; + this._releaseHtml5Audio(audioNode); + } catch (e) { + this.noAudio = true; + break; + } + } + + for (let i = 0; i < this._howls.length; i++) { + if (!this._howls[i]._webAudio) { + const ids = this._howls[i]._getSoundIds(); + for (let j = 0; j < ids.length; j++) { + const sound = this._howls[i]._soundById(ids[j]); + if ( + sound && + sound._node && + isHTMLAudioElement(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); + source.start(0); + + if (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 (let i = 0; i < this._howls.length; i++) { + this._howls[i]._emit("unlock"); + } + }; + }; + + document.addEventListener("touchstart", unlock, true); + document.addEventListener("touchend", unlock, true); + document.addEventListener("click", unlock, true); + document.addEventListener("keydown", unlock, true); + } + + _obtainHtml5Audio(): HTMLAudioElementWithUnlocked { + if (this._html5AudioPool.length) { + return this._html5AudioPool.pop()!; + } + + const testPlay = new (window as WindowWithAudio).Audio().play(); + if (testPlay && typeof Promise !== "undefined") { + if (testPlay instanceof Promise) { + testPlay.catch(() => { + console.warn( + "HTML5 Audio pool exhausted, returning potentially locked audio object.", + ); + }); + } else if ( + typeof testPlay === "object" && + testPlay !== null && + "then" in testPlay && + typeof (testPlay as { then?: unknown }).then === "function" + ) { + // Handle thenable objects + (testPlay as { catch: (onRejected: () => void) => void }).catch(() => { + console.warn( + "HTML5 Audio pool exhausted, returning potentially locked audio object.", + ); + }); + } + } + + return new ( + window as WindowWithAudio + ).Audio() as HTMLAudioElementWithUnlocked; + } + + _releaseHtml5Audio(audio: HTMLAudioElementWithUnlocked): HowlerGlobal { + if (audio._unlocked) { + this._html5AudioPool.push(audio); + } + + return this; + } + + _autoSuspend(): void { + if ( + !this.autoSuspend || + !this.ctx || + typeof this.ctx.suspend === "undefined" || + !this.usingWebAudio + ) { + return; + } + + for (let i = 0; i < this._howls.length; i++) { + if (this._howls[i]._webAudio) { + for (let j = 0; j < this._howls[i]._sounds.length; j++) { + if (!this._howls[i]._sounds[j]._paused) { + return; + } + } + } + } + + 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); + }, 30000); + } + + _autoResume(): void { + 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 (let 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; + } + } +} diff --git a/src/howler.core.ts b/src/howler.core.ts index 79c2bdd0..d245e9c2 100644 --- a/src/howler.core.ts +++ b/src/howler.core.ts @@ -7,2304 +7,26 @@ * * MIT License */ -// Import shared types -// Import helper functions -import { - isAppleVendor, - isIE, - isOldOpera, - isOldSafari, - loadBuffer, - setupAudioContext, -} from "./helpers"; -// Import plugin manager -import { globalPluginManager, type HowlerPlugin } from "./plugins"; -import { - type AudioBufferSourceNodeWithLegacy, - cache, - type EventListener, - type GainNodeWithBufferSource, - type HowlOptions, - type HTMLAudioElementWithUnlocked, - isGainNode, - isHTMLAudioElement, - type NavigatorWithCocoonJS, - type QueueItem, - type WindowWithAudio, -} from "./types"; - -export class HowlerGlobal { - _counter: number = 1000; - _html5AudioPool: HTMLAudioElement[] = []; - html5PoolSize: number = 10; - _codecs: Record = {}; - _howls: Howl[] = []; - _muted: boolean = false; - _volume: number = 1; - _canPlayEvent: string = "canplaythrough"; - _navigator: NavigatorWithCocoonJS | null = null; - masterGain: GainNode | null = null; - noAudio: boolean = false; - usingWebAudio: boolean = true; - autoSuspend: boolean = true; - ctx: AudioContext | null = null; - autoUnlock: boolean = true; - state: string = "suspended"; - _audioUnlocked: boolean = false; - _scratchBuffer: AudioBuffer | null = null; - _suspendTimer: ReturnType | null = null; - _resumeAfterSuspend?: boolean; - _mobileUnloaded?: boolean; - - constructor() { - // Initialize all properties (explicit initialization ensures correct values) - this._counter = 1000; - this._html5AudioPool = []; - this.html5PoolSize = 10; - this._codecs = {}; - this._howls = []; - this._muted = false; - this._volume = 1; - this._canPlayEvent = "canplaythrough"; - this._navigator = - typeof window !== "undefined" && window.navigator - ? window.navigator - : null; - this.masterGain = null; - this.noAudio = false; - this.usingWebAudio = true; - this.autoSuspend = true; - this.ctx = null; - this.autoUnlock = true; - - // Setup Howler (codecs, audio context, etc.) - this._setup(); - - // Register the Howler instance with the plugin manager - // This triggers onHowlerInit hooks for any plugins already registered - globalPluginManager.setHowlerInstance(this); - } - - volume(vol?: number): number | HowlerGlobal { - if (vol !== undefined) { - vol = parseFloat(String(vol)); - - if (!this.ctx) { - setupAudioContext(); - } - - if (typeof vol === "number" && vol >= 0 && vol <= 1) { - this._volume = vol; - - if (this._muted) { - return this; - } - - if (this.usingWebAudio) { - this.masterGain!.gain.setValueAtTime(vol, Howler.ctx!.currentTime); - } - - for (let i = 0; i < this._howls.length; i++) { - if (!this._howls[i]._webAudio) { - const ids = this._howls[i]._getSoundIds(); - for (let j = 0; j < ids.length; j++) { - const sound = this._howls[i]._soundById(ids[j]); - if (sound && sound._node && isHTMLAudioElement(sound._node)) { - sound._node.volume = sound._volume * vol; - } - } - } - } - - return this; - } - } - - return this._volume; - } - - mute(muted: boolean): HowlerGlobal { - if (!this.ctx) { - setupAudioContext(); - } - - this._muted = muted; - - if (this.usingWebAudio) { - this.masterGain!.gain.setValueAtTime( - muted ? 0 : this._volume, - Howler.ctx!.currentTime, - ); - } - - for (let i = 0; i < this._howls.length; i++) { - if (!this._howls[i]._webAudio) { - const ids = this._howls[i]._getSoundIds(); - for (let j = 0; j < ids.length; j++) { - const sound = this._howls[i]._soundById(ids[j]); - if (sound && sound._node && isHTMLAudioElement(sound._node)) { - sound._node.muted = muted ? true : sound._muted; - } - } - } - } - - return this; - } - - stop(): HowlerGlobal { - for (let i = 0; i < this._howls.length; i++) { - this._howls[i].stop(); - } - - return this; - } - - unload(): HowlerGlobal { - for (let 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; - setupAudioContext(); - } - - return this; - } - - codecs(ext: string): boolean { - return (this || Howler)._codecs[ext.replace(/^x-/, "")]; - } - - /** - * Register a plugin with Howler - * @param plugin - The plugin to register - * @returns this for chaining - * @throws Error if a plugin with the same name is already registered - */ - addPlugin(plugin: HowlerPlugin): HowlerGlobal { - globalPluginManager.register(plugin); - return this; - } - - /** - * Unregister a plugin from Howler - * @param plugin - The plugin instance to unregister - * @returns this for chaining - * @throws Error if the plugin is not registered - */ - removePlugin(plugin: HowlerPlugin): HowlerGlobal { - globalPluginManager.unregister(plugin.name); - return this; - } - - /** - * Check if a plugin is registered - * @param pluginName - The name of the plugin to check - * @returns true if the plugin is registered, false otherwise - */ - hasPlugin(pluginName: string): boolean { - return globalPluginManager.isRegistered(pluginName); - } - - _setup(): HowlerGlobal { - this.state = this.ctx ? this.ctx.state || "suspended" : "suspended"; - this._autoSuspend(); - - if (!this.usingWebAudio) { - if (typeof window.Audio !== "undefined") { - try { - const test = new window.Audio(); - if (typeof test.oncanplaythrough === "undefined") { - this._canPlayEvent = "canplay"; - } - } catch (e) { - this.noAudio = true; - } - } else { - this.noAudio = true; - } - } - - try { - const test = new window.Audio(); - if (test.muted) { - this.noAudio = true; - } - } catch (e) {} - - if (!this.noAudio) { - this._setupCodecs(); - } - - return this; - } - - _setupCodecs(): HowlerGlobal { - let audioTest: HTMLAudioElement | null = null; - - try { - audioTest = - typeof window.Audio !== "undefined" ? new window.Audio() : null; - } catch (err) { - return this; - } - - if (!audioTest || typeof audioTest.canPlayType !== "function") { - return this; - } - - const mpegTest = audioTest.canPlayType("audio/mpeg;").replace(/^no$/, ""); - const oldOpera = isOldOpera(this._navigator); - const oldSafari = isOldSafari(this._navigator); - - this._codecs = { - mp3: !!( - !oldOpera && - (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: !!( - !oldSafari && - audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, "") - ), - webm: !!( - !oldSafari && - 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(): void { - if (this._audioUnlocked || !this.ctx) { - return; - } - - this._audioUnlocked = false; - 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 { - const audioNode = new ( - window as WindowWithAudio - ).Audio() as HTMLAudioElementWithUnlocked; - audioNode._unlocked = true; - this._releaseHtml5Audio(audioNode); - } catch (e) { - this.noAudio = true; - break; - } - } - - for (let i = 0; i < this._howls.length; i++) { - if (!this._howls[i]._webAudio) { - const ids = this._howls[i]._getSoundIds(); - for (let j = 0; j < ids.length; j++) { - const sound = this._howls[i]._soundById(ids[j]); - if ( - sound && - sound._node && - isHTMLAudioElement(sound._node) && - !sound._node._unlocked - ) { - sound._node._unlocked = true; - sound._node.load(); - } - } - } - } +import { Howl } from "./howl"; +// Import classes from their own files +import { HowlerGlobal } from "./howler-global"; +import { Sound } from "./sound"; - this._autoResume(); - - const source = this.ctx!.createBufferSource(); - source.buffer = this._scratchBuffer; - source.connect(this.ctx!.destination); - - if (typeof source.start === "undefined") { - (source as AudioBufferSourceNodeWithLegacy).noteOn?.(0); - } else { - source.start(0); - } - - if (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 (let i = 0; i < this._howls.length; i++) { - this._howls[i]._emit("unlock"); - } - }; - }; - - document.addEventListener("touchstart", unlock, true); - document.addEventListener("touchend", unlock, true); - document.addEventListener("click", unlock, true); - document.addEventListener("keydown", unlock, true); - } - - _obtainHtml5Audio(): HTMLAudioElementWithUnlocked { - if (this._html5AudioPool.length) { - return this._html5AudioPool.pop()!; - } - - const testPlay = new (window as WindowWithAudio).Audio().play(); - if (testPlay && typeof Promise !== "undefined") { - if (testPlay instanceof Promise) { - testPlay.catch(() => { - console.warn( - "HTML5 Audio pool exhausted, returning potentially locked audio object.", - ); - }); - } else if ( - typeof testPlay === "object" && - testPlay !== null && - "then" in testPlay && - typeof (testPlay as { then?: unknown }).then === "function" - ) { - // Handle thenable objects - (testPlay as { catch: (onRejected: () => void) => void }).catch(() => { - console.warn( - "HTML5 Audio pool exhausted, returning potentially locked audio object.", - ); - }); - } - } - - return new ( - window as WindowWithAudio - ).Audio() as HTMLAudioElementWithUnlocked; - } - - _releaseHtml5Audio(audio: HTMLAudioElementWithUnlocked): HowlerGlobal { - if (audio._unlocked) { - this._html5AudioPool.push(audio); - } - - return this; - } - - _autoSuspend(): void { - if ( - !this.autoSuspend || - !this.ctx || - typeof this.ctx.suspend === "undefined" || - !Howler.usingWebAudio - ) { - return; - } - - for (let i = 0; i < this._howls.length; i++) { - if (this._howls[i]._webAudio) { - for (let j = 0; j < this._howls[i]._sounds.length; j++) { - if (!this._howls[i]._sounds[j]._paused) { - return; - } - } - } - } - - 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); - }, 30000); - } - - _autoResume(): void { - if ( - !this.ctx || - typeof this.ctx.resume === "undefined" || - !Howler.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 (let 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; - } - } -} - -// Setup the global audio controller +// Setup the global audio controller singleton const Howler = new HowlerGlobal(); -// Type guards for Sound._node - -class Sound { - _parent: Howl; - _muted: boolean = false; - _loop: boolean = false; - _volume: number = 1; - _rate: number = 1; - _seek: number = 0; - _paused: boolean = true; - _ended: boolean = true; - _sprite: string = "__default"; - _id: number = 0; - _node: HTMLAudioElementWithUnlocked | GainNodeWithBufferSource | null = null; - _playStart: number = 0; - _rateSeek: number = 0; - _errorFn?: (event: Event) => void; - _loadFn?: (event: Event) => void; - _endFn?: (event: Event) => void; - _start?: number; - _stop?: number; - _panner?: PannerNode | StereoPannerNode; - _fadeTo?: number; - _interval?: ReturnType; - - constructor(howl: Howl) { - this._parent = howl; - this.init(); - } - - init(): Sound { - const parent = this._parent; - - this._muted = parent._muted; - this._loop = parent._loop; - this._volume = parent._volume; - this._rate = parent._rate; - this._seek = 0; - this._paused = true; - this._ended = true; - this._sprite = "__default"; - - this._id = ++Howler._counter; - - parent._sounds.push(this); - - this.create(); - - // Execute plugin hooks - globalPluginManager.executeSoundCreate(this, parent); - - return this; - } - - create(): Sound { - const parent = this._parent; - const volume = - Howler._muted || this._muted || parent._muted ? 0 : this._volume; - - this._errorFn = this._errorListener.bind(this); - this._loadFn = this._loadListener.bind(this); - this._endFn = this._endListener.bind(this); - - if (parent._webAudio && Howler.ctx) { - const gainNode = - typeof Howler.ctx.createGain === "undefined" - ? ( - Howler.ctx as { createGainNode?: () => GainNode } - ).createGainNode?.() - : Howler.ctx.createGain(); - if (gainNode) { - this._node = gainNode as GainNodeWithBufferSource; - this._node.gain.setValueAtTime(volume, Howler.ctx.currentTime); - (this._node as { paused?: boolean }).paused = true; - this._node.connect(Howler.masterGain!); - } - } else if (!Howler.noAudio) { - this._node = Howler._obtainHtml5Audio(); - - this._errorFn = this._errorListener.bind(this); - this._node.addEventListener("error", this._errorFn, false); - - this._loadFn = this._loadListener.bind(this); - this._node.addEventListener(Howler._canPlayEvent, this._loadFn, false); - - this._endFn = this._endListener.bind(this); - this._node.addEventListener("ended", this._endFn, false); - - const src = - typeof parent._src === "string" - ? parent._src - : Array.isArray(parent._src) && parent._src.length > 0 - ? parent._src[0] - : ""; - this._node.src = src; - const preloadValue = - parent._preload === true - ? "auto" - : parent._preload === false - ? "none" - : parent._preload === "metadata" - ? "metadata" - : "auto"; - this._node.preload = preloadValue; - const volumeOrHowler = Howler.volume(); - if (typeof volumeOrHowler === "number") { - this._node.volume = volume * volumeOrHowler; - } - - this._node.load(); - } - - return this; - } - - reset(): Sound { - const 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._counter; - - return this; - } - - _errorListener(): void { - if (this._node && isHTMLAudioElement(this._node)) { - const errorCode = this._node.error ? this._node.error.code : 0; - this._parent._emit("loaderror", this._id, String(errorCode)); - if (this._errorFn) { - this._node.removeEventListener("error", this._errorFn, false); - } - } - } - - _loadListener(): void { - if (!this._node || !isHTMLAudioElement(this._node)) { - return; - } - - 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 * 1000] }; - } - - if (parent._state !== "loaded") { - parent._state = "loaded"; - parent._emit("load"); - parent._loadQueue(); - - // Execute plugin hooks - globalPluginManager.executeHowlLoad(parent); - } - - if (this._loadFn) { - this._node.removeEventListener(Howler._canPlayEvent, this._loadFn, false); - } - } - - _endListener(): void { - const parent = this._parent; - - if ( - parent._duration === Infinity && - this._node && - isHTMLAudioElement(this._node) - ) { - parent._duration = Math.ceil(this._node.duration * 10) / 10; - - if (parent._sprite.__default[1] === Infinity) { - parent._sprite.__default[1] = parent._duration * 1000; - } - - parent._ended(this); - } - - if (this._endFn && this._node) { - this._node.removeEventListener("ended", this._endFn, false); - } - } -} - -class Howl { - _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: Record = {}; - _src: string | string[] = []; - _volume: number = 1; - _xhr: { - method: string; - headers?: Record; - withCredentials: boolean; - } = { method: "GET", withCredentials: false }; - _duration: number = 0; - _state: string = "unloaded"; - _sounds: Sound[] = []; - _endTimers: Record> = {}; - _queue: QueueItem[] = []; - _playLock: boolean = false; - _webAudio: boolean = false; - _onend: EventListener[] = []; - _onfade: EventListener[] = []; - _onload: EventListener[] = []; - _onloaderror: EventListener[] = []; - _onplayerror: EventListener[] = []; - _onpause: EventListener[] = []; - _onplay: EventListener[] = []; - _onstop: EventListener[] = []; - _onmute: EventListener[] = []; - _onvolume: EventListener[] = []; - _onrate: EventListener[] = []; - _onseek: EventListener[] = []; - _onunlock: EventListener[] = []; - _onresume: EventListener[] = []; - - constructor(o: HowlOptions) { - if (!o.src || o.src.length === 0) { - console.error( - "An array of source files must be passed with any new Howl.", - ); - return; - } - - this.init(o); - } - - init(o: HowlOptions): Howl { - if (!Howler.ctx) { - setupAudioContext(); - } - - this._autoplay = o.autoplay || false; - this._format = 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, - }; - - this._duration = 0; - this._state = "unloaded"; - this._sounds = []; - this._endTimers = {}; - this._queue = []; - this._playLock = 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: (...args: unknown[]) => { - if ( - o.onloaderror && - typeof args[0] === "number" && - typeof args[1] === "string" - ) { - o.onloaderror(args[0], args[1]); - } - }, - }, - ] - : []; - this._onplayerror = o.onplayerror - ? [ - { - fn: (...args: unknown[]) => { - if ( - o.onplayerror && - typeof args[0] === "number" && - typeof args[1] === "string" - ) { - o.onplayerror(args[0], args[1]); - } - }, - }, - ] - : []; - 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.usingWebAudio && !this._html5; - - if (typeof Howler.ctx !== "undefined" && Howler.ctx && Howler.autoUnlock) { - Howler._unlockAudio(); - } - - Howler._howls.push(this); - - // Execute plugin hooks - globalPluginManager.executeHowlCreate(this, o); - - if (this._autoplay) { - this._queue.push({ - event: "play", - action: () => { - this.play(); - }, - }); - } - - if (this._preload === true || this._preload === "metadata") { - this.load(); - } - - return this; - } - - load(): Howl { - let url: string | null = null; - - if (Howler.noAudio) { - this._emit("loaderror", null, "No audio support."); - return this; - } - - if (typeof this._src === "string") { - this._src = [this._src]; - } - - for (let i = 0; i < (this._src as string[]).length; i++) { - let ext: string | null; - const str = (this._src as string[])[i]; - - if (this._format && this._format[i]) { - ext = this._format[i]; - } else { - if (typeof str !== "string") { - this._emit( - "loaderror", - null, - "Non-string found in selected audio sources - ignoring.", - ); - continue; - } - - let extMatch = /^data:audio\/([^;,]+);/i.exec(str); - if (!extMatch) { - extMatch = /\.([^.]+)$/.exec(str.split("?", 1)[0]); - } - - ext = extMatch ? extMatch[1].toLowerCase() : null; - } - - if (!ext) { - console.warn( - 'No file extension was found. Consider using the "format" property or specify an extension.', - ); - } - - if (ext && Howler.codecs(ext)) { - url = (this._src as string[])[i]; - break; - } - } - - if (!url) { - this._emit( - "loaderror", - null, - "No codec support for selected audio sources.", - ); - return this; - } - - this._src = url; - this._state = "loading"; - - if ( - typeof window !== "undefined" && - window.location.protocol === "https:" && - url.slice(0, 5) === "http:" - ) { - this._html5 = true; - this._webAudio = false; - } - - new Sound(this); - - if (this._webAudio) { - loadBuffer(this); - } - - return this; - } - - play(sprite?: string | number, internal?: boolean): number | null { - let id: number | null = null; - - if (typeof sprite === "number") { - id = sprite; - sprite = undefined; - } else if ( - typeof sprite === "string" && - this._state === "loaded" && - !this._sprite[sprite] - ) { - return null; - } else if (typeof sprite === "undefined") { - sprite = "__default"; - - if (!this._playLock) { - let num = 0; - for (let 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 = undefined; - } 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 || "__default"; - sound._ended = false; - - const 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._autoResume(); - } - - const seek = Math.max( - 0, - sound._seek > 0 ? sound._seek : this._sprite[sprite!][0] / 1000, - ); - const duration = Math.max( - 0, - (this._sprite[sprite!][0] + this._sprite[sprite!][1]) / 1000 - seek, - ); - const timeout = (duration * 1000) / Math.abs(sound._rate); - const start = this._sprite[sprite!][0] / 1000; - const stop = (this._sprite[sprite!][0] + this._sprite[sprite!][1]) / 1000; - 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 sound._id; - } - - const node = sound._node; - - if (this._webAudio && node && isGainNode(node)) { - const playWebAudio = () => { - this._playLock = false; - setParams(); - this._refreshBuffer(sound); - - const vol = sound._muted || this._muted ? 0 : sound._volume; - node.gain.setValueAtTime(vol, Howler.ctx!.currentTime); - sound._playStart = Howler.ctx!.currentTime; - - if (node.bufferSource) { - if (typeof node.bufferSource.start === "undefined") { - node.bufferSource.noteGrainOn?.( - 0, - seek, - sound._loop ? 86400 : duration, - ); - } else { - node.bufferSource.start(0, seek, sound._loop ? 86400 : 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.state === "running" && Howler.ctx!.state !== "interrupted") { - playWebAudio(); - } else { - this._playLock = true; - this.once("resume", playWebAudio); - this._clearTimer(sound._id); - } - } else if (node && isHTMLAudioElement(node)) { - const playHtml5 = () => { - node.currentTime = seek; - node.muted = sound._muted || this._muted || Howler._muted || node.muted; - const volume = Howler.volume(); - node.volume = sound._volume * (typeof volume === "number" ? volume : 1); - node.playbackRate = sound._rate; - - try { - const play = node.play(); - - if ( - play && - typeof Promise !== "undefined" && - (play instanceof Promise || - typeof (play as any).then === "function") - ) { - this._playLock = true; - - setParams(); - - (play as any) - .then(() => { - this._playLock = false; - if ("_unlocked" in node) { - (node as HTMLAudioElementWithUnlocked)._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 { - const endHandler = () => { - this._ended(sound); - node.removeEventListener("ended", endHandler, false); - }; - this._endTimers[sound._id] = setTimeout(endHandler, timeout); - node.addEventListener("ended", endHandler, false); - } - } catch (err: unknown) { - this._emit( - "playerror", - sound._id, - err instanceof Error ? err.message : String(err), - ); - } - }; - - if ( - node.src === - "data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA" - ) { - const src = - typeof this._src === "string" - ? this._src - : Array.isArray(this._src) && this._src.length > 0 - ? this._src[0] - : ""; - node.src = src; - node.load(); - } - - const loadedNoReadyState = - typeof (window as WindowWithAudio).ejecta !== "undefined" || - (!node.readyState && Howler._navigator && Howler._navigator.isCocoonJS); - if (node.readyState >= 3 || loadedNoReadyState) { - playHtml5(); - } else { - this._playLock = true; - this._state = "loading"; - - const listener = () => { - this._state = "loaded"; - playHtml5(); - node.removeEventListener(Howler._canPlayEvent, listener, false); - }; - node.addEventListener(Howler._canPlayEvent, listener, false); - - this._clearTimer(sound._id); - } - } - - return sound._id; - } - - pause(id?: number, internal?: boolean): Howl { - if (this._state !== "loaded" || this._playLock) { - this._queue.push({ - event: "pause", - action: () => { - this.pause(id); - }, - }); - - return this; - } - - const ids = this._getSoundIds(id); - - for (let i = 0; i < ids.length; i++) { - this._clearTimer(ids[i]); - - const sound = this._soundById(ids[i]); - - if (sound && !sound._paused) { - const seekResult = this.seek(ids[i]); - sound._seek = typeof seekResult === "number" ? seekResult : 0; - sound._rateSeek = 0; - sound._paused = true; - - this._stopFade(ids[i]); - - if (sound._node) { - if (this._webAudio && isGainNode(sound._node)) { - 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 ( - isHTMLAudioElement(sound._node) && - (!isNaN(sound._node.duration) || sound._node.duration === Infinity) - ) { - sound._node.pause(); - } - } - } - - if (!arguments[1]) { - this._emit("pause", sound ? sound._id : null); - } - } - - return this; - } - - stop(id?: number, internal?: boolean): Howl { - if (this._state !== "loaded" || this._playLock) { - this._queue.push({ - event: "stop", - action: () => { - this.stop(id); - }, - }); - - return this; - } - - const ids = this._getSoundIds(id); - - for (let i = 0; i < ids.length; i++) { - this._clearTimer(ids[i]); - - const 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 && isGainNode(sound._node)) { - 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 ( - isHTMLAudioElement(sound._node) && - (!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: boolean, id?: number): boolean | Howl { - 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; - } - } - - const ids = this._getSoundIds(id); - - for (let i = 0; i < ids.length; i++) { - const sound = this._soundById(ids[i]); - - if (sound) { - sound._muted = muted; - - if (sound._interval) { - this._stopFade(sound._id); - } - - if (this._webAudio && sound._node && isGainNode(sound._node)) { - sound._node.gain.setValueAtTime( - muted ? 0 : sound._volume, - Howler.ctx!.currentTime, - ); - } else if (sound._node && isHTMLAudioElement(sound._node)) { - sound._node.muted = Howler._muted ? true : muted; - } - - this._emit("mute", sound._id); - } - } - - return this; - } - - volume(): number; - volume(vol: number): Howl; - volume(vol: number, id: number): Howl; - volume(vol: number, id: number, internal: boolean): Howl; - volume(vol?: number): number | Howl { - const args = arguments; - let volume: number | undefined; - let id: number | undefined; - - if (args.length === 0) { - return this._volume; - } else if ( - args.length === 1 || - (args.length === 2 && typeof args[1] === "undefined") - ) { - const ids = this._getSoundIds(); - const index = ids.indexOf(args[0] as number); - if (index >= 0) { - id = parseInt(String(args[0]), 10); - } else { - volume = parseFloat(String(args[0])); - } - } else if (args.length >= 2) { - volume = parseFloat(String(args[0])); - id = parseInt(String(args[1]), 10); - } - - let sound: Sound | null = null; - if (typeof volume !== "undefined" && volume >= 0 && volume <= 1) { - if (this._state !== "loaded" || this._playLock) { - this._queue.push({ - event: "volume", - action: () => { - if (args.length >= 1 && typeof args[0] === "number") { - if (args.length >= 2 && typeof args[1] === "number") { - this.volume(args[0], args[1]); - } else { - this.volume(args[0]); - } - } - }, - }); - - return this; - } - - if (typeof id === "undefined") { - this._volume = volume; - } - - const soundIds = this._getSoundIds(id); - for (let i = 0; i < soundIds.length; i++) { - sound = this._soundById(soundIds[i]); - - if (sound) { - sound._volume = volume; - - if (!args[2]) { - this._stopFade(soundIds[i]); - } - - if ( - this._webAudio && - sound._node && - isGainNode(sound._node) && - !sound._muted - ) { - sound._node.gain.setValueAtTime(volume, Howler.ctx!.currentTime); - } else if ( - sound._node && - isHTMLAudioElement(sound._node) && - !sound._muted - ) { - const volumeMultiplierOrGlobal = Howler.volume(); - if (typeof volumeMultiplierOrGlobal === "number") { - sound._node.volume = volume * volumeMultiplierOrGlobal; - } - } - - this._emit("volume", sound._id); - } - } - } else { - sound = id ? this._soundById(id) : this._sounds[0]; - return sound ? sound._volume : 0; - } - - return this; - } - - fade(from: number, to: number, len: number, id?: number): Howl { - 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(String(from))), 1); - to = Math.min(Math.max(0, parseFloat(String(to))), 1); - len = parseFloat(String(len)); - - if (typeof id !== "undefined") { - this.volume(from, id); - } else { - this.volume(from); - } - - const ids = this._getSoundIds(id); - for (let i = 0; i < ids.length; i++) { - const sound = this._soundById(ids[i]); - - if (sound) { - if (!id) { - this._stopFade(ids[i]); - } - - if (this._webAudio && !sound._muted) { - const currentTime = Howler.ctx!.currentTime; - const end = currentTime + len / 1000; - sound._volume = from; - if (sound._node && isGainNode(sound._node)) { - 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: Sound, - from: number, - to: number, - len: number, - id: number, - isGroup: boolean, - ): void { - let vol = from; - const diff = to - from; - const steps = Math.abs(diff / 0.01); - const stepLen = Math.max(4, steps > 0 ? len / steps : len); - let lastTick = Date.now(); - - sound._fadeTo = to; - - sound._interval = setInterval(() => { - const 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 (sound._interval) { - clearInterval(sound._interval); - } - sound._interval = undefined; - sound._fadeTo = undefined; - this.volume(to, sound._id); - this._emit("fade", sound._id); - } - }, stepLen); - } - - _stopFade(id: number): Howl { - const sound = this._soundById(id); - - if (sound && sound._interval) { - if (this._webAudio && sound._node && isGainNode(sound._node)) { - sound._node.gain.cancelScheduledValues(Howler.ctx!.currentTime); - } - - if (sound._interval) { - clearInterval(sound._interval); - sound._interval = undefined; - } - this.volume(sound._fadeTo as number, id); - sound._fadeTo = undefined; - this._emit("fade", id); - } - - return this; - } - - loop(): boolean; - loop(loop: boolean): Howl; - loop(loop?: boolean): boolean | Howl { - const args = arguments; - let loopVal: boolean | undefined; - let id: number | undefined; - let sound: Sound | null = null; - - if (args.length === 0) { - return this._loop; - } else if (args.length === 1) { - if (typeof args[0] === "boolean") { - loopVal = args[0] as boolean; - this._loop = loopVal; - } else { - sound = this._soundById(parseInt(String(args[0]), 10)); - return sound ? sound._loop : false; - } - } else if (args.length === 2) { - loopVal = args[0] as boolean; - id = parseInt(String(args[1]), 10); - } - - const ids = this._getSoundIds(id); - for (let i = 0; i < ids.length; i++) { - sound = this._soundById(ids[i]); - - if (sound) { - sound._loop = loopVal as boolean; - if ( - this._webAudio && - sound._node && - isGainNode(sound._node) && - sound._node.bufferSource - ) { - sound._node.bufferSource.loop = loopVal; - if (loopVal) { - 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(): number; - rate(rate: number): Howl; - rate(rate: number, id: number): Howl; - rate(rate?: number): number | Howl { - const args = arguments; - let rateVal: number | undefined; - let id: number | undefined; - - if (args.length === 0) { - id = this._sounds[0]._id; - } else if (args.length === 1) { - const ids = this._getSoundIds(); - const index = ids.indexOf(args[0] as number); - if (index >= 0) { - id = parseInt(String(args[0]), 10); - } else { - rateVal = parseFloat(String(args[0])); - } - } else if (args.length === 2) { - rateVal = parseFloat(String(args[0])); - id = parseInt(String(args[1]), 10); - } - - let sound: Sound | null = null; - if (typeof rateVal === "number") { - if (this._state !== "loaded" || this._playLock) { - this._queue.push({ - event: "rate", - action: () => { - if (args.length >= 1 && typeof args[0] === "number") { - if (args.length >= 2 && typeof args[1] === "number") { - this.rate(args[0], args[1]); - } else { - this.rate(args[0]); - } - } - }, - }); - - return this; - } - - if (typeof id === "undefined") { - this._rate = rateVal; - } - - const soundIds = this._getSoundIds(id); - for (let i = 0; i < soundIds.length; i++) { - sound = this._soundById(soundIds[i]); - - if (sound) { - if (this.playing(soundIds[i])) { - const seekResult = this.seek(soundIds[i]); - sound._rateSeek = typeof seekResult === "number" ? seekResult : 0; - sound._playStart = this._webAudio - ? Howler.ctx!.currentTime - : sound._playStart; - } - sound._rate = rateVal; - - if ( - this._webAudio && - sound._node && - isGainNode(sound._node) && - sound._node.bufferSource - ) { - sound._node.bufferSource.playbackRate.setValueAtTime( - rateVal, - Howler.ctx!.currentTime, - ); - } else if (sound._node && isHTMLAudioElement(sound._node)) { - sound._node.playbackRate = rateVal; - } - - const seekResult = this.seek(soundIds[i]); - const seek = typeof seekResult === "number" ? seekResult : 0; - const duration = - (this._sprite[sound._sprite][0] + this._sprite[sound._sprite][1]) / - 1000 - - seek; - const timeout = (duration * 1000) / Math.abs(sound._rate); - - if (this._endTimers[soundIds[i]] || !sound._paused) { - this._clearTimer(soundIds[i]); - this._endTimers[soundIds[i]] = setTimeout( - this._ended.bind(this, sound), - timeout, - ); - } - - this._emit("rate", sound._id); - } - } - } else { - if (typeof id !== "undefined") { - sound = this._soundById(id); - return sound ? sound._rate : this._rate; - } - return this._rate; - } - - return this; - } - - seek(): number; - seek(seek: number): Howl; - seek(seek: number, id: number): Howl; - seek(seek?: number): number | Howl { - const args = arguments; - let seekVal: number | undefined; - let id: number | undefined; - - if (args.length === 0) { - if (this._sounds.length) { - id = this._sounds[0]._id; - } - } else if (args.length === 1) { - const ids = this._getSoundIds(); - const index = ids.indexOf(args[0] as number); - if (index >= 0) { - id = parseInt(String(args[0]), 10); - } else if (this._sounds.length) { - id = this._sounds[0]._id; - seekVal = parseFloat(String(args[0])); - } - } else if (args.length === 2) { - seekVal = parseFloat(String(args[0])); - id = parseInt(String(args[1]), 10); - } - - if (typeof id === "undefined") { - return 0; - } - - if ( - typeof seekVal === "number" && - (this._state !== "loaded" || this._playLock) - ) { - this._queue.push({ - event: "seek", - action: () => { - if (args.length >= 1 && typeof args[0] === "number") { - if (args.length >= 2 && typeof args[1] === "number") { - this.seek(args[0], args[1]); - } else { - this.seek(args[0]); - } - } - }, - }); - - return this; - } - - const sound = this._soundById(id); - - if (sound) { - if (typeof seekVal === "number" && seekVal >= 0) { - const playing = this.playing(id); - if (playing) { - this.pause(id, true); - } - - sound._seek = seekVal; - sound._ended = false; - this._clearTimer(id); - - if ( - !this._webAudio && - sound._node && - isHTMLAudioElement(sound._node) && - !isNaN(sound._node.duration) - ) { - sound._node.currentTime = seekVal; - } - - 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) { - 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 if (sound._node && isHTMLAudioElement(sound._node)) { - return sound._node.currentTime; - } - return 0; - } - } - - return this; - } - - playing(id?: number): boolean { - if (typeof id === "number") { - const sound = this._soundById(id); - return sound ? !sound._paused : false; - } - - for (let i = 0; i < this._sounds.length; i++) { - if (!this._sounds[i]._paused) { - return true; - } - } - - return false; - } - - duration(id?: number): number { - let duration = this._duration; - - if (typeof id !== "undefined") { - const sound = this._soundById(id); - if (sound) { - duration = this._sprite[sound._sprite][1] / 1000; - } - } - - return duration; - } - - state(): string { - return this._state; - } - - unload(): null { - // Execute plugin hooks before destruction - globalPluginManager.executeHowlDestroy(this); - - const sounds = this._sounds; - for (let i = 0; i < sounds.length; i++) { - if (!sounds[i]._paused) { - this.stop(sounds[i]._id); - } - - const node = sounds[i]._node; - if (!this._webAudio && node && isHTMLAudioElement(node)) { - this._clearSound(node); - - const errorFn = sounds[i]._errorFn; - if (errorFn) { - node.removeEventListener("error", errorFn, false); - } - const loadFn = sounds[i]._loadFn; - if (loadFn) { - node.removeEventListener( - Howler._canPlayEvent as string, - loadFn, - false, - ); - } - const endFn = sounds[i]._endFn; - if (endFn) { - node.removeEventListener("ended", endFn, false); - } - - Howler._releaseHtml5Audio(node); - } - - sounds[i]._node = null; - - this._clearTimer(sounds[i]._id); - } - - const index = Howler._howls.indexOf(this); - if (index >= 0) { - Howler._howls.splice(index, 1); - } - - let remCache = true; - for (let i = 0; i < Howler._howls.length; i++) { - if ( - Howler._howls[i]._src === this._src || - (this._src as string).indexOf(Howler._howls[i]._src as string) >= 0 - ) { - remCache = false; - break; - } - } - - if (cache && remCache) { - delete cache[this._src as string]; - } - - Howler.noAudio = false; - - this._state = "unloaded"; - this._sounds = []; - - return null; - } - - on( - event: string, - fn: (...args: unknown[]) => void, - id?: number, - once?: boolean, - ): Howl { - const events = (this as unknown as Record)[ - `_on${event}` - ]; - - if (typeof fn === "function") { - events.push(once ? { id, fn, once } : { id, fn }); - } - - return this; - } - - off(event: string, fn?: (...args: unknown[]) => void, id?: number): Howl { - const events = (this as unknown as Record)[ - `_on${event}` - ]; - let i = 0; - - if (typeof fn === "number") { - id = fn; - fn = undefined; - } - - if (fn || id) { - for (i = 0; i < events.length; i++) { - const isId = id === events[i].id; - if ((fn === events[i].fn && isId) || (!fn && isId)) { - events.splice(i, 1); - break; - } - } - } else if (event) { - (this as unknown as Record)[`_on${event}`] = []; - } else { - const keys = Object.keys(this); - for (i = 0; i < keys.length; i++) { - if ( - keys[i].indexOf("_on") === 0 && - Array.isArray( - (this as unknown as Record)[keys[i]], - ) - ) { - (this as unknown as Record)[keys[i]] = []; - } - } - } - - return this; - } - - once(event: string, fn: (...args: unknown[]) => void, id?: number): Howl { - this.on(event, fn, id, true); - - return this; - } - - _emit(event: string, id?: number | null, msg?: string): Howl { - const events = (this as unknown as Record)[ - `_on${event}` - ]; - - for (let i = events.length - 1; i >= 0; i--) { - if (!events[i].id || events[i].id === id || event === "load") { - const fn = events[i].fn; - setTimeout(() => { - fn(id, msg); - }, 0); - - if (events[i].once) { - this.off(event, events[i].fn, events[i].id); - } - } - } - - this._loadQueue(event); - - return this; - } - - _loadQueue(event?: string): Howl { - if (this._queue.length > 0) { - const task = this._queue[0]; - - if (task.event === event) { - this._queue.shift(); - this._loadQueue(); - } - - if (!event) { - task.action(); - } - } - - return this; - } - - _ended(sound: Sound): Howl { - const sprite = sound._sprite; - - if ( - !this._webAudio && - sound._node && - isHTMLAudioElement(sound._node) && - !sound._node.paused && - !sound._node.ended && - sound._node.currentTime < sound._stop! - ) { - setTimeout(this._ended.bind(this, sound), 100); - return this; - } - - const 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) { - this._emit("play", sound._id); - sound._seek = sound._start || 0; - sound._rateSeek = 0; - sound._playStart = Howler.ctx!.currentTime; - - const timeout = - ((sound._stop! - (sound._start || 0)) * 1000) / 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._autoSuspend(); - } - - if (!this._webAudio && !loop) { - this.stop(sound._id, true); - } - - return this; - } - - _clearTimer(id: number): Howl { - if (this._endTimers[id]) { - if (typeof this._endTimers[id] !== "function") { - clearTimeout(this._endTimers[id]); - } else { - const sound = this._soundById(id); - if (sound && sound._node) { - sound._node.removeEventListener("ended", this._endTimers[id], false); - } - } - - delete this._endTimers[id]; - } - - return this; - } - - _soundById(id: number): Sound | null { - for (let i = 0; i < this._sounds.length; i++) { - if (id === this._sounds[i]._id) { - return this._sounds[i]; - } - } - - return null; - } - - _inactiveSound(): Sound { - this._drain(); - - for (let i = 0; i < this._sounds.length; i++) { - if (this._sounds[i]._ended) { - return this._sounds[i].reset(); - } - } - - return new Sound(this); - } - - _drain(): void { - const limit = this._pool; - let cnt = 0; - - if (this._sounds.length < limit) { - return; - } - - for (let i = 0; i < this._sounds.length; i++) { - if (this._sounds[i]._ended) { - cnt++; - } - } - - for (let i = this._sounds.length - 1; i >= 0; i--) { - if (cnt <= limit) { - return; - } - - if (this._sounds[i]._ended) { - const node = this._sounds[i]._node; - if (this._webAudio && node && isGainNode(node)) { - node.disconnect(0); - } - - this._sounds.splice(i, 1); - cnt--; - } - } - } - - _getSoundIds(id?: number): number[] { - if (typeof id === "undefined") { - const ids: number[] = []; - for (let i = 0; i < this._sounds.length; i++) { - ids.push(this._sounds[i]._id); - } - - return ids; - } else { - return [id]; - } - } - - _refreshBuffer(sound: Sound): Howl { - if (!sound._node || !isGainNode(sound._node) || !Howler.ctx) { - return this; - } - - sound._node.bufferSource = - Howler.ctx.createBufferSource() as AudioBufferSourceNodeWithLegacy; - const src = - typeof this._src === "string" - ? this._src - : Array.isArray(this._src) && this._src.length > 0 - ? this._src[0] - : ""; - sound._node.bufferSource.buffer = cache[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.ctx.currentTime, - ); - - return this; - } - - _cleanBuffer(node: any): Howl { - const isIOS = isAppleVendor(Howler._navigator); - - if (!node.bufferSource) { - return this; - } - - 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 this; - } - - _clearSound(node: HTMLAudioElementWithUnlocked): void { - if (!isIE(Howler._navigator)) { - node.src = - "data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA"; - } - } -} - -/** - * Type declaration for Howler with optional spatial audio mixin methods. - * These methods are added dynamically by the SpatialAudioPlugin at runtime. - */ -export interface HowlerInstance extends HowlerGlobal { - // Optional spatial audio methods added by plugin - _pos?: [number, number, number]; - _orientation?: [number, number, number, number, number, number]; - pos?(x?: number, y?: number, z?: number): any; - orientation?( - x?: number, - y?: number, - z?: number, - xUp?: number, - yUp?: number, - zUp?: number, - ): any; - stereo?(pan?: number): any; -} - -/** - * Type declaration for Howl with optional spatial audio mixin methods. - * These methods are added dynamically by the SpatialAudioPlugin at runtime. - */ -export interface HowlInstance extends Howl { - // Optional spatial audio properties added by plugin - _pos?: [number, number, number] | null; - _orientation?: [number, number, number]; - _stereo?: number | null; - _pannerAttr?: any; - - // Optional spatial audio methods added by plugin - pos?(x?: number, y?: number, z?: number, id?: number): any; - orientation?(x?: number, y?: number, z?: number, id?: number): any; - stereo?(pan?: number, id?: number): any; - pannerAttr?(o?: any, id?: number): any; -} +export type { + AudioBufferSourceNodeWithLegacy, + EventListener, + GainNodeWithBufferSource, + HowlOptions, + HTMLAudioElementWithUnlocked, + NavigatorWithCocoonJS, + QueueItem, + WindowWithAudio +} from "./types"; +// Export for ESM - explicit exports for better tree-shaking +export { cache, isGainNode, isHTMLAudioElement } from "./types"; +export { Howl, Howler, HowlerGlobal, Sound }; -// Export for ESM -export * from "./types"; -export { Howl, Howler, Sound }; -export default { Howler, Howl, Sound }; diff --git a/src/plugins/spatial-plugin.ts b/src/plugins/spatial-plugin.ts index 04c91949..aac7e753 100644 --- a/src/plugins/spatial-plugin.ts +++ b/src/plugins/spatial-plugin.ts @@ -11,86 +11,149 @@ import type { HowlOptions } from "../howler.core"; import { - type Howl, - Howler, - type HowlerGlobal, - type Sound, + type Howl, + Howler, + type HowlerGlobal, + type Sound, } from "../howler.core"; import { isGainNode } from "../types"; import { globalPluginManager, HowlerPlugin, type PluginHooks } from "./plugin"; /** - * Extended HowlOptions with spatial audio properties + * Extended HowlOptions with spatial audio properties. + * Use this interface when creating a Howl instance with spatial audio capabilities. + * + * @example + * ```typescript + * const sound = new Howl({ + * src: ['sound.mp3'], + * pos: [10, 20, 30], + * stereo: 0.5, + * distanceModel: 'inverse' + * } as SpatialHowlOptions); + * ``` */ export interface SpatialHowlOptions extends HowlOptions { + /** 3D position of the sound source [x, y, z]. */ pos?: [number, number, number]; + /** Orientation vector of the sound source [x, y, z]. */ orientation?: [number, number, number]; + /** Stereo panning value from -1.0 (left) to 1.0 (right). */ stereo?: number; + /** Inner angle of the sound cone in degrees. Default: `360` */ coneInnerAngle?: number; + /** Outer angle of the sound cone in degrees. Default: `360` */ coneOuterAngle?: number; + /** Gain value outside the outer cone. Range: 0.0 to 1.0. Default: `0` */ coneOuterGain?: number; + /** Distance model algorithm: 'linear', 'inverse', or 'exponential'. Default: `'inverse'` */ distanceModel?: "linear" | "inverse" | "exponential"; + /** Maximum distance for the distance model. Default: `10000` */ maxDistance?: number; + /** Panning model: 'equalpower' or 'HRTF'. Default: `'HRTF'` */ panningModel?: "equalpower" | "HRTF"; + /** Reference distance for the distance model. Default: `1` */ refDistance?: number; + /** Rolloff factor for the distance model. Default: `1` */ rolloffFactor?: number; + /** Fires when the stereo panning changes. */ onstereo?: () => void; + /** Fires when the 3D position changes. */ onpos?: () => void; + /** Fires when the orientation changes. */ onorientation?: () => void; } /** - * Spatial audio properties for HowlerGlobal + * Spatial audio state for the global Howler instance. + * Contains the listener's position and orientation in 3D space. + * + * @internal */ export interface SpatialAudioState { + /** Listener's 3D position [x, y, z]. */ _pos: [number, number, number]; + /** Listener's orientation [forwardX, forwardY, forwardZ, upX, upY, upZ]. */ _orientation: [number, number, number, number, number, number]; } /** - * Spatial audio properties for Howl + * Spatial audio state for a Howl instance. + * Contains the sound source's position, orientation, and panner attributes. + * + * @internal */ export interface SpatialHowlState { + /** Sound source's 3D position [x, y, z], or null if not set. */ _pos: [number, number, number] | null; + /** Sound source's orientation vector [x, y, z]. */ _orientation: [number, number, number]; + /** Stereo panning value from -1.0 to 1.0, or null if not set. */ _stereo: number | null; + /** Panner node attributes for 3D audio processing. */ _pannerAttr: { + /** Inner angle of the sound cone in degrees. */ coneInnerAngle: number; + /** Outer angle of the sound cone in degrees. */ coneOuterAngle: number; + /** Gain value outside the outer cone (0.0 to 1.0). */ coneOuterGain: number; + /** Distance model algorithm. */ distanceModel: "linear" | "inverse" | "exponential"; + /** Maximum distance for the distance model. */ maxDistance: number; + /** Panning model algorithm. */ panningModel: "equalpower" | "HRTF"; + /** Reference distance for the distance model. */ refDistance: number; + /** Rolloff factor for the distance model. */ rolloffFactor: number; }; + /** Event listeners for stereo panning changes. */ _onstereo: Array<{ fn: () => void }>; + /** Event listeners for position changes. */ _onpos: Array<{ fn: () => void }>; + /** Event listeners for orientation changes. */ _onorientation: Array<{ fn: () => void }>; } /** - * Spatial audio properties for Sound + * Spatial audio state for a Sound instance. + * Contains per-sound spatial audio properties. + * + * @internal */ export interface SpatialSoundState { + /** Sound's 3D position [x, y, z], or null if not set. */ _pos: [number, number, number] | null; + /** Sound's orientation vector [x, y, z]. */ _orientation: [number, number, number]; + /** Stereo panning value from -1.0 to 1.0, or null if not set. */ _stereo: number | null; + /** Panner node attributes for 3D audio processing. */ _pannerAttr: { + /** Inner angle of the sound cone in degrees. */ coneInnerAngle: number; + /** Outer angle of the sound cone in degrees. */ coneOuterAngle: number; + /** Gain value outside the outer cone (0.0 to 1.0). */ coneOuterGain: number; + /** Distance model algorithm. */ distanceModel: "linear" | "inverse" | "exponential"; + /** Maximum distance for the distance model. */ maxDistance: number; + /** Panning model algorithm. */ panningModel: "equalpower" | "HRTF"; + /** Reference distance for the distance model. */ refDistance: number; + /** Rolloff factor for the distance model. */ rolloffFactor: number; }; } /** - * Howler instance with spatial audio capabilities - * Use this type when the spatial plugin is registered + * Howler instance with spatial audio capabilities. + * Use this type when the spatial plugin is registered to get full type safety for spatial audio methods. * * @example * ```typescript @@ -100,16 +163,35 @@ export interface SpatialSoundState { * Howler.addPlugin(new SpatialAudioPlugin()); * * const howler: SpatialHowler = Howler as SpatialHowler; - * howler.pos(10, 20, 30); + * howler.pos(10, 20, 30); // Set listener position + * howler.orientation(0, 0, -1, 0, 1, 0); // Set listener orientation + * howler.stereo(0.5); // Set stereo panning * ``` */ export type SpatialHowler = HowlerGlobal & SpatialAudioState & { + /** + * Set or get the listener's 3D position. + * @param x - X coordinate (optional) + * @param y - Y coordinate (optional) + * @param z - Z coordinate (optional) + * @returns If called with no arguments, returns the current position [x, y, z]. Otherwise, returns the Howler instance for chaining. + */ pos( x?: number, y?: number, z?: number, ): SpatialHowler | [number, number, number]; + /** + * Set or get the listener's orientation. + * @param x - Forward X component (optional) + * @param y - Forward Y component (optional) + * @param z - Forward Z component (optional) + * @param xUp - Up X component (optional) + * @param yUp - Up Y component (optional) + * @param zUp - Up Z component (optional) + * @returns If called with no arguments, returns the current orientation [forwardX, forwardY, forwardZ, upX, upY, upZ]. Otherwise, returns the Howler instance for chaining. + */ orientation( x?: number, y?: number, @@ -118,12 +200,17 @@ export type SpatialHowler = HowlerGlobal & yUp?: number, zUp?: number, ): SpatialHowler | [number, number, number, number, number, number]; + /** + * Set or get the stereo panning value. + * @param pan - Panning value from -1.0 (left) to 1.0 (right) (optional) + * @returns If called with no arguments, returns the current panning value. Otherwise, returns the Howler instance for chaining. + */ stereo(pan?: number): SpatialHowler; }; /** - * Howl instance with spatial audio capabilities - * Use this type when the spatial plugin is registered + * Howl instance with spatial audio capabilities. + * Use this type when the spatial plugin is registered to get full type safety for spatial audio methods. * * @example * ```typescript @@ -137,25 +224,54 @@ export type SpatialHowler = HowlerGlobal & * pos: [10, 20, 30] * } as SpatialHowlOptions) as SpatialHowl; * - * sound.pos(5, 10, 15); - * sound.stereo(0.5); + * sound.pos(5, 10, 15); // Set sound position + * sound.stereo(0.5); // Set stereo panning + * sound.orientation(0, 1, 0); // Set sound orientation * ``` */ export type SpatialHowl = Howl & SpatialHowlState & { + /** + * Set or get the sound's 3D position. + * @param x - X coordinate (optional) + * @param y - Y coordinate (optional) + * @param z - Z coordinate (optional) + * @param id - Sound ID to target a specific sound instance (optional) + * @returns If called with no arguments, returns the current position [x, y, z]. Otherwise, returns the Howl instance for chaining. + */ pos( x?: number, y?: number, z?: number, id?: number, ): SpatialHowl | [number, number, number]; + /** + * Set or get the sound's orientation vector. + * @param x - X component (optional) + * @param y - Y component (optional) + * @param z - Z component (optional) + * @param id - Sound ID to target a specific sound instance (optional) + * @returns If called with no arguments, returns the current orientation [x, y, z]. Otherwise, returns the Howl instance for chaining. + */ orientation( x?: number, y?: number, z?: number, id?: number, ): SpatialHowl | [number, number, number]; + /** + * Set or get the stereo panning value. + * @param pan - Panning value from -1.0 (left) to 1.0 (right) (optional) + * @param id - Sound ID to target a specific sound instance (optional) + * @returns If called with no arguments, returns the current panning value. Otherwise, returns the Howl instance for chaining. + */ stereo(pan?: number, id?: number): SpatialHowl | number; + /** + * Set or get panner node attributes. + * @param o - Panner attributes object (optional) + * @param id - Sound ID to target a specific sound instance (optional) + * @returns If called with no arguments, returns the current panner attributes. Otherwise, returns the Howl instance for chaining. + */ pannerAttr(o?: any, id?: number): SpatialHowl | any; }; @@ -912,13 +1028,7 @@ export class SpatialAudioPlugin extends HowlerPlugin { ) { try { // Stop the current buffer source - if ( - typeof (sound._node.bufferSource as any).stop === "undefined" - ) { - (sound._node.bufferSource as any).noteOff(0); - } else { - (sound._node.bufferSource as any).stop(0); - } + sound._node.bufferSource.stop(0); sound._node.bufferSource.disconnect(0); } catch (e) { // Buffer source may already be stopped or disconnected diff --git a/src/plugins/spatial.ts b/src/plugins/spatial.ts index 983977b2..c530e8dc 100644 --- a/src/plugins/spatial.ts +++ b/src/plugins/spatial.ts @@ -1,15 +1,42 @@ /** * Howler.js - Spatial Plugin Entry Point + * + * This module exports the spatial audio plugin and all related types for TypeScript support. + * + * @example + * ```typescript + * import { Howler } from 'howler'; + * import { SpatialAudioPlugin, type SpatialHowler, type SpatialHowl } from 'howler/plugins/spatial'; + * + * // Register the plugin + * Howler.addPlugin(new SpatialAudioPlugin()); + * + * // Use typed Howler instance + * const howler: SpatialHowler = Howler as SpatialHowler; + * howler.pos(10, 20, 30); + * ``` */ -// Export spatial types for TypeScript support +/** + * Type exports for spatial audio functionality. + * Import these types when using the spatial plugin for full TypeScript support. + */ export type { + /** Spatial audio state for the global Howler instance. */ SpatialAudioState, + /** Howl instance with spatial audio capabilities. */ SpatialHowl, + /** Howler instance with spatial audio capabilities. */ SpatialHowler, + /** Extended HowlOptions with spatial audio properties. */ SpatialHowlOptions, + /** Spatial audio state for a Howl instance. */ SpatialHowlState, - SpatialSoundState, + /** Spatial audio state for a Sound instance. */ + SpatialSoundState } from "./spatial-plugin"; -// Export the plugin for use with Howler.addPlugin() +/** + * Spatial audio plugin for Howler.js. + * Adds 3D spatial audio and stereo panning capabilities. + */ export { SpatialAudioPlugin } from "./spatial-plugin"; diff --git a/src/sound.ts b/src/sound.ts new file mode 100644 index 00000000..ca73c78b --- /dev/null +++ b/src/sound.ts @@ -0,0 +1,205 @@ +/*! + * howler.js v2.2.4 + * howlerjs.com + * + * (c) 2013-2020, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */ + +// Import Howler singleton from howler.core.ts +// This creates a circular dependency, but it's resolved at runtime +import { type Howl, Howler } from "./howler.core"; +import { globalPluginManager } from "./plugins/plugin"; +import { + type GainNodeWithBufferSource, + type HTMLAudioElementWithUnlocked, + isHTMLAudioElement, +} from "./types"; + +export class Sound { + _parent: Howl; + _muted: boolean = false; + _loop: boolean = false; + _volume: number = 1; + _rate: number = 1; + _seek: number = 0; + _paused: boolean = true; + _ended: boolean = true; + _sprite: string = "__default"; + _id: number = 0; + _node: HTMLAudioElementWithUnlocked | GainNodeWithBufferSource | null = null; + _playStart: number = 0; + _rateSeek: number = 0; + _errorFn?: (event: Event) => void; + _loadFn?: (event: Event) => void; + _endFn?: (event: Event) => void; + _start?: number; + _stop?: number; + _panner?: PannerNode | StereoPannerNode; + _fadeTo?: number; + _interval?: ReturnType; + + constructor(howl: Howl) { + this._parent = howl; + this.init(); + } + + init(): Sound { + const parent = this._parent; + + this._muted = parent._muted; + this._loop = parent._loop; + this._volume = parent._volume; + this._rate = parent._rate; + this._seek = 0; + this._paused = true; + this._ended = true; + this._sprite = "__default"; + + this._id = ++Howler._counter; + + parent._sounds.push(this); + + this.create(); + + // Execute plugin hooks + globalPluginManager.executeSoundCreate(this, parent); + + return this; + } + + create(): Sound { + const parent = this._parent; + const volume = + Howler._muted || this._muted || parent._muted ? 0 : this._volume; + + this._errorFn = this._errorListener.bind(this); + this._loadFn = this._loadListener.bind(this); + this._endFn = this._endListener.bind(this); + + if (parent._webAudio && Howler.ctx) { + const gainNode = Howler.ctx.createGain(); + if (gainNode) { + this._node = gainNode as GainNodeWithBufferSource; + this._node.gain.setValueAtTime(volume, Howler.ctx.currentTime); + (this._node as { paused?: boolean }).paused = true; + this._node.connect(Howler.masterGain!); + } + } else if (!Howler.noAudio) { + this._node = Howler._obtainHtml5Audio(); + + this._errorFn = this._errorListener.bind(this); + this._node.addEventListener("error", this._errorFn, false); + + this._loadFn = this._loadListener.bind(this); + this._node.addEventListener(Howler._canPlayEvent, this._loadFn, false); + + this._endFn = this._endListener.bind(this); + this._node.addEventListener("ended", this._endFn, false); + + const src = + typeof parent._src === "string" + ? parent._src + : Array.isArray(parent._src) && parent._src.length > 0 + ? parent._src[0] + : ""; + this._node.src = src; + const preloadValue = + parent._preload === true + ? "auto" + : parent._preload === false + ? "none" + : parent._preload === "metadata" + ? "metadata" + : "auto"; + this._node.preload = preloadValue; + const volumeOrHowler = Howler.volume(); + if (typeof volumeOrHowler === "number") { + this._node.volume = volume * volumeOrHowler; + } + + this._node.load(); + } + + return this; + } + + reset(): Sound { + const 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._counter; + + return this; + } + + _errorListener(): void { + if (this._node && isHTMLAudioElement(this._node)) { + const errorCode = this._node.error ? this._node.error.code : 0; + this._parent._emit("loaderror", this._id, String(errorCode)); + if (this._errorFn) { + this._node.removeEventListener("error", this._errorFn, false); + } + } + } + + _loadListener(): void { + if (!this._node || !isHTMLAudioElement(this._node)) { + return; + } + + 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 * 1000] }; + } + + if (parent._state !== "loaded") { + parent._state = "loaded"; + parent._emit("load"); + parent._loadQueue(); + + // Execute plugin hooks + globalPluginManager.executeHowlLoad(parent); + } + + if (this._loadFn) { + this._node.removeEventListener(Howler._canPlayEvent, this._loadFn, false); + } + } + + _endListener(): void { + const parent = this._parent; + + if ( + parent._duration === Infinity && + this._node && + isHTMLAudioElement(this._node) + ) { + parent._duration = Math.ceil(this._node.duration * 10) / 10; + + if (parent._sprite.__default[1] === Infinity) { + parent._sprite.__default[1] = parent._duration * 1000; + } + + parent._ended(this); + } + + if (this._endFn && this._node) { + this._node.removeEventListener("ended", this._endFn, false); + } + } +} diff --git a/src/types.ts b/src/types.ts index c31e6930..ae840fea 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,91 +8,182 @@ * MIT License */ +/** + * Configuration options for creating a new Howl instance. + * + * @example + * ```typescript + * const sound = new Howl({ + * src: ['sound.mp3', 'sound.ogg'], + * volume: 0.5, + * autoplay: true, + * onload: () => console.log('Sound loaded') + * }); + * ``` + */ export interface HowlOptions { + /** Source file(s) for the audio. Can be a string or array of strings for multiple formats. */ src: string | string[]; + /** Automatically start playback when ready. Default: `false` */ autoplay?: boolean; + /** Format(s) of the audio file(s). If not specified, will be extracted from the src URL. */ format?: string | string[]; + /** 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 mute the sound. Default: `false` */ mute?: boolean; + /** Set to true to automatically loop the sound. Default: `false` */ loop?: boolean; + /** The size of the inactive sounds pool. Default: `5` */ pool?: number; + /** Automatically begin downloading the audio file when the Howl is initialized. If using HTML5 Audio, you can set this to 'metadata' to only preload the metadata. Default: `true` */ preload?: boolean | "metadata"; + /** The rate of playback. 0.5 to 4.0, with 1.0 being normal speed. Default: `1.0` */ rate?: number; + /** Define a sprite map of sections within the audio file. */ sprite?: Record; + /** The volume of the sound, from 0.0 to 1.0. Default: `1.0` */ volume?: number; + /** Configure XHR options for loading audio files. */ xhr?: { + /** The HTTP method to use. Default: `'GET'` */ method?: string; + /** Custom headers to send with the request. */ headers?: Record; + /** Whether to send credentials with the request. Default: `false` */ withCredentials?: boolean; }; + /** Fires when the sound finishes playing. */ onend?: () => void; + /** Fires when the sound has been faded in/out. */ onfade?: () => void; + /** Fires when the sound has been loaded. */ onload?: () => void; + /** Fires when the sound is unable to load. */ onloaderror?: (id: number, msg: string) => void; + /** Fires when the sound is unable to play. */ onplayerror?: (id: number, msg: string) => void; + /** Fires when the sound has been paused. */ onpause?: () => void; + /** Fires when the sound begins playing. */ onplay?: () => void; + /** Fires when the sound has been stopped. */ onstop?: () => void; + /** Fires when the sound has been muted/unmuted. */ onmute?: () => void; + /** Fires when the sound's volume has changed. */ onvolume?: () => void; + /** Fires when the sound's playback rate has changed. */ onrate?: () => void; + /** Fires when the sound's current position has changed. */ onseek?: () => void; + /** Fires when the audio has been unlocked (required for some browsers on mobile). */ onunlock?: () => void; } +/** + * Event listener configuration for Howl events. + * + * @internal + */ export interface EventListener { + /** Optional sound ID to scope the event to a specific sound instance. */ id?: number; + /** The callback function to execute when the event fires. */ fn: (...args: unknown[]) => void; + /** If true, the listener will be removed after the first execution. */ once?: boolean; } +/** + * Queue item for deferred actions when a sound is not yet loaded. + * + * @internal + */ export interface QueueItem { + /** The event name that triggers this action. */ event: string; + /** The action to execute when the event fires. */ action: () => void; } -// Global audio context cache +/** + * Global cache for decoded audio buffers. + * This allows multiple Howl instances to share the same audio data. + * + * @internal + */ export const cache: Record = {}; -// Type for HTML5 Audio element with custom properties +/** + * Extended HTMLAudioElement with custom properties used by Howler. + * + * @internal + */ export interface HTMLAudioElementWithUnlocked extends HTMLAudioElement { + /** Internal flag indicating if the audio element has been unlocked for playback. */ _unlocked?: boolean; } -// Type for AudioBufferSourceNode with legacy methods +/** + * Extended AudioBufferSourceNode with optional loop properties. + * Used for Web Audio API compatibility. + * + * @internal + */ export interface AudioBufferSourceNodeWithLegacy extends Omit { - noteOn?: (when: number) => void; - noteOff?: (when: number) => void; - noteGrainOn?: ( - when: number, - grainOffset: number, - grainDuration: number, - ) => void; + /** Whether the audio should loop. */ loop?: boolean; + /** The start time of the loop in seconds. */ loopStart?: number | undefined; + /** The end time of the loop in seconds. */ loopEnd?: number | undefined; } -// Type for window with Audio constructor +/** + * Extended Window interface with Audio constructor. + * + * @internal + */ export interface WindowWithAudio extends Window { + /** Audio constructor for creating HTML5 audio elements. */ Audio: { new (): HTMLAudioElement; }; - ejecta?: unknown; } -// Type for Navigator with CocoonJS -export interface NavigatorWithCocoonJS extends Navigator { - isCocoonJS?: boolean; -} +/** + * Navigator interface (no extensions needed for target browsers). + * + * @internal + */ +export interface NavigatorWithCocoonJS extends Navigator {} -// Type for GainNode with bufferSource property +/** + * Extended GainNode with bufferSource property for Web Audio API usage. + * + * @internal + */ export interface GainNodeWithBufferSource extends GainNode { + /** The AudioBufferSourceNode connected to this gain node. */ bufferSource?: AudioBufferSourceNodeWithLegacy; } -// Type guards for audio node types +/** + * Type guard to check if a node is an HTMLAudioElement. + * This specifically excludes HTMLVideoElement by checking for the absence of videoWidth. + * + * @param node - The node to check + * @returns True if the node is an HTMLAudioElement + * + * @example + * ```typescript + * if (isHTMLAudioElement(node)) { + * node.volume = 0.5; + * } + * ``` + */ export function isHTMLAudioElement( node: HTMLAudioElementWithUnlocked | GainNodeWithBufferSource | null, ): node is HTMLAudioElementWithUnlocked { @@ -105,6 +196,19 @@ export function isHTMLAudioElement( ); } +/** + * Type guard to check if a node is a GainNode. + * + * @param node - The node to check + * @returns True if the node is a GainNode + * + * @example + * ```typescript + * if (isGainNode(node)) { + * node.gain.value = 0.5; + * } + * ``` + */ export function isGainNode( node: HTMLAudioElementWithUnlocked | GainNodeWithBufferSource | null, ): node is GainNodeWithBufferSource { From 430f695b04e5e05966f0f602da6cf444a3a58205 Mon Sep 17 00:00:00 2001 From: eatsjobs Date: Sun, 23 Nov 2025 01:39:19 +0100 Subject: [PATCH 21/25] Refactor audio loading to use Fetch API and update TypeScript types - Replaced XMLHttpRequest with Fetch API in `audio-loader.ts` for improved performance and modern browser support. - Updated the `headers` property in the `Howl` class to use `HeadersInit` type for better type safety. - Enhanced documentation in `types.ts` to clarify the use of fetch options for loading audio files. These changes aim to modernize the audio loading process and improve type definitions for better maintainability. --- src/helpers/audio-loader.ts | 77 +++++++++++++++---------------------- src/helpers/index.ts | 3 +- src/howl.ts | 2 +- src/types.ts | 2 +- 4 files changed, 35 insertions(+), 49 deletions(-) diff --git a/src/helpers/audio-loader.ts b/src/helpers/audio-loader.ts index 8e83e359..3d0c272e 100644 --- a/src/helpers/audio-loader.ts +++ b/src/helpers/audio-loader.ts @@ -31,59 +31,46 @@ export const loadBuffer = (self: Howl) => { decodeAudioData(dataView.buffer, self); } else { - const xhr = new XMLHttpRequest(); - xhr.open(self._xhr.method, url, true); - xhr.withCredentials = self._xhr.withCredentials; - xhr.responseType = "arraybuffer"; + // Use fetch API (supported in all target browsers) + const fetchOptions: RequestInit = { + method: self._xhr.method, + credentials: self._xhr.withCredentials ? "include" : "same-origin", + }; if (self._xhr.headers) { - Object.keys(self._xhr.headers).forEach((key) => { - xhr.setRequestHeader(key, self._xhr.headers![key]); - }); + fetchOptions.headers = self._xhr.headers; } - xhr.onload = () => { - const 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); - } -}; - -export const safeXhrSend = (xhr: XMLHttpRequest) => { - try { - xhr.send(); - } catch (e) { - if (xhr.onerror) { - // Create a ProgressEvent-like object for the error handler - const errorEvent = new ProgressEvent("error", { - lengthComputable: false, - loaded: 0, - total: 0, + fetch(url, fetchOptions) + .then((response) => { + if (!response.ok) { + self._emit( + "loaderror", + null, + "Failed loading audio file with status: " + response.status + ".", + ); + return; + } + return response.arrayBuffer(); + }) + .then((arrayBuffer) => { + if (arrayBuffer) { + decodeAudioData(arrayBuffer, self); + } + }) + .catch(() => { + if (self._webAudio) { + self._html5 = true; + self._webAudio = false; + self._sounds = []; + delete cache[url]; + self.load(); + } }); - xhr.onerror(errorEvent); - } } }; + export const decodeAudioData = (arraybuffer: ArrayBuffer, self: Howl) => { const error = () => { self._emit("loaderror", null, "Decoding audio data failed."); diff --git a/src/helpers/index.ts b/src/helpers/index.ts index b8e17643..d6c6171b 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -12,8 +12,7 @@ export { setupAudioContext } from "./audio-context"; export { decodeAudioData, loadBuffer, - loadSound, - safeXhrSend + loadSound } from "./audio-loader"; export { getIOSVersion, diff --git a/src/howl.ts b/src/howl.ts index e0aefccb..9226c531 100644 --- a/src/howl.ts +++ b/src/howl.ts @@ -39,7 +39,7 @@ class Howl { _volume: number = 1; _xhr: { method: string; - headers?: Record; + headers?: HeadersInit; withCredentials: boolean; } = { method: "GET", withCredentials: false }; _duration: number = 0; diff --git a/src/types.ts b/src/types.ts index ae840fea..f4746c40 100644 --- a/src/types.ts +++ b/src/types.ts @@ -44,7 +44,7 @@ export interface HowlOptions { sprite?: Record; /** The volume of the sound, from 0.0 to 1.0. Default: `1.0` */ volume?: number; - /** Configure XHR options for loading audio files. */ + /** Configure fetch options for loading audio files (when using Web Audio). */ xhr?: { /** The HTTP method to use. Default: `'GET'` */ method?: string; From 6e976d1fe9bb504a88833372edac6cb1773b34be Mon Sep 17 00:00:00 2001 From: eatsjobs Date: Sun, 23 Nov 2025 02:16:05 +0100 Subject: [PATCH 22/25] Remove outdated license headers and update versioning in source files for Howler.js v3.0.0-alpha.1 - Eliminated old license comments from multiple source files to streamline code. - Updated version information in `index.ts` to reflect the new alpha release. - Ensured consistency in code formatting and improved readability across various files. These changes aim to modernize the codebase in preparation for the upcoming release. --- src/helpers/audio-context.ts | 10 ---------- src/helpers/audio-loader.ts | 10 ---------- src/helpers/light-ua-parser.ts | 11 ----------- src/howl.ts | 10 ---------- src/howler-global.ts | 18 ++++-------------- src/howler.core.ts | 10 ---------- src/index.ts | 11 +++++++---- src/plugins/index.ts | 10 ---------- src/plugins/plugin.ts | 10 ---------- src/plugins/spatial-plugin.ts | 19 ++++--------------- src/sound.ts | 17 +++-------------- src/types.ts | 10 ---------- 12 files changed, 18 insertions(+), 128 deletions(-) diff --git a/src/helpers/audio-context.ts b/src/helpers/audio-context.ts index 288a2a2c..79ee15a6 100644 --- a/src/helpers/audio-context.ts +++ b/src/helpers/audio-context.ts @@ -1,13 +1,3 @@ -/*! - * Howler.js Audio Context Setup Helper - * howlerjs.com - * - * (c) 2013-2020, James Simpson of GoldFire Studios - * goldfirestudios.com - * - * MIT License - */ - import { Howler } from "../howler.core"; import { getIOSVersion, isIOS, isSafari } from "./light-ua-parser"; diff --git a/src/helpers/audio-loader.ts b/src/helpers/audio-loader.ts index 3d0c272e..f27b185e 100644 --- a/src/helpers/audio-loader.ts +++ b/src/helpers/audio-loader.ts @@ -1,13 +1,3 @@ -/*! - * Howler.js Audio Loading Helpers - * howlerjs.com - * - * (c) 2013-2020, James Simpson of GoldFire Studios - * goldfirestudios.com - * - * MIT License - */ - import type { Howl } from "../howler.core"; import { Howler } from "../howler.core"; import { globalPluginManager } from "../plugins/plugin"; diff --git a/src/helpers/light-ua-parser.ts b/src/helpers/light-ua-parser.ts index f22fac6c..ca8a72f8 100644 --- a/src/helpers/light-ua-parser.ts +++ b/src/helpers/light-ua-parser.ts @@ -1,14 +1,3 @@ -/*! - * Lightweight User Agent Parser for Howler.js - * Provides simple browser/device detection utilities - * howlerjs.com - * - * (c) 2013-2020, James Simpson of GoldFire Studios - * goldfirestudios.com - * - * MIT License - */ - /** * Get the user agent string from navigator */ diff --git a/src/howl.ts b/src/howl.ts index 9226c531..b853f9cc 100644 --- a/src/howl.ts +++ b/src/howl.ts @@ -1,13 +1,3 @@ -/*! - * howler.js v2.2.4 - * howlerjs.com - * - * (c) 2013-2020, James Simpson of GoldFire Studios - * goldfirestudios.com - * - * MIT License - */ - import { setupAudioContext } from "./helpers/audio-context"; import { loadBuffer } from "./helpers/audio-loader"; import { isAppleVendor } from "./helpers/light-ua-parser"; diff --git a/src/howler-global.ts b/src/howler-global.ts index 5b72e2e7..670a60a8 100644 --- a/src/howler-global.ts +++ b/src/howler-global.ts @@ -1,23 +1,13 @@ -/*! - * howler.js v2.2.4 - * howlerjs.com - * - * (c) 2013-2020, James Simpson of GoldFire Studios - * goldfirestudios.com - * - * MIT License - */ - // Import helper functions directly for better tree-shaking import { setupAudioContext } from "./helpers/audio-context"; import type { Howl } from "./howl"; // Import plugin manager import { globalPluginManager, type HowlerPlugin } from "./plugins/plugin"; import { - type HTMLAudioElementWithUnlocked, - isHTMLAudioElement, - type NavigatorWithCocoonJS, - type WindowWithAudio, + type HTMLAudioElementWithUnlocked, + isHTMLAudioElement, + type NavigatorWithCocoonJS, + type WindowWithAudio, } from "./types"; export class HowlerGlobal { diff --git a/src/howler.core.ts b/src/howler.core.ts index d245e9c2..8f545561 100644 --- a/src/howler.core.ts +++ b/src/howler.core.ts @@ -1,13 +1,3 @@ -/*! - * howler.js v2.2.4 - * howlerjs.com - * - * (c) 2013-2020, James Simpson of GoldFire Studios - * goldfirestudios.com - * - * MIT License - */ - import { Howl } from "./howl"; // Import classes from their own files import { HowlerGlobal } from "./howler-global"; diff --git a/src/index.ts b/src/index.ts index 0421ce63..5554d57d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,11 @@ -/** - * Howler.js - Javascript Audio Library - * Main entry point for the library +/*! + * howler.js v3.0.0-alpha.1 + * howlerjs.com + * (c) 2013-2025, James Simpson of GoldFire Studios + * goldfirestudios.com + * https://github.com/goldfire/howler.js + * MIT License */ - // Core library exports export { Howl, Howler, Sound } from "./howler.core"; export type { PluginHooks } from "./plugins"; diff --git a/src/plugins/index.ts b/src/plugins/index.ts index bf3f7e81..7cfe2238 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -1,13 +1,3 @@ -/*! - * Howler.js Plugin System - * howlerjs.com - * - * (c) 2013-2020, James Simpson of GoldFire Studios - * goldfirestudios.com - * - * MIT License - */ - export type { PluginHooks, RegisteredPlugin } from "./plugin"; // Core plugin infrastructure export { globalPluginManager, HowlerPlugin, PluginManager } from "./plugin"; diff --git a/src/plugins/plugin.ts b/src/plugins/plugin.ts index e2c7d957..8a40d1a8 100644 --- a/src/plugins/plugin.ts +++ b/src/plugins/plugin.ts @@ -1,13 +1,3 @@ -/*! - * Howler.js Plugin System - * howlerjs.com - * - * (c) 2013-2020, James Simpson of GoldFire Studios - * goldfirestudios.com - * - * MIT License - */ - import type { Howl, HowlerGlobal, Sound } from "../howler.core"; import type { HowlOptions } from "../types"; diff --git a/src/plugins/spatial-plugin.ts b/src/plugins/spatial-plugin.ts index aac7e753..ac6ec38f 100644 --- a/src/plugins/spatial-plugin.ts +++ b/src/plugins/spatial-plugin.ts @@ -1,20 +1,9 @@ -/*! - * Spatial Plugin for Howler.js - * Adds 3D spatial audio and stereo panning support - * howlerjs.com - * - * (c) 2013-2020, James Simpson of GoldFire Studios - * goldfirestudios.com - * - * MIT License - */ - import type { HowlOptions } from "../howler.core"; import { - type Howl, - Howler, - type HowlerGlobal, - type Sound, + type Howl, + Howler, + type HowlerGlobal, + type Sound, } from "../howler.core"; import { isGainNode } from "../types"; import { globalPluginManager, HowlerPlugin, type PluginHooks } from "./plugin"; diff --git a/src/sound.ts b/src/sound.ts index ca73c78b..4dec4f7c 100644 --- a/src/sound.ts +++ b/src/sound.ts @@ -1,21 +1,10 @@ -/*! - * howler.js v2.2.4 - * howlerjs.com - * - * (c) 2013-2020, James Simpson of GoldFire Studios - * goldfirestudios.com - * - * MIT License - */ - // Import Howler singleton from howler.core.ts -// This creates a circular dependency, but it's resolved at runtime import { type Howl, Howler } from "./howler.core"; import { globalPluginManager } from "./plugins/plugin"; import { - type GainNodeWithBufferSource, - type HTMLAudioElementWithUnlocked, - isHTMLAudioElement, + type GainNodeWithBufferSource, + type HTMLAudioElementWithUnlocked, + isHTMLAudioElement, } from "./types"; export class Sound { diff --git a/src/types.ts b/src/types.ts index f4746c40..dba686d7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,13 +1,3 @@ -/*! - * Howler.js Type Definitions - * howlerjs.com - * - * (c) 2013-2020, James Simpson of GoldFire Studios - * goldfirestudios.com - * - * MIT License - */ - /** * Configuration options for creating a new Howl instance. * From 636f7feb574300aaf043ebe091fcd2fad9260d80 Mon Sep 17 00:00:00 2001 From: eatsjobs Date: Sun, 23 Nov 2025 02:17:05 +0100 Subject: [PATCH 23/25] add attw and publit checks for Howler.js v3.0.0-alpha.1 - Modified `package.json` to change the repository URL format and added new development dependencies: `@arethetypeswrong/core` and `publint`. - Updated `package-lock.json` to reflect changes in dependencies and their versions. - Adjusted `tsdown.config.ts` to specify entry points for bundling, disable unbundling for plugins, and enhance minification settings. - Revised `check-size.js` to reflect changes in file structure due to bundling configuration. These updates aim to improve project configuration, enhance dependency management, and streamline the build process. --- package-lock.json | 2387 ++++++++++++++++++++++------------------- package.json | 12 +- scripts/check-size.js | 10 +- tsdown.config.ts | 18 +- 4 files changed, 1301 insertions(+), 1126 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9e1652b2..766d2704 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,1113 +1,1278 @@ { - "name": "howler", - "version": "3.0.0-alpha.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "howler", - "version": "3.0.0-alpha.1", - "license": "MIT", - "devDependencies": { - "@biomejs/biome": "2.3.7", - "tsdown": "~0.16.6", - "typescript": "~5.9.3" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.5" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@biomejs/biome": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.7.tgz", - "integrity": "sha512-CTbAS/jNAiUc6rcq94BrTB8z83O9+BsgWj2sBCQg9rD6Wkh2gjfR87usjx0Ncx0zGXP1NKgT7JNglay5Zfs9jw==", - "dev": true, - "license": "MIT OR Apache-2.0", - "bin": { - "biome": "bin/biome" - }, - "engines": { - "node": ">=14.21.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/biome" - }, - "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.3.7", - "@biomejs/cli-darwin-x64": "2.3.7", - "@biomejs/cli-linux-arm64": "2.3.7", - "@biomejs/cli-linux-arm64-musl": "2.3.7", - "@biomejs/cli-linux-x64": "2.3.7", - "@biomejs/cli-linux-x64-musl": "2.3.7", - "@biomejs/cli-win32-arm64": "2.3.7", - "@biomejs/cli-win32-x64": "2.3.7" - } - }, - "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.7.tgz", - "integrity": "sha512-LirkamEwzIUULhXcf2D5b+NatXKeqhOwilM+5eRkbrnr6daKz9rsBL0kNZ16Hcy4b8RFq22SG4tcLwM+yx/wFA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.7.tgz", - "integrity": "sha512-Q4TO633kvrMQkKIV7wmf8HXwF0dhdTD9S458LGE24TYgBjSRbuhvio4D5eOQzirEYg6eqxfs53ga/rbdd8nBKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.7.tgz", - "integrity": "sha512-inHOTdlstUBzgjDcx0ge71U4SVTbwAljmkfi3MC5WzsYCRhancqfeL+sa4Ke6v2ND53WIwCFD5hGsYExoI3EZQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.7.tgz", - "integrity": "sha512-/afy8lto4CB8scWfMdt+NoCZtatBUF62Tk3ilWH2w8ENd5spLhM77zKlFZEvsKJv9AFNHknMl03zO67CiklL2Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.7.tgz", - "integrity": "sha512-fJMc3ZEuo/NaMYo5rvoWjdSS5/uVSW+HPRQujucpZqm2ZCq71b8MKJ9U4th9yrv2L5+5NjPF0nqqILCl8HY/fg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.7.tgz", - "integrity": "sha512-CQUtgH1tIN6e5wiYSJqzSwJumHYolNtaj1dwZGCnZXm2PZU1jOJof9TsyiP3bXNDb+VOR7oo7ZvY01If0W3iFQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.7.tgz", - "integrity": "sha512-aJAE8eCNyRpcfx2JJAtsPtISnELJ0H4xVVSwnxm13bzI8RwbXMyVtxy2r5DV1xT3WiSP+7LxORcApWw0LM8HiA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-x64": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.7.tgz", - "integrity": "sha512-pulzUshqv9Ed//MiE8MOUeeEkbkSHVDVY5Cz5wVAnH1DUqliCQG3j6s1POaITTFqFfo7AVIx2sWdKpx/GS+Nqw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@emnapi/core": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", - "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", - "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", - "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.5.0", - "@emnapi/runtime": "^1.5.0", - "@tybys/wasm-util": "^0.10.1" - } - }, - "node_modules/@oxc-project/runtime": { - "version": "0.96.0", - "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.96.0.tgz", - "integrity": "sha512-34lh4o9CcSw09Hx6fKihPu85+m+4pmDlkXwJrLvN5nMq5JrcGhhihVM415zDqT8j8IixO1PYYdQZRN4SwQCncg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.98.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.98.0.tgz", - "integrity": "sha512-Vzmd6FsqVuz5HQVcRC/hrx7Ujo3WEVeQP7C2UNP5uy1hUY4SQvMB+93jxkI1KRHz9a/6cni3glPOtvteN+zpsw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@quansync/fs": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@quansync/fs/-/fs-0.1.5.tgz", - "integrity": "sha512-lNS9hL2aS2NZgNW7BBj+6EBl4rOf8l+tQ0eRY6JWCI8jI2kc53gSoqbjojU0OnAWhzoXiOjFyGsHcDGePB3lhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "quansync": "^0.2.11" - }, - "funding": { - "url": "https://github.com/sponsors/sxzz" - } - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-beta.51", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.51.tgz", - "integrity": "sha512-Ctn8FUXKWWQI9pWC61P1yumS9WjQtelNS9riHwV7oCkknPGaAry4o7eFx2KgoLMnI2BgFJYpW7Im8/zX3BuONg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-beta.51", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.51.tgz", - "integrity": "sha512-EL1aRW2Oq15ShUEkBPsDtLMO8GTqfb/ktM/dFaVzXKQiEE96Ss6nexMgfgQrg8dGnNpndFyffVDb5IdSibsu1g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-beta.51", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.51.tgz", - "integrity": "sha512-uGtYKlFen9pMIPvkHPWZVDtmYhMQi5g5Ddsndg1gf3atScKYKYgs5aDP4DhHeTwGXQglhfBG7lEaOIZ4UAIWww==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-beta.51", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.51.tgz", - "integrity": "sha512-JRoVTQtHYbZj1P07JLiuTuXjiBtIa7ag7/qgKA6CIIXnAcdl4LrOf7nfDuHPJcuRKaP5dzecMgY99itvWfmUFQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-beta.51", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.51.tgz", - "integrity": "sha512-BKATVnpPZ0TYBW9XfDwyd4kPGgvf964HiotIwUgpMrFOFYWqpZ+9ONNzMV4UFAYC7Hb5C2qgYQk/qj2OnAd4RQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-beta.51", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.51.tgz", - "integrity": "sha512-xLd7da5jkfbVsBCm1buIRdWtuXY8+hU3+6ESXY/Tk5X5DPHaifrUblhYDgmA34dQt6WyNC2kfXGgrduPEvDI6Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-beta.51", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.51.tgz", - "integrity": "sha512-EQFXTgHxxTzv3t5EmjUP/DfxzFYx9sMndfLsYaAY4DWF6KsK1fXGYsiupif6qPTViPC9eVmRm78q0pZU/kuIPg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-beta.51", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.51.tgz", - "integrity": "sha512-p5P6Xpa68w3yFaAdSzIZJbj+AfuDnMDqNSeglBXM7UlJT14Q4zwK+rV+8Mhp9MiUb4XFISZtbI/seBprhkQbiQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-beta.51", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.51.tgz", - "integrity": "sha512-sNVVyLa8HB8wkFipdfz1s6i0YWinwpbMWk5hO5S+XAYH2UH67YzUT13gs6wZTKg2x/3gtgXzYnHyF5wMIqoDAw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-beta.51", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.51.tgz", - "integrity": "sha512-e/JMTz9Q8+T3g/deEi8DK44sFWZWGKr9AOCW5e8C8SCVWzAXqYXAG7FXBWBNzWEZK0Rcwo9TQHTQ9Q0gXgdCaA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-beta.51", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.51.tgz", - "integrity": "sha512-We3LWqSu6J9s5Y0MK+N7fUiiu37aBGPG3Pc347EoaROuAwkCS2u9xJ5dpIyLW4B49CIbS3KaPmn4kTgPb3EyPw==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^1.0.7" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-beta.51", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.51.tgz", - "integrity": "sha512-fj56buHRuMM+r/cb6ZYfNjNvO/0xeFybI6cTkTROJatdP4fvmQ1NS8D/Lm10FCSDEOkqIz8hK3TGpbAThbPHsA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-ia32-msvc": { - "version": "1.0.0-beta.51", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.51.tgz", - "integrity": "sha512-fkqEqaeEx8AySXiDm54b/RdINb3C0VovzJA3osMhZsbn6FoD73H0AOIiaVAtGr6x63hefruVKTX8irAm4Jkt2w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-beta.51", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.51.tgz", - "integrity": "sha512-CWuLG/HMtrVcjKGa0C4GnuxONrku89g0+CsH8nT0SNhOtREXuzwgjIXNJImpE/A/DMf9JF+1Xkrq/YRr+F/rCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.51", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.51.tgz", - "integrity": "sha512-51/8cNXMrqWqX3o8DZidhwz1uYq0BhHDDSfVygAND1Skx5s1TDw3APSSxCMcFFedwgqGcx34gRouwY+m404BBQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/ansis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", - "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - } - }, - "node_modules/ast-kit": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz", - "integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.5", - "pathe": "^2.0.3" - }, - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/sponsors/sxzz" - } - }, - "node_modules/birpc": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.8.0.tgz", - "integrity": "sha512-Bz2a4qD/5GRhiHSwj30c/8kC8QGj12nNDwz3D4ErQ4Xhy35dsSDvF+RA/tWpjyU0pdGtSDiEk6B5fBGE1qNVhw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/diff": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", - "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/dts-resolver": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/dts-resolver/-/dts-resolver-2.1.3.tgz", - "integrity": "sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/sponsors/sxzz" - }, - "peerDependencies": { - "oxc-resolver": ">=11.0.0" - }, - "peerDependenciesMeta": { - "oxc-resolver": { - "optional": true - } - } - }, - "node_modules/empathic": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", - "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/get-tsconfig": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", - "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/hookable": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", - "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/obug": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.0.tgz", - "integrity": "sha512-uu/tgLPoa75CFA7UDkmqspKbefvZh1WMPwkU3bNr0PY746a/+xwXVgbw5co5C3GvJj3h5u8g/pbxXzI0gd1QFg==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT" - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/quansync": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", - "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/antfu" - }, - { - "type": "individual", - "url": "https://github.com/sponsors/sxzz" - } - ], - "license": "MIT" - }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/rolldown": { - "version": "1.0.0-beta.51", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.51.tgz", - "integrity": "sha512-ZRLgPlS91l4JztLYEZnmMcd3Umcla1hkXJgiEiR4HloRJBBoeaX8qogTu5Jfu36rRMVLndzqYv0h+M5gJAkUfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.98.0", - "@rolldown/pluginutils": "1.0.0-beta.51" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-beta.51", - "@rolldown/binding-darwin-arm64": "1.0.0-beta.51", - "@rolldown/binding-darwin-x64": "1.0.0-beta.51", - "@rolldown/binding-freebsd-x64": "1.0.0-beta.51", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.51", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.51", - "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.51", - "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.51", - "@rolldown/binding-linux-x64-musl": "1.0.0-beta.51", - "@rolldown/binding-openharmony-arm64": "1.0.0-beta.51", - "@rolldown/binding-wasm32-wasi": "1.0.0-beta.51", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.51", - "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.51", - "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.51" - } - }, - "node_modules/rolldown-plugin-dts": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/rolldown-plugin-dts/-/rolldown-plugin-dts-0.18.0.tgz", - "integrity": "sha512-2CJtKYa9WPClZxkJeCt4bGUegQvQKQ1VJp9jFJzG0h8I/80XI6qDgoWfVJUOEhT2swbsRQh/42N1RIWvbXT4rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/generator": "^7.28.5", - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", - "ast-kit": "^2.2.0", - "birpc": "^2.8.0", - "dts-resolver": "^2.1.3", - "get-tsconfig": "^4.13.0", - "magic-string": "^0.30.21", - "obug": "^2.0.0" - }, - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/sponsors/sxzz" - }, - "peerDependencies": { - "@ts-macro/tsc": "^0.3.6", - "@typescript/native-preview": ">=7.0.0-dev.20250601.1", - "rolldown": "^1.0.0-beta.51", - "typescript": "^5.0.0", - "vue-tsc": "~3.1.0" - }, - "peerDependenciesMeta": { - "@ts-macro/tsc": { - "optional": true - }, - "@typescript/native-preview": { - "optional": true - }, - "typescript": { - "optional": true - }, - "vue-tsc": { - "optional": true - } - } - }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } - }, - "node_modules/tsdown": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/tsdown/-/tsdown-0.16.6.tgz", - "integrity": "sha512-g3xHEnGdfwJTlXhEkqww3Q/KlCfyNFw4rnzuQ9Gqw8T2xjDYrw94qmSw5wYYTAW5zV1sEfWDlfgxZo5mmtu0NQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansis": "^4.2.0", - "cac": "^6.7.14", - "chokidar": "^4.0.3", - "diff": "^8.0.2", - "empathic": "^2.0.0", - "hookable": "^5.5.3", - "obug": "^2.1.0", - "rolldown": "1.0.0-beta.51", - "rolldown-plugin-dts": "^0.18.0", - "semver": "^7.7.3", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tree-kill": "^1.2.2", - "unconfig-core": "^7.4.1", - "unrun": "^0.2.11" - }, - "bin": { - "tsdown": "dist/run.mjs" - }, - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/sponsors/sxzz" - }, - "peerDependencies": { - "@arethetypeswrong/core": "^0.18.1", - "@vitejs/devtools": "^0.0.0-alpha.17", - "publint": "^0.3.0", - "typescript": "^5.0.0", - "unplugin-lightningcss": "^0.4.0", - "unplugin-unused": "^0.5.0" - }, - "peerDependenciesMeta": { - "@arethetypeswrong/core": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "publint": { - "optional": true - }, - "typescript": { - "optional": true - }, - "unplugin-lightningcss": { - "optional": true - }, - "unplugin-unused": { - "optional": true - } - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/unconfig-core": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/unconfig-core/-/unconfig-core-7.4.1.tgz", - "integrity": "sha512-Bp/bPZjV2Vl/fofoA2OYLSnw1Z0MOhCX7zHnVCYrazpfZvseBbGhwcNQMxsg185Mqh7VZQqK3C8hFG/Dyng+yA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@quansync/fs": "^0.1.5", - "quansync": "^0.2.11" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/unrun": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/unrun/-/unrun-0.2.11.tgz", - "integrity": "sha512-HjUuNLRGfRxMvxkwOuO/CpkSzdizTPPApbarLplsTzUm8Kex+nS9eomKU1qgVus6WGWkDYhtf/mgNxGEpyTR6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/runtime": "^0.96.0", - "rolldown": "1.0.0-beta.51" - }, - "bin": { - "unrun": "dist/cli.mjs" - }, - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/sponsors/Gugustinette" - }, - "peerDependencies": { - "synckit": "^0.11.11" - }, - "peerDependenciesMeta": { - "synckit": { - "optional": true - } - } - } - } + "name": "howler", + "version": "3.0.0-alpha.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "howler", + "version": "3.0.0-alpha.1", + "license": "MIT", + "devDependencies": { + "@arethetypeswrong/core": "~0.18.2", + "@biomejs/biome": "2.3.7", + "publint": "~0.3.15", + "tsdown": "~0.16.6", + "typescript": "~5.9.3" + } + }, + "node_modules/@andrewbranch/untar.js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@andrewbranch/untar.js/-/untar.js-1.0.3.tgz", + "integrity": "sha512-Jh15/qVmrLGhkKJBdXlK1+9tY4lZruYjsgkDFj08ZmDiWVBLJcqkok7Z0/R0In+i1rScBpJlSvrTS2Lm41Pbnw==", + "dev": true + }, + "node_modules/@arethetypeswrong/core": { + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@arethetypeswrong/core/-/core-0.18.2.tgz", + "integrity": "sha512-GiwTmBFOU1/+UVNqqCGzFJYfBXEytUkiI+iRZ6Qx7KmUVtLm00sYySkfe203C9QtPG11yOz1ZaMek8dT/xnlgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@andrewbranch/untar.js": "^1.0.3", + "@loaderkit/resolve": "^1.0.2", + "cjs-module-lexer": "^1.2.3", + "fflate": "^0.8.2", + "lru-cache": "^11.0.1", + "semver": "^7.5.4", + "typescript": "5.6.1-rc", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@arethetypeswrong/core/node_modules/typescript": { + "version": "5.6.1-rc", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.1-rc.tgz", + "integrity": "sha512-E3b2+1zEFu84jB0YQi9BORDjz9+jGbwwy1Zi3G0LUNw7a7cePUrHMRNy8aPh53nXpkFGVHSxIZo5vKTfYaFiBQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.7.tgz", + "integrity": "sha512-CTbAS/jNAiUc6rcq94BrTB8z83O9+BsgWj2sBCQg9rD6Wkh2gjfR87usjx0Ncx0zGXP1NKgT7JNglay5Zfs9jw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.3.7", + "@biomejs/cli-darwin-x64": "2.3.7", + "@biomejs/cli-linux-arm64": "2.3.7", + "@biomejs/cli-linux-arm64-musl": "2.3.7", + "@biomejs/cli-linux-x64": "2.3.7", + "@biomejs/cli-linux-x64-musl": "2.3.7", + "@biomejs/cli-win32-arm64": "2.3.7", + "@biomejs/cli-win32-x64": "2.3.7" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.7.tgz", + "integrity": "sha512-LirkamEwzIUULhXcf2D5b+NatXKeqhOwilM+5eRkbrnr6daKz9rsBL0kNZ16Hcy4b8RFq22SG4tcLwM+yx/wFA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.7.tgz", + "integrity": "sha512-Q4TO633kvrMQkKIV7wmf8HXwF0dhdTD9S458LGE24TYgBjSRbuhvio4D5eOQzirEYg6eqxfs53ga/rbdd8nBKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.7.tgz", + "integrity": "sha512-inHOTdlstUBzgjDcx0ge71U4SVTbwAljmkfi3MC5WzsYCRhancqfeL+sa4Ke6v2ND53WIwCFD5hGsYExoI3EZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.7.tgz", + "integrity": "sha512-/afy8lto4CB8scWfMdt+NoCZtatBUF62Tk3ilWH2w8ENd5spLhM77zKlFZEvsKJv9AFNHknMl03zO67CiklL2Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.7.tgz", + "integrity": "sha512-fJMc3ZEuo/NaMYo5rvoWjdSS5/uVSW+HPRQujucpZqm2ZCq71b8MKJ9U4th9yrv2L5+5NjPF0nqqILCl8HY/fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.7.tgz", + "integrity": "sha512-CQUtgH1tIN6e5wiYSJqzSwJumHYolNtaj1dwZGCnZXm2PZU1jOJof9TsyiP3bXNDb+VOR7oo7ZvY01If0W3iFQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.7.tgz", + "integrity": "sha512-aJAE8eCNyRpcfx2JJAtsPtISnELJ0H4xVVSwnxm13bzI8RwbXMyVtxy2r5DV1xT3WiSP+7LxORcApWw0LM8HiA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.7.tgz", + "integrity": "sha512-pulzUshqv9Ed//MiE8MOUeeEkbkSHVDVY5Cz5wVAnH1DUqliCQG3j6s1POaITTFqFfo7AVIx2sWdKpx/GS+Nqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@braidai/lang": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@braidai/lang/-/lang-1.1.2.tgz", + "integrity": "sha512-qBcknbBufNHlui137Hft8xauQMTZDKdophmLFv05r2eNmdIv/MlPuP4TdUknHG68UdWLgVZwgxVe735HzJNIwA==", + "dev": true, + "license": "ISC" + }, + "node_modules/@emnapi/core": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", + "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@loaderkit/resolve": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@loaderkit/resolve/-/resolve-1.0.4.tgz", + "integrity": "sha512-rJzYKVcV4dxJv+vW6jlvagF8zvGxHJ2+HTr1e2qOejfmGhAApgJHl8Aog4mMszxceTRiKTTbnpgmTO1bEZHV/A==", + "dev": true, + "license": "ISC", + "dependencies": { + "@braidai/lang": "^1.0.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", + "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@tybys/wasm-util": "^0.10.1" + } + }, + "node_modules/@oxc-project/runtime": { + "version": "0.96.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.96.0.tgz", + "integrity": "sha512-34lh4o9CcSw09Hx6fKihPu85+m+4pmDlkXwJrLvN5nMq5JrcGhhihVM415zDqT8j8IixO1PYYdQZRN4SwQCncg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.98.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.98.0.tgz", + "integrity": "sha512-Vzmd6FsqVuz5HQVcRC/hrx7Ujo3WEVeQP7C2UNP5uy1hUY4SQvMB+93jxkI1KRHz9a/6cni3glPOtvteN+zpsw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@publint/pack": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@publint/pack/-/pack-0.1.2.tgz", + "integrity": "sha512-S+9ANAvUmjutrshV4jZjaiG8XQyuJIZ8a4utWmN/vW1sgQ9IfBnPndwkmQYw53QmouOIytT874u65HEmu6H5jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://bjornlu.com/sponsor" + } + }, + "node_modules/@quansync/fs": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@quansync/fs/-/fs-0.1.5.tgz", + "integrity": "sha512-lNS9hL2aS2NZgNW7BBj+6EBl4rOf8l+tQ0eRY6JWCI8jI2kc53gSoqbjojU0OnAWhzoXiOjFyGsHcDGePB3lhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "quansync": "^0.2.11" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.51.tgz", + "integrity": "sha512-Ctn8FUXKWWQI9pWC61P1yumS9WjQtelNS9riHwV7oCkknPGaAry4o7eFx2KgoLMnI2BgFJYpW7Im8/zX3BuONg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.51.tgz", + "integrity": "sha512-EL1aRW2Oq15ShUEkBPsDtLMO8GTqfb/ktM/dFaVzXKQiEE96Ss6nexMgfgQrg8dGnNpndFyffVDb5IdSibsu1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.51.tgz", + "integrity": "sha512-uGtYKlFen9pMIPvkHPWZVDtmYhMQi5g5Ddsndg1gf3atScKYKYgs5aDP4DhHeTwGXQglhfBG7lEaOIZ4UAIWww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.51.tgz", + "integrity": "sha512-JRoVTQtHYbZj1P07JLiuTuXjiBtIa7ag7/qgKA6CIIXnAcdl4LrOf7nfDuHPJcuRKaP5dzecMgY99itvWfmUFQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.51.tgz", + "integrity": "sha512-BKATVnpPZ0TYBW9XfDwyd4kPGgvf964HiotIwUgpMrFOFYWqpZ+9ONNzMV4UFAYC7Hb5C2qgYQk/qj2OnAd4RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.51.tgz", + "integrity": "sha512-xLd7da5jkfbVsBCm1buIRdWtuXY8+hU3+6ESXY/Tk5X5DPHaifrUblhYDgmA34dQt6WyNC2kfXGgrduPEvDI6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.51.tgz", + "integrity": "sha512-EQFXTgHxxTzv3t5EmjUP/DfxzFYx9sMndfLsYaAY4DWF6KsK1fXGYsiupif6qPTViPC9eVmRm78q0pZU/kuIPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.51.tgz", + "integrity": "sha512-p5P6Xpa68w3yFaAdSzIZJbj+AfuDnMDqNSeglBXM7UlJT14Q4zwK+rV+8Mhp9MiUb4XFISZtbI/seBprhkQbiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.51.tgz", + "integrity": "sha512-sNVVyLa8HB8wkFipdfz1s6i0YWinwpbMWk5hO5S+XAYH2UH67YzUT13gs6wZTKg2x/3gtgXzYnHyF5wMIqoDAw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.51.tgz", + "integrity": "sha512-e/JMTz9Q8+T3g/deEi8DK44sFWZWGKr9AOCW5e8C8SCVWzAXqYXAG7FXBWBNzWEZK0Rcwo9TQHTQ9Q0gXgdCaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.51.tgz", + "integrity": "sha512-We3LWqSu6J9s5Y0MK+N7fUiiu37aBGPG3Pc347EoaROuAwkCS2u9xJ5dpIyLW4B49CIbS3KaPmn4kTgPb3EyPw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.0.7" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.51.tgz", + "integrity": "sha512-fj56buHRuMM+r/cb6ZYfNjNvO/0xeFybI6cTkTROJatdP4fvmQ1NS8D/Lm10FCSDEOkqIz8hK3TGpbAThbPHsA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-ia32-msvc": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.51.tgz", + "integrity": "sha512-fkqEqaeEx8AySXiDm54b/RdINb3C0VovzJA3osMhZsbn6FoD73H0AOIiaVAtGr6x63hefruVKTX8irAm4Jkt2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.51.tgz", + "integrity": "sha512-CWuLG/HMtrVcjKGa0C4GnuxONrku89g0+CsH8nT0SNhOtREXuzwgjIXNJImpE/A/DMf9JF+1Xkrq/YRr+F/rCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.51.tgz", + "integrity": "sha512-51/8cNXMrqWqX3o8DZidhwz1uYq0BhHDDSfVygAND1Skx5s1TDw3APSSxCMcFFedwgqGcx34gRouwY+m404BBQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/ast-kit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz", + "integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "pathe": "^2.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/birpc": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.8.0.tgz", + "integrity": "sha512-Bz2a4qD/5GRhiHSwj30c/8kC8QGj12nNDwz3D4ErQ4Xhy35dsSDvF+RA/tWpjyU0pdGtSDiEk6B5fBGE1qNVhw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", + "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dts-resolver": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/dts-resolver/-/dts-resolver-2.1.3.tgz", + "integrity": "sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "oxc-resolver": ">=11.0.0" + }, + "peerDependenciesMeta": { + "oxc-resolver": { + "optional": true + } + } + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/obug": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.0.tgz", + "integrity": "sha512-uu/tgLPoa75CFA7UDkmqspKbefvZh1WMPwkU3bNr0PY746a/+xwXVgbw5co5C3GvJj3h5u8g/pbxXzI0gd1QFg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/package-manager-detector": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.5.0.tgz", + "integrity": "sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/publint": { + "version": "0.3.15", + "resolved": "https://registry.npmjs.org/publint/-/publint-0.3.15.tgz", + "integrity": "sha512-xPbRAPW+vqdiaKy5sVVY0uFAu3LaviaPO3pZ9FaRx59l9+U/RKR1OEbLhkug87cwiVKxPXyB4txsv5cad67u+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@publint/pack": "^0.1.2", + "package-manager-detector": "^1.3.0", + "picocolors": "^1.1.1", + "sade": "^1.8.1" + }, + "bin": { + "publint": "src/cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://bjornlu.com/sponsor" + } + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-beta.51", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.51.tgz", + "integrity": "sha512-ZRLgPlS91l4JztLYEZnmMcd3Umcla1hkXJgiEiR4HloRJBBoeaX8qogTu5Jfu36rRMVLndzqYv0h+M5gJAkUfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.98.0", + "@rolldown/pluginutils": "1.0.0-beta.51" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-beta.51", + "@rolldown/binding-darwin-arm64": "1.0.0-beta.51", + "@rolldown/binding-darwin-x64": "1.0.0-beta.51", + "@rolldown/binding-freebsd-x64": "1.0.0-beta.51", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.51", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.51", + "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.51", + "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.51", + "@rolldown/binding-linux-x64-musl": "1.0.0-beta.51", + "@rolldown/binding-openharmony-arm64": "1.0.0-beta.51", + "@rolldown/binding-wasm32-wasi": "1.0.0-beta.51", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.51", + "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.51", + "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.51" + } + }, + "node_modules/rolldown-plugin-dts": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/rolldown-plugin-dts/-/rolldown-plugin-dts-0.18.0.tgz", + "integrity": "sha512-2CJtKYa9WPClZxkJeCt4bGUegQvQKQ1VJp9jFJzG0h8I/80XI6qDgoWfVJUOEhT2swbsRQh/42N1RIWvbXT4rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/generator": "^7.28.5", + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "ast-kit": "^2.2.0", + "birpc": "^2.8.0", + "dts-resolver": "^2.1.3", + "get-tsconfig": "^4.13.0", + "magic-string": "^0.30.21", + "obug": "^2.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@ts-macro/tsc": "^0.3.6", + "@typescript/native-preview": ">=7.0.0-dev.20250601.1", + "rolldown": "^1.0.0-beta.51", + "typescript": "^5.0.0", + "vue-tsc": "~3.1.0" + }, + "peerDependenciesMeta": { + "@ts-macro/tsc": { + "optional": true + }, + "@typescript/native-preview": { + "optional": true + }, + "typescript": { + "optional": true + }, + "vue-tsc": { + "optional": true + } + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tsdown": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/tsdown/-/tsdown-0.16.6.tgz", + "integrity": "sha512-g3xHEnGdfwJTlXhEkqww3Q/KlCfyNFw4rnzuQ9Gqw8T2xjDYrw94qmSw5wYYTAW5zV1sEfWDlfgxZo5mmtu0NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansis": "^4.2.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "diff": "^8.0.2", + "empathic": "^2.0.0", + "hookable": "^5.5.3", + "obug": "^2.1.0", + "rolldown": "1.0.0-beta.51", + "rolldown-plugin-dts": "^0.18.0", + "semver": "^7.7.3", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tree-kill": "^1.2.2", + "unconfig-core": "^7.4.1", + "unrun": "^0.2.11" + }, + "bin": { + "tsdown": "dist/run.mjs" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@arethetypeswrong/core": "^0.18.1", + "@vitejs/devtools": "^0.0.0-alpha.17", + "publint": "^0.3.0", + "typescript": "^5.0.0", + "unplugin-lightningcss": "^0.4.0", + "unplugin-unused": "^0.5.0" + }, + "peerDependenciesMeta": { + "@arethetypeswrong/core": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "publint": { + "optional": true + }, + "typescript": { + "optional": true + }, + "unplugin-lightningcss": { + "optional": true + }, + "unplugin-unused": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unconfig-core": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/unconfig-core/-/unconfig-core-7.4.1.tgz", + "integrity": "sha512-Bp/bPZjV2Vl/fofoA2OYLSnw1Z0MOhCX7zHnVCYrazpfZvseBbGhwcNQMxsg185Mqh7VZQqK3C8hFG/Dyng+yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@quansync/fs": "^0.1.5", + "quansync": "^0.2.11" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/unrun": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/unrun/-/unrun-0.2.11.tgz", + "integrity": "sha512-HjUuNLRGfRxMvxkwOuO/CpkSzdizTPPApbarLplsTzUm8Kex+nS9eomKU1qgVus6WGWkDYhtf/mgNxGEpyTR6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/runtime": "^0.96.0", + "rolldown": "1.0.0-beta.51" + }, + "bin": { + "unrun": "dist/cli.mjs" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/Gugustinette" + }, + "peerDependencies": { + "synckit": "^0.11.11" + }, + "peerDependenciesMeta": { + "synckit": { + "optional": true + } + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + } + } } diff --git a/package.json b/package.json index 6281b0c9..cfcf66cc 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "author": "James Simpson (http://goldfirestudios.com)", "repository": { "type": "git", - "url": "git://github.com/goldfire/howler.js.git" + "url": "git+https://github.com/goldfire/howler.js.git" }, "scripts": { "build": "tsdown", @@ -29,7 +29,9 @@ "release": "npm run build && git add dist && git commit -m 'build: update dist files' && npm publish" }, "devDependencies": { + "@arethetypeswrong/core": "~0.18.2", "@biomejs/biome": "2.3.7", + "publint": "~0.3.15", "tsdown": "~0.16.6", "typescript": "~5.9.3" }, @@ -38,12 +40,12 @@ "types": "dist/index.d.ts", "exports": { ".": { - "import": "./dist/index.js", - "types": "./dist/index.d.ts" + "types": "./dist/index.d.ts", + "import": "./dist/index.js" }, "./plugins/spatial": { - "import": "./dist/plugins/spatial.js", - "types": "./dist/plugins/spatial.d.ts" + "types": "./dist/plugins/spatial.d.ts", + "import": "./dist/plugins/spatial.js" } }, "license": "MIT", diff --git a/scripts/check-size.js b/scripts/check-size.js index c762eabb..25ae7433 100755 --- a/scripts/check-size.js +++ b/scripts/check-size.js @@ -9,19 +9,15 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const rootDir = join(__dirname, '..'); -// Files to check - core library files +// Files to check - core library files (now bundled into index.js) const coreFiles = [ 'dist/index.js', - 'dist/howler.core.js', - 'dist/howler-global.js', - 'dist/howl.js', - 'dist/sound.js', + 'dist/howler.core.js', ]; -// Plugin files +// Plugin files (now at root level due to bundling config) const pluginFiles = [ 'dist/plugins/spatial.js', - 'dist/plugins/spatial-plugin.js', ]; // All files for total calculation diff --git a/tsdown.config.ts b/tsdown.config.ts index 9897c1b4..cd5b6865 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -1,12 +1,24 @@ import { defineConfig } from "tsdown"; export default defineConfig({ - entry: "src/**/*.ts", + entry: ["src/index.ts", "src/plugins/spatial.ts"], format: "esm", platform: "browser", - unbundle: true, + unbundle: false, // Bundle core library, but plugins stay separate + hash: false, treeshake: true, - minify: true, + minify: { + compress: true, + mangle: true, + }, + publint: true, + attw: { + profile: "esmOnly", + }, + report: true, + banner: { + js: "// howler.js v3.0.0-alpha.1\n// howlerjs.com\n// (c) 2013-2025, James Simpson of GoldFire Studios\n// goldfirestudios.com\n// MIT License\n", + }, // Target browsers that support ES modules natively // Chrome 61+, Firefox 60+, Safari 11+, Edge 16+, Opera 48+ target: ["chrome61", "firefox60", "safari11", "edge16", "opera48"], From 0178463f7b2bd5877750abfbdc864cc2fdc5bdfb Mon Sep 17 00:00:00 2001 From: eatsjobs Date: Sun, 23 Nov 2025 02:17:13 +0100 Subject: [PATCH 24/25] Update pull request template formatting for consistency - Changed section headers from '###' to '##' for improved readability and uniformity in the pull request template. - Ensured the template aligns with standard markdown practices for better presentation. These updates aim to enhance the clarity and usability of the pull request template. --- .github/PULL_REQUEST_TEMPLATE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9d4ed7af..73bcbc0a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,10 +1,10 @@ -### Issue/Feature +# Issue/Feature -### Related Issues +## Related Issues ### Solution From e0199f2df1c294d9dbd432ee2bbd4190140f5f3a Mon Sep 17 00:00:00 2001 From: eatsjobs Date: Sun, 23 Nov 2025 02:37:22 +0100 Subject: [PATCH 25/25] move global cache for decoded audio buffers into separate file - Introduced a new `cache.ts` file to implement a global cache for decoded audio buffers, allowing multiple Howl instances to share audio data. - Updated imports in `howl.ts`, `howler.core.ts`, and `audio-loader.ts` to utilize the new cache implementation, enhancing audio data management and performance. These changes aim to improve audio handling efficiency and reduce memory usage across Howl instances. --- src/cache.ts | 7 +++++++ src/helpers/audio-loader.ts | 3 +-- src/howl.ts | 4 ++-- src/howler.core.ts | 4 ++-- src/types.ts | 8 +------- 5 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 src/cache.ts diff --git a/src/cache.ts b/src/cache.ts new file mode 100644 index 00000000..b5207509 --- /dev/null +++ b/src/cache.ts @@ -0,0 +1,7 @@ +/** + * Global cache for decoded audio buffers. + * This allows multiple Howl instances to share the same audio data. + * + * @internal + */ +export const cache: Record = {}; \ No newline at end of file diff --git a/src/helpers/audio-loader.ts b/src/helpers/audio-loader.ts index f27b185e..3554717f 100644 --- a/src/helpers/audio-loader.ts +++ b/src/helpers/audio-loader.ts @@ -1,7 +1,7 @@ +import { cache } from "../cache"; import type { Howl } from "../howler.core"; import { Howler } from "../howler.core"; import { globalPluginManager } from "../plugins/plugin"; -import { cache } from "../types"; export const loadBuffer = (self: Howl) => { const url = self._src as string; @@ -60,7 +60,6 @@ export const loadBuffer = (self: Howl) => { } }; - export const decodeAudioData = (arraybuffer: ArrayBuffer, self: Howl) => { const error = () => { self._emit("loaderror", null, "Decoding audio data failed."); diff --git a/src/howl.ts b/src/howl.ts index b853f9cc..1d43223f 100644 --- a/src/howl.ts +++ b/src/howl.ts @@ -1,3 +1,4 @@ +import { cache } from "./cache"; import { setupAudioContext } from "./helpers/audio-context"; import { loadBuffer } from "./helpers/audio-loader"; import { isAppleVendor } from "./helpers/light-ua-parser"; @@ -6,13 +7,12 @@ import { globalPluginManager } from "./plugins/plugin"; import { Sound } from "./sound"; import { type AudioBufferSourceNodeWithLegacy, - cache, type EventListener, type HowlOptions, type HTMLAudioElementWithUnlocked, isGainNode, isHTMLAudioElement, - type QueueItem + type QueueItem, } from "./types"; class Howl { diff --git a/src/howler.core.ts b/src/howler.core.ts index 8f545561..f57fc47d 100644 --- a/src/howler.core.ts +++ b/src/howler.core.ts @@ -17,6 +17,6 @@ export type { WindowWithAudio } from "./types"; // Export for ESM - explicit exports for better tree-shaking -export { cache, isGainNode, isHTMLAudioElement } from "./types"; +export { cache } from "./cache"; +export { isGainNode, isHTMLAudioElement } from "./types"; export { Howl, Howler, HowlerGlobal, Sound }; - diff --git a/src/types.ts b/src/types.ts index dba686d7..f47cad37 100644 --- a/src/types.ts +++ b/src/types.ts @@ -97,13 +97,7 @@ export interface QueueItem { action: () => void; } -/** - * Global cache for decoded audio buffers. - * This allows multiple Howl instances to share the same audio data. - * - * @internal - */ -export const cache: Record = {}; + /** * Extended HTMLAudioElement with custom properties used by Howler.