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; } interface ToneOptions { frequency: number; endFrequency?: number; duration: number; type?: OscillatorType; volume?: number; 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: { duration: number; volume?: number; delay?: number; 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++) { 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 playFlap() { const ctx = getCtx(); if (!ctx) return; playTone(ctx, { frequency: 260, endFrequency: 560, duration: 0.08, type: "triangle", volume: 0.12, }); } export function playHit() { const ctx = getCtx(); if (!ctx) return; playTone(ctx, { frequency: 160, endFrequency: 45, duration: 0.25, type: "square", volume: 0.2 }); playNoise(ctx, { duration: 0.18, volume: 0.25, lowpass: 900 }); } 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 }); }