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"; 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); } 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); }); }