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
JavaScript
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