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>
485 lines
34 KiB
JavaScript
485 lines
34 KiB
JavaScript
/* ================= 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(); });
|
||
})();
|