let audioCtx: AudioContext | null = null; function getCtx(): AudioContext | null { if (typeof window === "undefined") return null; const AudioCtor = window.AudioContext ?? (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext; if (!AudioCtor) return null; if (!audioCtx) audioCtx = new AudioCtor(); if (audioCtx.state === "suspended") { void audioCtx.resume(); } return audioCtx; } type ToneOptions = { readonly frequency: number; readonly endFrequency?: number; readonly duration: number; readonly type?: OscillatorType; readonly volume?: number; readonly delay?: number; }; function playTone(ctx: AudioContext, opts: ToneOptions) { const { frequency, endFrequency, duration, type = "sine", volume = 0.2, delay = 0 } = opts; const osc = ctx.createOscillator(); const gain = ctx.createGain(); const t0 = ctx.currentTime + delay; osc.type = type; osc.frequency.setValueAtTime(Math.max(frequency, 1), t0); if (endFrequency && endFrequency !== frequency) { osc.frequency.exponentialRampToValueAtTime(Math.max(endFrequency, 1), t0 + duration); } gain.gain.setValueAtTime(0.0001, t0); gain.gain.exponentialRampToValueAtTime(volume, t0 + 0.012); gain.gain.exponentialRampToValueAtTime(0.0001, t0 + duration); osc.connect(gain); gain.connect(ctx.destination); osc.start(t0); osc.stop(t0 + duration + 0.05); } function playNoise( ctx: AudioContext, opts: { readonly duration: number; readonly volume?: number; readonly delay?: number; readonly lowpass?: number }, ) { const { duration, volume = 0.2, delay = 0, lowpass = 800 } = opts; const bufferSize = Math.max(1, Math.floor(ctx.sampleRate * duration)); const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < bufferSize; i += 1) { data[i] = Math.random() * 2 - 1; } const source = ctx.createBufferSource(); source.buffer = buffer; const filter = ctx.createBiquadFilter(); filter.type = "lowpass"; filter.frequency.setValueAtTime(lowpass, ctx.currentTime + delay); const gain = ctx.createGain(); const t0 = ctx.currentTime + delay; gain.gain.setValueAtTime(0.0001, t0); gain.gain.exponentialRampToValueAtTime(volume, t0 + 0.005); gain.gain.exponentialRampToValueAtTime(0.0001, t0 + duration); source.connect(filter); filter.connect(gain); gain.connect(ctx.destination); source.start(t0); } export function playKeystroke() { const ctx = getCtx(); if (!ctx) return; playTone(ctx, { frequency: 520, duration: 0.025, type: "square", volume: 0.025 }); } export function playCorrect() { const ctx = getCtx(); if (!ctx) return; playTone(ctx, { frequency: 620, endFrequency: 920, duration: 0.08, type: "sine", volume: 0.12 }); } export function playError() { const ctx = getCtx(); if (!ctx) return; playTone(ctx, { frequency: 150, endFrequency: 80, duration: 0.16, type: "square", volume: 0.14 }); playNoise(ctx, { duration: 0.1, volume: 0.08, lowpass: 500 }); } export function playRoundComplete() { const ctx = getCtx(); if (!ctx) return; playTone(ctx, { frequency: 523, duration: 0.1, type: "sine", volume: 0.16 }); playTone(ctx, { frequency: 659, duration: 0.1, type: "sine", volume: 0.16, delay: 0.1 }); playTone(ctx, { frequency: 784, duration: 0.18, type: "sine", volume: 0.18, delay: 0.2 }); } export function playXp() { const ctx = getCtx(); if (!ctx) return; playTone(ctx, { frequency: 660, duration: 0.09, type: "sine", volume: 0.18 }); playTone(ctx, { frequency: 880, duration: 0.16, type: "sine", volume: 0.18, delay: 0.09 }); } export function playCountdownTick(intensity: "normal" | "fast" | "critical") { const ctx = getCtx(); if (!ctx) return; if (intensity === "normal") { playTone(ctx, { frequency: 880, duration: 0.03, type: "square", volume: 0.05 }); } else if (intensity === "fast") { playTone(ctx, { frequency: 1046, duration: 0.03, type: "square", volume: 0.07 }); } else { playTone(ctx, { frequency: 1318, duration: 0.045, type: "square", volume: 0.11 }); } } export function playCountdownEnd() { const ctx = getCtx(); if (!ctx) return; playTone(ctx, { frequency: 1318, endFrequency: 240, duration: 0.75, type: "sine", volume: 0.16 }); playTone(ctx, { frequency: 659, endFrequency: 120, duration: 0.75, type: "triangle", volume: 0.1 }); }