tidal.ts

examples/fractals/demo/tunes/tidal.ts · raw · open the example · all examples · site home

Exports: TIDAL

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
// Tidal: future garage at 100 BPM in F minor (Fm9, Dbmaj9, Abmaj7, Eb6).
// The sea under everything; a 2-step beat (the kick on one and the "and" of
// three, snaps on two and four, swung shakers, a rim before the backbeat);
// a sub that glides from bar to bar; pitched vocal chops singing a
// syncopated hook; glass pings in the delay. A breakdown of choir and slow
// chops for the Buddhabrot and its inverse, and a second groove with a
// glass arp over it.
import { tempo } from "../../../film/timeline.js";
import type { AirScore } from "../airScore.js";
import type { Tune } from "../tune.js";

const { BEAT, bars, beats } = tempo(100);

const CHORDS = [
  { root: 41, notes: [56, 60, 63, 67] },
  { root: 37, notes: [53, 56, 60, 63] },
  { root: 44, notes: [55, 60, 63, 68] },
  { root: 39, notes: [55, 58, 60, 63] },
];
const at4 = (bar: number) => ((Math.floor(bar) % 4) + 4) % 4;
const chordAt = (bar: number) => CHORDS[at4(bar)];

// The chops: [beat, note] per bar, off the beat
const HOOK: [number, number][][] = [
  [[0.75, 72], [1.5, 75], [2.25, 72], [3.5, 68]],
  [[0.75, 72], [1.5, 77], [2.25, 75], [3.5, 72]],
  [[0.75, 75], [1.5, 79], [2.25, 75], [3.5, 72]],
  [[0.75, 70], [1.5, 72], [2.25, 67], [3.5, 70]],
];

function chops(s: AirScore, bar: number, level: number, { octave = 0, slow = false } = {}) {
  HOOK[at4(bar)].forEach(([beat, note], i) => {
    s.vox(bars(bar) + beats(slow ? beat * 2 : beat), note + octave, slow ? 0.6 : 0.32, level, [-0.3, 0.3, -0.15, 0.15][i]);
  });
}

// Pings on the chord's top notes, every other beat, into the delay
function pings(s: AirScore, bar: number, level: number) {
  const notes = chordAt(bar).notes;
  [0, 2].forEach((beat, i) => s.glass(bars(bar) + beats(beat + 0.5), notes[(i + bar) % notes.length] + 24, level, { decay: 2.5, pan: i ? 0.5 : -0.5, delay: 0.6 }));
}

function garage(s: AirScore, from: number, to: number, { full = true } = {}) {
  for (let bar = from; bar < to; bar += 1) {
    const chord = chordAt(bar);
    const start = bars(bar);
    s.air(start, bars(1), chord.notes, 0.02, 1700, { ducked: true });
    s.sub(start, chord.root, bars(1) - 0.04, 0.17, chordAt(bar + 1).root);
    s.thump(start, 0.42);
    s.thump(start + beats(2.5), 0.32);
    if (bar % 2 === 1) {
      s.thump(start + beats(3.75), 0.18);
    }
    s.snap(start + beats(1), 0.11);
    s.snap(start + beats(3), 0.12);
    s.rim(start + beats(2.75), 0.035, -0.3);
    for (let i = 0; i < 16; i += 1) {
      s.shaker(start + beats(i / 4 + (i % 2 ? 0.08 : 0)), [0.03, 0.012, 0.02, 0.012][i % 4], i % 2 ? 0.35 : -0.15);
    }
    if (full) {
      s.hat(start + beats(1.5), 0.022, true, 0.3);
      s.hat(start + beats(3.5), 0.022, true, 0.3);
    }
  }
}

function compose(s: AirScore) {
  s.beat = BEAT;
  for (let bar = 0; bar < 23; bar += 1) {
    s.chord(bars(bar));
  }

  // --- 0-4: the sea, pads, the chops far away ---
  s.breath(0, bars(5), 0.04, { from: 250, to: 900 });
  s.breath(bars(1), bars(4.5), 0.02, { from: 900, to: 3000, pan: 0.4 });
  s.drone(0, bars(4.2), 29, 0.08);
  for (let bar = 0; bar < 4; bar += 1) {
    s.air(bars(bar), bars(1), chordAt(bar).notes, 0.026, 1000 + bar * 200);
    pings(s, bar, 0.025);
  }
  chops(s, 2, 0.03);
  chops(s, 3, 0.035);
  s.swell(bars(3), bars(4), 0.08);
  s.lift(bars(3), bars(4), 0.08);

  // --- 4-12: the groove ---
  s.boom(bars(4), 0.4);
  s.fall(bars(4), 2, 0.05);
  garage(s, 4, 12);
  for (let bar = 4; bar < 12; bar += 1) {
    chops(s, bar, 0.055);
    pings(s, bar, 0.02);
    if (bar >= 8) {
      const notes = chordAt(bar).notes;
      for (let j = 0; j < 8; j += 1) {
        s.kalimba(bars(bar) + beats(j / 2 + (j % 2 ? 0.08 : 0)), notes[[0, 2, 1, 3][j % 4]] + 12, 0.022, { pan: j % 2 ? 0.45 : -0.45, mark: 0.2 });
      }
    }
  }
  s.crash(bars(8), 0.06);

  // --- 12-16: the breakdown ---
  s.boom(bars(12), 0.3);
  for (let bar = 12; bar < 16; bar += 1) {
    const chord = chordAt(bar);
    s.air(bars(bar), bars(1), chord.notes, 0.03, 2000);
    s.choir(bars(bar), bars(1), chord.notes, 0.05);
    s.sub(bars(bar), chord.root, bars(1) - 0.04, 0.1, chordAt(bar + 1).root);
    s.rim(bars(bar) + beats(1), 0.03, 0.3);
    s.rim(bars(bar) + beats(3), 0.03, -0.3);
    for (let i = 0; i < 8; i += 1) {
      s.shaker(bars(bar) + beats(i / 2 + (i % 2 ? 0.16 : 0)), 0.014, i % 2 ? 0.3 : -0.3);
    }
  }
  chops(s, 12, 0.045, { octave: 12, slow: true });
  chops(s, 14, 0.045, { octave: 12, slow: true });
  s.breath(bars(12), bars(16), 0.035, { from: 300, to: 1200 });
  s.swell(bars(15), bars(16), 0.1);
  s.lift(bars(15), bars(16), 0.1);
  s.roll(bars(15.5), bars(16), 0.08);

  // --- 16-22: the groove, with a glass arp over it ---
  s.boom(bars(16), 0.5);
  s.crash(bars(16), 0.08);
  s.fall(bars(16), 2, 0.05);
  garage(s, 16, 22);
  for (let bar = 16; bar < 22; bar += 1) {
    chops(s, bar, 0.055);
    const notes = chordAt(bar).notes;
    for (let i = 0; i < 16; i += 1) {
      s.glass(bars(bar) + beats(i / 4 + (i % 2 ? 0.08 : 0)), notes[[0, 1, 2, 3, 2, 1][i % 6]] + 24, 0.014, { decay: 1.2, pan: Math.sin(i * 1.7) * 0.6, delay: 0.3 });
    }
  }
  s.crash(bars(20), 0.06);

  // --- 22-24: the tide goes out ---
  s.boom(bars(22), 0.3);
  s.air(bars(22), bars(1.6), [56, 60, 63, 67, 70], 0.03, 1800);
  s.choir(bars(22), bars(1.4), [60, 63, 67], 0.045);
  s.drone(bars(22), bars(24), 29, 0.08);
  s.sub(bars(22), 41, bars(1.5), 0.12);
  s.breath(bars(22), bars(24), 0.035, { from: 900, to: 250 });
  [[0.75, 72], [1.5, 75], [2.25, 72], [3.5, 68], [5, 67]].forEach(([beat, note], i) => s.vox(bars(22) + beats(beat), note, 0.5, 0.045 - i * 0.004, i % 2 ? 0.3 : -0.3));
}

export const TIDAL: Tune = {
  name: "tidal",
  title: "Tidal",
  blurb: "future garage · 100 BPM · 58 s",
  colors: ["#2b6cff", "#ff5fd2"],
  bpm: 100,
  end: 24,
  outro: 22,
  compose,
  mix: { delay: BEAT * 0.75, feedback: 0.48, reverb: 0.5, output: 1.5 },
  cues: [
    { bar: 0, preset: "Quaternion Bloom", style: "stardust", beats: 8, move: { distance: 4.3, azimuth: 0, elevation: 10, orbit: 5, push: 0.04 } },
    { bar: 2, preset: "Thomas Knot", style: "swirl", beats: 4, move: { orbit: -6 } },
    { bar: 4, preset: "Crystal Lattice", style: "ripple", beats: 4, move: { orbit: 8 } },
    { bar: 6, preset: "Amethyst Geode", style: "swirl", beats: 4, move: { orbit: -8 } },
    { bar: 8, preset: "Kleinian Forest", style: "galaxy", beats: 4, move: { orbit: 9, rise: 3 } },
    { bar: 10, preset: "Aizawa Silk", style: "sweep", beats: 4, move: { orbit: -10 } },
    { bar: 12, preset: "Buddha in 4D", style: "dissolve", beats: 6, move: { orbit: 3, push: 0.03 } },
    { bar: 14, preset: "Anti-Buddha", style: "dissolve", beats: 4, move: { orbit: -3, push: 0.03 } },
    { bar: 16, preset: "Neon Bulb", style: "burst", beats: 2, move: { orbit: 10 } },
    { bar: 18, preset: "Apollonian Pearl", style: "orb", beats: 4, move: { orbit: -10 } },
    { bar: 20, preset: "Halvorsen Bloom", style: "swirl", beats: 4, move: { orbit: 10, rise: 4 } },
    { bar: 22, preset: "Quaternion Bloom", style: "galaxy", beats: 4, move: { distance: 5.2, orbit: 4 } },
  ],
  lines: [
    { at: 0.4, until: 1.9, html: "every point of light is a Gaussian splat", className: "lower" },
    { at: 2.2, until: 3.9, html: "baked on the GPU, drawn by Spark Radix", className: "lower" },
    { at: 4, until: 5.8, html: "FRACTALS", className: "title", anim: "slam", enter: 0.3 },
    { at: 4.35, until: 5.8, html: "in Gaussian splats", className: "subtitle", anim: "rise" },
    { at: 8.2, until: 9.9, html: "kaleidoscopes, sponges, Kleinian forests", className: "lower" },
    { at: 12.2, until: 13.9, html: "a Buddhabrot, turned through four dimensions", className: "lower" },
    { at: 14.2, until: 15.9, html: "and its inverse", className: "lower" },
    { at: 16.2, until: 17.9, html: "each one becomes the next, splat for splat", className: "lower" },
    { at: 20.2, until: 21.9, html: "strange attractors, as threads of light", className: "lower" },
    { at: 22.3, until: 23.7, html: "SPARK RADIX", className: "title brand", enter: 0.9 },
    { at: 22.7, until: 23.7, html: "3D fractals in splats", className: "subtitle", anim: "rise", enter: 0.8 },
  ],
};