From 87e98cdd16579afb1653c0b1d60119f29670995b Mon Sep 17 00:00:00 2001 From: Cory Locklear Date: Thu, 11 Jun 2026 11:02:30 -0400 Subject: [PATCH] Preload audio clips on mount so first playthrough timing stays in sync. --- src/mediaSounds.ts | 40 ++++++++++++++++++++++++++++++++++++++++ src/useMediaConfig.ts | 5 +++++ 2 files changed, 45 insertions(+) diff --git a/src/mediaSounds.ts b/src/mediaSounds.ts index 33e185f..81edcc2 100644 --- a/src/mediaSounds.ts +++ b/src/mediaSounds.ts @@ -1,6 +1,46 @@ import type { MediaConfig } from './mediaConfig'; +const audioElements = new Map(); + +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; diff --git a/src/useMediaConfig.ts b/src/useMediaConfig.ts index 6158582..6abf766 100644 --- a/src/useMediaConfig.ts +++ b/src/useMediaConfig.ts @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; import { DEFAULT_MEDIA_CONFIG, mergeMediaConfig, type MediaConfig } from './mediaConfig'; +import { preloadAudioAssets } from './mediaSounds'; export function useMediaConfig(): MediaConfig { const [config, setConfig] = useState(DEFAULT_MEDIA_CONFIG); @@ -22,5 +23,9 @@ export function useMediaConfig(): MediaConfig { }; }, []); + useEffect(() => { + preloadAudioAssets(config); + }, [config]); + return config; }