Arslan1997's picture
launching
79ca2b4
Raw History Blame Contribute Delete
3.09 kB
/** Space-flavored UI sound, synthesized with Web Audio (no assets to load): one soft sonar ping
* (a sine with a shimmering detuned partner) that fades into a faint echo, used for every click.
* Very quiet. Viewers can turn sounds off in Settings (stored per browser). */
const KEY = "ns_click_sounds";
let ctx: AudioContext | null = null;
let bus: GainNode | null = null;
export function soundsEnabled(): boolean {
try {
return localStorage.getItem(KEY) !== "0";
} catch {
return true;
}
}
export function setSoundsEnabled(on: boolean): void {
try {
localStorage.setItem(KEY, on ? "1" : "0");
} catch {
/* private mode: default stays on */
}
}
/** Master bus with a dark feedback echo, built once. */
function output(ac: AudioContext): GainNode {
if (bus) return bus;
bus = ac.createGain();
bus.gain.value = 0.9;
const delay = ac.createDelay(1);
delay.delayTime.value = 0.19;
const feedback = ac.createGain();
feedback.gain.value = 0.32;
const tone = ac.createBiquadFilter();
tone.type = "lowpass";
tone.frequency.value = 1800;
const wet = ac.createGain();
wet.gain.value = 0.35;
bus.connect(ac.destination);
bus.connect(delay);
delay.connect(tone).connect(feedback).connect(delay);
tone.connect(wet).connect(ac.destination);
return bus;
}
function voice(ac: AudioContext, out: AudioNode, type: OscillatorType, from: number, to: number, start: number,
dur: number, peak: number, detune = 0): void {
const osc = ac.createOscillator();
const gain = ac.createGain();
osc.type = type;
osc.detune.value = detune;
osc.frequency.setValueAtTime(from, start);
osc.frequency.exponentialRampToValueAtTime(to, start + dur * 0.8);
gain.gain.setValueAtTime(0.0001, start);
gain.gain.exponentialRampToValueAtTime(peak, start + 0.012);
gain.gain.exponentialRampToValueAtTime(0.0001, start + dur);
osc.connect(gain).connect(out);
osc.start(start);
osc.stop(start + dur + 0.02);
}
export function playClick(): void {
if (!soundsEnabled()) return;
try {
const AC = window.AudioContext ?? (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
if (!AC) return;
ctx ??= new AC();
if (ctx.state === "suspended") void ctx.resume();
const now = ctx.currentTime + 0.005;
const out = output(ctx);
// Sonar ping: a clean tone with a slightly detuned shimmer, dropping a touch as it fades.
voice(ctx, out, "sine", 1046, 988, now, 0.22, 0.025);
voice(ctx, out, "sine", 1568, 1480, now, 0.16, 0.006, 7);
} catch {
/* audio is decoration only */
}
}
/** One delegated listener for the whole app. */
export function installClickSounds(): void {
document.addEventListener(
"pointerdown",
(e) => {
if (e.button !== 0) return;
const el = (e.target as Element | null)?.closest?.("button, a.btn, [role='tab'], .cal-item, .agenda-row");
if (!el || (el as HTMLButtonElement).disabled || el.getAttribute("aria-disabled") === "true") return;
playClick();
},
{ capture: true, passive: true },
);
}