aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/audio/state.ts14
-rw-r--r--lib/components/AudioPlayer.svelte53
-rw-r--r--lib/screens/BeginScreen.svelte6
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>