1// YOU'VE GOT JEV — 2-step garage, 132bpm, 64 bars as written; Jev arranges up to 24 four-bar 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(132/60/4) 4await samples('github:tidalcycles/dirt-samples') 5await samples('jev-samples/strudel.json') 6 7// ── Jev: two of them, in TypeSafe's own shape (SPEC.md, "Jev in the booth") ── 8// formJev picks the next section; djJev, asked once that is decided (questions 9// in one request cannot see each other), says how much of each part plays it, 10// how the song moves into it, and how hard. Every 4 bars. The form's rules (walk, at the bottom) narrow the 11// sections Jev may pick and end the song; without Jev it plays as written. 12const song = { 13 title: 'YOU’VE GOT JEV', 14 what: '2-step garage about Jev routing support mail: AOL dials in, a ticket arrives, and Jev routes it in a blink with no human in the loop', 15 tempo: '132 bpm, 4/4, swung, 4 bars a section', 16 key: 'C minor', 17 chords: 'Cm9 – Fm9 – Bbmaj7 – Abmaj7, two bars each (an 8-bar cycle), under the stabs and the sub', 18 lyrics: { 19 verse1a: 'New ticket in / Billing, bug, or refund', 20 verse1b: 'Choice: billing / Point nine three', 21 hook1: 'You’ve got mail / You’ve got Jev / You’ve got mail / You’ve got Jev', 22 verse2a: 'No human in the loop / Routed in a blink', 23 verse2b: 'Nobody had to read it / You’ve got Jev', 24 hook2: 'You’ve got mail / You’ve got Jev / You’ve got mail / You’ve got Jev', 25 outro: 'Goodbye', 26 }, 27} 28 29const formJev = jev({ 30 state: song, 31 questions: { 32 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', { 33 modem: 'a dial-up modem alone: the dial tone, the number dialed, the answer tone', 34 handshake: 'the modem handshake screech chopped into a rhythm, hats and muffled stabs coming in', 35 verse1a: 'verse 1, first half: 2-step drums, swung stabs, the warbly sub, vocoded lines', 36 verse1b: 'verse 1, second half', 37 hook1: 'the hook: AOL asks and the vocoder answers', 38 drop1a: 'drop 1: instrumental, open hi-hats, a crash', 39 drop1b: 'drop 1 continues', 40 drop1c: 'drop 1, last four bars', 41 verse2a: 'verse 2, first half', 42 verse2b: 'verse 2, second half', 43 hook2: 'the hook again, into the big drop', 44 drop2a: 'drop 2, the peak: the screech chops return over the full groove, a crash', 45 drop2b: 'drop 2 continues', 46 drop2c: 'drop 2 continues', 47 drop2d: 'drop 2, last four bars', 48 outro: 'a last hit and a ringing chord, then AOL says goodbye: the end', 49 }), 50 }, 51}) 52 53// how much of each part the section decidedThisSection has: out, back or full 54const level = (part) => score('How much of ' + 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', [ 55 'out: silent for the section', 56 'back: under the rest of the mix', 57 'full: as written', 58]) 59const djJev = jev({ 60 state: song, 61 questions: { 62 modem: level('the modem (a dial-up modem dialing in alone for the first four bars, the handshake screech chopped into a rhythm in the next four, and the chops again over drop 2)'), 63 drums: level('the drums (the 2-step kick and snare from verse 1 on, swung hi-hats from bar 5, open hats on the off-beats and crashes in the drops)'), 64 stabs: level('the stabs (short swung organ-style chord stabs, muffled at the end of the handshake, then from verse 1 to the outro)'), 65 sub: level('the sub (the warbly sub bass on the chord roots, the floor from verse 1 to the outro)'), 66 vocals: level('the vocals (the vocoded ticket lines in the verses, and AOL asking and the vocoder answering in the hooks; the drops are instrumental as written)'), 67 into: choice('How should the song move into the next section (decidedThisSection)? It plays over the last bar of the section before it', { 68 cut: 'straight in, no transition', 69 fill: 'a snare and clap roll in sixteenths, swelling into the downbeat, over the last half bar', 70 riser: 'the modem handshake screech, chopped into sixteenths, its filter sweeping up over the last half bar', 71 dropout: 'everything stops for the last beat, a gap before the section hits', 72 }), 73 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', [ 74 'held back: quieter', 75 'steady: under full', 76 'driving: nearly full', 77 'all out: full volume', 78 ]), 79 }, 80}) 81 82// a little chance in the form: the section is drawn from Jev's probabilities 83const { section } = formJev.every(4, { sample: 0.7 }) 84const dj = djJev.every(4, { 85 after: { section }, 86 forget: ['into'], 87 // the intro builds from the modem alone, so the handshake always cuts in 88 allow: { into: ({ section: next }) => (next === 'handshake' ? ['cut'] : ['cut', 'fill', 'riser', 'dropout']) }, 89 fallback: { modem: 2, drums: 2, stabs: 2, sub: 2, vocals: 2, into: 'cut', energy: 3 }, 90}) 91 92// Jev's most probable level. A score's probabilities are keyed by level, 93// "0", "1", … (docs.typesafe.ai/primitives/score); a fallback has none, so 94// it plays the level its score names. 95const mostLikely = (p, score) => { 96 const levels = Object.entries(p ?? {}) 97 return levels.length ? Number(levels.reduce((a, b) => (b[1] > a[1] ? b : a))[0]) : Math.round(score) 98} 99const fromAnswer = (answer, f) => answer.score.fmap((score) => (p) => f(p ?? {}, score)).appLeft(answer.probabilities) 100// A part's gain: silent when "out" is Jev's most probable level; otherwise 101// Jev's probability-weighted mix of back (0.5) and full (1). 102const partGain = (p, score) => { 103 const top = mostLikely(p, score) 104 if (top === 0) return 0 105 const back = p['1'] ?? 0, full = p['2'] ?? 0 106 return back + full > 0 ? (back * .5 + full) / (back + full) : top === 1 ? .5 : 1 107} 108// A part at Jev's level. Each part has its own orbit, named for its 109// question, so the meter hears it alone (measuredSound). 110const part = (name, pat) => { 111 const gain = fromAnswer(dj[name], partGain) 112 return pat.orbit(name).mask(gain.gt(0)).mul(velocity(gain)) 113} 114 115// bars: 1-4 modem | 5-8 chopped handshake | 9-16 verse 1 | 17-20 hook | 21-32 DROP 1 116// 33-40 verse 2 | 41-44 hook | 45-60 DROP 2 | 61-64 outro (each mask = 64 steps) 117const modemOn = "<1 0!63>" 118const chopIn = "<0!4 1!4 0!56>" 119const stabIn = "<0!6 1!2 0!56>" 120const groove = "<0!8 1!52 0!4>" 121const verse1 = "<0!8 1!8 0!48>" 122const verse2 = "<0!32 1!8 0!24>" 123const hooks = "<0!16 1!4 0!20 1!4 0!20>" 124const drops = "<0!20 1!12 0!12 1!16 0!4>" 125const drop2 = "<0!44 1!16 0!4>" 126const crash = "<0!20 1 0!23 1 0!19>" 127const lastHit = "<0!60 1 0!3>" 128const bye = "<0!61 1 0!2>" 129 130// two bars per chord: Cm9 Cm9 Fm9 Fm9 Bbmaj7 Bbmaj7 Abmaj7 Abmaj7, voiced 131// close so the stabs move by step (each keeps the pitch classes its vocoded 132// lines were rendered on) 133const cm9 = "[c3,eb3,g3,bb3,d4]" 134const fm9 = "[f3,g3,ab3,c4,eb4]" 135const bbmaj7 = "[bb2,f3,a3,d4]" 136const abmaj7 = "[ab2,eb3,g3,c4]" 137const chords = cat(cm9, cm9, fm9, fm9, bbmaj7, bbmaj7, abmaj7, abmaj7) // a chord a bar 138const roots = "<36!2 29!2 34!2 32!2>" // C2 F1 Bb1 Ab1, as MIDI numbers 139const chops = "<[0 ~ 4 ~ 8 2 ~ 12] [~ 6 ~ 14 0 ~ 10 3]>" 140 141// ── parts ───────────────────────────────────────── 142const modem = stack( 143 // bars 1-4: the modem dialing in, alone (first 7.27 s of 7.45) 144 s("modem").end(.976).gain(.9).mask(modemOn), 145 // the handshake screech, chopped into a fixed rhythm 146 s("screech").slice(16, chops).hpf(300).gain(.55).mask(chopIn), 147 // drop 2: the chops return at the intro's level 148 s("screech").slice(16, chops).hpf(300).gain(.55).pan(.3).mask(drop2) 149) 150 151// drums: 2-step 152const drums = stack( 153 s("[bd:3,808bd:1]").struct("x ~ ~ ~ ~ ~ ~ ~ ~ ~ x ~ ~ ~ ~ ~").gain(.4).mask(groove), 154 s("[sn:3,cp]").struct("~ ~ ~ ~ x ~ ~ ~ ~ ~ ~ ~ x ~ ~ ~").room(.2).gain(.3).mask(groove), 155 s("sn:1").struct("~ ~ ~ ~ ~ ~ ~ x ~ ~ ~ ~ ~ ~ ~ x").swingBy(1/3, 8).gain(.1).mask(groove), 156 s("hh*16").gain(".32 .12 .22 .12").swingBy(1/3, 8).mask("<0!4 1!56 0!4>"), 157 // open hats on the off-beats in the drops 158 s("808oh").struct("~ x ~ x ~ x ~ x").gain(.2).mask(drops), 159 // hh27:1 is a crash; `cr` is a ride (songs/CLAUDE.md) 160 s("hh27:1").gain(1).mask(crash) 161) 162 163// organ-style stabs, short and swung 164const stabs = stack( 165 // the filter opens in the drops: 3000 in drop 1, 4000 in drop 2 166 note(chords).struct("~ ~ x ~ ~ x ~ ~ ~ x ~ ~ x ~ ~ ~").swingBy(1/3, 8) 167 .s("sawtooth").lpf("<1700!20 3000!12 1700!12 4000!16 1700!4>").decay(.16).sustain(0).room(.3).gain(.45).mask(groove), 168 note(chords).struct("~ ~ x ~ ~ x ~ ~ ~ x ~ ~ x ~ ~ ~").swingBy(1/3, 8) 169 .s("sawtooth").lpf(600).decay(.16).sustain(0).gain(.12).mask(stabIn) 170) 171 172// warbly sub: garage push rhythm, a slow vibrato a fifth of a semitone deep. 173// subClipped is the same line with the note on step 10 cut to two steps, for 174// a section's last bar when a dropout follows, so the sub stops for the gap. 175const subIn = (rhythm) => note(roots).struct(rhythm) 176 .s("sine").vib(.5).vibmod(.2).release(.02).gain(.7).mask(groove) 177const sub = subIn("x@6 x@4 x@6") 178const subClipped = subIn("x@6 x@4 x@2 ~@4") 179 180const vox = stack( 181 // verses: vocoder, one line per 2 bars, each sung on its chord 182 s("<newticket ~ billingbug ~ choicebilling ~ ninetythree ~>").gain(1.25).room(.2).mask(verse1), 183 s("<nohuman ~ routedblink ~ nobodyread ~ gotjevab ~>").gain("<1.25!4 1.5!2 1.25!2>").room(.2).mask(verse2), 184 // hook: AOL asks, the vocoder answers on the hook's chords 185 s("<mail gotjevcm mail gotjevfm>").gain(1.35).room(.25).mask(hooks) 186) 187 188// the end: the last hit, a ringing Cm9 over a C2 sub, and AOL's goodbye. It always plays. 189const ending = stack( 190 s("[bd:3,808bd:1]").room(.5).gain(.4).mask(lastHit), 191 s("hh27:1").room(.5).gain(1).mask(lastHit), 192 note(cm9).s("sawtooth").lpf(1400).decay(1.5).sustain(0).room(.6).gain(.45).mask(lastHit), 193 note("c2").s("sine").vib(.5).vibmod(.2).release(.3).gain(.7).mask(lastHit), 194 s("goodbye").gain(1.2).room(.5).mask(bye) 195) 196 197const once = (p) => p.mask("<1!64 0!936>") // play once, then silence 198 199// ── the form, as rules in code: which sections may follow which ── 200// `at` is where each starts as written; an empty `next` ends the song. The 201// written order is always allowed, and is what plays without Jev. Nothing 202// leads back into a hook, so each plays once; and every move goes to a 203// section starting 4 bars off in the 8-bar chord cycle, so the two halves of 204// the progression alternate. 205// `times`: each drop's loop goes round once more at most (SPEC.md, "Loops end"). 206const form = walk(section, { 207 max: 24, 208 sections: { 209 modem: { at: 0, next: ['handshake'] }, 210 handshake: { at: 4, next: ['verse1a'] }, 211 verse1a: { at: 8, next: ['verse1b'] }, 212 verse1b: { at: 12, next: ['hook1'] }, 213 hook1: { at: 16, next: ['drop1a', 'drop1c'] }, 214 drop1a: { at: 20, next: ['drop1b'] }, 215 drop1b: { at: 24, next: ['drop1c'], times: 2 }, 216 drop1c: { at: 28, next: ['verse2a', 'drop1b'] }, 217 verse2a: { at: 32, next: ['verse2b'] }, 218 verse2b: { at: 36, next: ['hook2'] }, 219 hook2: { at: 40, next: ['drop2a'] }, 220 drop2a: { at: 44, next: ['drop2b'], times: 2 }, 221 drop2b: { at: 48, next: ['drop2c', 'outro'], times: 2 }, 222 drop2c: { at: 52, next: ['drop2d', 'drop2b'] }, 223 drop2d: { at: 56, next: ['outro', 'drop2a'] }, 224 outro: { at: 60, next: [] }, 225 }, 226}) 227 228// ── into the next section: over the last bar of each, the next one's answer ── 229// Past the last section there is no next one, so nothing plays there. 230const into = dj.into.choice.early(1) 231const lastBar = "<0!3 1>" 232// fill and riser build over the second half of the bar, clear of the line 233// on its downbeat 234const transition = into.pick({ 235 cut: silence, 236 fill: s("~ [sn:3,cp]*8").room(.2).gain(saw.fast(2).range(.12, .34)).orbit('drums'), 237 riser: s("screech").slice(16, "~ [8 9 10 11 12 13 14 15]") 238 .hpf(saw.fast(2).range(300, 5000)).gain(saw.fast(2).range(.15, .45)).orbit('modem'), 239 dropout: silence, 240}).mask("<0!3 [0 1]>") 241// dropout: the parts stop for the section's last beat 242const lastBeat = "<0!3 [0 0 0 1]>" 243const gap = stack( 244 "<1!3 [1 1 1 0]>", 245 into.fmap((c) => c !== 'dropout').mask(lastBeat), 246) 247// the sub's last bar: clipped when a dropout follows, so it stops for the gap 248const dropoutBar = into.fmap((c) => c === 'dropout').mask(lastBar) 249const notDropoutBar = stack("<1!3 0>", into.fmap((c) => c !== 'dropout').mask(lastBar)) 250 251// ── the song: Jev's section, parts and energy, composed ── 252// Each section plays its own bars, each part at the level Jev chose; the 253// energy, Jev's most probable step, scales every part's velocity together, 254// so the mix keeps its balance. The ending always plays. 255const parts = form.arrange(once(stack( 256 part('modem', modem), 257 part('drums', drums), 258 part('stabs', stabs), 259 part('vocals', vox), 260))) 261const subPart = stack( 262 form.arrange(once(part('sub', sub))).mask(notDropoutBar), 263 form.arrange(once(part('sub', subClipped))).mask(dropoutBar), 264) 265const energy = fromAnswer(dj.energy, mostLikely) 266 267// postgain: the whole mix's level, so it peaks under −1 dBFS (SPEC.md, Style: Mix) 268stack(parts.mask(gap), subPart.mask(gap), transition, form.arrange(once(ending))) 269 .mul(velocity(energy.div(3).range(.55, 1))) 270 .postgain(.52)