Seedbox v0.1 + firmware, generic kit/synth, ROOT/SCALE pages
Some checks failed
check / check (push) Has been cancelled

First commit of the repo. Contents beyond the v0.1 simulator and layout:

- Full Pico 2 W firmware (firmware/app, drivers, engine port, host test).
- BANK page and 404 mode A/B removed; learned pads play exactly what was learned.
- 404/MiniFreak renamed to KIT/SYNTH throughout (jacks, code, sim, docs,
  layout labels; wiring rebuilt).
- ROLL returns to page select after a re-roll; a freshly learned track turns on.
- LCD contrast 66.
- ROOT and SCALE pages: 21 scales, AUTO = the seed picks; global and sticky.

Intended music change: the scale table grew from 8 to 21 and AUTO picks from
all of them, so every seed's scale changed. Vectors regenerated with
npm run vectors, plus new key-override cases. Flash record is now version 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 20:39:36 +03:00
commit 2196633ed5
77 changed files with 52207 additions and 0 deletions

484
sim/app.js Normal file
View File

@@ -0,0 +1,484 @@
/* ================= APP (browser only: UI, Web MIDI, scheduler) ================= */
(function(){
var E=Engine, TPB=E.TPB, NT=E.NT, HOLD_MS=250;
function store(k,v){ try{ localStorage.setItem('seedbox3.'+k,JSON.stringify(v)); }catch(e){} }
function load(k,d){ try{ var v=localStorage.getItem('seedbox3.'+k); return v?JSON.parse(v):d; }catch(e){ return d; } }
function rnd32(){ try{ return crypto.getRandomValues(new Uint32Array(1))[0]>>>0; }catch(e){ return (Date.now()*2654435761)>>>0; } }
function hex(n){ return ('00000000'+(n>>>0).toString(16).toUpperCase()).slice(-8); }
function clamp(x,a,b){ return x<a?a:x>b?b:x; }
function mod(a,n){ return ((a%n)+n)%n; }
function laneTracks(l){ var a=[]; for(var t=0;t<NT;t++) if(E.TRACKS[t].lane===l) a.push(t); return a; }
var KIT=laneTracks(0);
var DEF={portDrums:'',portMono:'',portIn:'',clock:true,
kit:{learn:[null,null,null,null,null,null,null]}, mono:{ch:1,oct:0}, hw:{display:'5110',leds:false}};
var cfg=(function(saved){ var c=JSON.parse(JSON.stringify(DEF)); Object.keys(c).forEach(function(k){
if(saved[k]===undefined) return; if(typeof c[k]==='object'){ Object.keys(c[k]).forEach(function(f){ if(saved[k][f]!==undefined) c[k][f]=saved[k][f]; }); } else c[k]=saved[k]; });
return c; })(load('cfg',{}));
if(!Array.isArray(cfg.kit.learn) || cfg.kit.learn.length<7) cfg.kit.learn=[null,null,null,null,null,null,null];
function saveCfg(){ store('cfg',cfg); }
var app={ state:load('state',null)||{seed:0x5EEDB0C5,salts:[0,0]}, gen:load('gen',0), scale:load('scale',-1), root:load('root',-1), energy:load('energy',64), drift:load('drift',32), bpm:load('bpm',165),
on:load('on',null), inv:[], fill:false, history:[], slots:load('slots',[null,null,null,null,null,null,null,null]),
page:0, editing:false, histSel:0, slotSel:0, rollSel:0, learn:0, learnTr:-1 };
if(!(app.scale>=-1 && app.scale<E.SCALE_NAMES.length)) app.scale=-1;
if(!(app.root>=-1 && app.root<12)) app.root=-1;
if(!Array.isArray(app.on) || app.on.length!==NT){ app.on=[]; for(var i9=0;i9<NT;i9++) app.on.push(true); }
for(var i0=0;i0<NT;i0++) app.inv.push(false); // inv: flipped while a button is held
var PAGES=['SEED','GEN','ROOT','SCALE','BPM','SLOT','ROLL','LEARN'];
function params(fillNow){ return {gen:app.gen,energy:app.energy,drift:app.drift,fill:!!fillNow,scale:app.scale,root:app.root}; }
function save(){ store('state',app.state); store('gen',app.gen); store('scale',app.scale); store('root',app.root); store('energy',app.energy); store('drift',app.drift); store('bpm',app.bpm); }
function setState(s){
app.history.unshift({seed:app.state.seed,salts:app.state.salts.slice()}); if(app.history.length>16) app.history.pop();
app.state={seed:s.seed>>>0,salts:[s.salts[0]|0,s.salts[1]|0]}; app.histSel=0; save(); dirty=true;
}
function baseOn(tr){ return !!app.on[tr]; }
function sounds(tr,mask){ var b=baseOn(tr,mask); return app.inv[tr]?!b:b; }
var flashMsg='', flashMsgUntil=0;
function msg(t,ms){ flashMsg=t; flashMsgUntil=performance.now()+(ms||900); }
/* ---------- MIDI ---------- */
var midi=null, outs={}, ins={}, inPort=null, statusEl=document.getElementById('midiStatus');
function setStatus(t,cls){ statusEl.textContent=t; statusEl.className='pill'+(cls?' '+cls:''); }
function fillSelect(id,list,re){
var sel=document.getElementById(id), want=cfg[id], map={}; sel.innerHTML='';
var none=document.createElement('option'); none.value=''; none.textContent='(none)'; sel.appendChild(none);
list.forEach(function(o){ map[o.id]=o; var op=document.createElement('option'); op.value=o.id; op.textContent=o.name; sel.appendChild(op); });
if(!want || !map[want]){ var guess=list.filter(function(o){ return re.test(o.name); })[0]; if(guess){ want=guess.id; cfg[id]=want; } }
sel.value=map[want]?want:'';
}
function fillPorts(){
outs={}; ins={}; var lo=[], li=[];
if(midi){ midi.outputs.forEach(function(o){ outs[o.id]=o; lo.push(o); }); midi.inputs.forEach(function(o){ ins[o.id]=o; li.push(o); }); }
fillSelect('portDrums',lo,/404/i); fillSelect('portMono',lo,/minifreak|freak/i); fillSelect('portIn',li,/404/i);
hookInput();
if(midi) setStatus('MIDI: '+lo.length+' out, '+li.length+' in', lo.length?'ok':'');
}
function hookInput(){
if(inPort) inPort.onmidimessage=null;
inPort=ins[cfg.portIn]||null;
if(inPort) inPort.onmidimessage=function(e){ onMidiIn(e.data); };
}
if(navigator.requestMIDIAccess){
navigator.requestMIDIAccess({sysex:false}).then(function(m){ midi=m; m.onstatechange=fillPorts; fillPorts(); },
function(){ setStatus('MIDI blocked in this view. Open the standalone file in Chrome','bad'); fillPorts(); });
} else { setStatus('No Web MIDI here. Use Chrome or Edge on desktop','bad'); fillPorts(); }
/* Pad learn: LEARN page, push, press a kit track, then hit the pad on the sampler. Stores the exact channel + note received. */
function onMidiIn(d){
if(!d || d.length<3) return;
if((d[0]&0xF0)!==0x90 || d[2]===0) return;
if(app.learn===2 && app.learnTr>=0){
cfg.kit.learn[app.learnTr]={ch:d[0]&0x0F,note:d[1]}; saveCfg(); refreshPadFields();
applyOn(KIT[app.learnTr],true); dirty=true; // a freshly learned track starts on
msg('N'+d[1]+' CH'+((d[0]&15)+1)+' OK',1400);
app.learn=0; app.learnTr=-1; app.editing=false;
}
}
function portFor(lane){ return outs[lane===1?cfg.portMono:cfg.portDrums]||null; }
/* A learned pad plays on exactly the channel and note it was learned with. */
function learned(tr){ var L=cfg.kit.learn[tr]; return L?{ch:L.ch&15,note:L.note&127}:null; }
/* Kit tracks play only once learned; an unlearned track stays silent. */
function chFor(ev){ if(ev.lane===1) return clamp(cfg.mono.ch|0,1,16)-1; var L=learned(ev.tr); return L?L.ch:0; }
function noteFor(ev){ if(ev.lane===1) return clamp(ev.v+12*(cfg.mono.oct|0),0,127); var L=learned(ev.tr); return L?L.note:null; }
function kitChannels(){ var c={}; KIT.forEach(function(t){ var L=learned(t); if(L) c[L.ch]=1; }); return Object.keys(c).map(Number); }
/* ---------- scheduler (look-ahead, timestamped sends) ---------- */
var LOOK=110, S={running:false,anchorTime:0,anchorTick:0,nextBarTick:0,bar:0,nextClock:0,queue:[],sounding:{},timer:null,log:{},mask:0}, flashes=[];
function msPerTick(){ return 60000/(app.bpm*TPB); }
function timeOf(t){ return S.anchorTime+(t-S.anchorTick)*msPerTick(); }
function tickAt(ms){ return S.anchorTick+(ms-S.anchorTime)/msPerTick(); }
function clockPorts(){ var p=[], a=outs[cfg.portDrums], b=outs[cfg.portMono]; if(a) p.push(a); if(b && b!==a) p.push(b); return p; }
function send(port,bytes,ts){ if(!port) return; try{ port.send(bytes,ts); }catch(e){} }
function start(){
if(S.running) return;
S.running=true; S.anchorTime=performance.now()+60; S.anchorTick=0; S.nextBarTick=0; S.bar=0; S.nextClock=0; S.queue=[]; S.sounding={}; S.log={}; S.mask=0;
if(cfg.clock) clockPorts().forEach(function(p){ send(p,[0xFA],S.anchorTime); });
S.timer=setInterval(pump,15); pump(); runBtn.setAttribute('aria-pressed','true');
}
function stop(){
if(!S.running) return;
clearInterval(S.timer); S.running=false;
Object.keys(outs).forEach(function(id){ try{ outs[id].clear(); }catch(e){} });
Object.keys(S.sounding).forEach(function(k){ var s=S.sounding[k]; send(s.port,[0x80|s.ch,s.note,0]); });
var pd=portFor(0), pm=portFor(1);
if(pd) kitChannels().forEach(function(c){ send(pd,[0xB0|c,120,0]); }); // samplers that ignore All Notes Off still obey All Sound Off (120)
if(pm){ var mc=clamp(cfg.mono.ch|0,1,16)-1; send(pm,[0xB0|mc,120,0]); send(pm,[0xB0|mc,123,0]); }
if(cfg.clock) clockPorts().forEach(function(p){ send(p,[0xFC]); });
S.queue=[]; S.sounding={};
for(var t=0;t<NT;t++) if(pending[t]) applyOn(t,pending[t].v);
runBtn.setAttribute('aria-pressed','false'); dirty=true;
}
function pump(){
var now=performance.now(), horizon=now+LOOK, i;
applyPending(tickAt(now));
while(timeOf(S.nextBarTick)<horizon){
var p=params(app.fill), r=E.renderBar(app.state,p,S.bar);
S.log[S.bar]={start:S.nextBarTick,r:r,mask:S.mask}; delete S.log[S.bar-12];
for(i=0;i<r.events.length;i++){ var ev=r.events[i], at=S.nextBarTick+ev.t; S.queue.push({tick:at,on:1,ev:ev,mask:S.mask}); S.queue.push({tick:at+ev.len,on:0,ev:ev}); }
S.queue.sort(function(a,b){ return a.tick-b.tick || a.on-b.on; });
S.nextBarTick+=r.barTicks; S.bar++;
}
if(cfg.clock){ var cp=clockPorts(); while(timeOf(S.nextClock)<horizon){ var ct=timeOf(S.nextClock); cp.forEach(function(p){ send(p,[0xF8],ct); }); S.nextClock+=E.CLOCK_TICKS; } }
else S.nextClock=Math.ceil(tickAt(horizon)/E.CLOCK_TICKS)*E.CLOCK_TICKS;
while(S.queue.length && timeOf(S.queue[0].tick)<horizon){
var q=S.queue.shift(), e=q.ev, ts=Math.max(now,timeOf(q.tick));
applyPending(q.tick);
var port=portFor(e.lane), ch=chFor(e), note=noteFor(e), key=(port?port.id:'-')+':'+ch+':'+note;
if(q.on){ if(!sounds(e.tr,q.mask) || note===null) continue; send(port,[0x90|ch,note,e.vel],ts); S.sounding[key]={port:port,ch:ch,note:note,tr:e.tr,until:q.tick+e.len}; flashes.push({time:ts,tr:e.tr}); }
else { var s=S.sounding[key]; if(s){ send(port,[0x80|ch,note,0],ts); if(s.until<=q.tick) delete S.sounding[key]; } }
}
}
function setBpm(b){ b=clamp(b|0,60,240); if(b===app.bpm) return; if(S.running){ var now=performance.now(), tk=tickAt(now); S.anchorTime=now; S.anchorTick=tk; } app.bpm=b; save(); }
/* ---------- tracking by hand: every track is simply on or off ---------- */
var pending=[]; for(var i1=0;i1<NT;i1++) pending.push(null);
function releaseTrack(tr){ Object.keys(S.sounding).forEach(function(k){ var s=S.sounding[k]; if(s.tr===tr){ send(s.port,[0x80|s.ch,s.note,0]); delete S.sounding[k]; } }); }
function curMask(){ return 0; }
function applyOn(tr,v){ app.on[tr]=v; pending[tr]=null; store('on',app.on); if(!sounds(tr,0)) releaseTrack(tr); }
function applyPending(tick){ for(var t=0;t<NT;t++) if(pending[t] && pending[t].at<=tick) applyOn(t,pending[t].v); }
function targetOn(tr){ return pending[tr]?pending[tr].v:app.on[tr]; }
function setOn(tr,v){
if(S.running && document.getElementById('muteQ').checked){
var now=tickAt(performance.now()), at=S.nextBarTick;
for(var b in S.log){ var L=S.log[b]; if(L.start>now && L.start<at) at=L.start; }
pending[tr] = (v===app.on[tr]) ? null : {v:v,at:at};
} else applyOn(tr,v);
}
function setInv(tr,v){ app.inv[tr]=v; if(!sounds(tr,0)) releaseTrack(tr); }
/* ---------- actions ---------- */
function newSeed(){ setState({seed:rnd32(),salts:[0,0]}); }
function reroll(l){ var s={seed:app.state.seed,salts:app.state.salts.slice()}; s.salts[l]=(s.salts[l]+1)&0xFFFF; setState(s); msg(l?'NEW LINE':'NEW KIT',700); }
function encTurn(d){
if(!app.editing){ app.page=(app.page+d+PAGES.length)%PAGES.length; app.learn=0; app.learnTr=-1; return; }
var pg=PAGES[app.page];
if(pg==='SEED') app.histSel=clamp(app.histSel+d,0,app.history.length);
else if(pg==='GEN'){ app.gen=(app.gen+d+5)%5; save(); dirty=true; }
else if(pg==='ROOT'){ app.root=mod(app.root+1+d,13)-1; save(); dirty=true; } // AUTO, C .. B
else if(pg==='SCALE'){ var ns=E.SCALE_NAMES.length+1; app.scale=mod(app.scale+1+d,ns)-1; save(); dirty=true; } // AUTO, MAJOR .. LOCRIAN
else if(pg==='BPM') setBpm(app.bpm+d);
else if(pg==='SLOT') app.slotSel=(app.slotSel+d+8)%8;
else if(pg==='ROLL') app.rollSel=(app.rollSel+d+2)%2;
}
function encPush(hold){
var pg=PAGES[app.page];
if(pg==='LEARN'){
if(app.learn===0){ app.learn=1; app.editing=true; } // now press a kit track button
else { app.learn=0; app.learnTr=-1; app.editing=false; msg('CANCELLED',700); }
return;
}
if(!app.editing){ app.editing=true; return; }
if(pg==='SEED' && app.histSel>0) setState(app.history[app.histSel-1]);
if(pg==='SLOT'){
if(hold){ app.slots[app.slotSel]={seed:app.state.seed,salts:app.state.salts.slice(),gen:app.gen}; store('slots',app.slots); msg('SAVED S'+(app.slotSel+1)); return; }
var sl=app.slots[app.slotSel]; if(sl){ app.gen=sl.gen|0; setState(sl); }
}
if(pg==='ROLL') reroll(app.rollSel); // then back to page select
app.editing=false;
}
/* ---------- device controls ---------- */
var runBtn=(function(){ var b=document.createElement('button'); b.className='toggle'; b.id='run'; b.type='button'; b.setAttribute('aria-pressed','false'); b.setAttribute('aria-label','Run / stop');
b.innerHTML='<span class="label">Run</span><span class="tg"></span><span class="label">Stop</span>'; return b; })(), big=document.getElementById('big');
runBtn.addEventListener('click',function(){ S.running?stop():start(); });
var seedHeld=false, comboUsed=false;
big.addEventListener('pointerdown',function(e){ if(big.setPointerCapture) big.setPointerCapture(e.pointerId); seedHeld=true; comboUsed=false; big.classList.add('down'); });
big.addEventListener('pointerup',function(){ big.classList.remove('down'); if(seedHeld && !comboUsed) newSeed(); seedHeld=false; });
big.addEventListener('pointercancel',function(){ big.classList.remove('down'); seedHeld=false; });
big.addEventListener('keydown',function(e){ if(e.key==='Enter'){ e.preventDefault(); e.stopPropagation(); newSeed(); } });
/* Track buttons. Tap: on <-> off. Hold past 250 ms: flip while held.
In LEARN a kit button selects the track to learn instead. */
/* Panel grid, same order on the screen map: row 1 KICK KICK2 SNARE [RUN toggle], row 2 HAT HAT16 PERC GHOST, row 3 BASS MEL A MEL B PAD.
-1 = the RUN toggle. In the wiring matrix that slot is empty; GHOST is wired at row 2 / column 4. */
var PANEL=[0,1,2,-1, 4,5,6,3, 7,8,9,10];
var trk=document.getElementById('trk'), tleds=[], tbtns=[], held={};
function addCell(name,onDown,onUp){
var c=document.createElement('div'); c.className='tcell';
var led=document.createElement('span'); led.className='tled';
var b=document.createElement('button'); b.className='tbtn'; b.type='button'; b.setAttribute('aria-label',name);
var n=document.createElement('span'); n.className='tname'; n.textContent=name;
c.appendChild(led); c.appendChild(b); c.appendChild(n); trk.appendChild(c);
b.addEventListener('pointerdown',function(e){ if(b.setPointerCapture) b.setPointerCapture(e.pointerId); b.classList.add('down'); onDown(e); });
b.addEventListener('pointerup',function(){ b.classList.remove('down'); onUp(); });
b.addEventListener('pointercancel',function(){ b.classList.remove('down'); onUp(); });
b.addEventListener('keydown',function(e){ if((e.key==='Enter'||e.key===' ') && !e.repeat){ e.preventDefault(); e.stopPropagation(); b.classList.add('down'); onDown(e); } });
b.addEventListener('keyup',function(e){ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); b.classList.remove('down'); onUp(); } });
return {led:led,btn:b};
}
function trackDown(t,e){
var pg=PAGES[app.page];
if(app.learn===1 || app.learn===2){ if(E.TRACKS[t].lane!==0){ msg('KIT ONLY',700); return; } app.learnTr=KIT.indexOf(t); app.learn=2; return; }
held[t]={t0:performance.now(),timer:setTimeout(function(){ if(held[t]){ held[t].long=true; setInv(t,true); } },HOLD_MS)};
}
function trackUp(t){
var h=held[t]; if(!h) return; clearTimeout(h.timer); delete held[t];
if(h.long) setInv(t,false);
else setOn(t,!targetOn(t));
}
PANEL.forEach(function(t){
if(t<0){ var rc=document.createElement('div'); rc.className='tcell runcell'; rc.appendChild(runBtn); trk.appendChild(rc); return; } // the RUN toggle sits in the grid, outside the matrix
var c=addCell(E.TRACKS[t].name,function(e){ trackDown(t,e); },function(){ trackUp(t); });
tleds[t]=c.led; tbtns[t]=c.btn;
});
function knob(el,valEl,get,set){
function draw(){ var v=get(); el.style.setProperty('--rot',(-135+270*v/127)+'deg'); el.setAttribute('aria-valuenow',v); valEl.textContent=v; }
var y0=0,v0=0;
el.addEventListener('pointerdown',function(e){ el.setPointerCapture(e.pointerId); y0=e.clientY; v0=get(); });
el.addEventListener('pointermove',function(e){ if(!el.hasPointerCapture(e.pointerId)) return; set(clamp(Math.round(v0+(y0-e.clientY)/1.5),0,127)); draw(); });
el.addEventListener('wheel',function(e){ e.preventDefault(); set(clamp(get()+(e.deltaY<0?2:-2),0,127)); draw(); },{passive:false});
el.addEventListener('keydown',function(e){ var d=(e.key==='ArrowUp'||e.key==='ArrowRight')?1:(e.key==='ArrowDown'||e.key==='ArrowLeft')?-1:0; if(d){ e.preventDefault(); e.stopPropagation(); set(clamp(get()+d*(e.shiftKey?8:2),0,127)); draw(); } });
draw();
}
knob(document.getElementById('kEnergy'),document.getElementById('vEnergy'),function(){return app.energy;},function(v){ app.energy=v; save(); dirty=true; });
knob(document.getElementById('kDrift'),document.getElementById('vDrift'),function(){return app.drift;},function(v){ app.drift=v; save(); dirty=true; });
var enc=document.getElementById('enc'), encDot=enc.querySelector('i'), encRot=0;
function encStep(d){ encRot+=d*18; encDot.style.setProperty('--rot',encRot+'deg'); encTurn(d); }
(function(){
var x0=0, acc=0, moved=false, t0=0;
enc.addEventListener('pointerdown',function(e){ enc.setPointerCapture(e.pointerId); x0=e.clientX; acc=0; moved=false; t0=performance.now(); });
enc.addEventListener('pointermove',function(e){ if(!enc.hasPointerCapture(e.pointerId)) return; acc+=e.clientX-x0; x0=e.clientX; while(Math.abs(acc)>=14){ moved=true; var d=acc>0?1:-1; acc-=d*14; encStep(d); } });
enc.addEventListener('pointerup',function(){ if(!moved) encPush(performance.now()-t0>600); });
enc.addEventListener('wheel',function(e){ e.preventDefault(); encStep(e.deltaY>0?1:-1); },{passive:false});
})();
var KEYMAP=['Digit1','Digit2','Digit3','Digit4','Digit5','Digit6','Digit7','Digit8','Digit9','Digit0','Minus'];
document.addEventListener('keydown',function(e){
var tg=e.target.tagName; if(tg==='INPUT'||tg==='SELECT'||tg==='TEXTAREA'||tg==='BUTTON' && (e.key==='Enter'||e.key===' ')) return;
if(e.repeat && e.key!=='ArrowLeft' && e.key!=='ArrowRight') return;
var t=KEYMAP.indexOf(e.code);
if(t>=0){ tbtns[t].classList.add('down'); trackDown(t,e); }
else if(e.key===' '){ e.preventDefault(); big.classList.add('down'); newSeed(); setTimeout(function(){ big.classList.remove('down'); },90); }
else if(e.key==='ArrowLeft'){ e.preventDefault(); encStep(-1); }
else if(e.key==='ArrowRight'){ e.preventDefault(); encStep(1); }
else if(e.key==='Enter'){ e.preventDefault(); encPush(e.shiftKey); }
else if(e.key==='r'||e.key==='R'){ S.running?stop():start(); }
});
document.addEventListener('keyup',function(e){
var t=KEYMAP.indexOf(e.code);
if(t>=0){ tbtns[t].classList.remove('down'); trackUp(t); }
});
/* ---------- bench settings ---------- */
function bind(id,get,set){ var el=document.getElementById(id); el.value=get(); el.addEventListener('change',function(){ set(el.value); saveCfg(); dirty=true; }); return el; }
bind('monoCh',function(){return cfg.mono.ch;},function(v){cfg.mono.ch=clamp(v|0,1,16);});
bind('monoOct',function(){return cfg.mono.oct;},function(v){cfg.mono.oct=clamp(v|0,-3,3);});
['portDrums','portMono'].forEach(function(id){ document.getElementById(id).addEventListener('change',function(e){ cfg[id]=e.target.value; saveCfg(); }); });
document.getElementById('portIn').addEventListener('change',function(e){ cfg.portIn=e.target.value; saveCfg(); hookInput(); });
function applyHw(){
trk.classList.toggle('noled',!cfg.hw.leds);
var D=DISPLAYS[cfg.hw.display]||DISPLAYS['5110'], w=document.getElementById('hwNote');
w.textContent = D.h===32 ? (cfg.hw.leds ? 'This screen only fits the menu (two big lines). Track status is on the LEDs.' : 'This screen only fits the menu. Without LEDs there is no way to see which tracks are on.')
: 'Menu on top, a 4 × 3 track map underneath laid out like the buttons, so the LEDs are optional.';
}
bind('hwDisplay',function(){ return cfg.hw.display; },function(v){ cfg.hw.display=DISPLAYS[v]?v:'5110'; applyHw(); });
(function(){ var el=document.getElementById('hwLeds'); el.checked=!!cfg.hw.leds; el.addEventListener('change',function(){ cfg.hw.leds=el.checked; saveCfg(); applyHw(); }); })();
var clk=document.getElementById('sendClock'); clk.checked=!!cfg.clock; clk.addEventListener('change',function(){ cfg.clock=clk.checked; saveCfg(); });
var padNotes=[];
(function(){ var box=document.getElementById('padFields');
KIT.forEach(function(t,k){
var f=document.createElement('div'); f.className='field';
var lab=document.createElement('span'); lab.className='lbl'; lab.textContent=E.TRACKS[t].name;
var nt=document.createElement('span'); nt.className='val';
f.appendChild(lab); f.appendChild(nt); box.appendChild(f); padNotes[k]=nt;
}); })();
function refreshPadFields(){
KIT.forEach(function(t,k){ var L=learned(t); padNotes[k].textContent = L ? 'ch '+(L.ch+1)+' · note '+L.note : 'not learned'; padNotes[k].classList.toggle('miss',!L); });
}
document.getElementById('clearLearn').addEventListener('click',function(){ cfg.kit.learn=[null,null,null,null,null,null,null]; saveCfg(); refreshPadFields(); });
refreshPadFields();
document.getElementById('vecGo').addEventListener('click',function(){
var n=clamp(document.getElementById('vecBars').value|0,1,64), rows=[], bars=[], p=params(false), mask=0;
for(var b=0;b<n;b++){
var r=E.renderBar(app.state,p,b);
var ev=r.events.map(function(e){ return [e.t,e.lane,e.v,e.vel,e.len,e.tr]; });
bars.push({bar:b,fill:r.fill,events:ev}); rows=rows.concat(ev);
}
document.getElementById('vecOut').value=JSON.stringify({seed:'0x'+hex(app.state.seed),salts:app.state.salts,params:{gen:app.gen,energy:app.energy,drift:app.drift},
checksum:'0x'+hex(E.checksum(rows)),format:'[tick,lane,voice,vel,len,track]; all tracks are listed, on/off is applied at output',bars:bars});
});
document.getElementById('vecCopy').addEventListener('click',function(){
var ta=document.getElementById('vecOut'), b=this;
function ok(){ b.textContent='Copied'; setTimeout(function(){ b.textContent='Copy'; },1200); }
try{ navigator.clipboard.writeText(ta.value).then(ok,function(){ ta.select(); }); }catch(e){ ta.select(); }
});
/* ---------- Screen: 1-bit framebuffer, 5x7 font, laid out for the real panel ----------
0.91" 128x32: two big lines (label, value). No room for track status: that needs LEDs.
128x64 and Nokia 84x48: small label + value on top, a track map below that mirrors the button rows. */
var DISPLAYS={
'5110':{w:84,h:48,wmm:33.1,hmm:18.9,lcd:true,name:'Nokia 3310/5110 84×48 PCD8544'},
'091':{w:128,h:32,wmm:22.38,hmm:5.58,name:'0.91" 128×32 SSD1306'},
'096':{w:128,h:64,wmm:21.74,hmm:10.86,name:'0.96" 128×64 SSD1306'},
'130':{w:128,h:64,wmm:29.42,hmm:14.70,name:'1.3" 128×64 SH1106'},
'242':{w:128,h:64,wmm:55.01,hmm:27.49,name:'2.42" 128×64 SSD1309'}
};
var FONT={' ':[0,0,0,0,0],'+':[8,8,62,8,8],'-':[8,8,8,8,8],'.':[0,96,96,0,0],'/':[32,16,8,4,2],':':[0,54,54,0,0],'#':[20,127,20,127,20],'*':[20,8,62,8,20],'>':[0,65,34,20,8],'?':[2,1,81,9,6],'!':[0,0,95,0,0],
'0':[62,81,73,69,62],'1':[0,66,127,64,0],'2':[66,97,81,73,70],'3':[33,65,69,75,49],'4':[24,20,18,127,16],'5':[39,69,69,69,57],'6':[60,74,73,73,48],'7':[1,113,9,5,3],'8':[54,73,73,73,54],'9':[6,73,73,41,30],
'A':[126,17,17,17,126],'B':[127,73,73,73,54],'C':[62,65,65,65,34],'D':[127,65,65,34,28],'E':[127,73,73,73,65],'F':[127,9,9,9,1],'G':[62,65,73,73,122],'H':[127,8,8,8,127],'I':[0,65,127,65,0],'J':[32,64,65,63,1],
'K':[127,8,20,34,65],'L':[127,64,64,64,64],'M':[127,2,12,2,127],'N':[127,4,8,16,127],'O':[62,65,65,65,62],'P':[127,9,9,9,6],'Q':[62,65,81,33,94],'R':[127,9,25,41,70],'S':[70,73,73,73,49],'T':[1,1,127,1,1],
'U':[63,64,64,64,63],'V':[31,32,64,32,31],'W':[63,64,56,64,63],'X':[99,20,8,20,99],'Y':[7,8,112,8,7],'Z':[97,81,73,69,67]};
var oled=document.getElementById('oled'), octx=oled.getContext('2d'), zoom=document.getElementById('oledZoom'), zctx=zoom.getContext('2d');
var FB_W=128, fb=new Uint8Array(128*64), fbH=32;
function fitText(s,x,y,maxW,v){ if(textW(s,2)<=maxW) text(s,x,y,2,v); else text(s,x,y+4,1,v); }
function px(x,y,v){ if(x>=0&&x<FB_W&&y>=0&&y<fbH) fb[y*FB_W+x]=v; }
function fillR(x,y,w,h,v){ for(var j=y;j<y+h;j++) for(var i=x;i<x+w;i++) px(i,j,v); }
function boxR(x,y,w,h){ fillR(x,y,w,1,1); fillR(x,y+h-1,w,1,1); fillR(x,y,1,h,1); fillR(x+w-1,y,1,h,1); }
function text(s,x,y,sc,v){
s=String(s).toUpperCase();
for(var c=0;c<s.length;c++){ var g=FONT[s[c]]||FONT['?'];
for(var col=0;col<5;col++) for(var row=0;row<7;row++) if((g[col]>>row)&1) fillR(x+col*sc,y+row*sc,sc,sc,v);
x+=(sc===1?6:12); }
}
function textW(s,sc){ return String(s).length*(sc===1?6:12)-(sc===1?1:2); }
function screenText(){
var pg=PAGES[app.page], label='', value='';
if(pg==='SEED'){ var sh=app.histSel>0?app.history[app.histSel-1]:app.state; label='SEED'+(app.histSel>0?' -'+app.histSel:'')+((sh.salts[0]||sh.salts[1])?' *':''); value=hex(sh.seed); }
else if(pg==='GEN'){ label='GENERATOR'; value=E.GEN_NAMES[app.gen]; }
else if(pg==='ROOT' || pg==='SCALE'){ // on AUTO, show the seed's own pick
var sc=pg==='SCALE', x=sc?app.scale:app.root, gk=E.genome(app.state.seed);
label=(sc?'SCALE':'ROOT')+(x<0?' AUTO':''); if(x<0) x=sc?gk.scale:gk.root; value=sc?E.SCALE_NAMES[x]:E.NOTE_NAMES[x]; }
else if(pg==='BPM'){ label='TEMPO'; value=app.bpm+' BPM'; }
else if(pg==='SLOT'){ label='SLOT '+(app.slotSel+1); value=app.slots[app.slotSel]?hex(app.slots[app.slotSel].seed):'EMPTY'; }
else if(pg==='ROLL'){ label='RE-ROLL'; value=app.rollSel?'LINE':'KIT'; }
else if(pg==='LEARN'){ label= app.learn===2 ? 'LEARN '+E.TRACKS[KIT[app.learnTr]].name.replace(' ','') : 'LEARN';
value= app.learn===0 ? 'PUSH' : app.learn===1 ? 'PICK TRACK' : (inPort?'HIT PAD':'NO INPUT'); }
if(performance.now()<flashMsgUntil) value=flashMsg;
return {label:label.slice(0,10),value:String(value).slice(0,10)};
}
function drawCell(x,y,w,h,state){ // state: 0 off, 1 on, 2 on+hit, 3 selectable, 4 selected
if(state===1||state===2||state===4) fillR(x,y,w,h,1); else boxR(x,y,w,h);
if(state===2) fillR(x+3,y+3,w-6,h-6,0);
if(state===3) fillR(x+(w>>1)-1,y+(h>>1)-1,2,2,1);
}
function cellState(t,now,mask,blink){
if(app.learn>0) return E.TRACKS[t].lane!==0 ? 0 : (app.learn===2 && KIT[app.learnTr]===t) ? (blink?4:3) : 3;
if(E.TRACKS[t].lane===0 && !cfg.kit.learn[t]) return 3; // a dot: this kit track still needs learning
var on=sounds(t,mask); if(pending[t] && blink) on=!on;
return on ? (now<hitUntil[t]?2:1) : 0;
}
var lastDisp='';
function drawOled(curBar,mask){
var D=DISPLAYS[cfg.hw.display]||DISPLAYS['5110'], now=performance.now(), blink=Math.floor(now/250)%2===0, s=screenText(), i, t;
if(lastDisp!==cfg.hw.display){ lastDisp=cfg.hw.display; FB_W=D.w; fbH=D.h; oled.width=D.w; oled.height=D.h; zoom.width=D.w; zoom.height=D.h;
oled.style.width=D.wmm+'mm'; oled.style.height=D.hmm+'mm'; zoom.style.aspectRatio=D.w+' / '+D.h;
oled.parentNode.classList.toggle('lcd',!!D.lcd); zoom.classList.toggle('lcd',!!D.lcd); }
fb.fill(0);
function drawMap(x0,y0,cw,ch,gx,gy){ // 4 x 3, same as the buttons and the wiring matrix
for(var k=0;k<12;k++){ var t=PANEL[k], x=x0+(k%4)*(cw+gx), y=y0+((k/4)|0)*(ch+gy);
drawCell(x,y,cw,ch, t<0 ? (S.running?1:0) : cellState(t,now,mask,blink)); } }
if(D.lcd){
// Nokia 84x48: 14 chars per line at 1x. Label + bar dots, value (2x when it fits), then the 7+4 track map with 10 px cells.
text(s.label,0,0,1,1);
if(S.running && curBar>=0) for(i=0;i<4;i++){ var nx=64+i*5; if(i===curBar%4) fillR(nx,1,4,5,1); else boxR(nx,1,4,5); }
if(app.editing){ fillR(0,8,84,10,1); text(s.value,1,9,1,0); } else text(s.value,1,9,1,1);
drawMap(1,20,19,8,2,2);
} else if(D.h===32){
text(s.label,2,0,2,1);
if(app.editing){ fillR(0,16,128,16,1); text(s.value,2,17,2,0); } else text(s.value,2,17,2,1);
} else {
text(s.label,1,0,1,1);
if(S.running && curBar>=0) for(i=0;i<4;i++){ var bx=104+i*6; if(i===curBar%4) fillR(bx,1,5,5,1); else boxR(bx,1,5,5); }
if(app.editing){ fillR(0,9,128,18,1); text(s.value,2,11,2,0); } else text(s.value,2,11,2,1);
drawMap(1,29,30,10,2,2);
}
var img=octx.createImageData(FB_W,fbH), d=img.data, ON=D.lcd?[28,40,38]:[214,236,255], OFF=D.lcd?[150,176,160]:[3,6,10];
for(i=0;i<FB_W*fbH;i++){ var c=fb[i]?ON:OFF; d[i*4]=c[0]; d[i*4+1]=c[1]; d[i*4+2]=c[2]; d[i*4+3]=255; }
octx.putImageData(img,0,0); zctx.putImageData(img,0,0);
}
/* ---------- LED strip ---------- */
var hitUntil=[]; for(var i2=0;i2<NT;i2++) hitUntil.push(0);
function rgba(c,a){ return 'color-mix(in srgb, '+c+' '+Math.round(a*100)+'%, transparent)'; }
function drawLeds(now,mask,T){
var blink=Math.floor(now/250)%2===0, slow=Math.floor(now/450)%2===0;
for(var t=0;t<NT;t++){
var el=tleds[t], on=sounds(t,mask), lane=E.TRACKS[t].lane?T.mono:T.kit, col, glow=false;
if(app.learn>0){
if(E.TRACKS[t].lane!==0) col='var(--bezel)';
else if(app.learn===2 && KIT[app.learnTr]===t){ col=blink?'#FFB547':'var(--bezel)'; glow=blink; }
else { col=slow?'#FFB547':'var(--bezel)'; }
} else if(E.TRACKS[t].lane===0 && !cfg.kit.learn[t]){
col=rgba(lane,0.15);
} else {
if(on){ col=now<hitUntil[t]?lane:rgba(lane,0.7); glow=now<hitUntil[t]; }
else col='var(--bezel)';
if(pending[t] && !blink) col='var(--bezel)';
}
el.style.background=col; el.style.boxShadow=glow?'0 0 10px '+col:'inset 0 1px 1px rgba(0,0,0,.4)';
}
}
/* ---------- phrase view: one row per track ---------- */
var grid=document.getElementById('grid'), gctx=grid.getContext('2d'), dirty=true, view=null, viewKey='';
function tokens(){ var cs=getComputedStyle(document.documentElement); function v(n){ return cs.getPropertyValue(n).trim(); }
return {ink:v('--ink'),muted:v('--muted'),line:v('--line'),grid:v('--grid'),kit:v('--l-kit'),mono:v('--l-mono'),accent:v('--accent'),font:v('--f-mono')}; }
function phraseBars(phrase){ var out=[]; for(var i=0;i<4;i++){ var b=phrase*4+i; out.push(S.running && S.log[b] ? S.log[b].r : E.renderBar(app.state,params(false),b)); } return out; }
function drawGrid(curTick,mask,T){
var dpr=window.devicePixelRatio||1, W=grid.clientWidth, H=grid.clientHeight;
if(grid.width!==Math.round(W*dpr)||grid.height!==Math.round(H*dpr)){ grid.width=Math.round(W*dpr); grid.height=Math.round(H*dpr); }
gctx.setTransform(dpr,0,0,dpr,0,0); gctx.clearRect(0,0,W,H);
var bars=view.bars, total=0; bars.forEach(function(b){ total+=b.barTicks; });
var X0=58, GW=W-X0-4, rows=[], y=4, i;
for(i=0;i<NT;i++) rows.push({h:(i>=7?1.4:1)});
var unit=(H-24)/rows.reduce(function(a,r){ return a+r.h; },0);
rows.forEach(function(r){ r.y=y; r.hh=r.h*unit; y+=r.hh; });
var gb=y;
gctx.font='500 10px '+T.font; gctx.textBaseline='middle';
for(i=0;i<NT;i++){ var r=rows[i], on=sounds(i,mask);
gctx.fillStyle=on?T.ink:T.line; gctx.fillText(E.TRACKS[i].name,2,r.y+r.hh/2);
gctx.strokeStyle=(i===6)?T.line:T.grid; gctx.lineWidth=1; gctx.beginPath(); gctx.moveTo(X0,Math.round(r.y+r.hh)+.5); gctx.lineTo(X0+GW,Math.round(r.y+r.hh)+.5); gctx.stroke(); }
function X(t){ return X0+t/total*GW; }
var acc=0;
bars.forEach(function(b){
for(var t=0;t<b.barTicks;t+=E.TPS){ gctx.strokeStyle=(t===0)?T.muted:(t%TPB===0)?T.line:T.grid; gctx.lineWidth=(t===0)?1.5:1;
var xx=Math.round(X(acc+t))+.5; gctx.beginPath(); gctx.moveTo(xx,4); gctx.lineTo(xx,gb); gctx.stroke(); }
gctx.fillStyle=T.muted; gctx.textBaseline='top'; gctx.fillText(String.fromCharCode(65+b.sec)+(b.fill?' fill':''),X(acc)+4,gb+6); gctx.textBaseline='middle';
acc+=b.barTicks;
});
var rng={}; bars.forEach(function(b){ b.events.forEach(function(e){ if(e.lane===1){ var q=rng[e.tr]||(rng[e.tr]={lo:127,hi:0}); q.lo=Math.min(q.lo,e.v); q.hi=Math.max(q.hi,e.v); } }); });
acc=0;
bars.forEach(function(b){
b.events.forEach(function(e){
var r=rows[e.tr], x=X(acc+e.t), yy, hh, w, on=sounds(e.tr,mask);
if(e.lane===0){ hh=r.hh-6; yy=r.y+3; w=3; }
else { var q=rng[e.tr], span=Math.max(7,q.hi-q.lo); hh=3; yy=r.y+r.hh-5-((e.v-q.lo)/span)*(r.hh-8); w=Math.max(3,Math.min((e.len/total)*GW,X0+GW-x)); }
gctx.globalAlpha=on?0.3+0.7*e.vel/127:0.12; gctx.fillStyle=e.lane?T.mono:T.kit; gctx.fillRect(x,yy,w,hh);
});
acc+=b.barTicks;
});
gctx.globalAlpha=1;
if(curTick>=0){ var px=Math.round(X(curTick))+.5; gctx.strokeStyle=T.accent; gctx.lineWidth=2; gctx.beginPath(); gctx.moveTo(px,2); gctx.lineTo(px,gb); gctx.stroke(); }
}
var factsEl=document.getElementById('facts'), posEl=document.getElementById('pos');
function frame(){
var now=performance.now(), curBar=-1, curTick=-1, phrase=0, mask, T=tokens();
if(S.running){
var tk=tickAt(now);
for(var b in S.log){ var L=S.log[b]; if(tk>=L.start && tk<L.start+L.r.barTicks){ curBar=b|0; phrase=L.r.phrase; break; } }
if(curBar>=0){ var first=S.log[phrase*4]; if(first) curTick=tk-first.start; }
}
mask=curMask();
var key=phrase+'|'+app.state.seed+'|'+app.state.salts.join('.')+'|'+app.gen+'|'+S.running;
if(dirty || key!==viewKey || (S.running && view && view.bars.some(function(b,i){ return S.log[phrase*4+i] && S.log[phrase*4+i].r!==b; }))){
view={bars:phraseBars(phrase)}; viewKey=key; dirty=false;
var g=E.applyKey(E.genome(app.state.seed),params(false)), ds=E.describe(app.state,params(false));
factsEl.innerHTML='<span>Seed <b>'+hex(app.state.seed)+'</b></span><span>Generator <b>'+E.GEN_NAMES[app.gen].toLowerCase()+'</b></span><span>Bar <b>'+(app.gen===4?16:g.steps)+' steps</b></span><span>Key <b>'+E.NOTE_NAMES[g.root]+' '+E.SCALE_NAMES[g.scale].toLowerCase()+(app.scale<0||app.root<0?' (auto)':'')+'</b></span>'+(ds?'<span><b>'+ds+'</b></span>':'');
}
var on=0; for(var t=0;t<NT;t++) if(sounds(t,mask)) on++;
posEl.innerHTML=(S.running && curBar>=0 ? '<span>Phrase <b>'+(phrase+1)+'</b></span><span>Bar <b>'+(curBar%4+1)+'/4</b></span>' : '<span>Stopped</span>')+
'<span>Tracks on <b>'+on+'/'+NT+'</b></span><span><b>'+app.bpm+'</b> BPM</span>';
while(flashes.length && flashes[0].time<=now){ hitUntil[flashes[0].tr]=now+80; flashes.shift(); }
drawLeds(now,mask,T); drawGrid(curTick,mask,T); drawOled(curBar,mask);
requestAnimationFrame(frame);
}
applyHw();
requestAnimationFrame(frame);
window.addEventListener('beforeunload',function(){ stop(); });
})();