import { getDB } from "../app"; import { getSettings } from "../storage/db"; let audioCtx: AudioContext | null = null; /** * Creates or resumes AudioContext on first user interaction. * Call from a user-interaction event handler (click, keydown) to comply * with browser autoplay policy. */ export function initAudioOnInteraction(): void { try { if (!audioCtx) { audioCtx = new AudioContext(); } if (audioCtx.state === "suspended") { audioCtx.resume(); } } catch { // AudioContext not supported — silent fallback } } async function isAudioEnabled(): Promise { try { const db = getDB(); const settings = await getSettings(db); return settings?.audioEnabled !== false; } catch { return true; // default to enabled if settings unavailable } } function ensureAudioCtx(): AudioContext | null { if (!audioCtx) { initAudioOnInteraction(); } return audioCtx; } /** * Short confirmation beep: 800Hz sine, 0.1s, gain 0.15. */ export async function playCorrectSound(): Promise { try { if (!(await isAudioEnabled())) return; const ctx = ensureAudioCtx(); if (!ctx) return; const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = "sine"; osc.frequency.value = 800; gain.gain.value = 0.15; osc.connect(gain); gain.connect(ctx.destination); const now = ctx.currentTime; // Ramp gain to 0 over last 0.02s for click-free cutoff gain.gain.setValueAtTime(0.15, now); gain.gain.linearRampToValueAtTime(0, now + 0.1); osc.start(now); osc.stop(now + 0.1); } catch { // Never throw to caller } } /** * Ascending reward tone sequence: C5 E5 G5 C6, ~0.8s total. */ export async function playRewardSound(): Promise { try { if (!(await isAudioEnabled())) return; const ctx = ensureAudioCtx(); if (!ctx) return; const notes = [523, 659, 784, 1047]; // C5, E5, G5, C6 const noteSpacing = 0.2; const noteDuration = 0.15; const now = ctx.currentTime; for (const [i, freq] of notes.entries()) { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = "sine"; osc.frequency.value = freq; gain.gain.value = 0.12; osc.connect(gain); gain.connect(ctx.destination); const startTime = now + i * noteSpacing; gain.gain.setValueAtTime(0.12, startTime); gain.gain.linearRampToValueAtTime(0, startTime + noteDuration); osc.start(startTime); osc.stop(startTime + noteDuration); } } catch { // Never throw to caller } } /** * Forest element sound: filtered white noise, bandpass 3000Hz, 0.5s. */ export async function playForestElementSound(): Promise { try { if (!(await isAudioEnabled())) return; const ctx = ensureAudioCtx(); if (!ctx) return; // Create white noise buffer const bufferSize = ctx.sampleRate * 0.5; // 0.5 seconds const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) { data[i] = Math.random() * 2 - 1; } const source = ctx.createBufferSource(); source.buffer = buffer; const filter = ctx.createBiquadFilter(); filter.type = "bandpass"; filter.frequency.value = 3000; filter.Q.value = 0.5; const gain = ctx.createGain(); gain.gain.value = 0.1; source.connect(filter); filter.connect(gain); gain.connect(ctx.destination); const now = ctx.currentTime; // Ramp down gain over duration gain.gain.setValueAtTime(0.1, now); gain.gain.linearRampToValueAtTime(0, now + 0.5); source.start(now); source.stop(now + 0.5); } catch { // Never throw to caller } }