song.jsannotatedsong.jssource235 lines · 11.9 KB · raw

TOO CHEAP TO METER — filter house, 124bpm, 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(124/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: 'TOO CHEAP TO METER',
14  what: 'French filter house celebrating the price of Jev (input tokens 42 dollars per billion, output tokens free) and the jobs it is built for: routing, jailbreak flags, gating commands',
15  tempo: '124 bpm, 4/4, 8 bars a section',
16  key: 'A minor',
17  chords: 'Am7 Am7 D9 D9 Gmaj7 Gmaj7 Cmaj7 Cmaj7, two bars each, as filtered disco stabs',
18  lyrics: {
19    verse1: 'Input tokens, forty-two per billion / Output tokens: free / Seventy to five hundred milliseconds / Too cheap to meter',
20    hook1: 'Output tokens: free / It’s free real estate (four times, the last call Too cheap to meter)',
21    verse2: 'Route the ticket / Flag the jailbreak / Gate the sudo command / Too cheap to meter',
22    hook2: 'Output tokens: free / It’s free real estate (four times, the last call Too cheap to meter)',
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: 'filtered chord stabs and the kick; the filter opens',
31      verse1: 'verse 1: the vocoder sings the price list over claps, hats and the pumping bass',
32      hook1: 'the hook: the robot quotes the price and Tim Heidecker answers, with a white-noise riser into the drop',
33      drop: 'the drop, first half: the filter fully open, sixteenth hats, a crash on the downbeat',
34      drop2: 'the drop, second half: eight more bars of the open filter and sixteenth hats',
35      verse2: 'verse 2: the vocoder sings what Jev is for',
36      hook2: 'the hook again: output tokens free, free real estate',
37      outro: 'the filter closes over four more bars of kick, then the last stab rings: the end',
38    }),
39  },
40})

how much of each part the section decidedThisSection has: out, back or full (a choice, so allow below can leave out what a section cannot have)

44const level = (part) => choice('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', {
45  full: 'full: as written',
46  back: 'back: under the rest of the mix',
47  out: 'out: silent for the section',
48})
49const djJev = jev({
50  state: song,
51  questions: {
52    stabs: level('the filtered chord stabs (Am7 D9 Gmaj7 Cmaj7, the disco chords the song is built on: the filter sweeps open in the intro, both verses and both hooks, and stays open in the drop)'),
53    kick: level('the kick (four on the floor, the pulse of the whole song from the intro to the outro)'),
54    drums: level('the claps, hats, crash and white-noise riser (from verse 1 to the last hook, sixteenth hats in the drop, the riser into it)'),
55    bass: level('the bass (pumping octaves over a sine sub on the chord roots, from verse 1 to the last hook)'),
56    vocals: level('the vocals (the vocoder verse lines, and the hook where the robot quotes the price and Tim Heidecker answers)'),
57    into: choice('How should the song move into the next section (decidedThisSection)? It plays over the last bar of the section before it', {
58      cut: 'straight in, no transition',
59      fill: 'a clap roll, doubling up into the first beat',
60      riser: 'the white-noise riser sweeping up over the last bar',
61      dropout: 'everything stops for the last beat, a gap before the section hits',
62    }),
63    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', [
64      'held back: about 5 dB down',
65      'steady: about 3 dB down',
66      'driving: just under as written',
67      'all out: as written',
68    ]),
69  },
70})

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

73const { section } = formJev.every(8, { sample: 0.7 })

Jev is asked only about what a section has (SPEC.md, "The parts" and "Into the next section"); a single option is played without asking.

76const levels = ['full', 'back', 'out']
77const has = (sections) => (d) => (sections.includes(d.section) ? levels : ['full'])
78const intos = {
79  verse1: ['cut', 'fill'],
80  verse2: ['cut', 'fill'],
81  hook1: ['cut', 'fill', 'riser'],
82  hook2: ['cut', 'fill', 'riser'],
83  drop: ['cut', 'fill', 'dropout'],   // the drop's riser is written, at the end of hook 1
84  drop2: ['cut', 'fill'],
85  outro: ['cut', 'dropout'],
86}
87const dj = djJev.every(8, {
88  after: { section },
89  forget: ['into'],
90  allow: {
91    stabs: (d) => (d.section === 'outro' ? ['full'] : levels),
92    kick: (d) => (d.section === 'outro' ? ['full'] : levels),
93    drums: has(['verse1', 'hook1', 'drop', 'drop2', 'verse2', 'hook2']),
94    bass: has(['verse1', 'hook1', 'drop', 'drop2', 'verse2', 'hook2']),
95    // the words are always sung: in the verses and hooks back or full
96    vocals: (d) => (/^(verse|hook)/.test(d.section) ? ['full', 'back'] : ['full']),
97    into: (d) => intos[d.section] ?? ['cut', 'fill', 'riser', 'dropout'],
98  },
99  fallback: { stabs: 'full', kick: 'full', drums: 'full', bass: 'full', vocals: 'full', into: 'cut', energy: 3 },
100})

A part at Jev's level: silent when out, half velocity when back. Each part has its own orbit, named for its question, so the meter hears it alone (measuredSound).

105const part = (name, pat) =>
106  pat.orbit(name).mask(dj[name].choice.fmap((c) => c !== 'out'))
107    .mul(velocity(dj[name].choice.fmap((c) => (c === 'back' ? .5 : 1))))

bars: 1-8 intro | 9-16 verse 1 | 17-24 hook | 25-40 DROP | 41-48 verse 2 49-56 hook | 57-64 outro (each mask = 64 steps)

111const verse1  = "<0!8 1!8 0!48>"
112const verse2  = "<0!40 1!8 0!16>"
113const hooks   = "<0!16 1!8 0!24 1!8 0!8>"
114const drop    = "<0!24 1!16 0!24>"
115const swept   = "<1!24 0!16 1!16 0!8>"   // stabs under the 8-bar opening sweep
116const outro   = "<0!56 1!6 0!2>"
117const kick    = "<1!56 0!8>"            // the outro's kick is the ending's
118const outroKick = "<0!56 1!4 0!4>"
119const band    = "<0!8 1!48 0!8>"          // claps, hats, bass: verse to last hook
120const riser   = "<0!23 [0 1] 0!40>"     // after the last free real estate ends
121const crash   = "<0!24 1 0!39>"
122const lastHit = "<0!62 1 0>"

two bars per chord: Am7 Am7 D9 D9 Gmaj7 Gmaj7 Cmaj7 Cmaj7

125const chords = "<[a3,c4,e4,g4]!2 [d3,f#3,a3,c4,e4]!2 [g3,b3,d4,f#4]!2 [c4,e4,g4,b4]!2>"
126const roots  = "<33!2 38!2 31!2 36!2>"   // A1 D2 G1 C2, as MIDI numbers
127const chop   = "~ x ~ x ~ ~ x ~ ~ x ~ x ~ ~ x ~"

── parts ───────────────────────────────────────── chord stabs: the low-pass opens over each 8-bar phrase (saw.slow(8) restarts every 8 bars), then stays fully open in the drop

132const stabs = stack(
133  note(chords).struct(chop).s("supersaw").detune(.25).decay(.18).sustain(0)
134    .lpf(saw.range(300, 3200).slow(8)).lpq(6).room(.25).gain(.4).mask(swept),
135  // open, a touch louder: it has no resonant peak to lift it
136  note(chords).struct(chop).s("supersaw").detune(.25).decay(.18).sustain(0)
137    .lpf(5000).lpq(3).room(.25).gain(.44).mask(drop)
138)

the ending: the filter closes over the outro and its four bars of kick, and the last stab rings. It always plays: the outro's parts are not Jev's.

142const ending = stack(
143  note(chords).struct(chop).s("supersaw").detune(.25).decay(.18).sustain(0)
144    .lpf(saw.range(3200, 250).slow(8)).lpq(6).room(.3).gain(.4).mask(outro).orbit('stabs'),
145  note(chords).s("supersaw").detune(.25).decay(2.5).sustain(0)
146    .lpf(1800).room(.7).size(.9).gain(.5).mask(lastHit).orbit('stabs'),
147  s("bd:3*4").gain(.6).mask(outroKick).orbit('kick')
148)

four on the floor, and the claps, hats, crash and riser around it

151const fourOnFloor = s("bd:3*4").gain(.6).mask(kick)

the fill replaces the backbeat clap on the last beat of its bar

153const into = dj.into.choice.early(1)
154const lastBeat = "<0!7 [0 0 0 1]>"
155const noFillClap = stack("<1!7 [1 1 1 0]>", into.fmap((c) => c !== 'fill').mask(lastBeat))
156const drums = stack(
157  s("~ cp ~ cp").room(.15).gain(.7).mask(band).mask(noFillClap),
158  s("[~ 808oh]*4").gain(.28).mask(band),
159  s("hh*16").gain(".2 .08").mask(drop),
160  s("hh27:1").gain(1.5).mask(crash),   // hh27:1 is the crash; `cr` is a ride
161  s("white*16").decay(.06).sustain(0)
162    .hpf(saw.range(500, 9000)).gain(saw.range(.05, .3)).mask(riser)
163)

pumping octaves, off-beats louder, over a sine sub

166const bass = stack(
167  note("[0 12]*4".add(roots)).s("sawtooth").lpf(800).decay(.12).sustain(.3)
168    .gain(".3 .5").mask(band),
169  note(roots).struct("x x x x").s("sine").gain(.45).mask(band)
170)
172const vox = stack(
173  // verses: vocoder, one line per 2 bars, each sung on its chord
174  s("<tcinput ~ tcfree ~ tcseventy ~ tcmeter ~>").gain(1.1).room(.2).mask(verse1),
175  s("<tcroute ~ tcflag ~ tcgate ~ tcmeter ~>").gain(1.1).room(.2).mask(verse2),
176  // hook: the robot quotes the price, Tim Heidecker answers
177  s("<tcfreeam tcrealestate tcfree tcrealestate tcfreeg tcrealestate tcmeter tcrealestate>")
178    .gain(1).room(.2).mask(hooks)
179)
180
181const MASTER = .6                           // the master level: measured, SPEC.md "Style"
182const 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.

187const form = walk(section, {
188  max: 16,
189  sections: {
190    intro: { at: 0, next: ['verse1'] },
191    verse1: { at: 8, next: ['hook1'] },
192    hook1: { at: 16, next: ['drop'] },
193    drop: { at: 24, next: ['drop2'] },
194    drop2: { at: 32, next: ['verse2'] },   // every path sings verse 2 before the outro
195    verse2: { at: 40, next: ['hook2'] },
196    hook2: { at: 48, next: ['outro', 'drop', 'verse1'] },
197    outro: { at: 56, next: [] },
198  },
199})

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

205const parts = form.arrange(once(stack(
206  part('stabs', stabs),
207  part('kick', fourOnFloor),
208  part('drums', drums),
209  part('bass', bass),
210  part('vocals', vox),
211)))

── 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. The transitions have their own orbit: no part's level controls them.

216const transition = into.pick({
217  cut: silence,
218  fill: s("[~ cp] [~ cp] [cp cp] [cp cp cp cp]").room(.15).gain(saw.range(.4, .8)),
219  riser: s("white*16").decay(.06).sustain(0)
220    .hpf(saw.range(500, 9000)).gain(saw.range(.05, .3)),
221  dropout: silence,
222}).mask("<0!7 1>").orbit('into')

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

225const gap = stack(
226  "<1!7 [1 1 1 0]>",
227  into.fmap((c) => c !== 'dropout').mask(lastBeat),
228  section.choice.fmap((s) => s === 'outro').mask(lastBeat),
229)

energy scales velocity (SPEC.md: about -5, -3, -1.4 and 0 dB); the master level keeps the loudest pick under -1 dBFS (superdough has no limiter)

233stack(parts.mask(gap), transition, form.arrange(once(ending)))
234  .mul(velocity(dj.energy.score.div(3).range(.55, 1)))
235  .mul(postgain(MASTER))