diff options
| -rw-r--r-- | lib/audio/state.ts | 14 | ||||
| -rw-r--r-- | lib/components/AudioPlayer.svelte | 53 | ||||
| -rw-r--r-- | lib/screens/BeginScreen.svelte | 6 |
3 files changed, 73 insertions, 0 deletions
diff --git a/lib/audio/state.ts b/lib/audio/state.ts new file mode 100644 index 0000000..7ad273c --- /dev/null +++ b/lib/audio/state.ts @@ -0,0 +1,14 @@ +import { writable, type Writable } from 'svelte/store' + +/** + * Master mute. When true, every audio source is silenced regardless of + * category volume. Surfaced on the Settings screen as a single toggle. + */ +export const masterMuted: Writable<boolean> = writable(false) + +/** + * Volume for ambient layers — title music, location textures, region + * beds. 0..1. Ceremonial pieces (death, memoir) use a separate store + * when that category comes online. + */ +export const ambientVolume: Writable<number> = writable(0.6) diff --git a/lib/components/AudioPlayer.svelte b/lib/components/AudioPlayer.svelte new file mode 100644 index 0000000..d5aa1d0 --- /dev/null +++ b/lib/components/AudioPlayer.svelte @@ -0,0 +1,53 @@ +<script lang="ts"> + import { onDestroy, onMount } from 'svelte' + import { masterMuted, ambientVolume } from '@hollowdark/lib/audio/state' + + interface Props { + src: string + loop?: boolean + } + + let { src, loop = false }: Props = $props() + + let audioEl: HTMLAudioElement | null = null + let cleanupInteractionListeners: (() => void) | null = null + + onMount(async () => { + if (!audioEl) return + + audioEl.volume = $ambientVolume + audioEl.muted = $masterMuted + + try { + await audioEl.play() + } catch { + const start = (): void => { + audioEl?.play().catch(() => {}) + } + document.addEventListener('click', start, { once: true, passive: true }) + document.addEventListener('keydown', start, { once: true, passive: true }) + document.addEventListener('touchstart', start, { once: true, passive: true }) + + cleanupInteractionListeners = () => { + document.removeEventListener('click', start) + document.removeEventListener('keydown', start) + document.removeEventListener('touchstart', start) + } + } + }) + + onDestroy(() => { + cleanupInteractionListeners?.() + if (audioEl) { + audioEl.pause() + } + }) + + $effect(() => { + if (!audioEl) return + audioEl.volume = $ambientVolume + audioEl.muted = $masterMuted + }) +</script> + +<audio bind:this={audioEl} {src} {loop} preload="auto"></audio> diff --git a/lib/screens/BeginScreen.svelte b/lib/screens/BeginScreen.svelte index 5832a42..08ab565 100644 --- a/lib/screens/BeginScreen.svelte +++ b/lib/screens/BeginScreen.svelte @@ -1,6 +1,8 @@ <script lang="ts"> + import { base } from '$app/paths' import AppTitle from '@hollowdark/lib/components/AppTitle.svelte' import AppVersion from '@hollowdark/lib/components/AppVersion.svelte' + import AudioPlayer from '@hollowdark/lib/components/AudioPlayer.svelte' import BeginActions from '@hollowdark/lib/components/BeginActions.svelte' import type { BeginState } from '@hollowdark/loading/session' @@ -19,6 +21,8 @@ onSettings, onCredits }: Props = $props() + + const titleTrackSrc = `${base}/audio/title/piano-relaxing.mp3` </script> <section class="begin"> @@ -29,6 +33,8 @@ <BeginActions {state} {onBegin} {onContinue} {onSettings} {onCredits} /> <AppVersion /> + + <AudioPlayer src={titleTrackSrc} loop /> </section> <style> |
