--- howler.orig.js Fri May 23 11:52:20 2025 +++ howler.js Sun May 25 12:46:31 2025 @@ -1,11 +1,12 @@ -/*! - * howler.js v2.2.3 - * howlerjs.com +/*! + * @file howler.js v2.2.3 | howlerjs.com + * @version 2.2.3-planeptune + * @author James Simpson + * @copyright 2013-2020, James Simpson of GoldFire Studios | goldfirestudios.com + * @license MIT * - * (c) 2013-2020, James Simpson of GoldFire Studios - * goldfirestudios.com - * - * MIT License + * This version of howler has been modified by zefie, for the Planeptune Loop Player ("Planeptune Edition"), and minified using uglify-js v3.x + * See https://loops.planeptune.org/geshi/source/js/howler.diff.txt for modifications to "Planeptune Edition" */ (function() { @@ -29,6 +30,7 @@ */ init: function() { var self = this || Howler; + console.log('howler v2.2.3-planeptune initialized'); // Create a global ID counter. self._counter = 1000; @@ -61,6 +63,37 @@ return self; }, + /** + * Check if this browser is Internet Explorer. + * Returns the version if so, otherwise returns false + */ + + detectIE: function () { + var ua = window.navigator.userAgent; + + var msie = ua.indexOf('MSIE '); + if (msie > 0) { + // IE 10 or older => return version number + return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10); + } + + var trident = ua.indexOf('Trident/'); + if (trident > 0) { + // IE 11 => return version number + var rv = ua.indexOf('rv:'); + return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10); + } + + var edge = ua.indexOf('Edge/'); + if (edge > 0) { + // Edge (IE 12+) => return version number + return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10); + } + + // other browser + return false; + }, + /** * Get/set the global volume for all sounds. * @param {Float} vol Volume from 0.0 to 1.0. @@ -84,7 +117,7 @@ } // When using Web Audio, we just need to adjust the master gain. - if (self.usingWebAudio) { + if (self._webAudio && !self._html5) { self.masterGain.gain.setValueAtTime(vol, Howler.ctx.currentTime); } @@ -126,7 +159,7 @@ self._muted = muted; // With Web Audio, we just need to mute the master gain. - if (self.usingWebAudio) { + if (self._webAudio) { self.masterGain.gain.setValueAtTime(muted ? 0 : self._volume, Howler.ctx.currentTime); } @@ -176,7 +209,7 @@ } // Create a new AudioContext to make sure it is fully reset. - if (self.usingWebAudio && self.ctx && typeof self.ctx.close !== 'undefined') { + if (self._webAudio && self.ctx && typeof self.ctx.close !== 'undefined') { self.ctx.close(); self.ctx = null; setupAudioContext(); @@ -208,7 +241,7 @@ self._autoSuspend(); // Check if audio is available. - if (!self.usingWebAudio) { + if (!self._webAudio) { // No audio is available on this system if noAudio is set to true. if (typeof Audio !== 'undefined') { try { @@ -557,8 +590,8 @@ var self = this; // Throw an error if no source is provided. - if (!o.src || o.src.length === 0) { - console.error('An array of source files must be passed with any new Howl.'); + if (((!o.src || o.src.length === 0) && (!o.arraybuffer || !(o.arraybuffer instanceof ArrayBuffer) || !o.contenttype)) || (o.html5 && !o.src)) { + console.error('An array of source files, or an ArrayBuffer+ContentType, must be passed with any new Howl. Also, ArrayBuffer+ContentType are incompatible with html5 mode.'); return; } @@ -588,7 +621,11 @@ self._preload = (typeof o.preload === 'boolean' || o.preload === 'metadata') ? o.preload : true; self._rate = o.rate || 1; self._sprite = o.sprite || {}; + self._buffer = o.arraybuffer || null; + self._contenttype = o.contenttype || null; self._src = (typeof o.src !== 'string') ? o.src : [o.src]; + self._nextsprite = o.nextsprite || null; + self._timeroffset = o.timeroffset || 30; self._volume = o.volume !== undefined ? o.volume : 1; self._xhr = { method: o.xhr && o.xhr.method ? o.xhr.method : 'GET', @@ -598,6 +635,8 @@ // Setup all other default properties. self._duration = 0; + self._decoded = null; + self._hires = null; self._state = 'unloaded'; self._sounds = []; self._endTimers = {}; @@ -610,6 +649,7 @@ self._onload = o.onload ? [{fn: o.onload}] : []; self._onloaderror = o.onloaderror ? [{fn: o.onloaderror}] : []; self._onplayerror = o.onplayerror ? [{fn: o.onplayerror}] : []; + self._onspritechange = o.onspritechange ? [{fn: o.onspritechange}] : []; self._onpause = o.onpause ? [{fn: o.onpause}] : []; self._onplay = o.onplay ? [{fn: o.onplay}] : []; self._onstop = o.onstop ? [{fn: o.onstop}] : []; @@ -662,7 +702,14 @@ self._emit('loaderror', null, 'No audio support.'); return; } - + + if (self._buffer instanceof ArrayBuffer && !self._html5) { + if (!self._src) { + url = ['internal_buffer']; + self._src = url; + } + } + // Make sure our source is in an array. if (typeof self._src === 'string') { self._src = [self._src]; @@ -684,7 +731,11 @@ } // Extract the file extension from the URL or base64 data URI. - ext = /^data:audio\/([^;,]+);/i.exec(str); + if (self._contenttype) { + ext = self._contenttype.split("/")[1]; + } else { + ext = /^data:audio\/([^;,]+);/i.exec(str); + } if (!ext) { ext = /\.([^.]+)$/.exec(str.split('?', 1)[0]); } @@ -869,10 +920,15 @@ } else { sound._loop ? node.bufferSource.start(0, seek, 86400) : node.bufferSource.start(0, seek, duration); } + + if (Howler.detectIE() == 11) { + // IE11 Workaround where rate gets reset + node.playbackRate = sound._rate; + } // Start a new timer if none is present. if (timeout !== Infinity) { - self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout); + self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), (timeout - self._timeroffset)); } if (!internal) { @@ -1563,6 +1619,10 @@ var seek = self.seek(id[i]); var duration = ((self._sprite[sound._sprite][0] + self._sprite[sound._sprite][1]) / 1000) - seek; var timeout = (duration * 1000) / Math.abs(sound._rate); + + if (self._webAudio) { + timeout = timeout - self._timeroffset; + } // Start a new end timer if sound is already playing. if (self._endTimers[id[i]] || !sound._paused) { @@ -1639,14 +1699,44 @@ 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); + var sprite = sound._sprite; + sound._rateSeek = 0; + + var soundDuration = ((self._sprite[sprite][0] + self._sprite[sprite][1]) / 1000); + if (seek > soundDuration) { + seek = 0; + } + + if (playing && !self._webAudio) { + self.pause(id, true); } // Move the position of the track and cancel timer. sound._seek = seek; sound._ended = false; self._clearTimer(id); + + + // Restart the playback if the sound was playing. + if (playing && self._webAudio) { + var oldbuffer = sound._node.bufferSource; + self._refreshBuffer(sound); + sound._playStart = Howler.ctx.currentTime; + if (typeof sound._node.bufferSource.start === 'undefined') { + sound._node.bufferSource.noteGrainOn(0, seek); + } else { + sound._node.bufferSource.start(0, seek); + } + if (oldbuffer) { + oldbuffer.stop(); + oldbuffer = null; + } + var duration = soundDuration - seek; + var timeout = (duration * 1000) / Math.abs(sound._rate); + if (timeout !== Infinity) { + self._endTimers[id] = setTimeout(self._ended.bind(self, sound), (timeout - self._timeroffset)); + } + } // Update the seek position for HTML5 Audio. if (!self._webAudio && sound._node && !isNaN(sound._node.duration)) { @@ -1683,6 +1773,10 @@ return sound._seek + (rateSeek + realTime * Math.abs(sound._rate)); } else { return sound._node.currentTime; + // Workaround for IE11 reseting the rate + if (Howler.detectIE() == 11) { + self.rate(sound._rate,id); + } } } } @@ -1944,6 +2038,17 @@ }, /** + * Fired when playback is switched to another sprite with nextsprite + * @param {Sound} sound The sound object to work with. + * @return {Howl} + */ + + _onspritechange: function (sound) { + var self = this; + return self; + }, + + /** * Fired when playback ends at the end of the duration. * @param {Sound} sound The sound object to work with. * @return {Howl} @@ -1968,18 +2073,57 @@ // Restart the playback for HTML5 Audio loop. if (!self._webAudio && loop) { - self.stop(sound._id, true).play(sound._id); + if (self._nextsprite && sound._sprite != self._nextsprite) { + sprite = self._nextsprite; + self._nextsprite = null; + sound._sprite = sprite; // this line may be redundant + self.stop(sound._id, true).play(sprite); + self._emit('spritechange',sound); + } else { + 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); + var currpos = Math.round(self.seek() * 1000); + var wanted = self._sprite[sprite][0] + self._sprite[sprite][1]; + var offset = (currpos - wanted); + self._clearTimer(sound._id); + if (offset < -6) { + // "hi-res" mode + if (!self._hires) { + console.log("entering hi-res emulation mode"); + self._hires = setInterval(self._ended.bind(self, sound), 1); + } + return false; + } + + if (self._hires) { + console.log("exiting hi-res emulation mode"); + clearInterval(self._hires) + self._hires = null; + } + console.log("howler: end timer fired. playtime was "+currpos+"ms, expected "+wanted+"ms ("+offset+"ms off)"); + sound._rateSeek = 0; + sound._playStart = Howler.ctx.currentTime; + if (self._nextsprite && sound._sprite != self._nextsprite) { + // manually change sprite + sprite = self._nextsprite; + self._nextsprite = null; + sound._sprite = sprite; + self._emit('spritechange',sound); + sound._start = self._sprite[sprite][0] / 1000; + sound._stop = (self._sprite[sprite][0] + self._sprite[sprite][1]) / 1000; + sound._loop = loop; + sound._seek = sound._start || 0; + self.seek(sound._seek,sound._id); + } else { + self._emit('play', sound._id); + sound._seek = sound._start || 0; + var timeout = ((sound._stop - sound._start) * 1000) / Math.abs(sound._rate); + self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), (timeout - self._timeroffset)); + } } // Mark the node as paused. @@ -2378,6 +2522,11 @@ */ var loadBuffer = function(self) { var url = self._src; + + if (self._buffer instanceof ArrayBuffer && !self._html5) { + decodeAudioData(self._buffer, self); + return; + } // Check if the buffer has already been cached and use it instead. if (cache[url]) { @@ -2472,9 +2621,9 @@ // Decode the buffer into an audio source. if (typeof Promise !== 'undefined' && Howler.ctx.decodeAudioData.length === 1) { - Howler.ctx.decodeAudioData(arraybuffer).then(success).catch(error); + Howler.ctx.decodeAudioData(arraybuffer.slice(0)).then(success).catch(error); } else { - Howler.ctx.decodeAudioData(arraybuffer, success, error); + Howler.ctx.decodeAudioData(arraybuffer.slice(0), success, error); } } @@ -2496,6 +2645,7 @@ // Fire the loaded event. if (self._state !== 'loaded') { + self._decoded = buffer; self._state = 'loaded'; self._emit('load'); self._loadQueue();