song.jsannotatedsong.jssource298 lines · 15.4 KB · raw

HEY! LISTEN! — chiptune jungle, 170bpm, 64 bars as written; Jev arranges up to 24 sections. Spec: SPEC.md 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')

── 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 which parts play 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.

Which parts play in each section as written (SPEC.md, "The parts"). Jev is told this, and "back" is relative to the loudest of these (loudest).

15const verseParts = ['arps', 'drums', 'bass', 'vocals']
16const dropParts = ['arps', 'lead', 'drums', 'bass']
17const partsBySection = {
18  intro: ['arps'],
19  introB: ['arps', 'vocals'],
20  verse1: verseParts,
21  verse1b: verseParts,
22  build1: verseParts,
23  drop1: dropParts,
24  drop1b: dropParts,
25  drop1c: dropParts,
26  verse2: verseParts,
27  verse2b: verseParts,
28  build2: verseParts,
29  drop2: dropParts,
30  drop2b: dropParts,
31  drop2c: dropParts,
32  drop2d: dropParts,
33  outro: ['arps'],
34}
36const song = {
37  title: 'HEY! LISTEN!',
38  what: 'chiptune jungle about Jev playing A Link to the Past, where a C bot walks Link and Jev picks the goal: Navi shouting Hey! Listen!, the secret-found jingle on every drop, an amen break under SNES-style square waves',
39  tempo: '170 bpm, 4 bars a section',
40  key: 'G minor',
41  chords: 'Gm Gm Eb Eb F F D D, two bars each; a section starts on either Gm or F',
42  lyrics: {
43    introB: 'Hey! Listen!',
44    verse1: 'Jev plays Link to the Past / The core\'s a library',
45    verse1b: 'Three questions a minute / The bot walks, Jev picks',
46    build1: 'It\'s dangerous to go alone! Take this.',
47    verse2: 'Native now, WASM some day / Same program, both',
48    verse2b: 'Hey, listen / Jev plays Link to the Past',
49    build2: 'Hey! Listen! Hey! Listen!',
50    outro: 'Hey!',
51  },
52  partsBySection,
53}
54const parts = (s) => '. Parts: ' + partsBySection[s].join(', ')
55
56const formJev = jev({
57  state: song,
58  questions: {
59    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', {
60      intro: 'the square-wave arpeggio alone, with a wide reverb' + parts('intro'),
61      introB: 'the arpeggio, and Navi shouts Hey! Listen!' + parts('introB'),
62      verse1: 'verse 1, first half: half-speed beat, bass, and the vocoder sings ' + song.lyrics.verse1 + parts('verse1'),
63      verse1b: 'verse 1, second half: the vocoder sings ' + song.lyrics.verse1b + parts('verse1b'),
64      build1: 'the build: the old man\'s line, the arpeggio opening up over a bass pedal, then a snare roll into the drop' + parts('build1'),
65      drop1: 'drop 1 lands on the secret-found jingle alone, then amen break, 808, sub bass and the lead melody' + parts('drop1'),
66      drop1b: 'drop 1, middle' + parts('drop1b'),
67      drop1c: 'drop 1, last four bars' + parts('drop1c'),
68      verse2: 'verse 2, first half: half-speed beat, bass, and the vocoder sings ' + song.lyrics.verse2 + parts('verse2'),
69      verse2b: 'verse 2, second half: the vocoder sings ' + song.lyrics.verse2b + parts('verse2b'),
70      build2: 'the build: Hey! Listen! twice, the arpeggio opening up over a bass pedal, and a snare roll into the drop' + parts('build2'),
71      drop2: 'drop 2 lands on the secret-found jingle: sixteenth hats and a doubled arpeggio over the drop-1 groove' + parts('drop2'),
72      drop2b: 'drop 2, continued' + parts('drop2b'),
73      drop2c: 'drop 2, third four bars' + parts('drop2c'),
74      drop2d: 'drop 2, last four bars' + parts('drop2d'),
75      outro: 'the arpeggio fades out; Navi\'s last Hey!, then a kick, 808 and crash: the end' + parts('outro'),
76    }),
77  },
78})

how much of each part the section decidedThisSection has: out, back or full

81const 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. A part that does not play in that section (partsBySection) stays silent whatever you answer', [
82  'out: silent for the section',
83  'back: under the loudest part in the section, at half its level',
84  'full: as loud as written',
85])
86const djJev = jev({
87  state: song,
88  questions: {
89    arps: level('the arpeggios (the square-wave chord arpeggio with its wide reverb, in every section, doubled in drop 2)'),
90    lead: level('the lead (only in the drops: the square-wave lead melody, and the secret-found jingle that lands each drop alone)'),
91    drums: level('the drums (the half-speed beat in the verses, the snare rolls in the builds, the amen break, 808 and hats in the drops)'),
92    bass: level('the bass (a triangle bass in the verses, a triangle pedal in the builds, the supersaw and sine sub bass in the drops)'),
93    vocals: level('the voices (the vocoded verse lines, Navi\'s Hey! Listen! in the intro and build 2, the old man\'s line in build 1; the outro\'s last Hey! always plays)'),
94    into: choice('How should the song move into the next section (decidedThisSection)? It plays over the last bar of the section before it', {
95      cut: 'straight in, no transition',
96      fill: 'an amen fill over the last half bar, replacing the break, at the drums\' level',
97      riser: 'a square-wave sweep climbing three octaves over the last bar',
98      dropout: 'the parts stop for the last beat and their reverb rings on into the gap before the section hits',
99    }),
100    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', [
101      'held back: quieter, a little distance',
102      'steady: under full',
103      'driving: a touch under full',
104      'all out: full, as written',
105    ]),
106  },
107})

a little chance in the form: the section is drawn from Jev's probabilities

110const { section } = formJev.every(4, { sample: 0.7 })
111const dj = djJev.every(4, {
112  after: { section },
113  forget: ['into'],
114  // a fill never plays into a drop: the build's snare roll already is that fill
115  allow: { into: ({ section: next }) => (next.startsWith('drop') ? ['cut', 'riser', 'dropout'] : ['cut', 'fill', 'riser', 'dropout']) },
116  fallback: { arps: 2, lead: 2, drums: 2, bass: 2, vocals: 2, into: 'cut', energy: 3 },
117})

"back" is relative: each part's level is divided by the loudest level Jev chose among the parts that play in the section and are not out, so the loudest part plays as written and energy alone sets the overall level.

122const partNames = ['arps', 'lead', 'drums', 'bass', 'vocals']
123const zip = (pats) =>
124  pats.slice(1).reduce((acc, p) => acc.fmap((xs) => (x) => [...xs, x]).appLeft(p), pats[0].fmap((x) => [x]))
125const loudest = zip([section.choice, ...partNames.map((p) => dj[p].score)]).fmap(([s, ...scores]) => {
126  const heard = scores.filter((x, i) => Math.round(x) > 0 && (partsBySection[s] ?? []).includes(partNames[i]))
127  return heard.length ? Math.max(...heard) : 2
128})

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

132const part = (name, pat) =>
133  pat.orbit(name).mask(dj[name].score.round().gt(0)).mul(velocity(dj[name].score.div(loudest)))

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

137const heyIntro  = "<0!4 1 0!59>"
138const verses    = "<0!8 1!8 0!16 1!8 0!24>"
139const verse1    = "<0!8 1!8 0!48>"
140const verse2    = "<0!32 1!8 0!24>"
141const danger    = "<0!16 1 0!47>"
142const heyBuild  = "<0!40 1 0 1 0!21>"
143const builds    = "<0!16 1!4 0!20 1!4 0!20>"
144const notBuilds = "<1!16 0!4 1!20 0!4 1!20>"
145const roll      = "<0!18 1!2 0!22 1!2 0!20>"
146const drops     = "<0!20 1!12 0!12 1!16 0!4>"
147const drop1     = "<0!20 1!12 0!32>"
148const drop2     = "<0!44 1!16 0!4>"
149const secret    = "<0!20 1 0!23 1 0!19>"
150const leadRests = "<1!20 0 1!23 0 1!19>"   // the jingle lands alone
151const theEnd    = "<0!62 1 0>"
152const arpGain   = "<.38!8 .22!8 .32!16 .22!8 .32!20 .32 .26 .16 .08>"

two bars per chord: Gm Gm Eb Eb F F D D

155const arp = "<[g4 bb4 d5 g5 d5 bb4 g4 d4]!2 [eb4 g4 bb4 eb5 bb4 g4 eb4 bb3]!2 [f4 a4 c5 f5 c5 a4 f4 c4]!2 [d4 f#4 a4 d5 a4 f#4 d4 a3]!2>"
156const lead = "<[g5 ~ g5 ~ f5 g5 ~ d5] [~ d5 ~ bb4 ~ g4 ~ ~] [eb5 ~ eb5 ~ d5 eb5 ~ bb4] [~ bb4 ~ g4 ~ eb4 ~ ~] [f5 ~ f5 ~ eb5 f5 ~ c5] [~ c5 ~ a4 ~ f4 ~ ~] [f#5 ~ f#5 ~ e5 f#5 ~ a5] [a5 ~ f#5 ~ d5 ~ a4 ~]>"
157const bassRoots = "<43!2 39!2 41!2 38!2>"   // G2 Eb2 F2 D2, as MIDI numbers
158const subRoots  = "<31!2 27!2 29!2 26!2>"   // G1 Eb1 F1 D1
159const 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 ───────────────────────────────────────── chiptune, with a wide SNES-style reverb: the arpeggios, then the lead and jingle

163const arpLine = note(arp).s("square").decay(.12).sustain(0).room(.5).size(.8).gain(arpGain)
164const arps = stack(
165  arpLine.lpf(3200).mask(notBuilds),
166  // the builds rise: the filter opens from 400 Hz to 3.2 kHz over their four bars
167  arpLine.lpf(saw.slow(4).range(400, 3200)).mask(builds),
168  note(arp).ply(2).s("square").decay(.08).sustain(0).lpf(3600).pan(.7).gain(.2).mask(drop2)
169)
170const melody = stack(
171  note(lead).s("square").decay(.25).sustain(.15).lpf(4000).room(.5).size(.8)
172    .delay(.25).delaytime(.1765).delayfeedback(.3).gain(.6).mask(drops).mask(leadRests),
173  // the Zelda secret-found jingle, landing each drop: seven 16ths and a held last note
174  note("g5 f#5 d#5 a4 g#4 e5 g#5 c6@9").s("square").decay(.4).sustain(.3).release(.4)
175    .room(.6).size(.8).gain(.8).mask(secret)
176)
178const drums = stack(
179  // verses: half-speed beat
180  s("bd:3").struct("x ~ ~ ~ ~ ~ ~ ~ ~ ~ x ~ ~ ~ ~ ~").gain(.42).mask(verses),
181  s("sn:3").struct("~ ~ ~ ~ ~ ~ ~ ~ x ~ ~ ~ ~ ~ ~ ~").gain(.56).mask(verses),
182  s("hh*8").gain(".18 .08").mask(verses),
183  // drops: jungle two-step, 808 underneath
184  s("[bd:3,808bd:1]").struct("x ~ ~ ~ ~ ~ ~ ~ ~ ~ x ~ ~ ~ ~ ~").gain(.4).mask(drops),
185  s("sn:3").struct("~ ~ ~ ~ x ~ ~ ~ ~ ~ ~ ~ x ~ ~ ~").room(.15).gain(.42).mask(drops),
186  s("hh*8").gain(".25 .12").mask(drop1),
187  s("hh*16").gain(".25 .1 .18 .1").mask(drop2),
188  // the crash on each drop: hh27:1 is a crash (cr is a ride), about 15 dB quieter than cr
189  s("hh27:1").gain(1.75).mask(secret)
190)

the builds' snare roll (rising across its two bars) and the drops' rolling amen: a fill replaces these for its half-bar (noFill, below)

193const breaks = stack(
194  s("sn:3*16").gain(saw.range(.18, .7).slow(2)).speed(saw.range(1, 1.4).slow(2)).mask(roll),
195  n(amen).s("amencutup").hpf(150).cut(1).gain(.28).mask(drops)
196)
198const bass = stack(
199  note(bassRoots).s("triangle").struct("x ~ ~ x ~ ~ x ~")
200    .decay(.3).sustain(.4).gain(.5).mask(verses),
201  // the builds: a triangle pedal on G under the opening filter
202  note(43).s("triangle").struct("x*4").decay(.2).sustain(.3).gain(.4).mask(builds),
203  note(subRoots).struct("x@3 x@3 x@4 x@2 x@4").s("supersaw").detune(.6)
204    .lpf(sine.range(220, 900).slow(8)).lpq(3).shape(.3).gain(.45).mask(drops),
205  note(subRoots).struct("x@3 x@3 x@4 x@2 x@4").s("sine").gain(.6).mask(drops)
206)
207
208const vox = stack(
209  // vocoder verses, one line per 2 bars, each sung on its chord
210  s("<hljevplays ~ hlcore ~ hlframe ~ hlpress ~>").gain(1.4).room(.25).mask(verse1),
211  s("<hlnative ~ hlsame ~ hlheyvoc ~ hljevplaysd ~>").gain(1.4).room(.25).mask(verse2),
212  // Navi and the old man
213  s("hlheylisten").gain(1.5).room(.4).mask(heyIntro),
214  s("hldanger").gain(1.5).room(.3).mask(danger),
215  s("hlheylisten").gain(1.5).room(.4).mask(heyBuild)
216)

the end: Navi's last "Hey!" on beat 1 of bar 63, then kick, 808 and crash on beat 3. It always plays, at full: outside the part levels and the energy.

220const ending = stack(
221  s("hlhey").gain(1.2).room(.6).mask(theEnd),
222  s("[bd:3,808bd:1]").struct("~ ~ x ~").room(.6).size(.9).gain(.42).mask(theEnd),
223  s("hh27:1").struct("~ ~ x ~").room(.6).size(.9).gain(1.75).mask(theEnd)
224)
226const 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. The chords turn over every 8 bars, so each section may only be followed by one that starts on the other half of the turn (Gm Eb | F D). Build 2 comes only after verse 2, so every vocoded line is sung at least once. times: each drop's loop goes 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'], times: 2 },
245    drop1c: { at: 28, next: ['verse2', 'verse1', 'drop1b'] },
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})

── the song: Jev's section, parts and energy, composed ── Each section plays its own bars, each part at the level Jev chose.

259const arranged = form.arrange(once(stack(
260  part('arps', arps),
261  part('lead', melody),
262  part('drums', drums),
263  part('bass', bass),
264  part('vocals', vox),
265)))
266const arrangedBreaks = form.arrange(once(part('drums', breaks)))

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

270const into = dj.into.choice.early(1)
271const transition = into.pick({
272  cut: silence,
273  // the fill: the second half of the bar, at the drums' level
274  fill: part('drums', n("~ [[12 12] 13 14 15 [28 28] 29 [30 30 30 30] [31 31 31 31]]")
275    .s("amencutup").hpf(150).cut(1).gain(.28)),
276  riser: note(saw.range(43, 79).segment(16)).s("square").decay(.05).sustain(0).lpf(4000)
277    .room(.5).size(.8).gain(saw.range(.05, .2)).orbit('arps'),
278  dropout: silence,
279}).mask("<0!3 1>")

the fill replaces the break and the roll for that half-bar, so no hit doubles

281const noFill = stack(
282  "<1!3 [1 0]>",
283  into.fmap((c) => c !== 'fill').mask("<0!3 [0 1]>"),
284)

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

287const lastBeat = "<0!3 [0 0 0 1]>"
288const gap = stack(
289  "<1!3 [1 1 1 0]>",
290  into.fmap((c) => c !== 'dropout').mask(lastBeat),
291  section.choice.fmap((s) => s === 'outro').mask(lastBeat),
292)
294stack(
295  stack(arranged.mask(gap), arrangedBreaks.mask(noFill).mask(gap), transition)
296    .mul(velocity(dj.energy.score.div(3).range(.55, 1))),
297  form.arrange(once(ending)),
298).mul(postgain(.5)) // the master level: peaks under -1 dBFS, measured (SPEC.md, "Mix"; superdough has no limiter)