song.jsannotatedsong.jssource234 lines · 12.4 KB · raw
1// BORROW CHECKER BLUES — robot-funk, 110bpm, 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(110/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: 'BORROW CHECKER BLUES',
14  what: 'Daft Punk-style robot funk about deterministic tools in front of Jev: the verses are real rustc errors, the chorus says lint before you spend, and when the build finally succeeds, Wow',
15  tempo: '110 bpm, 4/4, 4 bars a section',
16  key: 'E dorian',
17  chords: 'Em7 Em7 A7 A7 Dmaj7 Dmaj7 Bm7 Bm7, two bars each, under the bass and the stabs',
18  lyrics: {
19    verse1a: 'Cannot borrow as mutable / Lifetime may not live long enough',
20    verse1b: 'x does not live long enough / Use of moved value',
21    chorus1: 'Lint. Before you spend / Lint. Before you spend / Finished dev profile / Wow',
22    verse2a: 'Mismatched types / Which is behind a shared reference',
23    verse2b: 'Expected Foo, found Bar / Cannot borrow as mutable more than once at a time',
24    chorus2: 'Lint. Before you spend / Lint. Before you spend / Finished dev profile / Wow',
25    outro: 'Finished dev profile / Wow (the ending; it always plays)',
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      intro1: 'the robot bass alone; plays: bass',
34      intro2: 'the bass and a four-on-the-floor kick; plays: bass, drums (kick only)',
35      verse1a: 'verse 1, first half: the band enters, dull stabs, vocoded rustc errors; plays: bass, drums, stabs, vocals',
36      verse1b: 'verse 1, second half; plays: bass, drums, stabs, vocals',
37      chorus1: 'the chorus: the hook sung twice, then the build succeeds and a Wow; plays: bass, drums, stabs, vocals',
38      groove1a: 'groove 1: instrumental, open hi-hats, a crash, the stab filter starting to open; plays: bass, drums, stabs',
39      groove1b: 'groove 1 continues, the stabs opening bar by bar; plays: bass, drums, stabs',
40      groove1c: 'groove 1, last four bars, the stabs at their brightest yet; plays: bass, drums, stabs',
41      verse2a: 'verse 2, first half: dull stabs again, more rustc errors; plays: bass, drums, stabs, vocals',
42      verse2b: 'verse 2, second half; plays: bass, drums, stabs, vocals',
43      chorus2: 'the chorus again, into the big groove; plays: bass, drums, stabs, vocals',
44      groove2a: 'groove 2, the peak: a crash, the stabs opening wider bar by bar; plays: bass, drums, stabs',
45      groove2b: 'groove 2 continues, opening; plays: bass, drums, stabs',
46      groove2c: 'groove 2 continues, opening; plays: bass, drums, stabs',
47      groove2d: 'groove 2, last four bars, wide open; plays: bass, drums, stabs',
48      outro: 'the bass alone, the build succeeds one last time, a last Wow, and the end; plays: bass, and the ending, which always plays',
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    bass: level('the robot bass (the slap-style synth bass and its sub, the song rides on it from the first bar to the last)'),
63    drums: level('the drums (the four-on-the-floor kick from bar 5, claps and hi-hats with the band from the verse on, open hats and crashes in the grooves)'),
64    stabs: level('the disco stabs (short chord stabs from the verse on, the only harmony: dull in the verses, opening bar by bar through each groove)'),
65    vocals: level('the vocals (the vocoded rustc errors in the verses and the sung hook in the choruses; the grooves are instrumental as written)'),
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 clap and kick fill into the first beat',
69      riser: 'the disco stabs repeating faster as their filter sweeps open over the last bar',
70      dropout: 'everything stops for the last beat, a gap before the section hits',
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      'held back: quieter',
74      'steady: a little under full',
75      'driving: full volume',
76      'all out: full volume, nothing held back',
77    ]),
78  },
79})
80
81// a phrase is a run of sections that belong together: intro1-2, a verse's
82// a and b, a groove's a to d. Within one, the song moves on with a cut or a
83// fill; a riser or a dropout only where one phrase gives way to another.
84const phrase = (s) => (s.startsWith('intro') ? 'intro' : s.replace(/[a-d]$/, ''))
85
86// a little chance in the form: the section is drawn from Jev's probabilities
87const { section } = formJev.every(4, { sample: 0.7 })
88const dj = djJev.every(4, {
89  after: { section },
90  forget: ['into'],
91  fallback: { bass: 2, drums: 2, stabs: 2, vocals: 2, into: 'cut', energy: 3 },
92  allow: {
93    // the opening has no section before it: nothing plays into it
94    into: ({ section, playingNow }) =>
95      !playingNow ? ['cut']
96        : phrase(section) === phrase(playingNow) ? ['cut', 'fill'] : ['cut', 'fill', 'riser', 'dropout'],
97  },
98})
99
100// A part at Jev's level: silent when out, otherwise its velocity scaled by
101// the level. Each part has its own orbit, named for its question, so the
102// meter hears it alone (measuredSound).
103const part = (name, pat) =>
104  pat.orbit(name).mask(dj[name].score.round().gt(0)).mul(velocity(dj[name].score.round().div(2)))
105
106// bars: 1-8 intro | 9-16 verse 1 | 17-20 chorus | 21-32 GROOVE 1 | 33-40 verse 2
107//       41-44 chorus | 45-60 GROOVE 2 | 61-64 outro          (each mask = 64 steps)
108const kick    = "<0!4 1!56 0!4>"
109const band    = "<0!8 1!52 0!4>"
110const verse1  = "<0!8 1!8 0!48>"
111const verse2  = "<0!32 1!8 0!24>"
112const chorus  = "<0!16 1!4 0!20 1!4 0!20>"
113const grooves = "<0!20 1!12 0!12 1!16 0!4>"
114const crash   = "<0!20 1 0!23 1 0!19>"
115// the hook's bars (17, 19, 41, 43): no clap on beat 2, over the "t" of "Lint"
116const hookClap = "<1!16 [1 0 1 1] 1 [1 0 1 1] 1!21 [1 0 1 1] 1 [1 0 1 1] 1!21>"
117// no transition over a chorus's Wow bar (20, 44)
118const noWow   = "<1!19 0 1!23 0 1!20>"
119// stab filter per bar: dull in the verses, opening bar by bar through each groove
120const stabLpf = "<600!8 800!8 800!4 1000 1070 1150 1220 1290 1360 1440 1510 1580 1650 1730 1800 800!8 800!4 2000 2270 2530 2800 3070 3330 3600 3870 4130 4400 4670 4930 5200 5470 5730 6000 600!4>"
121const stabGain = .4
122
123// two bars per chord: Em7 Em7 A7 A7 Dmaj7 Dmaj7 Bm7 Bm7
124const roots  = "<40!2 45!2 38!2 35!2>"   // E2 A2 D2 B1, as MIDI numbers
125const sev    = "<10!2 10!2 11!2 10!2>"   // each chord's 7th over its root: the major 7th on Dmaj7
126const chords = "<[e4,g4,b4,d5]!2 [a3,c#4,e4,g4]!2 [d4,f#4,a4,c#5]!2 [b3,d4,f#4,a4]!2>"
127
128// ── parts ─────────────────────────────────────────
129// bass: the song rides on it, start to finish. The sub doubles the root at E2-B1.
130const bass = stack(
131  stack(
132    note("0 ~ 12 ~ 0 0 ~ ~ ~ 12 0 ~ 7 ~ ~ ~"),
133    note(sev).struct("~ ~ ~ ~ ~ ~ ~ x ~ ~ ~ ~ ~ ~ x ~"),
134  ).add(note(roots))
135    .s("sawtooth").lpf(1100).lpq(10).decay(.12).sustain(.1).gain(.36),
136  note(roots).struct("x ~ ~ ~ x ~ ~ ~ x ~ ~ ~ x ~ ~ ~")
137    .s("sine").decay(.3).sustain(.4).gain(.4)
138)
139
140// drums: four on the floor, at levels that keep the mix under 0 dBFS (no master limiter)
141const drums = stack(
142  s("bd:3*4").gain(.5).mask(kick),
143  s("cp").struct("~ x ~ x").room(.2).gain(.42).mask(band).mask(hookClap),
144  s("hh*8").gain(".22 .11").mask(band),
145  s("808oh").struct("~ x ~ x ~ x ~ x").gain(.17).mask(grooves),
146  s("hh27:1").gain(1.9).mask(crash)
147)
148
149// disco stabs, short, filter by bar: the only harmony, near the bass
150const stabs = note(chords).struct("~ x ~ ~ ~ x ~ x").s("sawtooth")
151  .lpf(stabLpf).decay(.2).sustain(0).room(.3).gain(stabGain).mask(band)
152
153const vox = stack(
154  // verses: rustc errors, vocoded, one per 2 bars, each on its chord
155  s("<bcborrow ~ bclifetime ~ bcxlive ~ bcmoved ~>").gain(.8).room(.2).mask(verse1),
156  s("<bctypes ~ bcshared ~ bcfoobar ~ bctwice ~>").gain(.8).room(.2).mask(verse2),
157  // chorus: lint before you spend (Em7, then A7) ... it compiles (A7): wow,
158  // on the and of 3, after the 1.4 s success line
159  s("<bclintem ~ bclinta [bcfinished@5 bcwow@3]>").gain(.85).room(.25).mask(chorus)
160)
161
162// the end: the build succeeds (bar 62, Dmaj7) and the last "wow" (63), over
163// the bass. It always plays, at its written level.
164const ending = s("<~!61 bcfinishedd bcwow ~>").gain(.85).room(.5)
165
166const once = (p) => p.mask("<1!64 0!936>")   // play once, then silence
167
168// ── the form, as rules in code: which sections may follow which ──
169// `at` is where each starts as written; an empty `next` ends the song. The
170// written order is always allowed, and is what plays without Jev. `times`:
171// a groove goes round once more at most, cut short (SPEC.md, "Loops end").
172const form = walk(section, {
173  max: 24,
174  sections: {
175    intro1: { at: 0, next: ['intro2'] },
176    intro2: { at: 4, next: ['verse1a'] },
177    verse1a: { at: 8, next: ['verse1b'] },
178    verse1b: { at: 12, next: ['chorus1'] },
179    chorus1: { at: 16, next: ['groove1a', 'verse2a'] },
180    groove1a: { at: 20, next: ['groove1b'], times: 2 },
181    groove1b: { at: 24, next: ['groove1c', 'verse2a'] },
182    groove1c: { at: 28, next: ['verse2a', 'groove1a'], times: 1 },
183    verse2a: { at: 32, next: ['verse2b'] },
184    verse2b: { at: 36, next: ['chorus2'] },
185    chorus2: { at: 40, next: ['groove2a'] },
186    groove2a: { at: 44, next: ['groove2b'], times: 2 },
187    groove2b: { at: 48, next: ['groove2c', 'outro'] },
188    groove2c: { at: 52, next: ['groove2d', 'groove2a'], times: 1 },
189    groove2d: { at: 56, next: ['outro', 'groove1a', 'chorus2', 'verse1a'] },
190    outro: { at: 60, next: [] },
191  },
192})
193
194// ── the song: Jev's section, parts and energy, composed ──
195// Each section plays its own bars, each part at the level Jev chose.
196const parts = form.arrange(once(stack(
197  part('bass', bass),
198  part('drums', drums),
199  part('stabs', stabs),
200  part('vocals', vox),
201)))
202
203// ── into the next section: over the last bar of each, the next one's answer ──
204// Past the last section there is no next one, so nothing plays there. Each
205// transition is a part (the fill is drums, the riser stabs), so it follows
206// the level of the section it plays in; none plays over a chorus's Wow bar.
207const into = dj.into.choice.early(1)
208// the riser's filter ends where the next section's stabs begin
209const nextLpf = form.arrange(once(stabLpf)).early(1)
210const transition = into.pick({
211  cut: silence,
212  fill: part('drums', s("bd:3 ~ cp ~ [cp cp] cp [cp cp] [cp cp cp cp]").room(.2)
213    .gain(".5 .25 .28 .31 .35 .38 .41 .44")),
214  // on the chord of the section it leaves
215  riser: part('stabs', form.arrange(once(note(chords).struct("x*16").s("sawtooth")
216    .decay(.08).sustain(0)))
217    .lpf(saw.range(.15, 1).mul(nextLpf)).gain(saw.range(.1, stabGain))),
218  dropout: silence,
219}).mask("<0!3 1>").mask(form.arrange(once(noWow)))
220// dropout: the parts stop for the section's last beat. The outro has no
221// next section, so its last beat plays.
222const lastBeat = "<0!3 [0 0 0 1]>"
223const gap = stack(
224  "<1!3 [1 1 1 0]>",
225  into.fmap((c) => c !== 'dropout').mask(lastBeat),
226  section.choice.fmap((s) => s === 'outro').mask(lastBeat),
227)
228
229// Energy scales the parts and transitions together, so the mix keeps its
230// balance; the ending plays at its written level.
231stack(
232  stack(parts.mask(gap), transition).mul(velocity(dj.energy.score.div(3).range(.55, 1))),
233  form.arrange(once(ending)),
234).mul(postgain(.7)) // the master level: peaks under -1 dBFS (no limiter)