init
This commit is contained in:
121 files changed
+17641
No files matched your search
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Alarms and ticks, synthesised with the Web Audio API.
|
||||
*
|
||||
* No audio files to ship or to 404, and no autoplay surprise: the context is
|
||||
* created on the first user gesture (starting a timer counts) and reused.
|
||||
*/
|
||||
let context: AudioContext | null = null;
|
||||
|
||||
function ensureContext(): AudioContext | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
if (!context) {
|
||||
const Ctor = window.AudioContext ?? (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
|
||||
if (!Ctor) return null;
|
||||
context = new Ctor();
|
||||
}
|
||||
if (context.state === "suspended") void context.resume();
|
||||
return context;
|
||||
}
|
||||
|
||||
function beep(frequency: number, seconds: number, volume: number, at = 0): void {
|
||||
const ctx = ensureContext();
|
||||
if (!ctx) return;
|
||||
const oscillator = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
oscillator.type = "sine";
|
||||
oscillator.frequency.value = frequency;
|
||||
const start = ctx.currentTime + at;
|
||||
// A short ramp instead of a hard stop; a clipped sine clicks.
|
||||
gain.gain.setValueAtTime(0.0001, start);
|
||||
gain.gain.exponentialRampToValueAtTime(Math.max(0.0002, volume), start + 0.02);
|
||||
gain.gain.exponentialRampToValueAtTime(0.0001, start + seconds);
|
||||
oscillator.connect(gain).connect(ctx.destination);
|
||||
oscillator.start(start);
|
||||
oscillator.stop(start + seconds + 0.05);
|
||||
}
|
||||
|
||||
export const sound = {
|
||||
/** Work interval finished: three rising notes. */
|
||||
workDone(volume: number): void {
|
||||
const level = volume / 100;
|
||||
beep(660, 0.18, 0.25 * level, 0);
|
||||
beep(880, 0.18, 0.25 * level, 0.2);
|
||||
beep(1046, 0.4, 0.25 * level, 0.4);
|
||||
},
|
||||
/** Break finished: two falling notes — back to it. */
|
||||
breakDone(volume: number): void {
|
||||
const level = volume / 100;
|
||||
beep(880, 0.18, 0.22 * level, 0);
|
||||
beep(587, 0.35, 0.22 * level, 0.2);
|
||||
},
|
||||
tick(volume: number): void {
|
||||
beep(1200, 0.03, 0.04 * (volume / 100));
|
||||
},
|
||||
unlock(): void {
|
||||
ensureContext();
|
||||
},
|
||||
};
|
||||
|
||||
/** A desktop notification, when the browser (or Electron) allows one. */
|
||||
export function notify(title: string, body: string): void {
|
||||
if (typeof Notification === "undefined") return;
|
||||
if (Notification.permission === "granted") {
|
||||
new Notification(title, { body, icon: undefined });
|
||||
} else if (Notification.permission !== "denied") {
|
||||
void Notification.requestPermission();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user