song.jsannotatedsong.jssource261 lines · 14.1 KB · raw

OPTION ORDER — glitch-hop, 100bpm, 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(100/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: 'OPTION ORDER',
14  what: 'glitch-hop about Jev answers flipping when only the order of the options changes: the same question, the options swapped, the other verdict',
15  tempo: '100 bpm, swung half-time, 8 bars a section',
16  key: 'D minor',
17  chords: 'Dm Bb F C, two bars each: an 8-bar loop, carried by the bass and short stabs',
18  lyrics: {
19    chorus: 'Fair or unfair? / Fair / Unfair or fair? / Unfair / Approve or fix now? / Approve / Fix now or approve? / Fix now',
20    bridge: 'Thirty-two out of two hundred / thir- thir- thir- thirty-two out of two hundred',
21    hook: 'Wait, it is all shuffled? / Always has been',
22    outro: 'the chorus again with every answer swapped: Fair or unfair? / Unfair / Unfair or fair? / Fair / Approve or fix now? / Fix now / Fix now or approve? / Approve',
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 half-time kit and the wobble bass through a closed filter',
31      chorus: 'the vocoder chorus: a question with its options one way and its answer, then the options swapped and the other answer',
32      drop1: 'drop 1, first half: swung drums, the amen break straight, full wobble bass and stabs',
33      drop1b: 'drop 1, second half: the same groove on the break\'s other half, with a written stutter every other bar',
34      bridge: 'the bridge, the build: a stuttering break, the count on Dm, then a stuttered repeat on F, the bass filter opening across it',
35      hook: 'the meme hook, then drop 2 lands with the break reversed every other bar',
36      drop2: 'drop 2: the peak, the break alternating forwards and reversed, the bass doubled an octave up, the stabs wide open',
37      outro: 'the chorus once more with every answer swapped, as the drums thin out to a last hit: 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: half velocity, under the rest of the mix',
46  'full: as written',
47])
48const djJev = jev({
49  state: song,
50  questions: {
51    drums: level('the drums (the heavy swung half-time kit, in every section: through a closed filter in the intro; full in the chorus, drop1, drop1b and the bridge; only hats under the meme in the first half of the hook, then full through drop2; thinning out to a last hit in the outro)'),
52    breaks: level('the amen break (chopped over the kit, in drop1, drop1b, the bridge, the second half of the hook and drop2: straight in drop1, the break\'s other half with a stutter in drop1b, stuttering and dropping back under the count in the bridge, reversed every other bar in drop2; nothing in the intro, the chorus or the outro)'),
53    bass: level('the wobble bass (the chord roots under a wobbling filter, in every section but the outro: closed in the intro, full in the chorus, drop1 and drop1b, its filter opening across the bridge, in the second half of the hook only, doubled an octave up on the offbeats in drop2)'),
54    stabs: level('the chord stabs (short swung chords, the only harmony, in the chorus, drop1, drop1b, the second half of the hook and drop2, where they open fully; nothing in the intro, the bridge or the outro)'),
55    vocals: level('the bridge vocals (the count, thirty-two out of two hundred, and its stutter: in the bridge only; the chorus and hook lines always play)'),
56    into: choice('How should the song move into the next section (decidedThisSection)? It plays over the last bar of the section before it', {
57      cut: 'straight in, no transition',
58      fill: 'a snare-and-clap roll doubling up across the last bar',
59      riser: 'white noise swelling up over the last bar',
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? Shape the energy of the whole song, following its history, how it measured and how the listener reacted', [
63      'held back: 0.55 of full velocity, room to breathe',
64      'steady: 0.7 of full velocity',
65      'driving: 0.85 of full velocity',
66      'all out: full velocity, nothing held back',
67    ]),
68  },
69})

a little chance: the section is drawn from Jev's probabilities, and so is the transition

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

A part at Jev's level. The score is rounded to its label, and the label gives both the mask and the velocity: out (below 0.5) is silent, back (0.5 to 1.5) plays at .5, full (from 1.5) as written. Each part has its own orbit, named for its question, so the meter hears it alone.

84const rung = (score) => score.fmap((x) => (x < .5 ? 0 : x < 1.5 ? .5 : 1))
85const part = (name, pat) => {
86  const v = rung(dj[name].score)
87  return pat.orbit(name).mask(v.fmap((x) => x > 0)).mul(velocity(v))
88}

bars: 1-8 intro | 9-16 chorus | 17-24 DROP 1 | 25-32 drop 1b | 33-40 bridge 41-44 hook | 45-56 DROP 2 | 57-64 outro (chorus, answers swapped) (each mask = 64 steps)

92const intro   = "<1!8 0!56>"
93const chorus  = "<0!8 1!8 0!48>"
94const groove  = "<0!8 1!32 0!4 1!12 0!8>"
95const wobbles = "<0!8 1!24 0!12 1!12 0!8>"   // the groove without the bridge
96const drop1   = "<0!16 1!8 0!40>"
97const drop1b  = "<0!24 1!8 0!32>"
98const bridge  = "<0!32 1!8 0!24>"
99const hook    = "<0!40 1!4 0!20>"
100const drop2   = "<0!44 1!12 0!8>"
101const stabs   = "<0!8 1!24 0!12 1!12 0!8>"
102const outro   = "<0!56 1!8>"
103const outroDr = "<0!56 1!6 0!2>"
104const crash   = "<0!16 1 0!27 1 0!19>"
105const stutter = "<0!36 1 0!27>"
106const final   = "<0!63 1>"

two bars per chord: Dm Dm Bb Bb F F C C

109const chords = "<[d3,f3,a3]!2 [bb2,d3,f3]!2 [f3,a3,c4]!2 [c3,e3,g3]!2>"
110const roots  = "<38!2 34!2 41!2 36!2>"   // D2 Bb1 F2 C2, as MIDI numbers
111const wobbleRate = "<4 4 8 4 2 4 8 16>"

the wobble, struck in 32nds so every rate moves: a cosine read 32 times a bar is never read only at its zero crossings, as a sine at 16ths was

114const wobble = (lo, hi) => cosine.range(lo, hi).fast(wobbleRate)

the whole kit swings together: odd 16ths a third late

117const swing = (p) => p.swingBy(1/3, 8)
118const kickHits = "x ~ ~ ~ ~ ~ ~ x ~ ~ x ~ ~ ~ ~ ~"

the half-time snare, with a written-out roll closing every 4th bar

120const snare = "<[~ ~ ~ ~ ~ ~ ~ ~ x ~ ~ ~ ~ ~ ~ ~]!3 [~ ~ ~ ~ ~ ~ ~ ~ x ~ ~ ~ [x x] [x x] [x x x x] [x x x x]]>"

the break: forwards, and bar-for-bar reversed (slices in reverse order, each played backwards)

122const fwd = "0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15"

drop 1b: the break's other half, with a written ply stutter on beat 4 of every 2nd bar

124const half2 = "<[16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31] [16 17 18 19 20 21 22 23 24 25 26 27 [28 28] [29 29] [30 30 30 30] [31 31 31 31]]>"

── parts ───────────────────────────────────────── drums: heavy, swung half-time

128const kit = stack(
129  swing(s("[bd:3,808bd:1]").struct(kickHits)).gain(.6).mask(groove),
130  swing(s("[sn:3,cp]").struct(snare)).room(.2).gain(.85).mask(groove),
131  swing(s("hh*16").gain(".3 .1 .2 .1")).mask(groove),
132  swing(s("hh*16").gain(".25 .05 .12 .05")).mask(hook),
133  // intro: the same kit through a closed filter
134  swing(s("[bd:3,808bd:1]").struct(kickHits)).lpf(500).gain(.6).mask(intro),
135  swing(s("[sn:3,cp]").struct(snare)).lpf(900).gain(.6).mask(intro),
136  // outro: kick and snare thin out, then stop
137  s("[bd:3,808bd:1]").struct("x ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~").gain(.6).mask(outroDr),
138  s("sn:3").struct("~ ~ ~ ~ ~ ~ ~ ~ x ~ ~ ~ ~ ~ ~ ~").room(.4).gain(.6).mask(outroDr),
139  // hh27:1 is a crash (Dirt-Samples' cr is a ride), about 15 dB quieter than the ride was
140  s("hh27:1").gain(2.8).mask(crash)
141)

the break, each slice cut by the next: straight in drop 1, its other half in drop 1b, reversed every other bar in drop 2

145const brk = stack(
146  n(fwd).s("amencutup").gain(.6).mask(drop1),
147  n(half2).s("amencutup").gain(.6).mask(drop1b),
148  n("<[0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15] [15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0]>").s("amencutup").speed("<1 -1>").gain(.6).mask(drop2),
149  // bridge: the break with written-out stutters, dropping back while the count speaks (bars 33 and 37)
150  n("0 1 2 3 [4 4 4 4] 5 6 7 8 9 [10 10] 11 [12 12 12 12] 13 [14 14 14] 15")
151    .s("amencutup").gain("<.6!32 .2 .6 .6 .6 .2 .6 .6 .6 .6!24>").mask(bridge)
152).hpf(200).cut(2)

bass: wobble, struck in 32nds so the filter LFO moves in every bar

155const bass = stack(
156  note(roots).struct("x*32").s("sawtooth").lpf(wobble(250, 1600))
157    .lpq(8).shape(.4).gain(.35).mask(wobbles),
158  // bridge: the same wobble with its filter opening over the 8 bars
159  note(roots).struct("x*32").s("sawtooth").lpf(wobble(150, 450).add(saw.slow(8).range(0, 2200)))
160    .lpq(8).shape(.4).gain(.35).mask(bridge),
161  note(roots).struct("x*32").s("sawtooth").lpf(wobble(150, 500))
162    .lpq(6).gain(.3).mask(intro),
163  note(roots).s("sine").gain(.35).mask(groove),
164  // drop 2: the bass doubled an octave up on the offbeats
165  note(roots).add(note(12)).struct("[~ x]*4").s("sawtooth").lpf(2200)
166    .decay(.12).sustain(0).gain(.22).mask(drop2)
167)

harmony: swung stabs on odd 16ths, never a pad; fully open in drop 2

170const harmony = swing(note(chords).struct("~ ~ ~ x ~ ~ ~ x ~ ~ ~ x ~ ~ ~ ~"))
171  .s("sawtooth").lpf("<2500!44 6000!12 2500!8>").decay(.25).sustain(0).room(.3).gain(.85).mask(stabs)

the bridge count: Jev's to level

174const vox = stack(
175  // bridge: the count on Dm, then a written-out stutter of it on F
176  s("<oothirtydm ~ ~ ~ ~ ~ ~ ~>").gain(1.1).room(.2).mask(bridge),
177  s("[oothirtyf oothirtyf oothirtyf] oothirtyf@3").end("[.1 .1 .1] 1@3").cut(4)
178    .gain(1.1).room(.2).mask(stutter),
179)

the joke, which always plays: each question on its chord's first bar, the answer on its second. The chorus answers in the options' order; the outro asks the same questions and gets the other answers.

184const lines = stack(
185  s("<ooqfair ooafairdm ooqunfair ooaunfairbb ooqapprove ooaapprovef ooqfix ooafixc>")
186    .gain(1.1).room(.25).mask(chorus),
187  s("<ooqfair ooaunfairdm ooqunfair ooafairbb ooqapprove ooafixf ooqfix ooaapprovec>")
188    .gain(1.1).room(.35).mask(outro),
189  // hook: the astronaut asks, the other answers
190  s("<ooshuffled ~ ooalways ~>").gain(1.2).room(.3).mask(hook),
191).orbit('vocals')

the end: kick, sub kick and crash on the last bar. It always plays, in full.

194const ending = stack(
195  s("[bd:3,808bd:1]").gain(.6),
196  s("hh27:1").gain(2.8),
197).room(.6).size(.9).mask(final)
199const 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. Sections are 8-bar windows of the song as written, so hook holds the 4-bar hook and the first 4 bars of drop 2.

206const form = walk(section, {
207  max: 16,
208  sections: {
209    intro: { at: 0, next: ['chorus'] },
210    chorus: { at: 8, next: ['drop1'] },
211    drop1: { at: 16, next: ['drop1b'] },
212    drop1b: { at: 24, next: ['bridge', 'chorus'] },
213    bridge: { at: 32, next: ['hook'] },
214    hook: { at: 40, next: ['drop2'] },
215    drop2: { at: 48, next: ['outro', 'chorus', 'drop1', 'bridge'] },
216    outro: { at: 56, next: [] },
217  },
218})

── 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 joke's lines and the final hit always play, outside it.

224const parts = form.arrange(once(stack(
225  part('drums', kit),
226  part('breaks', brk),
227  part('bass', bass),
228  part('stabs', harmony),
229  part('vocals', vox),
230)))

── 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. Out of the chorus, only the last two beats, so the last answer is heard.

235const into = dj.into.choice.early(1)
236const transition = into.pick({
237  cut: silence,
238  fill: swing(s("[sn:3,cp]").struct("x ~ x ~ x x x x [x x] [x x] [x x] [x x] [x x x x] [x x x x] [x x x x] [x x x x]"))
239    .gain(saw.range(.35, .9)).room(.2).orbit('drums'),
240  riser: s("white*16").hpf(saw.range(400, 8000)).gain(saw.range(.05, .35)).decay(.07).sustain(0),
241  dropout: silence,
242}).mask(stack(
243  // the last two beats always; the two before them unless leaving the chorus
244  // (the halves never overlap, so no hit plays twice)
245  "<0!7 [0 1]>",
246  section.choice.fmap((s) => s !== 'chorus').mask("<0!7 [1 0]>"),
247))

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

250const lastBeat = "<0!7 [0 0 0 1]>"
251const gap = stack(
252  "<1!7 [1 1 1 0]>",
253  into.fmap((c) => c !== 'dropout').mask(lastBeat),
254  section.choice.fmap((s) => s === 'outro').mask(lastBeat),
255)

the master level keeps the peak at or under -1 dBFS (SPEC.md, Constraints)

258stack(
259  stack(parts.mask(gap), transition).mul(velocity(dj.energy.score.div(3).range(.55, 1))),
260  form.arrange(once(stack(lines, ending))),
261).mul(postgain(.36))