/** * Prozedurale Sound-Engine auf Basis der WebAudio API. * Keine Audio-Dateien nötig – alles wird live synthetisiert. */ export type SoundName = "tick" | "coin" | "dice" | "pop" | "win" | "whoosh"; let ctx: AudioContext | null = null; function ac(): AudioContext | null { if (typeof window === "undefined") return null; try { if (!ctx) { const AC = window.AudioContext || (window as unknown as { webkitAudioContext?: typeof AudioContext }) .webkitAudioContext; if (!AC) return null; ctx = new AC(); } if (ctx.state === "suspended") { ctx.resume().catch(() => {}); } return ctx; } catch { return null; } } function tone( c: AudioContext, freq: number, type: OscillatorType, dur: number, peak = 0.2, when = 0, slideTo?: number, ) { const t0 = c.currentTime + when; const osc = c.createOscillator(); const gain = c.createGain(); osc.type = type; osc.frequency.setValueAtTime(freq, t0); if (slideTo) { osc.frequency.exponentialRampToValueAtTime(slideTo, t0 + dur); } gain.gain.setValueAtTime(0, t0); gain.gain.linearRampToValueAtTime(peak, t0 + 0.008); gain.gain.exponentialRampToValueAtTime(0.0001, t0 + dur); osc.connect(gain).connect(c.destination); osc.start(t0); osc.stop(t0 + dur + 0.05); } function noiseBurst( c: AudioContext, dur: number, peak: number, filterFreq: number, when = 0, sweepTo?: number, ) { const t0 = c.currentTime + when; const len = Math.max(1, Math.floor(c.sampleRate * dur)); const buffer = c.createBuffer(1, len, c.sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < len; i++) data[i] = Math.random() * 2 - 1; const src = c.createBufferSource(); src.buffer = buffer; const filter = c.createBiquadFilter(); filter.type = "bandpass"; filter.frequency.setValueAtTime(filterFreq, t0); filter.Q.value = 1.1; if (sweepTo) { filter.frequency.exponentialRampToValueAtTime(sweepTo, t0 + dur); } const gain = c.createGain(); gain.gain.setValueAtTime(0, t0); gain.gain.linearRampToValueAtTime(peak, t0 + 0.01); gain.gain.exponentialRampToValueAtTime(0.0001, t0 + dur); src.connect(filter).connect(gain).connect(c.destination); src.start(t0); } export function playSound(name: SoundName) { const c = ac(); if (!c) return; try { switch (name) { case "tick": tone(c, 1900, "square", 0.035, 0.07); break; case "coin": // Metallischer Doppelton + Glitzer tone(c, 2093, "triangle", 0.55, 0.16); tone(c, 2637, "triangle", 0.45, 0.1, 0.012); tone(c, 3520, "sine", 0.3, 0.05, 0.02); break; case "dice": // Prasselndes Klackern [0, 0.09, 0.21, 0.31].forEach((w, i) => noiseBurst(c, 0.05, 0.2 - i * 0.03, 1600 + Math.random() * 1000, w), ); break; case "pop": tone(c, 420, "sine", 0.14, 0.22, 0, 880); break; case "whoosh": noiseBurst(c, 0.55, 0.1, 500, 0, 2600); break; case "win": // Kleine Fanfare C5–E5–G5–C6 [523.25, 659.25, 783.99, 1046.5].forEach((f, i) => tone(c, f, "triangle", 0.4, 0.13, i * 0.09), ); break; } } catch { // Sound ist Dekoration – niemals crashen } }