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>')+

View File

@@ -91,27 +91,36 @@ var Engine = (function(){
var RULES=[30,45,54,57,60,73,90,105,110,122,126,150,182,225];
var POLY_N=[5,7,9,11,12,6];
function genome(seed){
var r=new Rng(mix(seed,0x47454E)), fi, g={};
/* Three parts, each from its own stream: shared (the bar and the song shape) comes from the seed alone;
kit and keys also take their half's salt, so ROLL KIT / ROLL KEYS renew just that half. */
function genome(seed,salts){
var sl=salts||[0,0], r=new Rng(mix(seed,0x47454E)), fi, g={};
// shared
g.steps=STEPS[r.pickW(STEPS_W)];
fi=r.pickW(FORMS_W); g.form=FORMS[fi]; g.formName=FORM_NAMES[fi];
g.scale=r.below(SCALES.length); g.root=r.below(12);
g.dens=[40+r.below(60),30+r.below(70)];
g.fillP=40+r.below(80); g.monoLen=r.below(3); g.monoRange=1+r.below(2);
g.fillP=40+r.below(80);
g.rot=r.pickW([4,2,2,1]); // phase offset (16ths) of the ambiguous tracks: melody, perc
g.hocket=HOCKETS[r.below(HOCKETS.length)];
g.bassDeg=[4,2,3][r.below(3)];
// musical layer
g.prog=PROGS[r.below(PROGS.length)];
g.swing=SWINGS[r.pickW(SWING_W)];
g.arc=r.pickW([3,2,2]);
g.motifLen=3+r.below(3);
g.accentCycle=[0,3,5,3,7][r.pickW([3,3,2,2,1])];
g.layback=r.below(2);
g.frame=r.pickW(FRAME_W); g.freeK=2+r.below(3); g.freeRot=r.below(16);
// kit
var k=new Rng(mix(roleSeedOf(seed,sl,0),0x4B4954));
g.dens=[40+k.below(60),0];
g.accentCycle=[0,3,5,3,7][k.pickW([3,3,2,2,1])];
g.frame=k.pickW(FRAME_W); g.freeK=2+k.below(3); g.freeRot=k.below(16);
// keys
var l=new Rng(mix(roleSeedOf(seed,sl,1),0x4B4559));
g.scale=l.below(SCALES.length); g.root=l.below(12);
g.dens[1]=30+l.below(70);
g.monoLen=l.below(3); g.monoRange=1+l.below(2);
g.hocket=HOCKETS[l.below(HOCKETS.length)];
g.bassDeg=[4,2,3][l.below(3)];
g.prog=PROGS[l.below(PROGS.length)];
g.motifLen=3+l.below(3);
return g;
}
function roleSeed(st,r){ return mix(mix(st.seed,0x524F4C45+r),st.salts[r]); }
function roleSeedOf(seed,salts,r){ return mix(mix(seed,0x524F4C45+r),salts[r]); }
function roleSeed(st,r){ return roleSeedOf(st.seed,st.salts,r); }
function groups(steps){ var g=[],s; for(s=0;s<steps;s+=4) g.push([s,Math.min(4,steps-s)]); return g; }
function isBack(gi){ return (gi%2)===1; }
function eu(i,k,n,rot){ var j=(i+rot)%n; return ((j*k)%n)<k; } // Bresenham Euclidean
@@ -363,7 +372,9 @@ var Engine = (function(){
for(i=1;i<n;i++){ var stp=[-2,-1,1,2,-4,4][r.pickW([3,4,4,3,1,1])]; cont.push(cont[i-1]+stp); }
return {pos:pos,cont:cont,resp:r.below(3)}; // resp: how the second half answers (0 sequence up, 1 inversion, 2 displaced)
}
function lineTracks(rs,variant,g,steps,d,sec,pos,kit,fill){
function bassPitch(g,idx){ var p=degPitch(g,36,idx); while(p>47) p-=12; while(p<36) p+=12; return p; }
/* MOTIF: a motif stated on the bar's chord and answered, bass on the kicks with a colour tone, the triad on the pad. */
function keysMotif(rs,variant,g,steps,d,sec,pos,kit,fill){
var core=new Rng(mix(rs,0x4D4F+sec)), vr=new Rng(mix(mix(rs,0x5641+sec),variant));
var n=SCALES[g.scale].length, barT=steps*TPS, mel=[], bass=[], pad=[], i, j;
var gs=groups(steps), last=gs[gs.length-1], kickList=[], M=makeMotif(rs,g), c=chordDeg(g,pos);
@@ -399,7 +410,7 @@ var Engine = (function(){
mel=oneAtATime(mel);
for(i=0;i<mel.length;i++) mel[i].tr=((g.hocket>>(i&7))&1)?T_MELA:T_MELB;
// bass: chord root on the kicks, the seed's colour tone on alternate kicks, an approach note into the next bar
function bp(idx){ var p=degPitch(g,36,idx); while(p>47) p-=12; while(p<36) p+=12; return p; }
function bp(idx){ return bassPitch(g,idx); }
var nextC=chordDeg(g,pos+1);
for(i=0;i<kickList.length;i++){
var t=kickList[i], idx=c;
@@ -413,6 +424,91 @@ var Engine = (function(){
return {mel:mel,bass:oneAtATime(bass),pad:pad};
}
/* ---------- keys generators. Each gets (keys seed, phrase variant, genome, steps, keys density, section,
bar position, the bar's kit, fill, {bar, drift}) and returns {mel, bass, pad} sorted by time.
An event's optional len overrides the default note-length rule. ---------- */
function kickTimes(kit){ var a=[]; for(var i=0;i<kit.length;i++) if(kit[i].v===0 && kit[i].t%TPS===0) a.push(kit[i].t); return a; }
function padChord(g,tones){ var pad=[], seen={}; for(var i=0;i<tones.length;i++){ var p=48+mod(degPitch(g,0,tones[i]),12); if(!seen[p]){ seen[p]=1; pad.push({t:0,v:p,vel:i===0?58:50,tr:T_PAD}); } } return pad; }
/* 303: a 16-step acid line (gate rank, degree, octave, accent, slide per step) looped over the bar and moved to
each bar's chord root. ENERGY opens more gates; a DRIFT variant flips flags. Short answers fill the bass's rests;
the pad is silent or a drone on the key's root. Slides are overlaps: whether they glide is up to the synth. */
function keys303(rs,variant,g,steps,d,sec,pos,kit,fill,x){
var r=new Rng(mix(rs,0x333033)), c=chordDeg(g,pos), pat=[], i, s;
for(i=0;i<16;i++){
var rank=0; if(i>0) rank=r.below(128);
var off=[0,2,4,6][r.pickW([10,2,3,1])];
var up=r.chance(P(24)), acc=r.chance(P(40)), sl=r.chance(P(28));
pat.push({rank:rank,off:off,up:up,acc:acc,sl:sl});
}
var drone=r.chance(P(64));
if(variant){ var vr=new Rng(mix(mix(rs,0x5641+sec),variant));
for(i=0;i<16;i++){ if(vr.chance(P(20))) pat[i].acc=!pat[i].acc; if(vr.chance(P(16))) pat[i].sl=!pat[i].sl; if(vr.chance(P(16))) pat[i].up=!pat[i].up; if(i>0 && vr.chance(P(16))) pat[i].rank=127-pat[i].rank; } }
var thr=clamp(d+16,0,128), on=[], bass=[], mel=[];
for(s=0;s<steps;s++) on.push(pat[s%16].rank<thr);
for(s=0;s<steps;s++) if(on[s]){
var p=pat[s%16], nx=s+1; while(nx<steps && !on[nx]) nx++;
bass.push({t:s*TPS,v:bassPitch(g,c+p.off)+(p.up?12:0),vel:p.acc?120:80,tr:T_BASS,len:p.sl?(nx-s)*TPS+20:TPS>>1});
}
var ar=new Rng(mix(rs,0x414E53+sec));
for(s=0;s<steps;){
if(on[s]){ s++; continue; }
var e2=s; while(e2<steps && !on[e2]) e2++;
if(e2-s>=2 && ar.chance(P(d+32))){
var tr=((g.hocket>>(s&7))&1)?T_MELA:T_MELB; // the hocket split, by step
mel.push({t:s*TPS,v:melPitch(g,c+[0,2,4][ar.below(3)]),vel:84,tr:tr,len:TPS-40});
if(e2-s>=3 && ar.chance(P(64))) mel.push({t:(s+1)*TPS,v:melPitch(g,c+[0,2,4][ar.below(3)]),vel:72,tr:tr,len:TPS-40});
}
s=e2;
}
return {mel:mel,bass:bass,pad:drone?[{t:0,v:48+g.root,vel:52,tr:T_PAD}]:[]};
}
/* TURING: an 8- or 16-bit looping shift register, one step per 16th. DRIFT flips the bit coming round (0 = locked).
Whether absolute step k flips is a hash of (seed, k); the register is rebuilt over the last 16 bars, so flips
stick for 16 bars and any bar renders on its own. Melody = the register read as a scale degree; bass = the
chord root where bit 0 is set (another bit lifts it an octave); pad = the triad. */
function rotl(v,s,L){ return s?(((v<<s)|(v>>>(L-s)))&((1<<L)-1)):v; }
function keysTuring(rs,variant,g,steps,d,sec,pos,kit,fill,x){
var r=new Rng(mix(rs,0x545552)), L=r.chance(P(64))?8:16, mask=(1<<L)-1, base=r.next()&mask;
if(!base) base=1;
var n=SCALES[g.scale].length, c=chordDeg(g,pos), flipP=P(x.drift>>2), fs=mix(rs,0x464C50);
var from=Math.max(0,x.bar-16)*steps, to=x.bar*steps, reg=rotl(base,from%L,L), k, s, j=0;
function step(v,k){ var top=(v>>>(L-1))&1; if(flipP && (mix(fs,k)>>>16)<flipP) top^=1; return ((v<<1)|top)&mask; }
for(k=from;k<to;k++) reg=step(reg,k);
var span=n*g.monoRange, mel=[], bass=[];
for(s=0;s<steps;s++){
if(((reg>>>2)&7)*18<d){ mel.push({t:s*TPS,v:melPitch(g,c+(((reg&0xFF)*(span+1))>>>8)),vel:70+((reg>>>5)&3)*10+(s%4===0?12:0),tr:((g.hocket>>(j&7))&1)?T_MELA:T_MELB}); j++; }
if(reg&1) bass.push({t:s*TPS,v:bassPitch(g,c)+(((reg>>>(L>>1))&1)?12:0),vel:s===0?110:94,tr:T_BASS});
reg=step(reg,to+s);
}
return {mel:mel,bass:bass,pad:padChord(g,[c,c+2,c+4])};
}
/* ARP: the bar's chord (triad or 7th) arpeggiated up, down, up-down or in a fixed shuffle over 1-2 octaves,
8ths at low density and 16ths at high, alternating MEL A / B. Bass = the root on every kick; pad = the same chord. */
function keysArp(rs,variant,g,steps,d,sec,pos,kit,fill,x){
var r=new Rng(mix(rs,0x415250)), n=SCALES[g.scale].length, c=chordDeg(g,pos), i, o;
var seventh=r.chance(P(64)), dir=r.below(4), chord=seventh?[c,c+2,c+4,c+6]:[c,c+2,c+4], up=[], seq;
for(o=0;o<g.monoRange;o++) for(i=0;i<chord.length;i++) up.push(chord[i]+o*n);
if(dir===0) seq=up;
else if(dir===1) seq=up.slice().reverse();
else if(dir===2){ seq=up.slice(); for(i=up.length-2;i>0;i--) seq.push(up[i]); }
else { seq=up.slice(); for(i=seq.length-1;i>0;i--){ var j=r.below(i+1), tmp=seq[i]; seq[i]=seq[j]; seq[j]=tmp; } }
var off=0; if(variant) off=new Rng(mix(mix(rs,0x5641+sec),variant)).below(seq.length);
var rate=d>=64?1:2, gs=groups(steps), last0=gs[gs.length-1][0], mel=[], bass=[], s=0, k=0;
while(s<steps){
var stp=(fill && s>=last0)?1:rate;
mel.push({t:s*TPS,v:melPitch(g,seq[(k+off)%seq.length]),vel:s%4===0?96:78,tr:(k&1)?T_MELB:T_MELA,len:stp*TPS-20});
k++; s+=stp;
}
var kicks=kickTimes(kit);
for(i=0;i<kicks.length;i++) bass.push({t:kicks[i],v:bassPitch(g,c),vel:kicks[i]===0?112:98,tr:T_BASS});
if(!kicks.length) bass.push({t:0,v:bassPitch(g,c),vel:104,tr:T_BASS});
return {mel:mel,bass:bass,pad:padChord(g,chord)};
}
var KEYS=[keysMotif,keys303,keysTuring,keysArp], KEYS_NAMES=['MOTIF','303','TURING','ARP'];
function sortDedupe(ev){
ev.sort(function(a,b){ return a.t-b.t || a.v-b.v; });
var out=[], i;
@@ -467,7 +563,7 @@ var Engine = (function(){
/* All tracks are rendered every bar; which ones sound is up to the player. */
function renderBar(st,p,bar){
var g=applyKey(genome(st.seed),p), amen=p.gen===4; if(amen){ g.steps=16; g.swing=0; g.layback=0; } // the break: 16 steps, straight; drift adds the swing
var g=applyKey(genome(st.seed,st.salts),p), amen=p.gen===4; if(amen){ g.steps=16; g.swing=0; g.layback=0; } // the break: 16 steps, straight; drift adds the swing
var pos=bar%PHRASE_BARS, phrase=(bar/PHRASE_BARS)|0, sec=g.form[pos], barTicks=g.steps*TPS, i, j;
var fr=new Rng(mix(mix(st.seed,0x46494C),phrase));
var fill = !!p.fill || (!amen && pos===PHRASE_BARS-1 && fr.chance(P(g.fillP>>1)));
@@ -508,7 +604,8 @@ var Engine = (function(){
kit=sortDedupe(kit);
var melShift = disp && dRng.chance(P(disp>>1)) ? (dRng.below(2)?TPS:-TPS) : 0;
var swingBar = wob ? clamp(g.swing+dRng.below(2*fdiv(wob*40,127)+1)-fdiv(wob*40,127),0,90) : g.swing;
var lt=lineTracks(roleSeed(st,1),variants[1],g,g.steps,clamp(g.dens[1]+e-64,0,127),sec,pos,kit,fill), out=[];
var keysFn=KEYS[clamp(p.keys|0,0,KEYS.length-1)];
var lt=keysFn(roleSeed(st,1),variants[1],g,g.steps,clamp(g.dens[1]+e-64,0,127),sec,pos,kit,fill,{bar:bar,drift:dr}), out=[];
for(i=0;i<kit.length;i++){
var tr=kitTrack(kit[i]), t=kit[i].t;
if(tr===T_PERC) t=(t+g.rot*TPS)%barTicks; // perc shares the melody's phase offset
@@ -517,9 +614,9 @@ var Engine = (function(){
out.push({t:clamp(groove(t,g,tr===T_SNARE,barTicks,swingBar)+nudge(barSeed,t,NUDGE_GRP[tr],micro)+(amen?amenFeel(krs,bar,t,kit[i].v,kit[i].a):0),0,barTicks-1),lane:0,v:kit[i].v,vel:kit[i].vel,len:len,tr:tr});
}
function mono(list,lenRule){
for(var k=0;k<list.length;k++){ var nx=list[k+1], gap=(nx?nx.t:barTicks)-list[k].t, len=lenRule(gap,nx,list[k]);
for(var k=0;k<list.length;k++){ var nx=list[k+1], gap=(nx?nx.t:barTicks)-list[k].t, fixed=list[k].len!=null, len=fixed?list[k].len:lenRule(gap,nx,list[k]);
var t0=list[k].t; if(melShift && (list[k].tr===T_MELA || list[k].tr===T_MELB)) t0=mod(t0+melShift,barTicks);
out.push({t:clamp(groove(t0,g,false,barTicks,swingBar)+nudge(barSeed,list[k].t,NUDGE_GRP[list[k].tr],micro),0,barTicks-1),lane:1,v:list[k].v,vel:list[k].vel,len:Math.max(10,len-swingBar),tr:list[k].tr}); }
out.push({t:clamp(groove(t0,g,false,barTicks,swingBar)+nudge(barSeed,list[k].t,NUDGE_GRP[list[k].tr],micro),0,barTicks-1),lane:1,v:list[k].v,vel:list[k].vel,len:Math.max(10,fixed?len:len-swingBar),tr:list[k].tr}); }
}
var melA=[], melB=[]; for(i=0;i<lt.mel.length;i++) (lt.mel[i].tr===T_MELA?melA:melB).push(lt.mel[i]);
function melLen(gap,nx,cur){ return g.monoLen===0?Math.min(90,gap-5):g.monoLen===1?Math.max(30,gap>>1):((nx && nx.v!==cur.v)?gap+20:gap-5); }
@@ -531,8 +628,9 @@ var Engine = (function(){
}
var ROMAN=['i','ii','iii','iv','v','vi','vii'];
function describe(st,p){
var g=applyKey(genome(st.seed),p), s=[];
var g=applyKey(genome(st.seed,st.salts),p), s=[];
s.push(p.gen===4?'amen break':FRAMES[g.frame].n);
s.push('keys '+KEYS_NAMES[clamp(p.keys|0,0,KEYS.length-1)].toLowerCase());
s.push('chords '+g.prog.map(function(x){ return ROMAN[x%7]; }).join('–'));
if(p.gen!==4){
s.push(g.swing?'swing '+Math.round(g.swing*100/TPS)+'%':'straight');
@@ -548,6 +646,6 @@ var Engine = (function(){
return {TPB:TPB,TPS:TPS,CLOCK_TICKS:CLOCK_TICKS,PHRASE_BARS:PHRASE_BARS,TRACKS:TRACKS,NT:NT,
genome:genome,applyKey:applyKey,renderBar:renderBar,
describe:describe,checksum:checksum,mix:mix,Rng:Rng,SCALE_NAMES:SCALE_NAMES,NOTE_NAMES:NOTE_NAMES,GEN_NAMES:GEN_NAMES};
describe:describe,checksum:checksum,mix:mix,Rng:Rng,SCALE_NAMES:SCALE_NAMES,NOTE_NAMES:NOTE_NAMES,GEN_NAMES:GEN_NAMES,KEYS_NAMES:KEYS_NAMES};
})();
if(typeof module!=='undefined') module.exports=Engine;

View File

@@ -167,7 +167,7 @@ textarea{width:100%;min-height:140px;font:400 11px/1.4 var(--f-mono);background:
<p class="note" style="margin:8px 0 0">Landscape, laid out like the panel drawing: you sit at the bottom edge, cables leave from the back. The screen is drawn at roughly real size.</p>
<p class="hint"><b>Track buttons:</b> tap = on ↔ off. Hold = flip it while held (drop a track out, or punch one in).<br>
<b>Screen map / LEDs:</b> filled = on, hollow center = hit, empty = off, blinking = change waiting for the bar line. Map rows match the button rows.<br>
<b>Encoder pages:</b> SEED history · GEN · ROOT · SCALE (AUTO = the seed picks) · BPM · SLOT (push loads, hold push saves) · ROLL (re-roll kit or line) · LEARN (push, press a kit track, hit the pad).<br>
<b>Encoder pages:</b> SEED history · KIT GEN · KEYS GEN (MOTIF, 303, TURING, ARP) · ROOT · SCALE (AUTO = the seed picks) · BPM · SLOT (push loads, hold push saves) · ROLL (re-roll kit or keys) · LEARN (push, press a kit track, hit the pad).<br>
Keys: <kbd>Space</kbd> seed · <kbd>1</kbd>…<kbd>0</kbd> <kbd>-</kbd> tracks · <kbd>←</kbd><kbd>→</kbd> encoder · <kbd>Enter</kbd> push · <kbd>R</kbd> run</p>
</section>