clone

a daily shikaku puzzle for phones: one seeded puzzle per date, solved by dragging rectangles

main
rect.run / lib / audio / sfx.ts
sfx.ts 2.6K
1/**2 * Tiny synthesized arcade SFX. No assets, no network — square waves only.3 * Audio context is created lazily on first user gesture.4 */5export type SfxName =6 | "tap"7 | "place"8 | "remove"9 | "invalid"10 | "solve"11 | "clear";1213const SFX_KEY = "shikaku:v1:sfx";14let context: AudioContext | null = null;1516export function isSfxEnabled(17 storage: Pick<Storage, "getItem"> | undefined,18): boolean {19 return storage?.getItem(SFX_KEY) !== "off";20}2122export function setSfxEnabled(23 storage: Pick<Storage, "setItem" | "removeItem"> | undefined,24 enabled: boolean,25): void {26 if (!storage) return;27 if (enabled) storage.removeItem(SFX_KEY);28 else storage.setItem(SFX_KEY, "off");29}3031interface Note {32 freq: number;33 at: number;34 dur: number;35 type?: OscillatorType;36 gain?: number;37}3839const PATTERNS: Record<SfxName, Note[]> = {40 tap: [{ freq: 880, at: 0, dur: 0.03, gain: 0.05 }],41 place: [42 { freq: 523, at: 0, dur: 0.05 },43 { freq: 784, at: 0.05, dur: 0.07 },44 ],45 remove: [46 { freq: 660, at: 0, dur: 0.05 },47 { freq: 392, at: 0.05, dur: 0.07 },48 ],49 invalid: [50 { freq: 130, at: 0, dur: 0.08, type: "sawtooth", gain: 0.08 },51 { freq: 110, at: 0.08, dur: 0.1, type: "sawtooth", gain: 0.08 },52 ],53 clear: [54 { freq: 440, at: 0, dur: 0.05 },55 { freq: 330, at: 0.05, dur: 0.05 },56 { freq: 220, at: 0.1, dur: 0.08 },57 ],58 solve: [59 { freq: 523, at: 0, dur: 0.09 },60 { freq: 659, at: 0.09, dur: 0.09 },61 { freq: 784, at: 0.18, dur: 0.09 },62 { freq: 1047, at: 0.27, dur: 0.22 },63 { freq: 784, at: 0.42, dur: 0.06, gain: 0.05 },64 { freq: 1047, at: 0.48, dur: 0.3 },65 ],66};6768function getContext(): AudioContext | null {69 if (typeof globalThis.AudioContext === "undefined") return null;70 context ??= new AudioContext();71 if (context.state === "suspended") void context.resume();72 return context;73}7475export function playSfx(name: SfxName, enabled = true): void {76 if (!enabled) return;77 const ctx = getContext();78 if (!ctx) return;7980 const now = ctx.currentTime;81 for (const note of PATTERNS[name]) {82 const osc = ctx.createOscillator();83 const amp = ctx.createGain();84 osc.type = note.type ?? "square";85 osc.frequency.value = note.freq;86 const peak = note.gain ?? 0.07;87 amp.gain.setValueAtTime(0.0001, now + note.at);88 amp.gain.linearRampToValueAtTime(peak, now + note.at + 0.005);89 amp.gain.setValueAtTime(peak, now + note.at + note.dur * 0.7);90 amp.gain.exponentialRampToValueAtTime(0.0001, now + note.at + note.dur);91 osc.connect(amp).connect(ctx.destination);92 osc.start(now + note.at);93 osc.stop(now + note.at + note.dur + 0.02);94 }95}