103 lines
3.4 KiB
TypeScript
103 lines
3.4 KiB
TypeScript
let audioCtx: AudioContext | null = null;
|
|
|
|
function getCtx(): AudioContext | null {
|
|
if (typeof window === "undefined") return null;
|
|
const AudioCtor =
|
|
window.AudioContext ??
|
|
(window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
|
|
if (!AudioCtor) return null;
|
|
if (!audioCtx) audioCtx = new AudioCtor();
|
|
if (audioCtx.state === "suspended") {
|
|
void audioCtx.resume();
|
|
}
|
|
return audioCtx;
|
|
}
|
|
|
|
interface ToneOptions {
|
|
frequency: number;
|
|
endFrequency?: number;
|
|
duration: number;
|
|
type?: OscillatorType;
|
|
volume?: number;
|
|
delay?: number;
|
|
}
|
|
|
|
function playTone(ctx: AudioContext, opts: ToneOptions) {
|
|
const { frequency, endFrequency, duration, type = "sine", volume = 0.2, delay = 0 } = opts;
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
const t0 = ctx.currentTime + delay;
|
|
osc.type = type;
|
|
osc.frequency.setValueAtTime(Math.max(frequency, 1), t0);
|
|
if (endFrequency && endFrequency !== frequency) {
|
|
osc.frequency.exponentialRampToValueAtTime(Math.max(endFrequency, 1), t0 + duration);
|
|
}
|
|
gain.gain.setValueAtTime(0.0001, t0);
|
|
gain.gain.exponentialRampToValueAtTime(volume, t0 + 0.012);
|
|
gain.gain.exponentialRampToValueAtTime(0.0001, t0 + duration);
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.start(t0);
|
|
osc.stop(t0 + duration + 0.05);
|
|
}
|
|
|
|
function playNoise(
|
|
ctx: AudioContext,
|
|
opts: { duration: number; volume?: number; delay?: number; lowpass?: number },
|
|
) {
|
|
const { duration, volume = 0.2, delay = 0, lowpass = 800 } = opts;
|
|
const bufferSize = Math.max(1, Math.floor(ctx.sampleRate * duration));
|
|
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
|
|
const data = buffer.getChannelData(0);
|
|
for (let i = 0; i < bufferSize; i++) {
|
|
data[i] = Math.random() * 2 - 1;
|
|
}
|
|
const source = ctx.createBufferSource();
|
|
source.buffer = buffer;
|
|
const filter = ctx.createBiquadFilter();
|
|
filter.type = "lowpass";
|
|
filter.frequency.setValueAtTime(lowpass, ctx.currentTime + delay);
|
|
const gain = ctx.createGain();
|
|
const t0 = ctx.currentTime + delay;
|
|
gain.gain.setValueAtTime(0.0001, t0);
|
|
gain.gain.exponentialRampToValueAtTime(volume, t0 + 0.005);
|
|
gain.gain.exponentialRampToValueAtTime(0.0001, t0 + duration);
|
|
source.connect(filter);
|
|
filter.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
source.start(t0);
|
|
}
|
|
|
|
export function playReveal() {
|
|
const ctx = getCtx();
|
|
if (!ctx) return;
|
|
playTone(ctx, { frequency: 400, duration: 0.05, type: "sine", volume: 0.08 });
|
|
}
|
|
|
|
export function playFlag() {
|
|
const ctx = getCtx();
|
|
if (!ctx) return;
|
|
playTone(ctx, { frequency: 300, duration: 0.07, type: "triangle", volume: 0.12 });
|
|
}
|
|
|
|
export function playExplosion() {
|
|
const ctx = getCtx();
|
|
if (!ctx) return;
|
|
playTone(ctx, { frequency: 120, endFrequency: 30, duration: 0.35, type: "square", volume: 0.28 });
|
|
playNoise(ctx, { duration: 0.28, volume: 0.35, lowpass: 900 });
|
|
}
|
|
|
|
export function playClear() {
|
|
const ctx = getCtx();
|
|
if (!ctx) return;
|
|
playTone(ctx, { frequency: 523, duration: 0.1, type: "sine", volume: 0.16 });
|
|
playTone(ctx, { frequency: 659, duration: 0.1, type: "sine", volume: 0.16, delay: 0.1 });
|
|
playTone(ctx, { frequency: 784, duration: 0.18, type: "sine", volume: 0.18, delay: 0.2 });
|
|
}
|
|
|
|
export function playXp() {
|
|
const ctx = getCtx();
|
|
if (!ctx) return;
|
|
playTone(ctx, { frequency: 660, duration: 0.09, type: "sine", volume: 0.18 });
|
|
playTone(ctx, { frequency: 880, duration: 0.16, type: "sine", volume: 0.18, delay: 0.09 });
|
|
}
|