123 lines
3.3 KiB
TypeScript
123 lines
3.3 KiB
TypeScript
/**
|
||
* 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
|
||
}
|
||
}
|