feat(monitor/frontend): audio chime infrastructure (load/unlock/play, phase 6 wires the trigger)
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.
This commit is contained in:
parent
43df53f845
commit
21f54390df
|
|
@ -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<void> {
|
||||
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<void> => {
|
||||
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
|
||||
}
|
||||
Loading…
Reference in New Issue