Files
skrible-2/src/client/sfx.ts
T
2026-07-18 20:41:22 -05:00

53 lines
2.5 KiB
TypeScript

// Synthesized sound effects via WebAudio - no assets needed.
let ctx: AudioContext | null = null;
let muted = false;
function ac(): AudioContext | null {
try {
if (!ctx) ctx = new (window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext)();
if (ctx.state === 'suspended') void ctx.resume();
return ctx;
} catch { return null; }
}
interface ToneOpts { type?: OscillatorType; vol?: number; at?: number; slide?: number; }
function tone(freq: number, dur: number, opts: ToneOpts = {}): void {
if (muted) return;
const a = ac(); if (!a) return;
const t0 = a.currentTime + (opts.at ?? 0);
const osc = a.createOscillator();
const gain = a.createGain();
osc.type = opts.type ?? 'sine';
osc.frequency.setValueAtTime(freq, t0);
if (opts.slide) osc.frequency.exponentialRampToValueAtTime(Math.max(30, opts.slide), t0 + dur);
const v = opts.vol ?? 0.12;
gain.gain.setValueAtTime(0.0001, t0);
gain.gain.exponentialRampToValueAtTime(v, t0 + 0.012);
gain.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
osc.connect(gain).connect(a.destination);
osc.start(t0);
osc.stop(t0 + dur + 0.02);
}
export const sfx = {
setMuted(m: boolean) { muted = m; },
get muted() { return muted; },
unlock() { ac(); },
click() { tone(650, 0.05, { type: 'square', vol: 0.05 }); },
pop() { tone(880, 0.06, { vol: 0.07 }); },
join() { tone(523, 0.09, { type: 'triangle' }); tone(784, 0.13, { at: 0.08, type: 'triangle' }); },
leave() { tone(440, 0.09, { type: 'triangle' }); tone(294, 0.14, { at: 0.08, type: 'triangle' }); },
correct(){ [523, 659, 784, 1047].forEach((f, i) => tone(f, 0.13, { at: i * 0.07, type: 'triangle', vol: 0.12 })); },
yourTurn(){ [392, 523, 659, 784].forEach((f, i) => tone(f, 0.12, { at: i * 0.09, type: 'square', vol: 0.07 })); },
tick() { tone(1150, 0.035, { type: 'square', vol: 0.05 }); },
reveal() { tone(700, 0.09, { type: 'triangle', vol: 0.1, slide: 1000 }); },
closeGuess() { tone(500, 0.08, { type: 'sine', vol: 0.09 }); tone(650, 0.1, { at: 0.07, vol: 0.09 }); },
turnEnd(){ tone(330, 0.15, { type: 'triangle' }); tone(262, 0.22, { at: 0.13, type: 'triangle' }); },
gameEnd(){ [392, 523, 659, 784, 1047, 1319].forEach((f, i) => tone(f, 0.18, { at: i * 0.11, type: 'triangle', vol: 0.11 })); },
drawStart(){ tone(600, 0.08, { type: 'square', vol: 0.06, slide: 900 }); },
denied() { tone(220, 0.15, { type: 'sawtooth', vol: 0.07, slide: 110 }); },
erase() { tone(300, 0.05, { type: 'sawtooth', vol: 0.04, slide: 180 }); },
};