song.jsannotatedsong.jssource266 lines · 13.4 KB · raw
1// EMERGENCY MEETING — dark drum & bass, 174bpm, 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(174/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: '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}
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: '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})
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    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})
80
81// a little chance in the form: the section is drawn from Jev's probabilities
82const { section } = formJev.every(4, { sample: 0.7 })
83// A transition only at a real boundary (SPEC.md, "Into the next section"):
84// a section that continues the one before is always a cut, and a drop is
85// 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})
97// the transition into the next section, heard over the last bar of this one
98const into = dj.into.choice.early(1)
99// 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>"))
101
102// A part at Jev's level: silent when out, otherwise its velocity scaled by
103// the level. Each part has its own orbit, named for its question, so the
104// 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)))
107
108// bars: 1-8 intro | 9-16 build 1 | 17-32 DROP 1 | 33-40 verse | 41-44 build 2
109//       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>"
125
126// 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]>"
129
130// ── parts ─────────────────────────────────────────
131// intro / builds / verse / outro: a sine pulse on the roots, and its octave
132// 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>")
138
139// drops: detuned reese, low-passed, opening across drop 1 (cycles 16-32);
140// 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)
150
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),
158
159  // 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)
168
169  // 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)
174
175// the story: the verse on its chords, and the intro's whispered line. Not
176// one of Jev's parts: it always plays (SPEC.md, "The story"). Per-line gain
177// 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')
183
184// 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)
190
191// the whispered line on the last Eb bar and the last hit. They always
192// play, so the song always ends on them. The line rides the energy as the
193// 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')
197
198const once = (p) => p.mask("<1!64 0!936>")   // play once, then silence
199
200// ── the form, as rules in code: which sections may follow which ──
201// `at` is where each starts as written; an empty `next` ends the song. The
202// written order is always allowed, and is what plays without Jev.
203// `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})
225
226// ── the song: Jev's section, parts and energy, composed ──
227// Each section plays its own bars, each part at the level Jev chose; the
228// energy scales every part's velocity together, so the mix keeps its
229// 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)))
236
237// ── into the next section: over the last bar of each, the next one's answer ──
238// 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>")
249// dropout: the parts stop for the section's last beat. The outro has no
250// 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)
257
258// energy: held back is about 5 dB under full, all out is as written. The
259// story rides only the top of it. The master gain is set last, from the
260// 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))