Split kit and keys; KEYS GEN with MOTIF, 303, TURING, ARP
Some checks failed
check / check (push) Has been cancelled
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:
140
sim/engine.js
140
sim/engine.js
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user