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