song.jsannotatedsong.jssource235 lines · 11.9 KB · raw
1// TOO CHEAP TO METER — filter house, 124bpm, 64 bars as written; Jev arranges up to 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(124/60/4)
4await samples('github:tidalcycles/dirt-samples')
5await samples('jev-samples/strudel.json')
6
7// ── Jev: two of them, in TypeSafe's own shape (SPEC.md, "Jev in the booth") ──
8// formJev picks the next section; djJev, asked once that is decided (questions
9// in one request cannot see each other), says how much of each part plays
10// it, how the song moves into it, and how hard. Every 8 bars. The form's rules (walk, at the bottom) narrow the
11// 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}
25
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})
41
42// how much of each part the section decidedThisSection has: out, back or full
43// (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})
71
72// a little chance in the form: the section is drawn from Jev's probabilities
73const { section } = formJev.every(8, { sample: 0.7 })
74// Jev is asked only about what a section has (SPEC.md, "The parts" and
75// "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})
101
102// A part at Jev's level: silent when out, half velocity when back. Each
103// part has its own orbit, named for its question, so the meter hears it
104// 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))))
108
109// bars: 1-8 intro | 9-16 verse 1 | 17-24 hook | 25-40 DROP | 41-48 verse 2
110//       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>"
123
124// 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 ~"
128
129// ── parts ─────────────────────────────────────────
130// chord stabs: the low-pass opens over each 8-bar phrase (saw.slow(8)
131// 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)
139
140// the ending: the filter closes over the outro and its four bars of kick,
141// 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)
149
150// four on the floor, and the claps, hats, crash and riser around it
151const fourOnFloor = s("bd:3*4").gain(.6).mask(kick)
152// 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)
164
165// 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)
171
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
183
184// ── the form, as rules in code: which sections may follow which ──
185// `at` is where each starts as written; an empty `next` ends the song. The
186// 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})
200
201// ── the song: Jev's section, parts and energy, composed ──
202// Each section plays its own bars, each part at the level Jev chose; the
203// energy scales every part's velocity together, so the mix keeps its
204// 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)))
212
213// ── into the next section: over the last bar of each, the next one's answer ──
214// Past the last section there is no next one, so nothing plays there. The
215// 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')
223// dropout: the parts stop for the section's last beat. The outro has no
224// 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)
230
231// energy scales velocity (SPEC.md: about -5, -3, -1.4 and 0 dB); the master
232// 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))