UNPKG

sound-manager-ts

Version:

Lightweight TypeScript soundmanager for Web Audio API. Seamless audio control in web apps and games with full type safety and modern API

1,103 lines 81.6 kB
var U = Object.defineProperty; var H = (u, t, e) => t in u ? U(u, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : u[t] = e; var g = (u, t, e) => H(u, typeof t != "symbol" ? t + "" : t, e); var E = /* @__PURE__ */ ((u) => (u.Stereo = "stereo", u.Spatial = "spatial", u))(E || {}); class z { // Connect nodes based on the panning type connectNodes(t, e) { t.source && (this.disconnectNodes(t), t.panType === E.Spatial && t.pannerNode ? (t.source.connect(t.pannerNode), t.pannerNode.connect(t.gainNode)) : t.stereoPanner ? (t.source.connect(t.stereoPanner), t.stereoPanner.connect(t.gainNode)) : t.source.connect(t.gainNode), t.gainNode.connect(e)); } // Disconnect all nodes disconnectNodes(t) { var e; t.source && t.source.disconnect(), t.stereoPanner && t.stereoPanner.disconnect(), t.pannerNode && t.pannerNode.disconnect(), (e = t.playOptions) != null && e.createNewInstance && t.gainNode.disconnect(); } } var h = /* @__PURE__ */ ((u) => (u.ENDED = "ended", u.ERROR = "error", u.FADE_IN_COMPLETED = "fade_in_completed", u.FADE_MASTER_IN_COMPLETED = "fade_master_in_completed", u.FADE_MASTER_OUT_COMPLETED = "fade_master_out_completed", u.FADE_OUT_COMPLETED = "fade_out_completed", u.GLOBAL_SPATIAL_POSITION_CHANGED = "global_spatial_position_changed", u.LOADED = "loaded", u.LOOP_COMPLETED = "loop_completed", u.MASTER_PAN_CHANGED = "master_pan_changed", u.MASTER_VOLUME_CHANGED = "master_volume_changed", u.MUTE_GLOBAL = "mute_global", u.MUTED = "muted", u.OPTIONS_UPDATED = "options_updated", u.PAN_CHANGED = "pan_changed", u.PAN_RESET = "pan_reset", u.PAUSED = "paused", u.PLAYBACK_RATE_CHANGED = "playback_rate_changed", u.PROGRESS = "progress", u.RESET = "reset", u.RESUMED = "resumed", u.SEEKED = "seeked", u.SPATIAL_POSITION_CHANGED = "spatial_position_changed", u.SPATIAL_POSITION_RESET = "spatial_position_reset", u.SPRITE_SET = "sprite_set", u.STARTED = "started", u.STOPPED = "stopped", u.UNLOADED = "unloaded", u.UNMUTE_GLOBAL = "unmute_global", u.UNMUTED = "unmuted", u.UPDATED_URL = "updated_url", u.VOLUME_CHANGED = "volume_changed", u))(h || {}), B = /* @__PURE__ */ ((u) => (u.HRTF = "HRTF", u.EqualPower = "equalpower", u))(B || {}), j = /* @__PURE__ */ ((u) => (u.Linear = "linear", u.Inverse = "inverse", u.Exponential = "exponential", u))(j || {}); const O = { panningModel: "HRTF", distanceModel: "inverse", refDistance: 1, maxDistance: 1e4, rolloffFactor: 1, coneInnerAngle: 360, coneOuterAngle: 360, coneOuterGain: 0 }, W = { autoUnlock: !0, autoMuteOnHidden: !0, autoResumeOnFocus: !0, createNewInstance: !1, // ------- Loading Configuration: ------------------------------------------------------------- // Loading Behaviour webAudioPreferred: !0, // Whether to prefer Web Audio API (default: true) html5AudioFallback: !0, // Whether to use HTML5 Audio as fallback (default: true) maxParallelLoads: 10, // Maximum parallel sound loads (default: 10) retryDelay: 0.5, // Delay between retry attempts in seconds (default: 0.5 seconds) // Network Handling fetchRetries: 2, // Number of retries for failed fetches (default: 2) fetchTimeout: 8, // Timeout for fetch requests in seconds corsProxy: void 0, // URL of CORS proxy service fetchStrategy: "direct-first", // Security & Limits maxAudioSize: 50 * 1024 * 1024, // 50MB limit audioCache: !0, // Cache the audio file when loading. crossOrigin: null, credentialStrategy: "auto", // -----End Loading Configuration------------------------------------------------------------- debug: !1, defaultDuration: void 0, defaultPan: 0, defaultPanSpatialPosition: { x: 0, y: 0, z: 0 }, defaultPanType: E.Stereo, defaultPlaybackRate: 1, defaultStartTime: 0, defaultVolume: 1, fadeInDuration: 0.5, fadeOutDuration: 0.5, loopSounds: !1, maxLoops: -1, pannerNodeConfig: O, spatialAudio: !0, trackProgress: !0 }; var S = /* @__PURE__ */ ((u) => (u.Playing = "playing", u.Paused = "paused", u.Stopped = "stopped", u))(S || {}); class Y { constructor() { g(this, "callbacks", /* @__PURE__ */ new Map()); g(this, "animationFrameId", null); g(this, "lastTick", 0); this.tick = this.tick.bind(this); } tick(t) { const e = t - (this.lastTick || t); this.lastTick = t, this.callbacks.forEach((i) => { if (!i.interval) { i.callback(e); return; } i.lastUpdate = i.lastUpdate || t, t - i.lastUpdate >= i.interval && (i.callback(e), i.lastUpdate = t); }), this.animationFrameId = requestAnimationFrame(this.tick); } start() { this.animationFrameId || (this.lastTick = performance.now(), this.animationFrameId = requestAnimationFrame(this.tick)); } stop() { this.animationFrameId !== null && (cancelAnimationFrame(this.animationFrameId), this.animationFrameId = null); } addCallback(t, e, i) { this.callbacks.set(t, { id: t, callback: e, interval: i }), this.start(); } removeCallback(t) { this.callbacks.delete(t), this.callbacks.size === 0 && this.stop(); } clear() { this.callbacks.clear(), this.stop(); } } class X { constructor(t = {}) { g(this, "config"); g(this, "context"); g(this, "sounds", /* @__PURE__ */ new Map()); g(this, "masterGainNode"); g(this, "masterStereoPanner"); g(this, "masterPannerNode"); g(this, "previousGlobalVolume", 1); g(this, "isMuted", !1); g(this, "previousGlobalPan", 0); g(this, "PROGRESS_UPDATE_INTERVAL", 50); // 50ms default, could be configurable g(this, "eventListeners", /* @__PURE__ */ new Map()); g(this, "activeSources", /* @__PURE__ */ new Map()); g(this, "activeFadeCallbacks", /* @__PURE__ */ new Map()); g(this, "isHandlingError", !1); g(this, "audioNodeConnector", new z()); g(this, "ticker"); g(this, "lastError", null); g(this, "masterSpatialPosition", { x: 0, y: 0, z: 0 }); g(this, "_spatialAudioSupported", null); g(this, "DEFAULT_PRECISION", 2); g(this, "soundGroups", /* @__PURE__ */ new Map()); g(this, "instanceCounters", /* @__PURE__ */ new Map()); g(this, "unlockHandlers", null); g(this, "VERSION", "5.7.2"); this.ticker = new Y(), this.config = { debug: !1, ...t }, Object.values(h).forEach((e) => { this.eventListeners.set(e, /* @__PURE__ */ new Set()); }), t.defaultVolume !== void 0 && (t.defaultVolume = this.setValidatedVolume(t.defaultVolume)), t.fadeInDuration !== void 0 && t.fadeInDuration < 0 && (t.fadeInDuration = 0), t.fadeOutDuration !== void 0 && t.fadeOutDuration < 0 && (t.fadeOutDuration = 0), t.spatialAudio && !this.isSpatialAudioSupported() && (this.debugLog("Spatial audio requested but not supported, disabling feature"), t.spatialAudio = !1), this.config = { ...W, ...t }; try { const e = window.AudioContext || window.webkitAudioContext; this.context = new e({ latencyHint: "interactive" }), this.masterGainNode = this.context.createGain(), this.masterStereoPanner = this.context.createStereoPanner(), this.masterStereoPanner.connect(this.context.destination), this.masterGainNode.connect(this.masterStereoPanner), this.masterStereoPanner.pan.value = this.config.defaultPan ?? 0, this.previousGlobalPan = this.config.defaultPan ?? 0, this.masterGainNode.gain.value = this.config.defaultVolume, this.previousGlobalVolume = this.config.defaultVolume, this.setupAudioUnlock(), this.initialize(); } catch (e) { this.handleError("constructor, initialize", e); } } initialize() { this.showConsoleInfo(), this.setupContextResumeHandlers(), this.config.autoMuteOnHidden && this.setupVisibilityHandling(), this.config.spatialAudio && this.initializeSpatialAudio(), this.debugLog("Initialized with config:", this.config); } debugLog(...t) { this.config.debug && console.log("[SoundManager]", ...t); } setupVisibilityHandling() { document.addEventListener("visibilitychange", () => { document.hidden ? (this.debugLog("Page hidden, auto-muting sounds"), this.muteAllSounds()) : this.config.autoResumeOnFocus && (this.debugLog("Page visible, auto-resuming sounds"), this.unmuteAllSounds()); }); } initializeSpatialAudio() { if (!this.isSpatialAudioSupported()) { this.debugLog("Spatial audio not supported, disabling feature"), this.config.spatialAudio = !1; return; } try { const t = this.context.listener; t.positionX.setValueAtTime(0, this.context.currentTime), t.positionY.setValueAtTime(0, this.context.currentTime), t.positionZ.setValueAtTime(0, this.context.currentTime), t.forwardX.setValueAtTime(0, this.context.currentTime), t.forwardY.setValueAtTime(0, this.context.currentTime), t.forwardZ.setValueAtTime(-1, this.context.currentTime), t.upX.setValueAtTime(0, this.context.currentTime), t.upY.setValueAtTime(1, this.context.currentTime), t.upZ.setValueAtTime(0, this.context.currentTime), this.debugLog("Spatial audio initialized"); } catch (t) { this.handleError("initializing spatial audio", t); } } setupAudioUnlock() { if (!this.config.autoUnlock || (this.removeUnlockListeners(), !this.isMobileLikeEnvironment())) return; let t = !1; const e = async () => { if (!(t || this.context.state !== "suspended")) try { const a = this.context.createBuffer(1, 1, 22050), n = this.context.createBufferSource(); n.buffer = a, n.connect(this.context.destination), n.start(0, 0, 0.1), await this.context.resume(), t = !0, this.removeUnlockListeners(), this.debugLog("Audio context successfully unlocked"); } catch (a) { this.debugLog("Audio unlock attempt failed:", a); } }, i = () => e(), o = () => e(); this.unlockHandlers = { touchstart: i, touchend: i, click: o }; const s = { passive: !0, capture: !0 }; document.addEventListener("touchstart", this.unlockHandlers.touchstart, s), document.addEventListener("touchend", this.unlockHandlers.touchend, s), document.addEventListener("click", this.unlockHandlers.click, s), e(); } removeUnlockListeners() { this.unlockHandlers && (document.removeEventListener("touchstart", this.unlockHandlers.touchstart, !0), document.removeEventListener("touchend", this.unlockHandlers.touchend, !0), document.removeEventListener("click", this.unlockHandlers.click, !0), this.unlockHandlers = null); } isMobileLikeEnvironment() { return "ontouchstart" in window || navigator.maxTouchPoints > 0 || /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); } setupContextResumeHandlers() { const t = async () => { if (this.context.state === "suspended") try { await this.context.resume(), this.debugLog("AudioContext resumed after user interaction"), ["click", "touchstart", "keydown"].forEach((e) => { document.removeEventListener(e, t); }); } catch (e) { this.debugLog("Failed to resume AudioContext:", e); } }; ["click", "touchstart", "keydown"].forEach((e) => { document.addEventListener(e, t, { once: !0 }); }), this.debugLog("Context resume handlers set up, waiting for user interaction"); } setupAudioSource(t) { var o, s, a; const e = ((o = t.playOptions) == null ? void 0 : o.playbackRate) ?? 1, i = this.context.createBufferSource(); if (i.buffer = t.buffer, t.source = i, i.playbackRate.setValueAtTime(e, this.context.currentTime), ((s = t.playOptions) == null ? void 0 : s.pan) !== void 0 && t.panType !== E.Spatial && this.setPan(t.id, t.playOptions.pan, !0), (a = t.playOptions) != null && a.panSpatialPosition && (t.playOptions.panSpatialPosition.x !== 0 || t.playOptions.panSpatialPosition.y !== 0 || t.playOptions.panSpatialPosition.z !== 0)) { const n = t.playOptions.panSpatialPosition; this.setSpatialPosition(n.x, n.y, n.z, t.id, void 0, !0); } return this.audioNodeConnector.connectNodes(t, this.masterGainNode), this.activeSources.set(t.id, i), i.onended = () => { var n; this.debugLog(`Sound ${t.id} ended naturally`), t.state === S.Playing && ((n = t.playOptions) != null && n.loop ? this.handleLoopIteration(t) : this.handleSoundEnded(t)); }, i; } handleLoopIteration(t) { var i, o, s, a, n, r, d; if (this.debugLog(`Restarting loop for sound ${t.id}`), !((i = t.playOptions) != null && i.loop)) { this.debugLog(`Loop was disabled for ${t.id}, handling as ended`), this.handleSoundEnded(t); return; } if (t.state !== S.Playing) { this.debugLog(`Sound ${t.id} is no longer playing (state: ${t.state}), skipping loop iteration`), this.handleSoundEnded(t); return; } if (((o = t.playOptions) == null ? void 0 : o.maxLoops) !== void 0 && ((s = t.playOptions) == null ? void 0 : s.maxLoops) > 0 && (t.currentLoopCount ?? 0) >= ((a = t.playOptions) == null ? void 0 : a.maxLoops) - 1) { this.debugLog(`Max loops reached for ${t.id}, stopping`), t.currentLoopCount = 0, this.stop(t.id); return; } t.currentLoopCount = (t.currentLoopCount ?? 0) + 1, this.debugLog(`Loop count: ${t.currentLoopCount}`); const e = (((n = t.playOptions) == null ? void 0 : n.startTime) ?? 0) / (((r = t.playOptions) == null ? void 0 : r.playbackRate) ?? 1); (d = t.playOptions) != null && d.createNewInstance && (t.playOptions.createNewInstance = !1), this.seek(t.id, e, !0), this.dispatchEvent({ type: h.LOOP_COMPLETED, soundId: t.id, timestamp: this.context.currentTime, sound: t }); } handleSoundEnded(t) { var i, o, s, a, n, r; if (t.state === S.Stopped) return; if ((i = t.playOptions) != null && i.pauseAtDurationReached && ((o = t.playOptions) == null ? void 0 : o.duration) !== void 0 && t.playOptions.duration > 0) { this.pause(t.id), t.startTime = void 0, t.pausedAt = ((s = t.playOptions) == null ? void 0 : s.startTime) ?? 0, t.currentTime = 0; return; } t.state = S.Stopped, t.startTime = void 0, t.pausedAt = ((a = t.playOptions) == null ? void 0 : a.startTime) ?? 0, t.currentTime = 0, (n = t.playOptions) != null && n.createNewInstance ? this.cleanupExistingSource(t.id) : this.cleanupSound(t.id), this.stopProgressTracking(t.id), (r = t.playOptions) != null && r.createNewInstance && this.removeEventListenersForInstance(t.id); const e = t.id.includes(":") ? t.id.split(":")[0] : t.id; this.dispatchEvent({ type: h.ENDED, soundId: t.id, originalId: e, instanceId: t.id, timestamp: this.context.currentTime, sound: t }); } scheduleFadeOut(t, e, i) { if (!this.sounds.get(t)) return; const s = this.context.currentTime + e, a = () => { this.fadeOut(t, i); }; this.ticker.addCallback(`fadeOut_${t}`, () => { this.context.currentTime >= s && (a(), this.ticker.removeCallback(`fadeOut_${t}`)); }); } cancelScheduledFadeOut(t) { this.ticker.removeCallback(`fadeOut_${t}`); } cancelFadeAnimation(t) { this.ticker.removeCallback(`fade_${t}`); const e = this.activeFadeCallbacks.get(t); e && (e(), this.activeFadeCallbacks.delete(t)); const i = this.sounds.get(t); i != null && i.gainNode && i.gainNode.gain.cancelScheduledValues(this.context.currentTime), i && (i.isFadingIn = !1, i.isFadingOut = !1); } getValidatedSound(t) { const e = this.sounds.get(t); return e != null && e.buffer || this.handleError("validating sound", "Sound not found or not loaded properly", t), e; } setValidatedVolume(t) { return Math.max(0, Math.min(1, t)); } cleanupSound(t) { const e = this.sounds.get(t); e && (this.debugLog(`Cleaning up sound ${t}`), this.cancelScheduledFadeOut(t), this.cancelFadeAnimation(t), this.stopProgressTracking(t), this.removeEventListenersForInstance(t), this.audioNodeConnector.disconnectNodes(e)); } cleanup() { this.sounds.forEach((t, e) => { this.cleanupSound(e); }), this.sounds.clear(), this.activeSources.forEach((t) => { if (t) try { t.onended = null, t.stop(), t.disconnect(); } catch { } }), this.activeSources.clear(), this.cleanupGlobalPan(), this.sounds.forEach((t) => { t.pannerNode && t.pannerNode.disconnect(), t.stereoPanner && t.stereoPanner.disconnect(), t.source && (t.source.stop(), t.source.disconnect(), t.source.onended = null), t.gainNode.disconnect(); }), this.sounds.clear(), this.masterStereoPanner.disconnect(), this.masterGainNode.disconnect(); } handleError(t, e, i) { const o = e instanceof Error ? e.message : String(e), s = i ? ` (Sound ID: ${i})` : "", a = `[SoundManager] Error ${t}${s}: ${o}`; if (this.lastError = e instanceof Error ? e : new Error(o), this.config.debug ? console.error(a, e) : console.error(a), !this.isHandlingError) { this.isHandlingError = !0; try { this.dispatchEvent({ type: h.ERROR, timestamp: this.context.currentTime, error: new Error(a) }); } finally { this.isHandlingError = !1; } } } cleanupExistingSource(t) { var e, i; try { let o = this.getValidatedSound(t); if (!o.source) return; const s = this.context.currentTime, a = o.startTime || 0, n = s - a < (((e = o.buffer) == null ? void 0 : e.duration) || 0); if (o.state === S.Playing && n && ((i = o.playOptions) != null && i.createNewInstance)) return; o.source.onended && (o.source.onended = null), o.startTime !== void 0 && o.source.stop(), o.source.disconnect(), this.activeSources.delete(t), o.source = null; } catch (o) { this.debugLog(`Error cleaning up source for ${t}: ${o}`); } } getInstanceCounter(t) { const e = t.split(":")[0]; let i = this.instanceCounters.get(e) || 0; return this.instanceCounters.set(e, i + 1), i + 1; } resetCounterForSound(t) { const e = t.split(":")[0]; this.instanceCounters.delete(e), this.debugLog(`Counter reset for sound ${e}`); } reconnectAudioNodes(t) { const e = this.sounds.get(t); !e || !e.source || this.audioNodeConnector.connectNodes(e, this.masterGainNode); } // Playback control----------------------------------------------------------------------------------------------------------- play(t, e = {}, i = !1) { var o, s, a, n, r, d, p, l, m, f, T, y, A, b, D, $, x; this.context.state === "suspended" && this.config.autoUnlock && this.setupAudioUnlock(); try { const v = this.getValidatedSound(t); v || this.debugLog(`Sound ${t} not found`); let C = { ...v.playOptions, ...e }; const F = C.createNewInstance ?? this.config.createNewInstance ?? !1; let V = t, w, L = e.groupId || v.groupId; if (L) { let P = this.soundGroups.get(L); if (!P) { this.debugLog(`Group ${L} not found.`); return; } if (this.debugLog(`Group ${L} has ${P.sounds.size} instances. Max instances: ${P.maxInstances}`), P.maxInstances && P.sounds.size >= P.maxInstances) { const N = Array.from(P.sounds)[0]; this.debugLog(`Max instances reached. Stopping oldest instance: ${N}`), this.stop(N), P.sounds.delete(N), this.debugLog(`Stopped and removed oldest instance ${N} from group ${L}.`); } } if (F) { const P = t.split(":")[0], N = this.getInstanceCounter(P); V = `${P}:${N}`, this.debugLog(`Creating new instance with ID: ${V}`); const k = JSON.parse(JSON.stringify({ ...v.playOptions, ...e, createNewInstance: !1 })), I = this.context.createGain(), R = e.volume ?? v.volume ?? this.config.defaultVolume ?? 1; I.gain.value = R, w = { ...v, id: V, gainNode: I, state: S.Stopped, currentTime: 0, startTime: (k == null ? void 0 : k.startTime) ?? 0, pausedAt: 0, currentLoopCount: 0, playOptions: k, volume: R, originalVolume: R, pan: 0, panSpatialPosition: { x: 0, y: 0, z: 0 }, panType: k.panType ?? E.Stereo, source: null, stereoPanner: null, pannerNode: null }, this.sounds.set(V, w), L && (this.addToSoundGroup(L, V), this.debugLog(`Added new instance ${V} to group ${L}.`)); } const c = w || v; if (!c) { this.debugLog(`Failed to create sound instance for ${t}`); return; } w === void 0 && (c.playOptions = C), this.cleanupExistingSource(c.id); const G = this.setupAudioSource(c); if (!G) { this.debugLog(`Failed to create audio source for sound ${t}`); return; } const M = ((o = c.playOptions) == null ? void 0 : o.playbackRate) || 1; let _ = 0; if (c.pausedAt !== void 0 && c.pausedAt !== 0 ? _ = c.pausedAt : ((s = c.playOptions) == null ? void 0 : s.startTime) !== void 0 && (_ = c.playOptions.startTime), c.startTime = this.context.currentTime - _ / M, c.state = S.Playing, ((a = c.playOptions) == null ? void 0 : a.volume) !== void 0 && this.setSoundVolume(c.id, c.playOptions.volume, !0), ((n = c.playOptions) == null ? void 0 : n.pan) !== void 0 && c.panType !== E.Spatial && this.setPan(c.id, c.playOptions.pan, !0), ((r = c.playOptions) == null ? void 0 : r.panSpatialPosition) !== void 0 && c.panType === E.Spatial && this.setSpatialPosition(c.playOptions.panSpatialPosition.x, c.playOptions.panSpatialPosition.y, c.playOptions.panSpatialPosition.z, c.id, void 0, !0), ((d = c.playOptions) == null ? void 0 : d.fadeInDuration) !== void 0 && this.fadeIn( c.id, ((p = c.playOptions) == null ? void 0 : p.fadeInDuration) ?? ((l = this.config) == null ? void 0 : l.fadeInDuration) ?? 1, void 0, // startVolume (defaults to 0 or current volume) (m = c.playOptions) == null ? void 0 : m.volume // Use PlayOptions.volume as the end volume ), ((f = c.playOptions) == null ? void 0 : f.fadeOutDuration) !== void 0 && this.fadeOut(c.id, c.playOptions.fadeOutDuration ?? ((T = this.config) == null ? void 0 : T.fadeOutDuration) ?? 1), ((y = c.playOptions) == null ? void 0 : y.playbackRate) !== void 0 && this.setPlaybackRate(c.id, M, !0), ((A = c.playOptions) == null ? void 0 : A.loop) !== void 0 && this.setLoop(c.id, c.playOptions.loop, c.playOptions.maxLoops), ((b = c.playOptions) == null ? void 0 : b.fadeOutBeforeEndDuration) !== void 0) { this.cancelScheduledFadeOut(c.id); const P = c.playOptions.fadeOutBeforeEndDuration, I = (c.playOptions.duration ?? ((D = c.buffer) == null ? void 0 : D.duration) ?? 0) - (c.pausedAt || 0) - P; I > 0 && this.scheduleFadeOut(c.id, I, c.playOptions.fadeOutBeforeEndDuration ?? 1); } if (G.start( 0, _, (($ = c.playOptions) == null ? void 0 : $.duration) !== void 0 && c.playOptions.duration > 0 ? c.playOptions.duration * M : void 0 ), ((x = c.playOptions) == null ? void 0 : x.trackProgress) === !0 && this.startProgressTracking(c.id), !i) { const P = c.id.includes(":") ? c.id.split(":")[0] : c.id; this.dispatchEvent({ type: h.STARTED, soundId: c.id, originalId: P, instanceId: c.id, timestamp: this.context.currentTime, sound: c }); } return c; } catch (v) { this.handleError("playing sound", v, t); } } playSprite(t, e, i, o = !1) { const s = `${t}_${e}`; this.play(s, i, o); } pause(t, e = !1) { var i; try { const o = this.getValidatedSound(t); if (!this.isPlaying(t) || this.isPaused(t)) return; this.cancelScheduledFadeOut(t); const s = ((i = o.playOptions) == null ? void 0 : i.playbackRate) ?? 1, a = (this.context.currentTime - (o.startTime || 0)) * s; o.currentTime = a, o.pausedAt = a, this.debugLog(`Pausing sound ${t}: Raw elapsed time: ${a} Adjusted time: ${a / s} StartTime: ${o.startTime} CurrentTime: ${o.currentTime} PausedAt: ${o.pausedAt} PlaybackRate: ${s} `), o.state = S.Paused, this.stopProgressTracking(t), this.cleanupExistingSource(t), e || this.dispatchEvent({ type: h.PAUSED, soundId: t, timestamp: this.context.currentTime, sound: o }); } catch (o) { this.handleError("pausing sound", o, t); } } resume(t, e = !1) { let i = this.getValidatedSound(t); this.play(t, i == null ? void 0 : i.playOptions); try { e || this.dispatchEvent({ type: h.RESUMED, soundId: t, timestamp: this.context.currentTime, sound: i }), this.debugLog(`Resumed sound ${t}:`); } catch (o) { this.handleError("resuming sound", o, t); } } stop(t, e = !1) { var i; try { const o = this.sounds.get(t); if (!o) { this.debugLog(`Sound ${t} not found for stopping`); return; } this.cancelScheduledFadeOut(t), this.cancelFadeAnimation(t), this.stopProgressTracking(t), this.cleanupExistingSource(t), o.state = S.Stopped, o.startTime = ((i = o.playOptions) == null ? void 0 : i.startTime) ?? 0, o.pausedAt = 0, o.currentTime = 0, this.removeEventListenersForInstance(t), this.resetCounterForSound(t), e || this.dispatchEvent({ type: h.STOPPED, soundId: t, timestamp: this.context.currentTime, sound: o }); } catch (o) { console.error(`Error stopping sound ${t}:`, o), this.dispatchEvent({ type: h.ERROR, timestamp: this.context.currentTime, error: new Error(`Failed to stop sound ${t}: ${o}`) }); } } seek(t, e, i = !1) { var o, s, a; try { const n = this.getValidatedSound(t), { duration: r, currentTime: d } = this.getSoundState(t); if (e >= r) { if (n.state === S.Stopped) return; n.state === S.Playing && ((o = n.playOptions) != null && o.loop) ? this.handleLoopIteration(n) : this.handleSoundEnded(n); return; } const p = ((s = n.buffer) == null ? void 0 : s.duration) || 0, l = ((a = n.playOptions) == null ? void 0 : a.playbackRate) || 1, m = e * l, f = Math.max(0, Math.min(m, p)); if (n.currentTime = f, n.pausedAt = f, n.state === S.Playing && (this.cleanupExistingSource(t), n.startTime = this.context.currentTime - f / l, this.play(t, n.playOptions)), i) return; this.dispatchEvent({ type: h.SEEKED, soundId: t, currentTime: d, timestamp: this.context.currentTime, sound: n }); } catch (n) { this.handleError("seeking sound", n, t); } } stopAllSounds() { try { Array.from(this.activeSources.keys()).forEach((e) => this.stop(e)), this.debugLog("All sounds stopped"); } catch (t) { this.handleError("stopping all sounds", t); } } pauseAllSounds() { this.sounds.forEach((t, e) => { t.state === S.Playing && this.pause(e); }); } resumeAllSounds() { this.sounds.forEach((t, e) => { t.state == S.Paused && this.resume(e); }); } // End Playback control----------------------------------------------------------------------------------------------------------- // Fade managment ---------------------------------------------------------------------------------------------------------------- fadeIn(t, e, i, o, s = !1) { var p, l; const a = this.getValidatedSound(t); console.log("fade in", t), this.cancelFadeAnimation(t), a.isFadingOut = !1, a.isFadingIn = !0; const n = this.roundValue(a.gainNode.gain.value, 2); let r; o !== void 0 ? r = o : (r = a.volume ?? ((p = a.playOptions) == null ? void 0 : p.volume) ?? this.config.defaultVolume ?? 1, r === 0 && (r = 1)); let d; i !== void 0 ? d = i : a.isFadingOut ? d = n : n >= r ? d = ((l = a.playOptions) == null ? void 0 : l.fadeInStartVolume) ?? 0 : d = n, a.gainNode.gain.cancelScheduledValues(this.context.currentTime), a.gainNode.gain.setValueAtTime(d, this.context.currentTime), a.state !== S.Playing && this.play(t, { volume: d }), this.fadeSound(t, d, r, e, () => { a.volume = r, a.playOptions && (a.playOptions.volume = r), s || this.dispatchEvent({ type: h.FADE_IN_COMPLETED, soundId: t, timestamp: this.context.currentTime, sound: a }); }); } fadeOut(t, e = this.config.fadeOutDuration, i, o, s = !1, a = !1) { var l; const n = this.getValidatedSound(t); this.cancelFadeAnimation(t), n.isFadingIn = !1, n.isFadingOut = !0; const r = this.roundValue(n.gainNode.gain.value, 2), d = i ?? r, p = o ?? ((l = n.playOptions) == null ? void 0 : l.fadeOutEndVolume) ?? 0; n.previousVolume = r, n.gainNode.gain.setValueAtTime(d, this.context.currentTime), n.state !== S.Playing && this.play(t, { volume: d }), this.fadeSound(t, d, p, e, () => { a || this.dispatchEvent({ type: h.FADE_OUT_COMPLETED, soundId: t, timestamp: this.context.currentTime, sound: n }), o === 0 && s && this.stop(t); }); } fadeSound(t, e, i, o, s) { try { this.cancelFadeAnimation(t); const a = this.getValidatedSound(t); a.volume = e, a.gainNode.gain.cancelScheduledValues(this.context.currentTime); const n = Math.max(0, o - 0.02), r = this.context.currentTime, d = r + n, p = () => { a.isFadingIn = !1, a.isFadingOut = !1, a.volume = this.roundValue(i), a.gainNode.gain.setValueAtTime(i, this.context.currentTime), s == null || s(), this.dispatchEvent({ type: h.VOLUME_CHANGED, soundId: t, timestamp: this.context.currentTime, volume: a.volume, sound: a }), this.activeFadeCallbacks.delete(t); }; this.activeFadeCallbacks.set(t, p); const l = `fade_${t}`, m = () => { const f = this.context.currentTime; if (f >= d) { this.ticker.removeCallback(l), p(); return; } const T = (f - r) / n, y = e + (i - e) * T; a.gainNode.gain.setValueAtTime(y, f), a.volume = this.roundValue(y), a.playOptions = { ...a.playOptions, volume: a.volume }, this.dispatchEvent({ type: h.VOLUME_CHANGED, soundId: t, timestamp: f, volume: a.volume, sound: a }); }; this.ticker.addCallback(l, m), this.debugLog(`Fade scheduled for sound ${t}: Start time: ${r} Duration: ${n} Start volume: ${e} Target volume: ${i} `); } catch (a) { this.handleError("fading sound", a, t); } } fadeGlobalIn(t = this.config.fadeInDuration, e, i) { try { const o = e ?? 0, s = i ?? (this.masterGainNode.gain.value || this.previousGlobalVolume); this.masterGainNode.gain.cancelScheduledValues(this.context.currentTime); const a = this.context.currentTime, n = t, r = a + n, d = "fade_global_in", p = () => { const l = this.context.currentTime; if (l >= r) { this.masterGainNode.gain.setValueAtTime(s, l), this.isMuted = !1, this.dispatchEvent({ type: h.FADE_MASTER_IN_COMPLETED, timestamp: l, volume: s }), this.ticker.removeCallback(d); return; } const m = (l - a) / n, f = o + (s - o) * m; this.masterGainNode.gain.setValueAtTime(f, l), this.dispatchEvent({ type: h.MASTER_VOLUME_CHANGED, timestamp: l, volume: f, isMaster: !0 }); }; this.ticker.addCallback(d, p); } catch (o) { this.handleError("fading in master volume", o); } } fadeGlobalOut(t = this.config.fadeOutDuration, e, i = 0) { if (t <= 0) { this.debugLog(`Invalid fade duration: ${t}`); return; } try { const o = e ?? this.roundValue(this.masterGainNode.gain.value, 2); this.previousGlobalVolume = o; const s = this.context.currentTime, a = t, n = s + a, r = "fade_global_out", d = () => { const p = this.context.currentTime; if (p >= n) { this.masterGainNode.gain.setValueAtTime(i, p), this.isMuted = i === 0, this.dispatchEvent({ type: h.FADE_MASTER_OUT_COMPLETED, timestamp: p, volume: i }), this.ticker.removeCallback(r); return; } const l = (p - s) / a, m = o + (i - o) * l; this.masterGainNode.gain.setValueAtTime(m, p), this.dispatchEvent({ type: h.MASTER_VOLUME_CHANGED, timestamp: p, volume: m, isMaster: !0 }); }; this.ticker.addCallback(r, d); } catch (o) { this.handleError("fading out master volume", o); } } // End Fading ------------------------------------------------------------------------------------------------------------------------ // Volume control----------------------------------------------------------------------------------------------------------------- getVolume(t) { return this.getSoundVolume(t); } setSoundVolume(t, e, i = !1) { try { this.cancelFadeAnimation(t); const o = this.getValidatedSound(t), s = this.setValidatedVolume(e); o.volume = this.roundValue(s), o.originalVolume = s, o.playOptions = { ...o.playOptions, volume: s }, o.gainNode.gain.cancelScheduledValues(this.context.currentTime), o.gainNode.gain.setValueAtTime(s, this.context.currentTime), i || this.dispatchEvent({ type: h.VOLUME_CHANGED, soundId: t, timestamp: this.context.currentTime, volume: o.volume, sound: o }); } catch (o) { this.handleError("setting volume", o, t); } } getSoundVolume(t) { var e; try { const i = this.getValidatedSound(t); return i.originalVolume ?? i.volume ?? ((e = i.playOptions) == null ? void 0 : e.volume) ?? this.config.defaultVolume ?? 1; } catch (i) { return this.handleError("getting volume", i, t), 0; } } setGlobalVolume(t) { this.previousGlobalVolume = this.setValidatedVolume(t), this.isMuted || (this.masterGainNode.gain.value = this.previousGlobalVolume), this.dispatchEvent({ type: h.MASTER_VOLUME_CHANGED, timestamp: this.context.currentTime, volume: this.previousGlobalVolume, isMaster: !0 }); } getGlobalVolume() { return this.isMuted ? 0 : this.roundValue(this.masterGainNode.gain.value, 2); } // End Volume control------------------------------------------------------------------------------------------------------------- // Mute control------------------------------------------------------------------------------------------------------------------- muteAllSounds() { this.previousGlobalVolume = this.roundValue(this.masterGainNode.gain.value, 2), this.masterGainNode.gain.setValueAtTime(0, this.context.currentTime), this.isMuted = !0, this.dispatchEvent({ type: h.MUTE_GLOBAL, timestamp: this.context.currentTime, isMuted: !0, volume: 0 }); } unmuteAllSounds() { this.masterGainNode.gain.setValueAtTime(this.previousGlobalVolume, this.context.currentTime), this.isMuted = !1, this.dispatchEvent({ type: h.UNMUTE_GLOBAL, timestamp: this.context.currentTime, isMuted: !1, volume: this.previousGlobalVolume }); } mute(t) { try { const e = this.getValidatedSound(t); e.previousVolume = e.volume, e.volume = 0, e.gainNode.gain.setValueAtTime(0, this.context.currentTime), this.dispatchEvent({ type: h.MUTED, soundId: t, timestamp: this.context.currentTime, volume: 0, isMuted: !0, sound: e }); } catch (e) { this.handleError("muting sound", e, t); } } unmute(t) { try { const e = this.getValidatedSound(t), i = e.previousVolume ?? this.config.defaultVolume ?? 1; e.volume = i, e.gainNode.gain.setValueAtTime(i, this.context.currentTime), this.dispatchEvent({ type: h.UNMUTED, soundId: t, timestamp: this.context.currentTime, volume: i, isMuted: !1, sound: e }); } catch (e) { this.handleError("unmuting sound", e, t); } } toggleMute(t) { var i; const e = this.getValidatedSound(t); ((e == null ? void 0 : e.volume) ?? (e == null ? void 0 : e.originalVolume) ?? ((i = e.playOptions) == null ? void 0 : i.volume) ?? 1) > 0 ? this.mute(t) : this.unmute(t); } toggleGlobalMute() { this.isMuted ? this.unmuteAllSounds() : this.muteAllSounds(); } // End Mute control----------------------------------------------------------------------------------------------------------------------------- // Loop control -------------------------------------------------------------------------------------------------------------------------------- setLoop(t, e, i = -1) { const o = this.sounds.get(t); if (!o) { this.debugLog(`Sound ${t} not found for setting loop`); return; } o.playOptions = { ...o.playOptions, loop: e, maxLoops: i }, this.debugLog(`Loop set for sound ${t}: ${e}`); } getLoop(t) { var i; const e = this.sounds.get(t); return e ? ((i = e.playOptions) == null ? void 0 : i.loop) ?? !1 : (this.debugLog(`Sound ${t} not found for getting loop`), !1); } // End loop control----------------------------------------------------------------------------------------------------------------------------- // Sound loading and management----------------------------------------------------------------------------------------------------------------- shouldUseProxy(t) { return !(!this.config.corsProxy || this.isLocalUrl(t)); } isLocalUrl(t) { return t.startsWith("/") || t.startsWith("./") || t.startsWith("../") || t.startsWith("blob:") || t.startsWith("data:") || !/^https?:/i.test(t); } getProxyUrl(t) { if (!this.shouldUseProxy(t)) return t; const e = this.config.corsProxy; if (e.includes("cors-anywhere")) return `${e}${t}`; if (e.includes("?")) { const i = e.includes("url=") ? "" : "url="; return `${e}${i}${encodeURIComponent(t)}`; } return `${e}${t}`; } async loadWithWebAudio(t, e, i) { if (i != null && i.aborted) throw i.reason ?? new DOMException("Aborted", "AbortError"); const o = this.config.fetchStrategy === "proxy-first" && this.shouldUseProxy(e) ? ["proxy", "direct"] : ["direct"]; for (const s of o) try { const a = s === "proxy" || this.config.corsProxy ? this.getProxyUrl(e) : e; this.debugLog(`Trying ${s} strategy for ${t}`, { originalUrl: e, fetchUrl: a }); const n = await this.fetchWithRetry(a, { mode: "cors", credentials: "omit", cache: this.config.audioCache ? "default" : "no-cache", headers: { Accept: "audio/mpeg, audio/*" } }, i); return await this.processAudioResponse(t, n); } catch (a) { if (i != null && i.aborted || a instanceof DOMException && a.name === "AbortError") throw a; if (this.debugLog(`${s} strategy failed for ${t}`, { error: a instanceof Error ? a.message : String(a), url: e }), s === o[o.length - 1]) throw new Error(`Failed to load sound ${t}: ${a instanceof Error ? a.message : String(a)}`); } } async fetchWithRetry(t, e, i) { const { fetchRetries: o = 2, retryDelay: s = 0.5, fetchTimeout: a = 10 } = this.config; let n = null; if (i != null && i.aborted) throw i.reason ?? new DOMException("Aborted", "AbortError"); const r = this.config.credentialStrategy === "auto" ? this.config.crossOrigin === "use-credentials" ? ["include", "omit"] : ["omit"] : [this.config.credentialStrategy || "omit"]; for (const d of r) for (let p = 0; p <= o; p++) { const l = Date.now(), m = new AbortController(), f = setTimeout(() => m.abort(), a * 1e3), T = i ? AbortSignal.any([i, m.signal]) : m.signal; try { const y = { ...e, credentials: d, signal: T }; this.debugLog(`Attempt ${p + 1} with credentials=${d}`); const A = await fetch(t, y); if (clearTimeout(f), !A.ok) throw new Error(`HTTP ${A.status}`); if (e.mode === "cors") { const b = A.headers.get("access-control-allow-origin"); if (d === "include" && b === "*") throw new Error("Invalid CORS: Credentialed request with wildcard origin"); } return this.debugLog(`Success after ${Date.now() - l}ms`), A; } catch (y) { if (clearTimeout(f), i != null && i.aborted) throw y; n = y instanceof Error ? y : new Error(String(y)), this.debugLog(`Attempt ${p + 1} failed: ${n.message}`); } if (p < o) { if (i != null && i.aborted) throw i.reason ?? new DOMException("Aborted", "AbortError"); await new Promise((y) => setTimeout(y, s * 1e3)); } } throw n || new Error("Failed after all retries"); } async processAudioResponse(t, e) { const i = e.headers.get("content-type"); if (!(i != null && i.includes("audio/"))) throw new Error(`Invalid content type: ${i}`); const o = e.headers.get("content-length"); if (o && this.config.maxAudioSize && parseInt(o) > this.config.maxAudioSize) throw new Error(`Audio file too large: ${o} bytes (max ${this.config.maxAudioSize} bytes), change the maxAudioSize config in your SoundManagerConfig`); const s = o ? parseInt(o) : void 0, a = await e.arrayBuffer(), n = await this.context.decodeAudioData(a); this.createSoundNode(t, n, s), this.debugLog(`Sound ${t} loaded successfully`); } async loadWithHtml5Audio(t, e, i) { if (!this.config.html5AudioFallback) throw new Error("HTML5 Audio fallback is disabled in configuration"); if (i != null && i.aborted) throw i.reason ?? new DOMException("Aborted", "AbortError"); return new Promise((o, s) => { const a = new Audio(); a.crossOrigin = this.config.crossOrigin === "use-credentials" ? "use-credentials" : "anonymous", a.preload = "auto", a.src = e; const n = () => { a.oncanplaythrough = null, a.onerror = null, a.removeEventListener("error", d), i == null || i.removeEventListener("abort", r); }, r = () => { n(), a.src = "", s(i.reason ?? new DOMException("Aborted", "AbortError")); }, d = () => { n(), s(new Error(`HTML5 Audio load error: ${a.error ? a.error.message : "unknown"}`)); }; a.oncanplaythrough = async () => { n(); try { await this.loadWithWebAudio(t, e, i), this.debugLog(`Sound ${t} loaded with HTML5 Audio fallback`), o(); } catch (p) { s(p); } }, a.onerror = d, a.addEventListener("error", d), i && i.addEventListener("abort", r, { once: !0 }), a.load(); }); } async loadSounds(t, e) { if (t.length) { if (e != null && e.aborted) throw e.reason ?? new DOMException("Aborted", "AbortError"); try { const { maxParallelLoads: i = 10, webAudioPreferred: o = !0 } = this.config, s = Math.max(1, i), a = []; for (let n = 0; n < t.length; n += s) a.push(t.slice(n, n + s)); for (const n of a) { if (e != null && e.aborted) throw e.reason ?? new DOMException("Aborted", "AbortError"); const r = n.map(async ({ id: l, url: m }) => { if (this.sounds.has(l)) { this.debugLog(`Sound with id ${l} already exists. Skipping.`); return; } try { if (o) try { await this.loadWithWebAudio(l, m, e); return; } catch (f) { if (e != null && e.aborted) throw f; this.debugLog(`Web Audio load failed for ${l}`, f); } await this.loadWithHtml5Audio(l, m, e); } catch (f) { throw f instanceof DOMException && f.name === "AbortError" || this.handleError("loading sound", f, l), f; } }), d = await Promise.allSettled(r), p = d.filter((l) => l.status === "rejected"); if (p.length) { const l = p[0].reason; if (l instanceof DOMException && l.name === "AbortError") throw l; const m = n.filter((f, T) => d[T].status === "rejected").map((f) => f.id); throw new Error(`Failed to load sounds: ${m.join(", ")}`); } } } catch (i) { throw i instanceof DOMException && i.name === "AbortError" || this.handleError("preloading sounds", i), i; } } } calculateAudioSize(t) { return t.numberOfChannels * t.length * 4; } createSoundNode(t, e, i) { const o = this.context.createGain(); o.gain.value = this.config.defaultVolume ?? 1, o.connect(this.masterGainNode); const s = this.context.createBufferSource(); s.buffer = e; const a = this.calculateAudioSize(e), n = { id: t, buffer: e, gainNode: o, source: s, startTime: void 0, currentTime: 0, pausedAt: void 0, state: S.Stopped, volume: this.config.defaultVolume ?? 1, currentLoopCount: 0, originalVolume: this.config.defaultVolume ?? 1, playOptions: { startTime: this.config.defaultStartTime ?? 0, loop: this.config.loopSounds ?? !1, maxLoops: this.config.maxLoops ?? -1, playbackRate: this.config.defaultPlaybackRate ?? 1, pan: this.config.defaultPan ?? 0, volume: this.config.defaultVolume ?? 1, trackProgress: this.config.trackProgress ?? !1 }, panSpatialPosition: this.config.defaultPanSpatialPosition ?? { x: 0, y: 0, z: 0 }, pan: this.config.defaultPan ?? 0, panType: this.config.defaultPanType ?? E.Stereo }; this.sounds.set(t, n), this.dispatchEvent({ type: h.LOADED, soundId: t, timestamp: this.context.currentTime, sound: n, duration: e.duration, bufferSize: a, fileSize: i, sampleRate: e.sampleRate, channels: e.numberOfChannels }); } async loadSound(t, e, i) { try { await this.loadSounds([{ id: t, url: e }], i); } catch (o) { throw o instanceof DOMException && o.name === "AbortError" || this.handleError("loading sound", o, t), o; } } async updateSoundUrl(t, e) { try { const i = this.sounds.get(t); if (!i) { this.debugLog(`Sound ${t} not found for URL update`); return; } this.cleanupSound(t), await this.loadSound(t, e), this.dispatchEvent({ type: h.UPDATED_URL, soundId: t, timestamp: this.context.currentTime, sound: i }), this.debugLog(`Sound ${t} URL updated to ${e}`); } catch (i) { this.handleError("updating sound URL", i, t); } } unloadSound(t) { const e = this.sounds.get(t); if (!e) { this.debugLog(`Sound ${t} not found for unloading`); return; } this.stop(t, !1), this.cleanupSound(t), this.resetCounterForSound(t), this.dispatchEvent({ type: h.UNLOADED, soundId: t, timestamp: this.context.currentTime, sound: e }), this.debugLog(`Sound ${t} unloaded`); } removeSound(t) { try { if (!this.sounds.get(t)) return; this.unloadSound(t), this.sounds.delete(t), this.debugLog(`Removed sound ${t}`); } catch (e) { this.handleError("removing sound", e, t); } } isSoundLoaded(t) { const e = this.sounds.get(t); return (e == null ? void 0 : e.buffer) != null; } // Sound group management ---------------------------------------------------------------------------------------------------------------------- createSoundGroup(t, e = {}) { if (this.soundGroups.has(t)) { this.debugLog(`Group with id ${t} already exists.`); return; } this.soundGroups.set(t, { id: t, sounds: /* @__PURE__ */ new Set(), maxInstances: e.maxInstances, playOptions: e.playOptions }), this.debugLog(`Created group ${t} with options:`, e); } addToSoundGroup(t, e) { const i = this.soundGroups.get(t); if (!i) { this.debugLog(`Group ${t} not found.`); return; } if (i.maxInstances && i.sounds.size >= i.maxInstances) { const s = Array.from(i.sounds)[0]; this.stop(s), i.sounds.delete(s), this.debugLog(`Stopped oldest instance ${s} to make room for new instance in group ${t}.`); } const o = this.sounds.get(e); o && (o.groupId = t), o && i.playOptions && (o.playOptions = { ...i.playOptions, ...o.playOptions }), i.sounds.add(e), this.debugLog(`Added sound ${e} to group ${t}.`); } removeFromSoundGroup(t, e) { const i = this.soundGroups.get(t); if (!i) { this.debugLog(`Group ${t} not found.`); return; } i.sounds.delete(e), this.debugLog(`Removed sound ${e} from group ${t}.`); } getGroup(t) { return this.soundGroups.get(t); } removeSoundGroup(t) { const e = this.soundGroups.get(t); if (!e) { this.debugLog(`Group ${t} not found.`); return; } e.sounds.forEach((i) => { this.stop(i), this.sounds.delete(i); }), e.sounds.clear(), this.soundGroups.delete(t), this.debugLog(`Cleaned up group ${t}.`); } //End Sound group management ---------------------------------------------------------------------------------------------------------------------- // Sprite control---------------------------------------------------------------------------------------------------------------------------------- setSoundSprite(t, e) { t