song.jsannotatedsong.jssource267 lines · 13.5 KB · raw

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 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')

── Jev: two of them, in TypeSafe's own shape (SPEC.md, "Jev in the booth") ── formJev picks the next section; djJev, asked once that is decided (questions in one request cannot see each other), says how much of each part plays it, how the song moves into it, and how hard. Every 4 bars. The form's rules (walk, at the bottom) narrow the sections Jev may pick and end the 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}
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})

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})

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})

A part at Jev's level: silent when out, otherwise its velocity scaled by the level. Each part has its own orbit, named for its question, so the 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)))

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))))

bars: 1-4 intro | 5-12 verse 1 | 13-20 beat | 21-28 verse 2 | 29-40 beat, fuller 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

Stonks never plays in a section's last bar: that bar is the transition's. Chopped into stutters in each beat section's first two bars, the whole 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>"

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>"

two bars per chord; the cycle starts on Dm7 so both verses open on Fmaj7: 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 ~ ~ ~"

── parts ───────────────────────────────────────── Every part is straight here; one swing, 16ths by a third, is applied to the whole band below. ── 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)

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)

── percussion loop: the chest-thump hum's thumps, one per slice of ffhum (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)

── 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)

── 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)

── vox: vocoder, one line per 2 bars, each on its chord ── <…> indexes by cycle mod 8; verses start on cycles 4 and 20, so line 1 sits at index 4. "Up, flat, or down" comes in a beat late: on the bar, the downbeat buried its "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 ~>"

Dry on the word. The echo is a quiet copy of the line a dotted half (2.25 s) after it starts, so it lands after the line ends (the longest is 2.0 s). It is written out, not .delay(): superdough makes its DelayNode with the 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))

the last word: it always plays, over the faded bed

188const lastWord = s("fffine").gain(.8).room(.3).mask(fine)

the outro fade (bars 41-47 fall to about -12 dB) multiplies velocity, as Jev's part levels do; Jev's energy below uses postgain

192const faded = (...parts) => stack(...parts).mul(velocity(fade))

play once, 48 bars as written; the last notes start on bar 47's third 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)

── the form, as rules in code: which sections may follow which ── at is where each starts as written; an empty next ends the song. The written order is always allowed, and is what plays without Jev. The verse and beat halves are forced together, so a vocal line is never 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})

── into the next section: over the last bar of each, the next one's answer ── 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>"

a fill replaces the drums for its bar

226const noFill = stack("<1!3 0>", into.fmap((c) => c !== 'fill').mask(lastBar))

── the song: Jev's section, parts and energy, composed ── Each section plays its own bars, each part at the level Jev chose. The fade and the levels both multiply velocity, so neither undoes the other; 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)))

A transition plays at the level of the part it belongs to in the section 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))))

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)

postgain scales every part after its own gain and velocity, so the mix 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)])))