song.jsannotatedsong.jssource230 lines · 13.1 KB · raw

SELECT * FROM JEV — synthwave, 90bpm, 64 bars as written; Jev arranges up to 16 sections. Spec: SPEC.md Start from STOPPED so the song begins at bar 1 (re-running it mid-play keeps the cycle count).

3setcps(90/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 8 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: 'SELECT * FROM JEV',
14  what: 'synthwave about Jev inside SQL: a vocoder robot sings the query plan, select star from Jev where vibes are true',
15  tempo: '90 bpm, 8 bars a section',
16  key: 'F# minor; keyUp and outro are up a tone, in G# minor, however many of them play',
17  chords: 'F#m F#m D D A A E E, two bars each, arpeggiated in sixteenths; the song ends on G#m',
18  lyrics: {
19    verse1: 'Select star from Jev / Where vibes are true / Custom scan / Warm pool, cached answers',
20    chorus1: 'Select star from Jev / Where vibes are true / Hello there / General Kenobi / Where vibes are true',
21    verse2: 'Order by probability / Forty rows a batch / Two hundred fifty-five options / Then a label tree',
22    chorus2: 'Select star from Jev / Where vibes are true / Hello there / General Kenobi / Where vibes are true',
23  },
24}
26const formJev = jev({
27  state: song,
28  questions: {
29    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', {
30      intro: 'the arpeggio alone, its filter opening, then the drums come in',
31      verse1: 'verse 1: the vocoder sings the query plan over arps, pulsing bass and gated snare',
32      chorus1: 'the big chorus: the SQL lines and the Hello there / General Kenobi hook, with a clap, sixteenth hats, the arpeggio opened up and the bright saw lead',
33      instrumental: 'an instrumental: the saw lead up front',
34      verse2: 'verse 2: more of the query plan',
35      chorus2: 'the big chorus again, the SQL lines and the Hello there / General Kenobi hook',
36      keyUp: 'the key change: the instrumental up a tone, the lead up front, no vocals',
37      outro: 'the last of the key change, then the arpeggio with the kick and bass fading under it, resolving to a final gated snare and crash: the end',
38    }),
39  },
40})

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

43const 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', [
44  'out: silent for the section',
45  'back: under the rest of the mix',
46  'full: as written',
47])
48const djJev = jev({
49  state: song,
50  questions: {
51    arp: level('the arpeggio (sixteenth-note saw arps on the chords, the song\'s bed: filtered in the intro and opened up in the choruses, under every section as written, carrying the outro)'),
52    lead: level('the saw lead (the bright melody: at full in the choruses and up front in the instrumental and the key change)'),
53    bass: level('the bass (pulsing eighth-note chord roots with a sine sub, the floor under every section from verse 1 until the song\'s last bar)'),
54    drums: level('the drums (kick, gated snare and hats from bar 5 on, with a clap and sixteenth hats in the choruses; in the outro only the kick is left)'),
55    vocals: level('the vocoded SQL lines (verses and choruses only; nothing is sung in the intro, instrumental, key change or outro. The Hello there / General Kenobi hook is not part of this: it always plays in a chorus)'),
56    into: choice('How should the song move into the section being entered (decidedThisSection)? It plays over the last bar before it. Pick what suits that section on its own merits; the moves before it are no reason to repeat or avoid one', {
57      asWritten: 'as written: the 808 toms roll down across the second half of the bar into a chorus or the key change, and it goes straight in to any other section',
58      fill: 'the 808 toms rolling down across the second half of the bar, into any section',
59      riser: 'white noise swelling up across the whole bar through an opening high-pass',
60      dropout: 'everything stops for the last beat, a gap before the section hits',
61    }),
62    energy: score('How intense should the next section be? It scales the whole band\'s velocity. Shape the energy of the whole song, following its history, how it measured and how the listener reacted', [
63      'held back: the whole band at about half velocity (0.55)',
64      'steady: about 70%',
65      'driving: about 85%',
66      'all out: full velocity, as written',
67    ]),
68  },
69})

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

72const { section } = formJev.every(8, { sample: 0.7 })
73const dj = djJev.every(8, {
74  after: { section },
75  forget: ['into'],
76  fallback: { arp: 2, lead: 2, bass: 2, drums: 2, vocals: 2, into: 'asWritten', energy: 3 },
77})

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

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

bars: 1-8 intro | 9-16 verse 1 | 17-24 chorus | 25-32 instrumental | 33-40 verse 2 41-48 chorus | 49-60 instrumental, up a tone | 61-64 outro (each mask = 64 steps)

87const drums   = "<0!4 1!56 0!4>"
88const hats    = "<0!4 1!12 0!8 1!16 0!8 1!12 0!4>"      // eighths; the choruses have sixteenths
89const pulse   = "<0!60 1!3 0>"                          // the outro's kick, bars 61-63
90const bassOn  = "<0!8 1!55 0>"                          // verse 1 to bar 63
91const verse1  = "<0!8 1!8 0!48>"
92const verse2  = "<0!32 1!8 0!24>"
93const chorus  = "<0!16 1!8 0!16 1!8 0!16>"
94const leadOn  = "<0!16 1!16 0!8 1!20 0!4>"
95const crash   = "<0!16 1 0!7 1 0!15 1 0!7 1 0!15>"
96const final   = "<0!63 1>"
97const ending2 = "<0!62 1!2>"                            // the last two bars: G#m
98const arpLpf   = "<700 1000 1400 1800 2200!12 3600!8 2200!16 3600!8 2200!16>"
99const arpGain  = "<.54!4 .6!4 .36!8 .4!8 .5!8 .36!8 .4!8 .5!12 .7 .6 .5 .4>"
100const leadGain = "<0!16 1.2!16 0!8 1.2!20 0!4>"
101const outroLpf = "<20000!60 1600 900 500 300>"          // the outro kick's low-pass, closing bar by bar
102const bassLpf  = "<450!60 350 260 190 150>"             // and the bass's
103const subGain  = "<.45!60 .4 .32 .24 0>"

key change: up a tone for the last 16 bars (instrumental and outro only)

106const key = "<0!48 2!16>"

two bars per chord: F#m F#m D D A A E E — as MIDI numbers so the key change can add to them

109const fsm = "[54 57 61 66 69 66 61 57 54 57 61 66 69 66 61 57]"
110const arp = "<[54 57 61 66 69 66 61 57 54 57 61 66 69 66 61 57]!2 [50 54 57 62 66 62 57 54 50 54 57 62 66 62 57 54]!2 [57 61 64 69 73 69 64 61 57 61 64 69 73 69 64 61]!2 [52 56 59 64 68 64 59 56 52 56 59 64 68 64 59 56]!2>"
111const roots = "<30!2 26!2 33!2 28!2>"   // F#1 D1 A1 E1
112const lead = "<[73 ~ 73 71 69 ~ 71 73] [~ 69 ~ 66 ~ ~ ~ ~] [74 ~ 74 73 71 ~ 69 71] [~ 66 ~ 62 ~ ~ ~ ~] [73 ~ 76 ~ 73 71 69 ~] [~ 71 ~ 73 ~ 69 ~ ~] [71 ~ 71 73 71 ~ 68 ~] [64 ~ 68 ~ 71 ~ ~ ~]>"

the last two bars play the F#m row, which the key change raises to G#m: the song resolves

114const arpNotes  = stack(arp.mask("<1!62 0!2>"), fsm.mask(ending2))
115const bassRoots = stack(roots.mask("<1!62 0!2>"), "30".mask(ending2))

── parts ───────────────────────────────────────── sixteenth-note arpeggio: the whole song; its filter opens over the intro and in the choruses

119const arps = note(arpNotes.add(key)).s("sawtooth").decay(.14).sustain(0).lpf(arpLpf)
120  .delay(.3).delaytime(.5).delayfeedback(.35).room(.3).gain(arpGain)

bright saw lead: choruses and instrumentals

123const leadPart = note(lead.add(key)).s("supersaw").detune(.3).lpf(3800).attack(.01).decay(.3).sustain(.4)
124  .delay(.3).delaytime(.5).delayfeedback(.4).room(.5).gain(leadGain).mask(leadOn)

pulsing eighth-note bass, with a sine sub underneath; closes down through the outro

127const bass = stack(
128  note(bassRoots.add(key)).struct("x*8").s("sawtooth").lpf(bassLpf).decay(.2).sustain(.3).gain(.35).mask(bassOn),
129  note(bassRoots.add(key)).struct("x*8").s("sine").gain(subGain).mask(bassOn)
130)

The gate: every sound with reverb on the drums' orbit goes through the same impulse, 250 ms of flat noise cut dead (samples/ssgate), so the orbit's reverb is never regenerated between two impulses.

135const gated = (p, amount) => p.room(amount).ir('ssgate')
136const kickBeat = "x ~ ~ ~ ~ ~ ~ ~ x ~ x ~ ~ ~ ~ ~"
137const toms = gated(s("~ ~ ~ ~ ~ ~ ~ ~ 808ht 808ht 808mt 808mt 808lt 808lt 808lt 808lt"), .4).gain(.6).orbit('drums')
139const kit = stack(
140  s("bd:3").struct(kickBeat).gain(.4).mask(drums),
141  s("bd:3").struct(kickBeat).gain(.4).lpf(outroLpf).mask(pulse),
142  // gated snare: the snare through the gate, and a noise burst that holds 250 ms then cuts
143  gated(s("sn:3").struct("~ ~ ~ ~ x ~ ~ ~ ~ ~ ~ ~ x ~ ~ ~"), .6).gain(.85).mask(drums),
144  s("white").struct("[~@4 x@1.5 ~@6.5 x@1.5 ~@2.5]").attack(.001).sustain(1).release(.008).hpf(1800).gain(.3).mask(drums),
145  s("linnhats*8").gain(".3 .15").mask(hats),
146  s("hh27:1").gain(2).mask(crash),
147  // the chorus lift: a clap doubling the snare, and sixteenth hats
148  gated(s("~ cp ~ cp"), .5).gain(.55).mask(chorus),
149  s("linnhats*16").gain(".26 .12 .18 .12").mask(chorus),
150)

the vocoded SQL lines: the part Jev levels

153const vox = stack(
154  // verses: vocoder, one line per 2 bars, each sung on its chord
155  s("<ssselect ~ ssvibes ~ ssscan ~ sspool ~>").gain(1.1).room(.3).mask(verse1),
156  s("<ssorderby ~ ssforty ~ ssoptions ~ sstree ~>").gain(1.1).room(.3).mask(verse2),
157  // chorus: select star / where vibes are true / (the hook) / where vibes are true
158  s("<ssselect ~ ssvibes ~ ~ ~ ssvibese ~>").gain(1.15).room(.3).mask(chorus),
159)

the hook, hello there / general kenobi: always plays in a chorus, on the vocals' orbit

161const hook = s("<~ ~ ~ ~ sshellothere sskenobi ~ ~>").gain(1.15).room(.3).mask(chorus).orbit('vocals')

the end: the last gated snare, crash and noise tail. It always lands, at its own level.

164const ending = stack(
165  gated(s("sn:3"), .9).gain(.9).mask(final),
166  s("hh27:1").gain(1.6).mask(final),
167  s("white").attack(.001).sustain(1).release(.008).hpf(1800).gain(.35).struct("[x ~@9]").mask(final),
168)
170const 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 last 16 bars are up a tone, so "keyUp" and "outro" stay vocal-free. times: the key change plays twice at most (SPEC.md, "Loops end").

177const form = walk(section, {
178  max: 16,
179  sections: {
180    intro: { at: 0, next: ['verse1'] },
181    verse1: { at: 8, next: ['chorus1'] },
182    chorus1: { at: 16, next: ['instrumental', 'verse2'] },
183    instrumental: { at: 24, next: ['verse2', 'chorus1'] },
184    verse2: { at: 32, next: ['chorus2'] },
185    chorus2: { at: 40, next: ['keyUp', 'instrumental'] },
186    keyUp: { at: 48, next: ['outro', 'keyUp'], times: 2 },
187    outro: { at: 56, next: [] },
188  },
189})

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

193const parts = form.arrange(once(stack(
194  part('arp', arps),
195  part('lead', leadPart),
196  part('bass', bass),
197  part('drums', kit),
198  part('vocals', vox),
199  hook,
200)))

── into the next section: over the last bar of each, the answer for the section being entered ── Past the last section there is no next one, so nothing plays there. The toms are written only here: as written, they roll into the choruses and the key change, wherever the path goes.

206const into = dj.into.choice.early(1)
207const next = section.choice.early(1)
208const rollsInto = ['chorus1', 'chorus2', 'keyUp']
209const transition = into.pick({
210  asWritten: toms.mask(next.fmap((s) => rollsInto.includes(s))),
211  fill: toms,
212  // one note across the bar, its level and high-pass both rising: a swell
213  riser: s("white").attack(2.4).sustain(1).release(.05).hpf(400).hpenv(4.3).hpattack(2.6).hpsustain(1).gain(.6),
214  dropout: silence,
215}).mask("<0!7 1>")

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

218const lastBeat = "<0!7 [0 0 0 1]>"
219const gap = stack(
220  "<1!7 [1 1 1 0]>",
221  into.fmap((c) => c !== 'dropout').mask(lastBeat),
222  section.choice.fmap((s) => s === 'outro').mask(lastBeat),
223)

Energy scales the band; the ending always lands at its own level. One postgain for the whole mix keeps the master's peak under -1 dBFS (SPEC.md, "Mix").

227stack(
228  stack(parts.mask(gap), transition).mul(velocity(dj.energy.score.div(3).range(.55, 1))),
229  form.arrange(once(ending)),
230).mul(postgain(.47))