song.jsannotatedsong.jssource270 lines · 13.4 KB · raw

YOU'VE GOT JEV — 2-step garage, 132bpm, 64 bars as written; Jev arranges up to 24 four-bar sections. Spec: SPEC.md Start from STOPPED so the song begins at bar 1 (re-running it mid-play keeps the cycle count).

3setcps(132/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 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.

12const song = {
13  title: 'YOU’VE GOT JEV',
14  what: '2-step garage about Jev routing support mail: AOL dials in, a ticket arrives, and Jev routes it in a blink with no human in the loop',
15  tempo: '132 bpm, 4/4, swung, 4 bars a section',
16  key: 'C minor',
17  chords: 'Cm9 – Fm9 – Bbmaj7 – Abmaj7, two bars each (an 8-bar cycle), under the stabs and the sub',
18  lyrics: {
19    verse1a: 'New ticket in / Billing, bug, or refund',
20    verse1b: 'Choice: billing / Point nine three',
21    hook1: 'You’ve got mail / You’ve got Jev / You’ve got mail / You’ve got Jev',
22    verse2a: 'No human in the loop / Routed in a blink',
23    verse2b: 'Nobody had to read it / You’ve got Jev',
24    hook2: 'You’ve got mail / You’ve got Jev / You’ve got mail / You’ve got Jev',
25    outro: 'Goodbye',
26  },
27}
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      modem: 'a dial-up modem alone: the dial tone, the number dialed, the answer tone',
34      handshake: 'the modem handshake screech chopped into a rhythm, hats and muffled stabs coming in',
35      verse1a: 'verse 1, first half: 2-step drums, swung stabs, the warbly sub, vocoded lines',
36      verse1b: 'verse 1, second half',
37      hook1: 'the hook: AOL asks and the vocoder answers',
38      drop1a: 'drop 1: instrumental, open hi-hats, a crash',
39      drop1b: 'drop 1 continues',
40      drop1c: 'drop 1, last four bars',
41      verse2a: 'verse 2, first half',
42      verse2b: 'verse 2, second half',
43      hook2: 'the hook again, into the big drop',
44      drop2a: 'drop 2, the peak: the screech chops return over the full groove, a crash',
45      drop2b: 'drop 2 continues',
46      drop2c: 'drop 2 continues',
47      drop2d: 'drop 2, last four bars',
48      outro: 'a last hit and a ringing chord, then AOL says goodbye: the end',
49    }),
50  },
51})

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    modem: level('the modem (a dial-up modem dialing in alone for the first four bars, the handshake screech chopped into a rhythm in the next four, and the chops again over drop 2)'),
63    drums: level('the drums (the 2-step kick and snare from verse 1 on, swung hi-hats from bar 5, open hats on the off-beats and crashes in the drops)'),
64    stabs: level('the stabs (short swung organ-style chord stabs, muffled at the end of the handshake, then from verse 1 to the outro)'),
65    sub: level('the sub (the warbly sub bass on the chord roots, the floor from verse 1 to the outro)'),
66    vocals: level('the vocals (the vocoded ticket lines in the verses, and AOL asking and the vocoder answering in the hooks; the drops are instrumental as written)'),
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 snare and clap roll in sixteenths, swelling into the downbeat, over the last half bar',
70      riser: 'the modem handshake screech, chopped into sixteenths, its filter sweeping up over the last half bar',
71      dropout: 'everything stops for the last beat, a gap before the section hits',
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: under full',
76      'driving: nearly full',
77      'all out: full volume',
78    ]),
79  },
80})

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

83const { section } = formJev.every(4, { sample: 0.7 })
84const dj = djJev.every(4, {
85  after: { section },
86  forget: ['into'],
87  // the intro builds from the modem alone, so the handshake always cuts in
88  allow: { into: ({ section: next }) => (next === 'handshake' ? ['cut'] : ['cut', 'fill', 'riser', 'dropout']) },
89  fallback: { modem: 2, drums: 2, stabs: 2, sub: 2, vocals: 2, into: 'cut', energy: 3 },
90})

Jev's most probable level. A score's probabilities are keyed by level, "0", "1", … (docs.typesafe.ai/primitives/score); a fallback has none, so it plays the level its score names.

95const mostLikely = (p, score) => {
96  const levels = Object.entries(p ?? {})
97  return levels.length ? Number(levels.reduce((a, b) => (b[1] > a[1] ? b : a))[0]) : Math.round(score)
98}
99const fromAnswer = (answer, f) => answer.score.fmap((score) => (p) => f(p ?? {}, score)).appLeft(answer.probabilities)

A part's gain: silent when "out" is Jev's most probable level; otherwise Jev's probability-weighted mix of back (0.5) and full (1).

102const partGain = (p, score) => {
103  const top = mostLikely(p, score)
104  if (top === 0) return 0
105  const back = p['1'] ?? 0, full = p['2'] ?? 0
106  return back + full > 0 ? (back * .5 + full) / (back + full) : top === 1 ? .5 : 1
107}

A part at Jev's level. Each part has its own orbit, named for its question, so the meter hears it alone (measuredSound).

110const part = (name, pat) => {
111  const gain = fromAnswer(dj[name], partGain)
112  return pat.orbit(name).mask(gain.gt(0)).mul(velocity(gain))
113}

bars: 1-4 modem | 5-8 chopped handshake | 9-16 verse 1 | 17-20 hook | 21-32 DROP 1 33-40 verse 2 | 41-44 hook | 45-60 DROP 2 | 61-64 outro (each mask = 64 steps)

117const modemOn = "<1 0!63>"
118const chopIn  = "<0!4 1!4 0!56>"
119const stabIn  = "<0!6 1!2 0!56>"
120const groove  = "<0!8 1!52 0!4>"
121const verse1  = "<0!8 1!8 0!48>"
122const verse2  = "<0!32 1!8 0!24>"
123const hooks   = "<0!16 1!4 0!20 1!4 0!20>"
124const drops   = "<0!20 1!12 0!12 1!16 0!4>"
125const drop2   = "<0!44 1!16 0!4>"
126const crash   = "<0!20 1 0!23 1 0!19>"
127const lastHit = "<0!60 1 0!3>"
128const bye     = "<0!61 1 0!2>"

two bars per chord: Cm9 Cm9 Fm9 Fm9 Bbmaj7 Bbmaj7 Abmaj7 Abmaj7, voiced close so the stabs move by step (each keeps the pitch classes its vocoded lines were rendered on)

133const cm9    = "[c3,eb3,g3,bb3,d4]"
134const fm9    = "[f3,g3,ab3,c4,eb4]"
135const bbmaj7 = "[bb2,f3,a3,d4]"
136const abmaj7 = "[ab2,eb3,g3,c4]"
137const chords = cat(cm9, cm9, fm9, fm9, bbmaj7, bbmaj7, abmaj7, abmaj7)   // a chord a bar
138const roots  = "<36!2 29!2 34!2 32!2>"   // C2 F1 Bb1 Ab1, as MIDI numbers
139const chops  = "<[0 ~ 4 ~ 8 2 ~ 12] [~ 6 ~ 14 0 ~ 10 3]>"

── parts ─────────────────────────────────────────

142const modem = stack(
143  // bars 1-4: the modem dialing in, alone (first 7.27 s of 7.45)
144  s("modem").end(.976).gain(.9).mask(modemOn),
145  // the handshake screech, chopped into a fixed rhythm
146  s("screech").slice(16, chops).hpf(300).gain(.55).mask(chopIn),
147  // drop 2: the chops return at the intro's level
148  s("screech").slice(16, chops).hpf(300).gain(.55).pan(.3).mask(drop2)
149)

drums: 2-step

152const drums = stack(
153  s("[bd:3,808bd:1]").struct("x ~ ~ ~ ~ ~ ~ ~ ~ ~ x ~ ~ ~ ~ ~").gain(.4).mask(groove),
154  s("[sn:3,cp]").struct("~ ~ ~ ~ x ~ ~ ~ ~ ~ ~ ~ x ~ ~ ~").room(.2).gain(.3).mask(groove),
155  s("sn:1").struct("~ ~ ~ ~ ~ ~ ~ x ~ ~ ~ ~ ~ ~ ~ x").swingBy(1/3, 8).gain(.1).mask(groove),
156  s("hh*16").gain(".32 .12 .22 .12").swingBy(1/3, 8).mask("<0!4 1!56 0!4>"),
157  // open hats on the off-beats in the drops
158  s("808oh").struct("~ x ~ x ~ x ~ x").gain(.2).mask(drops),
159  // hh27:1 is a crash; `cr` is a ride (songs/CLAUDE.md)
160  s("hh27:1").gain(1).mask(crash)
161)

organ-style stabs, short and swung

164const stabs = stack(
165  // the filter opens in the drops: 3000 in drop 1, 4000 in drop 2
166  note(chords).struct("~ ~ x ~ ~ x ~ ~ ~ x ~ ~ x ~ ~ ~").swingBy(1/3, 8)
167    .s("sawtooth").lpf("<1700!20 3000!12 1700!12 4000!16 1700!4>").decay(.16).sustain(0).room(.3).gain(.45).mask(groove),
168  note(chords).struct("~ ~ x ~ ~ x ~ ~ ~ x ~ ~ x ~ ~ ~").swingBy(1/3, 8)
169    .s("sawtooth").lpf(600).decay(.16).sustain(0).gain(.12).mask(stabIn)
170)

warbly sub: garage push rhythm, a slow vibrato a fifth of a semitone deep. subClipped is the same line with the note on step 10 cut to two steps, for a section's last bar when a dropout follows, so the sub stops for the gap.

175const subIn = (rhythm) => note(roots).struct(rhythm)
176  .s("sine").vib(.5).vibmod(.2).release(.02).gain(.7).mask(groove)
177const sub = subIn("x@6 x@4 x@6")
178const subClipped = subIn("x@6 x@4 x@2 ~@4")
180const vox = stack(
181  // verses: vocoder, one line per 2 bars, each sung on its chord
182  s("<newticket ~ billingbug ~ choicebilling ~ ninetythree ~>").gain(1.25).room(.2).mask(verse1),
183  s("<nohuman ~ routedblink ~ nobodyread ~ gotjevab ~>").gain("<1.25!4 1.5!2 1.25!2>").room(.2).mask(verse2),
184  // hook: AOL asks, the vocoder answers on the hook's chords
185  s("<mail gotjevcm mail gotjevfm>").gain(1.35).room(.25).mask(hooks)
186)

the end: the last hit, a ringing Cm9 over a C2 sub, and AOL's goodbye. It always plays.

189const ending = stack(
190  s("[bd:3,808bd:1]").room(.5).gain(.4).mask(lastHit),
191  s("hh27:1").room(.5).gain(1).mask(lastHit),
192  note(cm9).s("sawtooth").lpf(1400).decay(1.5).sustain(0).room(.6).gain(.45).mask(lastHit),
193  note("c2").s("sine").vib(.5).vibmod(.2).release(.3).gain(.7).mask(lastHit),
194  s("goodbye").gain(1.2).room(.5).mask(bye)
195)
197const 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. Nothing leads back into a hook, so each plays once; and every move goes to a section starting 4 bars off in the 8-bar chord cycle, so the two halves of the progression alternate. times: each drop's loop goes round once more at most (SPEC.md, "Loops end").

206const form = walk(section, {
207  max: 24,
208  sections: {
209    modem: { at: 0, next: ['handshake'] },
210    handshake: { at: 4, next: ['verse1a'] },
211    verse1a: { at: 8, next: ['verse1b'] },
212    verse1b: { at: 12, next: ['hook1'] },
213    hook1: { at: 16, next: ['drop1a', 'drop1c'] },
214    drop1a: { at: 20, next: ['drop1b'] },
215    drop1b: { at: 24, next: ['drop1c'], times: 2 },
216    drop1c: { at: 28, next: ['verse2a', 'drop1b'] },
217    verse2a: { at: 32, next: ['verse2b'] },
218    verse2b: { at: 36, next: ['hook2'] },
219    hook2: { at: 40, next: ['drop2a'] },
220    drop2a: { at: 44, next: ['drop2b'], times: 2 },
221    drop2b: { at: 48, next: ['drop2c', 'outro'], times: 2 },
222    drop2c: { at: 52, next: ['drop2d', 'drop2b'] },
223    drop2d: { at: 56, next: ['outro', 'drop2a'] },
224    outro: { at: 60, next: [] },
225  },
226})

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

230const into = dj.into.choice.early(1)
231const lastBar = "<0!3 1>"

fill and riser build over the second half of the bar, clear of the line on its downbeat

234const transition = into.pick({
235  cut: silence,
236  fill: s("~ [sn:3,cp]*8").room(.2).gain(saw.fast(2).range(.12, .34)).orbit('drums'),
237  riser: s("screech").slice(16, "~ [8 9 10 11 12 13 14 15]")
238    .hpf(saw.fast(2).range(300, 5000)).gain(saw.fast(2).range(.15, .45)).orbit('modem'),
239  dropout: silence,
240}).mask("<0!3 [0 1]>")

dropout: the parts stop for the section's last beat

242const lastBeat = "<0!3 [0 0 0 1]>"
243const gap = stack(
244  "<1!3 [1 1 1 0]>",
245  into.fmap((c) => c !== 'dropout').mask(lastBeat),
246)

the sub's last bar: clipped when a dropout follows, so it stops for the gap

248const dropoutBar = into.fmap((c) => c === 'dropout').mask(lastBar)
249const notDropoutBar = stack("<1!3 0>", into.fmap((c) => c !== 'dropout').mask(lastBar))

── the song: Jev's section, parts and energy, composed ── Each section plays its own bars, each part at the level Jev chose; the energy, Jev's most probable step, scales every part's velocity together, so the mix keeps its balance. The ending always plays.

255const parts = form.arrange(once(stack(
256  part('modem', modem),
257  part('drums', drums),
258  part('stabs', stabs),
259  part('vocals', vox),
260)))
261const subPart = stack(
262  form.arrange(once(part('sub', sub))).mask(notDropoutBar),
263  form.arrange(once(part('sub', subClipped))).mask(dropoutBar),
264)
265const energy = fromAnswer(dj.energy, mostLikely)

postgain: the whole mix's level, so it peaks under −1 dBFS (SPEC.md, Style: Mix)

268stack(parts.mask(gap), subPart.mask(gap), transition, form.arrange(once(ending)))
269  .mul(velocity(energy.div(3).range(.55, 1)))
270  .postgain(.52)