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:
parent
50c4e4da32
commit
76a94b629b
4 changed files with 188 additions and 111 deletions
|
|
@ -93,8 +93,8 @@
|
|||
<div class="card">
|
||||
<span class="chip hw">Hardware</span>
|
||||
<h3>PM‑µ — Micro</h3>
|
||||
<p>Minimal home‑practice unit: one push scroll‑encoder, a red 7‑segment LED, a speaker and USB‑C.
|
||||
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 thumb‑roller, amber 14‑segment display, USB‑C or 2×AA.</p>
|
||||
<div class="links"><a href="/micro.html">Open ↗</a><a href="/info-micro.html">Info & BOM ⓘ</a></div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -110,8 +110,8 @@
|
|||
<div class="card">
|
||||
<span class="chip hw">Hardware</span>
|
||||
<h3>PM‑µ — Micro</h3>
|
||||
<p>Minimal home‑practice unit: one push scroll‑encoder, a red 7‑segment LED, a speaker and USB‑C.
|
||||
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 thumb‑roller, amber 14‑segment display, USB‑C or 2×AA.</p>
|
||||
<div class="links"><a href="/micro.html">Open ↗</a><a href="/info-micro.html">Info & BOM ⓘ</a></div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 one‑off</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 & 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 one‑off</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 thumb‑roller does everything, an amber
|
||||
14‑segment display shows tempo and track names, and it runs off USB‑C 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 scroll‑encoder does everything
|
||||
(spin = tempo, press = start/stop, hold + spin = switch track) and a bright 7‑segment LED shows the BPM
|
||||
(or the track number while you switch). It runs off any USB‑C charger, plays through a small built‑in
|
||||
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, USB‑C 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 thumb‑roller does it all
|
||||
(roll = tempo, press = start/stop, hold + roll = switch track), and the amber 14‑segment display shows the
|
||||
BPM or the track name. Powered from USB‑C 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 USB‑C‑powered RP2040 practice unit. Ballpark one‑off prices (USD); cheaper at volume.</p>
|
||||
<p class="sub">Rough parts list — a portable RP2040 inline bar (USB‑C or 2×AA) with analog click injection.
|
||||
Ballpark one‑off 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 & display</td></tr>
|
||||
<tr><td class="part">RP2040 board, USB‑C <span class="spec">— e.g. Waveshare RP2040‑Zero</span></td><td class="q">1</td><td class="c">4</td></tr>
|
||||
<tr><td class="part">3‑digit 7‑segment 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">4‑char 14‑segment alphanumeric LED + I²C driver <span class="spec">— amber; HT16K33. Shows BPM & 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 / hold‑spin</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 & build</td></tr>
|
||||
<tr><td class="part">USB‑C bus power (on the board) + PWR LED</td><td class="q">1</td><td class="c">1</td></tr>
|
||||
<tr><td class="part">Clickable thumb‑roller <span class="spec">— EC11 encoder + roller wheel · roll / press / hold‑roll</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">— line‑level click</span></td><td class="q">1</td><td class="c">3</td></tr>
|
||||
<tr><td class="part">Dual op‑amp, NE5532 / OPA2134 <span class="spec">— hi‑Z instrument buffer + summing mixer</span></td><td class="q">1</td><td class="c">1</td></tr>
|
||||
<tr><td class="part">PAM8302A mono Class‑D + 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 & 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">USB‑C 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">— bead‑blasted, matte‑black anodised</span></td><td class="q">1</td><td class="c">8</td></tr>
|
||||
<tr class="total"><td>Total (one‑off)</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">— bead‑blasted, matte‑black anodised</span></td><td class="q">1</td><td class="c">8</td></tr>
|
||||
<tr class="total"><td>Total (one‑off)</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 high‑impedance
|
||||
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 re‑digitised (no added latency).</p>
|
||||
</main>
|
||||
|
||||
<div class="site-foot">VARASYS · Simplifying Complexity — <span id="appVersion">v0.0.1-dev</span></div>
|
||||
|
|
|
|||
232
micro.html
232
micro.html
|
|
@ -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" TRS input — plug your instrument (or an aux source) in; the click is mixed into it"><i></i><b>TRS 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 USB‑C"><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>USB‑C (power)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hint">Spin the dial = <b>tempo</b> · press = <b>start / stop</b> · hold & spin = <b>switch track</b></div>
|
||||
<!-- RIGHT END: power + output -->
|
||||
<div class="endcap right">
|
||||
<div class="jk usb" title="USB‑C — power (5 V) & set-list transfer"><i></i><b>USB‑C</b></div>
|
||||
<div class="jk" title="1/4" TRS output — instrument + click, to your amp, headphones or the desk"><i></i><b>TRS Out</b></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hint">Roll = <b>tempo</b> · press = <b>start / stop</b> · hold & 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 */
|
||||
|
|
|
|||
Loading…
Reference in a new issue