Compare commits

..

No commits in common. "4340f1838ed684c9aac72bab83dd563a003323b4" and "00cb245331a1a0dc0f8d5a8bac4366008703c674" have entirely different histories.

2 changed files with 54 additions and 51 deletions

View file

@ -1 +1 @@
0.0.17 0.0.16

View file

@ -8,9 +8,9 @@
<!-- <!--
"As-built" variant of the PM-1 player: the same firmware/engine, drawn with the "As-built" variant of the PM-1 player: the same firmware/engine, drawn with the
parts you'd actually solder for an RP2040 build — parts you'd actually solder for an RP2040 build —
• a 2.0″ 320×240 colour IPS TFT (ST7789, e.g. Pimoroni Pico Display 2.0): • a 128×64 MONOCHROME OLED (SSD1306/SH1106 class): rendered as a true 1-bit
the upgrade from the cramped 128×64 mono OLED — full colour, ~5× the framebuffer (drawn, then thresholded to crisp on/off pixels, scaled with
resolution, smooth anti-aliased type (rendered hi-DPI on a canvas); image-rendering:pixelated) so the cramped real layout is honest;
• a 4×16 WS2812 ("NeoPixel") RGB matrix (PIO-driven): the bottom row is the • a 4×16 WS2812 ("NeoPixel") RGB matrix (PIO-driven): the bottom row is the
beat (cyan downbeats / amber group-starts), and the three rows above stack beat (cyan downbeats / amber group-starts), and the three rows above stack
the current beat's subdivisions as they pass (driven by the finest lane); the current beat's subdivisions as they pass (driven by the finest lane);
@ -77,13 +77,15 @@
.pwr{ display:flex; align-items:center; gap:6px; font-size:9px; color:var(--silk); text-transform:uppercase; letter-spacing:.14em; opacity:.85 } .pwr{ display:flex; align-items:center; gap:6px; font-size:9px; 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 } .pwr .dot{ width:7px; height:7px; border-radius:50%; background:#2fe07a; box-shadow:0 0 7px #2fe07a }
/* ---- 2.0″ 320×240 colour IPS TFT (the OLED upgrade) ---- */ /* ---- 128×64 monochrome OLED module ---- */
.tft-wrap{ display:flex; justify-content:center; margin:0 4px } .oled-wrap{ display:flex; justify-content:center; margin:0 4px }
.tft-mod{ background:#000; border-radius:10px; padding:9px; .oled-mod{ background:#0a0c0f; border:1px solid #000; border-radius:6px; padding:10px 12px;
box-shadow:inset 0 0 0 2px #15181d, inset 0 0 0 3px #000, 0 3px 9px rgba(0,0,0,.55) } box-shadow:inset 0 0 0 2px #1a1d22, 0 2px 4px rgba(0,0,0,.5); position:relative }
#tft{ display:block; width:320px; height:240px; max-width:100%; border-radius:4px; background:#06080c; .oled-mod::before{ content:""; position:absolute; top:6px; right:6px; width:26px; height:5px; border-radius:2px;
box-shadow:0 0 0 1px #000, inset 0 0 18px rgba(0,0,0,.45) } background:repeating-linear-gradient(90deg,#2a2f36 0 2px,transparent 2px 4px); opacity:.6 } /* tiny pin header detail */
.tft-cap{ text-align:center; font-size:10px; color:var(--muted); margin-top:7px; letter-spacing:.02em } #oled{ display:block; width:336px; height:168px; image-rendering:pixelated; image-rendering:crisp-edges;
background:#000; border-radius:2px }
.oled-cap{ text-align:center; font-size:10px; color:var(--muted); margin-top:6px; letter-spacing:.02em }
/* ---- 4×16 WS2812 RGB matrix: bottom row = beat, 3 rows above = subdivisions ---- */ /* ---- 4×16 WS2812 RGB matrix: bottom row = beat, 3 rows above = subdivisions ---- */
.ledgrid{ display:flex; flex-direction:column; gap:5px; width:max-content; margin:16px auto 4px; .ledgrid{ display:flex; flex-direction:column; gap:5px; width:max-content; margin:16px auto 4px;
@ -159,12 +161,12 @@
<div class="pwr"><span class="dot"></span>PWR</div> <div class="pwr"><span class="dot"></span>PWR</div>
</div> </div>
<div class="tft-wrap"> <div class="oled-wrap">
<div class="tft-mod"> <div class="oled-mod">
<canvas id="tft" width="320" height="240" aria-label="2 inch 320 by 240 colour IPS display"></canvas> <canvas id="oled" width="128" height="64" aria-label="128 by 64 monochrome OLED"></canvas>
</div> </div>
</div> </div>
<div class="tft-cap">2.0″ 320×240 IPS TFT (ST7789)</div> <div class="oled-cap">0.96″ 128×64 mono OLED (SSD1306)</div>
<div class="ledgrid" id="leds"></div> <div class="ledgrid" id="leds"></div>
<div class="ledbar-cap">4×16 WS2812 — beat (bottom) + 3 subdivision rows</div> <div class="ledbar-cap">4×16 WS2812 — beat (bottom) + 3 subdivision rows</div>
@ -283,41 +285,42 @@ function tapTempo(){ const now=performance.now(); taps=taps.filter(t=>now-t<2000
let knobAngle=0; let knobAngle=0;
function nudge(d){ ramp.on=false; setBpm(state.bpm+d); knobAngle+=d*9; $("enc").style.setProperty("--a",knobAngle+"deg"); renderAll(); } function nudge(d){ ramp.on=false; setBpm(state.bpm+d); knobAngle+=d*9; $("enc").style.setProperty("--a",knobAngle+"deg"); renderAll(); }
/* ===================== RENDER: 320×240 colour IPS TFT (ST7789) =============== */ /* ========================= RENDER: 128×64 mono OLED ========================== */
const TFT_W=320, TFT_H=240; const oled=$("oled"), oc=oled.getContext("2d");
const tft=$("tft"), tc=tft.getContext("2d"); const OLED_ON=[150,220,255]; // cool-white/blue monochrome panel
(function(){ const dpr=Math.max(1,Math.min(3,window.devicePixelRatio||1)); let nameScroll=0;
tft.width=TFT_W*dpr; tft.height=TFT_H*dpr; tc.scale(dpr,dpr); })(); // hi-DPI backing → crisp type function oledThreshold(){ // posterize the framebuffer to 1-bit, recolour to the panel
function drawTFT(){ const img=oc.getImageData(0,0,128,64), d=img.data, r=OLED_ON[0], g=OLED_ON[1], b=OLED_ON[2];
for(let i=0;i<d.length;i+=4){ const on=d[i]>110; d[i]=on?r:0; d[i+1]=on?g:0; d[i+2]=on?b:0; d[i+3]=255; }
oc.putImageData(img,0,0);
}
function drawOLED(){
const m=meters[0]; const m=meters[0];
const g=tc.createLinearGradient(0,0,0,TFT_H); g.addColorStop(0,"#0b0f18"); g.addColorStop(1,"#05070c"); oc.fillStyle="#000"; oc.fillRect(0,0,128,64);
tc.fillStyle=g; tc.fillRect(0,0,TFT_W,TFT_H); oc.fillStyle="#fff"; oc.textBaseline="top";
// header // top status row
tc.textBaseline="middle"; tc.textAlign="left"; oc.font='7px "Courier New",monospace'; oc.textAlign="left";
tc.font='600 13px "Segoe UI",system-ui,sans-serif'; tc.fillStyle="#5b7a93"; oc.fillText(setlist?((idx+1)+"/"+setlist.items.length):"-/-", 0, 0);
tc.fillText("♪ "+(setlist?((idx+1)+"/"+setlist.items.length):"/"), 14, 18); oc.textAlign="right"; oc.fillText(state.running?"PLAY":"STOP", 128, 0);
tc.textAlign="right"; tc.fillStyle = state.running ? "#2fe07a" : "#6b7787"; // big BPM
tc.fillText(state.running?"▶ PLAY":"■ STOP", TFT_W-14, 18); oc.textAlign="left"; oc.font='bold 27px Arial,sans-serif';
tc.fillStyle="#13283c"; tc.fillRect(14,32,TFT_W-28,1); const bpmStr=String(state.bpm); oc.fillText(bpmStr, 0, 8);
// tempo — the hero number const bw=Math.min(oc.measureText(bpmStr).width+5, 96);
tc.textBaseline="alphabetic"; tc.textAlign="left"; oc.font='8px "Courier New",monospace'; oc.fillText("BPM", bw, 12);
tc.fillStyle="#1fb6f0"; tc.font='800 86px "Segoe UI",system-ui,sans-serif'; if(m){ oc.fillText((m.groupsStr.replace(/[^0-9+]/g,"")||String(m.beatsPerBar)), bw, 24); } // grouping e.g. 2+2+3
const bpm=String(state.bpm); tc.fillText(bpm, 18, 134); oc.fillRect(0,37,128,1); // separator
const bx=18+tc.measureText(bpm).width+9; // item name (marquee if too wide)
tc.fillStyle="#5b7a93"; tc.font='700 17px "Segoe UI",system-ui,sans-serif'; tc.fillText("BPM", bx, 96); oc.font='8px "Courier New",monospace';
tc.font='600 18px "Segoe UI",system-ui,sans-serif'; tc.fillText("♩ "+(m?(m.groupsStr.replace(/[^0-9+]/g,"")||m.beatsPerBar):""), bx, 122); const name=setlist?(setlist.items[idx].name||"-"):"-";
// item name — centred, ellipsised const nw=oc.measureText(name).width;
tc.textAlign="center"; tc.fillStyle="#e9eff7"; tc.font='600 23px "Segoe UI",system-ui,sans-serif'; let nx=0;
let nm=setlist?(setlist.items[idx].name||"—"):"—"; if(nw>128){ const period=nw+24; nx=128-(nameScroll%period); }
if(tc.measureText(nm).width>TFT_W-28){ while(nm.length>1 && tc.measureText(nm+"…").width>TFT_W-28) nm=nm.slice(0,-1); nm+="…"; } oc.fillText(name, nx, 41);
tc.fillText(nm, TFT_W/2, 180); // bottom row
// bottom strip oc.font='7px "Courier New",monospace'; oc.textAlign="left";
tc.fillStyle="#13283c"; tc.fillRect(14,200,TFT_W-28,1); oc.fillText(state.running&&m ? ("bar "+(m.currentBar+1)+" beat "+(Math.floor(m.currentStep/m.stepsPerBeat)+1||"-")) : "ready", 0, 53);
tc.textAlign="left"; tc.fillStyle="#7f8b9a"; tc.font='600 14px "Segoe UI",system-ui,sans-serif'; if(segBars>0){ oc.textAlign="right"; const rem=Math.max(0,segBars-(m?m.currentBar:0)); oc.fillText((state.running?rem:segBars)+"b", 128, 53); }
tc.fillText(state.running&&m ? ("BAR "+(m.currentBar+1)+" BEAT "+(Math.floor(m.currentStep/m.stepsPerBeat)+1||"")) : "READY", 14, 224); oledThreshold();
if(segBars>0){ const rem=Math.max(0,segBars-(m?m.currentBar:0));
tc.textAlign="right"; tc.fillStyle="#ffd166"; tc.font='700 15px "Segoe UI",system-ui,sans-serif';
tc.fillText((state.running?rem:segBars)+" BARS", TFT_W-14, 224); }
} }
/* ========== RENDER: 4×16 WS2812 matrix — beat row + 3 subdivision rows ========= */ /* ========== RENDER: 4×16 WS2812 matrix — beat row + 3 subdivision rows ========= */
@ -353,12 +356,12 @@ function renderBar(){
} }
} }
function renderAll(){ drawTFT(); renderBar(); $("bPlay").textContent=state.running?"■":"▶"; $("bPlay").classList.toggle("on",state.running); function renderAll(){ drawOLED(); renderBar(); $("bPlay").textContent=state.running?"■":"▶"; $("bPlay").classList.toggle("on",state.running);
$("enc").style.setProperty("--a", knobAngle+"deg"); } $("enc").style.setProperty("--a", knobAngle+"deg"); }
function draw(){ function draw(){
if(audioCtx&&state.running){ const now=audioCtx.currentTime; 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++; } } } 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++; } } }
drawTFT(); renderBar(); nameScroll+=0.6; drawOLED(); renderBar();
requestAnimationFrame(draw); requestAnimationFrame(draw);
} }