As-built: recessed thumb-roller for tempo (no protruding knob); USB-C bus power
- Tempo control: replace the protruding knob with a recessed side-mount thumb-roller — a detented encoder (EC11/PEC12) with a ribbed wheel exposed only at the edge through a slot, like a mouse scroll wheel, so there's nothing to snap off. Scroll/drag tempo interaction is unchanged; the ribs scroll for roll feedback (--rib) instead of a knob pointer. - Power: no battery — the device is USB-C bus-powered from the same port that carries config. Dropped the LiPo + TP4056 charger + 5 V boost from the BOM (total ≈ $49) and marked USB-C as 5 V in + config. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8c94749e4f
commit
a40ff04fd1
1 changed files with 20 additions and 18 deletions
|
|
@ -12,9 +12,10 @@
|
|||
the upgrade from the cramped 128×64 mono OLED — full colour, smooth type,
|
||||
hi-DPI. It also draws the beat indicator itself — a row of beat dots with
|
||||
the current beat's subdivisions below — so there's no separate LED bar;
|
||||
• controls arranged for use: an EC11 encoder below the screen (turning it
|
||||
never hides the readout) with arcade pushbuttons spread below — PREV far
|
||||
left, NEXT far right, a big central PLAY — so you don't hit the wrong one;
|
||||
• controls arranged for use: a recessed thumb-roller for tempo — a detented
|
||||
encoder with a side-mount wheel, so nothing sticks out to snap off — below
|
||||
the screen, with arcade pushbuttons spread below: PREV far left, NEXT far
|
||||
right, a big central PLAY, so you don't hit the wrong one;
|
||||
• rear I/O: external trigger in (footswitch), a 1/4" instrument pass-through
|
||||
with the click mixed in the ANALOG domain (DAC → summing op-amp → balanced
|
||||
line driver), a shared 1/4" balanced-TRS main out, plus an analog monitor
|
||||
|
|
@ -101,12 +102,14 @@
|
|||
wheel never hides the readout · PREV far-left / NEXT far-right · big central PLAY */
|
||||
.controls{ display:flex; flex-direction:column; align-items:center; gap:13px; margin:14px 0 2px }
|
||||
.enc-wrap{ display:flex; flex-direction:column; align-items:center; gap:5px }
|
||||
.enc{ width:52px; height:52px; border-radius:50%; cursor:ns-resize; position:relative; touch-action:none;
|
||||
background:repeating-conic-gradient(from 0deg, #424b57 0 7deg, #2c333d 7deg 14deg);
|
||||
border:2px solid #565f6c; box-shadow:0 3px 8px rgba(0,0,0,.5), inset 0 1px 1px rgba(255,255,255,.12) }
|
||||
.enc::before{ content:""; position:absolute; inset:9px; border-radius:50%; background:radial-gradient(circle at 38% 32%,#3b434f,#181c22 75%) }
|
||||
.enc::after{ content:""; position:absolute; left:50%; top:7px; width:3px; height:12px; background:var(--cyan); border-radius:2px;
|
||||
transform-origin:50% 19px; transform:translateX(-50%) rotate(var(--a,0deg)); box-shadow:0 0 5px var(--cyan) }
|
||||
/* recessed thumb-roller (side-mount encoder wheel) — only the edge is exposed */
|
||||
.roller{ width:28px; height:50px; border-radius:7px; cursor:ns-resize; position:relative; touch-action:none; overflow:hidden;
|
||||
background:#0a0d12; border:1px solid #04060a; box-shadow:inset 0 0 0 1px rgba(255,255,255,.03), 0 2px 4px rgba(0,0,0,.5) }
|
||||
.roller::before{ content:""; position:absolute; inset:2px 4px; border-radius:4px; /* ribbed wheel surface, scrolls via --rib */
|
||||
background:repeating-linear-gradient(0deg, rgba(255,255,255,.11) 0 1px, rgba(0,0,0,.5) 1px 4px);
|
||||
background-position:0 var(--rib,0px) }
|
||||
.roller::after{ content:""; position:absolute; inset:0; border-radius:7px; pointer-events:none; /* cylinder sheen: bright centre, curving dark at top/bottom */
|
||||
background:linear-gradient(180deg, rgba(0,0,0,.72) 0%, rgba(255,255,255,.12) 48%, rgba(255,255,255,.12) 52%, rgba(0,0,0,.72) 100%) }
|
||||
.enc-wrap small{ font-size:8px; color:var(--silk); letter-spacing:.12em; opacity:.85 }
|
||||
.keys{ display:flex; align-items:flex-end; justify-content:space-between; width:100%; padding:0 2px }
|
||||
.key-mid{ display:flex; align-items:flex-end; gap:20px }
|
||||
|
|
@ -192,7 +195,7 @@
|
|||
<div class="tft-cap">2.0″ 320×240 IPS TFT (ST7789) — beat & subdivisions on‑screen</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="enc-wrap"><div class="enc" id="enc" title="Tempo — scroll or drag to turn"></div><small>TEMPO</small></div>
|
||||
<div class="enc-wrap"><div class="roller" id="enc" title="Tempo — roll it (scroll or drag)"></div><small>TEMPO</small></div>
|
||||
<div class="keys">
|
||||
<div class="key"><button class="abtn nav" id="bPrev" title="previous item">⏮</button><small>Prev</small></div>
|
||||
<div class="key-mid">
|
||||
|
|
@ -233,7 +236,7 @@
|
|||
<!-- ===================== BILL OF MATERIALS ===================== -->
|
||||
<div class="panel bom-panel">
|
||||
<h2>Bill of materials</h2>
|
||||
<p class="sub">Rough parts list for the device above — an RP2040 build with analog click injection.
|
||||
<p class="sub">Rough parts list for the device above — a USB‑C‑powered RP2040 build 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>
|
||||
|
|
@ -244,7 +247,7 @@
|
|||
<tr class="grp"><td colspan="3">Controls</td></tr>
|
||||
<tr><td class="part">Arcade pushbutton, 24 mm <span class="spec">— Prev · Next · Tap</span></td><td class="q">3</td><td class="c">4</td></tr>
|
||||
<tr><td class="part">Arcade pushbutton, 30 mm <span class="spec">— Play</span></td><td class="q">1</td><td class="c">2</td></tr>
|
||||
<tr><td class="part">EC11 rotary encoder + knurled knob <span class="spec">— tempo</span></td><td class="q">1</td><td class="c">2</td></tr>
|
||||
<tr><td class="part">Detented encoder (EC11 / PEC12) + side‑mount thumb‑roller <span class="spec">— recessed; nothing to snap off</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>
|
||||
|
|
@ -252,13 +255,12 @@
|
|||
<tr><td class="part">PAM8302A mono Class‑D + 8 Ω 2 W 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) · Trig In (TS)</span></td><td class="q">3</td><td class="c">3</td></tr>
|
||||
<tr><td class="part">LiPo 1200 mAh + TP4056 charger + 5 V boost</td><td class="q">1</td><td class="c">6</td></tr>
|
||||
<tr><td class="part">Power slide switch + PWR LED</td><td class="q">1</td><td class="c">1</td></tr>
|
||||
<tr><td class="part">USB‑C bus power (5 V) + PWR LED <span class="spec">— same port carries config; no battery</span></td><td class="q">1</td><td class="c">1</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">5</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">3</td></tr>
|
||||
<tr><td class="part">3D‑printed enclosure + screws / standoffs</td><td class="q">1</td><td class="c">5</td></tr>
|
||||
<tr class="total"><td>Total (one‑off)</td><td class="q"></td><td class="c">≈ $55</td></tr>
|
||||
<tr class="total"><td>Total (one‑off)</td><td class="q"></td><td class="c">≈ $49</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="sub" style="margin-top:12px">Audio is summed in the <b>analog domain</b>: the DAC's click is mixed with a high‑impedance
|
||||
|
|
@ -346,8 +348,8 @@ function loadSetlistObj(sl){ setlist=sl; idx=0; const wasRunning=state.running;
|
|||
let taps=[];
|
||||
function tapTempo(){ const now=performance.now(); taps=taps.filter(t=>now-t<2000); taps.push(now);
|
||||
if(taps.length>=2){ let s=0; for(let i=1;i<taps.length;i++) s+=taps[i]-taps[i-1]; setBpm(60000/(s/(taps.length-1))); ramp.on=false; renderAll(); } }
|
||||
let knobAngle=0;
|
||||
function nudge(d){ ramp.on=false; setBpm(state.bpm+d); knobAngle+=d*9; $("enc").style.setProperty("--a",knobAngle+"deg"); renderAll(); }
|
||||
let rollPos=0;
|
||||
function nudge(d){ ramp.on=false; setBpm(state.bpm+d); rollPos+=d*2.5; $("enc").style.setProperty("--rib",rollPos+"px"); renderAll(); }
|
||||
|
||||
/* ===================== RENDER: 320×240 colour IPS TFT (ST7789) =============== */
|
||||
const TFT_W=320, TFT_H=240;
|
||||
|
|
@ -408,7 +410,7 @@ function drawTFT(){
|
|||
}
|
||||
|
||||
function renderAll(){ drawTFT(); $("bPlay").textContent=state.running?"■":"▶"; $("bPlay").classList.toggle("on",state.running);
|
||||
$("enc").style.setProperty("--a", knobAngle+"deg"); }
|
||||
$("enc").style.setProperty("--rib", rollPos+"px"); }
|
||||
function draw(){
|
||||
if(audioCtx&&state.running){ const now=audioCtx.currentTime;
|
||||
for(const m of meters){ while(m.vqPtr<m.vq.length&&m.vq[m.vqPtr].time<=now){ m.currentStep=m.vq[m.vqPtr].step; m.currentBar=m.vq[m.vqPtr].bar; m.vqPtr++; } } }
|
||||
|
|
|
|||
Loading…
Reference in a new issue