song.jsannotatedsong.jssource265 lines · 13.8 KB · raw
1// TYPESAFE JUNGLE — 170bpm, 64 bars as written; Jev arranges up to 28 four-bar sections. Spec: SPEC.md
2// vox: the repo's samples/ folder, served with the site (see README)
3// Start from STOPPED so the song begins at bar 1 (re-running it mid-play keeps the cycle count).
4// (The MCP server rejects any code containing the substring e-v-a-l, comments included.)
5setcps(170/60/4)
6await samples('github:tidalcycles/dirt-samples')
7await samples('jev-samples/strudel.json')
8
9// ── Jev: two of them, in TypeSafe's own shape (SPEC.md, "Jev in the booth") ──
10// formJev picks the next section; djJev, asked once that is decided (questions
11// in one request cannot see each other), says how much of each part plays
12// it, how the song moves into it, and how hard. djJev is asked every section;
13// formJev only where the form branches. The form's rules (walk, at the bottom) narrow the
14// sections Jev may pick and end the song; without Jev it plays as written.
15const song = {
16  title: 'TYPESAFE JUNGLE',
17  what: 'a jungle track about Jev, the typed-decision model from TypeSafe AI, built on the My Name Is Jeff meme: the hook swaps Jeff for Jev',
18  tempo: '170 bpm, 4/4, 4 bars a section',
19  key: 'C minor',
20  chords: 'Cm Ab Fm G, one chord per bar, under a music-box lead and a reese bassline',
21  lyrics: {
22    build1: 'My name is Jeff / My name is Jev. J, E, V.',
23    build1b: 'My name is Jeff / Type safe Jev / Type safe... Jev',
24    build2: 'My name is Jeff / My name is Jev. J, E, V. / Type safe Jev / Type safe... Jev',
25  },
26}
27
28const formJev = jev({
29  state: song,
30  questions: {
31    section: choice('Pick the section of the song that plays next, from those allowed. Choosing outro ends the song (outro, then the final hit): choose it once the song has built to its peak and made its point, before it drags', {
32      intro: 'the music-box lead alone',
33      tease: 'the lead with a thin, high-passed amen break coming in',
34      build1: 'the build: the hook vocals, the amen filter opening, a low sub',
35      build1b: 'the build peaks: the hook, a snare roll and a noise riser into the drop',
36      drop1: 'drop 1: full jungle drums, the reese and sine bassline, eighth hats, a crash on the downbeat',
37      drop1b: 'drop 1 continues',
38      drop1c: 'drop 1 with the rave stabs',
39      drop1d: 'the end of drop 1, stabs and all',
40      breakdown: 'the breakdown: drums out, just the lead and the low sub',
41      build2: 'the hook returns, the whole Jeff-to-Jev swap in 4 bars, with a noise riser into the second drop',
42      drop2: 'drop 2: everything, sixteenth hats, rave stabs, a crash on the downbeat',
43      drop2b: 'drop 2 continues',
44      drop2c: 'drop 2, the peak of the song',
45      drop2d: 'the last bars of drop 2',
46      outro: 'the outro: kick and bass out, the amen thins, the lead starts to fade: the way to the end',
47      final: 'one last kick and crash left to ring while the lead fades out: the end',
48    }),
49  },
50})
51
52// how much of each part the section decidedThisSection has: out, back or full
53const 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', {
54  out: 'silent for the section',
55  back: 'under the rest of the mix, at half velocity',
56  full: 'as written',
57})
58const djJev = jev({
59  state: song,
60  questions: {
61    lead: level('the music-box lead (the tune, on its own in the intro, over everything from the intro until it fades in the outro)'),
62    drums: level('the jungle drums (the amen break with kick and snare in the drops, a thin filtered amen in the tease and builds, hats, crash, and the snare roll and riser into each drop, and the amen alone, thinning, in the outro)'),
63    stabs: level('the rave stabs (in the second half of drop 1 and all of drop 2)'),
64    bass: level('the bass (the reese and sine bassline in the drops, a low sub in the builds: the floor under them)'),
65    vocals: level('the vocals (the My name is Jeff / Type safe Jev hook, only in the builds)'),
66    into: choice('How should the song move into the next section (decidedThisSection)? It plays over the last bar of the section before it', {
67      cut: 'straight in, no transition',
68      fill: 'a snare roll rising in pitch into the first beat',
69      riser: 'white noise sweeping up over the last bar',
70      dropout: 'everything stops for the last beat, a gap before the section hits',
71    }),
72    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', [
73      'held back: about 5 dB under full',
74      'steady: about 3 dB under full',
75      'driving: about 1.5 dB under full',
76      'all out: full, as written',
77    ]),
78  },
79})
80
81// a little chance in the form: the section is drawn from Jev's probabilities
82const { section } = formJev.every(4, { sample: 0.7 })
83
84// Where each part plays as written. Jev is asked about a part only in these
85// sections; anywhere else it cannot sound, so it is not asked (full). The
86// lead plays in every section.
87const plays = {
88  drums: ['tease', 'build1', 'build1b', 'drop1', 'drop1b', 'drop1c', 'drop1d', 'build2',
89    'drop2', 'drop2b', 'drop2c', 'drop2d', 'outro'],
90  stabs: ['drop1c', 'drop1d', 'drop2', 'drop2b', 'drop2c', 'drop2d'],
91  bass: ['build1', 'build1b', 'drop1', 'drop1b', 'drop1c', 'drop1d', 'breakdown', 'build2',
92    'drop2', 'drop2b', 'drop2c', 'drop2d'],
93  vocals: ['build1', 'build1b', 'build2'],
94}
95const levels = (name) => ({ section: next }) =>
96  plays[name] && !plays[name].includes(next) ? ['full']
97    : name === 'drums' && next === 'outro' ? ['back', 'full']   // the outro is the amen alone
98    : ['out', 'back', 'full']
99const dj = djJev.every(4, {
100  after: { section },
101  forget: ['into'],
102  allow: {
103    lead: levels('lead'), drums: levels('drums'), stabs: levels('stabs'),
104    bass: levels('bass'), vocals: levels('vocals'),
105    // out of a build the roll and riser are already written: a cut or a dropout
106    into: ({ playingNow }) => (['build1b', 'build2'].includes(playingNow)
107      ? ['cut', 'dropout'] : ['cut', 'fill', 'riser', 'dropout']),
108  },
109  fallback: { lead: 'full', drums: 'full', stabs: 'full', bass: 'full', vocals: 'full', into: 'cut', energy: 3 },
110})
111
112// A part at Jev's level: silent when out, half velocity when back, as
113// written when full. Each part has its own orbit, named for its question,
114// so the meter hears it alone (measuredSound); the lead and the vocals each
115// get their own delay line there.
116const part = (name, pat) =>
117  pat.orbit(name)
118    .mask(dj[name].choice.fmap((c) => c !== 'out'))
119    .mul(velocity(dj[name].choice.fmap((c) => (c === 'back' ? .5 : 1))))
120
121// bars:  1-8 intro | 9-16 build+hook | 17-32 DROP 1 | 33-36 breakdown
122//        37-40 build+hook | 41-56 DROP 2 | 57-64 outro     (each mask = 64 steps)
123const tease1 = "<0!4 1!12 0!48>"            // filtered amen, opening from the tease into drop 1
124const tease2 = "<0!36 1!4 0!24>"            // and again over build 2
125const hook1  = "<0!8 1!8 0!48>"             // vocals ONLY here, in the builds
126const hook2  = "<0!36 1!4 0!24>"
127const drops  = "<0!16 1!16 0!8 1!16 0!8>"
128const drop1  = "<0!16 1!16 0!32>"
129const drop2  = "<0!40 1!16 0!8>"
130const stabs  = "<0!24 1!8 0!8 1!16 0!8>"
131const lowSub = "<0!8 1!8 0!16 1!8 0!24>"
132const riser  = "<0!14 1!2 0!22 1!2 0!24>"
133// the snare roll ducks out under every spoken word: in bar 15 it plays beat
134// 4 only, in bar 16 beat 1 only; in build 2 the words fill bars 39-40, so
135// the noise riser carries that build alone
136const roll   = "<0!14 [0 0 0 1] [1 0 0 0] 0!48>"
137const crash  = "<0!16 1 0!23 1 0!23>"
138const outroB = "<0!56 1!4 0!4>"
139const final  = "<0!60 1 0!3>"
140const leadGain = "<.3!16 .2!16 .3!8 .2!16 .28!4 .2 .14 .08 0>"
141
142const roots = "<c2 ab1 f1 g1>"   // F1-C2, 44-65 Hz: a sub that small speakers still carry
143const bassline = "<[c2@3 c2@3 c2@4 c3@2 bb1@4] [ab1@3 ab1@3 ab1@4 ab2@2 g1@4] [f1@3 f1@3 f1@4 f2@2 ab1@4] [g1@3 g1@3 g1@4 b1@2 d2@4]>"
144const amen = "<[0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15] [16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31] [0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15] [0 1 2 3 4 5 6 7 [12 12] [12 12] 14 14 [30 30] [30 30] 31 31]>"
145
146// ── parts ─────────────────────────────────────────
147// music-box lead: the tune, fading out at the end
148// each bar outlines its chord; the G bar takes B natural (harmonic minor)
149const lead = n("<[0 ~ 2 ~ 4 ~ 7 ~] [~ 0 ~ 3 ~ 5 ~ 3] [5 ~ 3 ~ 0 ~ -2 ~] [1 ~ 4 ~ 6 ~ 4 ~]>")
150  .scale("<C4:minor!3 C4:harmonic:minor>").s("triangle").decay(.3).sustain(0)
151  .delay(.3).delaytime(.375).delayfeedback(.35).room(.4).gain(leadGain)
152
153// each amen slice cuts the one before (cut group 1); the roll is group 2
154const drums = stack(
155  s("[bd:3,808bd:1]").struct("x ~ ~ ~ ~ ~ ~ ~ ~ ~ x ~ ~ ~ ~ ~").gain(.75).mask(drops),
156  s("sn:3").struct("~ ~ ~ ~ x ~ ~ ~ ~ ~ ~ ~ x ~ ~ ~").room(.15).gain(.9).mask(drops),
157  n(amen).s("amencutup").hpf(150).gain(.75).cut(1).mask(drops),
158  // the filter opens in one sweep from the tease into the drop, and again over build 2
159  n("0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15").s("amencutup")
160    .hpf(saw.range(2500,300).slow(12).late(4)).gain(.55).cut(1).mask(tease1),
161  n("0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15").s("amencutup")
162    .hpf(saw.range(2500,300).slow(4)).gain(.55).cut(1).mask(tease2),
163  n(amen).s("amencutup").hpf(saw.range(300,3000).slow(4)).gain(.6).cut(1).mask(outroB),
164  s("hh*8").gain(".35 .18").mask(drop1),
165  s("hh*16").gain(".35 .15 .25 .15").mask(drop2),
166  s("hh27:1").gain(2.5).mask(crash),   // the crash: Dirt-Samples' cr is a ride
167  // the roll rises across both bars of the build
168  s("sn:3*16").gain(saw.range(.25,.6).slow(2)).speed(saw.range(1,1.4).slow(2)).cut(2).mask(roll),
169  s("white*16").decay(.06).sustain(0)
170    .hpf(saw.range(400,9000).slow(2)).gain(saw.range(.05,.35).slow(2)).mask(riser)
171)
172
173// rave stabs, second half of drop 1 and all of drop 2
174// stab:3 is pitched at C#; these speeds put it on C, Ab, F and G
175const rave = s("stab:3").struct("x ~ ~ x ~ ~ x ~ ~ ~ x ~ ~ ~ ~ ~")
176  .speed("<.944 .749 .63 .707>").room(.3).gain(.45).mask(stabs)
177
178const bass = stack(
179  note(bassline).s("supersaw").detune(.6)
180    .lpf(sine.range(220,900).slow(8)).lpq(3).shape(.3).gain(.5).mask(drops),
181  note(bassline).s("sine").gain(.5).mask(drops),
182  // a little shape, so its second harmonic carries on small speakers
183  note(roots).s("sine").attack(.3).release(.5).shape(.25).gain(.45).mask(lowSub)
184)
185
186// the hook — builds only. Build 1: "my name is jeff … my name is jev, J E V …
187// my name is jeff … type safe jev", then the pickup "type safe … jev" into the
188// drop. Build 2 packs the swap into 4 bars; mnjev runs 2.3 s, so tsjev waits
189// for beat 3 of bar 39.
190const vox = stack(
191  s("<jeff ~ mnjev ~ jeff ~ tsjev [~ ts ~ jev]>").mask(hook1),
192  s("<jeff mnjev [~ ~ tsjev ~] [~ ts ~ jev]>").mask(hook2),
193).gain(1.3).room(.25).delay(.25).delaytime(.375).delayfeedback(.3)
194
195// the end: one last hit, then let it ring. It always plays.
196const ending = stack(s("[bd:3,808bd:1]").gain(.75), s("hh27:1").gain(2.5))
197  .room(.6).size(.9).mask(final)
198
199const once = (p) => p.mask("<1!64 0!936>")   // play once, then silence
200
201// ── the form, as rules in code: which sections may follow which ──
202// `at` is where each starts as written; an empty `next` ends the song. The
203// written order is always allowed, and is what plays without Jev.
204// `times`: each drop's loop goes round once more at most (SPEC.md, "Loops end").
205const form = walk(section, {
206  max: 28,
207  sections: {
208    intro: { at: 0, next: ['tease'] },
209    tease: { at: 4, next: ['build1'] },
210    build1: { at: 8, next: ['build1b'] },
211    build1b: { at: 12, next: ['drop1'] },
212    drop1: { at: 16, next: ['drop1b'] },
213    drop1b: { at: 20, next: ['drop1c'] },
214    drop1c: { at: 24, next: ['drop1d'], times: 2 },
215    drop1d: { at: 28, next: ['breakdown', 'drop1c'] },
216    breakdown: { at: 32, next: ['build2'] },
217    build2: { at: 36, next: ['drop2'] },
218    drop2: { at: 40, next: ['drop2b'], times: 2 },
219    drop2b: { at: 44, next: ['drop2c'] },
220    drop2c: { at: 48, next: ['drop2d', 'drop2'] },
221    drop2d: { at: 52, next: ['outro', 'breakdown', 'drop2'] },
222    outro: { at: 56, next: ['final'] },
223    final: { at: 60, next: [] },
224  },
225})
226
227// ── the song: Jev's section, parts and energy, composed ──
228// Each section plays its own bars, each part at the level Jev chose. The
229// vocals stand apart so a dropout never cuts them: out of a build, the
230// last "Jev" lands alone in the gap.
231const parts = form.arrange(once(stack(
232  part('lead', lead),
233  part('drums', drums),
234  part('stabs', rave),
235  part('bass', bass),
236)))
237const vocals = form.arrange(once(part('vocals', vox)))
238
239// ── into the next section: over the last bar of each, the next one's answer ──
240// Past the last section there is no next one, so nothing plays there.
241const into = dj.into.choice.early(1)
242const transition = into.pick({
243  cut: silence,
244  fill: s("sn:3*16").gain(saw.range(.25, 1)).speed(saw.range(1, 1.4)).cut(2).orbit('drums'),
245  riser: s("white*16").decay(.06).sustain(0)
246    .hpf(saw.range(400, 9000)).gain(saw.range(.05, .35)),
247  dropout: silence,
248}).mask("<0!3 1>")
249// dropout: every part but the vocals stops for the section's last beat. The
250// final section has no next section, so its last beat plays.
251const lastBeat = "<0!3 [0 0 0 1]>"
252const gap = stack(
253  "<1!3 [1 1 1 0]>",
254  into.fmap((c) => c !== 'dropout').mask(lastBeat),
255  section.choice.fmap((s) => s === 'final').mask(lastBeat),
256)
257
258// The energy scales every part's velocity together (0.56 to 1: about -5 dB
259// up to as written), so the mix keeps its balance; the last hit is outside
260// it and always plays as written. The master level is outside everything:
261// superdough has no limiter, so it keeps the peaks under -1 dBFS.
262stack(
263  stack(parts.mask(gap), vocals, transition).mul(velocity(dj.energy.score.div(3).range(.56, 1))),
264  form.arrange(once(ending)),
265).mul(postgain(.36))