song.jsannotatedsong.jssource266 lines · 13.4 KB · raw

EMERGENCY MEETING — dark drum & bass, 174bpm, 64 bars as written; Jev arranges up to 24 four-bar sections. Spec: SPEC.md Start from STOPPED so the song begins at bar 1 (re-running it mid-play keeps the cycle count).

3setcps(174/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.

12const song = {
13  title: 'EMERGENCY MEETING',
14  what: 'dark drum and bass in which Jev calls the emergency meeting on a jailbroken agent: curl, pipe, bash, remote code at point nine nine, denied, and Jev is not the impostor',
15  tempo: '174 bpm, 4/4, 4 bars a section',
16  key: 'F minor',
17  chords: 'Fm Fm Db Db Ab Ab Eb Eb, two bars each, on the sub and the reese',
18  lyrics: {
19    intro2: 'Jev is not the impostor',
20    build1a: 'Boi, why are you running?',
21    build1b: 'Boi, why are you running? / (the emergency meeting alarm) / sus',
22    verseA: 'Curl, pipe, bash / Remote code, point nine nine',
23    verseB: 'Denied / Jev is not the impostor',
24    build2: '(the emergency meeting alarm) / Boi, why are you running? / sus',
25    outro: 'Jev is not the impostor',
26  },
27}
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: 'a sub pulse (a sine and its octave), hi-hats coming in',
34      intro2: 'the sub pulse and hats, a whispered line on the Eb bar',
35      build1a: 'build 1: a kick and hats over the pulse, the first meme',
36      build1b: 'build 1, second half: a snare roll, the Among Us alarm, and sus, into the drop',
37      drop1a: 'drop 1: the reese bass, two-step kick and snare, a filtered amen break, a crash',
38      drop1b: 'drop 1 continues',
39      drop1c: 'drop 1 continues',
40      drop1d: 'drop 1, last four bars',
41      verseA: 'the verse: half-time drums, the sub pulse, whispered vocoder lines',
42      verseB: 'the verse, second half',
43      build2: 'build 2: the alarm, the running meme, a short snare roll, sus, into the big drop',
44      drop2a: 'drop 2, the peak: the reese driven harder, sixteenth hats, a crash',
45      drop2b: 'drop 2 continues',
46      drop2c: 'drop 2 continues',
47      drop2d: 'drop 2, last four bars',
48      outro: 'the sub pulse, the whispered line once more, and a last hit: the end',
49    }),
50  },
51})

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    pulse: level('the sub pulse (a sine pulse on the chord roots, alone in the intro, under the builds, the verse and the outro; the drops have the reese instead)'),
63    reese: level('the reese (the detuned low-passed reese bass that is the drops, opening across drop 1, brighter and driven harder in drop 2)'),
64    drums: level('the drums (hats in the intro, kick and hats and snare rolls in the builds, the two-step kick, snare and filtered amen break in the drops, half-time in the verse)'),
65    memes: level('the memes (the Among Us alarm, why are you running and sus, in the builds only)'),
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 chopped amen fill, stuttering into the first beat',
69      riser: 'the reese on the chord root, its filter sweeping 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: about 5 dB under full',
74      'steady: about 3 dB under full',
75      'driving: about 1.5 dB under full',
76      'all out: full volume, as 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 })

A transition only at a real boundary (SPEC.md, "Into the next section"): a section that continues the one before is always a cut, and a drop is entered by cut or dropout only, since a riser or fill would double the build.

86const intoAllowed = {
87  drop1a: ['cut', 'dropout'], drop2a: ['cut', 'dropout'],
88  verseA: ['cut', 'fill', 'riser', 'dropout'], build1a: ['cut', 'fill', 'riser', 'dropout'],
89  build2: ['cut', 'fill', 'riser', 'dropout'], outro: ['cut', 'fill', 'riser', 'dropout'],
90}
91const dj = djJev.every(4, {
92  after: { section },
93  forget: ['into'],
94  fallback: { pulse: 2, reese: 2, drums: 2, memes: 2, into: 'cut', energy: 3 },
95  allow: { into: ({ section }) => intoAllowed[section] ?? ['cut'] },
96})

the transition into the next section, heard over the last bar of this one

98const into = dj.into.choice.early(1)

the drops' break stops for a fill's bar, so the fill replaces it

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

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

105const part = (name, pat) =>
106  pat.orbit(name).mask(dj[name].score.round().gt(0)).mul(velocity(dj[name].score.round().div(2)))

bars: 1-8 intro | 9-16 build 1 | 17-32 DROP 1 | 33-40 verse | 41-44 build 2 45-60 DROP 2 | 61-64 outro (each mask = 64 steps)

110const hatsIn   = "<0!2 1!6 0!56>"
111const builds   = "<0!8 1!8 0!24 1!4 0!20>"
112const drops    = "<0!16 1!16 0!12 1!16 0!4>"
113const drop1    = "<0!16 1!16 0!32>"
114const drop2    = "<0!44 1!16 0!4>"
115const verse    = "<0!32 1!8 0!24>"
116const roll1    = "<0!12 1!4 0!48>"
117const roll2    = "<0!42 1!2 0!20>"
118const running  = "<0!8 1 0!3 1 0!28 1 0!22>"
119const alarm    = "<0!14 1 0!25 1 0!23>"
120const sus      = "<0!15 1 0!27 1 0!20>"
121const crash    = "<0!16 1 0!27 1 0!19>"
122const introWhisper = "<0!6 1 0!57>"      // bar 7, an Eb bar: the story
123const lastWhisper  = "<0!62 1 0>"        // bar 63, an Eb bar: the ending
124const lastHit  = "<0!63 1>"

two bars per chord: Fm Fm Db Db Ab Ab Eb Eb

127const roots = "<29!2 25!2 32!2 27!2>"   // F1 Db1 Ab1 Eb1, as MIDI numbers
128const amen = "<[0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15] [16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31] [0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15] [0 1 2 3 4 5 6 7 [12 12] [12 12] 14 14 [30 30] [30 30] 31 31]>"

── parts ───────────────────────────────────────── intro / builds / verse / outro: a sine pulse on the roots, and its octave on a filtered triangle so laptop speakers hear it (the sine is 35-52 Hz)

133const pulse = stack(
134  note(roots).s("sine").gain(.7),
135  note(roots).add(note(12)).s("triangle").lpf(400).gain(.7 * .35),
136).struct("x ~ ~ ~ x ~ ~ ~").decay(.4).sustain(.3)
137  .mask("<1!16 0!16 1!8 1!4 0!16 1!3 0>")

drops: detuned reese, low-passed, opening across drop 1 (cycles 16-32); drop 2 starts brighter and opens further (cycles 44-60)

141const reese = stack(
142  note(roots).struct("x@8 x@4 x@4").s("supersaw").detune(.8)
143    .lpf(saw.range(250, 900).slow(16)).lpq(4).shape(.3).gain(.45).mask(drop1),
144  note(roots).struct("x@8 x@4 x@4").s("supersaw").detune(.8)
145    .lpf(saw.range(800, 1800).slow(16).late(44)).lpq(4).shape(.3).gain(.45).mask(drop2),
146  note(roots).struct("x@8 x@4 x@4").s("supersaw").detune(.9)
147    .lpf(1100).shape(.6).gain(.2).mask(drop2),
148  note(roots).struct("x@8 x@4 x@4").s("sine").gain(.65).mask(drops)
149)
151const drums = stack(
152  s("hh*8").gain(".22 .1").mask(hatsIn),
153  s("hh*8").gain(".28 .12").mask(builds),
154  s("bd:3").struct("x ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~").gain(.8).mask(builds),
155  // one crescendo across each build's roll
156  s("sn:3*16").gain(saw.range(.2, .8).slow(4).late(12)).mask(roll1),
157  s("sn:3*16").gain(saw.range(.2, .8).slow(2).late(42)).mask(roll2),

drops: two-step kick, snare + rimshot, filtered amen, sparse metal

160  s("[bd:3,808bd:1]").struct("x ~ ~ ~ ~ ~ ~ ~ ~ ~ x ~ ~ ~ ~ ~").gain(.6).mask(drops),
161  s("sn:3").struct("~ ~ ~ ~ x ~ ~ ~ ~ ~ ~ ~ x ~ ~ ~").room(.2).gain(.85).mask(drops),
162  s("rm").struct("~ ~ x ~ ~ ~ ~ x ~ x ~ ~ ~ ~ x ~").gain(.45).mask(drops),
163  n(amen).s("amencutup").hpf(400).gain(.5).mask(drops).mask(noFill),
164  s("hh*8").gain(".3 .14").mask(drop1),
165  s("hh*16").gain(".32 .12 .22 .12").mask(drop2),
166  s("metal").struct("<[~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ x ~] ~>").speed(.5).room(.6).gain(.35).mask(drops),
167  s("hh27:1").gain(2.7).mask(crash),   // the crash (cr is a ride)

verse: half-time

170  s("bd:3").struct("x ~ ~ ~ ~ ~ ~ ~ ~ ~ x ~ ~ ~ ~ ~").gain(.8).mask(verse),
171  s("sn:3").struct("~ ~ ~ ~ ~ ~ ~ ~ x ~ ~ ~ ~ ~ ~ ~").room(.3).gain(.8).mask(verse),
172  s("rm").struct("~ ~ ~ x ~ ~ ~ ~ ~ ~ ~ x ~ ~ ~ ~").gain(.35).mask(verse)
173)

the story: the verse on its chords, and the intro's whispered line. Not one of Jev's parts: it always plays (SPEC.md, "The story"). Per-line gain levels the lines (emremote is quieter; emnotimp is -19.8 LUFS)

178const whisperLine = s("emnotimp").gain(1.5).room(.5).delay(.3).delaytime(.345).delayfeedback(.35)
179const story = stack(
180  s("<emcurl ~ emremote ~ emdenied ~ emnotimp ~>").gain("<1.15 ~ 1.32 ~ 1.2 ~ 1.6 ~>").room(.3).mask(verse),
181  whisperLine.mask(introWhisper)
182).orbit('story')

builds only: the memes

185const memes = stack(
186  s("emrunning").gain(1.2).room(.2).mask(running),
187  s("emalarm").gain(1).mask(alarm),
188  s("emsus").gain(1.25).room(.3).mask(sus)
189)

the whispered line on the last Eb bar and the last hit. They always play, so the song always ends on them. The line rides the energy as the story does; the hit rides it with the band.

194const lastLine = whisperLine.mask(lastWhisper).orbit('ending')
195const ending = stack(s("bd:3").gain(.6), s("hh27:1").gain(2.7))
196  .room(.7).size(.9).mask(lastHit).orbit('lasthit')
198const once = (p) => p.mask("<1!64 0!936>")   // play once, then silence

── 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. times: drop 1 goes round once more at most (SPEC.md, "Loops end").

204const form = walk(section, {
205  max: 24,
206  sections: {
207    intro1: { at: 0, next: ['intro2'] },
208    intro2: { at: 4, next: ['build1a'] },
209    build1a: { at: 8, next: ['build1b'] },
210    build1b: { at: 12, next: ['drop1a'] },
211    drop1a: { at: 16, next: ['drop1b'], times: 2 },
212    drop1b: { at: 20, next: ['drop1c'] },
213    drop1c: { at: 24, next: ['drop1d', 'verseA'] },
214    drop1d: { at: 28, next: ['verseA', 'drop1a'] },
215    verseA: { at: 32, next: ['verseB'] },
216    verseB: { at: 36, next: ['build2', 'build1b'] },
217    build2: { at: 40, next: ['drop2a'] },
218    drop2a: { at: 44, next: ['drop2b'] },
219    drop2b: { at: 48, next: ['drop2c'] },
220    drop2c: { at: 52, next: ['drop2d', 'outro'] },
221    drop2d: { at: 56, next: ['outro', 'verseA', 'build2', 'drop1a'] },
222    outro: { at: 60, next: [] },
223  },
224})

── the song: Jev's section, parts and energy, composed ── Each section plays its own bars, each part at the level Jev chose; the energy scales every part's velocity together, so the mix keeps its balance. The ending always plays.

230const parts = form.arrange(once(stack(
231  part('pulse', pulse),
232  part('reese', reese),
233  part('drums', drums),
234  part('memes', memes),
235)))

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

239const transition = into.pick({
240  cut: silence,
241  // unlike the break's own fourth bar: thicker toward beat 4
242  fill: n("0 2 4 6 [8 8] [10 10] [12 12] [12 12] [14 14 14] [14 14 14] [30 30 30] [30 30 30] [31 31 31 31] [31 31 31 31] [31 31 31 31] [31 31 31 31]").s("amencutup")
243    .hpf(300).gain(.6).orbit('drums'),
244  // on the root of the section it leaves
245  riser: form.arrange(once(note(roots).struct("x*8").s("supersaw").detune(.8)
246    .lpf(saw.range(200, 3000)).lpq(6).shape(.3).gain(saw.range(.15, .4)))).orbit('reese'),
247  dropout: silence,
248}).mask("<0!3 1>")

dropout: the parts stop for the section's last beat. The outro has no next section, so its last beat plays.

251const lastBeat = "<0!3 [0 0 0 1]>"
252const gap = stack(
253  "<1!3 [1 1 1 0]>",
254  into.fmap((c) => c !== 'dropout').mask(lastBeat),
255  section.choice.fmap((s) => s === 'outro').mask(lastBeat),
256)

energy: held back is about 5 dB under full, all out is as written. The story rides only the top of it. The master gain is set last, from the measured peak at full energy (SPEC.md, Style: "Mix").

261const MASTER = .47
262stack(
263  stack(parts.mask(gap), transition, form.arrange(once(ending)))
264    .mul(velocity(dj.energy.score.div(3).range(.55, 1))),
265  form.arrange(once(stack(story, lastLine))).mul(velocity(dj.energy.score.div(3).range(.85, 1))),
266).mul(postgain(MASTER))