song.jsannotatedsong.jssource298 lines · 15.3 KB · raw
1// MINIMAX WAS RIGHT THERE — chiptune jungle, 170bpm, 64 bars as written; Jev arranges up to 24 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(170/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 which parts play it and how
10// 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: 'MINIMAX WAS RIGHT THERE',
14  what: 'chiptune jungle roasting people who use Jev as a game bot, where plain code wins: Pong, tic-tac-toe, a Mario Kart racing line; Jev is for judgment, not for joysticks',
15  tempo: '170 bpm, 4 bars a section',
16  key: 'A minor',
17  chords: 'Am Am F F C C G G, two bars each; the verses start on Am or C, and the drops are phased to land on Am',
18  lyrics: {
19    verse1: 'You paid by the billion to press right jump / Tic tac toe was solved before you were born',
20    verse1b: 'A PID loop takes the racing line / Minimax was right there',
21    build1: 'Oh no no no',
22    verse2: 'Two hundred twenty seven milliseconds a move / Jev is for judgment, not for joysticks',
23    verse2b: 'Route the ticket. Gate the sudo. / Minimax was right there',
24    build2: 'Oh no no no',
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 ends the song: choose it once the song has built to its peak and made its point, before it drags', {
32      intro: 'the square-wave arpeggio alone',
33      introB: 'the arpeggio with Pong blips answering it',
34      verse1: 'verse 1: half-speed beat, bass, vocoded lines',
35      verse1b: 'verse 1, second half',
36      build1: 'the build: the Mario Kart blue shell locks on, Pong slows to one blip per decision, oh no no no, and the fall off the cliff',
37      drop1: 'drop 1 lands on A minor and a crash: crushed amen break, jungle kick, reese and sub bass, the chip lead with Pong answering it',
38      drop1b: 'drop 1, middle',
39      drop1c: 'drop 1, last four bars',
40      verse2: 'verse 2: half-speed beat, bass, vocoded lines',
41      verse2b: 'verse 2, second half',
42      build2: 'the build again: blue shell, slowing Pong, oh no no no, the fall',
43      drop2: 'drop 2 lands on A minor and a crash: the lead an octave up in harmony, sixteenth hats, a double-time arpeggio, the reese opening further',
44      drop2b: 'drop 2, continued',
45      drop2c: 'drop 2, the peak',
46      drop2d: 'drop 2, last four bars',
47      outro: 'a last kick and crash left to ring on A, the arpeggio fading on A minor, then the game-over blip: the end',
48    }),
49  },
50})
51
52// how much of each part the section decidedThisSection has: out, back or full
53const 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', [
54  'out: silent for the section',
55  'back: under the rest of the mix',
56  'full: as written',
57])
58const djJev = jev({
59  state: song,
60  questions: {
61    arps: level('the arpeggio (the square-wave chord arpeggio, under every section as written except the builds, double-time in drop 2)'),
62    lead: level('the chip lead (the square-wave melody, in the drops only, an octave up in harmony in drop 2)'),
63    pong: level('Pong (blips answering the arpeggio in the intro, answering the chip lead in the drops, and in the builds slowing down to one blip per 227 ms decision)'),
64    drums: level('the drums (the half-speed beat in the verses; in the drops the jungle kick and 808, the crushed amen break, hats and a crash where each drop lands)'),
65    bass: level('the bass (a triangle bass in the verses, the reese and sine sub bass in the drops)'),
66    vocals: level('the vocoded verse lines'),
67    into: choice('How should the song move into the next section (decidedThisSection)? It plays over the last bar of the section before it', {
68      cut: 'straight in, no transition',
69      fill: 'a crushed amen fill in place of the written break, chopped into rolls down into the first beat',
70      riser: 'a square-wave sweep climbing A minor three octaves over the last bar, like a power-up',
71      dropout: 'everything stops for the last beat, a gap the last word rings into',
72    }),
73    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', [
74      'held back: quieter',
75      'steady: a little under full',
76      'driving: full volume',
77      'all out: full volume, nothing held back',
78    ]),
79  },
80})
81
82// The transitions' rules, as code: a build always ends as written (so only a
83// cut or a dropout out of one), a fill goes into a verse or a build, a riser
84// into a build or a drop, a dropout into a drop or the outro.
85const intoAllowed = ({ section: next, playingNow: now = '' }) => [
86  'cut',
87  ...((next.startsWith('verse') || next.startsWith('build')) && !now.startsWith('build') ? ['fill'] : []),
88  ...((next.startsWith('build') || next.startsWith('drop')) && !now.startsWith('build') ? ['riser'] : []),
89  ...(next.startsWith('drop') || next === 'outro' ? ['dropout'] : []),
90]
91
92// a little chance in the form: the section is drawn from Jev's probabilities
93const { section } = formJev.every(4, { sample: 0.7 })
94const dj = djJev.every(4, {
95  after: { section },
96  forget: ['into'],
97  allow: { into: intoAllowed },
98  sample: { into: 1 },
99  fallback: { arps: 2, lead: 2, pong: 2, drums: 2, bass: 2, vocals: 2, into: 'cut', energy: 3 },
100})
101// A part at Jev's level: silent when out, otherwise its velocity scaled by
102// the level. Each part has its own orbit, named for its question, so the
103// meter hears it alone (measuredSound).
104const part = (name, pat) =>
105  pat.orbit(name).mask(dj[name].score.round().gt(0)).mul(velocity(dj[name].score.round().div(2)))
106
107// bars: 1-8 intro | 9-16 verse 1 | 17-20 build | 21-32 DROP 1 | 33-40 verse 2
108//       41-44 build | 45-60 DROP 2 | 61-64 outro          (each mask = 64 steps)
109const pongIn  = "<0!4 1!4 0!56>"
110const verses  = "<0!8 1!8 0!16 1!8 0!24>"
111const verse1  = "<0!8 1!8 0!48>"
112const verse2  = "<0!32 1!8 0!24>"
113const builds  = "<0!16 1!4 0!20 1!4 0!20>"
114const drops   = "<0!20 1!12 0!12 1!16 0!4>"
115const drop1   = "<0!20 1!12 0!32>"
116const drop2   = "<0!44 1!16 0!4>"
117const unsung  = "<1!16 0!16 1!8 0!24>"      // the intro and the verses
118const outro   = "<0!60 1!4>"
119const crash   = "<0!20 1 0!23 1 0!19>"
120const final   = "<0!60 1 0!3>"
121const gameOver = "<0!63 1>"
122// the line each verse bar starts (verse 1 bars 9-16, verse 2 bars 33-40)
123const titleLines = "<0!14 1 0!23 1 0!25>"
124
125// two bars per chord: Am Am F F C C G G. The drops play it four bars on
126// (`.early(4)`), so each lands on Am.
127const roots = "<a1!2 f1!2 c2!2 g1!2>"
128const subs  = "<a1!2 f2!2 c2!2 g2!2>"   // the sine sub: F and G an octave up, over 45 Hz
129const arp = "<[a4 c5 e5 a5 e5 c5 a4 e4]!2 [f4 a4 c5 f5 c5 a4 f4 c4]!2 [c5 e5 g5 c6 g5 e5 c5 g4]!2 [g4 b4 d5 g5 d5 b4 g4 d4]!2>"
130// the amen at its own groove: 8 slices a bar (each an eighth of the break),
131// choked; a new chop order, rolling in its fourth bar
132const amen = "<[0 1 2 3 4 5 6 7] [8 9 10 11 12 13 14 15] [0 1 2 3 16 17 18 19] [24 25 [26 26] 27 [28 28] 29 [30 31] [31 31]]>"
133
134// ── parts ─────────────────────────────────────────
135// square arpeggio: the whole song except the builds, double-time in drop 2,
136// on A minor alone in the outro, fading
137const arpSq = (p) => p.s("square").decay(.12).sustain(0).lpf(3200)
138const arps = stack(
139  arpSq(note(arp)).gain("<.24!8 .14!56>").mask(unsung),
140  arpSq(note(arp).early(4)).gain(.2).mask(drops),
141  note(arp).early(4).ply(2).s("square").decay(.08).sustain(0).lpf(3600).pan(.7).gain(.2).mask(drop2),
142  arpSq(note("[a4 c5 e5 a5 e5 c5 a4 e4]")).gain("<.2 .14 .08 .04>").mask(outro)
143)
144
145// the drops' riff: the chip lead calls in the first half of each bar, and
146// Pong answers it in the second, ping to pong across the stereo field
147const call   = "<[4 ~ 7 4 ~ ~ ~ ~] [2 4 ~ 0 ~ ~ ~ ~] [5 ~ 7 5 ~ ~ ~ ~] [2 3 ~ 5 ~ ~ ~ ~] [4 ~ 6 4 ~ ~ ~ ~] [2 4 ~ 7 ~ ~ ~ ~] [6 ~ 8 6 ~ ~ ~ ~] [1 3 ~ 6 ~ ~ ~ ~]>"
148const answer = "<[~ ~ ~ ~ 4 ~ 0 ~] [~ ~ ~ ~ 2 ~ 0 ~] [~ ~ ~ ~ 5 ~ 2 ~] [~ ~ ~ ~ 5 ~ 0 ~] [~ ~ ~ ~ 4 ~ 2 ~] [~ ~ ~ ~ 7 ~ 4 ~] [~ ~ ~ ~ 6 ~ 1 ~] [~ ~ ~ ~ 1 ~ 3 4]>"
149const chip = (p) => p.s("square").decay(.25).sustain(.15).lpf(4000)
150  .delay(.25).delaytime(.1765).delayfeedback(.3)
151const lead = stack(
152  chip(n(call).early(4).scale("A4:minor")).gain(.6).mask(drop1),
153  // drop 2: an octave up, with a third above
154  chip(n(call).early(4).scale("A5:minor")).gain(.5).mask(drop2),
155  chip(n(call).early(4).add(n(2)).scale("A5:minor")).gain(.35).mask(drop2)
156)
157
158const pong = stack(
159  // blips answering the arp in the intro
160  note("~ ~ ~ a5 ~ ~ e5 ~").s("square").decay(.05).sustain(0)
161    .pan("<0 1>").gain(.3).mask(pongIn),
162  // in the builds: sixteenths slowing to one blip per 227 ms decision
163  // (6.22 a bar) across bars 1-2, then nothing under the hit and "oh no"
164  note("a5").s("square").decay(.08).sustain(0)
165    .struct("<[x*8 x*6] [x*4 x*3.11] ~ ~>").pan(sine.fast(3)).gain(.7).mask(builds),
166  // in the drops: answering the chip lead
167  n(answer).early(4).scale("A5:minor").s("square").decay(.06).sustain(0)
168    .pan("[.15 .15 .85 .85]*2").gain(.45).mask(drops)
169)
170
171// the written amen, except where a fill takes a section's last bar
172const amenOut = stack("<1!3 0>", dj.into.choice.early(1).fmap((c) => c !== 'fill').mask("<0!3 1>"))
173const drums = stack(
174  // verses: half-speed beat
175  s("bd:3").struct("x ~ ~ ~ ~ ~ ~ ~ ~ ~ x ~ ~ ~ ~ ~").gain(.6).mask(verses),
176  s("sn:3").struct("~ ~ ~ ~ ~ ~ ~ ~ x ~ ~ ~ ~ ~ ~ ~").gain(.35).mask(verses),
177  s("hh*8").gain(".2 .1").mask(verses),
178
179  // drops: a jungle kick with 808 underneath, varied every fourth bar;
180  // snare on 2 and 4; the crushed amen at its own groove
181  s("[bd:3,808bd:1]").struct("<[x ~ ~ ~ ~ ~ x ~ ~ ~ x ~ ~ ~ ~ ~]!3 [x ~ x ~ ~ ~ ~ ~ ~ x ~ ~ x ~ ~ ~]>").gain(.45).mask(drops),
182  s("sn:3").struct("~ ~ ~ ~ x ~ ~ ~ ~ ~ ~ ~ x ~ ~ ~").room(.15).gain(.4).mask(drops),
183  n(amen).s("amencutup").cut(1).crush(6).hpf(150).gain(.35).mask(drops).mask(amenOut),
184  s("hh:0*8").gain(".7 .35").mask(drop1),
185  s("hh:0*16").gain(".7 .3 .5 .3").mask(drop2),
186  s("hh27:1").gain(2).mask(crash)
187)
188
189const bass = stack(
190  note(roots).add(note(12)).s("triangle").struct("x ~ ~ x ~ ~ x ~")
191    .decay(.3).sustain(.4).gain(.5).mask(verses),
192  // the reese, its filter opening further in drop 2
193  note(roots).struct("x@2 ~ x@3 ~@2 x@4 x@2 x@2").s("supersaw").detune(.6)
194    .lpf(sine.range(220, 900).slow(8)).lpq(3).shape(.3).gain(.45).early(4).mask(drop1),
195  note(roots).struct("x@2 ~ x@3 ~@2 x@4 x@2 x@2").s("supersaw").detune(.6)
196    .lpf(sine.range(300, 1600).slow(8)).lpq(3).shape(.3).gain(.45).early(4).mask(drop2),
197  note(subs).struct("x@2 ~ x@3 ~@2 x@4 x@2 x@2").s("sine").gain(.6).early(4).mask(drops)
198)
199
200// the vocoded verse lines Jev levels: one per 2 bars, each on its own chord
201const vox = stack(
202  s("<rightjump ~ tictactoe ~ pidloop ~ ~ ~>").gain(1).room(.2).mask(verse1),
203  s("<twotwoseven ~ joysticks ~ sudo ~ ~ ~>").gain(1).room(.2).mask(verse2)
204)
205
206// the hook: the title lines and the builds' memes. It always plays, like the
207// ending: the energy and a dropout's gap reach it, no part level does.
208const hook = stack(
209  s("minimax").gain(1).room(.2).mask(titleLines),
210  // the blue shell locks on under the slowing Pong, then hits on bar 3 at
211  // full; its tail stops before "oh no"
212  s("<blueshell ~ ~ ~>").end(.65).gain(.6).mask(builds),
213  s("<~ ~ blueshell ~>").begin(.65).end(.7).gain(.9).mask(builds),
214  s("<~ ~ [~ ohno@7] ~>").gain(1).mask(builds),
215  // the fall, after "oh no", from bar 4
216  s("<~ ~ ~ kartfall>").gain(.7).room(.4).mask(builds)
217).orbit('vocals')
218
219// the end: kick, 808, crash and a sine A left to ring, then the game-over
220// blip on A. It always plays.
221const ending = stack(
222  s("[bd:3,808bd:1]").room(.6).size(.9).gain(.55).mask(final),
223  s("hh27:1").room(.6).size(.9).gain(2.5).mask(final),
224  note("a1").s("sine").decay(1.4).sustain(0).gain(.5).mask(final),
225  note("a3").s("square").decay(.6).sustain(0).gain(.35).mask(gameOver)
226)
227
228const once = (p) => p.mask("<1!64 0!936>")   // play once, then silence
229
230// ── the form, as rules in code: which sections may follow which ──
231// `at` is where each starts as written; an empty `next` ends the song. The
232// written order is always allowed, and is what plays without Jev. Verse 2
233// always plays before drop 2: build2 follows only verse2b.
234// `times`: drop 2's loops go round once more at most (SPEC.md, "Loops end").
235const form = walk(section, {
236  max: 24,
237  sections: {
238    intro: { at: 0, next: ['introB'] },
239    introB: { at: 4, next: ['verse1'] },
240    verse1: { at: 8, next: ['verse1b'] },
241    verse1b: { at: 12, next: ['build1'] },
242    build1: { at: 16, next: ['drop1'] },
243    drop1: { at: 20, next: ['drop1b'] },
244    drop1b: { at: 24, next: ['drop1c'] },
245    drop1c: { at: 28, next: ['verse2', 'verse1'] },
246    verse2: { at: 32, next: ['verse2b'] },
247    verse2b: { at: 36, next: ['build2', 'build1'] },
248    build2: { at: 40, next: ['drop2'] },
249    drop2: { at: 44, next: ['drop2b'] },
250    drop2b: { at: 48, next: ['drop2c'], times: 2 },
251    drop2c: { at: 52, next: ['drop2d', 'drop2b'], times: 2 },
252    drop2d: { at: 56, next: ['outro', 'drop2c'] },
253    outro: { at: 60, next: [] },
254  },
255})
256
257// ── the song: Jev's section, parts and energy, composed ──
258// Each section plays its own bars, each part at the level Jev chose; the
259// energy scales every part's velocity together, so the mix keeps its
260// balance. The hook and the ending always play.
261const parts = form.arrange(once(stack(
262  part('arps', arps),
263  part('lead', lead),
264  part('pong', pong),
265  part('drums', drums),
266  part('bass', bass),
267  part('vocals', vox),
268  hook,
269)))
270
271// ── into the next section: over the last bar of each, the next one's answer ──
272// Past the last section there is no next one, so nothing plays there. Under
273// a verse's sung lines a fill or riser waits for the last half-bar.
274const into = dj.into.choice.early(1)
275const clearOfLines = (p) => stack(
276  p.mask(section.choice.fmap((s) => !s.startsWith('verse'))),
277  p.mask(section.choice.fmap((s) => s.startsWith('verse'))).mask("[0 1]"),
278)
279const transition = into.pick({
280  cut: silence,
281  fill: clearOfLines(n("[12 12] 13 14 15 [28 28] 29 [30 30 30 30] [31 31 31 31]").s("amencutup").crush(6)
282    .hpf(150).gain(.45)).orbit('drums').mask(dj.drums.score.round().gt(0)),
283  riser: clearOfLines(n(saw.range(0, 22).segment(16).floor()).scale("A2:minor").s("square").decay(.05).sustain(0).lpf(4000)
284    .gain(saw.range(.05, .2))).orbit('arps').mask(dj.arps.score.round().gt(0)),
285  dropout: silence,
286}).mask("<0!3 1>")
287// dropout: the parts stop for the section's last beat. The outro has no
288// next section, so its last beat plays.
289const lastBeat = "<0!3 [0 0 0 1]>"
290const gap = stack(
291  "<1!3 [1 1 1 0]>",
292  into.fmap((c) => c !== 'dropout').mask(lastBeat),
293  section.choice.fmap((s) => s === 'outro').mask(lastBeat),
294)
295
296stack(parts.mask(gap), transition, form.arrange(once(ending)))
297  .mul(velocity(dj.energy.score.div(3).range(.55, 1)))
298  .mul(postgain(.5)) // the master level: peaks under -1 dBFS (superdough has no limiter)