<!doctype html><html><head><meta charset="utf-8"><title>lilt bar</title><style>@font-face { font-family: Figtree; src: url(fonts/Figtree.ttf); font-weight: 300 900; }html, body { margin: 0; height: 100%; background: transparent; overflow: hidden; }body { font: 500 12px Figtree, sans-serif; color: #fff; user-select: none; -webkit-user-select: none; cursor: default; }#root { position: fixed; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; padding-bottom: 4px; box-sizing: border-box; gap: 6px; }button { all: unset; display: grid; place-items: center; cursor: pointer; }svg { display: block; } /* idle: the small grey handle */.idle { width: 34px; height: 6px; border-radius: 4px; background: rgba(110, 110, 110, .8); border: 1px solid rgba(255, 255, 255, .45); position: relative; }.idle.nt::after { content: ""; position: absolute; right: -10px; top: 0; width: 6px; height: 6px; border-radius: 50%; background: #ff4d4d; animation: pulse 1.4s infinite; } /* hover: mic pill + notetaker circle (grows a chevron when hovered), tooltips sit over their own pill */.row { display: flex; gap: 6px; align-items: center; }.slot { position: relative; display: flex; }.slot.nt { width: 52px; }.pill { height: 30px; border-radius: 15px; background: #161616; box-sizing: border-box; border: 1px solid rgba(255, 255, 255, .2); box-shadow: 0 4px 14px rgba(0, 0, 0, .3); display: flex; align-items: center; justify-content: center; color: #f2f2f2; }.mic { width: 50px; }.mic:hover { background: #222; }.ntp { min-width: 30px; background: #232323; padding: 0; transition: padding .12s; }.ntp .rec { width: 28px; height: 28px; border-radius: 50%; }.ntp.open { padding-right: 6px; }.ntp.open .rec { background: #333; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .14); }.ntp .dot { position: absolute; top: -3px; left: 21px; width: 7px; height: 7px; border-radius: 50%; background: #5cc0a8; box-shadow: 0 0 0 1.5px #e9f6f1; }.ntp.on .dot { background: #ff4d4d; box-shadow: 0 0 0 1.5px #ffe3e3; animation: pulse 1.4s infinite; }.chev { display: none !important; width: 16px; height: 28px; color: #8d8d8d; }.ntp.open .chev { display: grid !important; }.chev:hover { color: #fff; } .tip { position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%); background: #000; color: #fff; padding: 5px 12px; border-radius: 14px; font-size: 14px; line-height: 18px; font-weight: 500; white-space: nowrap; box-shadow: 0 0 0 1px rgba(255, 255, 255, .12); pointer-events: none; }.tip b { font-weight: 700; } /* popover from the chevron */.pop { width: 206px; background: #1b1b1b; border-radius: 16px; padding: 16px 14px 14px; box-sizing: border-box; box-shadow: 0 0 0 1px rgba(255, 255, 255, .1), 0 10px 30px rgba(0, 0, 0, .45); display: flex; flex-direction: column; align-items: center; gap: 10px; text-align: center; }.pop .t { font-size: 13px; font-weight: 500; color: #f2f2f2; }.pop button { background: #fdf9ea; color: #111; font-weight: 600; font-size: 12px; padding: 7px 14px; border-radius: 8px; }.pop button.ghost { background: #2c2c2c; color: #eee; } /* recording */.recp { height: 28px; border-radius: 14px; padding: 0 12px; gap: 8px; }.wave { display: flex; align-items: center; gap: 2px; height: 20px; }.wave i { display: block; width: 2.5px; border-radius: 2px; background: #fff; height: 3px; transition: height 60ms linear; }.x { width: 18px; height: 18px; border-radius: 50%; background: #333; color: #ddd; font-size: 13px; margin-left: -6px; }.stop { width: 18px; height: 18px; border-radius: 50%; background: #ff4d4d; margin-right: -6px; }.stop::after { content: ""; width: 6px; height: 6px; border-radius: 1.5px; background: #fff; } /* processing */.procp { height: 28px; border-radius: 14px; padding: 0 14px; gap: 4px; }.procp i { width: 5px; height: 5px; border-radius: 50%; background: #fff; animation: dots 1s infinite ease-in-out; }.procp i:nth-child(2) { animation-delay: .15s; }.procp i:nth-child(3) { animation-delay: .3s; } @keyframes pulse { 50% { opacity: .35; } }@keyframes dots { 0%, 100% { opacity: .3; transform: translateY(0); } 50% { opacity: 1; transform: translateY(-3px); } }</style></head><body><div id="root"></div><script>(function () { 'use strict'; var root = document.getElementById('root'); var S = { recording: false, processing: false, notetaker: false, hotkeyLabel: 'Right Alt', settings: {} }; var hover = false, menu = false, tip = null, lastSize = '', BARS = 18; var ICON = { mic: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><rect x="9" y="2.5" width="6" height="12" rx="3" fill="currentColor" stroke="none"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3.5"/></svg>', rec: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4"><circle cx="12" cy="12" r="8.5"/><circle cx="12" cy="12" r="4.2" fill="currentColor" stroke="none"/></svg>', chev: '<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M6 15l6-6 6 6"/></svg>', cal: '<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#cfcfcf" stroke-width="1.6" stroke-linecap="round"><rect x="3.5" y="5" width="17" height="15.5" rx="2.5"/><path d="M3.5 10h17M8 3v4M16 3v4"/><path d="M8 13.5h.01M12 13.5h.01M16 13.5h.01M8 17h.01M12 17h.01" stroke-width="2.4"/></svg>' }; function view() { if (S.recording) return 'rec'; if (S.processing) return 'proc'; if (menu) return 'menu'; if (hover) return 'hover'; return 'idle'; } // Window size per view, kept as small as possible so it never blocks clicks. function size(v) { if (v === 'rec') return S.recording === 'handsfree' ? [230, 40] : [190, 40]; if (v === 'proc') return [110, 40]; if (v === 'menu') return [260, 210]; if (v === 'hover') return [300, 76]; return [76, 22]; } function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); } function pills() { var open = tip === 'nt' || tip === 'chev' || menu; var micTip = tip === 'mic' ? '<div class="tip">Dictate <b>' + esc(S.hotkeyLabel) + '</b></div>' : ''; var ntTip = tip === 'nt' ? '<div class="tip">' + (S.notetaker ? 'Stop Notetaker' : 'Start Notetaker') + ' <b>Ctrl+Alt+M</b></div>' : ''; return '<div class="row">' + '<div class="slot">' + micTip + '<button class="pill mic" data-act="dictate" data-tip="mic">' + ICON.mic + '</button></div>' + '<div class="slot nt">' + ntTip + '<div class="pill ntp' + (open ? ' open' : '') + (S.notetaker ? ' on' : '') + '" data-tip="ntp">' + '<button class="rec" data-act="notetaker" data-tip="nt">' + ICON.rec + '</button><span class="dot"></span>' + '<button class="chev" data-act="menu" data-tip="chev">' + ICON.chev + '</button></div></div></div>'; } function render() { var v = view(), sz = size(v), key = sz.join('x'); if (key !== lastSize) { lastSize = key; window.lilt.bar.resize(sz[0], sz[1]); } var h = ''; if (v === 'idle') h = '<div class="idle' + (S.notetaker ? ' nt' : '') + '"></div>'; else if (v === 'hover') h = pills(); else if (v === 'menu') { h = '<div class="pop">' + '<div class="t">' + (S.notetaker ? 'Notetaker is recording' : 'Notetaker') + '</div>' + '<button data-act="notetaker">' + (S.notetaker ? 'Stop Notetaker' : 'Start Notetaker') + '</button>' + (S.settings.scratchpadInBar ? '<button class="ghost" data-act="scratchpad">Open Scratchpad</button>' : '') + '<button class="ghost" data-act="hub">Open lilt</button></div>' + pills(); } else if (v === 'rec') { var hf = S.recording === 'handsfree', bars = ''; for (var i = 0; i < BARS; i++) bars += '<i></i>'; h = '<div class="pill recp">' + (hf ? '<button class="x" data-act="cancel">✕</button>' : '') + '<div class="wave">' + bars + '</div>' + (hf ? '<button class="stop" data-act="stop"></button>' : '') + '</div>'; } else h = '<div class="pill procp"><i></i><i></i><i></i></div>'; root.innerHTML = h; } document.documentElement.addEventListener('mouseenter', function () { hover = true; render(); }); document.documentElement.addEventListener('mouseleave', function () { hover = false; menu = false; tip = null; render(); }); root.addEventListener('mouseover', function (e) { var t = e.target.closest('[data-tip]'), next = t ? t.getAttribute('data-tip') : null; if (next === 'ntp') next = 'chev'; if (next !== tip && view() === 'hover') { tip = next; render(); } }); root.addEventListener('click', function (e) { var b = e.target.closest('[data-act]'); if (!b) return; var act = b.getAttribute('data-act'); if (act === 'menu') { menu = !menu; tip = null; render(); return; } menu = false; window.lilt.bar.action(act); render(); }); // ------------------------------------------------------------ sounds function tone(up) { try { var c = new AudioContext(), o = c.createOscillator(), g = c.createGain(), t = c.currentTime; o.type = 'sine'; o.frequency.setValueAtTime(up ? 560 : 820, t); o.frequency.exponentialRampToValueAtTime(up ? 820 : 560, t + 0.09); g.gain.setValueAtTime(0.0001, t); g.gain.exponentialRampToValueAtTime(0.1, t + 0.015); g.gain.exponentialRampToValueAtTime(0.0001, t + 0.15); o.connect(g); g.connect(c.destination); o.start(t); o.stop(t + 0.17); o.onended = function () { c.close(); }; } catch (e) { /* no audio output */ } } // ------------------------------------------------------------ microphone var WORKLET = "class Tap extends AudioWorkletProcessor{process(i){var c=i[0]&&i[0][0];if(c)this.port.postMessage(c.slice(0));return true}}registerProcessor('tap',Tap)"; var ctx = null, stream = null, micKey = null, closeTimer = 0, session = 0; var recording = false, chunks = [], pre = [], preLen = 0, recStart = 0, level = 0, levels = []; function onChunk(c) { var s = 0; for (var i = 0; i < c.length; i++) s += c[i] * c[i]; level = Math.max(Math.sqrt(s / c.length), level * 0.9); if (recording) { chunks.push(c); return; } // Keep ~300 ms of pre-roll while the mic is warm so the first syllable survives. pre.push(c); preLen += c.length; while (pre.length && preLen - pre[0].length > ctx.sampleRate * 0.3) preLen -= pre.shift().length; } function getStream(micId) { var base = { channelCount: 1, echoCancellation: false, noiseSuppression: true, autoGainControl: true }; var want = micId ? Object.assign({ deviceId: { exact: micId } }, base) : base; return navigator.mediaDevices.getUserMedia({ audio: want }).catch(function () { return navigator.mediaDevices.getUserMedia({ audio: base }); // chosen mic unplugged }); } async function openMic(micId) { if (stream && micKey === micId) return; closeMic(); stream = await getStream(micId); var src; try { ctx = new AudioContext({ sampleRate: 16000 }); src = ctx.createMediaStreamSource(stream); } catch (e) { ctx = new AudioContext(); src = ctx.createMediaStreamSource(stream); } await ctx.audioWorklet.addModule(URL.createObjectURL(new Blob([WORKLET], { type: 'text/javascript' }))); var node = new AudioWorkletNode(ctx, 'tap'); node.port.onmessage = function (e) { onChunk(e.data); }; var mute = ctx.createGain(); mute.gain.value = 0; src.connect(node); node.connect(mute); mute.connect(ctx.destination); micKey = micId; pre = []; preLen = 0; } function closeMic() { if (stream) stream.getTracks().forEach(function (t) { t.stop(); }); if (ctx) ctx.close(); stream = null; ctx = null; micKey = null; } function idleLater() { clearTimeout(closeTimer); closeTimer = setTimeout(function () { if (!recording) closeMic(); }, 120000); } function resample(x, from) { if (from === 16000) return x; var r = from / 16000, n = Math.floor(x.length / r), y = new Float32Array(n); for (var i = 0; i < n; i++) { var a = Math.floor(i * r), b = Math.min(x.length, Math.floor((i + 1) * r)), s = 0; for (var j = a; j < b; j++) s += x[j]; y[i] = s / Math.max(1, b - a); } return y; } async function start(cmd) { var my = ++session; clearTimeout(closeTimer); if (cmd.sounds) tone(true); try { await openMic(cmd.micId || ''); } catch (e) { console.error('microphone:', e); return; } if (my !== session) return; chunks = pre.slice(); pre = []; preLen = 0; recording = true; recStart = performance.now(); levels = []; } function stop(cmd) { session++; if (cmd.sounds) tone(false); var ms = recording ? performance.now() - recStart : 0; var total = 0, i, off = 0; for (i = 0; i < chunks.length; i++) total += chunks[i].length; var all = new Float32Array(recording ? total : 0); if (recording) for (i = 0; i < chunks.length; i++) { all.set(chunks[i], off); off += chunks[i].length; } var out = recording && ctx ? resample(all, ctx.sampleRate) : all; recording = false; chunks = []; window.lilt.bar.audio(out.buffer, Math.round(ms)); idleLater(); } function cancel() { session++; recording = false; chunks = []; idleLater(); } // Waveform: a scrolling history of the mic level. function tick() { if (view() === 'rec') { levels.push(Math.min(1, Math.sqrt(level) * 2.2)); level *= 0.6; if (levels.length > BARS) levels.shift(); var bars = root.querySelectorAll('.wave i'); for (var i = 0; i < bars.length; i++) { var v = levels[i - (BARS - levels.length)] || 0; bars[i].style.height = (3 + v * 17).toFixed(1) + 'px'; } } setTimeout(tick, 50); } tick(); window.lilt.bar.onCommand(function (c) { if (c.type === 'start') start(c); else if (c.type === 'stop') stop(c); else if (c.type === 'cancel') cancel(); else if (c.type === 'state') { var was = view(); S = c; if (!S.settings) S.settings = {}; if (view() !== was || was === 'hover' || was === 'idle') render(); } }); render();})();</script></body></html>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>lilt bar</title>
<style>
@font-face { font-family: Figtree; src: url(fonts/Figtree.ttf); font-weight: 300 900; }
html, body { margin: 0; height: 100%; background: transparent; overflow: hidden; }
body { font: 500 12px Figtree, sans-serif; color: #fff; user-select: none; -webkit-user-select: none; cursor: default; }
#root { position: fixed; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; padding-bottom: 4px; box-sizing: border-box; gap: 6px; }
button { all: unset; display: grid; place-items: center; cursor: pointer; }
svg { display: block; }
/* idle: the small grey handle */
.idle { width: 34px; height: 6px; border-radius: 4px; background: rgba(110, 110, 110, .8); border: 1px solid rgba(255, 255, 255, .45); position: relative; }
.idle.nt::after { content: ""; position: absolute; right: -10px; top: 0; width: 6px; height: 6px; border-radius: 50%; background: #ff4d4d; animation: pulse 1.4s infinite; }
/* hover: mic pill + notetaker circle (grows a chevron when hovered), tooltips sit over their own pill */
.row { display: flex; gap: 6px; align-items: center; }
.slot { position: relative; display: flex; }
.slot.nt { width: 52px; }
.pill { height: 30px; border-radius: 15px; background: #161616; box-sizing: border-box; border: 1px solid rgba(255, 255, 255, .2); box-shadow: 0 4px 14px rgba(0, 0, 0, .3); display: flex; align-items: center; justify-content: center; color: #f2f2f2; }
.mic { width: 50px; }
.mic:hover { background: #222; }
.ntp { min-width: 30px; background: #232323; padding: 0; transition: padding .12s; }
.ntp .rec { width: 28px; height: 28px; border-radius: 50%; }
.ntp.open { padding-right: 6px; }
.ntp.open .rec { background: #333; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .14); }
.ntp .dot { position: absolute; top: -3px; left: 21px; width: 7px; height: 7px; border-radius: 50%; background: #5cc0a8; box-shadow: 0 0 0 1.5px #e9f6f1; }
.ntp.on .dot { background: #ff4d4d; box-shadow: 0 0 0 1.5px #ffe3e3; animation: pulse 1.4s infinite; }
.chev { display: none !important; width: 16px; height: 28px; color: #8d8d8d; }
.ntp.open .chev { display: grid !important; }
.chev:hover { color: #fff; }
.tip { position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%); background: #000; color: #fff; padding: 5px 12px; border-radius: 14px; font-size: 14px; line-height: 18px; font-weight: 500; white-space: nowrap; box-shadow: 0 0 0 1px rgba(255, 255, 255, .12); pointer-events: none; }
.tip b { font-weight: 700; }
/* popover from the chevron */
.pop { width: 206px; background: #1b1b1b; border-radius: 16px; padding: 16px 14px 14px; box-sizing: border-box; box-shadow: 0 0 0 1px rgba(255, 255, 255, .1), 0 10px 30px rgba(0, 0, 0, .45); display: flex; flex-direction: column; align-items: center; gap: 10px; text-align: center; }
.pop .t { font-size: 13px; font-weight: 500; color: #f2f2f2; }
.pop button { background: #fdf9ea; color: #111; font-weight: 600; font-size: 12px; padding: 7px 14px; border-radius: 8px; }
.pop button.ghost { background: #2c2c2c; color: #eee; }
/* recording */
.recp { height: 28px; border-radius: 14px; padding: 0 12px; gap: 8px; }
.wave { display: flex; align-items: center; gap: 2px; height: 20px; }
.wave i { display: block; width: 2.5px; border-radius: 2px; background: #fff; height: 3px; transition: height 60ms linear; }
.x { width: 18px; height: 18px; border-radius: 50%; background: #333; color: #ddd; font-size: 13px; margin-left: -6px; }
.stop { width: 18px; height: 18px; border-radius: 50%; background: #ff4d4d; margin-right: -6px; }
.stop::after { content: ""; width: 6px; height: 6px; border-radius: 1.5px; background: #fff; }
/* processing */
.procp { height: 28px; border-radius: 14px; padding: 0 14px; gap: 4px; }
.procp i { width: 5px; height: 5px; border-radius: 50%; background: #fff; animation: dots 1s infinite ease-in-out; }
.procp i:nth-child(2) { animation-delay: .15s; }
.procp i:nth-child(3) { animation-delay: .3s; }
@keyframes pulse { 50% { opacity: .35; } }
@keyframes dots { 0%, 100% { opacity: .3; transform: translateY(0); } 50% { opacity: 1; transform: translateY(-3px); } }
</style>
</head>
<body>
<div id="root"></div>
<script>
(function () {
'use strict';
var root = document.getElementById('root');
var S = { recording: false, processing: false, notetaker: false, hotkeyLabel: 'Right Alt', settings: {} };
var hover = false, menu = false, tip = null, lastSize = '', BARS = 18;
var ICON = {
mic: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><rect x="9" y="2.5" width="6" height="12" rx="3" fill="currentColor" stroke="none"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3.5"/></svg>',
rec: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4"><circle cx="12" cy="12" r="8.5"/><circle cx="12" cy="12" r="4.2" fill="currentColor" stroke="none"/></svg>',
chev: '<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M6 15l6-6 6 6"/></svg>',
cal: '<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#cfcfcf" stroke-width="1.6" stroke-linecap="round"><rect x="3.5" y="5" width="17" height="15.5" rx="2.5"/><path d="M3.5 10h17M8 3v4M16 3v4"/><path d="M8 13.5h.01M12 13.5h.01M16 13.5h.01M8 17h.01M12 17h.01" stroke-width="2.4"/></svg>'
};
function view() {
if (S.recording) return 'rec';
if (S.processing) return 'proc';
if (menu) return 'menu';
if (hover) return 'hover';
return 'idle';
}
// Window size per view, kept as small as possible so it never blocks clicks.
function size(v) {
if (v === 'rec') return S.recording === 'handsfree' ? [230, 40] : [190, 40];
if (v === 'proc') return [110, 40];
if (v === 'menu') return [260, 210];
if (v === 'hover') return [300, 76];
return [76, 22];
}
function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); }
function pills() {
var open = tip === 'nt' || tip === 'chev' || menu;
var micTip = tip === 'mic' ? '<div class="tip">Dictate <b>' + esc(S.hotkeyLabel) + '</b></div>' : '';
var ntTip = tip === 'nt' ? '<div class="tip">' + (S.notetaker ? 'Stop Notetaker' : 'Start Notetaker') + ' <b>Ctrl+Alt+M</b></div>' : '';
return '<div class="row">' +
'<div class="slot">' + micTip + '<button class="pill mic" data-act="dictate" data-tip="mic">' + ICON.mic + '</button></div>' +
'<div class="slot nt">' + ntTip + '<div class="pill ntp' + (open ? ' open' : '') + (S.notetaker ? ' on' : '') + '" data-tip="ntp">' +
'<button class="rec" data-act="notetaker" data-tip="nt">' + ICON.rec + '</button><span class="dot"></span>' +
'<button class="chev" data-act="menu" data-tip="chev">' + ICON.chev + '</button></div></div></div>';
}
function render() {
var v = view(), sz = size(v), key = sz.join('x');
if (key !== lastSize) { lastSize = key; window.lilt.bar.resize(sz[0], sz[1]); }
var h = '';
if (v === 'idle') h = '<div class="idle' + (S.notetaker ? ' nt' : '') + '"></div>';
else if (v === 'hover') h = pills();
else if (v === 'menu') {
h = '<div class="pop">' + '<div class="t">' + (S.notetaker ? 'Notetaker is recording' : 'Notetaker') + '</div>' +
'<button data-act="notetaker">' + (S.notetaker ? 'Stop Notetaker' : 'Start Notetaker') + '</button>' +
(S.settings.scratchpadInBar ? '<button class="ghost" data-act="scratchpad">Open Scratchpad</button>' : '') +
'<button class="ghost" data-act="hub">Open lilt</button></div>' + pills();
} else if (v === 'rec') {
var hf = S.recording === 'handsfree', bars = '';
for (var i = 0; i < BARS; i++) bars += '<i></i>';
h = '<div class="pill recp">' + (hf ? '<button class="x" data-act="cancel">✕</button>' : '') +
'<div class="wave">' + bars + '</div>' + (hf ? '<button class="stop" data-act="stop"></button>' : '') + '</div>';
} else h = '<div class="pill procp"><i></i><i></i><i></i></div>';
root.innerHTML = h;
}
document.documentElement.addEventListener('mouseenter', function () { hover = true; render(); });
document.documentElement.addEventListener('mouseleave', function () { hover = false; menu = false; tip = null; render(); });
root.addEventListener('mouseover', function (e) {
var t = e.target.closest('[data-tip]'), next = t ? t.getAttribute('data-tip') : null;
if (next === 'ntp') next = 'chev';
if (next !== tip && view() === 'hover') { tip = next; render(); }
});
root.addEventListener('click', function (e) {
var b = e.target.closest('[data-act]');
if (!b) return;
var act = b.getAttribute('data-act');
if (act === 'menu') { menu = !menu; tip = null; render(); return; }
menu = false;
window.lilt.bar.action(act);
render();
});
// ------------------------------------------------------------ sounds
function tone(up) {
try {
var c = new AudioContext(), o = c.createOscillator(), g = c.createGain(), t = c.currentTime;
o.type = 'sine';
o.frequency.setValueAtTime(up ? 560 : 820, t);
o.frequency.exponentialRampToValueAtTime(up ? 820 : 560, t + 0.09);
g.gain.setValueAtTime(0.0001, t);
g.gain.exponentialRampToValueAtTime(0.1, t + 0.015);
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.15);
o.connect(g); g.connect(c.destination);
o.start(t); o.stop(t + 0.17);
o.onended = function () { c.close(); };
} catch (e) { /* no audio output */ }
}
// ------------------------------------------------------------ microphone
var WORKLET = "class Tap extends AudioWorkletProcessor{process(i){var c=i[0]&&i[0][0];if(c)this.port.postMessage(c.slice(0));return true}}registerProcessor('tap',Tap)";
var ctx = null, stream = null, micKey = null, closeTimer = 0, session = 0;
var recording = false, chunks = [], pre = [], preLen = 0, recStart = 0, level = 0, levels = [];
function onChunk(c) {
var s = 0;
for (var i = 0; i < c.length; i++) s += c[i] * c[i];
level = Math.max(Math.sqrt(s / c.length), level * 0.9);
if (recording) { chunks.push(c); return; }
// Keep ~300 ms of pre-roll while the mic is warm so the first syllable survives.
pre.push(c); preLen += c.length;
while (pre.length && preLen - pre[0].length > ctx.sampleRate * 0.3) preLen -= pre.shift().length;
}
function getStream(micId) {
var base = { channelCount: 1, echoCancellation: false, noiseSuppression: true, autoGainControl: true };
var want = micId ? Object.assign({ deviceId: { exact: micId } }, base) : base;
return navigator.mediaDevices.getUserMedia({ audio: want }).catch(function () {
return navigator.mediaDevices.getUserMedia({ audio: base }); // chosen mic unplugged
});
}
async function openMic(micId) {
if (stream && micKey === micId) return;
closeMic();
stream = await getStream(micId);
var src;
try { ctx = new AudioContext({ sampleRate: 16000 }); src = ctx.createMediaStreamSource(stream); }
catch (e) { ctx = new AudioContext(); src = ctx.createMediaStreamSource(stream); }
await ctx.audioWorklet.addModule(URL.createObjectURL(new Blob([WORKLET], { type: 'text/javascript' })));
var node = new AudioWorkletNode(ctx, 'tap');
node.port.onmessage = function (e) { onChunk(e.data); };
var mute = ctx.createGain();
mute.gain.value = 0;
src.connect(node); node.connect(mute); mute.connect(ctx.destination);
micKey = micId; pre = []; preLen = 0;
}
function closeMic() {
if (stream) stream.getTracks().forEach(function (t) { t.stop(); });
if (ctx) ctx.close();
stream = null; ctx = null; micKey = null;
}
function idleLater() {
clearTimeout(closeTimer);
closeTimer = setTimeout(function () { if (!recording) closeMic(); }, 120000);
}
function resample(x, from) {
if (from === 16000) return x;
var r = from / 16000, n = Math.floor(x.length / r), y = new Float32Array(n);
for (var i = 0; i < n; i++) {
var a = Math.floor(i * r), b = Math.min(x.length, Math.floor((i + 1) * r)), s = 0;
for (var j = a; j < b; j++) s += x[j];
y[i] = s / Math.max(1, b - a);
}
return y;
}
async function start(cmd) {
var my = ++session;
clearTimeout(closeTimer);
if (cmd.sounds) tone(true);
try { await openMic(cmd.micId || ''); } catch (e) { console.error('microphone:', e); return; }
if (my !== session) return;
chunks = pre.slice(); pre = []; preLen = 0;
recording = true; recStart = performance.now(); levels = [];
}
function stop(cmd) {
session++;
if (cmd.sounds) tone(false);
var ms = recording ? performance.now() - recStart : 0;
var total = 0, i, off = 0;
for (i = 0; i < chunks.length; i++) total += chunks[i].length;
var all = new Float32Array(recording ? total : 0);
if (recording) for (i = 0; i < chunks.length; i++) { all.set(chunks[i], off); off += chunks[i].length; }
var out = recording && ctx ? resample(all, ctx.sampleRate) : all;
recording = false; chunks = [];
window.lilt.bar.audio(out.buffer, Math.round(ms));
idleLater();
}
function cancel() {
session++; recording = false; chunks = [];
idleLater();
}
// Waveform: a scrolling history of the mic level.
function tick() {
if (view() === 'rec') {
levels.push(Math.min(1, Math.sqrt(level) * 2.2));
level *= 0.6;
if (levels.length > BARS) levels.shift();
var bars = root.querySelectorAll('.wave i');
for (var i = 0; i < bars.length; i++) {
var v = levels[i - (BARS - levels.length)] || 0;
bars[i].style.height = (3 + v * 17).toFixed(1) + 'px';
}
}
setTimeout(tick, 50);
}
tick();
window.lilt.bar.onCommand(function (c) {
if (c.type === 'start') start(c);
else if (c.type === 'stop') stop(c);
else if (c.type === 'cancel') cancel();
else if (c.type === 'state') {
var was = view();
S = c;
if (!S.settings) S.settings = {};
if (view() !== was || was === 'hover' || was === 'idle') render();
}
});
render();
})();
</script>
</body>
</html>