1// LIGHTNING IN A BOTTLE — metal anthem, 150bpm, 64 bars as written; Jev arranges 7-16 sections. Spec: SPEC.md 2// Start from STOPPED so the song begins at bar 1 (re-running it mid-play keeps the cycle count). 3setcps(150/60/4) 4await samples('github:tidalcycles/dirt-samples') 5await samples('jev-samples/strudel.json') 6 7// bars: 1-8 intro | 9-16 riff | 17-24 verse | 25-32 chorus | 33-40 build 8// 41-48 drop | 49-56 drop2 | 57-64 outro (each mask = 64 steps) 9// Where each section starts as written: the form (walk) and the text Jev is 10// told (playsIn) both read it. 11const at = { intro: 0, riff: 8, verse: 16, chorus: 24, build: 32, drop: 40, drop2: 48, outro: 56 } 12 13const introBars = "<1!8 0!56>" 14const afterIntro = "<0!8 1!56>" 15const quoteOn = "<0 0 1 0!61>" 16const riffPedal = "<0!8 1!8 0!48>" 17const riffChord = "<0!16 1!16 0!24 1!4 0!4>" // verse, chorus, outro 18const riffBuild = "<0!32 1!8 0!24>" 19const riffHigh = "<0!24 1!16 0!24>" // chorus and build; the drop has its own 20const bassRoots = "<0!16 1!44 0!4>" 21const rock = "<0!8 1!16 0!8 1!8 0!16 1!4 0!4>" 22const chorus = "<0!24 1!8 0!32>" 23const hats = "<0!8 1!32 0!16 1!4 0!4>" 24const snare = "<0!8 1!52 0!4>" 25const drop = "<0!40 1!16 0!8>" 26const power = "<0!24 1!8 0!8 1!16 0!8>" 27const verse = "<0!16 1!8 0!40>" 28const outroVox = "<0!56 1 0!7>" 29const fills = "<0!7 1 0!56>" 30const roll39 = "<0!38 1 0!25>" 31const roll40 = "<0!39 1 0!24>" 32const riserBars = "<0!36 1!4 0!24>" 33const shotgun = "<0!8 1 0!23 1 0!7 1 0!23>" 34const crash = "<0!8 1 0!17 1 0 1 0 1 0!9 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0!9>" // 9, 27, 29, 31, the drop's chords 35const crashSoft = "<0!24 1 0!39>" // 25, under the sung "Th-" 36const final = "<0!60 1 0!3>" 37 38// Each part's masks, the one place that says where it plays: the parts 39// below play from these, and the text Jev is told is derived from them. 40const where = { 41 riff: [riffPedal, riffChord, riffBuild, riffHigh, drop], 42 chords: [power], 43 bass: [introBars, bassRoots], 44 drums: [fills, rock, chorus, hats, snare, drop, roll39, roll40, riserBars, crash, crashSoft, shotgun], 45 vocals: [verse, chorus], 46} 47// a mask is on in a section if the pattern itself has an event there 48const onIn = (mask, from, to) => mask.queryArc(from, to).some((h) => h.value && Number(h.value) !== 0) 49const sectionNames = Object.keys(at) 50const sectionsOf = (masks) => sectionNames.filter((name) => 51 masks.some((m) => onIn(m, at[name], at[name] + 8))) 52const playsIn = (partName) => 'plays in: ' + sectionsOf(where[partName]).join(', ') 53const plays = (section) => 'plays: ' + Object.keys(where).filter((p) => sectionsOf(where[p]).includes(section)).join(', ') 54 55// ── Jev: two of them, in TypeSafe's own shape (SPEC.md, "Jev in the booth") ── 56// formJev picks the next section; djJev, asked once that is decided (questions 57// in one request cannot see each other), says how much of each part plays it, 58// how the song moves into it, and how hard. Every 8 bars. The form's rules (walk, at the bottom) narrow the 59// sections Jev may pick and end the song; without Jev it plays as written. 60const song = { 61 title: 'LIGHTNING IN A BOTTLE', 62 what: 'a metal anthem on the Doom E1M1 riff, about using Jev properly: code decides what code can decide, Jev takes the rest', 63 tempo: '150 bpm, 4/4, 8 bars a section', 64 key: 'E minor', 65 chords: 'Em Em C C G G D D, two bars each, under the riff and the power chords', 66 lyrics: { 67 intro: 'We have lightning in a bottle, and yet it is not useful.', 68 verse: 'Remove the human from the loop / Routine calls never leave the machine / Only the residue goes to Jev / Forty-four out of forty-four', 69 chorus: 'The code decides what code can decide / Jev takes the rest', 70 outro: 'The code decides what code can decide', 71 }, 72} 73 74const formJev = jev({ 75 state: song, 76 questions: { 77 section: choice('Pick the section of the song that plays next, from those allowed. The outro ends the song: choose it once the song has built to its peak and made its point, before it drags', { 78 intro: 'a sub pulse and a plain robot voice reading the quote; ' + plays('intro'), 79 riff: 'the E1M1 riff on one note, the rock beat starts, a shotgun blast; ' + plays('riff'), 80 verse: 'the riff follows the chords under the vocoded verse: "Remove the human from the loop … Forty-four out of forty-four"; ' + plays('verse'), 81 chorus: 'the chorus: power chords, the riff an octave up, a fuller beat, the hook sung; ' + plays('chorus'), 82 build: 'the build: the riff\'s filter opening, a noise riser, a snare roll into the drop; ' + plays('build'), 83 drop: 'the drop, first half: sixteenth double kick, the guitars wide open, power chords, crashes; ' + plays('drop'), 84 drop2: 'the drop, second half, the same; ' + plays('drop2'), 85 outro: 'the last hook line, then the E power chord and a crash left to ring: the end; ' + plays('outro'), 86 }), 87 }, 88}) 89 90// how much of each part the section decidedThisSection has: out, back or full 91const level = (part, what) => score('How much of ' + part + ' (' + what + '; ' + playsIn(part) + ') should the next section (decidedThisSection) have? Drop parts to build tension or make space, pull them back to let others speak, and bring them in full for the payoff; keep what defines the section', [ 92 'out: silent for the section', 93 'back: quieter than written', 94 'full: at its written level', 95]) 96const djJev = jev({ 97 state: song, 98 questions: { 99 riff: level('riff', 'the E1M1 riff, the song\'s signature'), 100 chords: level('chords', 'the power chords, the weight of the band'), 101 bass: level('bass', 'the sub on the chord roots, low'), 102 drums: level('drums', 'the drums, the crashes and the shotgun'), 103 vocals: level('vocals', 'the vocoded verse and chorus lines'), 104 into: choice('How should the song move into the next section (decidedThisSection)? It plays over the last bar of the section before it', { 105 cut: 'straight in, no transition', 106 fill: 'a drum fill down the toms into the first beat', 107 riser: 'white noise swelling up over the last bar', 108 dropout: 'everything stops for the last beat, a gap before the section hits', 109 }), 110 energy: score('How intense should the next section be? Shape the energy of the whole song, following its history, how it measured and how the listener reacted', [ 111 'held back: quieter', 112 'steady: a little under full', 113 'driving: full volume', 114 'all out: full volume, nothing held back', 115 ]), 116 }, 117}) 118 119// a little chance in the form: the section is drawn from Jev's probabilities 120const { section } = formJev.every(8, { sample: 0.7 }) 121// The build has its own riser, so the move into the drop is never a riser. 122const dj = djJev.every(8, { 123 after: { section }, 124 forget: ['into'], 125 allow: { into: ({ section: next }) => (next === 'drop' ? ['cut', 'fill', 'dropout'] : ['cut', 'fill', 'riser', 'dropout']) }, 126 fallback: { riff: 2, chords: 2, bass: 2, drums: 2, vocals: 2, into: 'cut', energy: 3 }, 127}) 128 129// how the song moves into the next section, over the last bar of each 130const into = dj.into.choice.early(1) 131const onCutOr = (...moves) => (c) => moves.includes(c) 132 133// A part at Jev's level: silent when out, otherwise its velocity scaled by 134// the level. The intro is not Jev's: it plays every part as written (level 135// 2). Parts are composed inside the arrangement, so these are written bars. 136// Each part has its own orbit, named for its question, so the meter hears 137// it alone (measuredSound). 138const levelOf = (name) => stack(pure(2).mask(introBars), dj[name].score.mask(afterIntro)) 139const part = (name, pat) => { 140 const lvl = levelOf(name) 141 return pat.orbit(name).mask(lvl.round().gt(0)).mul(velocity(lvl.round().div(2))) 142} 143// energy scales the parts together; the intro plays at full 144const energy = stack(pure(1).mask(introBars), dj.energy.score.div(3).range(.55, 1).mask(afterIntro)) 145 146// E1M1 ("At Doom's Gate"), eighths, MIDI numbers: E2=40 E3=52 D3=50 C3=48 A#2=46 B2=47 147const riff = "<[40 40 52 40 40 50 40 40] [48 40 40 46 40 40 47 48] [40 40 52 40 40 50 40 40] [48 40 40 46@5]>" 148// two bars per chord: Em Em C C G G D D — the riff follows the root 149const shift = "<0!2 -4!2 3!2 -2!2>" 150const roots = "<28!2 24!2 31!2 26!2>" // E1 C1 G1 D1 151const chords = "<[e2,b2,e3]!2 [c2,g2,c3]!2 [g2,d3,g3]!2 [d2,a2,d3]!2>" 152 153// a dropout's gap is silent: the section's last bar is cut short before its 154// last beat, so the held A#2 does not ring through it 155const gapClip = stack(pure(1).mask("<1!7 0>"), into.fmap((c) => (c === 'dropout' ? .6 : 1)).mask("<0!7 1>")) 156 157// ── parts ───────────────────────────────────────── 158const saw1 = (p) => p.s("sawtooth").decay(.14).sustain(.2).distort(1.5) 159const gtr = stack( 160 saw1(note(riff)).lpf(1500).gain(.35).mask(riffPedal), 161 saw1(note(riff).add(note(shift))).lpf(1500).gain(.35).mask(riffChord), 162 // the build: the filter opens from dull to bright across its 8 bars 163 saw1(note(riff).add(note(shift))).lpf(saw.slow(8).range(700, 3000)).gain(.35).mask(riffBuild), 164 // the drop: the guitars open up 165 saw1(note(riff).add(note(shift))).lpf(3000).gain(.35).mask(drop), 166 note(riff).add(note(shift)).add(note(12)).s("square").decay(.1).sustain(.1).lpf(2500).gain(.14).mask(riffHigh), 167 note(riff).add(note(shift)).add(note(12)).s("square").decay(.1).sustain(.1).lpf(3000).gain(.25).mask(drop), 168).clip(gapClip) 169 170// power chords, chorus and drop 171const stabs = note(chords).struct("x ~ ~ x ~ ~ x ~").s("sawtooth").decay(.3).sustain(.3) 172 .lpf(2200).distort(2).gain(.22).mask(power) 173 174// sub: a pulse in the intro, then the chord roots, each with a triangle an 175// octave up so small speakers hear it 176const sub = stack( 177 note("28*4").s("sine").decay(.35).sustain(0).gain(.7).mask(introBars), 178 note("40*4").s("triangle").decay(.35).sustain(0).lpf(300).gain(.25).mask(introBars), 179 note(roots).struct("x x x x").s("sine").decay(.3).sustain(.3).gain(.55).mask(bassRoots), 180 note(roots).add(note(12)).struct("x x x x").s("triangle").decay(.3).sustain(.3).lpf(250).gain(.2).mask(bassRoots), 181) 182 183const kit = stack( 184 s("bd:3").struct("x ~ ~ x x ~ ~ ~").gain(.6).mask(rock), 185 s("bd:3").struct("x ~ x x x ~ x ~").gain(.6).mask(chorus), 186 s("bd:3").struct("x*16").gain(.3).mask(drop), 187 s("808bd:1*4").gain(.2).mask(drop), 188 s("sn:3").struct("~ ~ x ~ ~ ~ x ~").room(.3).gain(.52).mask(snare), 189 s("hh*8").gain(".16 .08").mask(hats), 190 s("cr*4").gain(.25).mask(drop), // cr is a ride 191 // the intro's written fill: only when the move into the riff is a cut 192 s("ht ht mt mt lt lt lt lt").gain(.5).mask(fills).mask(into.fmap(onCutOr('cut'))), 193 // the build's roll, one crescendo over bars 39-40: only on a cut or a 194 // dropout (bar 39 looks two bars ahead for the drop's answer) 195 s("sn:3*16").gain(saw.slow(2).range(.16, .55)).mask(roll39).mask(dj.into.choice.early(2).fmap(onCutOr('cut', 'dropout'))), 196 s("sn:3*16").gain(saw.slow(2).range(.16, .55)).mask(roll40).mask(into.fmap(onCutOr('cut', 'dropout'))), 197 // the build's riser, bars 37-40 198 s("white*16").hpf(saw.slow(4).range(400, 8000)).gain(saw.slow(4).range(.03, .25)).decay(.07).sustain(0).mask(riserBars), 199 s("hh27:1").gain(3).mask(crash), // hh27:1 is the crash 200 s("hh27:1").gain(2.1).mask(crashSoft), // 3 dB down, under "Th-" 201 s("lbshotgun").gain(.85).mask(shotgun), 202) 203 204const vox = stack( 205 s("<lbremove ~ lbroutine ~ lbresidue ~ lbfortyfour ~>").gain(.85).room(.2).mask(verse), 206 s("<lbcodeem ~ lbcodec ~ lbrestg ~ lbrestd ~>").gain(.85).room(.25).mask(chorus), 207) 208 209// the story: the quote and the outro's hook line. Not Jev's: they always play. 210const story = stack( 211 s("lbquote").gain(.85).room(.4).mask(quoteOn), 212 s("lbcodeem").gain(.85).room(.4).mask(outroVox), 213).orbit('vocals') 214 215// the end: E power chord, crash, kick and shotgun, left to ring. It always plays. 216const ending = stack( 217 note("[e2,b2,e3]").s("sawtooth").decay(5.5).sustain(0).lpf(2000).distort(1.5) 218 .room(.7).size(.95).gain(.3).mask(final), 219 s("[bd:3,808bd:1]").room(.6).gain(.5).mask(final), 220 s("hh27:1").room(.6).gain(2.5).mask(final), 221 s("lbshotgun").gain(.85).mask(final), 222).mul(gain(.45)) // with the band: measured 6 dB over it at full, the master's peak 223 224const once = (p) => p.mask("<1!64 0!936>") // play once, then silence 225 226// ── the form, as rules in code: which sections may follow which ── 227// `at` is where each starts as written; an empty `next` ends the song. The 228// written order is always allowed, and is what plays without Jev. One drop: 229// drop2 never leads back into it. 230const form = walk(section, { 231 max: 16, 232 sections: { 233 intro: { at: at.intro, next: ['riff'] }, 234 riff: { at: at.riff, next: ['verse', 'chorus'] }, 235 verse: { at: at.verse, next: ['chorus', 'riff'] }, 236 chorus: { at: at.chorus, next: ['build', 'verse'] }, 237 build: { at: at.build, next: ['drop'] }, 238 drop: { at: at.drop, next: ['drop2'] }, 239 drop2: { at: at.drop2, next: ['verse', 'chorus', 'outro'] }, 240 outro: { at: at.outro, next: [] }, 241 }, 242}) 243 244// ── the song: Jev's section, parts and energy, composed ── 245// Each section plays its own bars, each part at the level Jev chose; the 246// energy scales the parts' velocity together, so the mix keeps its 247// balance. The story and the ending always play. 248const parts = form.arrange(once(stack( 249 part('riff', gtr.mul(gain(.8))), 250 part('chords', stabs.mul(gain(.8))), 251 part('bass', sub), 252 part('drums', kit.mul(gain(.42))), // the drums 7.5 dB down: measured 10 dB over the vocals 253 part('vocals', vox), 254).mul(velocity(energy)))) 255 256// ── into the next section: over the last bar of each, the next one's answer ── 257// Past the last section there is no next one, so nothing plays there. The 258// fill is the drums', at the level of the section it ends; this is played 259// time, not written bars, so the intro (always the first 8) plays it full. 260const drumsNow = stack(pure(2).mask("<1!8 0!992>"), dj.drums.score.mask("<0!8 1!992>")) 261const transition = into.pick({ 262 cut: silence, 263 fill: s("ht ht mt mt lt lt [lt lt] [sn:3 sn:3]").gain(.5).orbit('drums') 264 .mask(drumsNow.round().gt(0)).mul(velocity(drumsNow.round().div(2))), 265 riser: s("white*16").hpf(saw.range(400, 8000)).gain(saw.range(.05, .35)).decay(.07).sustain(0).orbit('riser'), 266 dropout: silence, 267}).mask("<0!7 1>") 268// dropout: the parts stop for the section's last beat. The outro has no 269// next section, so its last beat plays. 270const lastBeat = "<0!7 [0 0 0 1]>" 271const gap = stack( 272 "<1!7 [1 1 1 0]>", 273 into.fmap((c) => c !== 'dropout').mask(lastBeat), 274 section.choice.fmap((s) => s === 'outro').mask(lastBeat), 275) 276 277stack(parts.mask(gap), transition, form.arrange(once(stack(story, ending)))) 278 .mul(postgain(.52)) // the master level: peaks under -1 dBFS (superdough has no limiter)