Files
seedbox/sim/engine.js
Gee 346c820b10
Some checks failed
check / check (push) Has been cancelled
Split kit and keys; KEYS GEN with MOTIF, 303, TURING, ARP
- 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>
2026-10-11 21:50:41 +03:00

652 lines
41 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ================= ENGINE (portable) =================
No DOM, no Math.random, no floats in decisions. 840 ticks per quarter, 210 per 16th
(840 divides by 2,3,4,5,7 for tuplets). MIDI clock every 35 ticks.
The seed builds 11 looped tracks; the player brings them in and out by hand.
Energy = pattern density (hats only lightly). Drift = how often a phrase swaps in a variation of the detail.
Musical layer: a 4-chord progression per phrase shared by bass, pad and melody; a melodic motif that is
restated, sequenced and answered across the phrase; swing and metric velocity on every lane; B/C sections
are transformations of A instead of unrelated patterns; a phrase arc (flat, build, or break in bar 3).
Lanes: 0 = kit (v = voice 0 kick, 1 snare, 2 hat, 3 perc), 1 = synth (v = MIDI pitch). */
var Engine = (function(){
var TPB=840, TPS=210, CLOCK_TICKS=35, PHRASE_BARS=4;
function mix(a,b){ var h=(a ^ Math.imul(b|0,0x9E3779B1))>>>0; h^=h>>>16; h=Math.imul(h,0x85EBCA6B)>>>0; h^=h>>>13; h=Math.imul(h,0xC2B2AE35)>>>0; h^=h>>>16; return (h>>>0)||0x6D2B79F5; }
function Rng(s){ this.s=(s>>>0)||0x6D2B79F5; }
Rng.prototype.next=function(){ var x=this.s; x=(x^(x<<13))>>>0; x=(x^(x>>>17))>>>0; x=(x^(x<<5))>>>0; this.s=x; return x; };
Rng.prototype.below=function(n){ return n<=0?0:(((this.next()>>>16)*n)>>>16); };
Rng.prototype.chance=function(p){ return (this.next()>>>16) < p; };
Rng.prototype.pickW=function(w){ var t=0,i; for(i=0;i<w.length;i++) t+=w[i]; var r=this.below(t); for(i=0;i<w.length;i++){ if(r<w[i]) return i; r-=w[i]; } return w.length-1; };
function clamp(x,a,b){ return x<a?a:x>b?b:x; }
function P(x){ return clamp(x|0,0,127)*516; } // 0..127 -> 0..65532
function fdiv(a,b){ return Math.floor(a/b); } // floor division (C port: handle negatives explicitly)
function mod(a,b){ return ((a%b)+b)%b; }
var TRACKS=[
{name:'KICK', lane:0,voice:0},
{name:'KICK 2',lane:0,voice:0},
{name:'SNARE', lane:0,voice:1},
{name:'GHOST', lane:0,voice:1},
{name:'HAT', lane:0,voice:2},
{name:'HAT 16',lane:0,voice:2},
{name:'PERC', lane:0,voice:3},
{name:'BASS', lane:1,voice:0},
{name:'MEL A', lane:1,voice:0},
{name:'MEL B', lane:1,voice:0},
{name:'PAD', lane:1,voice:0}
];
var NT=TRACKS.length;
var T_KICK=0,T_KICK2=1,T_SNARE=2,T_GHOST=3,T_HAT=4,T_HAT16=5,T_PERC=6,T_BASS=7,T_MELA=8,T_MELB=9,T_PAD=10;
var STEPS=[16,12,14,20], STEPS_W=[12,3,2,2];
var FORMS=[[0,0,0,1],[0,0,1,0],[0,1,0,1],[0,0,0,0],[0,1,0,2],[0,1,0,0]], FORMS_W=[5,5,3,2,3,2];
var FORM_NAMES=['AAAB','AABA','ABAB','AAAA','ABAC','ABAA'];
/* Menu order = index. 5 to 8 notes; chords stack every other scale note, so pentatonics give open chords,
whole tone augmented, octatonic diminished. Names fit the 10-character screen line. */
var SCALES=[[0,2,4,5,7,9,11],[0,2,3,5,7,8,10],[0,2,3,5,7,9,10],[0,2,4,5,7,9,10],[0,2,4,6,7,9,11],[0,1,3,5,7,8,10],[0,2,3,5,7,8,11],
[0,3,5,7,10],[0,2,4,7,9],
[0,1,4,5,7,8,10],[0,1,4,5,7,8,11],[0,2,3,6,7,8,11],[0,2,3,6,7,9,10],[0,2,4,6,7,9,10],
[0,2,3,7,8],[0,1,5,7,10],[0,1,5,6,10],[0,2,4,6,9,10],[0,2,4,6,8,10],[0,1,3,4,6,7,9,10],[0,1,3,5,6,8,10]];
var SCALE_NAMES=['MAJOR','MINOR','DORIAN','MIXOLYDIAN','LYDIAN','PHRYGIAN','HARM MINOR','MINOR PENT','MAJOR PENT',
'PHRYG DOM','DBL HARM','HUNG MINOR','UKR DORIAN','LYD DOM','HIRAJOSHI','IN SEN','IWATO','PROMETHEUS','WHOLE TONE','OCTATONIC','LOCRIAN'];
var NOTE_NAMES=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];
var GEN_NAMES=['EUCLID','SKELETON','SUBDIV','AUTOMATA','AMENIZER'];
var HOCKETS=[0xAA,0x55,0xB4,0x6C,0x96,0xD2,0x5A,0xA5];
/* chord roots as scale-degree indices, one per bar of the phrase */
var PROGS=[[0,0,5,4],[0,5,3,4],[0,3,0,4],[0,6,5,4],[0,2,5,4],[0,0,3,3],[0,5,0,5],[0,3,5,6],[0,1,0,6],[0,4,5,3]];
var SWINGS=[0,21,35,50,70], SWING_W=[4,3,3,2,1]; // delay of every second 16th, in ticks (70 = triplet feel)
var ARC_NAMES=['flat','build','break'];
var SPLITS=[1,2,3,4,5,7];
var SKEL=[
{k:[0,10],s:[4,12],g:[7,15]}, // two-step
{k:[0,2,10,11],s:[4,12],g:[7,9,15]}, // amen-ish
{k:[0,11],s:[8],g:[14,3]}, // half-time
{k:[0,3,10],s:[4,12],g:[7]}, // think-ish
{k:[0,6,11],s:[4,13],g:[9,15]}, // broken
{k:[0,7,10],s:[4,12],g:[2,14]} // steppy
];
/* Rhythmic frames: where the main kick(s) and snare(s) live. Positions in 16ths of a 16-step bar, scaled to the seed's bar.
One frame per seed, shared by every generator, so the generators vary the detail and the frame varies the feel. */
var FRAMES=[
{n:'backbeat', k:[0], s:[4,12]},
{n:'half-time', k:[0], s:[8]},
{n:'four-floor', k:[0,4,8,12], s:[4,12], layer:1},
{n:'3-3-2', k:[0,3,8,11], s:[6,14]},
{n:'broken', k:[0,10], s:[6,12]},
{n:'displaced', k:[3,10], s:[8], noOne:1},
{n:'over the bar',k:null, s:null, cyc:12},
{n:'free snare', k:[0], s:null, free:1}
], FRAME_W=[3,3,2,2,2,1,1,2];
function frameSets(g,steps,pos){
var F=FRAMES[g.frame], K={}, S={}, i;
function sc(x){ return Math.floor(x*steps/16); }
if(F.cyc){ for(i=0;i<steps;i++){ var x=pos*steps+i; if(x%F.cyc===0) K[i]=1; else if(x%F.cyc===(F.cyc>>1)) S[i]=1; } }
else {
for(i=0;i<F.k.length;i++) K[sc(F.k[i])]=1;
if(F.free){ for(i=1;i<steps;i++) if(eu(i,g.freeK,steps,g.freeRot) && !K[i]) S[i]=1; }
else for(i=0;i<F.s.length;i++) S[sc(F.s[i])]=1;
}
if(!F.layer) for(i in S) if(K[i]) delete K[i]; // kick and snare share a step only in four-floor
return {K:K,S:S,noOne:!!F.noOne};
}
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];
/* 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.fillP=40+r.below(80);
g.rot=r.pickW([4,2,2,1]); // phase offset (16ths) of the ambiguous tracks: melody, perc
g.swing=SWINGS[r.pickW(SWING_W)];
g.arc=r.pickW([3,2,2]);
g.layback=r.below(2);
// 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 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
function divide(r,start,len,depth,maxD,W,prob,out,v,velFn){
if(depth<maxD){
var w=W.slice(); if(depth>0) w[0]+=depth*3;
var f=SPLITS[r.pickW(w)];
if(f>1){ for(var i=0;i<f;i++){ var a=start+Math.floor(i*len/f), b=start+Math.floor((i+1)*len/f); divide(r,a,b-a,depth+1,maxD,W,prob,out,v,velFn); } return; }
}
if(r.chance(prob)) out.push({t:start,v:v,vel:velFn(r,depth)});
}
/* ---------- kit generators. Hat accents (vel >= 85) become HAT, the rest HAT 16. ---------- */
function euclidParams(coreSeed,steps,d,hd){
var c=new Rng(coreSeed), kOpts=[3,5,4,2,7], k=kOpts[c.pickW([4,3,2,1,1+(d>>5)])];
k=clamp(k,1,steps>>1);
var rots=[], r0; for(r0=0;r0<steps;r0++) if(((r0*k)%steps)<k) rots.push(r0); // rotations that keep a kick on step 0
var kick={k:k,n:steps,rot:rots[c.below(rots.length)]};
var ghost={k:1+c.below(3),n:steps,rot:c.below(steps)};
var hn=c.pickW([3,3,1,1,1])<2?steps:POLY_N[c.below(3)];
var hat={k:clamp((hn*(32+(hd>>2)))>>7,1,hn),n:hn,rot:c.below(hn)};
var pn=POLY_N[c.below(POLY_N.length)];
var perc={k:clamp(1+((pn*d)>>9)+c.below(2),1,pn-1),n:pn,rot:c.below(pn)};
return {kick:kick,ghost:ghost,hat:hat,perc:perc,accent:c.below(hat.n)};
}
function kitEuclid(coreSeed,varSeed,steps,d,hd,pos,fs){
var E=euclidParams(coreSeed,steps,d,hd), vr=new Rng(varSeed), hr=new Rng(mix(varSeed,0x484154+pos));
var ev=[], back=fs.S, i;
for(i=0;i<steps;i++){
var t=i*TPS, x=pos*steps+i;
if(back[i]) ev.push({t:t,v:1,vel:112});
else if(eu(i,E.ghost.k,E.ghost.n,E.ghost.rot) && vr.chance(P(d>>1))) ev.push({t:t,v:1,vel:38+vr.below(20)});
if(fs.K[i]) ev.push({t:t,v:0,vel:i===0?120:112,a:1});
else if(eu(i,E.kick.k,E.kick.n,E.kick.rot) && !back[i] && !(i===0 && fs.noOne)) ev.push({t:t,v:0,vel:104});
if(eu(x,E.hat.k,E.hat.n,E.hat.rot)){
var acc=eu(x,Math.max(1,E.hat.k>>1),E.hat.n,E.accent);
ev.push({t:t,v:2,vel:acc?92:66});
if(hr.chance(P(hd>>3))) ev.push({t:t+(TPS>>1),v:2,vel:44});
}
if(eu(x,E.perc.k,E.perc.n,E.perc.rot)) ev.push({t:t,v:3,vel:70+vr.below(40)});
}
return ev;
}
function kitSubdiv(coreSeed,varSeed,steps,d,hd,pos,fs){
var core=new Rng(coreSeed), vr=new Rng(varSeed), hr=new Rng(mix(varSeed,0x484154+pos));
var ev=[], gs=groups(steps), snT=[], gi, j;
var burst = core.chance(P(d>>1)) ? core.below(gs.length) : -1;
for(j=0;j<steps;j++){ if(fs.S[j]){ ev.push({t:j*TPS,v:1,vel:110+core.below(14)}); snT.push(j*TPS); } if(fs.K[j]) ev.push({t:j*TPS,v:0,vel:j===0?118:112,a:1}); }
for(gi=0;gi<gs.length;gi++){
var s=gs[gi][0]*TPS, L=gs[gi][1]*TPS, tmp=[];
divide(core,s,L,0,1,[6,5,1,0,0,0],P(10+(d>>2)),tmp,0,function(r){ return 96+r.below(16); });
for(j=0;j<tmp.length;j++) if(!(tmp[j].t===0 && fs.noOne) && snT.indexOf(tmp[j].t)<0) ev.push(tmp[j]);
divide(vr,s,L,0,1,[4,4,1,0,0,0],P(d>>3),ev,1,function(r){ return 34+r.below(22); });
if(gi===burst) divide(hr,s,L,0,2,[1,3,4,2,3,2],P(90),ev,2,function(r){ return 50+r.below(30); });
else divide(hr,s,L,0,1,[3,6,0,2,0,0],P(36+(hd>>2)),ev,2,function(r,dp){ return dp===0?90:58+r.below(20); });
divide(vr,s,L,0,1,[6,2,3,0,1,1],P(d>>2),ev,3,function(r){ return 60+r.below(45); });
}
return ev;
}
function kitSkeleton(coreSeed,varSeed,steps,d,hd,pos,fs,frame){
var core=new Rng(coreSeed), vr=new Rng(varSeed), hr=new Rng(mix(varSeed,0x484154+pos));
var ev=[], i, sk=SKEL[core.below(SKEL.length)];
function sc(x){ return Math.floor(x*steps/16); }
var sn=[];
if(frame===0) for(i=0;i<sk.s.length;i++) sn.push(sc(sk.s[i])); // backbeat frame: the template's own snares
else for(i in fs.S) sn.push(i|0); // any other frame decides the snares
for(i in fs.K) ev.push({t:(i|0)*TPS,v:0,vel:116,a:1});
for(i=0;i<sk.k.length;i++){ var s=sc(sk.k[i]); if(sk.k[i]===0) continue; if(core.chance(P(d>>2))) s=clamp(s+(core.below(2)?1:-1),1,steps-1); if(sn.indexOf(s)<0) ev.push({t:s*TPS,v:0,vel:104+core.below(12)}); }
for(i=0;i<sn.length;i++) ev.push({t:sn[i]*TPS,v:1,vel:110+core.below(14)});
for(i=0;i<sk.g.length;i++) if(vr.chance(P(d))) ev.push({t:sc(sk.g[i])*TPS,v:1,vel:36+vr.below(24)});
if(vr.chance(P(d>>1))){ var xs=1+vr.below(steps-1); if(sn.indexOf(xs)<0) ev.push({t:xs*TPS,v:0,vel:90+vr.below(20)}); }
var div=[2,2,1][core.below(3)], hp=P(36+(hd>>2));
for(i=0;i<steps;i+=div){
if(i%4===0) ev.push({t:i*TPS,v:2,vel:92});
else if(hr.chance(hp)){ var vel=60+hr.below(22);
if(hr.chance(P(hd>>3))){ var L=TPS*div; ev.push({t:i*TPS,v:2,vel:vel}); ev.push({t:i*TPS+(L>>1),v:2,vel:vel-8}); }
else ev.push({t:i*TPS,v:2,vel:vel}); }
}
var ek=clamp(1+((d*steps)>>8)+core.below(3),1,steps-1), rot=core.below(steps);
for(i=0;i<steps;i++) if(eu(i,ek,steps,rot)) ev.push({t:i*TPS,v:3,vel:64+vr.below(40)});
return ev;
}
function caRow(rule,init,steps,gens){
var row=init.slice(), g, i;
for(g=0;g<gens;g++){ var nx=[]; for(i=0;i<steps;i++){ var l=row[(i+steps-1)%steps], c=row[i], r=row[(i+1)%steps]; nx.push((rule>>((l<<2)|(c<<1)|r))&1); } row=nx; }
return row;
}
function randRow(r,steps,p){ var a=[]; for(var i=0;i<steps;i++) a.push(r.chance(p)?1:0); return a; }
function kitCA(coreSeed,varSeed,steps,d,hd,pos,sec,fs){
var core=new Rng(coreSeed), vr=new Rng(varSeed), thin=new Rng(mix(varSeed,0x5448+pos));
var ev=[], gen=sec*5+pos, i, nR=RULES.length;
var rk=RULES[core.below(nR)], rs=RULES[core.below(nR)], rh=RULES[vr.below(nR)], rp=RULES[vr.below(nR)];
var K=caRow(rk,randRow(core,steps,P(40)),steps,gen), S=caRow(rs,randRow(core,steps,P(40)),steps,gen),
H=caRow(rh,randRow(vr,steps,P(64)),steps,gen+pos), C=caRow(rp,randRow(vr,steps,P(40)),steps,gen);
for(i=0;i<steps;i++){
var t=i*TPS, back=!!fs.S[i];
if(back) ev.push({t:t,v:1,vel:112});
else if(S[i] && thin.chance(P(d>>1))) ev.push({t:t,v:1,vel:40+thin.below(20)});
if(fs.K[i]) ev.push({t:t,v:0,vel:118,a:1});
else if(K[i] && !back && !(i===0 && fs.noOne) && thin.chance(P(40+(d>>1)))) ev.push({t:t,v:0,vel:96+thin.below(20)});
if(H[i] && thin.chance(P(56+(hd>>2)))){ var nb=H[(i+steps-1)%steps]+H[(i+1)%steps]; ev.push({t:t,v:2,vel:58+nb*16}); }
if(C[i] && !C[(i+1)%steps] && thin.chance(P(d))) ev.push({t:t,v:3,vel:70+thin.below(40)});
}
return ev;
}
/* AMENIZER: the four bars of the Amen break, velocities included, played on the kit tracks.
[step, voice (0 kick, 1 snare, 2 ride/hat, 3 crash/perc), velocity, main kick flag]. Ride: on-beat 8ths go to HAT, off-beat to HAT 16.
Snares under 90 land on GHOST, kicks without the flag on KICK 2. This is a by-ear transcription: adjust the table to taste. */
/* [step, voice, velocity, main flag]. Main flag = KICK / SNARE / HAT track; the rest land on KICK 2 / GHOST / HAT 16.
Velocities leave headroom: backbeats ~110, offbeat kicks in the 80s, ghosts in the 50s, ride 82 / 60. */
var AMEN=(function(){
function ride(b,crashAt){ for(var s=0;s<16;s+=2) if(s!==crashAt) b.push(s%4===0?[s,2,82,1]:[s,2,60,0]); return b; }
var A=ride([[0,0,112,1],[2,0,84,0],[10,0,104,1],[11,0,80,0],[4,1,112,1],[7,1,52,0],[9,1,86,1],[12,1,110,1],[15,1,58,0]],-1);
var C=ride([[0,0,112,1],[2,0,84,0],[10,0,104,1],[4,1,112,1],[7,1,52,0],[9,1,86,1],[14,1,110,1]],-1);
var D=ride([[2,0,104,1],[3,0,84,0],[10,0,108,1],[1,1,48,0],[4,1,112,1],[7,1,54,0],[9,1,88,1],[14,1,112,1],[10,3,110,0]],10);
return [A,A,C,D];
})();
/* A drummer, not a grid: every hit moves a little in level, a hit that follows the same drum closely comes in softer,
and the ride hand has a slightly different weight every bar. */
function humanizeAmen(ev,seed,bar){
ev.sort(function(a,b){ return a.t-b.t || a.v-b.v; });
var last=[-1e6,-1e6,-1e6,-1e6], lastV=[0,0,0,0], br=new Rng(mix(mix(seed,0x48554D),bar)), rideBias=br.below(11)-5;
for(var i=0;i<ev.length;i++){
var x=ev[i], base=x.vel, h=mix(mix(mix(seed,0x564C),bar),x.t*4+x.v), v=base+(h%13)-6;
if(x.v===2) v+=rideBias;
if(x.v<2 && x.t-last[x.v]<=2*TPS && lastV[x.v]*10>=base*9) v=fdiv(v*80,100); // a quick second stroke on a kick or snare that was just hit hard: softer
last[x.v]=x.t; lastV[x.v]=base; x.vel=clamp(v,1,127);
}
return ev;
}
/* Timing feel per drum, in ticks (210 per 16th): snares sit back, ghosts float, the ride pushes slightly. */
function amenFeel(seed,bar,t,v,a){
var h=mix(mix(mix(seed,0x46454C),bar),t*4+v);
function j(n){ return (h%(2*n+1))-n; }
if(v===0) return j(4);
if(v===1) return a ? 10+j(5) : j(10);
if(v===2) return -4+j(6);
return j(4);
}
function amenSlice(bar,beat){ var b=AMEN[bar&3], out=[], i; for(i=0;i<b.length;i++) if(b[i][0]>=beat*4 && b[i][0]<beat*4+4) out.push(b[i]); return out; }
/* Drift chops the break per beat, like a jungle edit: swap in another beat of the break, repeat the last beat,
stutter a half beat, or ratchet the snare. At 0 the break plays straight. */
function kitAmen(seed,bar,pos,dr,fill){
var src=fill?3:pos, ev=[], prev=null, j, k;
for(j=0;j<4;j++){
var sl=amenSlice(src,j), r=new Rng(mix(mix(seed,0x414D454E),bar*4+j)), op=-1;
// edits grow steadily with the knob; later beats in the bar get edited more, the first beat stays put until ~70%
var pc=fdiv(dr*80,127), pb=fdiv(pc*[2,3,5,6][j],4), keep=(pos===0 && j===0) || (j===0 && dr<88);
if(!keep && r.chance(P(pb))) op=r.pickW([4,2,2,2,dr>80?1:0]);
if(op===0){ sl=amenSlice(r.below(4),j); } // same beat from another bar: the classic edit
else if(op===4){ sl=amenSlice(r.below(4),r.below(4)); op=0; } // any beat of the break
else if(op===1 && prev){ sl=prev; }
var base=j*4*TPS;
for(k=0;k<sl.length;k++){
var s=sl[k], rel=s[0]%4, t=base+rel*TPS;
if(op===2){ if(rel<2){ ev.push({t:t,v:s[1],vel:s[2],a:s[3]}); ev.push({t:t+2*TPS,v:s[1],vel:Math.max(1,s[2]-10),a:s[3]}); } continue; }
ev.push({t:t,v:s[1],vel:s[2],a:s[3]});
if(op===3 && s[1]===1){ ev.push({t:t+(TPS>>1),v:1,vel:Math.max(1,s[2]-24),a:0}); }
}
prev=sl;
}
return ev;
}
/* Sections B and C reuse A's core pattern and transform it, so the phrase hangs together. */
function transformKit(ev,steps,sec,seed,fs){
if(sec===0) return ev;
var r=new Rng(mix(seed,0x5345+sec)), ops=[r.below(8)], i, out=[];
if(r.chance(P(64))) ops.push(r.below(8));
var half=(steps>>1)*TPS;
for(i=0;i<ev.length;i++) out.push({t:ev[i].t,v:ev[i].v,vel:ev[i].vel,a:ev[i].a});
function has(o){ return ops.indexOf(o)>=0; }
if(has(0)){ // kick answer: push the last off-beat kick a 16th later, add one before the backbeat
var lastK=-1; for(i=0;i<out.length;i++) if(out[i].v===0 && out[i].t>0) lastK=i;
if(lastK>=0 && out[lastK].t+TPS<steps*TPS) out[lastK].t+=TPS;
out.push({t:3*TPS,v:0,vel:98});
}
if(has(1)){ // second half doubles its hats into 16ths
var hs=[]; for(i=0;i<out.length;i++) if(out[i].v===2 && out[i].t>=half && out[i].t%TPS===0) hs.push(out[i].t);
for(i=0;i<hs.length;i++) if(hs[i]+TPS<steps*TPS) out.push({t:hs[i]+TPS,v:2,vel:60});
}
if(has(2)){ // first half thins out: no perc, no ghosts
out=out.filter(function(e){ return !(e.t<half && (e.v===3 || (e.v===1 && e.vel<90))); });
}
if(has(3)){ // perc moves two 16ths later
for(i=0;i<out.length;i++) if(out[i].v===3) out[i].t=(out[i].t+2*TPS)%(steps*TPS);
}
if(has(4)){ // ghost snare pickups into each main snare
for(var sk in fs.S){ var sp=sk|0; if(sp>0) out.push({t:(sp-1)*TPS,v:1,vel:44}); }
}
if(has(5)){ // snare drag: the last main snare lands a 16th late
var ls=-1; for(i=0;i<out.length;i++) if(out[i].v===1 && out[i].vel>=90) ls=i;
if(ls>=0 && out[ls].t+TPS<steps*TPS) out[ls].t+=TPS;
}
if(has(6)){ // drop the one: no kick on the downbeat, the bass and pad keep the bar
var kn=0; for(i=0;i<out.length;i++) if(out[i].v===0) kn++;
if(kn>1) out=out.filter(function(e){ return !(e.v===0 && e.t===0); });
}
if(has(7)){ // half-time switch: main snares collapse to one in the middle of the bar
out=out.filter(function(e){ return !(e.v===1 && e.vel>=90); });
out.push({t:(steps>>1)*TPS,v:1,vel:116});
}
return out;
}
/* Phrase arc. build: bars 1-2 drop their offbeat clutter. break: bar 3 strips to kick-on-one, backbeat, hats. */
function arcKit(ev,g,pos){
if(g.arc===1 && pos<2) return ev.filter(function(e){ return !((e.v===3 || (e.v===1 && e.vel<90)) && (pos===0 || e.t%(2*TPS)!==0)); });
if(g.arc===2 && pos===2){ var k0=1e9; for(var i=0;i<ev.length;i++) if(ev[i].v===0 && ev[i].t<k0) k0=ev[i].t;
return ev.filter(function(e){ return (e.v===0 && e.t===k0) || (e.v===1 && e.vel>=90) || (e.v===2 && e.t%(2*TPS)===0); }); }
return ev;
}
/* Velocity by metric position (beat > 8th > 16th), plus an accent cycle of 3/5/7 sixteenths that runs across the bar line. */
function shapeKit(ev,g,pos,aoff,jit,jr){
for(var i=0;i<ev.length;i++){
var e=ev[i]; if(e.v<2 || e.t%TPS!==0) continue;
var st=e.t/TPS, sw=st+aoff, w=(sw%4===0)?100:(sw%2===0)?86:74, x=pos*g.steps+sw; // aoff slides where the 'strong' 16ths are
var v=fdiv(e.vel*w,100);
if(g.accentCycle && (x%g.accentCycle)===0) v+=24;
if(jit) v+=jr.below(2*jit+1)-jit;
e.vel=clamp(v,1,127);
}
return ev;
}
function kitFill(ev,steps,e,fr){
var gs=groups(steps), last=gs[gs.length-1], st=last[0]*TPS, L=last[1]*TPS, out=[], i, type=fr.below(4);
for(i=0;i<ev.length;i++) if(ev[i].t<st) out.push(ev[i]);
if(type===0){ out.push({t:st,v:0,vel:110}); var m=4+(e>>5); for(i=0;i<m;i++){ var q=m-i; out.push({t:st+L-Math.floor(L*q*q/(m*m)),v:1,vel:90+Math.floor(30*i/m)}); } }
else if(type===1){ out.push({t:st,v:0,vel:110}); var k=0; for(var t=st;t<st+L;t+=(TPS>>1),k++) out.push({t:t,v:(k%2)?3:1,vel:(k%2)?70+k*4:92+k*3}); }
else if(type===2){ out.push({t:st,v:0,vel:116}); out.push({t:st+L-TPS,v:1,vel:100}); out.push({t:st+L-(TPS>>1),v:1,vel:116}); } // drop-out, then a two-hit pickup
else { var prev=gs.length>1?gs[gs.length-2]:last, ps=prev[0]*TPS; // previous beat, stuttered
for(i=0;i<ev.length;i++) if(ev[i].t>=ps && ev[i].t<ps+(L>>1)){ out.push({t:st+(ev[i].t-ps),v:ev[i].v,vel:ev[i].vel}); out.push({t:st+(L>>1)+(ev[i].t-ps),v:ev[i].v,vel:clamp(ev[i].vel+10,1,127)}); } }
return out;
}
/* ---------- synth tracks ---------- */
function chordDeg(g,pos){ return g.prog[pos%PHRASE_BARS]; }
function degPitch(g,base,idx){ var sc=SCALES[g.scale], n=sc.length; return base+g.root+12*fdiv(idx,n)+sc[mod(idx,n)]; }
function melPitch(g,idx){ var p=degPitch(g,60,idx); while(p<60) p+=12; while(p>96) p-=12; return p; } // keep the melody above the pad
function isChordTone(g,pos,idx){ var n=SCALES[g.scale].length, c=chordDeg(g,pos), x=mod(idx-c,n); return x===0 || x===2 || x===4; }
function nearestChordTone(g,pos,idx){ for(var d=0;d<4;d++){ if(isChordTone(g,pos,idx-d)) return idx-d; if(isChordTone(g,pos,idx+d)) return idx+d; } return idx; }
/* Motif: 3-5 notes inside an 8-step window, contour in scale steps. Same motif across the whole seed. */
function makeMotif(rs,g){
var r=new Rng(mix(rs,0x4D4F54)), n=g.motifLen, pos=[0], used={0:1}, cont=[0], i;
if(r.chance(P(40))){ pos[0]=r.below(2)?1:2; used={}; used[pos[0]]=1; }
while(pos.length<n){ var p=1+r.below(7); if(!used[p]){ used[p]=1; pos.push(p); } }
pos.sort(function(a,b){ return a-b; });
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 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);
for(i=0;i<kit.length;i++) if(kit[i].v===0 && kit[i].t%TPS===0) kickList.push(kit[i].t);
// melody: statement anchored on the bar's chord, then an answer in the second half
function place(start,cont,anchor,velBase){
for(var k=0;k<M.pos.length && k<cont.length;k++){
var s=start+M.pos[k]; if(s>=steps) break;
if(fill && s>=last[0]) break;
var idx=anchor+cont[k];
if(s%4===0) idx=nearestChordTone(g,pos,idx); // strong positions land on chord tones
if(vr.chance(P(d>>3))) continue; // drift/energy drops the odd note
mel.push({t:s*TPS,v:melPitch(g,idx),vel:velBase+(s%4===0?18:0)+vr.below(12)});
if(k+1<M.pos.length && k+1<cont.length && vr.chance(P(d>>2))){ // passing note between motif notes
var s2=start+M.pos[k+1]; if(s2-s>=2 && (s+1)<steps) mel.push({t:(s+1)*TPS,v:melPitch(g,idx+(cont[k+1]>cont[k]?1:-1)),vel:velBase-14});
}
}
}
var anchor=c+(core.chance(P(50))?2:0), half=steps>>1;
place(0,M.cont,anchor,82);
if(d>=40 || steps>=16){
var c2=M.cont.slice(), a2=anchor, st2=half;
if(M.resp===0) a2=anchor+2;
else if(M.resp===1) for(j=0;j<c2.length;j++) c2[j]=-c2[j];
else st2=half+1;
if(pos===PHRASE_BARS-1){ c2=c2.slice(0,Math.max(1,c2.length-1)); } // last bar: shorter answer, then the cadence
place(st2,c2,a2,74);
}
if(pos===PHRASE_BARS-1 && !fill){ var cs=Math.max(0,steps-4); mel=mel.filter(function(m){ return m.t<cs*TPS; }); mel.push({t:cs*TPS,v:melPitch(g,chordDeg(g,0)),vel:96}); }
if(fill){ var nxt=chordDeg(g,0)+n, k2=0; for(var tt=last[0]*TPS;tt<(last[0]+last[1])*TPS;tt+=TPS,k2++) mel.push({t:tt,v:melPitch(g,nxt-(last[1]-1-k2)),vel:76+k2*8}); }
var rot=g.rot*TPS;
for(i=0;i<mel.length;i++) mel[i].t=(mel[i].t+rot)%barT;
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){ return bassPitch(g,idx); }
var nextC=chordDeg(g,pos+1);
for(i=0;i<kickList.length;i++){
var t=kickList[i], idx=c;
if(t>0 && (i%2)===1 && core.chance(P(70))) idx=c+g.bassDeg;
bass.push({t:t,v:bp(idx),vel:t===0?112:98,tr:T_BASS});
}
if(steps>=8 && core.chance(P(60))){ var at=(steps-1)*TPS; if(kickList.indexOf(at)<0) bass.push({t:at,v:bp(nextC+(core.below(2)?1:-1)),vel:84,tr:T_BASS}); }
// pad: the bar's triad (in scale), voiced inside C3-B3
var tones=[c,c+2,c+4], seen={};
for(i=0;i<tones.length;i++){ var pc=mod(degPitch(g,0,tones[i]),12), p=48+pc; if(!seen[p]){ seen[p]=1; pad.push({t:0,v:p,vel:i===0?58:50,tr:T_PAD}); } }
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;
for(i=0;i<ev.length;i++){ var p=out[out.length-1]; if(p && p.t===ev[i].t && p.v===ev[i].v){ if(ev[i].vel>p.vel) p.vel=ev[i].vel; } else out.push({t:ev[i].t,v:ev[i].v,vel:ev[i].vel,tr:ev[i].tr,a:ev[i].a}); }
for(i=0;i<out.length;i++) out[i].vel=clamp(out[i].vel,1,127);
return out;
}
function oneAtATime(ev){ var o=sortDedupe(ev), out=[]; for(var i=0;i<o.length;i++){ if(out.length && out[out.length-1].t===o[i].t) out[out.length-1]=o[i]; else out.push(o[i]); } return out; }
function kitTrack(e){
if(e.v===0) return (e.a || e.t===0)?T_KICK:T_KICK2; // frame kicks are the main kick, wherever they fall
if(e.v===1) return (e.a || e.vel>=90)?T_SNARE:T_GHOST;
if(e.v===2) return (e.a || e.vel>=85)?T_HAT:T_HAT16;
return T_PERC;
}
/* Groove: every second 16th is delayed by the seed's swing; main snares can sit slightly behind. */
function groove(t,g,isSnare,barTicks,swing){
if(t%TPS===0 && (t/TPS)%2===1) t+=swing;
if(isSnare && g.layback) t+=12;
return Math.min(t,barTicks-1);
}
/* DRIFT, one knob with overlapping layers that each fade in smoothly (0 = locked loop):
0-42 phrase variations go from never to every phrase
20-127 microtiming: hits lean off the grid, hats drag, snares rush, kick and bass move together
40-127 swing wobbles from bar to bar
55-127 accents slip: the strong 16ths of the hats and perc move, velocities loosen
80-127 whole parts slide a 16th early or late for a bar (hats, perc, ghosts, second kick, melody)
105-127 a beat now and then is squeezed onto a triplet grid */
function amt(d,a,b){ return clamp(fdiv((d-a)*127,b-a),0,127); }
var NUDGE_GRP=[0,0,1,3,2,2,3,0,4,4,5]; // per track: 0 low end (kick, bass), 1 snare, 2 hats, 3 ghost/perc, 4 melody, 5 pad
function nudge(barSeed,t,grp,micro){
if(!micro || grp===5) return 0;
var mx=fdiv(micro*(grp===4?30:48),127), h=mix(mix(barSeed,t),grp), n=(h%(2*mx+1))-mx;
if(grp===2) n+=fdiv(micro*10,127); // hats drag
if(grp===1) n-=fdiv(micro*8,127); // snares rush
return n;
}
function shiftGroup(ev,test,by,barT){ for(var i=0;i<ev.length;i++) if(test(ev[i])) ev[i].t=mod(ev[i].t+by,barT); }
function tripletWarp(ev,beat){ // the four 16ths of one beat land on three triplet 8ths
var s=beat*TPB, out=[];
for(var i=0;i<ev.length;i++){ var e=ev[i]; if(e.t>=s && e.t<s+TPB){ var k=Math.min(2,fdiv((e.t-s)*3+TPB/2,TPB)); e.t=s+k*280; } out.push(e); }
return out;
}
/* The player's ROOT and SCALE pages: -1 (or absent) = AUTO, the seed's own pick. */
function applyKey(g,p){
if(typeof p.scale==='number' && p.scale>=0 && p.scale<SCALES.length) g.scale=p.scale;
if(typeof p.root==='number' && p.root>=0 && p.root<12) g.root=p.root;
return g;
}
/* 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,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)));
var variants=[], r;
var dr=clamp(p.drift|0,0,127), dVar=Math.min(127,dr*3), micro=amt(dr,20,127), wob=amt(dr,40,127), slip=amt(dr,55,127), disp=amt(dr,80,127), warp=amt(dr,105,127);
if(amen){ micro=fdiv(micro,2); wob=fdiv(wob,2); slip=fdiv(slip,2); disp=fdiv(amt(dr,90,127),2); warp=amt(dr,115,127); } // the break already has its feel: the generic layers come in later and softer
var dRng=new Rng(mix(mix(st.seed,0x445249),bar)), barSeed=mix(st.seed,0x4E55+bar);
for(r=0;r<2;r++) variants.push(phrase>0 && new Rng(mix(mix(roleSeed(st,r),0xD1F7),phrase)).chance(P(dVar)) ? phrase : 0);
var e=clamp(p.energy|0,0,127), krs=roleSeed(st,0), cS=mix(krs,0x434F), vS=mix(mix(krs,0x5641+sec),variants[0]);
var arcD=(g.arc===1)?(pos*10-15):0;
var kd=clamp(g.dens[0]+e-64+arcD,0,127), hd=clamp(g.dens[0]+((e-64)>>2)+arcD,0,127); // energy moves hats only a quarter as much
var fs=frameSets(g,g.steps,pos);
var kit;
if(amen){
kit=humanizeAmen(kitAmen(krs,bar,pos,dr,fill),krs,bar); // the break's own velocities, humanized, untouched by the shaping below
if(slip){ var jt=fdiv(slip*24,127); for(i=0;i<kit.length;i++) kit[i].vel=clamp(kit[i].vel+dRng.below(2*jt+1)-jt,1,127); }
} else {
kit = p.gen===0 ? kitEuclid(cS,vS,g.steps,kd,hd,pos,fs)
: p.gen===1 ? kitSkeleton(cS,vS,g.steps,kd,hd,pos,fs,g.frame)
: p.gen===2 ? kitSubdiv(cS,vS,g.steps,kd,hd,pos,fs)
: kitCA(cS,vS,g.steps,kd,hd,pos,sec,fs);
kit=transformKit(kit,g.steps,sec,krs,fs);
kit=arcKit(kit,g,pos);
if(fill) kit=kitFill(kit,g.steps,p.energy,fr);
var aoff = dRng.chance(P(slip)) ? 1+dRng.below(3) : 0;
kit=shapeKit(kit,g,pos,aoff,fdiv(slip*24,127),dRng);
}
if(disp){ // whole parts slide a 16th for this bar
var dirs=[TPS,-TPS];
if(dRng.chance(P(disp))) shiftGroup(kit,function(x){ return x.v===2; },dirs[dRng.below(2)],barTicks);
if(dRng.chance(P(disp))) shiftGroup(kit,function(x){ return x.v===3; },dirs[dRng.below(2)],barTicks);
if(dRng.chance(P(disp>>1))) shiftGroup(kit,function(x){ return x.v===1 && x.vel<90; },dirs[dRng.below(2)],barTicks);
if(dRng.chance(P(disp>>1))) shiftGroup(kit,function(x){ return x.v===0 && !x.a && x.t>0; },dirs[dRng.below(2)],barTicks);
if(dRng.chance(P(disp>>2))) shiftGroup(kit,function(x){ return x.v===1 && x.vel>=90; },TPS,barTicks);
}
var warpBeat = warp && dRng.chance(P(warp)) ? dRng.below(Math.max(1,fdiv(g.steps,4))) : -1;
if(warpBeat>=0) kit=tripletWarp(kit,warpBeat);
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 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
var nt=barTicks; for(j=i+1;j<kit.length;j++) if(kit[j].v===kit[i].v){ nt=kit[j].t; break; }
var len=Math.max(10,Math.min(105,nt>t?nt-t:105));
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, 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,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); }
mono(melA,melLen); mono(melB,melLen);
mono(lt.bass,function(gap){ return gap-10; });
for(i=0;i<lt.pad.length;i++) out.push({t:0,lane:1,v:lt.pad[i].v,vel:lt.pad[i].vel,len:barTicks-30,tr:T_PAD});
out.sort(function(a,b){ return a.t-b.t || a.tr-b.tr || a.v-b.v; });
return {bar:bar,pos:pos,phrase:phrase,sec:sec,fill:fill,variants:variants,barTicks:barTicks,steps:g.steps,events:out};
}
var ROMAN=['i','ii','iii','iv','v','vi','vii'];
function describe(st,p){
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');
if(g.arc) s.push(ARC_NAMES[g.arc]);
if(g.accentCycle) s.push('accents every '+g.accentCycle);
}
if(p.gen===0){ var dd=clamp(g.dens[0]+(p.energy|0)-64,0,127), E=euclidParams(mix(roleSeed(st,0),0x434F),g.steps,dd,clamp(g.dens[0]+(((p.energy|0)-64)>>2),0,127));
s.push('kick E('+E.kick.k+','+E.kick.n+') hat E('+E.hat.k+','+E.hat.n+') perc E('+E.perc.k+','+E.perc.n+')'); }
if(g.rot) s.push('melody + perc shifted '+g.rot+'/16');
return s.join(' · ');
}
function checksum(rows){ var h=0x811C9DC5>>>0; for(var i=0;i<rows.length;i++) for(var j=0;j<rows[i].length;j++){ h=Math.imul(h^(rows[i][j]&0xFFFF),0x01000193)>>>0; } return h; }
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,KEYS_NAMES:KEYS_NAMES};
})();
if(typeof module!=='undefined') module.exports=Engine;