/* ============================================================ combo.codes — TypeFX Tiny Web Audio keystroke engine for the logo typewriter. No audio files: each keypress is a short filtered noise burst + a low "thock"; the closing ";" gets a bright two-tone ding. Exposed on window.TypeFX. Respects browser autoplay policy — call unlock() from a user gesture before sound will play. ============================================================ */ window.TypeFX = (function () { var ctx = null; function ensure() { if (!ctx) { var AC = window.AudioContext || window.webkitAudioContext; if (!AC) return null; try { ctx = new AC(); } catch (e) { return null; } } return ctx; } function unlock() { var c = ensure(); if (!c) return false; if (c.state === 'suspended' && c.resume) { c.resume(); } return c.state === 'running'; } function running() { return !!ctx && ctx.state === 'running'; } // one mechanical key click function click(opts) { var c = ensure(); if (!c || c.state !== 'running') return; var o = opts || {}; var t = c.currentTime; var gain = o.gain != null ? o.gain : 0.14; // high-frequency click (the "tick" of the key cap) var dur = 0.03; var frames = Math.max(1, Math.ceil(c.sampleRate * dur)); var buf = c.createBuffer(1, frames, c.sampleRate); var data = buf.getChannelData(0); for (var i = 0; i < frames; i++) { data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / frames, 2.6); } var src = c.createBufferSource(); src.buffer = buf; var bp = c.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.value = o.freq || 2400; bp.Q.value = 0.9; var g = c.createGain(); g.gain.value = gain; src.connect(bp); bp.connect(g); g.connect(c.destination); src.start(t); src.stop(t + dur); // low "thock" (the key bottoming out) var osc = c.createOscillator(); osc.type = 'triangle'; osc.frequency.setValueAtTime(o.thock || 175, t); var g2 = c.createGain(); g2.gain.setValueAtTime(gain * 0.7, t); g2.gain.exponentialRampToValueAtTime(0.0008, t + 0.05); osc.connect(g2); g2.connect(c.destination); osc.start(t); osc.stop(t + 0.06); } // bright confirmation tone for the closing ";" (fallback if no TTS) function ding() { var c = ensure(); if (!c || c.state !== 'running') return; var t = c.currentTime; var osc = c.createOscillator(); osc.type = 'square'; osc.frequency.setValueAtTime(740, t); osc.frequency.exponentialRampToValueAtTime(1180, t + 0.05); var g = c.createGain(); g.gain.setValueAtTime(0.0001, t); g.gain.exponentialRampToValueAtTime(0.10, t + 0.012); g.gain.exponentialRampToValueAtTime(0.0006, t + 0.20); osc.connect(g); g.connect(c.destination); osc.start(t); osc.stop(t + 0.22); } // robotic computer-voice announcing the name (game-synth style). // Uses the browser speech synth with a low pitch for a mechanical tone. function speak(text, opts) { try { if (!('speechSynthesis' in window)) { ding(); return; } var o = opts || {}; var ss = window.speechSynthesis; ss.cancel(); var u = new SpeechSynthesisUtterance(text); u.lang = 'en-US'; u.rate = o.rate != null ? o.rate : 0.9; u.pitch = o.pitch != null ? o.pitch : 0.4; u.volume = o.volume != null ? o.volume : 1; var voices = ss.getVoices() || []; function byName(list) { for (var i = 0; i < list.length; i++) { var needle = list[i]; var hit = voices.find(function (v) { return v.name.toLowerCase().indexOf(needle) !== -1; }); if (hit) return hit; } return null; } // classic 80s synth-chip voices first (macOS), then generic English var pref = (o.voiceName ? byName([String(o.voiceName).toLowerCase()]) : null) || byName(['zarvox', 'trinoids', 'albert', 'ralph', 'fred']) || voices.find(function (v) { return /en[-_]US/i.test(v.lang) && /google/i.test(v.name); }) || voices.find(function (v) { return /en[-_]US/i.test(v.lang); }) || voices.find(function (v) { return /^en/i.test(v.lang); }); if (pref) u.voice = pref; ss.speak(u); } catch (e) { ding(); } } function silence() { try { if ('speechSynthesis' in window) window.speechSynthesis.cancel(); } catch (e) {} } // prime the voice list (populated async in some browsers) try { if ('speechSynthesis' in window) { window.speechSynthesis.getVoices(); window.speechSynthesis.onvoiceschanged = function () { window.speechSynthesis.getVoices(); }; } } catch (e) {} return { unlock: unlock, running: running, click: click, ding: ding, speak: speak, silence: silence }; })();