1// FORTY-FIVE PERCENT — lo-fi hip hop, 80bpm, 48 bars as written; Jev arranges up to 16 sections and it fades out. Spec: SPEC.md 2// Start from STOPPED so the song begins at bar 1 (re-running it mid-play keeps the cycle count). 3setcps(80/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 10// it, how the song moves into it, and how hard. Every 4 bars. The form's 11// rules (walk, at the bottom) narrow the sections Jev may pick and end the 12// song; without Jev it plays as written. 13const song = { 14 title: 'FORTY-FIVE PERCENT', 15 what: 'lo-fi hip hop about a stock-picking bot: on a three-way guess (up, flat or down) it scored 45 percent, better than the 33 percent of a blind guess and worse than an index fund; it fades out on This is fine', 16 tempo: '80 bpm, 16ths swung by a third, 4 bars a section', 17 key: 'D dorian (C major)', 18 chords: 'Dm7 Dm7 Cmaj7 Cmaj7 Fmaj7 Fmaj7 Em7 Em7, two bars each', 19 lyrics: { 20 verse1: 'Forty-five percent / Up, flat, or down', 21 verse1b: 'Better than a coin / Worse than an index fund', 22 verse2: 'A disciplined reasoning layer / Not a trading model', 23 verse2b: 'Don\'t trust the word / Probability', 24 outroEnd: 'This is fine', 25 }, 26} 27 28const formJev = jev({ 29 state: song, 30 questions: { 31 section: choice('Pick the section of the song that plays next, from those allowed. The outro starts the fade that ends the song: choose it once the song has built to its peak and made its point, before it drags', { 32 intro: 'Rhodes keys with tape wobble, vinyl crackle and the chest-thump hum loop', 33 verse1: 'first half of verse 1, vocoded, over the boom-bap and bass', 34 verse1b: 'second half of verse 1, vocoded', 35 beat: 'the beat alone: the hum loop with Stonks chopped into it, no vocals', 36 beatB: 'the beat alone, second half', 37 verse2: 'first half of verse 2, vocoded', 38 verse2b: 'second half of verse 2, vocoded', 39 beatFull: 'the fuller beat: sixteenth hats, an open hat, a kick variation, the bass up to the fifth', 40 beatFullB: 'the fuller beat, middle', 41 beatFullC: 'the fuller beat, last four bars', 42 outro: 'the drums fall away and the bed starts to fade', 43 outroEnd: 'the bed fades down over the crackle, then This is fine: the end', 44 }), 45 }, 46}) 47 48// how much of each part the section decidedThisSection has: out, back or full 49const 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', [ 50 'out: silent for the section', 51 'back: under the rest of the mix', 52 'full: as written', 53]) 54const djJev = jev({ 55 state: song, 56 questions: { 57 keys: level('the keys (Rhodes chords with tape wobble and the vinyl crackle, the bed under every section as written)'), 58 loop: level('the hum loop (the chest-thump hum\'s thumps on the beat grid, with Stonks chopped into it in the beat sections, from the intro to the outro)'), 59 drums: level('the drums (the swung boom-bap kick, snare and hats, from verse 1 until the outro)'), 60 bass: level('the bass (a round filtered bass locked to the kick, from verse 1 until the outro)'), 61 // the words are always sung: in a verse Jev only sets how far forward 62 vocals: choice('How far forward should the vocals (the vocoded verse lines) sit in the next section (decidedThisSection)? They are always sung', { 63 full: 'full: as written, the words up front', 64 back: 'back: a little under the beat', 65 }), 66 into: choice('How should the song move into the next section (decidedThisSection)? It plays over the last bar of the section before it', { 67 cut: 'straight in, no transition', 68 fill: 'a swung snare fill tumbling into the first beat, in place of the drums for that bar', 69 riser: 'the vinyl crackle thickening into a hiss that swells over the last bar', 70 dropout: 'everything stops for the last beat, a gap before the section comes in', 71 }), 72 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', [ 73 'hushed: background music, well under full', 74 'laid back: a little under full', 75 'as written', 76 'up front: louder than written', 77 ]), 78 }, 79}) 80 81// a little chance in the form: the section is drawn from Jev's probabilities 82const { section } = formJev.every(4, { sample: 0.7 }) 83const dj = djJev.every(4, { 84 after: { section }, 85 forget: ['into'], 86 // vocals only in the verses: elsewhere there is nothing to ask 87 allow: { vocals: (d) => (/^verse/.test(d.section) ? ['back', 'full'] : ['full']) }, 88 fallback: { keys: 2, loop: 2, drums: 2, bass: 2, vocals: 'full', into: 'cut', energy: 2 }, 89}) 90// A part at Jev's level: silent when out, otherwise its velocity scaled by 91// the level. Each part has its own orbit, named for its question, so the 92// meter hears it alone (measuredSound). 93const part = (name, pat) => 94 pat.orbit(name).mask(dj[name].score.round().gt(0)).mul(velocity(dj[name].score.round().div(2))) 95// The vocals are never out: back or full 96const vocals = (pat) => 97 pat.orbit('vocals').mul(velocity(dj.vocals.choice.fmap((c) => (c === 'back' ? .65 : 1)))) 98 99// bars: 1-4 intro | 5-12 verse 1 | 13-20 beat | 21-28 verse 2 | 29-40 beat, fuller 100// 41-48 outro: drums fall away, the bed fades (each mask = 48 steps) 101const verse1 = "<0!4 1!8 0!36>" 102const verse2 = "<0!20 1!8 0!20>" 103const kick = "<0!4 1!40 0!4>" 104const snare = "<0!4 1!38 0!6>" 105const hats8 = "<0!4 1!24 0!20>" 106const hats16 = "<0!28 1!14 0!6>" 107const fuller = "<0!28 1!12 0!8>" 108const bassOn = "<0!4 1!24 0!12 1!4 0!4>" // the plain bass line; the fuller beat has its own 109// Stonks never plays in a section's last bar: that bar is the transition's. 110// Chopped into stutters in each beat section's first two bars, the whole 111// word on the last 8th of its third. 112const chops = "<0!12 1 1 0 0 1 1 0 0 0!8 1 1 0 0 1 1 0 0 1 1 0 0 0!8>" 113const stab = "<0!12 0 0 1 0 0 0 1 0 0!8 0 0 1 0 0 0 1 0 0 0 1 0 0!8>" 114const humOn = "<1!44 0!4>" 115const humGain = "<.31!4 .21!8 .31!8 .21!8 .34!12 .24!4 .18!4>" 116// the bed falls to about -12 dB, never to nothing; the crackle is not in it 117const fade = "<1!40 .85 .72 .6 .5 .4 .32 .25 0>" 118const fine = "<0!46 1 0>" 119 120// two bars per chord; the cycle starts on Dm7 so both verses open on Fmaj7: 121// Dm7 Dm7 Cmaj7 Cmaj7 Fmaj7 Fmaj7 Em7 Em7 (MIDI numbers) 122const chords = "<[50,53,57,60]!2 [48,52,55,59]!2 [53,57,60,64]!2 [52,55,59,62]!2>" 123const roots = "<38!2 36!2 41!2 40!2>" // D2 C2 F2 E2 124const comp = "x ~ ~ ~ ~ ~ x ~ ~ ~ ~ ~ x ~ ~ ~" 125 126// ── parts ───────────────────────────────────────── 127// Every part is straight here; one swing, 16ths by a third, is applied to 128// the whole band below. 129// ── keys: Rhodes-ish comping; the tape wobble is a slow vibrato ── 130const keys = stack( 131 note(chords).struct(comp) 132 .s("triangle").attack(.01).decay(.9).sustain(.15).release(.4) 133 .lpf(1800).room(.4).gain(.17), 134 note(chords).add(note(12)).struct(comp) 135 .s("sine").decay(.5).sustain(0).gain(.045) 136).vib(.7).vibmod(.1) 137 138// vinyl crackle: sparse, fixed, high-passed noise ticks 139const crackle = s("white").struct("x ~ ~ ~ ~ ~ ~ x ~ ~ ~ ~ x ~ ~ ~ ~ x ~ ~ ~ ~ ~ ~ ~ ~ x ~ ~ ~ ~ ~") 140 .decay(.01).sustain(0).hpf(5000).gain(.068) 141 142// ── percussion loop: the chest-thump hum's thumps, one per slice of ffhum 143// (0 A, 1 E, 2 D, 3 a short thump), on the 8th grid, and Stonks ── 144const loop = stack( 145 s("ffhum").slice(8, "0 ~ 1 ~ 2 ~ 3 1").lpf(3000).gain(humGain).mask(humOn), 146 s("ffstonks").slice(4, "~ ~ ~ ~ ~ [0 0] ~ [0 1 2 3]").clip(1).gain(.28).mask(chops), 147 s("ffstonks").struct("~ ~ ~ ~ ~ ~ ~ x").gain(.28).mask(stab) 148) 149 150// ── drums: dusty boom-bap ── 151const drums = stack( 152 s("bd:4").struct("x ~ ~ ~ ~ ~ ~ x ~ ~ x ~ ~ ~ ~ ~").lpf(3500).gain(.31).mask(kick), 153 // the fuller beat's two-bar kick variation: a pickup into every other bar 154 s("bd:4").struct("<~ [~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ x]>").lpf(3500).gain(.26).mask(fuller), 155 s("sn:2").struct("~ ~ ~ ~ x ~ ~ ~ ~ ~ ~ ~ x ~ ~ ~").lpf(3500).room(.2).gain(.26).mask(snare), 156 s("hh*16").gain(".16 0 .08 0").lpf(6000).mask(hats8), 157 s("hh*16").gain(".14 .056 .1 .056").lpf(6000).mask(hats16), 158 // the fuller beat's open hat on the "and" of 4 159 s("808oh").struct("~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ x ~").lpf(7000).gain(.045).mask(fuller) 160) 161 162// ── bass: locked to the kick, root, root, 5th ── 163const bassSound = (pat) => pat.s("sawtooth").attack(.005).decay(.3).sustain(.5).release(.15) 164 .lpf(700).lpq(2).shape(.3).gain(.26).vib(.7).vibmod(.1) 165const low = stack( 166 bassSound(note("0@7 0@3 7@6").add(note(roots))).mask(bassOn), 167 // the fuller beat: the bar's second half on the 5th, again on the "and" of 4 168 bassSound(note("0@7 0@3 7@4 7@2").add(note(roots))).mask(fuller) 169) 170 171// ── vox: vocoder, one line per 2 bars, each on its chord ── 172// <…> indexes by cycle mod 8; verses start on cycles 4 and 20, so line 1 sits at index 4. 173// "Up, flat, or down" comes in a beat late: on the bar, the downbeat buried its 174// "Up", and an 8th late, the beat-2 snare buried its "flat". 175const lines1 = "<ffcoin ~ ffindex ~ fffortyfive ~ [~ ffupflat@3] ~>" 176const lines2 = "<fftrust ~ ffprob ~ fflayer ~ ffnotmodel ~>" 177// Dry on the word. The echo is a quiet copy of the line a dotted half (2.25 s) 178// after it starts, so it lands after the line ends (the longest is 2.0 s). 179// It is written out, not .delay(): superdough makes its DelayNode with the 180// default one-second maximum, so a longer delaytime plays at 1 s, on the line. 181const sung = (lines, verse) => stack( 182 s(lines).gain(1).room(.3), 183 s(lines).late(3/4).gain(.22).hpf(400).lpf(3000).room(.3) 184).mask(verse) 185const vox = stack(sung(lines1, verse1), sung(lines2, verse2)) 186 187// the last word: it always plays, over the faded bed 188const lastWord = s("fffine").gain(.8).room(.3).mask(fine) 189 190// the outro fade (bars 41-47 fall to about -12 dB) multiplies velocity, as 191// Jev's part levels do; Jev's energy below uses postgain 192const faded = (...parts) => stack(...parts).mul(velocity(fade)) 193// play once, 48 bars as written; the last notes start on bar 47's third 194// beat, so the song stops about a beat after This is fine 195const ending = "<1!46 [1 1 1 0] 0>" 196const once = (p) => p.mask("<1!48 0!952>").mask(ending) 197 198// ── the form, as rules in code: which sections may follow which ── 199// `at` is where each starts as written; an empty `next` ends the song. The 200// written order is always allowed, and is what plays without Jev. 201// The verse and beat halves are forced together, so a vocal line is never 202// cut; every path to the outro passes through verse 2, so every line is sung. 203const form = walk(section, { 204 max: 16, 205 sections: { 206 intro: { at: 0, next: ['verse1'] }, 207 verse1: { at: 4, next: ['verse1b'] }, 208 verse1b: { at: 8, next: ['beat'] }, 209 beat: { at: 12, next: ['beatB'] }, 210 beatB: { at: 16, next: ['verse2', 'verse1'] }, 211 verse2: { at: 20, next: ['verse2b'] }, 212 verse2b: { at: 24, next: ['beatFull', 'beat'] }, 213 beatFull: { at: 28, next: ['beatFullB'] }, 214 beatFullB: { at: 32, next: ['beatFullC', 'verse2', 'outro'] }, 215 beatFullC: { at: 36, next: ['outro', 'verse2'] }, 216 outro: { at: 40, next: ['outroEnd'] }, 217 outroEnd: { at: 44, next: [] }, 218 }, 219}) 220 221// ── into the next section: over the last bar of each, the next one's answer ── 222// Past the last section there is no next one, so nothing plays there. 223const into = dj.into.choice.early(1) 224const lastBar = "<0!3 1>" 225// a fill replaces the drums for its bar 226const noFill = stack("<1!3 0>", into.fmap((c) => c !== 'fill').mask(lastBar)) 227 228// ── the song: Jev's section, parts and energy, composed ── 229// Each section plays its own bars, each part at the level Jev chose. The 230// fade and the levels both multiply velocity, so neither undoes the other; 231// one swing for the whole band; the last word always plays. 232const parts = form.arrange(once(stack( 233 faded( 234 part('keys', keys), 235 part('loop', loop), 236 part('drums', drums.mask(noFill)), 237 part('bass', low), 238 vocals(vox), 239 ), 240 part('keys', crackle), 241).swingBy(1/3, 8))) 242 243// A transition plays at the level of the part it belongs to in the section 244// it plays in, and fades with it. 245const transition = into.pick({ 246 cut: silence, 247 fill: stack( 248 s("bd:4").struct("x ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~").gain(.31), 249 s("sn:2").struct("~ ~ ~ ~ ~ ~ x ~ ~ ~ x ~ x ~ x x").gain(.26) 250 ).lpf(3500).room(.2).orbit('drums') 251 .mask(dj.drums.score.round().gt(0)).mul(velocity(dj.drums.score.round().div(2))), 252 riser: s("white*32").decay(.012).sustain(0).hpf(saw.range(1500, 7000)).lpf(9000) 253 .gain(saw.range(.016, .11)).orbit('keys') 254 .mask(dj.keys.score.round().gt(0)).mul(velocity(dj.keys.score.round().div(2))), 255 dropout: silence, 256}).mask(lastBar).swingBy(1/3, 8).mul(velocity(form.arrange(once(fade)))) 257// dropout: the parts stop for the section's last beat 258const lastBeat = "<0!3 [0 0 0 1]>" 259const gap = stack( 260 "<1!3 [1 1 1 0]>", 261 into.fmap((c) => c !== 'dropout').mask(lastBeat), 262) 263 264// postgain scales every part after its own gain and velocity, so the mix 265// and the fade keep their shape; as written (rung 2) is 1 266stack(parts.mask(gap), transition, form.arrange(once(lastWord))) 267 .mul(postgain(dj.energy.score.fmap((e) => [.6, .8, 1, 1.1][Math.round(e)])))