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

View file

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

View file

@ -48,42 +48,53 @@
<main> <main>
<h1>PMµ — Micro</h1> <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> <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">The smallest possible polymeter unit for daily practice: one dial and a red LED, nothing to <p class="lead">A long, narrow practice bar you patch <i>into</i> your signal: instrument in one end, amp or
learn. Spin for tempo, press to start/stop, hold &amp; spin to flip through grooves — that's it.</p> 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 class="embed-wrap">
<div data-varasys-metronome="micro" <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> </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> <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> <h2>Designed for</h2>
<p>The practice desk. No screen to read, no menus — a single depressable scrollencoder does everything <p>Practising plugged in — at the desk or on the go. It sits inline in your signal chain: a 1/4″ TRS input on
(spin = tempo, press = start/stop, hold + spin = switch track) and a bright 7segment LED shows the BPM one end, USBC and a 1/4″ TRS output on the other, with the metronome click summed into your signal in the
(or the track number while you switch). It runs off any USBC charger, plays through a small builtin <b>analog domain</b> (and a small monitor speaker). No menus — a single clickable thumbroller does it all
speaker, and ships with the editor's grooves built in. Synth voices only — no analog audio path.</p> (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> <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"> <table class="bom">
<thead><tr><th>Part</th><th class="q">Qty</th><th class="c">~$</th></tr></thead> <thead><tr><th>Part</th><th class="q">Qty</th><th class="c">~$</th></tr></thead>
<tbody> <tbody>
<tr class="grp"><td colspan="3">Brain &amp; display</td></tr> <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">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 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><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</td></tr> <tr class="grp"><td colspan="3">Audio — analog click injection</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><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 class="grp"><td colspan="3">Power &amp; build</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">USBC bus power (on the board) + PWR LED</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">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">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">Small aluminium enclosure <span class="spec">— beadblasted, matteblack anodised</span></td><td class="q">1</td><td class="c">8</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">≈ $28</td></tr> <tr class="total"><td>Total (oneoff)</td><td class="q"></td><td class="c">≈ $38</td></tr>
</tbody> </tbody>
</table> </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> </main>
<div class="site-foot">VARASYS · Simplifying Complexity — <span id="appVersion">v0.0.1-dev</span></div> <div class="site-foot">VARASYS · Simplifying Complexity — <span id="appVersion">v0.0.1-dev</span></div>

View file

@ -14,13 +14,14 @@
})(); })();
</script> </script>
<!-- <!--
"Micro" — a stripped-down home-practice metronome on the same RP2040 firmware. "Micro" — a long, narrow INLINE practice bar on the same RP2040 firmware.
Hardware is just: ONE depressable scroll/rotary encoder (tempo), a red 7-seg Patch your instrument through it: 1/4" TRS in on one end; USB-C + 1/4" TRS out
LED BPM display, a small speaker, and a USB-C port for power. No screen, no on the other; powered from USB-C or 2×AA. The click is summed into your signal
buttons. Interaction lives entirely in the encoder: in the ANALOG domain (and a small speaker). Display is a 4-char amber 14-segment
• spin → tempo (shows BPM *and* short track names). One control — a clickable thumb-ROLLER:
• roll → tempo
• press (click) → start / stop • 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. Built-in tracks are the editor's seed grooves, flattened. Shares src/engine.js.
--> -->
<script> <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{ 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) } .tbtn:hover{ color:var(--txt) }
/* ---- the micro device: small brushed-aluminium box ---- */ /* ---- the micro device: a long, narrow brushed-aluminium bar ---- */
.device{ width:100%; max-width:330px; position:relative; border-radius:13px; padding:16px 16px 14px; .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: background:
radial-gradient(rgba(255,255,255,.022) .5px, transparent .6px) 0 0/3px 3px, /* bead-blast micro-texture */ 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 */ linear-gradient(180deg, #2b2d33, #161719); /* matte anodised graphite */
border:1px solid var(--device-bd); 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) }
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 } /* end caps — the extrusion ends, where the jacks exit */
.brand-logo{ height:14px; width:auto; display:block } .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{ display:flex; align-items:center; gap:7px; color:var(--silk) }
.silk .model{ font-size:9px; text-transform:uppercase; letter-spacing:.16em; opacity:.85 } .silk .model{ font-size:8.5px; 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 } .meta{ display:flex; align-items:center; gap:12px }
.pwr .dot{ width:7px; height:7px; border-radius:50%; background:#2fe07a; box-shadow:0 0 7px #2fe07a } .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 ---- */ .facemain{ display:flex; align-items:center; gap:14px }
.led-win{ background:#160403; border:2px solid #050100; border-radius:8px; padding:8px 14px; margin:0 2px; /* ---- amber 14-segment alphanumeric display ---- */
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-win{ flex:1; min-width:0; background:#140a02; border:2px solid #050100; border-radius:8px; padding:6px 12px;
#led{ display:block; width:100%; max-width:240px; height:84px; margin:0 auto } 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) }
.inds{ display:flex; justify-content:center; gap:16px; margin:9px 0 2px } #led{ display:block; width:100%; max-width:236px; height:58px; margin:0 auto }
.ind{ display:flex; align-items:center; gap:5px; font-size:8.5px; color:var(--silk); letter-spacing:.12em; text-transform:uppercase; opacity:.85 } /* ---- recessed clickable thumb-roller (tempo) ---- */
.ind .d{ width:7px; height:7px; border-radius:50%; background:#4a0b09; box-shadow:inset 0 0 2px #000; transition:background .08s, box-shadow .08s } .rollwrap{ display:flex; flex-direction:column; align-items:center; gap:3px }
.ind.on .d{ background:#ff3b30; box-shadow:0 0 7px #ff3b30 } .roller{ width:92px; height:46px; border-radius:9px; position:relative; cursor:ew-resize; overflow:hidden; touch-action:none;
.ind.play.on .d{ background:#2fe07a; box-shadow:0 0 7px #2fe07a } 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 ---- */ /* speaker grille + status indicators along the bottom of the face */
.knob-wrap{ display:flex; justify-content:center; margin:16px 0 6px } .facebot{ display:flex; align-items:center; gap:12px }
.knob{ width:96px; height:96px; border-radius:50%; cursor:pointer; position:relative; touch-action:none; .grille{ flex:1; height:9px; border-radius:5px; background:radial-gradient(circle, #000 1px, transparent 1.3px) 0 0/7px 7px; opacity:.45 }
background:repeating-conic-gradient(from 0deg, #3c444f 0 6deg, #2a313b 6deg 12deg); .inds{ display:flex; gap:11px }
border:2px solid #565f6c; box-shadow:0 6px 14px rgba(0,0,0,.5), inset 0 1px 2px rgba(255,255,255,.12) } .ind{ display:flex; align-items:center; gap:4px; font-size:7.5px; color:var(--silk); letter-spacing:.1em; text-transform:uppercase; opacity:.85 }
.knob::before{ content:""; position:absolute; inset:13px; border-radius:50%; .ind .d{ width:6px; height:6px; border-radius:50%; background:#3a2306; box-shadow:inset 0 0 2px #000; transition:background .08s, box-shadow .08s }
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) } .ind.on .d{ background:#ff8a1e; box-shadow:0 0 6px #ff8a1e }
.knob::after{ content:""; position:absolute; left:50%; top:9px; width:4px; height:17px; background:#ff3b30; border-radius:2px; .ind.play.on .d{ background:#2fe07a; box-shadow:0 0 6px #2fe07a }
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 + USB-C ---- */ .hint{ max-width:560px; text-align:center; font-size:11px; color:var(--muted); line-height:1.5 }
.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 }
/* embed mode: just the device */ /* embed mode: just the device */
[data-embed] .hint { display:none !important; } [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> </style>
</head> </head>
<body> <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-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" /> <img class="brand-logo brand-light" src="data:image/png;base64,@BUILD:logo-light@" alt="VARASYS — Simplifying Complexity" />
</a> </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> </div>
<nav class="site-nav"> <nav class="site-nav">
<a href="/editor.html">Editor</a> <a href="/editor.html">Editor</a>
@ -117,25 +139,49 @@
</header> </header>
<div class="device"> <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="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="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 class="pwr"><span class="dot"></span>PWR</div>
</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="inds">
<div class="ind on" id="indBpm"><span class="d"></span>BPM</div> <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 class="ind play" id="indPlay"><span class="d"></span></div>
</div> </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> <script>
const APP_VERSION = "v0.0.1-dev"; const APP_VERSION = "v0.0.1-dev";
@ -187,32 +233,47 @@ function loadTrack(i){
if(was) startAudio(); else render(); if(was) startAudio(); else render();
} }
/* ========================= 7-SEGMENT LED ===================================== */ /* ========================= 14-SEGMENT DISPLAY ================================ */
const led=$("led"), lc=led.getContext("2d"), LW=240, LH=84; 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); })(); (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 LED_ON="#ff8a1e", LED_OFF="#1d1004", LED_BG="#120802"; // OFF kept very dim so the lit digits read clearly
const SEG7={ // a,b,c,d,e,f,g // 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
"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], const SEG14={ " ":0x0000,"-":0x00C0,
"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":0x0C3F,"1":0x0006,"2":0x00DB,"3":0x008F,"4":0x00E6,"5":0x2069,"6":0x00FD,"7":0x0007,"8":0x00FF,"9":0x00EF,
" ":[0,0,0,0,0,0,0],"-":[0,0,0,0,0,0,1],"P":[1,1,0,0,1,1,1] }; "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"; let displayMode="bpm";
function ledText(){ return displayMode==="track" ? String(previewIdx+1).padStart(3," ") : String(state.bpm).padStart(3," "); } function trackName(i){ const raw=(tracks[i]&&tracks[i].name)||("TR"+(i+1));
function drawDigit(dx,dy,dw,dh,ch){ return (raw.replace(/[^A-Za-z0-9]/g,"").toUpperCase().slice(0,NCH)) || ("T"+(i+1)); }
const segs=SEG7[ch]||SEG7[" "], t=Math.max(3,Math.round(dw*0.17)), vh=(dh-3*t)/2; function ledText(){ return (displayMode==="track" ? trackName(previewIdx) : String(state.bpm)).padStart(NCH," "); }
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; } function drawChar(dx,dy,w,h,ch){
lc.fillRect(x,y,w,h); lc.shadowBlur=0; }; const m=SEG14[ch]!=null?SEG14[ch]:0, t=Math.max(2.5,w*0.13), g=Math.max(1.5,t*0.5),
put(segs[0], dx+t, dy, dw-2*t, t); // a cx=dx+w/2, midY=dy+h/2, vH=h/2-t-g;
put(segs[5], dx, dy+t, t, vh); // f 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; }
put(segs[1], dx+dw-t, dy+t, t, vh); // b lc.fillRect(x,y,ww,hh); lc.shadowBlur=0; };
put(segs[6], dx+t, dy+t+vh, dw-2*t, t); // g const diag=(b,x1,y1,x2,y2)=>{ lc.lineCap="round"; lc.lineWidth=t*0.82;
put(segs[4], dx, dy+2*t+vh, t, vh); // e if((m>>b)&1){ lc.strokeStyle=LED_ON; lc.shadowColor=LED_ON; lc.shadowBlur=6; } else { lc.strokeStyle=LED_OFF; lc.shadowBlur=0; }
put(segs[2], dx+dw-t, dy+2*t+vh, t, vh); // c lc.beginPath(); lc.moveTo(x1,y1); lc.lineTo(x2,y2); lc.stroke(); lc.shadowBlur=0; };
put(segs[3], dx+t, dy+dh-t, dw-2*t, t); // d 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(){ function drawLED(){
lc.fillStyle=LED_BG; lc.fillRect(0,0,LW,LH); 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; 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++) drawDigit(pad+i*(dw+gap), pad, dw, dh, txt[i]||" "); for(let i=0;i<n;i++) drawChar(pad+i*(dw+gap), pad, dw, dh, txt[i]||" ");
} }
function render(){ function render(){
drawLED(); drawLED();
@ -221,22 +282,27 @@ function render(){
$("indPlay").classList.toggle("on", state.running); $("indPlay").classList.toggle("on", state.running);
} }
/* ========================= ENCODER (the only control) ======================== */ /* ========================= ROLLER (the only control) ========================= */
let knobAngle=0; let rollPos=0;
function nudge(d){ setBpm(state.bpm+d); displayMode="bpm"; knobAngle+=d*10; $("enc").style.setProperty("--a",knobAngle+"deg"); render(); } function nudge(d){ setBpm(state.bpm+d); displayMode="bpm"; rollPos+=d*5; $("enc").style.setProperty("--rib", rollPos+"px"); render(); }
let revertT=null; let revertT=null;
function previewTrack(d){ previewIdx=((previewIdx+d)%tracks.length+tracks.length)%tracks.length; displayMode="track"; render(); } 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(){ (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("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("pointerdown",(e)=>{ down=true; moved=false; held=false; lastX=e.clientX; acc=0; previewIdx=trackIdx;
k.addEventListener("pointermove",(e)=>{ if(!down) return; acc += lastY - e.clientY; lastY=e.clientY; k.classList.add("press"); k.setPointerCapture(e.pointerId);
while(Math.abs(acc) >= 9){ const d = acc>0?1:-1; acc -= d*9; moved=true; previewTrack(d); } }); holdT=setTimeout(()=>{ if(down && !moved){ held=true; displayMode="track"; render(); } }, 350); });
k.addEventListener("pointerup",()=>{ if(!down) return; down=false; k.classList.remove("press"); k.addEventListener("pointermove",(e)=>{ if(!down) return; acc += e.clientX - lastX; lastX=e.clientX;
if(moved) commitTrack(); else toggle(); }); // hold+spin → track ; quick press → start/stop if(held){ while(Math.abs(acc)>=12){ const d=acc>0?1:-1; acc-=d*12; moved=true; previewTrack(d); } } // hold + roll → track
k.addEventListener("pointercancel",()=>{ down=false; k.classList.remove("press"); }); 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 */ /* theme toggle — cycles system → light → dark; shares the "metronome.theme" key */