72 lines
1.7 KiB
TypeScript
72 lines
1.7 KiB
TypeScript
import type { MediaConfig } from './mediaConfig';
|
|
|
|
const audioElements = new Map<string, HTMLAudioElement>();
|
|
|
|
function ensureAudio(src: string): HTMLAudioElement {
|
|
let audio = audioElements.get(src);
|
|
if (!audio) {
|
|
audio = new Audio();
|
|
audio.preload = 'auto';
|
|
audio.src = src;
|
|
audioElements.set(src, audio);
|
|
}
|
|
return audio;
|
|
}
|
|
|
|
const AUDIO_URL_KEYS = [
|
|
'incorrectUrl',
|
|
'unlockConfirmUrl',
|
|
'celebrationPopUrl',
|
|
'celebrationHornUrl',
|
|
] as const satisfies readonly (keyof MediaConfig)[];
|
|
|
|
/** Fetch and decode audio clips so first playback is not delayed. */
|
|
export function preloadAudioAssets(config: MediaConfig): void {
|
|
for (const key of AUDIO_URL_KEYS) {
|
|
ensureAudio(config[key]).load();
|
|
}
|
|
}
|
|
|
|
export function playClip(src: string): HTMLAudioElement {
|
|
const cached = audioElements.get(src);
|
|
if (cached) {
|
|
if (cached.paused || cached.ended) {
|
|
cached.currentTime = 0;
|
|
void cached.play();
|
|
return cached;
|
|
}
|
|
|
|
const fallback = new Audio(src);
|
|
void fallback.play();
|
|
return fallback;
|
|
}
|
|
|
|
const audio = new Audio(src);
|
|
void audio.play();
|
|
return audio;
|
|
}
|
|
|
|
export function playIncorrectSound(incorrectUrl: string) {
|
|
playClip(incorrectUrl);
|
|
}
|
|
|
|
type CelebrationConfig = Pick<
|
|
MediaConfig,
|
|
| 'unlockConfirmUrl'
|
|
| 'celebrationPopUrl'
|
|
| 'celebrationHornUrl'
|
|
| 'celebrationHornDurationMs'
|
|
>;
|
|
|
|
/** Confirm, pop, and horn together; horn is trimmed — ends before unlock video audio. */
|
|
export function playCelebrationSounds(config: CelebrationConfig) {
|
|
playClip(config.unlockConfirmUrl);
|
|
playClip(config.celebrationPopUrl);
|
|
|
|
const horn = playClip(config.celebrationHornUrl);
|
|
window.setTimeout(() => {
|
|
horn.pause();
|
|
horn.currentTime = 0;
|
|
}, config.celebrationHornDurationMs);
|
|
}
|