Split kit and keys; KEYS GEN with MOTIF, 303, TURING, ARP
Some checks failed
check / check (push) Has been cancelled

- genome(seed, salts) now has three parts with their own RNG streams:
  shared (steps, form, swing, arc, layback, fill odds, phase offset),
  kit (density, frame, accent cycle, free snare) and keys (scale/root,
  progression, motif, hocket, bass tone, note lengths, range). ROLL KEYS
  now renews the harmony too; ROLL KIT leaves the keys alone.
- Keys generators share one signature and can fix note lengths:
  MOTIF (the old line), 303 (acid bass, answers in the rests, drone or
  silent pad), TURING (8/16-bit shift register, DRIFT flips bits, 16-bar
  window), ARP (triad/7th arpeggio, roots on kicks).
- Pages: KIT GEN, KEYS GEN; ROLL shows KIT / KEYS; slots keep both gens.
- Flash record v3 (old settings ignored). Bring-up golden checksums updated.
- C port matches; host test and ASan/UBSan pass.

Intended music change: every seed sounds different (genome split, new
generators). Vectors regenerated with npm run vectors; the case grid now
crosses all kit and keys generators, with TURING runs past its window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 21:50:41 +03:00
parent c2c8a55642
commit 346c820b10
26 changed files with 64270 additions and 42622 deletions

View File

@@ -18,16 +18,17 @@
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),
var app={ state:load('state',null)||{seed:0x5EEDB0C5,salts:[0,0]}, gen:load('gen',0), keys:load('keys',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.keys>=0 && app.keys<E.KEYS_NAMES.length)) app.keys=0;
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); }
var PAGES=['SEED','GEN','KEYS','ROOT','SCALE','BPM','SLOT','ROLL','LEARN'];
function params(fillNow){ return {gen:app.gen,keys:app.keys,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('keys',app.keys); 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;
@@ -150,12 +151,13 @@
/* ---------- 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 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 KEYS':'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==='KEYS'){ app.keys=mod(app.keys+d,E.KEYS_NAMES.length); 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);
@@ -172,8 +174,8 @@
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(hold){ app.slots[app.slotSel]={seed:app.state.seed,salts:app.state.salts.slice(),gen:app.gen,keys:app.keys}; store('slots',app.slots); msg('SAVED S'+(app.slotSel+1)); return; }
var sl=app.slots[app.slotSel]; if(sl){ app.gen=sl.gen|0; app.keys=(sl.keys|0)%E.KEYS_NAMES.length; save(); setState(sl); }
}
if(pg==='ROLL') reroll(app.rollSel); // then back to page select
app.editing=false;
@@ -299,7 +301,7 @@
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},
document.getElementById('vecOut').value=JSON.stringify({seed:'0x'+hex(app.state.seed),salts:app.state.salts,params:{gen:app.gen,keys:app.keys,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(){
@@ -339,13 +341,14 @@
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==='GEN'){ label='KIT GEN'; value=E.GEN_NAMES[app.gen]; }
else if(pg==='KEYS'){ label='KEYS GEN'; value=E.KEYS_NAMES[app.keys]; }
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);
var sc=pg==='SCALE', x=sc?app.scale:app.root, gk=E.genome(app.state.seed,app.state.salts);
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==='ROLL'){ label='RE-ROLL'; value=app.rollSel?'KEYS':'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;
@@ -465,11 +468,11 @@
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;
var key=phrase+'|'+app.state.seed+'|'+app.state.salts.join('.')+'|'+app.gen+'|'+app.keys+'|'+app.scale+'|'+app.root+'|'+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 g=E.applyKey(E.genome(app.state.seed,app.state.salts),params(false)), ds=E.describe(app.state,params(false));
factsEl.innerHTML='<span>Seed <b>'+hex(app.state.seed)+'</b></span><span>Kit <b>'+E.GEN_NAMES[app.gen].toLowerCase()+'</b></span><span>Keys <b>'+E.KEYS_NAMES[app.keys].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>')+