@adea-ai/audio 0.11.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.js +2 -2
- package/dist/controller.d.ts +1 -1
- package/dist/controller.js +8 -8
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/react.d.ts +3 -3
- package/dist/react.js +15 -14
- package/dist/scene-music.d.ts +1 -1
- package/dist/scene-music.js +3 -3
- package/package.json +8 -8
- package/src/config.ts +5 -5
- package/src/controller.ts +76 -76
- package/src/index.ts +4 -4
- package/src/react.tsx +47 -47
- package/src/scene-music.ts +5 -5
package/dist/config.js
CHANGED
package/dist/controller.d.ts
CHANGED
package/dist/controller.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { MUSIC_DIR, MUSIC_FILES } from
|
|
2
|
-
const MUSIC_MUTE_KEY =
|
|
3
|
-
const LEGACY_MUTE_KEY =
|
|
1
|
+
import { MUSIC_DIR, MUSIC_FILES } from './config';
|
|
2
|
+
const MUSIC_MUTE_KEY = 'adea:audio:music-muted';
|
|
3
|
+
const LEGACY_MUTE_KEY = 'adea:audio:muted';
|
|
4
4
|
const DEFAULT_MUSIC_VOLUME = 0.8;
|
|
5
5
|
const FADE_STEP_MS = 30;
|
|
6
6
|
function clampVolume(value) {
|
|
7
7
|
return value <= 0 ? 0 : value > 1 ? 1 : value;
|
|
8
8
|
}
|
|
9
9
|
function readStorage(key) {
|
|
10
|
-
if (typeof window ===
|
|
10
|
+
if (typeof window === 'undefined')
|
|
11
11
|
return null;
|
|
12
12
|
try {
|
|
13
13
|
return window.localStorage.getItem(key);
|
|
@@ -17,11 +17,11 @@ function readStorage(key) {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
function writeStorage(key, muted) {
|
|
20
|
-
if (typeof window ===
|
|
20
|
+
if (typeof window === 'undefined')
|
|
21
21
|
return;
|
|
22
22
|
try {
|
|
23
23
|
if (muted)
|
|
24
|
-
window.localStorage.setItem(key,
|
|
24
|
+
window.localStorage.setItem(key, '1');
|
|
25
25
|
else
|
|
26
26
|
window.localStorage.removeItem(key);
|
|
27
27
|
}
|
|
@@ -30,7 +30,7 @@ function writeStorage(key, muted) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
function loadMusicMuted() {
|
|
33
|
-
return readStorage(MUSIC_MUTE_KEY) ===
|
|
33
|
+
return readStorage(MUSIC_MUTE_KEY) === '1' || readStorage(LEGACY_MUTE_KEY) === '1';
|
|
34
34
|
}
|
|
35
35
|
export class SoundController {
|
|
36
36
|
musicElements = new Map();
|
|
@@ -78,7 +78,7 @@ export class SoundController {
|
|
|
78
78
|
element.loop = true;
|
|
79
79
|
// Defer the request until playback is allowed so audio never competes with
|
|
80
80
|
// the scene's first visual and physics assets.
|
|
81
|
-
element.preload =
|
|
81
|
+
element.preload = 'none';
|
|
82
82
|
this.musicElements.set(id, element);
|
|
83
83
|
}
|
|
84
84
|
startMusic(id, options = {}) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { AUDIO_BASE_URL, MUSIC_FILES, type MusicId } from
|
|
2
|
-
export { SoundController, soundController, type MusicOptions } from
|
|
3
|
-
export { musicForScene, sceneMusicTracks } from
|
|
4
|
-
export { MusicToggle, SoundProvider, useSceneMusic, useSound, type SoundContextValue, } from
|
|
1
|
+
export { AUDIO_BASE_URL, MUSIC_FILES, type MusicId } from './config';
|
|
2
|
+
export { SoundController, soundController, type MusicOptions } from './controller';
|
|
3
|
+
export { musicForScene, sceneMusicTracks } from './scene-music';
|
|
4
|
+
export { MusicToggle, SoundProvider, useSceneMusic, useSound, type SoundContextValue, } from './react';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Optional soundtrack hooks for Adea.
|
|
2
|
-
export { AUDIO_BASE_URL, MUSIC_FILES } from
|
|
3
|
-
export { SoundController, soundController } from
|
|
4
|
-
export { musicForScene, sceneMusicTracks } from
|
|
5
|
-
export { MusicToggle, SoundProvider, useSceneMusic, useSound, } from
|
|
2
|
+
export { AUDIO_BASE_URL, MUSIC_FILES } from './config';
|
|
3
|
+
export { SoundController, soundController } from './controller';
|
|
4
|
+
export { musicForScene, sceneMusicTracks } from './scene-music';
|
|
5
|
+
export { MusicToggle, SoundProvider, useSceneMusic, useSound, } from './react';
|
package/dist/react.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type ReactNode } from
|
|
2
|
-
import { soundController, type MusicOptions } from
|
|
3
|
-
import type { MusicId } from
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { soundController, type MusicOptions } from './controller';
|
|
3
|
+
import type { MusicId } from './config';
|
|
4
4
|
export type SoundContextValue = {
|
|
5
5
|
controller: typeof soundController;
|
|
6
6
|
ready: boolean;
|
package/dist/react.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { createContext, useContext, useEffect, useMemo, useState } from
|
|
4
|
-
import { Music, Music2 } from
|
|
5
|
-
import { soundController } from
|
|
6
|
-
import { musicForScene } from
|
|
3
|
+
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { Music, Music2 } from 'lucide-react';
|
|
5
|
+
import { soundController } from './controller';
|
|
6
|
+
import { musicForScene } from './scene-music';
|
|
7
7
|
const SoundContext = createContext(null);
|
|
8
8
|
export function SoundProvider({ children }) {
|
|
9
9
|
const [musicMuted, setMusicMuted] = useState(false);
|
|
@@ -20,18 +20,19 @@ export function SoundProvider({ children }) {
|
|
|
20
20
|
setReady(true);
|
|
21
21
|
});
|
|
22
22
|
};
|
|
23
|
-
window.addEventListener(
|
|
24
|
-
window.addEventListener(
|
|
25
|
-
window.addEventListener(
|
|
26
|
-
if (new URLSearchParams(window.location.search).has(
|
|
23
|
+
window.addEventListener('pointerdown', onGesture, { capture: true });
|
|
24
|
+
window.addEventListener('keydown', onGesture, { capture: true });
|
|
25
|
+
window.addEventListener('touchstart', onGesture, { capture: true });
|
|
26
|
+
if (new URLSearchParams(window.location.search).has('debug')) {
|
|
27
|
+
;
|
|
27
28
|
window.__agentHqSound =
|
|
28
29
|
soundController;
|
|
29
30
|
}
|
|
30
31
|
return () => {
|
|
31
32
|
mounted = false;
|
|
32
|
-
window.removeEventListener(
|
|
33
|
-
window.removeEventListener(
|
|
34
|
-
window.removeEventListener(
|
|
33
|
+
window.removeEventListener('pointerdown', onGesture, { capture: true });
|
|
34
|
+
window.removeEventListener('keydown', onGesture, { capture: true });
|
|
35
|
+
window.removeEventListener('touchstart', onGesture, { capture: true });
|
|
35
36
|
};
|
|
36
37
|
}, []);
|
|
37
38
|
const value = useMemo(() => ({
|
|
@@ -46,7 +47,7 @@ export function SoundProvider({ children }) {
|
|
|
46
47
|
export function useSound() {
|
|
47
48
|
const value = useContext(SoundContext);
|
|
48
49
|
if (!value)
|
|
49
|
-
throw new Error(
|
|
50
|
+
throw new Error('useSound must be used within <SoundProvider>');
|
|
50
51
|
return value;
|
|
51
52
|
}
|
|
52
53
|
export function useSceneMusic(sceneId) {
|
|
@@ -58,7 +59,7 @@ export function useSceneMusic(sceneId) {
|
|
|
58
59
|
}, [sceneId]);
|
|
59
60
|
}
|
|
60
61
|
function MusicButton({ muted, onToggle }) {
|
|
61
|
-
return (_jsx("button", { type: "button", "aria-label": muted ?
|
|
62
|
+
return (_jsx("button", { type: "button", "aria-label": muted ? 'Unmute music' : 'Mute music', "aria-pressed": muted, onClick: onToggle, className: `inline-flex size-9 items-center justify-center rounded-lg border border-input bg-background transition-colors ${muted ? 'text-muted-foreground hover:text-foreground' : 'bg-primary text-primary-foreground'}`, children: muted ? (_jsx(Music, { className: "size-5", "aria-hidden": "true" })) : (_jsx(Music2, { className: "size-5", "aria-hidden": "true" })) }));
|
|
62
63
|
}
|
|
63
64
|
export function MusicToggle() {
|
|
64
65
|
const { musicMuted, toggleMusicMute } = useSound();
|
package/dist/scene-music.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MusicId } from
|
|
1
|
+
import type { MusicId } from './config';
|
|
2
2
|
/** Scene route id -> optional track. Deployments can add tracks without changing scene code. */
|
|
3
3
|
export declare const sceneMusicTracks: Record<string, MusicId>;
|
|
4
4
|
export declare function musicForScene(sceneId: string | null | undefined): MusicId;
|
package/dist/scene-music.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/** Scene route id -> optional track. Deployments can add tracks without changing scene code. */
|
|
2
2
|
export const sceneMusicTracks = {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
'hq-home': 'silent',
|
|
4
|
+
'hq-work': 'silent',
|
|
5
5
|
};
|
|
6
6
|
export function musicForScene(sceneId) {
|
|
7
|
-
return sceneMusicTracks[sceneId ??
|
|
7
|
+
return sceneMusicTracks[sceneId ?? ''] ?? 'silent';
|
|
8
8
|
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adea-ai/audio",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"types": "dist/index.d.ts",
|
|
3
|
+
"version": "0.12.1",
|
|
7
4
|
"files": [
|
|
8
5
|
"dist",
|
|
9
6
|
"src",
|
|
10
7
|
"tsconfig.json",
|
|
11
8
|
"README.md"
|
|
12
9
|
],
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "dist/index.js",
|
|
12
|
+
"types": "dist/index.d.ts",
|
|
13
13
|
"exports": {
|
|
14
14
|
".": {
|
|
15
15
|
"types": "./dist/index.d.ts",
|
|
@@ -18,16 +18,16 @@
|
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
|
-
"lint": "
|
|
21
|
+
"lint": "oxlint",
|
|
22
22
|
"build": "tsc -p tsconfig.json",
|
|
23
23
|
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
|
24
24
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
25
25
|
},
|
|
26
|
-
"devDependencies": {
|
|
27
|
-
"@types/react": "^19.2.18"
|
|
28
|
-
},
|
|
29
26
|
"dependencies": {
|
|
30
27
|
"lucide-react": "^1.33.0",
|
|
31
28
|
"react": "19.2.8"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/react": "^19.2.18"
|
|
32
32
|
}
|
|
33
33
|
}
|
package/src/config.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/** Optional soundtrack hooks for Adea. */
|
|
2
|
-
export const AUDIO_BASE_URL =
|
|
3
|
-
export const MUSIC_DIR = `${AUDIO_BASE_URL}/sounds/music
|
|
2
|
+
export const AUDIO_BASE_URL = '/assets/audio'
|
|
3
|
+
export const MUSIC_DIR = `${AUDIO_BASE_URL}/sounds/music`
|
|
4
4
|
|
|
5
5
|
export const MUSIC_FILES = {
|
|
6
|
-
silent:
|
|
7
|
-
} as const
|
|
6
|
+
silent: '',
|
|
7
|
+
} as const
|
|
8
8
|
|
|
9
|
-
export type MusicId = keyof typeof MUSIC_FILES
|
|
9
|
+
export type MusicId = keyof typeof MUSIC_FILES
|
package/src/controller.ts
CHANGED
|
@@ -1,144 +1,144 @@
|
|
|
1
|
-
import { MUSIC_DIR, MUSIC_FILES, type MusicId } from
|
|
1
|
+
import { MUSIC_DIR, MUSIC_FILES, type MusicId } from './config'
|
|
2
2
|
|
|
3
3
|
export type MusicOptions = {
|
|
4
4
|
/** 0..1 track gain (default 0.8). */
|
|
5
|
-
volume?: number
|
|
5
|
+
volume?: number
|
|
6
6
|
/** Crossfade/fade duration in ms (default 500). */
|
|
7
|
-
fadeMs?: number
|
|
8
|
-
}
|
|
7
|
+
fadeMs?: number
|
|
8
|
+
}
|
|
9
9
|
|
|
10
|
-
const MUSIC_MUTE_KEY =
|
|
11
|
-
const LEGACY_MUTE_KEY =
|
|
12
|
-
const DEFAULT_MUSIC_VOLUME = 0.8
|
|
13
|
-
const FADE_STEP_MS = 30
|
|
10
|
+
const MUSIC_MUTE_KEY = 'adea:audio:music-muted'
|
|
11
|
+
const LEGACY_MUTE_KEY = 'adea:audio:muted'
|
|
12
|
+
const DEFAULT_MUSIC_VOLUME = 0.8
|
|
13
|
+
const FADE_STEP_MS = 30
|
|
14
14
|
|
|
15
15
|
function clampVolume(value: number): number {
|
|
16
|
-
return value <= 0 ? 0 : value > 1 ? 1 : value
|
|
16
|
+
return value <= 0 ? 0 : value > 1 ? 1 : value
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
function readStorage(key: string): string | null {
|
|
20
|
-
if (typeof window ===
|
|
20
|
+
if (typeof window === 'undefined') return null
|
|
21
21
|
try {
|
|
22
|
-
return window.localStorage.getItem(key)
|
|
22
|
+
return window.localStorage.getItem(key)
|
|
23
23
|
} catch {
|
|
24
|
-
return null
|
|
24
|
+
return null
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function writeStorage(key: string, muted: boolean): void {
|
|
29
|
-
if (typeof window ===
|
|
29
|
+
if (typeof window === 'undefined') return
|
|
30
30
|
try {
|
|
31
|
-
if (muted) window.localStorage.setItem(key,
|
|
32
|
-
else window.localStorage.removeItem(key)
|
|
31
|
+
if (muted) window.localStorage.setItem(key, '1')
|
|
32
|
+
else window.localStorage.removeItem(key)
|
|
33
33
|
} catch {
|
|
34
34
|
// Storage can be unavailable; the in-memory mute still applies.
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
function loadMusicMuted(): boolean {
|
|
39
|
-
return readStorage(MUSIC_MUTE_KEY) ===
|
|
39
|
+
return readStorage(MUSIC_MUTE_KEY) === '1' || readStorage(LEGACY_MUTE_KEY) === '1'
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export class SoundController {
|
|
43
|
-
private musicElements = new Map<MusicId, HTMLAudioElement>()
|
|
44
|
-
private desiredMusic: MusicId | null = null
|
|
45
|
-
private currentMusic: MusicId | null = null
|
|
46
|
-
private activeFades = new Set<number>()
|
|
47
|
-
private _musicMuted: boolean
|
|
43
|
+
private musicElements = new Map<MusicId, HTMLAudioElement>()
|
|
44
|
+
private desiredMusic: MusicId | null = null
|
|
45
|
+
private currentMusic: MusicId | null = null
|
|
46
|
+
private activeFades = new Set<number>()
|
|
47
|
+
private _musicMuted: boolean
|
|
48
48
|
|
|
49
49
|
constructor() {
|
|
50
|
-
this._musicMuted = loadMusicMuted()
|
|
50
|
+
this._musicMuted = loadMusicMuted()
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
get musicMuted(): boolean {
|
|
54
|
-
return this._musicMuted
|
|
54
|
+
return this._musicMuted
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/** Retry queued playback after a user gesture. */
|
|
58
58
|
async unlock(): Promise<void> {
|
|
59
|
-
this.startDesiredMusic()
|
|
59
|
+
this.startDesiredMusic()
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/** Start or switch to a looping soundtrack. */
|
|
63
63
|
playMusic(id: MusicId, options: MusicOptions = {}): void {
|
|
64
|
-
if (!MUSIC_FILES[id]) return
|
|
65
|
-
this.desiredMusic = id
|
|
66
|
-
this.prepareMusicElement(id)
|
|
67
|
-
if (!this.musicMuted) this.startMusic(id, options)
|
|
64
|
+
if (!MUSIC_FILES[id]) return
|
|
65
|
+
this.desiredMusic = id
|
|
66
|
+
this.prepareMusicElement(id)
|
|
67
|
+
if (!this.musicMuted) this.startMusic(id, options)
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
setMusicMuted(muted: boolean): void {
|
|
71
|
-
this._musicMuted = muted
|
|
72
|
-
writeStorage(MUSIC_MUTE_KEY, muted)
|
|
71
|
+
this._musicMuted = muted
|
|
72
|
+
writeStorage(MUSIC_MUTE_KEY, muted)
|
|
73
73
|
for (const element of this.musicElements.values()) {
|
|
74
|
-
if (muted) element.pause()
|
|
74
|
+
if (muted) element.pause()
|
|
75
75
|
}
|
|
76
|
-
if (!muted) this.startDesiredMusic()
|
|
76
|
+
if (!muted) this.startDesiredMusic()
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
toggleMusicMute(): boolean {
|
|
80
|
-
this.setMusicMuted(!this._musicMuted)
|
|
81
|
-
return this._musicMuted
|
|
80
|
+
this.setMusicMuted(!this._musicMuted)
|
|
81
|
+
return this._musicMuted
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
private prepareMusicElement(id: MusicId): void {
|
|
85
|
-
if (this.musicElements.has(id)) return
|
|
86
|
-
const element = new Audio(`${MUSIC_DIR}/${MUSIC_FILES[id]}`)
|
|
87
|
-
element.loop = true
|
|
85
|
+
if (this.musicElements.has(id)) return
|
|
86
|
+
const element = new Audio(`${MUSIC_DIR}/${MUSIC_FILES[id]}`)
|
|
87
|
+
element.loop = true
|
|
88
88
|
// Defer the request until playback is allowed so audio never competes with
|
|
89
89
|
// the scene's first visual and physics assets.
|
|
90
|
-
element.preload =
|
|
91
|
-
this.musicElements.set(id, element)
|
|
90
|
+
element.preload = 'none'
|
|
91
|
+
this.musicElements.set(id, element)
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
private startMusic(id: MusicId, options: MusicOptions = {}): void {
|
|
95
95
|
if (this.currentMusic === id) {
|
|
96
|
-
const active = this.musicElements.get(id)
|
|
97
|
-
if (active?.paused) void active.play().catch(() => undefined)
|
|
98
|
-
return
|
|
96
|
+
const active = this.musicElements.get(id)
|
|
97
|
+
if (active?.paused) void active.play().catch(() => undefined)
|
|
98
|
+
return
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
const fadeMs = options.fadeMs ?? 500
|
|
102
|
-
const target = clampVolume(options.volume ?? DEFAULT_MUSIC_VOLUME)
|
|
103
|
-
const previousId = this.currentMusic
|
|
104
|
-
const previous = previousId ? (this.musicElements.get(previousId) ?? null) : null
|
|
105
|
-
this.currentMusic = id
|
|
106
|
-
const element = this.musicElements.get(id)
|
|
107
|
-
if (!element) return
|
|
108
|
-
element.volume = 0
|
|
109
|
-
element.currentTime = 0
|
|
101
|
+
const fadeMs = options.fadeMs ?? 500
|
|
102
|
+
const target = clampVolume(options.volume ?? DEFAULT_MUSIC_VOLUME)
|
|
103
|
+
const previousId = this.currentMusic
|
|
104
|
+
const previous = previousId ? (this.musicElements.get(previousId) ?? null) : null
|
|
105
|
+
this.currentMusic = id
|
|
106
|
+
const element = this.musicElements.get(id)
|
|
107
|
+
if (!element) return
|
|
108
|
+
element.volume = 0
|
|
109
|
+
element.currentTime = 0
|
|
110
110
|
void element
|
|
111
111
|
.play()
|
|
112
112
|
.then(() => {
|
|
113
113
|
if (this.currentMusic !== id) {
|
|
114
|
-
element.pause()
|
|
115
|
-
return
|
|
114
|
+
element.pause()
|
|
115
|
+
return
|
|
116
116
|
}
|
|
117
117
|
if (previous) {
|
|
118
118
|
this.fadeMusic(previous, 0, fadeMs, () => {
|
|
119
|
-
previous.pause()
|
|
120
|
-
previous.currentTime = 0
|
|
121
|
-
})
|
|
119
|
+
previous.pause()
|
|
120
|
+
previous.currentTime = 0
|
|
121
|
+
})
|
|
122
122
|
}
|
|
123
|
-
this.fadeMusic(element, target, fadeMs)
|
|
123
|
+
this.fadeMusic(element, target, fadeMs)
|
|
124
124
|
})
|
|
125
125
|
.catch(() => {
|
|
126
126
|
// Autoplay is blocked before the first user gesture. Keep the desired
|
|
127
127
|
// track so unlock() can retry it later.
|
|
128
|
-
if (previous) this.currentMusic = previousId
|
|
129
|
-
else this.currentMusic = null
|
|
130
|
-
})
|
|
128
|
+
if (previous) this.currentMusic = previousId
|
|
129
|
+
else this.currentMusic = null
|
|
130
|
+
})
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
private startDesiredMusic(): void {
|
|
134
|
-
if (this.musicMuted || !this.desiredMusic) return
|
|
135
|
-
const desired = this.desiredMusic
|
|
134
|
+
if (this.musicMuted || !this.desiredMusic) return
|
|
135
|
+
const desired = this.desiredMusic
|
|
136
136
|
if (this.currentMusic !== desired) {
|
|
137
|
-
this.startMusic(desired)
|
|
138
|
-
return
|
|
137
|
+
this.startMusic(desired)
|
|
138
|
+
return
|
|
139
139
|
}
|
|
140
|
-
const element = this.musicElements.get(desired)
|
|
141
|
-
if (element?.paused) void element.play().catch(() => undefined)
|
|
140
|
+
const element = this.musicElements.get(desired)
|
|
141
|
+
if (element?.paused) void element.play().catch(() => undefined)
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
private fadeMusic(
|
|
@@ -147,19 +147,19 @@ export class SoundController {
|
|
|
147
147
|
durationMs: number,
|
|
148
148
|
onDone?: () => void
|
|
149
149
|
): void {
|
|
150
|
-
const start = element.volume
|
|
151
|
-
const startTime = performance.now()
|
|
150
|
+
const start = element.volume
|
|
151
|
+
const startTime = performance.now()
|
|
152
152
|
const interval = window.setInterval(() => {
|
|
153
|
-
const progress = Math.min(1, (performance.now() - startTime) / Math.max(durationMs, 1))
|
|
154
|
-
element.volume = start + (target - start) * progress
|
|
153
|
+
const progress = Math.min(1, (performance.now() - startTime) / Math.max(durationMs, 1))
|
|
154
|
+
element.volume = start + (target - start) * progress
|
|
155
155
|
if (progress >= 1) {
|
|
156
|
-
window.clearInterval(interval)
|
|
157
|
-
this.activeFades.delete(interval)
|
|
158
|
-
onDone?.()
|
|
156
|
+
window.clearInterval(interval)
|
|
157
|
+
this.activeFades.delete(interval)
|
|
158
|
+
onDone?.()
|
|
159
159
|
}
|
|
160
|
-
}, FADE_STEP_MS)
|
|
161
|
-
this.activeFades.add(interval)
|
|
160
|
+
}, FADE_STEP_MS)
|
|
161
|
+
this.activeFades.add(interval)
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
export const soundController = new SoundController()
|
|
165
|
+
export const soundController = new SoundController()
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// Optional soundtrack hooks for Adea.
|
|
2
2
|
|
|
3
|
-
export { AUDIO_BASE_URL, MUSIC_FILES, type MusicId } from
|
|
4
|
-
export { SoundController, soundController, type MusicOptions } from
|
|
5
|
-
export { musicForScene, sceneMusicTracks } from
|
|
3
|
+
export { AUDIO_BASE_URL, MUSIC_FILES, type MusicId } from './config'
|
|
4
|
+
export { SoundController, soundController, type MusicOptions } from './controller'
|
|
5
|
+
export { musicForScene, sceneMusicTracks } from './scene-music'
|
|
6
6
|
export {
|
|
7
7
|
MusicToggle,
|
|
8
8
|
SoundProvider,
|
|
9
9
|
useSceneMusic,
|
|
10
10
|
useSound,
|
|
11
11
|
type SoundContextValue,
|
|
12
|
-
} from
|
|
12
|
+
} from './react'
|
package/src/react.tsx
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
|
|
1
|
+
'use client'
|
|
2
2
|
|
|
3
|
-
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from
|
|
4
|
-
import { Music, Music2 } from
|
|
5
|
-
import { soundController, type MusicOptions } from
|
|
6
|
-
import { musicForScene } from
|
|
7
|
-
import type { MusicId } from
|
|
3
|
+
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react'
|
|
4
|
+
import { Music, Music2 } from 'lucide-react'
|
|
5
|
+
import { soundController, type MusicOptions } from './controller'
|
|
6
|
+
import { musicForScene } from './scene-music'
|
|
7
|
+
import type { MusicId } from './config'
|
|
8
8
|
|
|
9
9
|
export type SoundContextValue = {
|
|
10
|
-
controller: typeof soundController
|
|
11
|
-
ready: boolean
|
|
12
|
-
musicMuted: boolean
|
|
13
|
-
toggleMusicMute: () => void
|
|
14
|
-
playMusic: (id: MusicId, options?: MusicOptions) => void
|
|
15
|
-
}
|
|
10
|
+
controller: typeof soundController
|
|
11
|
+
ready: boolean
|
|
12
|
+
musicMuted: boolean
|
|
13
|
+
toggleMusicMute: () => void
|
|
14
|
+
playMusic: (id: MusicId, options?: MusicOptions) => void
|
|
15
|
+
}
|
|
16
16
|
|
|
17
|
-
const SoundContext = createContext<SoundContextValue | null>(null)
|
|
17
|
+
const SoundContext = createContext<SoundContextValue | null>(null)
|
|
18
18
|
|
|
19
19
|
export function SoundProvider({ children }: { children: ReactNode }) {
|
|
20
|
-
const [musicMuted, setMusicMuted] = useState(false)
|
|
21
|
-
const [ready, setReady] = useState(false)
|
|
20
|
+
const [musicMuted, setMusicMuted] = useState(false)
|
|
21
|
+
const [ready, setReady] = useState(false)
|
|
22
22
|
|
|
23
23
|
useEffect(() => {
|
|
24
|
-
let mounted = true
|
|
24
|
+
let mounted = true
|
|
25
25
|
queueMicrotask(() => {
|
|
26
|
-
if (mounted) setMusicMuted(soundController.musicMuted)
|
|
27
|
-
})
|
|
26
|
+
if (mounted) setMusicMuted(soundController.musicMuted)
|
|
27
|
+
})
|
|
28
28
|
|
|
29
29
|
const onGesture = () => {
|
|
30
30
|
void soundController.unlock().then(() => {
|
|
31
|
-
if (mounted) setReady(true)
|
|
32
|
-
})
|
|
33
|
-
}
|
|
34
|
-
window.addEventListener(
|
|
35
|
-
window.addEventListener(
|
|
36
|
-
window.addEventListener(
|
|
37
|
-
if (new URLSearchParams(window.location.search).has(
|
|
38
|
-
(window as unknown as { __agentHqSound?: typeof soundController }).__agentHqSound =
|
|
39
|
-
soundController
|
|
31
|
+
if (mounted) setReady(true)
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
window.addEventListener('pointerdown', onGesture, { capture: true })
|
|
35
|
+
window.addEventListener('keydown', onGesture, { capture: true })
|
|
36
|
+
window.addEventListener('touchstart', onGesture, { capture: true })
|
|
37
|
+
if (new URLSearchParams(window.location.search).has('debug')) {
|
|
38
|
+
;(window as unknown as { __agentHqSound?: typeof soundController }).__agentHqSound =
|
|
39
|
+
soundController
|
|
40
40
|
}
|
|
41
41
|
return () => {
|
|
42
|
-
mounted = false
|
|
43
|
-
window.removeEventListener(
|
|
44
|
-
window.removeEventListener(
|
|
45
|
-
window.removeEventListener(
|
|
46
|
-
}
|
|
47
|
-
}, [])
|
|
42
|
+
mounted = false
|
|
43
|
+
window.removeEventListener('pointerdown', onGesture, { capture: true })
|
|
44
|
+
window.removeEventListener('keydown', onGesture, { capture: true })
|
|
45
|
+
window.removeEventListener('touchstart', onGesture, { capture: true })
|
|
46
|
+
}
|
|
47
|
+
}, [])
|
|
48
48
|
|
|
49
49
|
const value = useMemo<SoundContextValue>(
|
|
50
50
|
() => ({
|
|
@@ -55,34 +55,34 @@ export function SoundProvider({ children }: { children: ReactNode }) {
|
|
|
55
55
|
playMusic: (id, options) => soundController.playMusic(id, options),
|
|
56
56
|
}),
|
|
57
57
|
[musicMuted, ready]
|
|
58
|
-
)
|
|
58
|
+
)
|
|
59
59
|
|
|
60
|
-
return <SoundContext.Provider value={value}>{children}</SoundContext.Provider
|
|
60
|
+
return <SoundContext.Provider value={value}>{children}</SoundContext.Provider>
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
export function useSound(): SoundContextValue {
|
|
64
|
-
const value = useContext(SoundContext)
|
|
65
|
-
if (!value) throw new Error(
|
|
66
|
-
return value
|
|
64
|
+
const value = useContext(SoundContext)
|
|
65
|
+
if (!value) throw new Error('useSound must be used within <SoundProvider>')
|
|
66
|
+
return value
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
export function useSceneMusic(sceneId: string | null | undefined): void {
|
|
70
70
|
useEffect(() => {
|
|
71
71
|
const timeout = window.setTimeout(() => {
|
|
72
|
-
soundController.playMusic(musicForScene(sceneId))
|
|
73
|
-
}, 1500)
|
|
74
|
-
return () => window.clearTimeout(timeout)
|
|
75
|
-
}, [sceneId])
|
|
72
|
+
soundController.playMusic(musicForScene(sceneId))
|
|
73
|
+
}, 1500)
|
|
74
|
+
return () => window.clearTimeout(timeout)
|
|
75
|
+
}, [sceneId])
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
function MusicButton({ muted, onToggle }: { muted: boolean; onToggle: () => void }) {
|
|
79
79
|
return (
|
|
80
80
|
<button
|
|
81
81
|
type="button"
|
|
82
|
-
aria-label={muted ?
|
|
82
|
+
aria-label={muted ? 'Unmute music' : 'Mute music'}
|
|
83
83
|
aria-pressed={muted}
|
|
84
84
|
onClick={onToggle}
|
|
85
|
-
className={`inline-flex size-9 items-center justify-center rounded-lg border border-input bg-background transition-colors ${muted ?
|
|
85
|
+
className={`inline-flex size-9 items-center justify-center rounded-lg border border-input bg-background transition-colors ${muted ? 'text-muted-foreground hover:text-foreground' : 'bg-primary text-primary-foreground'}`}
|
|
86
86
|
>
|
|
87
87
|
{muted ? (
|
|
88
88
|
<Music className="size-5" aria-hidden="true" />
|
|
@@ -90,10 +90,10 @@ function MusicButton({ muted, onToggle }: { muted: boolean; onToggle: () => void
|
|
|
90
90
|
<Music2 className="size-5" aria-hidden="true" />
|
|
91
91
|
)}
|
|
92
92
|
</button>
|
|
93
|
-
)
|
|
93
|
+
)
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
export function MusicToggle() {
|
|
97
|
-
const { musicMuted, toggleMusicMute } = useSound()
|
|
98
|
-
return <MusicButton muted={musicMuted} onToggle={toggleMusicMute}
|
|
97
|
+
const { musicMuted, toggleMusicMute } = useSound()
|
|
98
|
+
return <MusicButton muted={musicMuted} onToggle={toggleMusicMute} />
|
|
99
99
|
}
|
package/src/scene-music.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { MusicId } from
|
|
1
|
+
import type { MusicId } from './config'
|
|
2
2
|
|
|
3
3
|
/** Scene route id -> optional track. Deployments can add tracks without changing scene code. */
|
|
4
4
|
export const sceneMusicTracks: Record<string, MusicId> = {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
5
|
+
'hq-home': 'silent',
|
|
6
|
+
'hq-work': 'silent',
|
|
7
|
+
}
|
|
8
8
|
|
|
9
9
|
export function musicForScene(sceneId: string | null | undefined): MusicId {
|
|
10
|
-
return sceneMusicTracks[sceneId ??
|
|
10
|
+
return sceneMusicTracks[sceneId ?? ''] ?? 'silent'
|
|
11
11
|
}
|