song.jsannotatedsong.jssource342 lines · 21.6 KB · raw
1// SNEK HISS — house, 124bpm, 72 bars as written; Jev arranges up to 10 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
11// (walk, at the bottom) narrow the sections Jev may pick and end the song;
12// without Jev it plays as written.
13const song = {
14  title: 'SNEK HISS',
15  what: 'house about the almost metallic hiss a listener hears between 3 and 8 kHz in AI-made music, like a snake. The song blames a codec picking tokens one at a time, hunts the snake band by band, finds it, and fixes it as TQCodec does (a 2026 music streaming codec that found its own quantizer noisy in that band); then it takes the other road, WanSong (a 2026 song generator on continuous tokens, with no codebook to pick from), until the last chorus is clean. That the band is why generated music hisses is the song\'s claim, not the papers\'',
16  tempo: '124 bpm, 8 bars a section',
17  key: 'A minor',
18  chords: 'Am Am F F C C G G, two bars each, a plucked sixteenth-note arpeggio; the song ends on Am',
19  sound: 'until the fix the band is bit-crushed (the codec): rough through chorus 1, half fixed in verse 2, clean from the flow section on. The snake never is: it is band-limited noise, 3 to 8 kHz, with ringing edges',
20  lyrics: {
21    verse1: 'One token at a time / Pick a code from the book / Every pick, a little off / And something starts to hiss',
22    hunt: 'Check it, band by band / Three to eight kilohertz / (the band drops out, the snake alone) That\'s a snake',
23    chorus1: 'Three to eight kilohertz / Almost metallic / Like a snake\'s hiss / (the snake answers)',
24    verse2: 'Freeze the codebook / A linear projection / Keep the mid band detail / Residual sim V Q',
25    flow: 'Continuous tokens / Flow matching / From noise, to a song / Five minutes long',
26    stems: 'Two stems, one run (sung alone) / (the backing alone) / The vocals, and the backing',
27    chorus2: 'Three to eight kilohertz / Nothing metallic / No snake',
28  },
29}
30
31const formJev = jev({
32  state: song,
33  questions: {
34    section: choice('Pick the section of the song that plays next, from those allowed. The story runs one way: the hiss, the hunt, the fix, the clean chorus. The outro ends the song: choose it once the clean chorus has made its point', {
35      intro: 'the crushed arpeggio alone, the snake creeping in, then the kick',
36      verse1: 'verse 1: the codec picks tokens one at a time over the crushed band, the snake under everything',
37      hunt: 'the hunt, in two halves. First: Check it, band by band / Three to eight kilohertz, while a band-pass steps up through the band, one band a bar. Then the band stops: only the kick and the snake, alone and loud, and That\'s a snake',
38      chorus1: 'chorus 1, still crushed: Three to eight kilohertz / Almost metallic / Like a snake\'s hiss, with the lead and sixteenth hats, and the snake answering with one long swell',
39      verse2: 'verse 2, the fix (TQCodec): Freeze the codebook / A linear projection. The crushing halves and the snake fades bar by bar',
40      flow: 'the other road (WanSong): Continuous tokens / Flow matching, with no codebook to pick from. Noise fades away under a pad that fades in; nothing is crushed from here on',
41      stems: 'two stems: the vocal sung alone for a bar, then the backing alone, then both, then the band',
42      chorus2: 'the clean chorus: Three to eight kilohertz / Nothing metallic / No snake, the lead up an octave and carrying the bars where the snake used to answer',
43      outro: 'the arpeggio and bass closing down to a last A minor chord, then one small hiss, alone: the end',
44    }),
45  },
46})
47
48// how much of each part the section decidedThisSection has: out, back or full
49const 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. Where the song as written already stops a part for some bars, that happens by itself: the level is for the bars where the part plays', [
50  'out: silent for the section',
51  'back: under the rest of the mix',
52  'full: as written',
53])
54const djJev = jev({
55  state: song,
56  questions: {
57    keys: level('the keys (the plucked sixteenth-note arpeggio on the chords, the song\'s bed, in every section; in the flow section also the noise fading out and the pad fading in, and the pad stays under the stems and the clean chorus. The intro\'s first four bars, the arpeggio alone, are not part of this: they always play)'),
58    lead: level('the lead (the square-wave melody, only in the choruses, answering the sung lines; an octave up in the clean chorus)'),
59    bass: level('the bass (offbeat saw bass on the chord roots with a sine sub, from verse 1 on; silent with the rest of the band in the hunt\'s second half; out for the first half of the flow section and while the vocal is sung alone)'),
60    drums: level('the drums (four-on-the-floor kick from bar 5; clap on 2 and 4 and offbeat hats in the verses and choruses, and in the stems once the backing is in; sixteenth hats and a crash added in the choruses; out for the first half of the flow section and while the vocal is sung alone; in the hunt the kick keeps going alone under the snake)'),
61    // the sung lines are never out, so their question has no such level (SPEC.md, "The parts")
62    vocals: score('How loud should the vocoded lines be in the next section (decidedThisSection)? They are in every section but the intro and the outro and they carry the story, so they are never out: pull them back to let the band speak, or bring them in full. Two lines are not part of this and always play in full: That\'s a snake, in the hunt, and Two stems, one run, sung alone at the top of the stems section', [
63      'back: under the rest of the mix',
64      'full: as written',
65    ]),
66    snake: level('the snake under the band (the hiss itself: noise between 3 and 8 kHz with ringing, metallic edges, breathing in swells. It creeps in over the intro, sits under verse 1, the first half of the hunt and chorus 1\'s sung lines, and fades out across verse 2; it is gone after that. Where the snake is all a passage has, it is not part of this and always plays: alone in the hunt\'s second half, and its answer in chorus 1\'s last two bars)'),
67    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', {
68      asWritten: 'as written: a noise riser into chorus 1, a pad swelling up into the clean chorus, straight in to any other section',
69      fill: '808 toms rolling down across the second half of the bar',
70      riser: 'white noise swelling up across the whole bar, its high-pass rising',
71      dropout: 'everything stops for the last beat, held sounds too: a gap before the section hits',
72    }),
73    energy: score('How hard should the band play the section being entered (decidedThisSection)? It scales the whole band\'s velocity for that section. Judge that section by its place in the story: the choruses are the payoffs, and the clean chorus is the peak of the song. Then shape it by how the song has measured so far and how the listener reacted', [
74      'held back, 55% velocity: for a section that waits, like the intro, or closes down, like the outro',
75      'steady, 70%: for a verse, telling the story under the sung lines',
76      'driving, 85%: for a section that builds towards a payoff, like the hunt, the flow section and the stems',
77      'all out, full velocity as written: for a payoff, the choruses',
78    ]),
79  },
80})
81
82// a little chance in the form: the section is drawn from Jev's probabilities
83const { section } = formJev.every(8, { sample: 0.7 })
84// The transitions Jev may pick (SPEC.md, "The transitions"): none at the
85// opening, where nothing is playing yet; no noise riser from the flow section
86// on, nor into chorus 1, where as written already is one.
87const noRiserInto = ['chorus1', 'flow', 'stems', 'chorus2', 'outro']
88const dj = djJev.every(8, {
89  after: { section },
90  forget: ['into'],
91  allow: {
92    into: ({ section: entering, playingNow }) =>
93      !playingNow ? ['asWritten']
94        : noRiserInto.includes(entering) ? ['asWritten', 'fill', 'dropout']
95        : ['asWritten', 'fill', 'riser', 'dropout'],
96  },
97  fallback: { keys: 2, lead: 2, bass: 2, drums: 2, vocals: 1, snake: 2, into: 'asWritten', energy: 3 },
98})
99
100// A part at Jev's level: silent when out, otherwise its velocity scaled by
101// the level. Each part has its own orbit, named for its question, so the
102// meter hears it alone (measuredSound).
103const part = (name, pat) =>
104  pat.orbit(name).mask(dj[name].score.round().gt(0)).mul(velocity(dj[name].score.round().div(2)))
105// the sung lines: back (half velocity) or full, never out
106const sung = (pat) => pat.orbit('vocals').mul(velocity(dj.vocals.score.round().add(1).div(2)))
107
108// A dropout stops held sounds too: on a section's last bar, when the song
109// drops out into the next section, a held note plays its cut form, which ends
110// at the last beat. Only parts whose sections always have a next one use
111// it (the snake, the pad): past the last section there is no answer to read.
112const dropsOut = dj.into.choice.early(1).fmap((c) => c === 'dropout')
113const stopping = (held, cut) => stack(
114  held.mask("<1!7 0>"),
115  held.mask("<0!7 1>").mask(dropsOut.fmap((d) => !d)),
116  cut.mask("<0!7 1>").mask(dropsOut),
117)
118
119// bars: 1-8 intro | 9-16 verse 1 | 17-24 hunt | 25-32 chorus 1 | 33-40 verse 2
120//       41-48 flow | 49-56 stems | 57-64 chorus 2 | 65-72 outro   (each mask = 72 steps)
121const inVerse1  = "<0!8 1!8 0!56>"
122const inHunt    = "<0!16 1!8 0!48>"
123const inChorus1 = "<0!24 1!8 0!40>"
124const inVerse2  = "<0!32 1!8 0!32>"
125const inFlow    = "<0!40 1!8 0!24>"
126const inStems   = "<0!48 1!8 0!16>"
127const inChorus2 = "<0!56 1!8 0!8>"
128const choruses  = "<0!24 1!8 0!24 1!8 0!8>"
129// the choruses are played harder than everything else: each measures at least
130// 2 dB over the verse before it (SPEC.md, "Mix")
131const push      = "<.78!24 1!8 .78!24 1!8 .78!8>"
132
133// ── the codec: what the band goes through until the fix ──
134// rough (crushed, sample rate reduced) through chorus 1; the hunt's first
135// four bars also step a band-pass up through the band, one band a bar, and
136// its last four are silent (the snake alone); half fixed in verse 2; clean
137// from the flow section on.
138const rough = "<1!16 0!8 1!8 0!40>"
139const scan  = "<0!16 1!4 0!52>"
140const half  = "<0!32 1!8 0!32>"
141const clean = "<0!40 1!32>"
142const codec = (p) => stack(
143  p.crush(5).coarse(4).mask(rough),
144  p.crush(5).coarse(4).bpf("<250 700 1600 3200>").bpq(1.5).mask(scan),
145  p.crush(8).coarse(2).mask(half),
146  p.mask(clean),
147)
148
149// two bars per chord: Am Am F F C C G G, as MIDI numbers
150const arp = "<[57 60 64 69 64 60 57 60]*2!2 [53 57 60 65 60 57 53 57]*2!2 [48 52 55 60 55 52 48 52]*2!2 [55 59 62 67 62 59 55 59]*2!2>"
151const pad = "<[57,60,64]!2 [53,57,60]!2 [55,60,64]!2 [55,59,62]!2>"
152const roots = "<33!2 29!2 36!2 31!2>"   // A1 F1 C2 G1
153const subs  = "<33!2 41!2 36!2 31!2>"   // the sine under them: its F an octave up (F1 is 43.7 Hz, under what small speakers play)
154// the lead answers each sung line: a pickup at the end of the line's bar, the phrase in the next
155// a melody, not the chord again: stepwise, each phrase ending somewhere new.
156// The clean chorus has a last phrase of its own, a held A (an octave up there).
157const lead1 = "<[~!6 72 74] [76 ~ 74 72 ~ 74 76 ~] [~!6 74 76] [77 ~ 76 74 ~ 72 ~ 76] [~!6 76 77] [79 ~ 77 76 ~ 74 72 ~] [~!6 71 72] [74@3 72 71 ~ 69 ~]>"
158const lead2 = "<[~!6 72 74] [76 ~ 74 72 ~ 74 76 ~] [~!6 74 76] [77 ~ 76 74 ~ 72 ~ 76] [~!6 76 77] [79 ~ 77 76 ~ 74 72 ~] [~!6 64 67] [69@4 67 64 ~ ~]>"
159const lead = stack(lead1.mask("<1!56 0!16>"), lead2.mask("<0!56 1!16>"))
160
161// In the stems section only bar 49 is the vocal alone: the backing comes in on bar 50.
162const keysOn  = "<0!4 1!44 0 1!21 0!2>"                 // from bar 5 (bars 1-4 always play, below); ends before the last chord
163const arpGain = "<.4!8 .35!8 .4!8 .45!8 .35!8 .08 .12 .18 .25 .32 .4 .45 .5 .45!8 .5!8 .45 .4 .35 .3 .25 .2 0 0>"
164const arpLpf  = "<600 800 1000 1300 1600 1900 2200 2500 2200!16 3000!8 2500!8 4000!16 5000!8 4000 3200 2400 1800 1200 800 500 500>"
165const padOn   = "<0!40 1!8 0 1!15 0!8>"                 // the flow, the stems once the backing is in, chorus 2
166const padGain = "<0!40 .04 .08 .12 .16 .2 .24 .26 .28 .22!24>"
167const leadOn  = "<0!24 1!6 0!26 1!8 0!8>"               // chorus 1 leaves its last two bars to the snake
168const leadUp  = "<0!56 12!16>"                          // the clean chorus is an octave up
169const bassOn  = "<0!8 1!32 0!4 1!4 0 1!21 0!2>"
170const drumsOn = "<0!4 1!36 0!4 1!4 0 1!21 0!2>"
171const clapOn  = "<0!8 1!8 0!8 1!16 0!9 1!15 0!8>"       // verses, choruses, the stems once the backing is in
172const crash   = "<0!24 1 0!31 1 0!15>"                  // where each chorus lands
173// the snake pulls back in the hunt's last bar (.8), so chorus 1 lands over it
174const snakeGain = "<0 0 .2 .3 .4 .5 .6 .7 .7!8 .8!4 1.6!3 .8 .7!6 1.5!2 .6 .5 .4 .3 .2 .15 .1 .05 0!32>"
175const snakeUnder = "<0!2 1!18 0!4 1!6 0!2 1!8 0!32>"    // under the band: bars 3-20, 25-30, 33-40
176const snakeAlone = "<0!20 1!4 0!48>"                    // the hunt's second half
177const snakeAnswers = "<0!30 1!2 0!40>"                  // chorus 1's last two bars
178const noiseGain = "<0!40 .5 .42 .34 .26 .18 .12 .07 .03 0!24>"
179const noiseLpf  = "<12000!40 12000 9000 7000 5000 3500 2500 1800 1200 12000!24>"
180
181// ── parts ─────────────────────────────────────────
182// The echoes are a dotted eighth at the song's tempo: superdough's default
183// (delaysync 3/16). A delaytime is seconds, which fits one tempo only.
184const arpLine = codec(note(arp).s("sawtooth").decay(.12).sustain(0).lpf(arpLpf)
185  .delay(.25).delayfeedback(.3).room(.25).gain(arpGain).mul(gain(1.4)))
186const padNote = note(pad).s("supersaw").detune(.2).attack(.4).lpf(2400).room(.4).gain(padGain)
187const noise = s("white").attack(.05).sustain(1).lpf(noiseLpf).gain(noiseGain)
188// the last two bars play nothing here: the ending has the last chord
189const keys = stack(
190  arpLine.mask(keysOn),
191  // the flow: noise to song. Full-band noise closing down and fading, a pad fading in
192  stopping(noise.release(.1), noise.clip(.75).release(.05)).mask(inFlow),
193  stopping(padNote.release(.4), padNote.clip(.75).release(.05)).mask(padOn),
194)
195// bars 1-4, the arpeggio alone: all the song has there, so it always plays, on the keys' orbit
196const opening = arpLine.mask("<1!4 0!68>").orbit('keys')
197
198const leadPart = codec(note(lead.add(leadUp)).s("square").lpf(3200).attack(.005).decay(.2).sustain(.3)
199  .delay(.3).delayfeedback(.35).room(.4).gain(1).mask(leadOn))
200
201// offbeat saw bass with a sine sub under it
202const bass = codec(stack(
203  note(roots).struct("~ x ~ x ~ x ~ x").s("sawtooth").lpf(500).decay(.18).sustain(.2).gain(.5),
204  note(subs).struct("~ x ~ x ~ x ~ x").s("sine").decay(.2).sustain(.4).gain(.6),
205).mask(bassOn))
206
207const kit = stack(
208  codec(stack(
209    s("bd:3*4").gain(.6).mask(drumsOn),
210    s("~ cp ~ cp").room(.2).gain(.55).mask(clapOn),
211    // one hat on each offbeat eighth; the choruses add sixteenths around them
212    s("[~ linnhats]*4").gain(.3).mask(drumsOn),
213    s("linnhats*16").gain("[.2 .1 .14 .1]*4").mask(choruses),
214    s("hh27:1").gain(1.8).mask(crash),
215  )),
216  // the hunt's last four bars: the kick alone under the snake, outside the codec's silence
217  s("bd:3*4").gain(.6).mask(snakeAlone),
218)
219
220// the vocoded lines, one per 2 bars, each rendered on its chord and levelled
221// to the same loudness. Until the fix they are crushed too, lightly, so every
222// word still reads.
223const line = (names, where) => s(names).gain(1.95).room(.2).mask(where)
224const vox = stack(
225  line("<shtoken ~ shcode ~ shoff ~ shhiss ~>", inVerse1).crush(7),
226  line("<shband ~ shkhzf ~ ~ ~ ~ ~>", inHunt).crush(7),
227  line("<shkhza ~ shmetal ~ shlike ~ ~ ~>", inChorus1).crush(7),
228  line("<shfreeze ~ shlinear ~ shmid ~ shsimvq ~>", inVerse2).crush(9),
229  line("<shcont ~ shflow ~ shnoise ~ shfive ~>", inFlow),
230  line("<~ ~ ~ ~ shvb ~ ~ ~>", inStems),
231  line("<shkhza ~ shnometal ~ shnosnake ~ ~ ~>", inChorus2),
232)
233
234// Two lines always play in full, on the vocals' orbit: the hunt's punchline,
235// over the snake alone, and the stem sung alone on bar 49.
236const found = line("<~ ~ ~ ~ shsnake ~ ~ ~>", inHunt).crush(7).orbit('vocals')
237const alone = line("<shstems ~ ~ ~ ~ ~ ~ ~>", inStems).orbit('vocals')
238
239// the snake: noise held between 3 and 8 kHz, both edges resonant so they ring
240// (the metal), swelling once a bar; and a run of inharmonic tones inside the
241// band (a codec's warble). It never goes through the codec: it is what the
242// codec leaves behind.
243const hiss = (p) => p.hpf(3000).hpq(8).lpf(8000).lpq(8)
244const breath = (shape, release) => hiss(s("white").struct(shape)).sustain(1).release(release).gain(.5)
245const swell = stopping(breath("x@7 ~", .25).attack(.9), breath("x@6 ~@2", .05).attack(.9))
246const warble = freq("[3170 5870 4410 7350 3730 6610 4990 7910]*4").s("sine").decay(.03).sustain(0).pan(sine.fast(3)).gain(.1)
247// chorus 1's answer: one swell, a single note across both bars. It starts a
248// bar before the section's last, so it reads the next section's transition
249// two bars ahead; on a dropout it ends at the last beat.
250const dropsOutAfter = dj.into.choice.early(2).fmap((c) => c === 'dropout')
251const answer = stack(
252  breath("x@15 ~", .25).slow(2).attack(2.2).mask(dropsOutAfter.fmap((d) => !d)),
253  breath("x@14 ~@2", .05).slow(2).attack(2.2).mask(dropsOutAfter),
254)
255// under the band the snake is a part, at Jev's level
256const snake = stack(swell, warble).mul(gain(snakeGain)).mask(snakeUnder)
257// where the snake is all a passage has, it always plays, on the snake's orbit
258const snakeSolo = stack(
259  stack(swell, warble).mask(snakeAlone),
260  stack(answer, warble).mask(snakeAnswers),
261).mul(gain(snakeGain)).orbit('snake')
262
263// the end: the last chord on bar 71, gone by the end of its bar, then one
264// small hiss alone on bar 72. It always lands, at its own level.
265const ending = stack(
266  note("[33,45,57,60,64,69]").s("sawtooth").lpf(2200).attack(.005).decay(1.9).sustain(0).room(.5).gain(.3).mask("<0!70 1 0>"),
267  s("bd:3").gain(.8).mask("<0!70 1 0>"),
268  hiss(s("white").struct("~ x@2 ~")).attack(.25).sustain(1).release(.2).gain(.45).mask("<0!71 1>"),
269).orbit('ending')
270
271const once = (p) => p.mask("<1!72 0!928>")   // play once, then silence
272
273// ── the form, as rules in code: which sections may follow which ──
274// `at` is where each starts as written; an empty `next` ends the song. The
275// written order is always allowed, and is what plays without Jev. Nothing
276// after the fix leads back before it (SPEC.md, "The form").
277const form = walk(section, {
278  max: 10,
279  sections: {
280    intro: { at: 0, next: ['verse1'] },
281    verse1: { at: 8, next: ['hunt'] },
282    hunt: { at: 16, next: ['chorus1'] },
283    chorus1: { at: 24, next: ['verse2'] },
284    verse2: { at: 32, next: ['flow'] },
285    flow: { at: 40, next: ['stems'] },
286    stems: { at: 48, next: ['chorus2'] },
287    chorus2: { at: 56, next: ['outro', 'chorus2'], times: 2 },
288    outro: { at: 64, next: [] },
289  },
290})
291
292// ── the song: Jev's section, parts and energy, composed ──
293// Each section plays its own bars, each part at the level Jev chose; what a
294// passage is made of plays outside the levels (SPEC.md, "The parts").
295const parts = form.arrange(once(stack(
296  part('keys', keys),
297  opening,
298  part('lead', leadPart),
299  part('bass', bass),
300  part('drums', kit),
301  sung(vox),
302  found,
303  alone,
304  part('snake', snake),
305  snakeSolo,
306).mul(velocity(push))))
307
308// ── into the next section: over the last bar of each, the answer for the section being entered ──
309// Past the last section there is no next one, so nothing plays there.
310const into = dj.into.choice.early(1)
311const next = section.choice.early(1)
312const enters = (name) => next.fmap((s) => s === name)
313const toms = s("~ ~ ~ ~ ~ ~ ~ ~ 808ht 808ht 808mt 808mt 808lt 808lt 808lt 808lt").room(.3).gain(.6).orbit('drums')
314// one note across the bar, its level and high-pass both rising: a swell.
315// As written it leads into chorus 1 only, louder and sooner than Jev's own.
316const riser = s("white").attack(1.7).sustain(1).release(.05).hpf(400).hpenv(4.3).hpattack(1.9).hpsustain(1).gain(.5)
317const rise  = s("white").attack(1.2).sustain(1).release(.05).hpf(400).hpenv(4.3).hpattack(1.9).hpsustain(1).gain(.9)
318// into the clean chorus nothing is noise: the pad's G chord, an octave
319// doubled, swelling up as its filter opens
320const lift = note("[55,59,62,67,71,74]").s("supersaw").detune(.2).attack(1.7).sustain(1).release(.05)
321  .lpf(700).lpenv(3).lpattack(1.8).room(.4).gain(.3).orbit('keys')
322const transition = into.pick({
323  asWritten: stack(rise.mask(enters('chorus1')), lift.mask(enters('chorus2'))),
324  fill: toms,
325  riser,
326  dropout: silence,
327}).mask("<0!7 1>")
328// dropout: the parts stop for the section's last beat. The outro has no
329// next section, so its last beat plays.
330const lastBeat = "<0!7 [0 0 0 1]>"
331const gap = stack(
332  "<1!7 [1 1 1 0]>",
333  into.fmap((c) => c !== 'dropout').mask(lastBeat),
334  section.choice.fmap((s) => s === 'outro').mask(lastBeat),
335)
336
337// Energy scales the band; the ending always lands at its own level. One
338// postgain for the whole mix keeps the master's peak under -1 dBFS (SPEC.md, "Mix").
339stack(
340  stack(parts.mask(gap), transition).mul(velocity(dj.energy.score.div(3).range(.55, 1))),
341  form.arrange(once(ending)),
342).mul(postgain(.52))