- Shared header/footer/chrome (src/header.html, src/footer.html, src/chrome.js) now on every page: editor (header above its app toolbar), player, teacher, stage, micro, showcase, embed. chrome.js defers to DOMContentLoaded so the footer version stamps regardless of placement. Player's fullscreen toggle relocated out of the header to a floating control. - Open = Info: each form-factor page is self-contained — a more-detailed description (.about) + an expandable "Spec & BOM" (<details class="spec">, hidden in embed). info-*.html retired; build/deploy/README updated. Next: teacher-style dimensioned front + top/side views + loading panels for Stage, Micro and Showcase. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
351 lines
21 KiB
HTML
351 lines
21 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‑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@*/
|
||
|
||
<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‑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>
|
||
|
||
<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))); }
|
||
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,
|
||
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);
|
||
</script>
|
||
|
||
<section class="about pageonly">
|
||
<h2>PM‑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>
|
||
|
||
<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>
|
||
|
||
/*@BUILD:include:src/footer.html@*/
|
||
</body>
|
||
</html>
|