Initial commit -- Core Cooldown v0.1.0
This commit is contained in:
@@ -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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user