53 lines
2.5 KiB
TypeScript
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 }); },
|
|
};
|