Files
entscheidomat/lib/sounds.ts
2026-07-23 21:59:15 +02:00

123 lines
3.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 C5E5G5C6
[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
}
}