song.jsannotatedsong.jssource278 lines · 14.5 KB · raw
1// LIGHTNING IN A BOTTLE — metal anthem, 150bpm, 64 bars as written; Jev arranges 7-16 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(150/60/4)
4await samples('github:tidalcycles/dirt-samples')
5await samples('jev-samples/strudel.json')
6
7// bars: 1-8 intro | 9-16 riff | 17-24 verse | 25-32 chorus | 33-40 build
8//       41-48 drop | 49-56 drop2 | 57-64 outro             (each mask = 64 steps)
9// Where each section starts as written: the form (walk) and the text Jev is
10// told (playsIn) both read it.
11const at = { intro: 0, riff: 8, verse: 16, chorus: 24, build: 32, drop: 40, drop2: 48, outro: 56 }
12
13const introBars  = "<1!8 0!56>"
14const afterIntro = "<0!8 1!56>"
15const quoteOn    = "<0 0 1 0!61>"
16const riffPedal  = "<0!8 1!8 0!48>"
17const riffChord  = "<0!16 1!16 0!24 1!4 0!4>"   // verse, chorus, outro
18const riffBuild  = "<0!32 1!8 0!24>"
19const riffHigh   = "<0!24 1!16 0!24>"           // chorus and build; the drop has its own
20const bassRoots  = "<0!16 1!44 0!4>"
21const rock       = "<0!8 1!16 0!8 1!8 0!16 1!4 0!4>"
22const chorus     = "<0!24 1!8 0!32>"
23const hats       = "<0!8 1!32 0!16 1!4 0!4>"
24const snare      = "<0!8 1!52 0!4>"
25const drop       = "<0!40 1!16 0!8>"
26const power      = "<0!24 1!8 0!8 1!16 0!8>"
27const verse      = "<0!16 1!8 0!40>"
28const outroVox   = "<0!56 1 0!7>"
29const fills      = "<0!7 1 0!56>"
30const roll39     = "<0!38 1 0!25>"
31const roll40     = "<0!39 1 0!24>"
32const riserBars  = "<0!36 1!4 0!24>"
33const shotgun    = "<0!8 1 0!23 1 0!7 1 0!23>"
34const crash      = "<0!8 1 0!17 1 0 1 0 1 0!9 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0!9>"   // 9, 27, 29, 31, the drop's chords
35const crashSoft  = "<0!24 1 0!39>"              // 25, under the sung "Th-"
36const final      = "<0!60 1 0!3>"
37
38// Each part's masks, the one place that says where it plays: the parts
39// below play from these, and the text Jev is told is derived from them.
40const where = {
41  riff: [riffPedal, riffChord, riffBuild, riffHigh, drop],
42  chords: [power],
43  bass: [introBars, bassRoots],
44  drums: [fills, rock, chorus, hats, snare, drop, roll39, roll40, riserBars, crash, crashSoft, shotgun],
45  vocals: [verse, chorus],
46}
47// a mask is on in a section if the pattern itself has an event there
48const onIn = (mask, from, to) => mask.queryArc(from, to).some((h) => h.value && Number(h.value) !== 0)
49const sectionNames = Object.keys(at)
50const sectionsOf = (masks) => sectionNames.filter((name) =>
51  masks.some((m) => onIn(m, at[name], at[name] + 8)))
52const playsIn = (partName) => 'plays in: ' + sectionsOf(where[partName]).join(', ')
53const plays = (section) => 'plays: ' + Object.keys(where).filter((p) => sectionsOf(where[p]).includes(section)).join(', ')
54
55// ── Jev: two of them, in TypeSafe's own shape (SPEC.md, "Jev in the booth") ──
56// formJev picks the next section; djJev, asked once that is decided (questions
57// in one request cannot see each other), says how much of each part plays it,
58// how the song moves into it, and how hard. Every 8 bars. The form's rules (walk, at the bottom) narrow the
59// sections Jev may pick and end the song; without Jev it plays as written.
60const song = {
61  title: 'LIGHTNING IN A BOTTLE',
62  what: 'a metal anthem on the Doom E1M1 riff, about using Jev properly: code decides what code can decide, Jev takes the rest',
63  tempo: '150 bpm, 4/4, 8 bars a section',
64  key: 'E minor',
65  chords: 'Em Em C C G G D D, two bars each, under the riff and the power chords',
66  lyrics: {
67    intro: 'We have lightning in a bottle, and yet it is not useful.',
68    verse: 'Remove the human from the loop / Routine calls never leave the machine / Only the residue goes to Jev / Forty-four out of forty-four',
69    chorus: 'The code decides what code can decide / Jev takes the rest',
70    outro: 'The code decides what code can decide',
71  },
72}
73
74const formJev = jev({
75  state: song,
76  questions: {
77    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', {
78      intro: 'a sub pulse and a plain robot voice reading the quote; ' + plays('intro'),
79      riff: 'the E1M1 riff on one note, the rock beat starts, a shotgun blast; ' + plays('riff'),
80      verse: 'the riff follows the chords under the vocoded verse: "Remove the human from the loop … Forty-four out of forty-four"; ' + plays('verse'),
81      chorus: 'the chorus: power chords, the riff an octave up, a fuller beat, the hook sung; ' + plays('chorus'),
82      build: 'the build: the riff\'s filter opening, a noise riser, a snare roll into the drop; ' + plays('build'),
83      drop: 'the drop, first half: sixteenth double kick, the guitars wide open, power chords, crashes; ' + plays('drop'),
84      drop2: 'the drop, second half, the same; ' + plays('drop2'),
85      outro: 'the last hook line, then the E power chord and a crash left to ring: the end; ' + plays('outro'),
86    }),
87  },
88})
89
90// how much of each part the section decidedThisSection has: out, back or full
91const level = (part, what) => score('How much of ' + part + ' (' + what + '; ' + playsIn(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', [
92  'out: silent for the section',
93  'back: quieter than written',
94  'full: at its written level',
95])
96const djJev = jev({
97  state: song,
98  questions: {
99    riff: level('riff', 'the E1M1 riff, the song\'s signature'),
100    chords: level('chords', 'the power chords, the weight of the band'),
101    bass: level('bass', 'the sub on the chord roots, low'),
102    drums: level('drums', 'the drums, the crashes and the shotgun'),
103    vocals: level('vocals', 'the vocoded verse and chorus lines'),
104    into: choice('How should the song move into the next section (decidedThisSection)? It plays over the last bar of the section before it', {
105      cut: 'straight in, no transition',
106      fill: 'a drum fill down the toms into the first beat',
107      riser: 'white noise swelling up over the last bar',
108      dropout: 'everything stops for the last beat, a gap before the section hits',
109    }),
110    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', [
111      'held back: quieter',
112      'steady: a little under full',
113      'driving: full volume',
114      'all out: full volume, nothing held back',
115    ]),
116  },
117})
118
119// a little chance in the form: the section is drawn from Jev's probabilities
120const { section } = formJev.every(8, { sample: 0.7 })
121// The build has its own riser, so the move into the drop is never a riser.
122const dj = djJev.every(8, {
123  after: { section },
124  forget: ['into'],
125  allow: { into: ({ section: next }) => (next === 'drop' ? ['cut', 'fill', 'dropout'] : ['cut', 'fill', 'riser', 'dropout']) },
126  fallback: { riff: 2, chords: 2, bass: 2, drums: 2, vocals: 2, into: 'cut', energy: 3 },
127})
128
129// how the song moves into the next section, over the last bar of each
130const into = dj.into.choice.early(1)
131const onCutOr = (...moves) => (c) => moves.includes(c)
132
133// A part at Jev's level: silent when out, otherwise its velocity scaled by
134// the level. The intro is not Jev's: it plays every part as written (level
135// 2). Parts are composed inside the arrangement, so these are written bars.
136// Each part has its own orbit, named for its question, so the meter hears
137// it alone (measuredSound).
138const levelOf = (name) => stack(pure(2).mask(introBars), dj[name].score.mask(afterIntro))
139const part = (name, pat) => {
140  const lvl = levelOf(name)
141  return pat.orbit(name).mask(lvl.round().gt(0)).mul(velocity(lvl.round().div(2)))
142}
143// energy scales the parts together; the intro plays at full
144const energy = stack(pure(1).mask(introBars), dj.energy.score.div(3).range(.55, 1).mask(afterIntro))
145
146// E1M1 ("At Doom's Gate"), eighths, MIDI numbers: E2=40 E3=52 D3=50 C3=48 A#2=46 B2=47
147const riff = "<[40 40 52 40 40 50 40 40] [48 40 40 46 40 40 47 48] [40 40 52 40 40 50 40 40] [48 40 40 46@5]>"
148// two bars per chord: Em Em C C G G D D — the riff follows the root
149const shift  = "<0!2 -4!2 3!2 -2!2>"
150const roots  = "<28!2 24!2 31!2 26!2>"   // E1 C1 G1 D1
151const chords = "<[e2,b2,e3]!2 [c2,g2,c3]!2 [g2,d3,g3]!2 [d2,a2,d3]!2>"
152
153// a dropout's gap is silent: the section's last bar is cut short before its
154// last beat, so the held A#2 does not ring through it
155const gapClip = stack(pure(1).mask("<1!7 0>"), into.fmap((c) => (c === 'dropout' ? .6 : 1)).mask("<0!7 1>"))
156
157// ── parts ─────────────────────────────────────────
158const saw1 = (p) => p.s("sawtooth").decay(.14).sustain(.2).distort(1.5)
159const gtr = stack(
160  saw1(note(riff)).lpf(1500).gain(.35).mask(riffPedal),
161  saw1(note(riff).add(note(shift))).lpf(1500).gain(.35).mask(riffChord),
162  // the build: the filter opens from dull to bright across its 8 bars
163  saw1(note(riff).add(note(shift))).lpf(saw.slow(8).range(700, 3000)).gain(.35).mask(riffBuild),
164  // the drop: the guitars open up
165  saw1(note(riff).add(note(shift))).lpf(3000).gain(.35).mask(drop),
166  note(riff).add(note(shift)).add(note(12)).s("square").decay(.1).sustain(.1).lpf(2500).gain(.14).mask(riffHigh),
167  note(riff).add(note(shift)).add(note(12)).s("square").decay(.1).sustain(.1).lpf(3000).gain(.25).mask(drop),
168).clip(gapClip)
169
170// power chords, chorus and drop
171const stabs = note(chords).struct("x ~ ~ x ~ ~ x ~").s("sawtooth").decay(.3).sustain(.3)
172  .lpf(2200).distort(2).gain(.22).mask(power)
173
174// sub: a pulse in the intro, then the chord roots, each with a triangle an
175// octave up so small speakers hear it
176const sub = stack(
177  note("28*4").s("sine").decay(.35).sustain(0).gain(.7).mask(introBars),
178  note("40*4").s("triangle").decay(.35).sustain(0).lpf(300).gain(.25).mask(introBars),
179  note(roots).struct("x x x x").s("sine").decay(.3).sustain(.3).gain(.55).mask(bassRoots),
180  note(roots).add(note(12)).struct("x x x x").s("triangle").decay(.3).sustain(.3).lpf(250).gain(.2).mask(bassRoots),
181)
182
183const kit = stack(
184  s("bd:3").struct("x ~ ~ x x ~ ~ ~").gain(.6).mask(rock),
185  s("bd:3").struct("x ~ x x x ~ x ~").gain(.6).mask(chorus),
186  s("bd:3").struct("x*16").gain(.3).mask(drop),
187  s("808bd:1*4").gain(.2).mask(drop),
188  s("sn:3").struct("~ ~ x ~ ~ ~ x ~").room(.3).gain(.52).mask(snare),
189  s("hh*8").gain(".16 .08").mask(hats),
190  s("cr*4").gain(.25).mask(drop),   // cr is a ride
191  // the intro's written fill: only when the move into the riff is a cut
192  s("ht ht mt mt lt lt lt lt").gain(.5).mask(fills).mask(into.fmap(onCutOr('cut'))),
193  // the build's roll, one crescendo over bars 39-40: only on a cut or a
194  // dropout (bar 39 looks two bars ahead for the drop's answer)
195  s("sn:3*16").gain(saw.slow(2).range(.16, .55)).mask(roll39).mask(dj.into.choice.early(2).fmap(onCutOr('cut', 'dropout'))),
196  s("sn:3*16").gain(saw.slow(2).range(.16, .55)).mask(roll40).mask(into.fmap(onCutOr('cut', 'dropout'))),
197  // the build's riser, bars 37-40
198  s("white*16").hpf(saw.slow(4).range(400, 8000)).gain(saw.slow(4).range(.03, .25)).decay(.07).sustain(0).mask(riserBars),
199  s("hh27:1").gain(3).mask(crash),       // hh27:1 is the crash
200  s("hh27:1").gain(2.1).mask(crashSoft), // 3 dB down, under "Th-"
201  s("lbshotgun").gain(.85).mask(shotgun),
202)
203
204const vox = stack(
205  s("<lbremove ~ lbroutine ~ lbresidue ~ lbfortyfour ~>").gain(.85).room(.2).mask(verse),
206  s("<lbcodeem ~ lbcodec ~ lbrestg ~ lbrestd ~>").gain(.85).room(.25).mask(chorus),
207)
208
209// the story: the quote and the outro's hook line. Not Jev's: they always play.
210const story = stack(
211  s("lbquote").gain(.85).room(.4).mask(quoteOn),
212  s("lbcodeem").gain(.85).room(.4).mask(outroVox),
213).orbit('vocals')
214
215// the end: E power chord, crash, kick and shotgun, left to ring. It always plays.
216const ending = stack(
217  note("[e2,b2,e3]").s("sawtooth").decay(5.5).sustain(0).lpf(2000).distort(1.5)
218    .room(.7).size(.95).gain(.3).mask(final),
219  s("[bd:3,808bd:1]").room(.6).gain(.5).mask(final),
220  s("hh27:1").room(.6).gain(2.5).mask(final),
221  s("lbshotgun").gain(.85).mask(final),
222).mul(gain(.45)) // with the band: measured 6 dB over it at full, the master's peak
223
224const once = (p) => p.mask("<1!64 0!936>")   // play once, then silence
225
226// ── the form, as rules in code: which sections may follow which ──
227// `at` is where each starts as written; an empty `next` ends the song. The
228// written order is always allowed, and is what plays without Jev. One drop:
229// drop2 never leads back into it.
230const form = walk(section, {
231  max: 16,
232  sections: {
233    intro: { at: at.intro, next: ['riff'] },
234    riff: { at: at.riff, next: ['verse', 'chorus'] },
235    verse: { at: at.verse, next: ['chorus', 'riff'] },
236    chorus: { at: at.chorus, next: ['build', 'verse'] },
237    build: { at: at.build, next: ['drop'] },
238    drop: { at: at.drop, next: ['drop2'] },
239    drop2: { at: at.drop2, next: ['verse', 'chorus', 'outro'] },
240    outro: { at: at.outro, next: [] },
241  },
242})
243
244// ── the song: Jev's section, parts and energy, composed ──
245// Each section plays its own bars, each part at the level Jev chose; the
246// energy scales the parts' velocity together, so the mix keeps its
247// balance. The story and the ending always play.
248const parts = form.arrange(once(stack(
249  part('riff', gtr.mul(gain(.8))),
250  part('chords', stabs.mul(gain(.8))),
251  part('bass', sub),
252  part('drums', kit.mul(gain(.42))), // the drums 7.5 dB down: measured 10 dB over the vocals
253  part('vocals', vox),
254).mul(velocity(energy))))
255
256// ── into the next section: over the last bar of each, the next one's answer ──
257// Past the last section there is no next one, so nothing plays there. The
258// fill is the drums', at the level of the section it ends; this is played
259// time, not written bars, so the intro (always the first 8) plays it full.
260const drumsNow = stack(pure(2).mask("<1!8 0!992>"), dj.drums.score.mask("<0!8 1!992>"))
261const transition = into.pick({
262  cut: silence,
263  fill: s("ht ht mt mt lt lt [lt lt] [sn:3 sn:3]").gain(.5).orbit('drums')
264    .mask(drumsNow.round().gt(0)).mul(velocity(drumsNow.round().div(2))),
265  riser: s("white*16").hpf(saw.range(400, 8000)).gain(saw.range(.05, .35)).decay(.07).sustain(0).orbit('riser'),
266  dropout: silence,
267}).mask("<0!7 1>")
268// dropout: the parts stop for the section's last beat. The outro has no
269// next section, so its last beat plays.
270const lastBeat = "<0!7 [0 0 0 1]>"
271const gap = stack(
272  "<1!7 [1 1 1 0]>",
273  into.fmap((c) => c !== 'dropout').mask(lastBeat),
274  section.choice.fmap((s) => s === 'outro').mask(lastBeat),
275)
276
277stack(parts.mask(gap), transition, form.arrange(once(stack(story, ending))))
278  .mul(postgain(.52)) // the master level: peaks under -1 dBFS (superdough has no limiter)