Initial commit -- Core Cooldown v0.1.0

This commit is contained in:
2026-02-07 01:12:32 +02:00
commit 48cbbfa552
47 changed files with 15106 additions and 0 deletions
+283
View File
@@ -0,0 +1,283 @@
/**
* Synthesized notification sounds using the Web Audio API.
* No external audio files needed — all sounds are generated programmatically.
*/
let audioCtx: AudioContext | null = null;
function getAudioContext(): AudioContext {
if (!audioCtx) {
audioCtx = new AudioContext();
}
return audioCtx;
}
type SoundPreset = "bell" | "chime" | "soft" | "digital" | "harp" | "bowl" | "rain" | "whistle";
/**
* Play a notification sound with the given preset and volume.
* @param preset - One of: "bell", "chime", "soft", "digital"
* @param volume - 0 to 100
*/
export function playSound(preset: SoundPreset, volume: number): void {
const ctx = getAudioContext();
const gain = ctx.createGain();
gain.connect(ctx.destination);
const vol = (volume / 100) * 0.3; // Scale to reasonable max
switch (preset) {
case "bell":
playBell(ctx, gain, vol);
break;
case "chime":
playChime(ctx, gain, vol);
break;
case "soft":
playSoft(ctx, gain, vol);
break;
case "digital":
playDigital(ctx, gain, vol);
break;
case "harp":
playHarp(ctx, gain, vol);
break;
case "bowl":
playBowl(ctx, gain, vol);
break;
case "rain":
playRain(ctx, gain, vol);
break;
case "whistle":
playWhistle(ctx, gain, vol);
break;
}
}
/** Warm bell — two sine tones with harmonics and slow decay */
function playBell(ctx: AudioContext, destination: GainNode, vol: number) {
const now = ctx.currentTime;
// Fundamental
const osc1 = ctx.createOscillator();
const g1 = ctx.createGain();
osc1.type = "sine";
osc1.frequency.setValueAtTime(830, now);
g1.gain.setValueAtTime(vol, now);
g1.gain.exponentialRampToValueAtTime(0.001, now + 1.5);
osc1.connect(g1);
g1.connect(destination);
osc1.start(now);
osc1.stop(now + 1.5);
// Harmonic
const osc2 = ctx.createOscillator();
const g2 = ctx.createGain();
osc2.type = "sine";
osc2.frequency.setValueAtTime(1245, now);
g2.gain.setValueAtTime(vol * 0.4, now);
g2.gain.exponentialRampToValueAtTime(0.001, now + 1.0);
osc2.connect(g2);
g2.connect(destination);
osc2.start(now);
osc2.stop(now + 1.0);
}
/** Two-note ascending chime */
function playChime(ctx: AudioContext, destination: GainNode, vol: number) {
const now = ctx.currentTime;
const notes = [523.25, 659.25]; // C5, E5
notes.forEach((freq, i) => {
const osc = ctx.createOscillator();
const g = ctx.createGain();
osc.type = "sine";
osc.frequency.setValueAtTime(freq, now);
const start = now + i * 0.15;
g.gain.setValueAtTime(0, start);
g.gain.linearRampToValueAtTime(vol, start + 0.02);
g.gain.exponentialRampToValueAtTime(0.001, start + 0.8);
osc.connect(g);
g.connect(destination);
osc.start(start);
osc.stop(start + 0.8);
});
}
/** Gentle soft ping — filtered triangle wave */
function playSoft(ctx: AudioContext, destination: GainNode, vol: number) {
const now = ctx.currentTime;
const osc = ctx.createOscillator();
const filter = ctx.createBiquadFilter();
const g = ctx.createGain();
osc.type = "triangle";
osc.frequency.setValueAtTime(600, now);
osc.frequency.exponentialRampToValueAtTime(400, now + 0.5);
filter.type = "lowpass";
filter.frequency.setValueAtTime(2000, now);
filter.frequency.exponentialRampToValueAtTime(400, now + 0.8);
g.gain.setValueAtTime(vol, now);
g.gain.exponentialRampToValueAtTime(0.001, now + 1.2);
osc.connect(filter);
filter.connect(g);
g.connect(destination);
osc.start(now);
osc.stop(now + 1.2);
}
/** Digital blip — short square wave burst */
function playDigital(ctx: AudioContext, destination: GainNode, vol: number) {
const now = ctx.currentTime;
for (let i = 0; i < 2; i++) {
const osc = ctx.createOscillator();
const g = ctx.createGain();
osc.type = "square";
osc.frequency.setValueAtTime(880, now);
const start = now + i * 0.12;
g.gain.setValueAtTime(0, start);
g.gain.linearRampToValueAtTime(vol * 0.5, start + 0.01);
g.gain.exponentialRampToValueAtTime(0.001, start + 0.15);
osc.connect(g);
g.connect(destination);
osc.start(start);
osc.stop(start + 0.15);
}
}
/** Harp — cascading arpeggiated sine tones (C5-E5-G5-C6) */
function playHarp(ctx: AudioContext, destination: GainNode, vol: number) {
const now = ctx.currentTime;
const notes = [523.25, 659.25, 783.99, 1046.5]; // C5, E5, G5, C6
notes.forEach((freq, i) => {
const osc = ctx.createOscillator();
const g = ctx.createGain();
osc.type = "sine";
osc.frequency.setValueAtTime(freq, now);
const start = now + i * 0.09;
g.gain.setValueAtTime(0, start);
g.gain.linearRampToValueAtTime(vol * 0.8, start + 0.01);
g.gain.exponentialRampToValueAtTime(0.001, start + 1.2);
osc.connect(g);
g.connect(destination);
osc.start(start);
osc.stop(start + 1.2);
});
}
/** Singing bowl — low sine with slow beating from detuned pair */
function playBowl(ctx: AudioContext, destination: GainNode, vol: number) {
const now = ctx.currentTime;
// Two slightly detuned sines create a beating/shimmering effect
for (const freq of [293.66, 295.5]) { // ~D4 with 2Hz beat
const osc = ctx.createOscillator();
const g = ctx.createGain();
osc.type = "sine";
osc.frequency.setValueAtTime(freq, now);
g.gain.setValueAtTime(vol, now);
g.gain.exponentialRampToValueAtTime(0.001, now + 2.5);
osc.connect(g);
g.connect(destination);
osc.start(now);
osc.stop(now + 2.5);
}
// Upper harmonic shimmer
const osc3 = ctx.createOscillator();
const g3 = ctx.createGain();
osc3.type = "sine";
osc3.frequency.setValueAtTime(880, now);
g3.gain.setValueAtTime(vol * 0.15, now);
g3.gain.exponentialRampToValueAtTime(0.001, now + 1.5);
osc3.connect(g3);
g3.connect(destination);
osc3.start(now);
osc3.stop(now + 1.5);
}
/** Rain — filtered noise burst with gentle decay */
function playRain(ctx: AudioContext, destination: GainNode, vol: number) {
const now = ctx.currentTime;
const bufferSize = ctx.sampleRate * 1;
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
const data = buffer.getChannelData(0);
// White noise
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
const noise = ctx.createBufferSource();
noise.buffer = buffer;
const filter = ctx.createBiquadFilter();
filter.type = "bandpass";
filter.frequency.setValueAtTime(3000, now);
filter.frequency.exponentialRampToValueAtTime(800, now + 0.8);
filter.Q.setValueAtTime(0.5, now);
const g = ctx.createGain();
g.gain.setValueAtTime(vol * 0.6, now);
g.gain.exponentialRampToValueAtTime(0.001, now + 1.0);
noise.connect(filter);
filter.connect(g);
g.connect(destination);
noise.start(now);
noise.stop(now + 1.0);
}
/** Whistle — gentle two-note sine glide */
function playWhistle(ctx: AudioContext, destination: GainNode, vol: number) {
const now = ctx.currentTime;
const osc = ctx.createOscillator();
const g = ctx.createGain();
osc.type = "sine";
osc.frequency.setValueAtTime(880, now);
osc.frequency.exponentialRampToValueAtTime(1318.5, now + 0.3); // A5 → E6 glide up
osc.frequency.setValueAtTime(1318.5, now + 0.5);
osc.frequency.exponentialRampToValueAtTime(1046.5, now + 0.8); // E6 → C6 settle
g.gain.setValueAtTime(0, now);
g.gain.linearRampToValueAtTime(vol * 0.5, now + 0.05);
g.gain.setValueAtTime(vol * 0.5, now + 0.6);
g.gain.exponentialRampToValueAtTime(0.001, now + 1.0);
osc.connect(g);
g.connect(destination);
osc.start(now);
osc.stop(now + 1.0);
}
/** Play a completion sound — slightly different from start (descending) */
export function playBreakEndSound(preset: SoundPreset, volume: number): void {
const ctx = getAudioContext();
const gain = ctx.createGain();
gain.connect(ctx.destination);
const vol = (volume / 100) * 0.3;
const now = ctx.currentTime;
// Always a gentle descending two-note resolution
const notes = [659.25, 523.25]; // E5, C5 (descending = "done" feeling)
notes.forEach((freq, i) => {
const osc = ctx.createOscillator();
const g = ctx.createGain();
osc.type = preset === "digital" ? "square" : "sine";
osc.frequency.setValueAtTime(freq, now);
const start = now + i * 0.2;
g.gain.setValueAtTime(0, start);
g.gain.linearRampToValueAtTime(vol, start + 0.02);
g.gain.exponentialRampToValueAtTime(0.001, start + 0.6);
osc.connect(g);
g.connect(gain);
osc.start(start);
osc.stop(start + 0.6);
});
}