Redesign PM-µ Micro as an inline practice bar

Reworks the Micro per the new brief: a long, narrow extruded-aluminium bar you
patch into your signal instead of a little box.
- Better display: amber 4-char 14-segment (Adafruit font) that shows BPM *and*
  short track names, replacing the 3-digit 7-segment. Off-segments kept very dim
  so the lit digits read clearly.
- Roller instead of knob: a recessed, clickable horizontal thumb-roller —
  roll = tempo, press = start/stop, hold + roll = switch track.
- New form/I-O: 1/4" TRS in on one end; USB-C + 1/4" TRS out on the other;
  USB-C or 2×AA power (battery gauge on the face). Click is summed into the
  signal in the analog domain (+ a small monitor speaker).
info-micro, concepts and the landing card updated to match; BOM reworked
(analog path + 2 jacks + 2×AA + 14-seg) → ≈ $38.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Me Here 2026-05-28 07:42:15 -05:00
parent 50c4e4da32
commit 76a94b629b
4 changed files with 188 additions and 111 deletions

View file

@ -93,8 +93,8 @@
<div class="card">
<span class="chip hw">Hardware</span>
<h3>PMµ — Micro</h3>
<p>Minimal homepractice unit: one push scrollencoder, a red 7segment LED, a speaker and USBC.
Spin = tempo · press = start/stop · hold + spin = switch track.</p>
<p>Long, narrow inline practice bar: instrument in one end, amp/headphones out the other, click mixed in.
Clickable thumbroller, amber 14segment display, USBC or 2×AA.</p>
<div class="links"><a href="/micro.html">Open ↗</a><a href="/info-micro.html">Info &amp; BOM ⓘ</a></div>
</div>

View file

@ -110,8 +110,8 @@
<div class="card">
<span class="chip hw">Hardware</span>
<h3>PMµ — Micro</h3>
<p>Minimal homepractice unit: one push scrollencoder, a red 7segment LED, a speaker and USBC.
Spin = tempo · press = start/stop · hold + spin = switch track.</p>
<p>Long, narrow inline practice bar: instrument in one end, amp/headphones out the other, click mixed in.
Clickable thumbroller, amber 14segment display, USBC or 2×AA.</p>
<div class="links"><a href="/micro.html">Open ↗</a><a href="/info-micro.html">Info &amp; BOM ⓘ</a></div>
</div>

View file

@ -48,42 +48,53 @@
<main>
<h1>PMµ — Micro</h1>
<div class="tags"><span class="tag hw">Hardware</span><span class="tag">Home practice</span><span class="tag">~$28 oneoff</span></div>
<p class="lead">The smallest possible polymeter unit for daily practice: one dial and a red LED, nothing to
learn. Spin for tempo, press to start/stop, hold &amp; spin to flip through grooves — that's it.</p>
<div class="tags"><span class="tag hw">Hardware</span><span class="tag">Inline practice bar</span><span class="tag">~$38 oneoff</span></div>
<p class="lead">A long, narrow practice bar you patch <i>into</i> your signal: instrument in one end, amp or
headphones out the other, the click mixed in. One clickable thumbroller does everything, an amber
14segment display shows tempo and track names, and it runs off USBC or 2×AA.</p>
<div class="embed-wrap">
<div data-varasys-metronome="micro"
data-patch="v1;t120;kick:4;snare:4=.X.X;hatClosed:4/2" data-height="300"></div>
data-patch="v1;t120;kick:4;snare:4=.X.X;hatClosed:4/2" data-height="240"></div>
</div>
<p class="cap">Live widget (embedded). <a href="/micro.html">Open the full Micro page ↗</a> · <a href="/embed.html">embed this</a></p>
<h2>Designed for</h2>
<p>The practice desk. No screen to read, no menus — a single depressable scrollencoder does everything
(spin = tempo, press = start/stop, hold + spin = switch track) and a bright 7segment LED shows the BPM
(or the track number while you switch). It runs off any USBC charger, plays through a small builtin
speaker, and ships with the editor's grooves built in. Synth voices only — no analog audio path.</p>
<p>Practising plugged in — at the desk or on the go. It sits inline in your signal chain: a 1/4″ TRS input on
one end, USBC and a 1/4″ TRS output on the other, with the metronome click summed into your signal in the
<b>analog domain</b> (and a small monitor speaker). No menus — a single clickable thumbroller does it all
(roll = tempo, press = start/stop, hold + roll = switch track), and the amber 14segment display shows the
BPM or the track name. Powered from USBC or 2×AA for portability; ships with the editor's grooves built in.</p>
<h2>Bill of materials</h2>
<p class="sub">Rough parts list — a USBCpowered RP2040 practice unit. Ballpark oneoff prices (USD); cheaper at volume.</p>
<p class="sub">Rough parts list — a portable RP2040 inline bar (USBC or 2×AA) with analog click injection.
Ballpark oneoff prices (USD); cheaper at volume.</p>
<table class="bom">
<thead><tr><th>Part</th><th class="q">Qty</th><th class="c">~$</th></tr></thead>
<tbody>
<tr class="grp"><td colspan="3">Brain &amp; display</td></tr>
<tr><td class="part">RP2040 board, USBC <span class="spec">— e.g. Waveshare RP2040Zero</span></td><td class="q">1</td><td class="c">4</td></tr>
<tr><td class="part">3digit 7segment LED (red) + driver <span class="spec">— MAX7219 / shift register, or direct GPIO</span></td><td class="q">1</td><td class="c">3</td></tr>
<tr><td class="part">4char 14segment alphanumeric LED + I²C driver <span class="spec">— amber; HT16K33. Shows BPM &amp; track names</span></td><td class="q">1</td><td class="c">4</td></tr>
<tr class="grp"><td colspan="3">Control</td></tr>
<tr><td class="part">Detented push encoder (EC11) + knob <span class="spec">— tempo / press / holdspin</span></td><td class="q">1</td><td class="c">2</td></tr>
<tr class="grp"><td colspan="3">Audio</td></tr>
<tr><td class="part">MAX98357A I²S amp + 8 Ω 2 W speaker <span class="spec">— synth click (no analog in)</span></td><td class="q">1</td><td class="c">4</td></tr>
<tr class="grp"><td colspan="3">Power &amp; build</td></tr>
<tr><td class="part">USBC bus power (on the board) + PWR LED</td><td class="q">1</td><td class="c">1</td></tr>
<tr><td class="part">Clickable thumbroller <span class="spec">— EC11 encoder + roller wheel · roll / press / holdroll</span></td><td class="q">1</td><td class="c">2</td></tr>
<tr class="grp"><td colspan="3">Audio — analog click injection</td></tr>
<tr><td class="part">PCM5102A I²S DAC <span class="spec">— linelevel click</span></td><td class="q">1</td><td class="c">3</td></tr>
<tr><td class="part">Dual opamp, NE5532 / OPA2134 <span class="spec">— hiZ instrument buffer + summing mixer</span></td><td class="q">1</td><td class="c">1</td></tr>
<tr><td class="part">PAM8302A mono ClassD + 8 Ω speaker <span class="spec">— monitor</span></td><td class="q">1</td><td class="c">4</td></tr>
<tr class="grp"><td colspan="3">Connectors &amp; power</td></tr>
<tr><td class="part">1/4″ jack <span class="spec">— Inst In (TS) · Out (TRS)</span></td><td class="q">2</td><td class="c">2</td></tr>
<tr><td class="part">USBC bus power (5 V) + PWR LED <span class="spec">— also carries config</span></td><td class="q">1</td><td class="c">1</td></tr>
<tr><td class="part">2×AA holder + boost/management + battery gauge <span class="spec">— AA → 3.3/5 V for portable use</span></td><td class="q">1</td><td class="c">3</td></tr>
<tr class="grp"><td colspan="3">Build</td></tr>
<tr><td class="part">Custom PCB (or perfboard)</td><td class="q">1</td><td class="c">4</td></tr>
<tr><td class="part">Passives, headers, wire</td><td class="q"></td><td class="c">2</td></tr>
<tr><td class="part">Small aluminium enclosure <span class="spec">— beadblasted, matteblack anodised</span></td><td class="q">1</td><td class="c">8</td></tr>
<tr class="total"><td>Total (oneoff)</td><td class="q"></td><td class="c">≈ $28</td></tr>
<tr><td class="part">Passives, headers, wire <span class="spec">— R/C for the analog stage + decoupling</span></td><td class="q"></td><td class="c">2</td></tr>
<tr><td class="part">Extruded aluminium bar enclosure + end caps <span class="spec">— beadblasted, matteblack anodised</span></td><td class="q">1</td><td class="c">8</td></tr>
<tr class="total"><td>Total (oneoff)</td><td class="q"></td><td class="c">≈ $38</td></tr>
</tbody>
</table>
<p class="sub" style="margin-top:12px">Like the Stage, the click is summed in the <b>analog domain</b>: a highimpedance
buffer of the 1/4″ instrument input is mixed with the DAC's click and sent to the 1/4″ output and the monitor
amp — your instrument is never redigitised (no added latency).</p>
</main>
<div class="site-foot">VARASYS · Simplifying Complexity — <span id="appVersion">v0.0.1-dev</span></div>

View file

@ -14,13 +14,14 @@
})();
</script>
<!--
"Micro" — a stripped-down home-practice metronome on the same RP2040 firmware.
Hardware is just: ONE depressable scroll/rotary encoder (tempo), a red 7-seg
LED BPM display, a small speaker, and a USB-C port for power. No screen, no
buttons. Interaction lives entirely in the encoder:
• spin → tempo
"Micro" — a long, narrow INLINE practice bar on the same RP2040 firmware.
Patch your instrument through it: 1/4" TRS in on one end; USB-C + 1/4" TRS out
on the other; powered from USB-C or 2×AA. The click is summed into your signal
in the ANALOG domain (and a small speaker). Display is a 4-char amber 14-segment
(shows BPM *and* short track names). One control — a clickable thumb-ROLLER:
• roll → tempo
• press (click) → start / stop
• hold + spin → switch track (the LED shows the track number)
• hold + roll → switch track (the display shows the track name)
Built-in tracks are the editor's seed grooves, flattened. Shares src/engine.js.
-->
<script>
@ -51,50 +52,71 @@
.tbtn{ background:transparent; color:var(--muted); border:1px solid var(--panel-bd); border-radius:8px; padding:3px 9px; font-size:14px; line-height:1; cursor:pointer }
.tbtn:hover{ color:var(--txt) }
/* ---- the micro device: small brushed-aluminium box ---- */
.device{ width:100%; max-width:330px; position:relative; border-radius:13px; padding:16px 16px 14px;
/* ---- the micro device: a long, narrow brushed-aluminium bar ---- */
.device{ width:100%; max-width:620px; display:flex; align-items:stretch; position:relative;
border-radius:18px; overflow:hidden; border:1px solid var(--device-bd);
background:
radial-gradient(rgba(255,255,255,.022) .5px, transparent .6px) 0 0/3px 3px, /* bead-blast micro-texture */
linear-gradient(160deg, #26282d, #15161a); /* matte anodised graphite */
border:1px solid var(--device-bd);
box-shadow:0 22px 46px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.05), inset 0 -2px 8px rgba(0,0,0,.5) }
.brandrow{ display:flex; align-items:center; justify-content:space-between; margin:0 2px 14px }
.brand-logo{ height:14px; width:auto; display:block }
linear-gradient(180deg, #2b2d33, #161719); /* matte anodised graphite */
box-shadow:0 24px 50px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.05), inset 0 -2px 8px rgba(0,0,0,.5) }
/* end caps — the extrusion ends, where the jacks exit */
.endcap{ flex:0 0 auto; width:76px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:11px;
padding:12px 8px; background:linear-gradient(180deg,#202227,#0d0e11) }
.endcap.left{ box-shadow:inset -7px 0 13px rgba(0,0,0,.55) }
.endcap.right{ box-shadow:inset 7px 0 13px rgba(0,0,0,.55) }
.endlbl{ font-size:7px; color:var(--silk); text-transform:uppercase; letter-spacing:.1em; text-align:center; line-height:1.35; opacity:.8 }
.jk{ display:flex; flex-direction:column; align-items:center; gap:4px }
.jk i{ width:23px; height:23px; border-radius:50%; background:radial-gradient(circle at 40% 34%, #333a44, #07090c 72%);
border:2px solid #5b6470; box-shadow:inset 0 0 5px #000 }
.jk.usb i{ width:25px; height:10px; border-radius:4px; border:2px solid #5b6470; background:#07090c }
.jk b{ font-size:7px; font-weight:700; color:var(--silk); letter-spacing:.05em; text-transform:uppercase; opacity:.9; text-align:center; line-height:1.25 }
/* the top face (between the end caps) */
.face{ flex:1; min-width:0; display:flex; flex-direction:column; padding:11px 16px; gap:8px }
.brandrow{ display:flex; align-items:center; justify-content:space-between; margin:0 }
.brand-logo{ height:13px; width:auto; display:block }
.silk{ display:flex; align-items:center; gap:7px; color:var(--silk) }
.silk .model{ font-size:9px; text-transform:uppercase; letter-spacing:.16em; opacity:.85 }
.pwr{ display:flex; align-items:center; gap:6px; font-size:8px; color:var(--silk); text-transform:uppercase; letter-spacing:.14em; opacity:.85 }
.pwr .dot{ width:7px; height:7px; border-radius:50%; background:#2fe07a; box-shadow:0 0 7px #2fe07a }
.silk .model{ font-size:8.5px; text-transform:uppercase; letter-spacing:.16em; opacity:.85 }
.meta{ display:flex; align-items:center; gap:12px }
.pwr{ display:flex; align-items:center; gap:5px; font-size:7.5px; color:var(--silk); text-transform:uppercase; letter-spacing:.12em; opacity:.85 }
.pwr .dot{ width:6px; height:6px; border-radius:50%; background:#2fe07a; box-shadow:0 0 6px #2fe07a }
/* battery gauge (2×AA) */
.batt{ display:flex; align-items:center; gap:5px; font-size:7.5px; color:var(--silk); text-transform:uppercase; letter-spacing:.1em; opacity:.85 }
.batt .cell{ width:21px; height:10px; border:1px solid var(--silk); border-radius:2px; position:relative }
.batt .cell::after{ content:""; position:absolute; right:-3px; top:2.5px; width:2px; height:5px; background:var(--silk); border-radius:0 1px 1px 0 }
.batt .cell::before{ content:""; position:absolute; left:1.5px; top:1.5px; bottom:1.5px; width:65%; background:#2fe07a; border-radius:1px }
/* ---- red 7-segment LED display ---- */
.led-win{ background:#160403; border:2px solid #050100; border-radius:8px; padding:8px 14px; margin:0 2px;
box-shadow:inset 0 0 16px rgba(0,0,0,.85), inset 0 0 7px rgba(255,40,30,.14), 0 1px 0 rgba(255,255,255,.3) }
#led{ display:block; width:100%; max-width:240px; height:84px; margin:0 auto }
.inds{ display:flex; justify-content:center; gap:16px; margin:9px 0 2px }
.ind{ display:flex; align-items:center; gap:5px; font-size:8.5px; color:var(--silk); letter-spacing:.12em; text-transform:uppercase; opacity:.85 }
.ind .d{ width:7px; height:7px; border-radius:50%; background:#4a0b09; box-shadow:inset 0 0 2px #000; transition:background .08s, box-shadow .08s }
.ind.on .d{ background:#ff3b30; box-shadow:0 0 7px #ff3b30 }
.ind.play.on .d{ background:#2fe07a; box-shadow:0 0 7px #2fe07a }
.facemain{ display:flex; align-items:center; gap:14px }
/* ---- amber 14-segment alphanumeric display ---- */
.led-win{ flex:1; min-width:0; background:#140a02; border:2px solid #050100; border-radius:8px; padding:6px 12px;
box-shadow:inset 0 0 16px rgba(0,0,0,.85), inset 0 0 8px rgba(255,150,30,.10), 0 1px 0 rgba(255,255,255,.25) }
#led{ display:block; width:100%; max-width:236px; height:58px; margin:0 auto }
/* ---- recessed clickable thumb-roller (tempo) ---- */
.rollwrap{ display:flex; flex-direction:column; align-items:center; gap:3px }
.roller{ width:92px; height:46px; border-radius:9px; position:relative; cursor:ew-resize; overflow:hidden; touch-action:none;
background:#0a0d12; border:1px solid #04060a; box-shadow:inset 0 0 0 1px rgba(255,255,255,.03), 0 2px 5px rgba(0,0,0,.5) }
.roller::before{ content:""; position:absolute; inset:4px 3px; border-radius:5px; /* ribbed cylinder, scrolls via --rib */
background:repeating-linear-gradient(90deg, rgba(255,255,255,.12) 0 1px, rgba(0,0,0,.5) 1px 5px); background-position:var(--rib,0px) 0 }
.roller::after{ content:""; position:absolute; inset:0; border-radius:9px; pointer-events:none; /* cylinder sheen: bright centre, dark edges */
background:linear-gradient(90deg, rgba(0,0,0,.72) 0%, rgba(255,255,255,.13) 49%, rgba(255,255,255,.13) 51%, rgba(0,0,0,.72) 100%) }
.roller.press{ filter:brightness(.9); box-shadow:inset 0 0 0 1px rgba(255,255,255,.03), 0 1px 2px rgba(0,0,0,.6) }
.roll-cap{ font-size:7px; color:var(--silk); letter-spacing:.16em; text-transform:uppercase; opacity:.8 }
/* ---- single push encoder ---- */
.knob-wrap{ display:flex; justify-content:center; margin:16px 0 6px }
.knob{ width:96px; height:96px; border-radius:50%; cursor:pointer; position:relative; touch-action:none;
background:repeating-conic-gradient(from 0deg, #3c444f 0 6deg, #2a313b 6deg 12deg);
border:2px solid #565f6c; box-shadow:0 6px 14px rgba(0,0,0,.5), inset 0 1px 2px rgba(255,255,255,.12) }
.knob::before{ content:""; position:absolute; inset:13px; border-radius:50%;
background:radial-gradient(circle at 38% 32%, #4a525e, #1b212a 76%); box-shadow:inset 0 1px 2px rgba(255,255,255,.1), inset 0 -2px 4px rgba(0,0,0,.5) }
.knob::after{ content:""; position:absolute; left:50%; top:9px; width:4px; height:17px; background:#ff3b30; border-radius:2px;
transform:translateX(-50%) rotate(var(--a,0deg)); transform-origin:50% 39px; box-shadow:0 0 6px #ff3b30 }
.knob.press{ box-shadow:0 2px 6px rgba(0,0,0,.5), inset 0 1px 2px rgba(255,255,255,.1); transform:translateY(2px) }
/* speaker grille + status indicators along the bottom of the face */
.facebot{ display:flex; align-items:center; gap:12px }
.grille{ flex:1; height:9px; border-radius:5px; background:radial-gradient(circle, #000 1px, transparent 1.3px) 0 0/7px 7px; opacity:.45 }
.inds{ display:flex; gap:11px }
.ind{ display:flex; align-items:center; gap:4px; font-size:7.5px; color:var(--silk); letter-spacing:.1em; text-transform:uppercase; opacity:.85 }
.ind .d{ width:6px; height:6px; border-radius:50%; background:#3a2306; box-shadow:inset 0 0 2px #000; transition:background .08s, box-shadow .08s }
.ind.on .d{ background:#ff8a1e; box-shadow:0 0 6px #ff8a1e }
.ind.play.on .d{ background:#2fe07a; box-shadow:0 0 6px #2fe07a }
/* ---- speaker grille + USB-C ---- */
.grille{ height:11px; margin:16px 8px 9px; border-radius:5px;
background:radial-gradient(circle, #000 1.1px, transparent 1.4px) 0 0/8px 8px; opacity:.5 }
.usb{ display:flex; align-items:center; justify-content:center; gap:6px; font-size:8px; color:var(--silk); letter-spacing:.12em; text-transform:uppercase; opacity:.8 }
.usb .port{ width:24px; height:9px; border-radius:4px; background:#0a0c0f; border:2px solid #5b6470; box-shadow:inset 0 0 2px #000 }
.hint{ max-width:330px; text-align:center; font-size:11px; color:var(--muted); line-height:1.5 }
.hint{ max-width:560px; text-align:center; font-size:11px; color:var(--muted); line-height:1.5 }
/* embed mode: just the device */
[data-embed] .hint { display:none !important; }
/* stack the bar's end caps under the face on very narrow screens */
@media (max-width:430px){ .device{ flex-wrap:wrap } .endcap{ width:50%; flex-direction:row; gap:18px; justify-content:center } .face{ flex-basis:100%; order:-1 } }
</style>
</head>
<body>
@ -105,7 +127,7 @@
<img class="brand-logo brand-dark" src="data:image/png;base64,@BUILD:logo-dark@" alt="VARASYS — Simplifying Complexity" />
<img class="brand-logo brand-light" src="data:image/png;base64,@BUILD:logo-light@" alt="VARASYS — Simplifying Complexity" />
</a>
<span class="page-name"><b>PMµ</b> · Micro (home practice)</span>
<span class="page-name"><b>PMµ</b> · Micro (inline practice bar)</span>
</div>
<nav class="site-nav">
<a href="/editor.html">Editor</a>
@ -117,25 +139,49 @@
</header>
<div class="device">
<!-- LEFT END: instrument / aux in -->
<div class="endcap left">
<div class="jk" title="1/4&quot; TRS input — plug your instrument (or an aux source) in; the click is mixed into it"><i></i><b>TRS&nbsp;In</b></div>
<div class="endlbl">Inst /<br>aux in</div>
</div>
<!-- TOP FACE: display + roller + speaker -->
<div class="face">
<div class="brandrow">
<div class="silk"><img class="brand-logo" src="data:image/png;base64,@BUILD:logo-dark@" alt="VARASYS" /><span class="model">PMµ Micro</span></div>
<div class="meta">
<div class="batt" title="2×AA, or run from USBC"><span class="cell"></span>2×AA</div>
<div class="pwr"><span class="dot"></span>PWR</div>
</div>
</div>
<div class="led-win"><canvas id="led" width="240" height="84" aria-label="LED tempo display"></canvas></div>
<div class="facemain">
<div class="led-win"><canvas id="led" width="236" height="58" aria-label="14-segment tempo / track display"></canvas></div>
<div class="rollwrap">
<div class="roller" id="enc" title="Roll = tempo · press = start/stop · hold + roll = switch track"></div>
<div class="roll-cap">Tempo</div>
</div>
</div>
<div class="facebot">
<div class="grille" title="monitor speaker"></div>
<div class="inds">
<div class="ind on" id="indBpm"><span class="d"></span>BPM</div>
<div class="ind" id="indTrk"><span class="d"></span>Track</div>
<div class="ind" id="indTrk"><span class="d"></span>Trk</div>
<div class="ind play" id="indPlay"><span class="d"></span></div>
</div>
<div class="knob-wrap"><div class="knob" id="enc" title="Spin = tempo · Press = start/stop · Hold + spin = switch track"></div></div>
<div class="grille"></div>
<div class="usb"><span class="port"></span>USBC (power)</div>
</div>
</div>
<div class="hint">Spin the dial = <b>tempo</b> · press = <b>start / stop</b> · hold &amp; spin = <b>switch track</b></div>
<!-- RIGHT END: power + output -->
<div class="endcap right">
<div class="jk usb" title="USBC — power (5 V) &amp; set-list transfer"><i></i><b>USBC</b></div>
<div class="jk" title="1/4&quot; TRS output — instrument + click, to your amp, headphones or the desk"><i></i><b>TRS&nbsp;Out</b></div>
</div>
</div>
<div class="hint">Roll = <b>tempo</b> · press = <b>start / stop</b> · hold &amp; roll = <b>switch track</b>.
Instrument in one end, amp/headphones out the other — the click is mixed into your signal in the analog domain.</div>
<script>
const APP_VERSION = "v0.0.1-dev";
@ -187,32 +233,47 @@ function loadTrack(i){
if(was) startAudio(); else render();
}
/* ========================= 7-SEGMENT LED ===================================== */
const led=$("led"), lc=led.getContext("2d"), LW=240, LH=84;
/* ========================= 14-SEGMENT DISPLAY ================================ */
const led=$("led"), lc=led.getContext("2d"), NCH=4, LW=236, LH=58;
(function(){ const dpr=Math.max(1,Math.min(3,window.devicePixelRatio||1)); led.width=LW*dpr; led.height=LH*dpr; lc.scale(dpr,dpr); })();
const LED_ON="#ff3b30", LED_OFF="#330807", LED_BG="#160403";
const SEG7={ // a,b,c,d,e,f,g
"0":[1,1,1,1,1,1,0],"1":[0,1,1,0,0,0,0],"2":[1,1,0,1,1,0,1],"3":[1,1,1,1,0,0,1],"4":[0,1,1,0,0,1,1],
"5":[1,0,1,1,0,1,1],"6":[1,0,1,1,1,1,1],"7":[1,1,1,0,0,0,0],"8":[1,1,1,1,1,1,1],"9":[1,1,1,1,0,1,1],
" ":[0,0,0,0,0,0,0],"-":[0,0,0,0,0,0,1],"P":[1,1,0,0,1,1,1] };
const LED_ON="#ff8a1e", LED_OFF="#1d1004", LED_BG="#120802"; // OFF kept very dim so the lit digits read clearly
// 14-seg font (Adafruit bit order): 0=A 1=B 2=C 3=D 4=E 5=F 6=G1 7=G2 8=H 9=I 10=J 11=K 12=L 13=M
const SEG14={ " ":0x0000,"-":0x00C0,
"0":0x0C3F,"1":0x0006,"2":0x00DB,"3":0x008F,"4":0x00E6,"5":0x2069,"6":0x00FD,"7":0x0007,"8":0x00FF,"9":0x00EF,
"A":0x00F7,"B":0x128F,"C":0x0039,"D":0x120F,"E":0x00F9,"F":0x0071,"G":0x00BD,"H":0x00F6,"I":0x1209,"J":0x001E,
"K":0x2470,"L":0x0038,"M":0x0536,"N":0x2136,"O":0x003F,"P":0x00F3,"Q":0x203F,"R":0x20F3,"S":0x00ED,"T":0x1201,
"U":0x003E,"V":0x0C30,"W":0x2836,"X":0x2D00,"Y":0x1500,"Z":0x0C09 };
let displayMode="bpm";
function ledText(){ return displayMode==="track" ? String(previewIdx+1).padStart(3," ") : String(state.bpm).padStart(3," "); }
function drawDigit(dx,dy,dw,dh,ch){
const segs=SEG7[ch]||SEG7[" "], t=Math.max(3,Math.round(dw*0.17)), vh=(dh-3*t)/2;
const put=(on,x,y,w,h)=>{ if(on){ lc.fillStyle=LED_ON; lc.shadowColor=LED_ON; lc.shadowBlur=7; } else { lc.fillStyle=LED_OFF; lc.shadowBlur=0; }
lc.fillRect(x,y,w,h); lc.shadowBlur=0; };
put(segs[0], dx+t, dy, dw-2*t, t); // a
put(segs[5], dx, dy+t, t, vh); // f
put(segs[1], dx+dw-t, dy+t, t, vh); // b
put(segs[6], dx+t, dy+t+vh, dw-2*t, t); // g
put(segs[4], dx, dy+2*t+vh, t, vh); // e
put(segs[2], dx+dw-t, dy+2*t+vh, t, vh); // c
put(segs[3], dx+t, dy+dh-t, dw-2*t, t); // d
function trackName(i){ const raw=(tracks[i]&&tracks[i].name)||("TR"+(i+1));
return (raw.replace(/[^A-Za-z0-9]/g,"").toUpperCase().slice(0,NCH)) || ("T"+(i+1)); }
function ledText(){ return (displayMode==="track" ? trackName(previewIdx) : String(state.bpm)).padStart(NCH," "); }
function drawChar(dx,dy,w,h,ch){
const m=SEG14[ch]!=null?SEG14[ch]:0, t=Math.max(2.5,w*0.13), g=Math.max(1.5,t*0.5),
cx=dx+w/2, midY=dy+h/2, vH=h/2-t-g;
const bar=(b,x,y,ww,hh)=>{ if((m>>b)&1){ lc.fillStyle=LED_ON; lc.shadowColor=LED_ON; lc.shadowBlur=6; } else { lc.fillStyle=LED_OFF; lc.shadowBlur=0; }
lc.fillRect(x,y,ww,hh); lc.shadowBlur=0; };
const diag=(b,x1,y1,x2,y2)=>{ lc.lineCap="round"; lc.lineWidth=t*0.82;
if((m>>b)&1){ lc.strokeStyle=LED_ON; lc.shadowColor=LED_ON; lc.shadowBlur=6; } else { lc.strokeStyle=LED_OFF; lc.shadowBlur=0; }
lc.beginPath(); lc.moveTo(x1,y1); lc.lineTo(x2,y2); lc.stroke(); lc.shadowBlur=0; };
bar(0, dx+t, dy, w-2*t, t); // A top
bar(5, dx, dy+t, t, vH); // F upper-left
bar(1, dx+w-t, dy+t, t, vH); // B upper-right
bar(9, cx-t/2, dy+t, t, vH); // I centre-upper
bar(6, dx+t, midY-t/2, w/2-t-g, t); // G1 mid-left
bar(7, cx+g, midY-t/2, w/2-t-g, t); // G2 mid-right
bar(4, dx, midY+g, t, vH); // E lower-left
bar(2, dx+w-t, midY+g, t, vH); // C lower-right
bar(12, cx-t/2, midY+g, t, vH); // L centre-lower
bar(3, dx+t, dy+h-t, w-2*t, t); // D bottom
diag(8, dx+t+1, dy+t+1, cx-t*0.6, midY-t*0.6); // H top-left
diag(10, dx+w-t-1, dy+t+1, cx+t*0.6, midY-t*0.6); // J top-right
diag(11, dx+t+1, dy+h-t-1, cx-t*0.6, midY+t*0.6); // K bottom-left
diag(13, dx+w-t-1, dy+h-t-1, cx+t*0.6, midY+t*0.6); // M bottom-right
}
function drawLED(){
lc.fillStyle=LED_BG; lc.fillRect(0,0,LW,LH);
const txt=ledText(), pad=12, gap=12, n=3, dw=(LW-2*pad-(n-1)*gap)/n, dh=LH-2*pad;
for(let i=0;i<n;i++) drawDigit(pad+i*(dw+gap), pad, dw, dh, txt[i]||" ");
const txt=ledText(), pad=10, gap=9, n=NCH, dw=(LW-2*pad-(n-1)*gap)/n, dh=LH-2*pad;
for(let i=0;i<n;i++) drawChar(pad+i*(dw+gap), pad, dw, dh, txt[i]||" ");
}
function render(){
drawLED();
@ -221,22 +282,27 @@ function render(){
$("indPlay").classList.toggle("on", state.running);
}
/* ========================= ENCODER (the only control) ======================== */
let knobAngle=0;
function nudge(d){ setBpm(state.bpm+d); displayMode="bpm"; knobAngle+=d*10; $("enc").style.setProperty("--a",knobAngle+"deg"); render(); }
/* ========================= ROLLER (the only control) ========================= */
let rollPos=0;
function nudge(d){ setBpm(state.bpm+d); displayMode="bpm"; rollPos+=d*5; $("enc").style.setProperty("--rib", rollPos+"px"); render(); }
let revertT=null;
function previewTrack(d){ previewIdx=((previewIdx+d)%tracks.length+tracks.length)%tracks.length; displayMode="track"; render(); }
function commitTrack(){ loadTrack(previewIdx); displayMode="track"; render(); clearTimeout(revertT); revertT=setTimeout(()=>{ displayMode="bpm"; render(); }, 1000); }
function commitTrack(){ loadTrack(previewIdx); displayMode="track"; render(); clearTimeout(revertT); revertT=setTimeout(()=>{ displayMode="bpm"; render(); }, 1100); }
/* roll = tempo · quick press = start/stop · hold (~350ms) then roll = switch track */
(function(){
const k=$("enc"); let down=false, moved=false, lastY=0, acc=0;
const k=$("enc"); let down=false, moved=false, held=false, lastX=0, acc=0, holdT=null;
k.addEventListener("wheel",(e)=>{ e.preventDefault(); nudge(e.deltaY<0 ? (e.shiftKey?5:1) : (e.shiftKey?-5:-1)); }, {passive:false});
k.addEventListener("pointerdown",(e)=>{ down=true; moved=false; lastY=e.clientY; acc=0; previewIdx=trackIdx; k.classList.add("press"); k.setPointerCapture(e.pointerId); });
k.addEventListener("pointermove",(e)=>{ if(!down) return; acc += lastY - e.clientY; lastY=e.clientY;
while(Math.abs(acc) >= 9){ const d = acc>0?1:-1; acc -= d*9; moved=true; previewTrack(d); } });
k.addEventListener("pointerup",()=>{ if(!down) return; down=false; k.classList.remove("press");
if(moved) commitTrack(); else toggle(); }); // hold+spin → track ; quick press → start/stop
k.addEventListener("pointercancel",()=>{ down=false; k.classList.remove("press"); });
k.addEventListener("pointerdown",(e)=>{ down=true; moved=false; held=false; lastX=e.clientX; acc=0; previewIdx=trackIdx;
k.classList.add("press"); k.setPointerCapture(e.pointerId);
holdT=setTimeout(()=>{ if(down && !moved){ held=true; displayMode="track"; render(); } }, 350); });
k.addEventListener("pointermove",(e)=>{ if(!down) return; acc += e.clientX - lastX; lastX=e.clientX;
if(held){ while(Math.abs(acc)>=12){ const d=acc>0?1:-1; acc-=d*12; moved=true; previewTrack(d); } } // hold + roll → track
else { while(Math.abs(acc)>=6){ const d=acc>0?1:-1; acc-=d*6; moved=true; clearTimeout(holdT); nudge(d); } } }); // roll → tempo
k.addEventListener("pointerup",()=>{ if(!down) return; down=false; clearTimeout(holdT); k.classList.remove("press");
if(held){ if(moved) commitTrack(); else { displayMode="bpm"; render(); } }
else if(!moved){ toggle(); } }); // quick press → start/stop
k.addEventListener("pointercancel",()=>{ down=false; clearTimeout(holdT); k.classList.remove("press"); });
})();
/* theme toggle — cycles system → light → dark; shares the "metronome.theme" key */