update
This commit is contained in:
@@ -1,85 +0,0 @@
|
||||
import { logger } from "@/utils/logger";
|
||||
|
||||
export class AudioManager {
|
||||
private static _instance: AudioManager | null = null;
|
||||
private readonly _audioPools = new Map<string, HTMLAudioElement[]>();
|
||||
|
||||
private static readonly MAX_POOL_SIZE_PER_SOUND = 6;
|
||||
private static readonly IGNORED_PLAYBACK_ERRORS = new Set([
|
||||
"AbortError",
|
||||
"NotAllowedError",
|
||||
]);
|
||||
|
||||
static getInstance(): AudioManager {
|
||||
if (!AudioManager._instance) {
|
||||
AudioManager._instance = new AudioManager();
|
||||
}
|
||||
|
||||
return AudioManager._instance;
|
||||
}
|
||||
|
||||
private constructor() {}
|
||||
|
||||
playSound(path: string, volume = 1): void {
|
||||
const audio = this._acquireAudio(path);
|
||||
audio.volume = Math.max(0, Math.min(1, volume));
|
||||
audio.currentTime = 0;
|
||||
|
||||
void audio.play().catch((error: unknown) => {
|
||||
if (
|
||||
error instanceof DOMException &&
|
||||
AudioManager.IGNORED_PLAYBACK_ERRORS.has(error.name)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.error("AudioManager", "Failed to play sound", {
|
||||
path,
|
||||
error: AudioManager._toLogValue(error),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this._audioPools.forEach((pool) => {
|
||||
pool.forEach((audio) => {
|
||||
audio.pause();
|
||||
audio.src = "";
|
||||
});
|
||||
});
|
||||
this._audioPools.clear();
|
||||
AudioManager._instance = null;
|
||||
}
|
||||
|
||||
private _acquireAudio(path: string): HTMLAudioElement {
|
||||
const existingPool = this._audioPools.get(path);
|
||||
|
||||
if (existingPool) {
|
||||
const availableAudio = existingPool.find(
|
||||
(audio) => audio.paused || audio.ended,
|
||||
);
|
||||
if (availableAudio) return availableAudio;
|
||||
|
||||
if (existingPool.length < AudioManager.MAX_POOL_SIZE_PER_SOUND) {
|
||||
const pooledAudio = new Audio(path);
|
||||
existingPool.push(pooledAudio);
|
||||
return pooledAudio;
|
||||
}
|
||||
|
||||
const recycledAudio = existingPool[0];
|
||||
if (recycledAudio) return recycledAudio;
|
||||
}
|
||||
|
||||
const initialAudio = new Audio(path);
|
||||
this._audioPools.set(path, [initialAudio]);
|
||||
return initialAudio;
|
||||
}
|
||||
|
||||
private static _toLogValue(error: unknown): Error | DOMException | string {
|
||||
if (error instanceof Error || error instanceof DOMException) {
|
||||
return error;
|
||||
}
|
||||
|
||||
return String(error);
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import type {
|
||||
GrabInteractableHandle,
|
||||
InteractableHandle,
|
||||
InteractionSnapshot,
|
||||
} from "@/types/interaction";
|
||||
|
||||
export class InteractionManager {
|
||||
private static _instance: InteractionManager | null = null;
|
||||
|
||||
private _focused: InteractableHandle | null = null;
|
||||
private _holding = false;
|
||||
private _holdingHandle: GrabInteractableHandle | null = null;
|
||||
private _snapshot: InteractionSnapshot = {
|
||||
focused: null,
|
||||
holding: false,
|
||||
};
|
||||
private readonly _listeners = new Set<() => void>();
|
||||
|
||||
static getInstance(): InteractionManager {
|
||||
if (!InteractionManager._instance) {
|
||||
InteractionManager._instance = new InteractionManager();
|
||||
}
|
||||
|
||||
return InteractionManager._instance;
|
||||
}
|
||||
|
||||
private constructor() {}
|
||||
|
||||
getState(): InteractionSnapshot {
|
||||
return this._snapshot;
|
||||
}
|
||||
|
||||
setFocused(handle: InteractableHandle | null): void {
|
||||
if (this._focused === handle) return;
|
||||
if (this._holding) return;
|
||||
|
||||
this._focused = handle;
|
||||
this._emit();
|
||||
}
|
||||
|
||||
pressInteract(): void {
|
||||
if (!this._focused) return;
|
||||
|
||||
if (this._focused.kind === "grab") {
|
||||
this._holding = true;
|
||||
this._holdingHandle = this._focused;
|
||||
} else {
|
||||
this._holding = false;
|
||||
this._holdingHandle = null;
|
||||
}
|
||||
|
||||
this._focused.onPress();
|
||||
this._emit();
|
||||
}
|
||||
|
||||
releaseInteract(): void {
|
||||
const handle = this._holding ? this._holdingHandle : null;
|
||||
if (!handle) return;
|
||||
|
||||
handle.onRelease();
|
||||
this._holding = false;
|
||||
this._holdingHandle = null;
|
||||
this._emit();
|
||||
}
|
||||
|
||||
subscribe(listener: () => void): () => void {
|
||||
this._listeners.add(listener);
|
||||
|
||||
return () => {
|
||||
this._listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this._focused = null;
|
||||
this._holding = false;
|
||||
this._holdingHandle = null;
|
||||
this._snapshot = {
|
||||
focused: null,
|
||||
holding: false,
|
||||
};
|
||||
this._listeners.clear();
|
||||
InteractionManager._instance = null;
|
||||
}
|
||||
|
||||
private _emit(): void {
|
||||
this._snapshot = {
|
||||
focused: this._focused,
|
||||
holding: this._holding,
|
||||
};
|
||||
this._listeners.forEach((cb) => cb());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user