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>
652 lines
41 KiB
JavaScript
652 lines
41 KiB
JavaScript
/* ================= 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;
|