From 21f54390dfcf238b6da5f319114ab5091eaa5a61 Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Sun, 3 May 2026 09:41:22 -0400 Subject: [PATCH] feat(monitor/frontend): audio chime infrastructure (load/unlock/play, phase 6 wires the trigger) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per spec §10.5 — three pure functions sitting on top of the existing useAudioStore (so React surfaces can subscribe to .unlocked if a "click anywhere to enable audio" toast is needed later): loadChime(file) fetch / decode arraybuffer into AudioBuffer unlockOnFirstGesture() attach one-shot pointerdown listener that resumes a suspended AudioContext, sets unlocked=true. Idempotent across re-mounts via module-level listenerAttached guard (StrictMode double-mount safe). playChime() → boolean returns false if the audio context isn't ready / buffer not loaded / not unlocked, so callers can fall back to a toast. Phase 6 calls playChime() on alert match; Phase 5 only wires the unlockOnFirstGesture() call into the dashboard lifecycle (Task 24) so the audio is ready when Phase 6 lands. --- .../frontend/src/lib/audio.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/lib/audio.ts diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/lib/audio.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/lib/audio.ts new file mode 100644 index 00000000..4ee3ed13 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/lib/audio.ts @@ -0,0 +1,47 @@ +// ©AngelaMos | 2026 +// audio.ts + +import { useAudioStore } from '@/stores/audio' + +let listenerAttached = false + +function ensureCtx(): AudioContext { + let ctx = useAudioStore.getState().ctx + if (!ctx) { + ctx = new AudioContext() + useAudioStore.getState().setCtx(ctx) + } + return ctx +} + +export async function loadChime(file: File): Promise { + const ctx = ensureCtx() + const ab = await file.arrayBuffer() + const buffer = await ctx.decodeAudioData(ab) + useAudioStore.getState().setBuffer(buffer) +} + +export function unlockOnFirstGesture(): void { + if (useAudioStore.getState().unlocked) return + if (listenerAttached) return + listenerAttached = true + + const handler = async (): Promise => { + listenerAttached = false + const ctx = ensureCtx() + if (ctx.state === 'suspended') await ctx.resume() + useAudioStore.getState().setUnlocked(true) + } + + document.addEventListener('pointerdown', handler, { once: true }) +} + +export function playChime(): boolean { + const { ctx, buffer, unlocked } = useAudioStore.getState() + if (!ctx || !buffer || !unlocked) return false + const src = ctx.createBufferSource() + src.buffer = buffer + src.connect(ctx.destination) + src.start(0) + return true +}