UNPKG

sound-manager-ts

Version:

A lightweight, TypeScript-based Web Audio API manager for seamless sound control in web apps and games

1,115 lines 76.9 kB
var U = Object.defineProperty; var H = (r, t, e) => t in r ? U(r, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : r[t] = e; var y = (r, t, e) => H(r, typeof t != "symbol" ? t + "" : t, e); var T = /* @__PURE__ */ ((r) => (r.Stereo = "stereo", r.Spatial = "spatial", r))(T || {}); class z { // Connect nodes based on the panning type connectNodes(t, e) { t.source && (this.disconnectNodes(t), t.panType === T.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 d = /* @__PURE__ */ ((r) => (r.ENDED = "ended", r.ERROR = "error", r.FADE_IN_COMPLETED = "fade_in_completed", r.FADE_MASTER_IN_COMPLETED = "fade_master_in_completed", r.FADE_MASTER_OUT_COMPLETED = "fade_master_out_completed", r.FADE_OUT_COMPLETED = "fade_out_completed", r.GLOBAL_SPATIAL_POSITION_CHANGED = "global_spatial_position_changed", r.LOADED = "loaded", r.LOOP_COMPLETED = "loop_completed", r.MASTER_PAN_CHANGED = "master_pan_changed", r.MASTER_VOLUME_CHANGED = "master_volume_changed", r.MUTE_GLOBAL = "mute_global", r.MUTED = "muted", r.OPTIONS_UPDATED = "options_updated", r.PAN_CHANGED = "pan_changed", r.PAN_RESET = "pan_reset", r.PAUSED = "paused", r.PLAYBACK_RATE_CHANGED = "playback_rate_changed", r.PROGRESS = "progress", r.RESET = "reset", r.RESUMED = "resumed", r.SEEKED = "seeked", r.SPATIAL_POSITION_CHANGED = "spatial_position_changed", r.SPATIAL_POSITION_RESET = "spatial_position_reset", r.SPRITE_SET = "sprite_set", r.STARTED = "started", r.STOPPED = "stopped", r.UNLOADED = "unloaded", r.UNMUTE_GLOBAL = "unmute_global", r.UNMUTED = "unmuted", r.UPDATED_URL = "updated_url", r.VOLUME_CHANGED = "volume_changed", r))(d || {}), B = /* @__PURE__ */ ((r) => (r.HRTF = "HRTF", r.EqualPower = "equalpower", r))(B || {}), W = /* @__PURE__ */ ((r) => (r.Linear = "linear", r.Inverse = "inverse", r.Exponential = "exponential", r))(W || {}); const A = { panningModel: "HRTF", distanceModel: "inverse", refDistance: 1, maxDistance: 1e4, rolloffFactor: 0.2, coneInnerAngle: 360, coneOuterAngle: 360, coneOuterGain: 0 }, j = { 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: T.Stereo, defaultPlaybackRate: 1, defaultStartTime: 0, defaultVolume: 1, fadeInDuration: 0.5, fadeOutDuration: 0.5, loopSounds: !1, maxLoops: -1, pannerNodeConfig: A, spatialAudio: !0, trackProgress: !0 }; var f = /* @__PURE__ */ ((r) => (r.Playing = "playing", r.Paused = "paused", r.Stopped = "stopped", r))(f || {}); class Y { constructor() { y(this, "callbacks", /* @__PURE__ */ new Map()); y(this, "animationFrameId", null); y(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 = {}) { y(this, "config"); y(this, "context"); y(this, "sounds", /* @__PURE__ */ new Map()); y(this, "masterGainNode"); y(this, "masterStereoPanner"); y(this, "masterPannerNode"); y(this, "previousGlobalVolume", 1); y(this, "isMuted", !1); y(this, "previousGlobalPan", 0); y(this, "PROGRESS_UPDATE_INTERVAL", 50); // 50ms default, could be configurable y(this, "eventListeners", /* @__PURE__ */ new Map()); y(this, "activeSources", /* @__PURE__ */ new Map()); y(this, "activeFadeCallbacks", /* @__PURE__ */ new Map()); y(this, "isHandlingError", !1); y(this, "audioNodeConnector", new z()); y(this, "ticker"); y(this, "lastError", null); y(this, "masterSpatialPosition", { x: 0, y: 0, z: 0 }); y(this, "DEFAULT_PRECISION", 2); y(this, "soundGroups", /* @__PURE__ */ new Map()); y(this, "instanceCounters", /* @__PURE__ */ new Map()); y(this, "unlockHandlers", null); this.ticker = new Y(), this.config = { debug: !1, ...t }, Object.values(d).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 = { ...j, ...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.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), o = this.context.createBufferSource(); o.buffer = a, o.connect(this.context.destination), o.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(), n = () => e(); this.unlockHandlers = { touchstart: i, touchend: i, click: n }; 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 n, s, a; const e = ((n = t.playOptions) == null ? void 0 : n.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 !== T.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 o = t.playOptions.panSpatialPosition; this.setSpatialPosition(o.x, o.y, o.z, t.id, void 0, !0); } return this.audioNodeConnector.connectNodes(t, this.masterGainNode), this.activeSources.set(t.id, i), i.onended = () => { var o; this.debugLog(`Sound ${t.id} ended naturally`), t.state === f.Playing && ((o = t.playOptions) != null && o.loop ? this.handleLoopIteration(t) : this.handleSoundEnded(t)); }, i; } handleLoopIteration(t) { var i, n, s, a, o, c; if (this.debugLog(`Restarting loop for sound ${t.id}`), ((i = t.playOptions) == null ? void 0 : i.maxLoops) !== void 0 && ((n = t.playOptions) == null ? void 0 : n.maxLoops) > 0 && (t.currentLoopCount ?? 0) >= ((s = t.playOptions) == null ? void 0 : s.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 = (((a = t.playOptions) == null ? void 0 : a.startTime) ?? 0) / (((o = t.playOptions) == null ? void 0 : o.playbackRate) ?? 1); (c = t.playOptions) != null && c.createNewInstance && (t.playOptions.createNewInstance = !1), this.seek(t.id, e, !0), this.dispatchEvent({ type: d.LOOP_COMPLETED, soundId: t.id, timestamp: this.context.currentTime, sound: t }); } handleSoundEnded(t) { var e, i, n, s, a, o; if (t.state !== f.Stopped) { if ((e = t.playOptions) != null && e.pauseAtDurationReached && ((i = t.playOptions) == null ? void 0 : i.duration) !== void 0 && t.playOptions.duration > 0) { this.pause(t.id), t.startTime = void 0, t.pausedAt = ((n = t.playOptions) == null ? void 0 : n.startTime) ?? 0, t.currentTime = 0; return; } t.state = f.Stopped, t.startTime = void 0, t.pausedAt = ((s = t.playOptions) == null ? void 0 : s.startTime) ?? 0, t.currentTime = 0, (a = t.playOptions) != null && a.createNewInstance ? this.cleanupExistingSource(t.id) : this.cleanupSound(t.id), this.stopProgressTracking(t.id), (o = t.playOptions) != null && o.createNewInstance && this.removeEventListenersForInstance(t.id), this.dispatchEvent({ type: d.ENDED, soundId: 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 n = e instanceof Error ? e.message : String(e), s = i ? ` (Sound ID: ${i})` : "", a = `[SoundManager] Error ${t}${s}: ${n}`; if (this.lastError = e instanceof Error ? e : new Error(n), this.config.debug ? console.error(a, e) : console.error(a), !this.isHandlingError) { this.isHandlingError = !0; try { this.dispatchEvent({ type: d.ERROR, timestamp: this.context.currentTime, error: new Error(a) }); } finally { this.isHandlingError = !1; } } } cleanupExistingSource(t) { var e, i; try { let n = this.getValidatedSound(t); if (!n.source) return; const s = this.context.currentTime, a = n.startTime || 0, o = s - a < (((e = n.buffer) == null ? void 0 : e.duration) || 0); if (n.state === f.Playing && o && ((i = n.playOptions) != null && i.createNewInstance)) return; n.source.onended && (n.source.onended = null), n.startTime !== void 0 && n.source.stop(), n.source.disconnect(), this.activeSources.delete(t), n.source = null; } catch (n) { this.debugLog(`Error cleaning up source for ${t}: ${n}`); } } 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 n, s, a, o, c, l, h, p, m, g, S, P, E, b, D, $, x; this.context.state === "suspended" && this.config.autoUnlock && this.setupAudioUnlock(); try { const O = this.getValidatedSound(t); O || this.debugLog(`Sound ${t} not found`); let C = { ...O.playOptions, ...e }; const F = C.createNewInstance ?? this.config.createNewInstance ?? !1; let V = t, _, L = e.groupId || O.groupId; if (L) { let v = this.soundGroups.get(L); if (!v) { this.debugLog(`Group ${L} not found.`); return; } if (this.debugLog(`Group ${L} has ${v.sounds.size} instances. Max instances: ${v.maxInstances}`), v.maxInstances && v.sounds.size >= v.maxInstances) { const N = Array.from(v.sounds)[0]; this.debugLog(`Max instances reached. Stopping oldest instance: ${N}`), this.stop(N), v.sounds.delete(N), this.debugLog(`Stopped and removed oldest instance ${N} from group ${L}.`); } } if (F) { const v = t.split(":")[0], N = this.getInstanceCounter(v); V = `${v}:${N}`, this.debugLog(`Creating new instance with ID: ${V}`); const k = JSON.parse(JSON.stringify({ ...O.playOptions, ...e, createNewInstance: !1 })), I = this.context.createGain(), G = e.volume ?? O.volume ?? this.config.defaultVolume ?? 1; I.gain.value = G, _ = { ...O, id: V, gainNode: I, state: f.Stopped, currentTime: 0, startTime: (k == null ? void 0 : k.startTime) ?? 0, pausedAt: 0, currentLoopCount: 0, playOptions: k, volume: G, originalVolume: G, pan: 0, panSpatialPosition: { x: 0, y: 0, z: 0 }, panType: k.panType ?? T.Stereo, source: null, stereoPanner: null, pannerNode: null }, this.sounds.set(V, _), L && (this.addToSoundGroup(L, V), this.debugLog(`Added new instance ${V} to group ${L}.`)); } const u = _ || O; if (!u) { this.debugLog(`Failed to create sound instance for ${t}`); return; } _ === void 0 && (u.playOptions = C), this.cleanupExistingSource(u.id); const M = this.setupAudioSource(u); if (!M) { this.debugLog(`Failed to create audio source for sound ${t}`); return; } const R = ((n = u.playOptions) == null ? void 0 : n.playbackRate) || 1; let w = 0; if (u.pausedAt !== void 0 && u.pausedAt !== 0 ? w = u.pausedAt : ((s = u.playOptions) == null ? void 0 : s.startTime) !== void 0 && (w = u.playOptions.startTime), u.startTime = this.context.currentTime - w / R, this.reconnectAudioNodes(u.id), u.state = f.Playing, ((a = u.playOptions) == null ? void 0 : a.volume) !== void 0 && this.setSoundVolume(u.id, u.playOptions.volume, !0), ((o = u.playOptions) == null ? void 0 : o.pan) !== void 0 && u.panType !== T.Spatial && this.setPan(u.id, u.playOptions.pan, !0), ((c = u.playOptions) == null ? void 0 : c.panSpatialPosition) !== void 0 && u.panType === T.Spatial && this.setSpatialPosition(u.playOptions.panSpatialPosition.x, u.playOptions.panSpatialPosition.y, u.playOptions.panSpatialPosition.z, u.id, void 0, !0), ((l = u.playOptions) == null ? void 0 : l.fadeInDuration) !== void 0 && this.fadeIn( u.id, ((h = u.playOptions) == null ? void 0 : h.fadeInDuration) ?? ((p = this.config) == null ? void 0 : p.fadeInDuration) ?? 1, void 0, // startVolume (defaults to 0 or current volume) (m = u.playOptions) == null ? void 0 : m.volume // Use PlayOptions.volume as the end volume ), ((g = u.playOptions) == null ? void 0 : g.fadeOutDuration) !== void 0 && this.fadeOut(u.id, u.playOptions.fadeOutDuration ?? ((S = this.config) == null ? void 0 : S.fadeOutDuration) ?? 1), ((P = u.playOptions) == null ? void 0 : P.playbackRate) !== void 0 && this.setPlaybackRate(u.id, R, !0), ((E = u.playOptions) == null ? void 0 : E.loop) !== void 0 && this.setLoop(u.id, u.playOptions.loop, u.playOptions.maxLoops), ((b = u.playOptions) == null ? void 0 : b.fadeOutBeforeEndDuration) !== void 0) { this.cancelScheduledFadeOut(u.id); const v = u.playOptions.fadeOutBeforeEndDuration, I = (u.playOptions.duration ?? ((D = u.buffer) == null ? void 0 : D.duration) ?? 0) - (u.pausedAt || 0) - v; I > 0 && this.scheduleFadeOut(u.id, I, u.playOptions.fadeOutBeforeEndDuration ?? 1); } return M.start( 0, w, (($ = u.playOptions) == null ? void 0 : $.duration) !== void 0 && u.playOptions.duration > 0 ? u.playOptions.duration * R : void 0 ), ((x = u.playOptions) == null ? void 0 : x.trackProgress) === !0 && this.startProgressTracking(u.id), i || this.dispatchEvent({ type: d.STARTED, soundId: t, timestamp: this.context.currentTime, sound: u }), u; } catch (O) { this.handleError("playing sound", O, t); } } playSprite(t, e, i, n = !1) { const s = `${t}_${e}`; this.play(s, i, n); } pause(t, e = !1) { var i; try { const n = this.getValidatedSound(t); if (!this.isPlaying(t) || this.isPaused(t)) return; this.cancelScheduledFadeOut(t); const s = ((i = n.playOptions) == null ? void 0 : i.playbackRate) ?? 1, a = (this.context.currentTime - (n.startTime || 0)) * s; n.currentTime = a, n.pausedAt = a, this.debugLog(`Pausing sound ${t}: Raw elapsed time: ${a} Adjusted time: ${a / s} StartTime: ${n.startTime} CurrentTime: ${n.currentTime} PausedAt: ${n.pausedAt} PlaybackRate: ${s} `), n.state = f.Paused, this.stopProgressTracking(t), this.cleanupExistingSource(t), e || this.dispatchEvent({ type: d.PAUSED, soundId: t, timestamp: this.context.currentTime, sound: n }); } catch (n) { this.handleError("pausing sound", n, t); } } resume(t, e = !1) { let i = this.getValidatedSound(t); this.play(t, i == null ? void 0 : i.playOptions); try { e || this.dispatchEvent({ type: d.RESUMED, soundId: t, timestamp: this.context.currentTime, sound: i }), this.debugLog(`Resumed sound ${t}:`); } catch (n) { this.handleError("resuming sound", n, t); } } stop(t, e = !1) { var i; try { const n = this.sounds.get(t); if (!n) { this.debugLog(`Sound ${t} not found for stopping`); return; } this.cancelScheduledFadeOut(t), this.cancelFadeAnimation(t), this.stopProgressTracking(t), this.cleanupExistingSource(t), n.state = f.Stopped, n.startTime = ((i = n.playOptions) == null ? void 0 : i.startTime) ?? 0, n.pausedAt = 0, n.currentTime = 0, this.removeEventListenersForInstance(t), this.resetCounterForSound(t), e || this.dispatchEvent({ type: d.STOPPED, soundId: t, timestamp: this.context.currentTime, sound: n }); } catch (n) { console.error(`Error stopping sound ${t}:`, n), this.dispatchEvent({ type: d.ERROR, timestamp: this.context.currentTime, error: new Error(`Failed to stop sound ${t}: ${n}`) }); } } seek(t, e, i = !1) { var n, s, a; try { const o = this.getValidatedSound(t), { duration: c, currentTime: l } = this.getSoundState(t); if (e >= c) { if (o.state === f.Stopped) return; o.state === f.Playing && ((n = o.playOptions) != null && n.loop) ? this.handleLoopIteration(o) : this.handleSoundEnded(o); return; } const h = ((s = o.buffer) == null ? void 0 : s.duration) || 0, p = ((a = o.playOptions) == null ? void 0 : a.playbackRate) || 1, m = e * p, g = Math.max(0, Math.min(m, h)); if (o.currentTime = g, o.pausedAt = g, o.state === f.Playing && (this.cleanupExistingSource(t), o.startTime = this.context.currentTime - g / p, this.play(t, o.playOptions)), i) return; this.dispatchEvent({ type: d.SEEKED, soundId: t, currentTime: l, timestamp: this.context.currentTime, sound: o }); } catch (o) { this.handleError("seeking sound", o, 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 === f.Playing && this.pause(e); }); } resumeAllSounds() { this.sounds.forEach((t, e) => { t.state == f.Paused && this.resume(e); }); } // End Playback control----------------------------------------------------------------------------------------------------------- // Fade managment ---------------------------------------------------------------------------------------------------------------- fadeIn(t, e, i, n, s = !1) { var h, p; const a = this.getValidatedSound(t); console.log("fade in", t), this.cancelFadeAnimation(t), a.isFadingOut = !1, a.isFadingIn = !0; const o = this.roundValue(a.gainNode.gain.value, 2); let c = n ?? a.volume ?? ((h = a.playOptions) == null ? void 0 : h.volume) ?? this.config.defaultVolume ?? 1; c === 0 && (c = 1); let l; i !== void 0 ? l = i : a.isFadingOut ? l = o : o >= c ? l = ((p = a.playOptions) == null ? void 0 : p.fadeInStartVolume) ?? 0 : l = o, a.gainNode.gain.cancelScheduledValues(this.context.currentTime), a.gainNode.gain.setValueAtTime(l, this.context.currentTime), a.state !== f.Playing && this.play(t, { volume: l }), this.fadeSound(t, l, c, e, () => { a.volume = c, a.playOptions && (a.playOptions.volume = c), s || this.dispatchEvent({ type: d.FADE_IN_COMPLETED, soundId: t, timestamp: this.context.currentTime, sound: a }); }); } fadeOut(t, e = this.config.fadeOutDuration, i, n, s = !1, a = !1) { var p; const o = this.getValidatedSound(t); this.cancelFadeAnimation(t), o.isFadingIn = !1, o.isFadingOut = !0; const c = this.roundValue(o.gainNode.gain.value, 2), l = i ?? c, h = n ?? ((p = o.playOptions) == null ? void 0 : p.fadeOutEndVolume) ?? 0; o.previousVolume = c, o.gainNode.gain.setValueAtTime(l, this.context.currentTime), o.state !== f.Playing && this.play(t, { volume: l }), this.fadeSound(t, l, h, e, () => { a || this.dispatchEvent({ type: d.FADE_OUT_COMPLETED, soundId: t, timestamp: this.context.currentTime, sound: o }), n === 0 && s && this.stop(t); }); } fadeSound(t, e, i, n, s) { try { this.cancelFadeAnimation(t); const a = this.getValidatedSound(t); a.volume = e, a.gainNode.gain.cancelScheduledValues(this.context.currentTime); const o = Math.max(0, n - 0.02), c = this.context.currentTime, l = c + o, h = () => { 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: d.VOLUME_CHANGED, soundId: t, timestamp: this.context.currentTime, volume: a.volume, sound: a }), this.activeFadeCallbacks.delete(t); }; this.activeFadeCallbacks.set(t, h); const p = `fade_${t}`, m = () => { const g = this.context.currentTime; if (g >= l) { this.ticker.removeCallback(p), h(); return; } const S = (g - c) / o, P = e + (i - e) * S; a.gainNode.gain.setValueAtTime(P, g), a.volume = this.roundValue(P), a.playOptions = { ...a.playOptions, volume: a.volume }, this.dispatchEvent({ type: d.VOLUME_CHANGED, soundId: t, timestamp: g, volume: a.volume, sound: a }); }; this.ticker.addCallback(p, m), this.debugLog(`Fade scheduled for sound ${t}: Start time: ${c} Duration: ${o} Start volume: ${e} Target volume: ${i} `); } catch (a) { this.handleError("fading sound", a, t); } } fadeGlobalIn(t = this.config.fadeInDuration, e, i) { try { const n = e ?? 0, s = i ?? (this.masterGainNode.gain.value || this.previousGlobalVolume); this.masterGainNode.gain.cancelScheduledValues(this.context.currentTime); const a = this.context.currentTime, o = t, c = a + o, l = "fade_global_in", h = () => { const p = this.context.currentTime; if (p >= c) { this.masterGainNode.gain.setValueAtTime(s, p), this.isMuted = !1, this.dispatchEvent({ type: d.FADE_MASTER_IN_COMPLETED, timestamp: p, volume: s }), this.ticker.removeCallback(l); return; } const m = (p - a) / o, g = n + (s - n) * m; this.masterGainNode.gain.setValueAtTime(g, p), this.dispatchEvent({ type: d.MASTER_VOLUME_CHANGED, timestamp: p, volume: g, isMaster: !0 }); }; this.ticker.addCallback(l, h); } catch (n) { this.handleError("fading in master volume", n); } } fadeGlobalOut(t = this.config.fadeOutDuration, e, i = 0) { if (t <= 0) { this.debugLog(`Invalid fade duration: ${t}`); return; } try { const n = e ?? this.roundValue(this.masterGainNode.gain.value, 2); this.previousGlobalVolume = n; const s = this.context.currentTime, a = t, o = s + a, c = "fade_global_out", l = () => { const h = this.context.currentTime; if (h >= o) { this.masterGainNode.gain.setValueAtTime(i, h), this.isMuted = i === 0, this.dispatchEvent({ type: d.FADE_MASTER_OUT_COMPLETED, timestamp: h, volume: i }), this.ticker.removeCallback(c); return; } const p = (h - s) / a, m = n + (i - n) * p; this.masterGainNode.gain.setValueAtTime(m, h), this.dispatchEvent({ type: d.MASTER_VOLUME_CHANGED, timestamp: h, volume: m, isMaster: !0 }); }; this.ticker.addCallback(c, l); } catch (n) { this.handleError("fading out master volume", n); } } // End Fading ------------------------------------------------------------------------------------------------------------------------ // Volume control----------------------------------------------------------------------------------------------------------------- getVolume(t) { return this.getSoundVolume(t); } setSoundVolume(t, e, i = !1) { try { this.cancelFadeAnimation(t); const n = this.getValidatedSound(t), s = this.setValidatedVolume(e); n.volume = this.roundValue(s), n.originalVolume = s, n.playOptions = { ...n.playOptions, volume: s }, n.gainNode.gain.cancelScheduledValues(this.context.currentTime), n.gainNode.gain.setValueAtTime(s, this.context.currentTime), i || this.dispatchEvent({ type: d.VOLUME_CHANGED, soundId: t, timestamp: this.context.currentTime, volume: n.volume, sound: n }); } catch (n) { this.handleError("setting volume", n, 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: d.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: d.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: d.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: d.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: d.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 n = this.sounds.get(t); if (!n) { this.debugLog(`Sound ${t} not found for setting loop`); return; } n.playOptions = { ...n.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) { const i = this.config.fetchStrategy === "proxy-first" && this.shouldUseProxy(e) ? ["proxy", "direct"] : ["direct"]; for (const n of i) try { const s = n === "proxy" || this.config.corsProxy ? this.getProxyUrl(e) : e; this.debugLog(`Trying ${n} strategy for ${t}`, { originalUrl: e, fetchUrl: s }); const a = await this.fetchWithRetry(s, { mode: "cors", credentials: "omit", cache: this.config.audioCache ? "default" : "no-cache", headers: { Accept: "audio/mpeg, audio/*" } }); return await this.processAudioResponse(t, a); } catch (s) { if (this.debugLog(`${n} strategy failed for ${t}`, { error: s instanceof Error ? s.message : String(s), url: e }), n === i[i.length - 1]) throw new Error(`Failed to load sound ${t}: ${s instanceof Error ? s.message : String(s)}`); } } async fetchWithRetry(t, e) { const { fetchRetries: i = 2, retryDelay: n = 0.5, fetchTimeout: s = 10 } = this.config; let a = null; const o = this.config.credentialStrategy === "auto" ? this.config.crossOrigin === "use-credentials" ? ["include", "omit"] : ["omit"] : [this.config.credentialStrategy || "omit"]; for (const c of o) for (let l = 0; l <= i; l++) { const h = Date.now(), p = new AbortController(), m = setTimeout(() => p.abort(), s * 1e3); try { const g = { ...e, credentials: c, signal: p.signal }; this.debugLog(`Attempt ${l + 1} with credentials=${c}`); const S = await fetch(t, g); if (clearTimeout(m), !S.ok) throw new Error(`HTTP ${S.status}`); if (e.mode === "cors") { const P = S.headers.get("access-control-allow-origin"); if (c === "include" && P === "*") throw new Error("Invalid CORS: Credentialed request with wildcard origin"); } return this.debugLog(`Success after ${Date.now() - h}ms`), S; } catch (g) { clearTimeout(m), a = g instanceof Error ? g : new Error(String(g)), this.debugLog(`Attempt ${l + 1} failed: ${a.message}`); } l < i && await new Promise((g) => setTimeout(g, n * 1e3)); } throw a || 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 n = e.headers.get("content-length"); if (n && this.config.maxAudioSize && parseInt(n) > this.config.maxAudioSize) throw new Error(`Audio file too large: ${n} bytes (max ${this.config.maxAudioSize} bytes), change the maxAudioSize config in your SoundManagerConfig`); const s = n ? parseInt(n) : void 0, a = await e.arrayBuffer(), o = await this.context.decodeAudioData(a); this.createSoundNode(t, o, s), this.debugLog(`Sound ${t} loaded successfully`); } async loadWithHtml5Audio(t, e) { if (!this.config.html5AudioFallback) throw new Error("HTML5 Audio fallback is disabled in configuration"); return new Promise((i, n) => { const s = new Audio(); s.crossOrigin = this.config.crossOrigin === "use-credentials" ? "use-credentials" : "anonymous", s.preload = "auto", s.src = e; const a = () => { s.oncanplaythrough = null, s.onerror = null, s.removeEventListener("error", o); }, o = () => { a(), n(new Error(`HTML5 Audio load error: ${s.error ? s.error.message : "unknown"}`)); }; s.oncanplaythrough = async () => { a(); try { await this.loadWithWebAudio(t, e), this.debugLog(`Sound ${t} loaded with HTML5 Audio fallback`), i(); } catch (c) { n(c); } }, s.onerror = o, s.addEventListener("error", o), s.load(); }); } async loadSounds(t) { if (t.length) try { const { maxParallelLoads: e = 10, webAudioPreferred: i = !0 } = this.config, n = Math.max(1, e), s = []; for (let a = 0; a < t.length; a += n) s.push(t.slice(a, a + n)); for (const a of s) { const o = a.map(async ({ id: h, url: p }) => { if (this.sounds.has(h)) { this.debugLog(`Sound with id ${h} already exists. Skipping.`); return; } try { if (i) try { await this.loadWithWebAudio(h, p); return; } catch (m) { this.debugLog(`Web Audio load failed for ${h}`, m); } await this.loadWithHtml5Audio(h, p); } catch (m) { throw this.handleError("loading sound", m, h), m; } }), c = await Promise.allSettled(o); if (c.filter((h) => h.status === "rejected").length) { const h = a.filter((p, m) => c[m].status === "rejected").map((p) => p.id); throw new Error(`Failed to load sounds: ${h.join(", ")}`); } } } catch (e) { throw this.handleError("preloading sounds", e), e; } } calculateAudioSize(t) { return t.numberOfChannels * t.length * 4; } createSoundNode(t, e, i) { const n = this.context.createGain(); n.gain.value = this.config.defaultVolume ?? 1, n.connect(this.masterGainNode); const s = this.context.createBufferSource(); s.buffer = e; const a = this.calculateAudioSize(e), o = { id: t, buffer: e, gainNode: n, source: s, startTime: void 0, currentTime: 0, pausedAt: void 0, state: f.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 ?? T.Stereo }; this.sounds.set(t, o), this.dispatchEvent({ type: d.LOADED, soundId: t, timestamp: this.context.currentTime, sound: o, duration: e.duration, bufferSize: a, fileSize: i, sampleRate: e.sampleRate, channels: e.numberOfChannels }); } async loadSound(t, e) { try { await this.loadSounds([{ id: t, url: e }]); } catch (i) { throw this.handleError("loading sound", i, t), i; } } 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: d.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: d.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 n = this.sounds.get(e); n && (n.groupId = t), n && i.playOptions && (n.playOptions = { ...i.playOptions, ...n.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) { try { const i = this.getValidatedSound(t); if (!i || !i.buffer) throw new Error(`Sound ${t} not found or buffer not loaded`); Object.entries(e).forEach(([n, [s, a]]) => { const o = `${t}_${n}`; this.debugLog( `Creating sprite ${o} for sound ${t}: Start=${s}ms, End=${a}ms, Duration=${a - s}ms` ); const c = i.buffer.sampleRate, l = Math.floor(s * c), h = Math.floor(a * c), p = a - s, m = i.buffer.numberOfChannels, g = this.context.createBuffer(m, h - l, c); for (let b = 0; b < m; b++) { const D = i.buffer.getChannelData(b), $ = g.getChannelData(b); for (let x = 0; x < $.length; x++) $[x] = D[x + l]; } const S = this.context.createGain(), P = i.volume ?? this.config.defaultVolume ?? 1; S.gain.value = P, S.connect(this.masterGainNode); const E = { id: o, buffer: g, currentTime: 0, source: this.context.createBufferSource(), // volume: volume, originalVolume: P, state: f.Stopped, gainNode: S, // duration: duration, playOptions: { ...i.playOptions }, currentLoopCount: 0, panSpatialPosition: this.config.defaultPanSpatialPosition || { x: 0, y: 0, z: 0 }, pan: i.pan ?? this.config.defaultPan ?? 0 // pausedAt: 0, //this.context.currentTime // startTime: 0, }; if (i.stereoPanner) { const b = this.context.createStereoPanner(); b.connect(S), E.stereoPanner = b; } this.sounds.set(o, E), this.debugLog(`Created sprite sound ${o}: Duration: ${p}s Sample rate: ${c} Channels: ${m} Buffer length: ${g.length} samples `), this.dispatchEvent({ type: d.SPRITE_SET, soundId: o, timestamp: this.context.currentTime, sound: E }); }); } catch (i) { this.handleError("setting sound sprite", i, t); } } getSpriteConfig(t) { try { const e = this.getValidate