- Add front + top/side dimensioned schematic views (inch + mm) inside the "Show info" section of Stage (front + top-edge I/O), Practice (front bar + both end faces), and Display (pyramid front + side profile). Shared .dview / .dschem CSS in base.css. - Fix: progbox.js now defers to DOM-ready, so the "Show info" toggle + #techinfo are wired even when they sit after the page script (Stage/Practice/Player were silently not toggling / ignoring ?info=1). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
393 lines
24 KiB
HTML
393 lines
24 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||
<title>VARASYS PM_S‑1 — Stage (foot‑pedal stompbox)</title>
|
||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,@BUILD:favicon@">
|
||
<script>
|
||
/* ?embed=1 → strip site chrome + auto-size to the host */
|
||
(function(){ if(!/[?&]embed=1/.test(location.search)) return;
|
||
document.documentElement.dataset.embed="1";
|
||
function ph(){ try{ parent.postMessage({type:"varasys-h",h:Math.ceil(document.documentElement.getBoundingClientRect().height)},"*"); }catch(e){} }
|
||
addEventListener("load",ph); addEventListener("resize",ph); setTimeout(ph,300); setTimeout(ph,1000);
|
||
})();
|
||
</script>
|
||
<!--
|
||
PM-1 "Stage" — the foot-operated live stompbox (the hands-free sibling of the
|
||
desktop /teacher.html). Same RP2040 firmware/engine. Floor-driven controls:
|
||
• LEFT footswitch = TAP tempo (tap to set BPM) ; hold = start / stop
|
||
• RIGHT footswitch = NEXT set-list item ; hold = previous
|
||
• 1/4" expression-pedal input = sweep tempo with your foot
|
||
A big floor-readable RGB BEAT light + a small angled TFT (BPM, item, beats).
|
||
Analog click injection (Inst in -> summed -> balanced TRS out) like the Teacher.
|
||
Power: TWO USB-C ports — one data+power, one power-thru, so pedals daisy-chain
|
||
off a single charger / power bank. Shares src/engine.js.
|
||
-->
|
||
<script>
|
||
(function(){ try{ var p = localStorage.getItem("metronome.theme");
|
||
if (p!=="light" && p!=="dark" && p!=="system") p = "system";
|
||
document.documentElement.dataset.theme = p==="system" ? (matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark") : p;
|
||
} catch(e){ document.documentElement.dataset.theme = "dark"; } })();
|
||
</script>
|
||
<style>
|
||
/*@BUILD:include:src/base.css@*/
|
||
:root{ --bg1:#12151c; --bg2:#05070a; --txt:#c7d0db; --muted:#7f8b9a; --link:#6cb6ff;
|
||
--panel-bd:#2a313c; --device-bd:#33363c; --silk:#aab2bc; --cyan:#0AB3F7; }
|
||
:root[data-theme="light"]{ --bg1:#f5f8fc; --bg2:#dde4ec; --txt:#1e2630; --muted:#5c6776; --link:#1769c4; --panel-bd:#d2dae4 }
|
||
body{ margin:0; min-height:100vh; padding:26px 14px 46px;
|
||
background:radial-gradient(circle at 50% -8%, var(--bg1), var(--bg2)); color:var(--txt);
|
||
display:flex; flex-direction:column; align-items:center; gap:16px }
|
||
a{ color:var(--link) }
|
||
|
||
/* ---- the stompbox ---- */
|
||
.device{ width:100%; max-width:340px; position:relative; border-radius:16px; padding:0 0 20px;
|
||
background:
|
||
radial-gradient(rgba(255,255,255,.022) .5px, transparent .6px) 0 0/3px 3px,
|
||
linear-gradient(180deg, #2b2d33, #141518);
|
||
border:1px solid var(--device-bd);
|
||
box-shadow:0 26px 52px rgba(0,0,0,.62), inset 0 1px 0 rgba(255,255,255,.05), inset 0 -3px 10px rgba(0,0,0,.55) }
|
||
|
||
/* top edge — all the jacks (cables exit upward off the board) */
|
||
.edge{ display:flex; align-items:flex-start; justify-content:space-between; gap:4px;
|
||
padding:11px 12px 12px; border-radius:16px 16px 0 0; background:linear-gradient(180deg,#1c1e22,#0d0e11);
|
||
border-bottom:1px solid #04060a; box-shadow:inset 0 -6px 12px rgba(0,0,0,.5) }
|
||
.jk{ flex:1; display:flex; flex-direction:column; align-items:center; gap:4px }
|
||
.jk i{ width:17px; height:17px; border-radius:50%; background:radial-gradient(circle at 40% 34%, #333a44, #07090c 72%);
|
||
border:2px solid #5b6470; box-shadow:inset 0 0 4px #000 }
|
||
.jk.usb i{ width:19px; height:8px; border-radius:3px; border:2px solid #5b6470; background:#07090c }
|
||
.jk b{ font-size:6.5px; font-weight:700; color:var(--silk); letter-spacing:.03em; text-transform:uppercase; opacity:.9; text-align:center; line-height:1.2 }
|
||
|
||
.brandrow{ display:flex; align-items:center; justify-content:space-between; margin:13px 16px 10px }
|
||
.brand-logo{ height:13px; width:auto; display:block }
|
||
.silk{ display:flex; align-items:center; gap:7px; color:var(--silk) }
|
||
.silk .model{ font-size:8.5px; text-transform:uppercase; letter-spacing:.16em; opacity:.85 }
|
||
.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 }
|
||
|
||
/* small angled TFT */
|
||
.tft-wrap{ margin:0 16px; padding:7px; border-radius:9px; background:linear-gradient(180deg,#0b0d11,#05070a);
|
||
border:1px solid #04060a; box-shadow:inset 0 2px 8px rgba(0,0,0,.7);
|
||
transform:perspective(440px) rotateX(7deg) }
|
||
#tft{ display:block; width:100%; height:96px; border-radius:5px; background:#06080c }
|
||
|
||
/* big floor-readable RGB beat light (dome LED) */
|
||
.beat-wrap{ display:flex; flex-direction:column; align-items:center; gap:5px; margin:16px 0 4px }
|
||
.beatlight{ width:74px; height:74px; border-radius:50%; position:relative;
|
||
background:radial-gradient(circle at 42% 36%, #20242b, #0a0c10 72%);
|
||
border:3px solid #2a2f37; box-shadow:0 3px 8px rgba(0,0,0,.55), inset 0 2px 5px rgba(255,255,255,.06) }
|
||
.beatlight::after{ content:""; position:absolute; inset:11px; border-radius:50%;
|
||
background:var(--bc,#0c0f14); box-shadow:0 0 var(--bg-glow,0) var(--bc,#0c0f14); transition:none }
|
||
.beat-cap{ font-size:7.5px; color:var(--silk); letter-spacing:.16em; text-transform:uppercase; opacity:.8 }
|
||
|
||
/* expression-pedal stand-in: a rocker that sweeps tempo */
|
||
.exp{ margin:14px 18px 4px; display:flex; align-items:center; gap:10px }
|
||
.exp label{ font-size:7.5px; color:var(--silk); letter-spacing:.1em; text-transform:uppercase; opacity:.85; white-space:nowrap }
|
||
.exp input[type=range]{ flex:1; -webkit-appearance:none; appearance:none; height:8px; border-radius:5px; outline:none;
|
||
background:linear-gradient(90deg,#1b2733,#33424f); border:1px solid #04060a }
|
||
.exp input[type=range]::-webkit-slider-thumb{ -webkit-appearance:none; width:26px; height:18px; border-radius:4px; cursor:ew-resize;
|
||
background:linear-gradient(180deg,#444c57,#222730); border:1px solid #565f6c; box-shadow:0 2px 4px rgba(0,0,0,.5) }
|
||
.exp input[type=range]::-moz-range-thumb{ width:26px; height:18px; border-radius:4px; cursor:ew-resize;
|
||
background:linear-gradient(180deg,#444c57,#222730); border:1px solid #565f6c }
|
||
|
||
/* two heavy footswitches */
|
||
.switches{ display:flex; justify-content:space-around; gap:18px; margin:14px 14px 2px }
|
||
.fsw{ flex:1; display:flex; flex-direction:column; align-items:center; gap:8px }
|
||
.stomp{ width:78px; height:78px; border-radius:50%; cursor:pointer; position:relative; border:0; padding:0; touch-action:none;
|
||
background:radial-gradient(circle at 38% 30%, #eef2f6, #aab2bc 40%, #6c7480 70%, #3b424c 100%);
|
||
box-shadow:0 6px 10px rgba(0,0,0,.55), inset 0 -3px 6px rgba(0,0,0,.4), inset 0 3px 5px rgba(255,255,255,.5) }
|
||
.stomp::after{ content:""; position:absolute; inset:18px; border-radius:50%;
|
||
background:radial-gradient(circle at 40% 34%, #d7dde3, #8b939e 70%, #5a626c 100%);
|
||
box-shadow:inset 0 2px 4px rgba(255,255,255,.5), inset 0 -3px 6px rgba(0,0,0,.4) }
|
||
.stomp.down{ transform:translateY(3px); box-shadow:0 2px 4px rgba(0,0,0,.55), inset 0 -2px 4px rgba(0,0,0,.4), inset 0 2px 4px rgba(255,255,255,.4) }
|
||
.fsw b{ font-size:9px; font-weight:800; color:var(--silk); letter-spacing:.12em; text-transform:uppercase }
|
||
.fsw small{ font-size:7px; color:var(--muted); letter-spacing:.04em; text-align:center; line-height:1.3 }
|
||
|
||
.hint{ max-width:340px; text-align:center; font-size:11px; color:var(--muted); line-height:1.5 }
|
||
[data-embed] .hint{ display:none !important }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
/*@BUILD:include:src/header.html@*/
|
||
|
||
<h1 class="ff-title">PM_S‑1 Stage</h1>
|
||
<p class="ff-sum">Foot‑pedal stompbox for the stage — hands‑free with two footswitches and an expression pedal, a big floor‑readable RGB beat light, instrument pass‑through with the click mixed in.</p>
|
||
|
||
<div class="device">
|
||
<!-- top edge: all jacks, including dual USB-C daisy-chain power -->
|
||
<div class="edge">
|
||
<div class="jk" title="External trigger / aux footswitch in"><i></i><b>Trig</b></div>
|
||
<div class="jk" title="Instrument in — 1/4" pass-through; the click is mixed into your signal"><i></i><b>Inst In</b></div>
|
||
<div class="jk" title="Main out — 1/4" balanced TRS (instrument + click)"><i></i><b>Out TRS</b></div>
|
||
<div class="jk" title="Expression-pedal input — sweep tempo with your foot"><i></i><b>Exp</b></div>
|
||
<div class="jk usb" title="USB-C — power + data (config / firmware)"><i></i><b>USB‑C</b></div>
|
||
<div class="jk usb" title="USB-C — power thru (daisy-chain the next pedal)"><i></i><b>USB‑C thru</b></div>
|
||
</div>
|
||
|
||
<div class="brandrow">
|
||
<div class="silk"><img class="brand-logo" src="data:image/png;base64,@BUILD:logo-dark@" alt="VARASYS" /><span class="model">PM_S‑1 Stage</span></div>
|
||
<div class="pwr"><span class="dot"></span>USB‑C PWR</div>
|
||
</div>
|
||
|
||
<div class="tft-wrap"><canvas id="tft" width="300" height="96" aria-label="tempo / item display"></canvas></div>
|
||
|
||
<div class="beat-wrap">
|
||
<div class="beatlight" id="beat"></div>
|
||
<div class="beat-cap">Beat</div>
|
||
</div>
|
||
|
||
<div class="exp">
|
||
<label for="expPedal">Exp pedal<br>(tempo)</label>
|
||
<input type="range" id="expPedal" min="40" max="240" value="120" title="External expression pedal → tempo sweep">
|
||
</div>
|
||
|
||
<div class="switches">
|
||
<div class="fsw">
|
||
<button class="stomp" id="swTap" title="Tap to set tempo · hold to start/stop"></button>
|
||
<b>Tap</b><small>tap = tempo<br>hold = start/stop</small>
|
||
</div>
|
||
<div class="fsw">
|
||
<button class="stomp" id="swNext" title="Next item · hold for previous"></button>
|
||
<b>Next</b><small>tap = next<br>hold = previous</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="hint">Stomp <b>Tap</b> to set tempo (hold = start/stop) · <b>Next</b> to change item (hold = previous) ·
|
||
an expression pedal sweeps tempo. Instrument passes through with the click mixed in (analog).</div>
|
||
|
||
/*@BUILD:include:src/progbox.html@*/
|
||
|
||
<script>
|
||
const APP_VERSION = "v0.0.1-dev";
|
||
const $ = (id) => document.getElementById(id);
|
||
|
||
/* ========================= ENGINE (shared; synth voices only) ================= */
|
||
const SAMPLES = {};
|
||
/*@BUILD:include:src/engine.js@*/
|
||
/*@BUILD:include:src/setlists.js@*/
|
||
const state = { bpm:120, volume:0.85, running:false };
|
||
let meters = [], muteWindows = [];
|
||
|
||
function setBpm(v){ state.bpm = Math.max(30, Math.min(300, Math.round(v))); if(window.progRefresh) progRefresh(); }
|
||
function scheduler(){
|
||
const ahead = audioCtx.currentTime + SCHEDULE_AHEAD;
|
||
for(const m of meters){ while(m.nextTime < ahead){ scheduleMeterTick(m, m.nextTime); m.nextTime += laneStepDur(m, m.tick); m.tick++; } }
|
||
}
|
||
function buildMeters(lanes){
|
||
return (lanes||[]).map(c=>{ const p=parseGroups(c.groupsStr);
|
||
return {groupsStr:c.groupsStr,groups:p.groups,beatsPerBar:p.beatsPerBar,groupStarts:p.groupStarts,
|
||
stepsPerBeat:c.stepsPerBeat||1,sound:c.sound,beatsOn:(c.beatsOn||[]).slice(),poly:!!c.poly,swing:!!c.swing,enabled:c.enabled!==false,gainDb:c.gainDb||0,
|
||
tick:0,nextTime:0,vq:[],vqPtr:0,currentStep:-1,currentBar:0}; });
|
||
}
|
||
function startAudio(){
|
||
ensureAudio(); audioCtx.resume(); state.running=true;
|
||
const t0=audioCtx.currentTime+0.08;
|
||
for(const m of meters){ m.tick=0; m.nextTime=t0; m.vq=[]; m.vqPtr=0; m.currentStep=-1; }
|
||
beatCount=-1; lastBeatTime=t0; muteWindows=[];
|
||
schedulerTimer=setInterval(scheduler,LOOKAHEAD_MS); scheduler();
|
||
}
|
||
function stopAudio(){ state.running=false; clearInterval(schedulerTimer); schedulerTimer=null; }
|
||
function toggle(){ state.running ? stopAudio() : startAudio(); }
|
||
|
||
/* ========================= TRACKS (seed grooves, flattened) =================== */
|
||
let tracks = SEED_SETLISTS.flatMap(sl => sl.items.map(([n,p]) => ({ name:n, ...patchToSetup(p) })));
|
||
let trackIdx = 0;
|
||
function tracksFromHash(){
|
||
const m=(location.hash||"").match(/[#&](p|sl)=([^&]+)/); if(!m) return null;
|
||
let payload=m[2]; try{ payload=decodeURIComponent(payload); }catch(e){}
|
||
try{
|
||
if(m[1]==="sl"){ const sl=codeToSetlist(payload); return sl.items.length ? sl.items.map(it=>({...it})) : null; }
|
||
const s=patchToSetup(payload); return s.lanes.length ? [{name:"Patch",...s}] : null;
|
||
}catch(e){ return null; }
|
||
}
|
||
function loadTrack(i){
|
||
const n=tracks.length; if(!n) return; trackIdx=((i%n)+n)%n;
|
||
const t=tracks[trackIdx]; setBpm(t.bpm||120); $("expPedal").value=state.bpm; meters=buildMeters(t.lanes);
|
||
const was=state.running; if(was){ clearInterval(schedulerTimer); schedulerTimer=null; state.running=false; }
|
||
if(was) startAudio();
|
||
flashName=performance.now();
|
||
}
|
||
|
||
/* ========================= DISPLAY (TFT) + BEAT LIGHT ======================== */
|
||
const tft=$("tft"), tc=tft.getContext("2d"), TW=300, TH=96;
|
||
(function(){ const dpr=Math.max(1,Math.min(3,window.devicePixelRatio||1)); tft.width=TW*dpr; tft.height=TH*dpr; tc.scale(dpr,dpr); })();
|
||
let beatCount=-1, lastBeatTime=0, flash=0, flashAccent=false, flashName=0;
|
||
|
||
function masterLane(){ return meters[0]; }
|
||
function drawTFT(){
|
||
tc.fillStyle="#06080c"; tc.fillRect(0,0,TW,TH);
|
||
// tempo
|
||
tc.fillStyle="#eaf6ff"; tc.font="700 40px 'Segoe UI',Roboto,Arial,sans-serif"; tc.textBaseline="alphabetic";
|
||
tc.fillText(String(state.bpm), 12, 50);
|
||
tc.fillStyle="#5b86a3"; tc.font="600 11px 'Segoe UI',Roboto,Arial,sans-serif"; tc.fillText("BPM", 14, 64);
|
||
// running state
|
||
tc.fillStyle=state.running?"#2fe07a":"#7f8b9a"; tc.font="700 11px 'Segoe UI',Roboto,Arial,sans-serif";
|
||
tc.textAlign="right"; tc.fillText(state.running?"▶ RUN":"■ STOP", TW-12, 22); tc.textAlign="left";
|
||
// item name
|
||
const nm=(tracks[trackIdx]&&tracks[trackIdx].name)||"—";
|
||
tc.fillStyle="#c7d0db"; tc.font="600 13px 'Segoe UI',Roboto,Arial,sans-serif";
|
||
tc.fillText(nm.length>22?nm.slice(0,21)+"…":nm, 12, 84);
|
||
// beat dots (master lane)
|
||
const m=masterLane();
|
||
if(m){ const bpb=m.beatsPerBar, r=4, gap=13, x0=TW-12-(bpb-1)*gap, y=46;
|
||
const curBeat = m.currentStep>=0 ? Math.floor(m.currentStep/m.stepsPerBeat) : -1;
|
||
for(let i=0;i<bpb;i++){ const on = state.running && i===curBeat;
|
||
tc.beginPath(); tc.arc(x0+i*gap, y, r, 0, 7);
|
||
tc.fillStyle = on ? (m.groupStarts.has(i)?"#ff9b2e":"#33d0ff") : "#243240"; tc.fill(); } }
|
||
}
|
||
function setBeatLight(){
|
||
const el=$("beat"), c=flashAccent?"#ff9b2e":"#33d0ff";
|
||
const lit = Math.max(0, flash);
|
||
el.style.setProperty("--bc", lit>0.02 ? c : "#0c0f14");
|
||
el.style.setProperty("--bg-glow", (10+lit*34).toFixed(0)+"px");
|
||
el.style.filter = "brightness("+(1+lit*1.3)+")";
|
||
}
|
||
// Visuals follow the SAME clock the audio is scheduled on, but compensated for
|
||
// output latency so the on-screen pulse lands when the click is *heard* (not when
|
||
// it's queued). Without this the visual leads the sound by the output buffer /
|
||
// Bluetooth latency — up to a full subdivision on high-latency outputs.
|
||
function audioLatency(){ return audioCtx ? (audioCtx.outputLatency || audioCtx.baseLatency || 0) : 0; }
|
||
function draw(){
|
||
const now = audioCtx ? audioCtx.currentTime - audioLatency() : 0;
|
||
if(audioCtx && state.running){
|
||
for(const m of meters){ while(m.vqPtr<m.vq.length && m.vq[m.vqPtr].time<=now){
|
||
const e=m.vq[m.vqPtr]; m.currentStep=e.step; m.currentBar=e.bar;
|
||
if(m===meters[0] && e.step % m.stepsPerBeat === 0){ // a beat on the master lane
|
||
beatCount++; lastBeatTime=e.time;
|
||
const lvl=m.beatsOn[e.step]|0; flashAccent = lvl>=2 || m.groupStarts.has(e.step/m.stepsPerBeat);
|
||
if(lvl!==0) flash=1; // don't flash on a muted beat
|
||
}
|
||
m.vqPtr++;
|
||
}
|
||
if(m.vqPtr>512){ m.vq=m.vq.slice(m.vqPtr); m.vqPtr=0; } // bound the visual queue
|
||
}
|
||
}
|
||
flash = Math.max(0, flash - 0.085); // decay
|
||
drawTFT(); setBeatLight();
|
||
requestAnimationFrame(draw);
|
||
}
|
||
|
||
/* ========================= FOOTSWITCHES + EXP PEDAL ========================== */
|
||
function holdSwitch(el, onTap, onHold){
|
||
let t=null, held=false;
|
||
el.addEventListener("pointerdown",(e)=>{ e.preventDefault(); try{el.setPointerCapture(e.pointerId);}catch(_){}
|
||
held=false; el.classList.add("down"); t=setTimeout(()=>{ held=true; onHold&&onHold(); }, 480); });
|
||
const end=(fire)=>{ if(t){clearTimeout(t); t=null;} el.classList.remove("down"); if(fire && !held) onTap&&onTap(); held=false; };
|
||
el.addEventListener("pointerup",()=>end(true));
|
||
el.addEventListener("pointercancel",()=>end(false));
|
||
}
|
||
let taps=[];
|
||
function tapTempo(){ const now=performance.now(); taps.push(now); taps=taps.filter(x=>now-x<2400);
|
||
if(taps.length>=2){ let s=0; for(let i=1;i<taps.length;i++) s+=taps[i]-taps[i-1];
|
||
const bpm=Math.round(60000/(s/(taps.length-1))); if(bpm>=30&&bpm<=300){ setBpm(bpm); $("expPedal").value=state.bpm; } } }
|
||
holdSwitch($("swTap"), ()=>tapTempo(), ()=>toggle());
|
||
holdSwitch($("swNext"), ()=>loadTrack(trackIdx+1), ()=>loadTrack(trackIdx-1));
|
||
$("expPedal").addEventListener("input", (e)=>{ setBpm(+e.target.value); });
|
||
|
||
/* theme toggle (shared "metronome.theme") */
|
||
/*@BUILD:include:src/chrome.js@*/
|
||
|
||
addEventListener("keydown",(e)=>{
|
||
const tag=e.target?e.target.tagName:""; if(tag==="INPUT"||tag==="TEXTAREA"||tag==="SELECT") return;
|
||
if(e.key===" "){ e.preventDefault(); toggle(); }
|
||
else if(e.key==="t"||e.key==="T"){ tapTempo(); }
|
||
else if(e.key==="ArrowRight"){ loadTrack(trackIdx+1); }
|
||
else if(e.key==="ArrowLeft"){ loadTrack(trackIdx-1); }
|
||
});
|
||
|
||
/* ========================= INIT ============================================== */
|
||
{ const ht=tracksFromHash(); if(ht) tracks=ht; }
|
||
loadTrack(0);
|
||
requestAnimationFrame(draw);
|
||
window.currentProgramString = function(){ var t=tracks[trackIdx]||{}; return setupToPatch({bpm:state.bpm, volume:state.volume, lanes:t.lanes||[]}); };
|
||
window.loadProgramString = function(plain){ var s=patchToSetup(plain); tracks=[{name:"Program", ...s}]; trackIdx=0; loadTrack(0); };
|
||
/*@BUILD:include:src/progbox.js@*/
|
||
</script>
|
||
|
||
<label class="info-toggle pageonly"><input type="checkbox" id="infoToggle"> Show technical info (dimensions, BOM, embedding)</label>
|
||
<div id="techinfo" class="pageonly" hidden>
|
||
|
||
<section class="about">
|
||
<h2>PM_S‑1 — Stage</h2>
|
||
<div class="ff-tags"><span class="hw">Hardware</span><span>Foot‑pedal stompbox</span><span>~$52 one‑off</span></div>
|
||
<p>A foot‑operated polymeter stompbox for the stage: drive it hands‑free with two heavy footswitches and an
|
||
expression pedal, read it off the floor from the big RGB beat light, and run your instrument through it with
|
||
the click mixed in. (For a desk/lesson unit with a full screen, see the <a href="/teacher.html">Teacher</a>.)</p>
|
||
<p>The controls are built for feet: the <b>left footswitch</b> taps tempo (hold to start/stop), the <b>right</b>
|
||
steps through your set list (hold for previous), and a <b>1/4″ expression‑pedal input</b> sweeps tempo on the
|
||
fly. Your instrument passes through (1/4″ in) with the click summed in the <b>analog domain</b> and sent to a
|
||
balanced 1/4″ TRS out. Powered over USB‑C — with a second USB‑C <b>"thru"</b> port so several pedals
|
||
daisy‑chain off one charger or power bank.</p>
|
||
</section>
|
||
|
||
<div class="dview">
|
||
<p class="cap">Dimensions & layout — ≈ 4.7 × 3.7 × 1.5 in (120 × 93 × 38 mm), a 1590BB‑style stompbox</p>
|
||
<div class="drow">
|
||
<div class="dvy">↕ 3.7 in (93 mm)</div>
|
||
<div class="dschem" style="height:150px">
|
||
<span class="scap">Front</span>
|
||
<div class="scr" style="left:18%; right:18%; top:22px; height:34px"></div>
|
||
<div class="ctl" style="left:calc(50% - 15px); top:64px; width:30px; height:30px"></div>
|
||
<div class="ctl" style="left:20%; top:100px; width:34px; height:34px"></div>
|
||
<div class="ctl" style="left:calc(80% - 34px); top:100px; width:34px; height:34px"></div>
|
||
<div class="jl" style="left:0; right:0; bottom:5px">angled TFT · RGB beat light · Tap + Next footswitches</div>
|
||
</div>
|
||
</div>
|
||
<div class="dvx">↔ 4.7 in (120 mm) wide</div>
|
||
<div class="drow" style="margin-top:12px">
|
||
<div class="dvy">↕ 1.5 in (38 mm)</div>
|
||
<div class="dschem" style="height:56px">
|
||
<span class="scap">Top edge — I/O</span>
|
||
<div class="jk" style="left:7%; top:18px"></div><div class="jk" style="left:22%; top:18px"></div>
|
||
<div class="jk" style="left:37%; top:18px"></div><div class="jk" style="left:52%; top:18px"></div>
|
||
<div class="jk u" style="left:68%; top:21px"></div><div class="jk u" style="left:83%; top:21px"></div>
|
||
<div class="jl" style="left:0; right:0; bottom:4px">Trig · Inst In · Out TRS · Exp · USB‑C · USB‑C thru</div>
|
||
</div>
|
||
</div>
|
||
<div class="dvx">↔ 4.7 in (120 mm)</div>
|
||
</div>
|
||
|
||
<details class="spec pageonly">
|
||
<summary>Spec & bill of materials</summary>
|
||
<div class="spec-body">
|
||
<p class="sub">Rough parts list — a foot‑operated RP2040 stompbox (USB‑C, dual‑port) 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">1.3″ IPS TFT, ST7789 <span class="spec">— SPI; angled BPM / item readout</span></td><td class="q">1</td><td class="c">6</td></tr>
|
||
<tr><td class="part">High‑bright diffused RGB beat indicator <span class="spec">— floor‑readable</span></td><td class="q">1</td><td class="c">1</td></tr>
|
||
<tr class="grp"><td colspan="3">Controls</td></tr>
|
||
<tr><td class="part">Heavy‑duty momentary footswitch (soft‑touch) <span class="spec">— Tap · Next</span></td><td class="q">2</td><td class="c">6</td></tr>
|
||
<tr><td class="part">1/4″ expression‑pedal input jack (TRS) <span class="spec">— tempo sweep</span></td><td class="q">1</td><td class="c">1</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">Balanced line driver, DRV134 <span class="spec">— → 1/4″ TRS out</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">2× USB‑C (data+power & power‑thru) + power‑path/protection + PWR LED <span class="spec">— daisy‑chain pedals</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">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">Die‑cast aluminium stompbox (Hammond 1590BB‑style) <span class="spec">— bead‑blasted, matte‑black Type II anodise, laser‑etched</span></td><td class="q">1</td><td class="c">12</td></tr>
|
||
<tr class="total"><td>Total (one‑off)</td><td class="q"></td><td class="c">≈ $52</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<p class="sub" style="margin-top:12px">No built‑in speaker — the Stage feeds your amp / PA. The click is summed in
|
||
the <b>analog domain</b> (hi‑Z instrument buffer + DAC → balanced line driver), so your instrument is never
|
||
re‑digitised (no added latency).</p>
|
||
</div>
|
||
</details>
|
||
|
||
<p class="sub" style="max-width:760px;margin:14px auto 0">Embed this widget elsewhere with one <code><div></code> + a script —
|
||
see <a href="/embed.html">the embed docs</a>.</p>
|
||
</div><!-- /#techinfo -->
|
||
|
||
/*@BUILD:include:src/footer.html@*/
|
||
</body>
|
||
</html>
|