@adea-ai/audio 0.10.8 → 0.10.9

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.
@@ -0,0 +1 @@
1
+ $ tsc -p tsconfig.json
@@ -0,0 +1,7 @@
1
+ /** Optional soundtrack hooks for Agent HQ. */
2
+ export declare const AUDIO_BASE_URL = "/assets/audio";
3
+ export declare const MUSIC_DIR = "/assets/audio/sounds/music";
4
+ export declare const MUSIC_FILES: {
5
+ readonly silent: "";
6
+ };
7
+ export type MusicId = keyof typeof MUSIC_FILES;
package/dist/config.js ADDED
@@ -0,0 +1,6 @@
1
+ /** Optional soundtrack hooks for Agent HQ. */
2
+ export const AUDIO_BASE_URL = "/assets/audio";
3
+ export const MUSIC_DIR = `${AUDIO_BASE_URL}/sounds/music`;
4
+ export const MUSIC_FILES = {
5
+ silent: "",
6
+ };
@@ -0,0 +1,27 @@
1
+ import { type MusicId } from "./config";
2
+ export type MusicOptions = {
3
+ /** 0..1 track gain (default 0.8). */
4
+ volume?: number;
5
+ /** Crossfade/fade duration in ms (default 500). */
6
+ fadeMs?: number;
7
+ };
8
+ export declare class SoundController {
9
+ private musicElements;
10
+ private desiredMusic;
11
+ private currentMusic;
12
+ private activeFades;
13
+ private _musicMuted;
14
+ constructor();
15
+ get musicMuted(): boolean;
16
+ /** Retry queued playback after a user gesture. */
17
+ unlock(): Promise<void>;
18
+ /** Start or switch to a looping soundtrack. */
19
+ playMusic(id: MusicId, options?: MusicOptions): void;
20
+ setMusicMuted(muted: boolean): void;
21
+ toggleMusicMute(): boolean;
22
+ private prepareMusicElement;
23
+ private startMusic;
24
+ private startDesiredMusic;
25
+ private fadeMusic;
26
+ }
27
+ export declare const soundController: SoundController;
@@ -0,0 +1,152 @@
1
+ import { MUSIC_DIR, MUSIC_FILES } from "./config";
2
+ const MUSIC_MUTE_KEY = "agent-hq:audio:music-muted";
3
+ const LEGACY_MUTE_KEY = "agent-hq:audio:muted";
4
+ const DEFAULT_MUSIC_VOLUME = 0.8;
5
+ const FADE_STEP_MS = 30;
6
+ function clampVolume(value) {
7
+ return value <= 0 ? 0 : value > 1 ? 1 : value;
8
+ }
9
+ function readStorage(key) {
10
+ if (typeof window === "undefined")
11
+ return null;
12
+ try {
13
+ return window.localStorage.getItem(key);
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ function writeStorage(key, muted) {
20
+ if (typeof window === "undefined")
21
+ return;
22
+ try {
23
+ if (muted)
24
+ window.localStorage.setItem(key, "1");
25
+ else
26
+ window.localStorage.removeItem(key);
27
+ }
28
+ catch {
29
+ // Storage can be unavailable; the in-memory mute still applies.
30
+ }
31
+ }
32
+ function loadMusicMuted() {
33
+ return readStorage(MUSIC_MUTE_KEY) === "1" || readStorage(LEGACY_MUTE_KEY) === "1";
34
+ }
35
+ export class SoundController {
36
+ musicElements = new Map();
37
+ desiredMusic = null;
38
+ currentMusic = null;
39
+ activeFades = new Set();
40
+ _musicMuted;
41
+ constructor() {
42
+ this._musicMuted = loadMusicMuted();
43
+ }
44
+ get musicMuted() {
45
+ return this._musicMuted;
46
+ }
47
+ /** Retry queued playback after a user gesture. */
48
+ async unlock() {
49
+ this.startDesiredMusic();
50
+ }
51
+ /** Start or switch to a looping soundtrack. */
52
+ playMusic(id, options = {}) {
53
+ if (!MUSIC_FILES[id])
54
+ return;
55
+ this.desiredMusic = id;
56
+ this.prepareMusicElement(id);
57
+ if (!this.musicMuted)
58
+ this.startMusic(id, options);
59
+ }
60
+ setMusicMuted(muted) {
61
+ this._musicMuted = muted;
62
+ writeStorage(MUSIC_MUTE_KEY, muted);
63
+ for (const element of this.musicElements.values()) {
64
+ if (muted)
65
+ element.pause();
66
+ }
67
+ if (!muted)
68
+ this.startDesiredMusic();
69
+ }
70
+ toggleMusicMute() {
71
+ this.setMusicMuted(!this._musicMuted);
72
+ return this._musicMuted;
73
+ }
74
+ prepareMusicElement(id) {
75
+ if (this.musicElements.has(id))
76
+ return;
77
+ const element = new Audio(`${MUSIC_DIR}/${MUSIC_FILES[id]}`);
78
+ element.loop = true;
79
+ // Defer the request until playback is allowed so audio never competes with
80
+ // the scene's first visual and physics assets.
81
+ element.preload = "none";
82
+ this.musicElements.set(id, element);
83
+ }
84
+ startMusic(id, options = {}) {
85
+ if (this.currentMusic === id) {
86
+ const active = this.musicElements.get(id);
87
+ if (active?.paused)
88
+ void active.play().catch(() => undefined);
89
+ return;
90
+ }
91
+ const fadeMs = options.fadeMs ?? 500;
92
+ const target = clampVolume(options.volume ?? DEFAULT_MUSIC_VOLUME);
93
+ const previousId = this.currentMusic;
94
+ const previous = previousId ? (this.musicElements.get(previousId) ?? null) : null;
95
+ this.currentMusic = id;
96
+ const element = this.musicElements.get(id);
97
+ if (!element)
98
+ return;
99
+ element.volume = 0;
100
+ element.currentTime = 0;
101
+ void element
102
+ .play()
103
+ .then(() => {
104
+ if (this.currentMusic !== id) {
105
+ element.pause();
106
+ return;
107
+ }
108
+ if (previous) {
109
+ this.fadeMusic(previous, 0, fadeMs, () => {
110
+ previous.pause();
111
+ previous.currentTime = 0;
112
+ });
113
+ }
114
+ this.fadeMusic(element, target, fadeMs);
115
+ })
116
+ .catch(() => {
117
+ // Autoplay is blocked before the first user gesture. Keep the desired
118
+ // track so unlock() can retry it later.
119
+ if (previous)
120
+ this.currentMusic = previousId;
121
+ else
122
+ this.currentMusic = null;
123
+ });
124
+ }
125
+ startDesiredMusic() {
126
+ if (this.musicMuted || !this.desiredMusic)
127
+ return;
128
+ const desired = this.desiredMusic;
129
+ if (this.currentMusic !== desired) {
130
+ this.startMusic(desired);
131
+ return;
132
+ }
133
+ const element = this.musicElements.get(desired);
134
+ if (element?.paused)
135
+ void element.play().catch(() => undefined);
136
+ }
137
+ fadeMusic(element, target, durationMs, onDone) {
138
+ const start = element.volume;
139
+ const startTime = performance.now();
140
+ const interval = window.setInterval(() => {
141
+ const progress = Math.min(1, (performance.now() - startTime) / Math.max(durationMs, 1));
142
+ element.volume = start + (target - start) * progress;
143
+ if (progress >= 1) {
144
+ window.clearInterval(interval);
145
+ this.activeFades.delete(interval);
146
+ onDone?.();
147
+ }
148
+ }, FADE_STEP_MS);
149
+ this.activeFades.add(interval);
150
+ }
151
+ }
152
+ export const soundController = new SoundController();
@@ -0,0 +1,4 @@
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 ADDED
@@ -0,0 +1,5 @@
1
+ // Optional soundtrack hooks for Agent HQ.
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";
@@ -0,0 +1,16 @@
1
+ import { type ReactNode } from "react";
2
+ import { soundController, type MusicOptions } from "./controller";
3
+ import type { MusicId } from "./config";
4
+ export type SoundContextValue = {
5
+ controller: typeof soundController;
6
+ ready: boolean;
7
+ musicMuted: boolean;
8
+ toggleMusicMute: () => void;
9
+ playMusic: (id: MusicId, options?: MusicOptions) => void;
10
+ };
11
+ export declare function SoundProvider({ children }: {
12
+ children: ReactNode;
13
+ }): import("react").JSX.Element;
14
+ export declare function useSound(): SoundContextValue;
15
+ export declare function useSceneMusic(sceneId: string | null | undefined): void;
16
+ export declare function MusicToggle(): import("react").JSX.Element;
package/dist/react.js ADDED
@@ -0,0 +1,66 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
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
+ const SoundContext = createContext(null);
8
+ export function SoundProvider({ children }) {
9
+ const [musicMuted, setMusicMuted] = useState(false);
10
+ const [ready, setReady] = useState(false);
11
+ useEffect(() => {
12
+ let mounted = true;
13
+ queueMicrotask(() => {
14
+ if (mounted)
15
+ setMusicMuted(soundController.musicMuted);
16
+ });
17
+ const onGesture = () => {
18
+ void soundController.unlock().then(() => {
19
+ if (mounted)
20
+ setReady(true);
21
+ });
22
+ };
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
+ window.__agentHqSound =
28
+ soundController;
29
+ }
30
+ return () => {
31
+ mounted = false;
32
+ window.removeEventListener("pointerdown", onGesture, { capture: true });
33
+ window.removeEventListener("keydown", onGesture, { capture: true });
34
+ window.removeEventListener("touchstart", onGesture, { capture: true });
35
+ };
36
+ }, []);
37
+ const value = useMemo(() => ({
38
+ controller: soundController,
39
+ ready,
40
+ musicMuted,
41
+ toggleMusicMute: () => setMusicMuted(soundController.toggleMusicMute()),
42
+ playMusic: (id, options) => soundController.playMusic(id, options),
43
+ }), [musicMuted, ready]);
44
+ return _jsx(SoundContext.Provider, { value: value, children: children });
45
+ }
46
+ export function useSound() {
47
+ const value = useContext(SoundContext);
48
+ if (!value)
49
+ throw new Error("useSound must be used within <SoundProvider>");
50
+ return value;
51
+ }
52
+ export function useSceneMusic(sceneId) {
53
+ useEffect(() => {
54
+ const timeout = window.setTimeout(() => {
55
+ soundController.playMusic(musicForScene(sceneId));
56
+ }, 1500);
57
+ return () => window.clearTimeout(timeout);
58
+ }, [sceneId]);
59
+ }
60
+ function MusicButton({ muted, onToggle }) {
61
+ 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
+ export function MusicToggle() {
64
+ const { musicMuted, toggleMusicMute } = useSound();
65
+ return _jsx(MusicButton, { muted: musicMuted, onToggle: toggleMusicMute });
66
+ }
@@ -0,0 +1,4 @@
1
+ import type { MusicId } from "./config";
2
+ /** Scene route id -> optional track. Deployments can add tracks without changing scene code. */
3
+ export declare const sceneMusicTracks: Record<string, MusicId>;
4
+ export declare function musicForScene(sceneId: string | null | undefined): MusicId;
@@ -0,0 +1,8 @@
1
+ /** Scene route id -> optional track. Deployments can add tracks without changing scene code. */
2
+ export const sceneMusicTracks = {
3
+ "hq-home": "silent",
4
+ "hq-work": "silent",
5
+ };
6
+ export function musicForScene(sceneId) {
7
+ return sceneMusicTracks[sceneId ?? ""] ?? "silent";
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adea-ai/audio",
3
- "version": "0.10.8",
3
+ "version": "0.10.9",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",