/** * Cheer — Web Audio chimes and arpeggios used on correct answers / mastery / level-up. */ let ctx = null; function ensure() { if (!ctx) { try { ctx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) {} } return ctx; } function tone(c, freq, when, dur = 0.45, gain = 0.18, type = 'triangle') { const o = c.createOscillator(), g = c.createGain(); o.type = type; o.frequency.value = freq; g.gain.setValueAtTime(0, when); g.gain.linearRampToValueAtTime(gain, when + 0.02); g.gain.exponentialRampToValueAtTime(0.001, when + dur); o.connect(g).connect(c.destination); o.start(when); o.stop(when + dur + 0.05); } export const Cheer = { enabled: true, prime() { ensure(); }, small() { if (!this.enabled) return; const c = ensure(); if (!c) return; const now = c.currentTime; [523.25, 659.25, 783.99, 1046.5].forEach((f, i) => tone(c, f, now + i * 0.08)); }, master() { if (!this.enabled) return; const c = ensure(); if (!c) return; const now = c.currentTime; [523.25, 659.25, 783.99, 1046.5, 1318.51, 1567.98, 2093.0].forEach((f, i) => tone(c, f, now + i * 0.07, 0.6, 0.22, i % 2 ? 'sine' : 'triangle') ); }, levelUp() { this.master(); }, };