0xray 4.0.14 → 4.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.grok-plugin/plugin.json +1 -1
- package/.opencode/skills/orchestrator/SKILL.md +19 -1
- package/.xray/features.json +2 -2
- package/AGENTS-consumer.md +10 -5
- package/AGENTS.md +10 -7
- package/README.md +8 -7
- package/SKILLS.md +3 -2
- package/dist/AGENTS.md +10 -7
- package/dist/CHANGELOG.md +48 -0
- package/dist/README.md +8 -7
- package/dist/SKILLS.md +3 -2
- package/dist/cli/commands/hermes-install.js +2 -1
- package/dist/cli/commands/openclaw-install.js +2 -1
- package/dist/cli/commands/opencode-install.js +2 -1
- package/dist/cli/index.js +2 -1
- package/dist/integrations/cursor/hooks/cursor-hook-utils.js +9 -0
- package/dist/integrations/grok/grok-cli.js +2 -1
- package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/dist/reporting/log-parser.js +2 -2
- package/dist/skills/orchestrator/SKILL.md +19 -1
- package/llms.txt +8 -7
- package/package.json +2 -1
- package/scripts/foundry/CHANGELOG.md +33 -1
- package/scripts/foundry/README.md +12 -5
- package/scripts/foundry/assert-packed-dist-cli.mjs +6 -0
- package/scripts/foundry/blip-kapow.cjs +2 -0
- package/scripts/foundry/blip-looker.cjs +2 -0
- package/scripts/foundry/blip-render.cjs +2 -1237
- package/scripts/foundry/blip-rippel-organs.cjs +2 -0
- package/scripts/foundry/blip-rippel.cjs +2 -1963
- package/scripts/foundry/blip-scene.cjs +2 -0
- package/scripts/foundry/blip-stroke.cjs +2 -0
- package/scripts/foundry/blip-vibe.cjs +2 -0
- package/scripts/foundry/blip.mjs +23 -97
- package/scripts/foundry/cli.mjs +2 -2
- package/scripts/foundry/inspect.mjs +62 -19
- package/scripts/foundry/mill-plant.cjs +598 -0
- package/scripts/foundry/mint-suit.cjs +189 -99
- package/scripts/foundry/package.json +12 -1
- package/scripts/foundry/plant/agents/blip-inspect.yml +1 -1
- package/scripts/foundry/plant/agents/blip-looker.yml +5 -0
- package/scripts/foundry/plant/agents/blip-vibe.yml +5 -0
- package/scripts/foundry/plant/agents/blip.yml +1 -1
- package/scripts/foundry/plant/agents/inspect.yml +1 -1
- package/scripts/foundry/plant/agents/mill.yml +1 -1
- package/scripts/foundry/plant/agents/sound-inspect.yml +1 -1
- package/scripts/foundry/plant/agents/sound-mixer.yml +5 -0
- package/scripts/foundry/plant/agents/sound.yml +1 -1
- package/scripts/foundry/plant/motions/MOTION-REGISTRY.md +15 -9
- package/scripts/foundry/plant/motions/registry.json +5 -4
- package/scripts/foundry/plant/skills/blip/SKILL.md +4 -4
- package/scripts/foundry/plant/skills/blip-inspect/SKILL.md +2 -2
- package/scripts/foundry/plant/skills/blip-looker/SKILL.md +30 -0
- package/scripts/foundry/plant/skills/blip-vibe/SKILL.md +30 -0
- package/scripts/foundry/plant/skills/inspect/SKILL.md +2 -2
- package/scripts/foundry/plant/skills/sound/SKILL.md +3 -1
- package/scripts/foundry/plant/skills/sound-inspect/SKILL.md +1 -1
- package/scripts/foundry/plant/skills/sound-mixer/SKILL.md +28 -0
- package/scripts/foundry/sound-bed.cjs +2 -454
- package/scripts/foundry/sound-mixer.cjs +2 -0
- package/scripts/foundry/sound-rippel.cjs +2 -1434
- package/scripts/foundry/sound-taste.cjs +2 -0
- package/scripts/foundry/sound-voice.cjs +2 -0
- package/scripts/foundry/sound.mjs +23 -81
- package/scripts/node/install-bridges.cjs +39 -4
- package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/src/skills/orchestrator/SKILL.md +19 -1
- package/xray/features.json +2 -2
|
@@ -1,1434 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* SSOT (htafolla/rippel-synapse-flow@e5014cd):
|
|
5
|
-
* professionalTechnoInstruments.ts
|
|
6
|
-
* professionalPhonkInstruments.ts
|
|
7
|
-
* professionalJazzInstruments.ts (synthetic drums / sax fallback)
|
|
8
|
-
* professionalAmbientInstruments.ts (pad / keys envelopes)
|
|
9
|
-
* ProfessionalOrchestrator.ts (intro → body → outro)
|
|
10
|
-
* globalAudioManager.ts ProfessionalMixer
|
|
11
|
-
* getGenreConfig.ts / checksumService.ts (tempo tables)
|
|
12
|
-
*
|
|
13
|
-
* Tone.Offline is blocked in Node (no OfflineAudioContext). This is a lean headless
|
|
14
|
-
* port of those chains, then mixed like a mill — not the Lovable volume-hack soup,
|
|
15
|
-
* not the #62 three-sine bed.
|
|
16
|
-
*
|
|
17
|
-
* 4.44s is a Short, not a loop tile: hook → turn → tag on the motion grid.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
const TONE_OFFLINE_BLOCKER =
|
|
21
|
-
"Tone.Offline blocked in Node: Missing the native OfflineAudioContext constructor. Lean Node port of MembraneSynth / MetalSynth / mixer topology.";
|
|
22
|
-
|
|
23
|
-
const ENGINE = "rippel-headless";
|
|
24
|
-
const MIX = "crystal";
|
|
25
|
-
const TOPOLOGY = "membrane+metal+mixer";
|
|
26
|
-
const SSOT = {
|
|
27
|
-
repo: "htafolla/rippel-synapse-flow",
|
|
28
|
-
commit: "e5014cd46fbe5f132391333d8296f4416896dbee",
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/** Desktop (not mobile) construction numbers from the prototype. */
|
|
32
|
-
const RIPPEL = {
|
|
33
|
-
technoKick: {
|
|
34
|
-
pitchDecay: 0.08,
|
|
35
|
-
octaves: 10,
|
|
36
|
-
attack: 0.005,
|
|
37
|
-
decay: 0.5,
|
|
38
|
-
release: 0.8,
|
|
39
|
-
eq: { low: 6, mid: -2, high: -8 },
|
|
40
|
-
dist: 0.2,
|
|
41
|
-
comp: { thresholdDb: -20, ratio: 10 },
|
|
42
|
-
volDb: -6,
|
|
43
|
-
noteHz: 65.406, // C2 — generate-sequence kick
|
|
44
|
-
},
|
|
45
|
-
technoSnare: {
|
|
46
|
-
pitchDecay: 0.05,
|
|
47
|
-
octaves: 10,
|
|
48
|
-
attack: 0.005,
|
|
49
|
-
decay: 0.1,
|
|
50
|
-
release: 0.1,
|
|
51
|
-
bandpass: 4000,
|
|
52
|
-
dist: 0.8,
|
|
53
|
-
comp: { thresholdDb: -24, ratio: 6 },
|
|
54
|
-
volDb: -8,
|
|
55
|
-
},
|
|
56
|
-
technoHat: {
|
|
57
|
-
harmonicity: 5.1,
|
|
58
|
-
modulationIndex: 32,
|
|
59
|
-
resonance: 4000,
|
|
60
|
-
octaves: 1.5,
|
|
61
|
-
attack: 0.001,
|
|
62
|
-
decay: 0.05,
|
|
63
|
-
release: 0.02,
|
|
64
|
-
hp: 12000,
|
|
65
|
-
comp: { thresholdDb: -30, ratio: 4 },
|
|
66
|
-
volDb: -8,
|
|
67
|
-
},
|
|
68
|
-
technoBass: {
|
|
69
|
-
harmonicity: 1.005,
|
|
70
|
-
vibratoAmount: 0.1,
|
|
71
|
-
vibratoRate: 2,
|
|
72
|
-
attack: 0.01,
|
|
73
|
-
decay: 0.25,
|
|
74
|
-
sustain: 0.4,
|
|
75
|
-
release: 1.2,
|
|
76
|
-
lp: 250,
|
|
77
|
-
dist: 0.4,
|
|
78
|
-
eq: { low: 3, mid: -2, high: -6 },
|
|
79
|
-
comp: { thresholdDb: -24, ratio: 8 },
|
|
80
|
-
volDb: -8,
|
|
81
|
-
},
|
|
82
|
-
phonk808: {
|
|
83
|
-
pitchDecay: 0.12,
|
|
84
|
-
octaves: 16,
|
|
85
|
-
attack: 0.001,
|
|
86
|
-
decay: 1.2,
|
|
87
|
-
release: 2.0,
|
|
88
|
-
dist: 0.15,
|
|
89
|
-
eq: { low: 2, mid: -2, high: -8 },
|
|
90
|
-
hp: 30,
|
|
91
|
-
lp: 8000,
|
|
92
|
-
comp: { thresholdDb: -10, ratio: 4, attack: 0.003, release: 0.12 },
|
|
93
|
-
volDb: -3,
|
|
94
|
-
noteHz: 32.703, // C1
|
|
95
|
-
},
|
|
96
|
-
phonkCowbell: {
|
|
97
|
-
harmonicity: 5.1,
|
|
98
|
-
modulationIndex: 32,
|
|
99
|
-
resonance: 4000,
|
|
100
|
-
octaves: 1.5,
|
|
101
|
-
attack: 0.001,
|
|
102
|
-
decay: 0.15,
|
|
103
|
-
release: 0.3,
|
|
104
|
-
dist: 0.15,
|
|
105
|
-
eq: { high: 2, mid: 2, low: -10 },
|
|
106
|
-
lp: 8000,
|
|
107
|
-
volDb: -12,
|
|
108
|
-
},
|
|
109
|
-
phonkReese: {
|
|
110
|
-
attack: 0.01,
|
|
111
|
-
decay: 0.6,
|
|
112
|
-
sustain: 0.8,
|
|
113
|
-
release: 1.2,
|
|
114
|
-
dist: 0.2,
|
|
115
|
-
lp: 200,
|
|
116
|
-
lpQ: 1.5,
|
|
117
|
-
eq: { high: -8, mid: -2, low: 2 },
|
|
118
|
-
volDb: -6,
|
|
119
|
-
},
|
|
120
|
-
jazzKick: {
|
|
121
|
-
pitchDecay: 0.03,
|
|
122
|
-
octaves: 6,
|
|
123
|
-
attack: 0.001,
|
|
124
|
-
decay: 0.2,
|
|
125
|
-
release: 0.8,
|
|
126
|
-
volDb: -7,
|
|
127
|
-
},
|
|
128
|
-
jazzRide: {
|
|
129
|
-
harmonicity: 3.1,
|
|
130
|
-
modulationIndex: 16,
|
|
131
|
-
resonance: 2000,
|
|
132
|
-
octaves: 0.8,
|
|
133
|
-
attack: 0.001,
|
|
134
|
-
decay: 0.3,
|
|
135
|
-
release: 0.2,
|
|
136
|
-
},
|
|
137
|
-
jazzBrush: { attack: 0.001, decay: 0.15, release: 0.05 },
|
|
138
|
-
jazzSax: {
|
|
139
|
-
attack: 0.08,
|
|
140
|
-
decay: 0.2,
|
|
141
|
-
sustain: 0.7,
|
|
142
|
-
release: 0.6,
|
|
143
|
-
bp: 800,
|
|
144
|
-
bpQ: 1.2,
|
|
145
|
-
eq: { high: -2, mid: 4, low: -3 },
|
|
146
|
-
},
|
|
147
|
-
ambientPad: {
|
|
148
|
-
attack: 0.55, // 3.0s in the app; shortened so a 4s bed still has a body
|
|
149
|
-
decay: 1.2,
|
|
150
|
-
sustain: 0.85,
|
|
151
|
-
release: 1.6,
|
|
152
|
-
lp: 1200,
|
|
153
|
-
eq: { high: -8, mid: 3, low: 4 },
|
|
154
|
-
volDb: -5,
|
|
155
|
-
},
|
|
156
|
-
mixer: {
|
|
157
|
-
masterVolume: 0.7,
|
|
158
|
-
masterCompDb: -12,
|
|
159
|
-
masterEq: { high: 1.2, mid: 0, low: 0.8 },
|
|
160
|
-
limiterDb: -0.1,
|
|
161
|
-
},
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Prototype stacked limiters + 0.8 snare grit. The mill keeps the topology and
|
|
166
|
-
* tightens the mix: rumble HP, mud cut, milder saturation, sidechain, one limiter.
|
|
167
|
-
*/
|
|
168
|
-
const CRYSTAL = {
|
|
169
|
-
kickFloorHz: 28,
|
|
170
|
-
eightOhEightFloorHz: 24,
|
|
171
|
-
mudHz: 320,
|
|
172
|
-
mudDb: -3.5,
|
|
173
|
-
distScale: 0.55,
|
|
174
|
-
hatAirHz: 9000,
|
|
175
|
-
sidechain: 0.55,
|
|
176
|
-
peakTarget: 0.82,
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
const GENRE_ALIASES = {
|
|
180
|
-
ambient: "ambient",
|
|
181
|
-
techno: "techno",
|
|
182
|
-
jazz: "jazz",
|
|
183
|
-
phonk: "phonk",
|
|
184
|
-
destination: "ambient",
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
const TEMPO_TABLES = {
|
|
188
|
-
ambient: [70, 80, 90],
|
|
189
|
-
techno: [120, 128, 140],
|
|
190
|
-
jazz: [100, 110, 120],
|
|
191
|
-
phonk: [130, 140, 150],
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
const GENRES = {
|
|
195
|
-
ambient: {
|
|
196
|
-
bpm: 90,
|
|
197
|
-
voices: ["triangle-pad", "rhodes-keys", "membrane-sub", "metal-air", "mixer"],
|
|
198
|
-
},
|
|
199
|
-
techno: {
|
|
200
|
-
bpm: 128,
|
|
201
|
-
voices: ["membrane-kick", "metal-hat", "snare-clap", "duo-bass", "mixer"],
|
|
202
|
-
},
|
|
203
|
-
phonk: {
|
|
204
|
-
bpm: 150,
|
|
205
|
-
voices: ["membrane-808", "metal-hat", "metal-cowbell", "reese-bass", "mixer"],
|
|
206
|
-
},
|
|
207
|
-
jazz: {
|
|
208
|
-
bpm: 120,
|
|
209
|
-
voices: ["membrane-kick", "metal-ride", "brush-snare", "upright-bass", "sax-lead", "mixer"],
|
|
210
|
-
},
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
const SCALES = {
|
|
214
|
-
ambient: [261.63, 311.13, 349.23, 392.0, 466.16],
|
|
215
|
-
techno: [65.41, 87.31, 130.81, 174.61, 196.0],
|
|
216
|
-
phonk: [32.7, 43.65, 55.0, 65.41, 82.41],
|
|
217
|
-
jazz: [130.81, 164.81, 196.0, 220.0, 261.63],
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
function dbLin(db) {
|
|
221
|
-
return Math.pow(10, db / 20);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function resolveGenre(name) {
|
|
225
|
-
const raw = String(name || "ambient")
|
|
226
|
-
.trim()
|
|
227
|
-
.toLowerCase();
|
|
228
|
-
const id = GENRE_ALIASES[raw] || (GENRES[raw] ? raw : "ambient");
|
|
229
|
-
const spec = GENRES[id];
|
|
230
|
-
return { id, alias: raw !== id ? raw : null, bpm: spec.bpm, voices: [...spec.voices] };
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function tempoFromSeed(genreId, seedHex) {
|
|
234
|
-
const table = TEMPO_TABLES[genreId] || TEMPO_TABLES.ambient;
|
|
235
|
-
const hex = String(seedHex || "").replace(/^0x/, "");
|
|
236
|
-
const n = Number.parseInt(hex.slice(8, 16) || hex.slice(0, 8) || "1", 16);
|
|
237
|
-
const idx = Number.isFinite(n) ? n % table.length : 0;
|
|
238
|
-
return table[idx];
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/** Shared motion/audio grid. phase0=0 so t=0 is downbeat for both LFOs and the bed. */
|
|
242
|
-
function motionGrid(seedHex, genre) {
|
|
243
|
-
const g = resolveGenre(genre);
|
|
244
|
-
const bpm = tempoFromSeed(g.id, seedHex);
|
|
245
|
-
return {
|
|
246
|
-
genre: g.id,
|
|
247
|
-
bpm,
|
|
248
|
-
beatSec: 60 / bpm,
|
|
249
|
-
phase0: 0,
|
|
250
|
-
downbeat: 0,
|
|
251
|
-
e: 0.25,
|
|
252
|
-
and: 0.5,
|
|
253
|
-
a: 0.75,
|
|
254
|
-
syncopate: true,
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function ease01(a, b, x) {
|
|
259
|
-
if (b <= a) return x >= b ? 1 : 0;
|
|
260
|
-
const t = x < a ? 0 : x > b ? 1 : (x - a) / (b - a);
|
|
261
|
-
return t * t * (3 - 2 * t);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* 4.44s entertainment spine — YouTube-short form, not a pasted bar.
|
|
266
|
-
* hook (beats 0–2) → turn (2 → last ~1.1) → tag. Hits are one-shots, not strobe.
|
|
267
|
-
* hookEase/turnEase/tagEase are continuous weights (crossfade). Binary hook/turn/tag stay section flags.
|
|
268
|
-
*/
|
|
269
|
-
function shortformPhrase(t, seconds, grid) {
|
|
270
|
-
const dur = seconds > 0 ? seconds : 4.44;
|
|
271
|
-
const beatSec = (grid && grid.beatSec) || 60 / 90;
|
|
272
|
-
const beats = t / beatSec;
|
|
273
|
-
const total = dur / beatSec;
|
|
274
|
-
const hookEnd = 2;
|
|
275
|
-
const tagLen = Math.min(1.25, Math.max(0.85, total * 0.2));
|
|
276
|
-
const tagStart = Math.max(hookEnd + 1.6, total - tagLen);
|
|
277
|
-
let section = "tag";
|
|
278
|
-
if (beats < hookEnd) section = "hook";
|
|
279
|
-
else if (beats < tagStart) section = "turn";
|
|
280
|
-
function near(at) {
|
|
281
|
-
const d = Math.abs(beats - at);
|
|
282
|
-
return d < 0.14 ? 1 - d / 0.14 : 0;
|
|
283
|
-
}
|
|
284
|
-
const fade = 0.28;
|
|
285
|
-
const hookEase = 1 - ease01(hookEnd - fade, hookEnd + fade, beats);
|
|
286
|
-
const tagEase = ease01(tagStart - fade, tagStart + fade, beats);
|
|
287
|
-
const turnEase = Math.max(0, 1 - hookEase - tagEase);
|
|
288
|
-
return {
|
|
289
|
-
section,
|
|
290
|
-
beats,
|
|
291
|
-
total,
|
|
292
|
-
hook: section === "hook" ? 1 : 0,
|
|
293
|
-
turn: section === "turn" ? 1 : 0,
|
|
294
|
-
tag: section === "tag" ? 1 : 0,
|
|
295
|
-
hookEase,
|
|
296
|
-
turnEase,
|
|
297
|
-
tagEase,
|
|
298
|
-
turnHit: near(hookEnd),
|
|
299
|
-
tagHit: near(tagStart),
|
|
300
|
-
hookEndBeats: hookEnd,
|
|
301
|
-
tagStartBeats: tagStart,
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function padTones(genreId, scale) {
|
|
306
|
-
const s = scale || SCALES[genreId] || SCALES.ambient;
|
|
307
|
-
if (genreId === "ambient") return [s[0] * 0.75, s[0], s[1], s[3]];
|
|
308
|
-
if (genreId === "jazz") return [s[0], s[2], s[3], s[0] * 2];
|
|
309
|
-
if (genreId === "phonk") return [s[0], s[0] * 1.5, s[3]];
|
|
310
|
-
return [s[0], s[2], s[3]];
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function degreeLine(kind, count) {
|
|
314
|
-
if (kind === "bass") {
|
|
315
|
-
const cell = [0, 0, 3, 4];
|
|
316
|
-
return Array.from({ length: count }, (_, i) => cell[i % cell.length]);
|
|
317
|
-
}
|
|
318
|
-
if (kind === "rhodes") {
|
|
319
|
-
const cell = [0, 2, 4, 4, 2, 0];
|
|
320
|
-
return Array.from({ length: count }, (_, i) => cell[i % cell.length]);
|
|
321
|
-
}
|
|
322
|
-
const cell = [0, 0, 2, 3];
|
|
323
|
-
return Array.from({ length: count }, (_, i) => cell[i % cell.length]);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function walkDegree(last, len, rng) {
|
|
327
|
-
const dir = rng() < 0.5 ? -1 : 1;
|
|
328
|
-
const next = last + dir;
|
|
329
|
-
if (next < 0) return 0;
|
|
330
|
-
if (next >= len) return len - 1;
|
|
331
|
-
return next;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function gridTime(grid, beats) {
|
|
335
|
-
return (grid.phase0 || 0) + beats * grid.beatSec;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function mixInto(dest, src, start, gain) {
|
|
339
|
-
const offset = Math.max(0, start | 0);
|
|
340
|
-
const g = gain || 1;
|
|
341
|
-
for (let i = 0; i < src.length; i++) {
|
|
342
|
-
const j = offset + i;
|
|
343
|
-
if (j >= dest.length) break;
|
|
344
|
-
dest[j] += src[i] * g;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function expDecay(t, decay) {
|
|
349
|
-
if (decay <= 0) return t <= 0 ? 1 : 0;
|
|
350
|
-
if (t < 0) return 0;
|
|
351
|
-
return Math.exp((-5 * t) / decay);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function attackGain(t, attack) {
|
|
355
|
-
if (attack <= 0) return 1;
|
|
356
|
-
if (t >= attack) return 1;
|
|
357
|
-
if (t <= 0) return 0;
|
|
358
|
-
const x = t / attack;
|
|
359
|
-
return x * x;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function adsr(t, attack, decay, sustain, release, hold) {
|
|
363
|
-
const held = hold == null ? 0 : hold;
|
|
364
|
-
if (t < 0) return 0;
|
|
365
|
-
if (t < attack) return attackGain(t, attack);
|
|
366
|
-
if (t < attack + decay) {
|
|
367
|
-
const u = (t - attack) / Math.max(decay, 1e-6);
|
|
368
|
-
return 1 - (1 - sustain) * u;
|
|
369
|
-
}
|
|
370
|
-
if (t < attack + decay + held) return sustain;
|
|
371
|
-
const relT = t - attack - decay - held;
|
|
372
|
-
if (relT >= release) return 0;
|
|
373
|
-
return sustain * expDecay(relT, release);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function oneshotAmp(t, attack, decay) {
|
|
377
|
-
return attackGain(t, attack) * expDecay(Math.max(0, t - attack), decay);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function saw(phase) {
|
|
381
|
-
return 2 * ((phase / (2 * Math.PI)) % 1) - 1;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function triangle(phase) {
|
|
385
|
-
const p = ((phase / (2 * Math.PI)) % 1 + 1) % 1;
|
|
386
|
-
return p < 0.5 ? 4 * p - 1 : 3 - 4 * p;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function square(phase) {
|
|
390
|
-
return Math.sin(phase) >= 0 ? 1 : -1;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function polyBlep(t, dt) {
|
|
394
|
-
if (dt <= 0) return 0;
|
|
395
|
-
if (t < dt) {
|
|
396
|
-
const x = t / dt;
|
|
397
|
-
return x + x - x * x - 1;
|
|
398
|
-
}
|
|
399
|
-
if (t > 1 - dt) {
|
|
400
|
-
const x = (t - 1) / dt;
|
|
401
|
-
return x * x + x + x + 1;
|
|
402
|
-
}
|
|
403
|
-
return 0;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function sawBlep(phase, inc) {
|
|
407
|
-
const t = ((phase / (2 * Math.PI)) % 1 + 1) % 1;
|
|
408
|
-
const dt = Math.abs(inc) / (2 * Math.PI);
|
|
409
|
-
return 2 * t - 1 - polyBlep(t, dt);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function squareBlep(phase, inc) {
|
|
413
|
-
const t = ((phase / (2 * Math.PI)) % 1 + 1) % 1;
|
|
414
|
-
const dt = Math.abs(inc) / (2 * Math.PI);
|
|
415
|
-
const s = t < 0.5 ? 1 : -1;
|
|
416
|
-
return s + polyBlep(t, dt) - polyBlep((t + 0.5) % 1, dt);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function noiseLcg(seed) {
|
|
420
|
-
let s = (seed >>> 0) || 1;
|
|
421
|
-
return function next() {
|
|
422
|
-
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
|
|
423
|
-
return s / 4294967296;
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function biquadCoeffs(kind, freq, q, sampleRate, gainDb) {
|
|
428
|
-
const f = Math.max(20, Math.min((sampleRate || 44100) * 0.45, freq || 1000));
|
|
429
|
-
const sr = sampleRate || 44100;
|
|
430
|
-
const w0 = (2 * Math.PI * f) / sr;
|
|
431
|
-
const cos = Math.cos(w0);
|
|
432
|
-
const sin = Math.sin(w0);
|
|
433
|
-
const qq = Math.max(0.2, q || 0.707);
|
|
434
|
-
const alpha = sin / (2 * qq);
|
|
435
|
-
const a = Math.pow(10, (gainDb || 0) / 40);
|
|
436
|
-
let b0 = 1;
|
|
437
|
-
let b1 = 0;
|
|
438
|
-
let b2 = 0;
|
|
439
|
-
let a0 = 1;
|
|
440
|
-
let a1 = 0;
|
|
441
|
-
let a2 = 0;
|
|
442
|
-
if (kind === "highpass") {
|
|
443
|
-
b0 = (1 + cos) / 2;
|
|
444
|
-
b1 = -(1 + cos);
|
|
445
|
-
b2 = (1 + cos) / 2;
|
|
446
|
-
a0 = 1 + alpha;
|
|
447
|
-
a1 = -2 * cos;
|
|
448
|
-
a2 = 1 - alpha;
|
|
449
|
-
} else if (kind === "bandpass") {
|
|
450
|
-
b0 = alpha;
|
|
451
|
-
b1 = 0;
|
|
452
|
-
b2 = -alpha;
|
|
453
|
-
a0 = 1 + alpha;
|
|
454
|
-
a1 = -2 * cos;
|
|
455
|
-
a2 = 1 - alpha;
|
|
456
|
-
} else if (kind === "peaking") {
|
|
457
|
-
b0 = 1 + alpha * a;
|
|
458
|
-
b1 = -2 * cos;
|
|
459
|
-
b2 = 1 - alpha * a;
|
|
460
|
-
a0 = 1 + alpha / a;
|
|
461
|
-
a1 = -2 * cos;
|
|
462
|
-
a2 = 1 - alpha / a;
|
|
463
|
-
} else {
|
|
464
|
-
b0 = (1 - cos) / 2;
|
|
465
|
-
b1 = 1 - cos;
|
|
466
|
-
b2 = (1 - cos) / 2;
|
|
467
|
-
a0 = 1 + alpha;
|
|
468
|
-
a1 = -2 * cos;
|
|
469
|
-
a2 = 1 - alpha;
|
|
470
|
-
}
|
|
471
|
-
return { b0: b0 / a0, b1: b1 / a0, b2: b2 / a0, a1: a1 / a0, a2: a2 / a0 };
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function biquad(kind, freq, q, sampleRate, gainDb) {
|
|
475
|
-
const c = biquadCoeffs(kind, freq, q, sampleRate, gainDb);
|
|
476
|
-
let x1 = 0;
|
|
477
|
-
let x2 = 0;
|
|
478
|
-
let y1 = 0;
|
|
479
|
-
let y2 = 0;
|
|
480
|
-
return {
|
|
481
|
-
step(x) {
|
|
482
|
-
const y = c.b0 * x + c.b1 * x1 + c.b2 * x2 - c.a1 * y1 - c.a2 * y2;
|
|
483
|
-
x2 = x1;
|
|
484
|
-
x1 = x;
|
|
485
|
-
y2 = y1;
|
|
486
|
-
y1 = y;
|
|
487
|
-
return y;
|
|
488
|
-
},
|
|
489
|
-
};
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function highpass(cutoff, sampleRate) {
|
|
493
|
-
const rc = 1 / (2 * Math.PI * Math.max(40, cutoff));
|
|
494
|
-
const dt = 1 / sampleRate;
|
|
495
|
-
const a = rc / (rc + dt);
|
|
496
|
-
let prevX = 0;
|
|
497
|
-
let prevY = 0;
|
|
498
|
-
return {
|
|
499
|
-
step(x) {
|
|
500
|
-
const y = a * (prevY + x - prevX);
|
|
501
|
-
prevX = x;
|
|
502
|
-
prevY = y;
|
|
503
|
-
return y;
|
|
504
|
-
},
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
function lowpass(cutoff, sampleRate) {
|
|
509
|
-
const rc = 1 / (2 * Math.PI * Math.max(20, cutoff));
|
|
510
|
-
const dt = 1 / sampleRate;
|
|
511
|
-
const a = dt / (rc + dt);
|
|
512
|
-
let y = 0;
|
|
513
|
-
return {
|
|
514
|
-
step(x) {
|
|
515
|
-
y += a * (x - y);
|
|
516
|
-
return y;
|
|
517
|
-
},
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
function bandpass(cutoff, sampleRate) {
|
|
522
|
-
const hp = highpass(cutoff * 0.7, sampleRate);
|
|
523
|
-
const lp = lowpass(cutoff * 1.4, sampleRate);
|
|
524
|
-
return {
|
|
525
|
-
step(x) {
|
|
526
|
-
return lp.step(hp.step(x));
|
|
527
|
-
},
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function makeEq3(sampleRate) {
|
|
532
|
-
const lp = lowpass(250, sampleRate);
|
|
533
|
-
const hp = highpass(2500, sampleRate);
|
|
534
|
-
return {
|
|
535
|
-
step(x, lowDb, midDb, highDb) {
|
|
536
|
-
const l = lp.step(x);
|
|
537
|
-
const h = hp.step(x);
|
|
538
|
-
const m = x - l - h;
|
|
539
|
-
return l * dbLin(lowDb) + m * dbLin(midDb) + h * dbLin(highDb);
|
|
540
|
-
},
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
function distort(x, amount) {
|
|
545
|
-
const a = Math.max(0, amount) * CRYSTAL.distScale;
|
|
546
|
-
if (a <= 0.001) return x;
|
|
547
|
-
const k = 1 + a * 12;
|
|
548
|
-
return Math.tanh(x * k) / Math.tanh(k);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
function compress(samples, threshold, ratio, sampleRate, attackSec, releaseSec) {
|
|
552
|
-
const out = new Float64Array(samples.length);
|
|
553
|
-
let env = 0;
|
|
554
|
-
const atk = 1 - Math.exp(-1 / (Math.max(0.0008, attackSec || 0.004) * sampleRate));
|
|
555
|
-
const rel = 1 - Math.exp(-1 / (Math.max(0.02, releaseSec || 0.12) * sampleRate));
|
|
556
|
-
for (let i = 0; i < samples.length; i++) {
|
|
557
|
-
const x = Math.abs(samples[i]);
|
|
558
|
-
env += (x > env ? atk : rel) * (x - env);
|
|
559
|
-
const gain = env > threshold ? threshold / env + (1 - threshold / env) / ratio : 1;
|
|
560
|
-
out[i] = samples[i] * gain;
|
|
561
|
-
}
|
|
562
|
-
return out;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
function limiter(samples, ceiling) {
|
|
566
|
-
const out = new Float64Array(samples.length);
|
|
567
|
-
const cap = Math.max(0.2, ceiling);
|
|
568
|
-
for (let i = 0; i < samples.length; i++) {
|
|
569
|
-
const x = samples[i];
|
|
570
|
-
const a = Math.abs(x);
|
|
571
|
-
out[i] = a > cap ? Math.sign(x) * (cap + (a - cap) / (1 + (a - cap) * 8)) : x;
|
|
572
|
-
}
|
|
573
|
-
return out;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
function normalize(samples, peakTarget) {
|
|
577
|
-
let peak = 0;
|
|
578
|
-
for (let i = 0; i < samples.length; i++) {
|
|
579
|
-
const a = Math.abs(samples[i]);
|
|
580
|
-
if (a > peak) peak = a;
|
|
581
|
-
}
|
|
582
|
-
const scale = peak > 0 ? peakTarget / peak : 0;
|
|
583
|
-
const out = new Float64Array(samples.length);
|
|
584
|
-
for (let i = 0; i < samples.length; i++) out[i] = samples[i] * scale;
|
|
585
|
-
return out;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
function envelopeFollow(samples, sampleRate, attack, release) {
|
|
589
|
-
const out = new Float64Array(samples.length);
|
|
590
|
-
let env = 0;
|
|
591
|
-
const atk = 1 - Math.exp(-1 / (attack * sampleRate));
|
|
592
|
-
const rel = 1 - Math.exp(-1 / (release * sampleRate));
|
|
593
|
-
for (let i = 0; i < samples.length; i++) {
|
|
594
|
-
const x = Math.abs(samples[i]);
|
|
595
|
-
env += (x > env ? atk : rel) * (x - env);
|
|
596
|
-
out[i] = env;
|
|
597
|
-
}
|
|
598
|
-
return out;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function applyBus(samples, sampleRate, { hp, lp, eq, dist, comp, volDb }) {
|
|
602
|
-
const hpF = hp ? highpass(hp, sampleRate) : null;
|
|
603
|
-
const lpF = lp ? lowpass(lp, sampleRate) : null;
|
|
604
|
-
const mud = highpass(CRYSTAL.mudHz, sampleRate);
|
|
605
|
-
const eq3 = eq ? makeEq3(sampleRate) : null;
|
|
606
|
-
const out = new Float64Array(samples.length);
|
|
607
|
-
const mudKeep = dbLin(CRYSTAL.mudDb);
|
|
608
|
-
for (let i = 0; i < samples.length; i++) {
|
|
609
|
-
let x = samples[i];
|
|
610
|
-
if (hpF) x = hpF.step(x);
|
|
611
|
-
if (lpF) x = lpF.step(x);
|
|
612
|
-
const air = mud.step(x);
|
|
613
|
-
x = x * (1 - 0.28) + air * (0.28 + (1 - mudKeep) * 0.15);
|
|
614
|
-
if (eq3 && eq) x = eq3.step(x, eq.low || 0, eq.mid || 0, eq.high || 0);
|
|
615
|
-
if (dist) x = distort(x, dist);
|
|
616
|
-
out[i] = x * dbLin(volDb || 0);
|
|
617
|
-
}
|
|
618
|
-
if (comp) {
|
|
619
|
-
return compress(
|
|
620
|
-
out,
|
|
621
|
-
dbLin(comp.thresholdDb),
|
|
622
|
-
comp.ratio,
|
|
623
|
-
sampleRate,
|
|
624
|
-
comp.attack,
|
|
625
|
-
comp.release,
|
|
626
|
-
);
|
|
627
|
-
}
|
|
628
|
-
return out;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
/** Tone MembraneSynth: sine + exponential pitch drop, floored so it does not become DC. */
|
|
632
|
-
function renderMembrane({
|
|
633
|
-
sampleRate,
|
|
634
|
-
freq,
|
|
635
|
-
octaves,
|
|
636
|
-
pitchDecay,
|
|
637
|
-
attack,
|
|
638
|
-
decay,
|
|
639
|
-
release,
|
|
640
|
-
velocity,
|
|
641
|
-
floorHz,
|
|
642
|
-
shape,
|
|
643
|
-
}) {
|
|
644
|
-
const rel = release || 0.12;
|
|
645
|
-
const dur = attack + decay + rel;
|
|
646
|
-
const n = Math.floor(dur * sampleRate);
|
|
647
|
-
const out = new Float64Array(n);
|
|
648
|
-
const floor = floorHz || CRYSTAL.kickFloorHz;
|
|
649
|
-
const endFreq = Math.max(floor, freq * Math.pow(2, -octaves));
|
|
650
|
-
let phase = 0;
|
|
651
|
-
for (let i = 0; i < n; i++) {
|
|
652
|
-
const t = i / sampleRate;
|
|
653
|
-
const f =
|
|
654
|
-
t < pitchDecay ? freq * Math.pow(endFreq / freq, t / Math.max(pitchDecay, 1e-6)) : endFreq;
|
|
655
|
-
const inc = (2 * Math.PI * f) / sampleRate;
|
|
656
|
-
const osc = shape === "square" ? squareBlep(phase, inc) : Math.sin(phase);
|
|
657
|
-
const harm =
|
|
658
|
-
shape === "square" ? 0 : Math.sin(phase * 2) * Math.max(0, 1 - t / Math.max(pitchDecay, 1e-6)) * 0.32;
|
|
659
|
-
out[i] = velocity * oneshotAmp(t, attack, decay + rel * 0.45) * (osc + harm);
|
|
660
|
-
phase += inc;
|
|
661
|
-
}
|
|
662
|
-
return out;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
function kickClick(sampleRate, velocity) {
|
|
666
|
-
const n = Math.floor(0.007 * sampleRate);
|
|
667
|
-
const out = new Float64Array(n);
|
|
668
|
-
const hp = biquad("highpass", 1400, 0.7, sampleRate);
|
|
669
|
-
const noise = noiseLcg(0x51ed);
|
|
670
|
-
let phase = 0;
|
|
671
|
-
for (let i = 0; i < n; i++) {
|
|
672
|
-
const t = i / sampleRate;
|
|
673
|
-
const env = Math.exp(-t / 0.0022);
|
|
674
|
-
const click = 0.5 * Math.sin(phase) + 0.5 * (noise() * 2 - 1);
|
|
675
|
-
out[i] = velocity * env * hp.step(click);
|
|
676
|
-
phase += (2 * Math.PI * 2400) / sampleRate;
|
|
677
|
-
}
|
|
678
|
-
return out;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
/**
|
|
682
|
-
* Tone MetalSynth: FM square/square + high resonance, short env.
|
|
683
|
-
*/
|
|
684
|
-
function renderMetal({
|
|
685
|
-
sampleRate,
|
|
686
|
-
freq,
|
|
687
|
-
harmonicity,
|
|
688
|
-
modulationIndex,
|
|
689
|
-
resonance,
|
|
690
|
-
attack,
|
|
691
|
-
decay,
|
|
692
|
-
release,
|
|
693
|
-
velocity,
|
|
694
|
-
}) {
|
|
695
|
-
const rel = release || 0.02;
|
|
696
|
-
const dur = attack + decay + rel;
|
|
697
|
-
const n = Math.floor(dur * sampleRate);
|
|
698
|
-
const out = new Float64Array(n);
|
|
699
|
-
const hp = biquad("highpass", Math.max(800, resonance * 0.55), 0.7, sampleRate);
|
|
700
|
-
const bp = biquad("bandpass", Math.max(900, resonance), 5.2, sampleRate);
|
|
701
|
-
const noise = noiseLcg((freq * 100) | 0);
|
|
702
|
-
let carrierPhase = 0;
|
|
703
|
-
let modPhase = 0;
|
|
704
|
-
const modHz = freq * harmonicity;
|
|
705
|
-
for (let i = 0; i < n; i++) {
|
|
706
|
-
const t = i / sampleRate;
|
|
707
|
-
const mod = Math.sin(modPhase);
|
|
708
|
-
const carrier = Math.sin(carrierPhase + modulationIndex * 0.08 * mod);
|
|
709
|
-
const grit = noise() * 2 - 1;
|
|
710
|
-
const amp = velocity * oneshotAmp(t, attack, decay + rel);
|
|
711
|
-
out[i] = hp.step(bp.step(carrier * 0.74 + grit * 0.26) * amp);
|
|
712
|
-
carrierPhase += (2 * Math.PI * freq) / sampleRate;
|
|
713
|
-
modPhase += (2 * Math.PI * modHz) / sampleRate;
|
|
714
|
-
}
|
|
715
|
-
return out;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
function renderDuoBass({ sampleRate, freq, velocity, hold }) {
|
|
719
|
-
const p = RIPPEL.technoBass;
|
|
720
|
-
const dur = p.attack + p.decay + (hold || 0.18) + p.release * 0.5;
|
|
721
|
-
const n = Math.floor(dur * sampleRate);
|
|
722
|
-
const out = new Float64Array(n);
|
|
723
|
-
const lp0 = biquad("lowpass", 420, 0.85, sampleRate);
|
|
724
|
-
const lp1 = biquad("lowpass", 140, 0.8, sampleRate);
|
|
725
|
-
const lpA = biquad("lowpass", p.lp, 1.1, sampleRate);
|
|
726
|
-
const lpB = biquad("lowpass", p.lp, 0.9, sampleRate);
|
|
727
|
-
let ph0 = 0;
|
|
728
|
-
let ph1 = 0;
|
|
729
|
-
for (let i = 0; i < n; i++) {
|
|
730
|
-
const t = i / sampleRate;
|
|
731
|
-
const env = adsr(t, p.attack, p.decay, p.sustain, p.release, hold || 0.12);
|
|
732
|
-
const vib = 1 + p.vibratoAmount * 0.15 * Math.sin(2 * Math.PI * p.vibratoRate * t);
|
|
733
|
-
const inc0 = (2 * Math.PI * freq * vib) / sampleRate;
|
|
734
|
-
const inc1 = (2 * Math.PI * freq * p.harmonicity * vib) / sampleRate;
|
|
735
|
-
const v0 = lp0.step(sawBlep(ph0, inc0));
|
|
736
|
-
const v1 = lp1.step(triangle(ph1));
|
|
737
|
-
out[i] = velocity * env * lpB.step(lpA.step(0.62 * v0 + 0.38 * v1));
|
|
738
|
-
ph0 += inc0;
|
|
739
|
-
ph1 += inc1;
|
|
740
|
-
}
|
|
741
|
-
return out;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
function renderReese({ sampleRate, freq, velocity, hold }) {
|
|
745
|
-
const p = RIPPEL.phonkReese;
|
|
746
|
-
const dur = p.attack + p.decay + (hold || 0.25) + p.release * 0.4;
|
|
747
|
-
const n = Math.floor(dur * sampleRate);
|
|
748
|
-
const out = new Float64Array(n);
|
|
749
|
-
const lp = lowpass(p.lp, sampleRate);
|
|
750
|
-
let ph0 = 0;
|
|
751
|
-
let ph1 = 0;
|
|
752
|
-
for (let i = 0; i < n; i++) {
|
|
753
|
-
const t = i / sampleRate;
|
|
754
|
-
const env = adsr(t, p.attack, p.decay, p.sustain, p.release, hold || 0.2);
|
|
755
|
-
const mix = 0.5 * saw(ph0) + 0.5 * saw(ph1);
|
|
756
|
-
out[i] = velocity * env * lp.step(mix);
|
|
757
|
-
ph0 += (2 * Math.PI * freq) / sampleRate;
|
|
758
|
-
ph1 += (2 * Math.PI * freq * 1.01) / sampleRate;
|
|
759
|
-
}
|
|
760
|
-
return out;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
function renderCowbell({ sampleRate, velocity }) {
|
|
764
|
-
const p = RIPPEL.phonkCowbell;
|
|
765
|
-
return renderMetal({
|
|
766
|
-
sampleRate,
|
|
767
|
-
freq: 845,
|
|
768
|
-
harmonicity: p.harmonicity,
|
|
769
|
-
modulationIndex: p.modulationIndex,
|
|
770
|
-
resonance: p.resonance,
|
|
771
|
-
attack: p.attack,
|
|
772
|
-
decay: p.decay,
|
|
773
|
-
release: p.release,
|
|
774
|
-
velocity,
|
|
775
|
-
});
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
function renderBrush({ sampleRate, velocity, rng }) {
|
|
779
|
-
const p = RIPPEL.jazzBrush;
|
|
780
|
-
const n = Math.floor((p.attack + p.decay + p.release + 0.04) * sampleRate);
|
|
781
|
-
const out = new Float64Array(n);
|
|
782
|
-
const hp = highpass(1800, sampleRate);
|
|
783
|
-
const lp = lowpass(6500, sampleRate);
|
|
784
|
-
for (let i = 0; i < n; i++) {
|
|
785
|
-
const t = i / sampleRate;
|
|
786
|
-
const noise = rng() * 2 - 1;
|
|
787
|
-
out[i] = lp.step(hp.step(velocity * oneshotAmp(t, p.attack, p.decay) * noise));
|
|
788
|
-
}
|
|
789
|
-
return out;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
function renderSax({ sampleRate, freq, velocity, hold }) {
|
|
793
|
-
const p = RIPPEL.jazzSax;
|
|
794
|
-
const dur = p.attack + p.decay + (hold || 0.22) + p.release;
|
|
795
|
-
const n = Math.floor(dur * sampleRate);
|
|
796
|
-
const out = new Float64Array(n);
|
|
797
|
-
const bp = bandpass(p.bp, sampleRate);
|
|
798
|
-
const eq = makeEq3(sampleRate);
|
|
799
|
-
let phase = 0;
|
|
800
|
-
for (let i = 0; i < n; i++) {
|
|
801
|
-
const t = i / sampleRate;
|
|
802
|
-
const env = adsr(t, p.attack, p.decay, p.sustain, p.release, hold || 0.18);
|
|
803
|
-
const breath = (Math.sin(phase * 0.17) * 0.04 + 0.02) * (Math.sin(phase * 11.3) > 0 ? 1 : -1);
|
|
804
|
-
const tone = 0.82 * saw(phase) + 0.18 * triangle(phase * 2);
|
|
805
|
-
out[i] = eq.step(bp.step(velocity * env * (tone + breath)), p.eq.low, p.eq.mid, p.eq.high);
|
|
806
|
-
phase += (2 * Math.PI * freq) / sampleRate;
|
|
807
|
-
}
|
|
808
|
-
return out;
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
function renderRhodes({ sampleRate, freq, velocity }) {
|
|
812
|
-
const attack = 0.008;
|
|
813
|
-
const decay = 0.55;
|
|
814
|
-
const n = Math.floor(2.2 * sampleRate);
|
|
815
|
-
const out = new Float64Array(n);
|
|
816
|
-
const lp = biquad("lowpass", 2600, 0.8, sampleRate);
|
|
817
|
-
const hp = biquad("highpass", 220, 0.7, sampleRate);
|
|
818
|
-
const hammer = noiseLcg((freq * 17) | 0);
|
|
819
|
-
const tine = biquad("highpass", 1800, 0.8, sampleRate);
|
|
820
|
-
let phase = 0;
|
|
821
|
-
let mod = 0;
|
|
822
|
-
for (let i = 0; i < n; i++) {
|
|
823
|
-
const t = i / sampleRate;
|
|
824
|
-
const env = adsr(t, attack, decay, 0.38, 1.8, 0.04);
|
|
825
|
-
const index = 6.5 * Math.exp(-t / 0.22) + 0.35;
|
|
826
|
-
const fm = Math.sin(mod) * index;
|
|
827
|
-
const body = Math.sin(phase + fm);
|
|
828
|
-
const tick = tine.step((hammer() * 2 - 1) * Math.exp(-t / 0.012) * 0.22);
|
|
829
|
-
out[i] = hp.step(lp.step(velocity * env * (body + tick)));
|
|
830
|
-
phase += (2 * Math.PI * freq) / sampleRate;
|
|
831
|
-
mod += (2 * Math.PI * freq) / sampleRate;
|
|
832
|
-
}
|
|
833
|
-
return out;
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
function renderPad(n, sampleRate, freqs, rng, attack) {
|
|
837
|
-
const p = RIPPEL.ambientPad;
|
|
838
|
-
const out = new Float64Array(n);
|
|
839
|
-
const hp = biquad("highpass", 180, 0.7, sampleRate);
|
|
840
|
-
const eq = makeEq3(sampleRate);
|
|
841
|
-
const phases = freqs.map(() => rng() * Math.PI * 2);
|
|
842
|
-
const detune = freqs.map(() => 0.996 + rng() * 0.008);
|
|
843
|
-
const lfo = freqs.map(() => 0.09 + rng() * 0.16);
|
|
844
|
-
const tap = new Float64Array(Math.max(8, Math.floor(0.018 * sampleRate)));
|
|
845
|
-
let tapI = 0;
|
|
846
|
-
let cutoff = p.lp;
|
|
847
|
-
let lp = biquad("lowpass", cutoff, 0.9, sampleRate);
|
|
848
|
-
for (let i = 0; i < n; i++) {
|
|
849
|
-
const t = i / sampleRate;
|
|
850
|
-
if (i % 64 === 0) {
|
|
851
|
-
cutoff = p.lp * (0.82 + 0.18 * Math.sin(2 * Math.PI * 0.21 * t));
|
|
852
|
-
lp = biquad("lowpass", cutoff, 0.95, sampleRate);
|
|
853
|
-
}
|
|
854
|
-
const env = adsr(t, attack || p.attack, p.decay, p.sustain, p.release, Math.max(0, n / sampleRate - 2));
|
|
855
|
-
let mix = 0;
|
|
856
|
-
for (let v = 0; v < freqs.length; v++) {
|
|
857
|
-
const f = freqs[v] * detune[v];
|
|
858
|
-
const inc = (2 * Math.PI * f) / sampleRate;
|
|
859
|
-
const ph = phases[v] + 2 * Math.PI * f * t + 0.01 * Math.sin(2 * Math.PI * lfo[v] * t);
|
|
860
|
-
mix += 0.62 * sawBlep(ph, inc) + 0.38 * triangle(ph);
|
|
861
|
-
}
|
|
862
|
-
const dry = hp.step(lp.step((mix / freqs.length) * env));
|
|
863
|
-
const delayed = tap[tapI];
|
|
864
|
-
tap[tapI] = dry;
|
|
865
|
-
tapI = (tapI + 1) % tap.length;
|
|
866
|
-
out[i] = eq.step(dry * 0.82 + delayed * 0.18, p.eq.low, p.eq.mid, p.eq.high);
|
|
867
|
-
}
|
|
868
|
-
return out;
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
/** Moving air bed — not a sine drone. Pink-ish noise through a wandering bandpass. */
|
|
872
|
-
function renderAirGlue(n, sampleRate, rng, amount) {
|
|
873
|
-
const out = new Float64Array(n);
|
|
874
|
-
const hp = highpass(180, sampleRate);
|
|
875
|
-
const lp = lowpass(1400, sampleRate);
|
|
876
|
-
let b0 = 0;
|
|
877
|
-
let b1 = 0;
|
|
878
|
-
let b2 = 0;
|
|
879
|
-
const lfoHz = 0.17 + rng() * 0.11;
|
|
880
|
-
for (let i = 0; i < n; i++) {
|
|
881
|
-
const t = i / sampleRate;
|
|
882
|
-
const white = rng() * 2 - 1;
|
|
883
|
-
b0 = 0.997 * b0 + white * 0.04;
|
|
884
|
-
b1 = 0.985 * b1 + white * 0.05;
|
|
885
|
-
b2 = 0.95 * b2 + white * 0.1;
|
|
886
|
-
const pink = (b0 + b1 + b2) / 3;
|
|
887
|
-
const wobble = 0.7 + 0.3 * Math.sin(2 * Math.PI * lfoHz * t);
|
|
888
|
-
out[i] = lp.step(hp.step(pink)) * amount * wobble;
|
|
889
|
-
}
|
|
890
|
-
return out;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
function masterEnvelope(samples, sampleRate, seconds, lock) {
|
|
894
|
-
const blip = Boolean(lock) && seconds <= 5;
|
|
895
|
-
const introHold = blip ? 0 : 0.5;
|
|
896
|
-
const introEase = blip ? 0.03 : 0.35;
|
|
897
|
-
const fadeSec = blip ? 0.35 : 1;
|
|
898
|
-
const fadeStart = Math.max(seconds - fadeSec, blip ? seconds * 0.88 : 1.25);
|
|
899
|
-
const out = new Float64Array(samples.length);
|
|
900
|
-
for (let i = 0; i < samples.length; i++) {
|
|
901
|
-
const t = i / sampleRate;
|
|
902
|
-
let g = 1;
|
|
903
|
-
if (blip && t < introEase) {
|
|
904
|
-
g = t / introEase;
|
|
905
|
-
} else if (!blip && t < introHold) g = 0.46;
|
|
906
|
-
else if (!blip && t < introHold + introEase) {
|
|
907
|
-
const u = (t - introHold) / introEase;
|
|
908
|
-
g = 0.46 + 0.54 * (0.5 - 0.5 * Math.cos(Math.PI * u));
|
|
909
|
-
} else if (t >= fadeStart) {
|
|
910
|
-
const u = (t - fadeStart) / Math.max(seconds - fadeStart, 1e-6);
|
|
911
|
-
g = 1 - (0.5 - 0.5 * Math.cos(Math.PI * Math.min(1, u)));
|
|
912
|
-
}
|
|
913
|
-
out[i] = samples[i] * g;
|
|
914
|
-
}
|
|
915
|
-
return out;
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
function renderPlate(samples, sampleRate) {
|
|
919
|
-
const delays = [0.0297, 0.0371, 0.0411, 0.0437].map((s) => Math.max(2, Math.floor(s * sampleRate)));
|
|
920
|
-
const bufs = delays.map((d) => new Float64Array(d));
|
|
921
|
-
const idx = delays.map(() => 0);
|
|
922
|
-
const pre = Math.floor(0.012 * sampleRate);
|
|
923
|
-
const hp = biquad("highpass", 1500, 0.7, sampleRate);
|
|
924
|
-
const lp = biquad("lowpass", 7800, 0.8, sampleRate);
|
|
925
|
-
const out = new Float64Array(samples.length);
|
|
926
|
-
for (let i = 0; i < samples.length; i++) {
|
|
927
|
-
const x = i >= pre ? samples[i - pre] : 0;
|
|
928
|
-
let acc = 0;
|
|
929
|
-
for (let c = 0; c < bufs.length; c++) {
|
|
930
|
-
const d = bufs[c][idx[c]];
|
|
931
|
-
bufs[c][idx[c]] = x + d * 0.52;
|
|
932
|
-
idx[c] = (idx[c] + 1) % bufs[c].length;
|
|
933
|
-
acc += d;
|
|
934
|
-
}
|
|
935
|
-
out[i] = lp.step(hp.step(acc * 0.25));
|
|
936
|
-
}
|
|
937
|
-
return out;
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
function schedule(seconds, step, endPad) {
|
|
941
|
-
const times = [];
|
|
942
|
-
const end = seconds - (endPad || 0.1);
|
|
943
|
-
for (let t = 0; t < end; t += step) times.push(t);
|
|
944
|
-
return times;
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
function pick(arr, rng) {
|
|
948
|
-
return arr[Math.floor(rng() * arr.length) % arr.length];
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
function sectionGain(t, seconds, lock) {
|
|
952
|
-
const p = t / seconds;
|
|
953
|
-
if (lock && seconds <= 5) {
|
|
954
|
-
if (p > 0.88) return 0.72;
|
|
955
|
-
return 1;
|
|
956
|
-
}
|
|
957
|
-
if (p < 0.15) return 0.62;
|
|
958
|
-
if (p < 0.4) return 0.88;
|
|
959
|
-
if (p < 0.75) return 1;
|
|
960
|
-
if (p < 0.9) return 0.82;
|
|
961
|
-
return 0.55;
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
function renderRippelBed({ brief, genre, seconds, seedHex, rng, sampleRate, syncopate }) {
|
|
965
|
-
void brief;
|
|
966
|
-
const g = resolveGenre(genre);
|
|
967
|
-
const grid = motionGrid(seedHex, g.id);
|
|
968
|
-
g.bpm = grid.bpm;
|
|
969
|
-
const lock = Boolean(syncopate);
|
|
970
|
-
const n = Math.floor(sampleRate * seconds);
|
|
971
|
-
const kickBus = new Float64Array(n);
|
|
972
|
-
const hatBus = new Float64Array(n);
|
|
973
|
-
const colorBus = new Float64Array(n);
|
|
974
|
-
const padBus = new Float64Array(n);
|
|
975
|
-
const beat = grid.beatSec;
|
|
976
|
-
const scale = SCALES[g.id] || SCALES.ambient;
|
|
977
|
-
const hatSlip = (t) => (lock ? t : t + (rng() - 0.5) * 0.003);
|
|
978
|
-
|
|
979
|
-
if (g.id === "techno") {
|
|
980
|
-
const kick = RIPPEL.technoKick;
|
|
981
|
-
const hat = RIPPEL.technoHat;
|
|
982
|
-
const snare = RIPPEL.technoSnare;
|
|
983
|
-
for (const t of schedule(seconds, beat, 0.08)) {
|
|
984
|
-
const hit = renderMembrane({
|
|
985
|
-
sampleRate,
|
|
986
|
-
freq: kick.noteHz,
|
|
987
|
-
octaves: kick.octaves,
|
|
988
|
-
pitchDecay: kick.pitchDecay,
|
|
989
|
-
attack: kick.attack,
|
|
990
|
-
decay: kick.decay,
|
|
991
|
-
release: kick.release,
|
|
992
|
-
velocity: 0.92 * sectionGain(t, seconds, lock),
|
|
993
|
-
floorHz: CRYSTAL.kickFloorHz,
|
|
994
|
-
});
|
|
995
|
-
mixInto(kickBus, hit, Math.floor(t * sampleRate), 1);
|
|
996
|
-
mixInto(kickBus, kickClick(sampleRate, 0.22), Math.floor(t * sampleRate), 1);
|
|
997
|
-
}
|
|
998
|
-
for (const t of schedule(seconds, beat / 2, 0.04)) {
|
|
999
|
-
const off = Math.round(t / (beat / 2)) % 2 === 1;
|
|
1000
|
-
const hit = renderMetal({
|
|
1001
|
-
sampleRate,
|
|
1002
|
-
freq: 380,
|
|
1003
|
-
harmonicity: hat.harmonicity,
|
|
1004
|
-
modulationIndex: hat.modulationIndex,
|
|
1005
|
-
resonance: hat.resonance,
|
|
1006
|
-
attack: hat.attack,
|
|
1007
|
-
decay: hat.decay,
|
|
1008
|
-
release: hat.release,
|
|
1009
|
-
velocity: (off ? 0.34 : 0.2) * sectionGain(t, seconds, lock),
|
|
1010
|
-
});
|
|
1011
|
-
mixInto(hatBus, hit, Math.floor(hatSlip(t) * sampleRate), 1);
|
|
1012
|
-
}
|
|
1013
|
-
for (const t of schedule(seconds, beat * 2, 0.08)) {
|
|
1014
|
-
const at = t + beat;
|
|
1015
|
-
if (at >= seconds - 0.1) continue;
|
|
1016
|
-
const hit = renderMembrane({
|
|
1017
|
-
sampleRate,
|
|
1018
|
-
freq: 196,
|
|
1019
|
-
octaves: snare.octaves,
|
|
1020
|
-
pitchDecay: snare.pitchDecay,
|
|
1021
|
-
attack: snare.attack,
|
|
1022
|
-
decay: snare.decay,
|
|
1023
|
-
release: snare.release,
|
|
1024
|
-
velocity: 0.42,
|
|
1025
|
-
floorHz: 120,
|
|
1026
|
-
shape: "square",
|
|
1027
|
-
});
|
|
1028
|
-
const bp = bandpass(snare.bandpass, sampleRate);
|
|
1029
|
-
for (let i = 0; i < hit.length; i++) hit[i] = bp.step(hit[i]);
|
|
1030
|
-
mixInto(colorBus, hit, Math.floor(at * sampleRate), 0.85);
|
|
1031
|
-
}
|
|
1032
|
-
const technoLine = degreeLine("bass", 8);
|
|
1033
|
-
for (const t of schedule(seconds, beat, 0.12)) {
|
|
1034
|
-
const beatIdx = Math.round(t / beat);
|
|
1035
|
-
const bass = renderDuoBass({
|
|
1036
|
-
sampleRate,
|
|
1037
|
-
freq: scale[technoLine[beatIdx % technoLine.length] % scale.length],
|
|
1038
|
-
velocity: 0.42,
|
|
1039
|
-
hold: beat * 0.45,
|
|
1040
|
-
});
|
|
1041
|
-
mixInto(colorBus, bass, Math.floor(t * sampleRate), 1);
|
|
1042
|
-
}
|
|
1043
|
-
} else if (g.id === "phonk") {
|
|
1044
|
-
mixInto(padBus, renderPad(n, sampleRate, padTones("phonk", scale), rng, 0.35), 0, 0.22);
|
|
1045
|
-
const kick = RIPPEL.phonk808;
|
|
1046
|
-
const hat = RIPPEL.technoHat;
|
|
1047
|
-
for (const t of schedule(seconds, beat, 0.12)) {
|
|
1048
|
-
const hit = renderMembrane({
|
|
1049
|
-
sampleRate,
|
|
1050
|
-
freq: kick.noteHz,
|
|
1051
|
-
octaves: kick.octaves,
|
|
1052
|
-
pitchDecay: kick.pitchDecay,
|
|
1053
|
-
attack: kick.attack,
|
|
1054
|
-
decay: kick.decay,
|
|
1055
|
-
release: kick.release,
|
|
1056
|
-
velocity: 0.78 * sectionGain(t, seconds, lock),
|
|
1057
|
-
floorHz: CRYSTAL.eightOhEightFloorHz,
|
|
1058
|
-
});
|
|
1059
|
-
mixInto(kickBus, hit, Math.floor(t * sampleRate), 1);
|
|
1060
|
-
mixInto(kickBus, kickClick(sampleRate, 0.1), Math.floor(t * sampleRate), 1);
|
|
1061
|
-
}
|
|
1062
|
-
for (const t of schedule(seconds, beat / 2, 0.04)) {
|
|
1063
|
-
const hit = renderMetal({
|
|
1064
|
-
sampleRate,
|
|
1065
|
-
freq: 420,
|
|
1066
|
-
harmonicity: hat.harmonicity,
|
|
1067
|
-
modulationIndex: 36,
|
|
1068
|
-
resonance: 4500,
|
|
1069
|
-
attack: 0.001,
|
|
1070
|
-
decay: 0.055,
|
|
1071
|
-
release: 0.02,
|
|
1072
|
-
velocity: 0.18 * sectionGain(t, seconds, lock),
|
|
1073
|
-
});
|
|
1074
|
-
mixInto(hatBus, hit, Math.floor(hatSlip(t) * sampleRate), 1);
|
|
1075
|
-
}
|
|
1076
|
-
for (const t of schedule(seconds, beat, 0.08)) {
|
|
1077
|
-
mixInto(
|
|
1078
|
-
colorBus,
|
|
1079
|
-
renderCowbell({ sampleRate, velocity: 0.2 }),
|
|
1080
|
-
Math.floor((t + beat / 2) * sampleRate),
|
|
1081
|
-
1,
|
|
1082
|
-
);
|
|
1083
|
-
}
|
|
1084
|
-
const phonkLine = [0, 3];
|
|
1085
|
-
for (const t of schedule(seconds, beat * 2, 0.2)) {
|
|
1086
|
-
const beatIdx = Math.round(t / beat);
|
|
1087
|
-
const bass = renderReese({
|
|
1088
|
-
sampleRate,
|
|
1089
|
-
freq: scale[phonkLine[beatIdx % phonkLine.length] % scale.length],
|
|
1090
|
-
velocity: 0.38,
|
|
1091
|
-
hold: beat * 0.9,
|
|
1092
|
-
});
|
|
1093
|
-
mixInto(colorBus, bass, Math.floor((t + beat * 0.5) * sampleRate), 1);
|
|
1094
|
-
}
|
|
1095
|
-
} else if (g.id === "jazz") {
|
|
1096
|
-
mixInto(padBus, renderPad(n, sampleRate, padTones("jazz", scale), rng, 0.4), 0, 0.28);
|
|
1097
|
-
const kick = RIPPEL.jazzKick;
|
|
1098
|
-
const ride = RIPPEL.jazzRide;
|
|
1099
|
-
const swing = beat / 2 * 0.32;
|
|
1100
|
-
for (const t of schedule(seconds, beat, 0.1)) {
|
|
1101
|
-
const hit = renderMembrane({
|
|
1102
|
-
sampleRate,
|
|
1103
|
-
freq: 73.4,
|
|
1104
|
-
octaves: kick.octaves,
|
|
1105
|
-
pitchDecay: kick.pitchDecay,
|
|
1106
|
-
attack: kick.attack,
|
|
1107
|
-
decay: kick.decay,
|
|
1108
|
-
release: kick.release,
|
|
1109
|
-
velocity: 0.55 * sectionGain(t, seconds, lock),
|
|
1110
|
-
floorHz: 40,
|
|
1111
|
-
});
|
|
1112
|
-
mixInto(kickBus, hit, Math.floor(t * sampleRate), 1);
|
|
1113
|
-
}
|
|
1114
|
-
for (const t of schedule(seconds, beat / 2, 0.05)) {
|
|
1115
|
-
const eighth = Math.round(t / (beat / 2));
|
|
1116
|
-
const at = eighth % 2 === 1 ? t + swing : t;
|
|
1117
|
-
const hit = renderMetal({
|
|
1118
|
-
sampleRate,
|
|
1119
|
-
freq: 280,
|
|
1120
|
-
harmonicity: ride.harmonicity,
|
|
1121
|
-
modulationIndex: ride.modulationIndex,
|
|
1122
|
-
resonance: ride.resonance,
|
|
1123
|
-
attack: ride.attack,
|
|
1124
|
-
decay: ride.decay,
|
|
1125
|
-
release: ride.release,
|
|
1126
|
-
velocity: 0.18 * sectionGain(t, seconds, lock),
|
|
1127
|
-
});
|
|
1128
|
-
mixInto(hatBus, hit, Math.floor(at * sampleRate), 1);
|
|
1129
|
-
}
|
|
1130
|
-
for (const t of schedule(seconds, beat * 2, 0.1)) {
|
|
1131
|
-
mixInto(
|
|
1132
|
-
colorBus,
|
|
1133
|
-
renderBrush({ sampleRate, velocity: 0.28, rng }),
|
|
1134
|
-
Math.floor((t + beat) * sampleRate),
|
|
1135
|
-
1,
|
|
1136
|
-
);
|
|
1137
|
-
}
|
|
1138
|
-
let walk = 0;
|
|
1139
|
-
for (const t of schedule(seconds, beat, 0.12)) {
|
|
1140
|
-
walk = walkDegree(walk, scale.length, rng);
|
|
1141
|
-
const bass = renderDuoBass({
|
|
1142
|
-
sampleRate,
|
|
1143
|
-
freq: scale[walk] / 2,
|
|
1144
|
-
velocity: 0.34,
|
|
1145
|
-
hold: beat * 0.55,
|
|
1146
|
-
});
|
|
1147
|
-
mixInto(colorBus, bass, Math.floor(t * sampleRate), 0.85);
|
|
1148
|
-
}
|
|
1149
|
-
const saxCell = degreeLine("rhodes", 6);
|
|
1150
|
-
let saxI = 0;
|
|
1151
|
-
for (const t of schedule(seconds, beat * 2, 0.25)) {
|
|
1152
|
-
const deg = saxCell[saxI % saxCell.length];
|
|
1153
|
-
saxI += 1;
|
|
1154
|
-
mixInto(
|
|
1155
|
-
colorBus,
|
|
1156
|
-
renderSax({ sampleRate, freq: scale[deg % scale.length] * 2, velocity: 0.22, hold: beat * 0.8 }),
|
|
1157
|
-
Math.floor((t + beat * 0.25) * sampleRate),
|
|
1158
|
-
1,
|
|
1159
|
-
);
|
|
1160
|
-
}
|
|
1161
|
-
} else {
|
|
1162
|
-
const padFreqs = padTones("ambient", scale);
|
|
1163
|
-
mixInto(padBus, renderPad(n, sampleRate, padFreqs, rng, RIPPEL.ambientPad.attack), 0, dbLin(-8));
|
|
1164
|
-
const bassLine = degreeLine("bass", 8);
|
|
1165
|
-
const rhodesLine = degreeLine("rhodes", 8);
|
|
1166
|
-
let rhodesI = 0;
|
|
1167
|
-
let bassWalk = bassLine[0] % scale.length;
|
|
1168
|
-
const phrase0 = shortformPhrase(0, seconds, grid);
|
|
1169
|
-
const turnAt = phrase0.hookEndBeats * beat;
|
|
1170
|
-
const tagAt = phrase0.tagStartBeats * beat;
|
|
1171
|
-
for (const t of schedule(seconds, beat, 0.2)) {
|
|
1172
|
-
const phrase = shortformPhrase(t, seconds, grid);
|
|
1173
|
-
const lastKick = phrase.tagEase > 0.55 && phrase.beats + 1 >= phrase.total;
|
|
1174
|
-
const vel =
|
|
1175
|
-
(lastKick ? 0.92 : 0.7 + 0.16 * phrase.hookEase + 0.06 * phrase.turnEase) *
|
|
1176
|
-
sectionGain(t, seconds, lock);
|
|
1177
|
-
const hit = renderMembrane({
|
|
1178
|
-
sampleRate,
|
|
1179
|
-
freq: 36.7,
|
|
1180
|
-
octaves: 4.5,
|
|
1181
|
-
pitchDecay: 0.09,
|
|
1182
|
-
attack: 0.004,
|
|
1183
|
-
decay: 0.45,
|
|
1184
|
-
release: 0.6,
|
|
1185
|
-
velocity: vel,
|
|
1186
|
-
floorHz: CRYSTAL.kickFloorHz,
|
|
1187
|
-
});
|
|
1188
|
-
mixInto(kickBus, hit, Math.floor(t * sampleRate), 1);
|
|
1189
|
-
mixInto(kickBus, kickClick(sampleRate, lastKick ? 0.22 : 0.16), Math.floor(t * sampleRate), 1);
|
|
1190
|
-
}
|
|
1191
|
-
if (lock && turnAt < seconds - 0.05) {
|
|
1192
|
-
mixInto(
|
|
1193
|
-
hatBus,
|
|
1194
|
-
renderMetal({
|
|
1195
|
-
sampleRate,
|
|
1196
|
-
freq: 320,
|
|
1197
|
-
harmonicity: 5.1,
|
|
1198
|
-
modulationIndex: 28,
|
|
1199
|
-
resonance: 3200,
|
|
1200
|
-
attack: 0.003,
|
|
1201
|
-
decay: 0.22,
|
|
1202
|
-
release: 0.1,
|
|
1203
|
-
velocity: 0.48,
|
|
1204
|
-
}),
|
|
1205
|
-
Math.floor(turnAt * sampleRate),
|
|
1206
|
-
1,
|
|
1207
|
-
);
|
|
1208
|
-
mixInto(kickBus, kickClick(sampleRate, 0.2), Math.floor(turnAt * sampleRate), 1);
|
|
1209
|
-
}
|
|
1210
|
-
if (lock && tagAt < seconds - 0.05) {
|
|
1211
|
-
mixInto(
|
|
1212
|
-
colorBus,
|
|
1213
|
-
renderRhodes({ sampleRate, freq: scale[4 % scale.length] * 2, velocity: 0.46 }),
|
|
1214
|
-
Math.floor(tagAt * sampleRate),
|
|
1215
|
-
1,
|
|
1216
|
-
);
|
|
1217
|
-
mixInto(
|
|
1218
|
-
kickBus,
|
|
1219
|
-
renderMembrane({
|
|
1220
|
-
sampleRate,
|
|
1221
|
-
freq: 41.2,
|
|
1222
|
-
octaves: 4.2,
|
|
1223
|
-
pitchDecay: 0.08,
|
|
1224
|
-
attack: 0.003,
|
|
1225
|
-
decay: 0.38,
|
|
1226
|
-
release: 0.5,
|
|
1227
|
-
velocity: 0.8,
|
|
1228
|
-
floorHz: CRYSTAL.kickFloorHz,
|
|
1229
|
-
}),
|
|
1230
|
-
Math.floor(tagAt * sampleRate),
|
|
1231
|
-
1,
|
|
1232
|
-
);
|
|
1233
|
-
}
|
|
1234
|
-
for (const t of schedule(seconds, beat, 0.08)) {
|
|
1235
|
-
const at = lock ? t + beat * grid.and : t;
|
|
1236
|
-
if (at >= seconds - 0.08) continue;
|
|
1237
|
-
const phrase = shortformPhrase(at, seconds, grid);
|
|
1238
|
-
const hatW = 1 - 0.45 * phrase.tagEase;
|
|
1239
|
-
const hit = renderMetal({
|
|
1240
|
-
sampleRate,
|
|
1241
|
-
freq: 240,
|
|
1242
|
-
harmonicity: 4.8,
|
|
1243
|
-
modulationIndex: 22,
|
|
1244
|
-
resonance: 2800,
|
|
1245
|
-
attack: 0.004,
|
|
1246
|
-
decay: 0.18,
|
|
1247
|
-
release: 0.08,
|
|
1248
|
-
velocity: (lock ? 0.38 : 0.16) * hatW * sectionGain(t, seconds, lock),
|
|
1249
|
-
});
|
|
1250
|
-
mixInto(hatBus, hit, Math.floor(at * sampleRate), 1);
|
|
1251
|
-
}
|
|
1252
|
-
for (const t of schedule(seconds, beat, 0.12)) {
|
|
1253
|
-
const beatIdx = Math.round(t / beat);
|
|
1254
|
-
const phrase = shortformPhrase(t, seconds, grid);
|
|
1255
|
-
if (lock && phrase.hookEase > 0.55 && beatIdx % 4 === 1) continue;
|
|
1256
|
-
if (lock && phrase.tagEase > 0.55 && beatIdx % 2 === 1) continue;
|
|
1257
|
-
if (phrase.turnEase > 0.35) bassWalk = walkDegree(bassWalk, scale.length, rng);
|
|
1258
|
-
else bassWalk = bassLine[beatIdx % bassLine.length] % scale.length;
|
|
1259
|
-
const register = phrase.turnEase > 0.5 ? 1 : 2;
|
|
1260
|
-
mixInto(
|
|
1261
|
-
colorBus,
|
|
1262
|
-
renderDuoBass({
|
|
1263
|
-
sampleRate,
|
|
1264
|
-
freq: scale[bassWalk] / register,
|
|
1265
|
-
velocity: 0.36,
|
|
1266
|
-
hold: beat * 0.42,
|
|
1267
|
-
}),
|
|
1268
|
-
Math.floor(t * sampleRate),
|
|
1269
|
-
1,
|
|
1270
|
-
);
|
|
1271
|
-
}
|
|
1272
|
-
for (const t of schedule(seconds, beat * 2, 0.2)) {
|
|
1273
|
-
const beatIdx = Math.round(t / beat);
|
|
1274
|
-
const phrase = shortformPhrase(t + beat * (lock ? grid.a : 0.5), seconds, grid);
|
|
1275
|
-
if (lock && phrase.hookEase > 0.6) continue;
|
|
1276
|
-
if (phrase.tagEase > 0.6) continue;
|
|
1277
|
-
const deg = rhodesLine[rhodesI % rhodesLine.length];
|
|
1278
|
-
rhodesI += 1;
|
|
1279
|
-
mixInto(
|
|
1280
|
-
colorBus,
|
|
1281
|
-
renderRhodes({ sampleRate, freq: scale[deg % scale.length], velocity: 0.4 }),
|
|
1282
|
-
Math.floor((t + beat * (lock ? grid.a : 0.5)) * sampleRate),
|
|
1283
|
-
1,
|
|
1284
|
-
);
|
|
1285
|
-
void beatIdx;
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
const kickCh = applyBus(kickBus, sampleRate, {
|
|
1290
|
-
hp: g.id === "phonk" ? RIPPEL.phonk808.hp : 30,
|
|
1291
|
-
lp: g.id === "phonk" ? RIPPEL.phonk808.lp : 9000,
|
|
1292
|
-
eq: g.id === "phonk" ? RIPPEL.phonk808.eq : RIPPEL.technoKick.eq,
|
|
1293
|
-
dist: g.id === "phonk" ? RIPPEL.phonk808.dist : RIPPEL.technoKick.dist,
|
|
1294
|
-
comp: g.id === "phonk" ? RIPPEL.phonk808.comp : RIPPEL.technoKick.comp,
|
|
1295
|
-
volDb: g.id === "ambient" ? -4 : g.id === "jazz" ? RIPPEL.jazzKick.volDb : g.id === "phonk" ? RIPPEL.phonk808.volDb : RIPPEL.technoKick.volDb,
|
|
1296
|
-
});
|
|
1297
|
-
const hatCh = applyBus(hatBus, sampleRate, {
|
|
1298
|
-
hp: g.id === "techno" || g.id === "phonk" ? CRYSTAL.hatAirHz : 7200,
|
|
1299
|
-
lp: 16000,
|
|
1300
|
-
eq: { low: -10, mid: -2, high: 3 },
|
|
1301
|
-
dist: 0.04,
|
|
1302
|
-
comp: RIPPEL.technoHat.comp,
|
|
1303
|
-
volDb: RIPPEL.technoHat.volDb,
|
|
1304
|
-
});
|
|
1305
|
-
const colorCh = applyBus(colorBus, sampleRate, {
|
|
1306
|
-
hp: g.id === "ambient" ? 90 : 40,
|
|
1307
|
-
lp: g.id === "phonk" ? 7000 : 9000,
|
|
1308
|
-
eq: g.id === "techno" ? RIPPEL.technoBass.eq : g.id === "phonk" ? RIPPEL.phonkReese.eq : { low: -2, mid: 2, high: -3 },
|
|
1309
|
-
dist: g.id === "techno" ? RIPPEL.technoBass.dist : g.id === "phonk" ? RIPPEL.phonkReese.dist : 0.08,
|
|
1310
|
-
comp: RIPPEL.technoBass.comp,
|
|
1311
|
-
volDb: g.id === "techno" ? RIPPEL.technoBass.volDb : g.id === "phonk" ? RIPPEL.phonkReese.volDb : -8,
|
|
1312
|
-
});
|
|
1313
|
-
const padCh = applyBus(padBus, sampleRate, {
|
|
1314
|
-
hp: lock && g.id === "ambient" ? 180 : 90,
|
|
1315
|
-
lp: 6200,
|
|
1316
|
-
eq: { low: lock ? -5 : -2, mid: 2, high: -2 },
|
|
1317
|
-
dist: 0,
|
|
1318
|
-
volDb: -2,
|
|
1319
|
-
});
|
|
1320
|
-
|
|
1321
|
-
const kickEnv = envelopeFollow(kickCh, sampleRate, 0.003, 0.2);
|
|
1322
|
-
const glue = renderAirGlue(n, sampleRate, rng, g.id === "ambient" ? (lock ? 0.03 : 0.07) : g.id === "phonk" ? 0.04 : 0.028);
|
|
1323
|
-
const wetSrc = new Float64Array(n);
|
|
1324
|
-
for (let i = 0; i < n; i++) wetSrc[i] = hatCh[i] * 0.62 + colorCh[i] * 0.38;
|
|
1325
|
-
const plate = renderPlate(wetSrc, sampleRate);
|
|
1326
|
-
const mix = new Float64Array(n);
|
|
1327
|
-
const kickGain = g.id === "ambient" ? (lock ? 1 : 0.7) : 0.9;
|
|
1328
|
-
const hatGain = g.id === "techno" || g.id === "phonk" ? 0.72 : g.id === "ambient" ? 0.58 : 0.48;
|
|
1329
|
-
const padGain = g.id === "ambient" ? (lock ? 0.17 : 0.48) : g.id === "jazz" ? 0.36 : g.id === "phonk" ? 0.42 : 0.12;
|
|
1330
|
-
for (let i = 0; i < n; i++) {
|
|
1331
|
-
const duck = 1 / (1 + CRYSTAL.sidechain * kickEnv[i] * 7);
|
|
1332
|
-
mix[i] =
|
|
1333
|
-
kickCh[i] * kickGain +
|
|
1334
|
-
hatCh[i] * hatGain +
|
|
1335
|
-
colorCh[i] * 0.52 * duck +
|
|
1336
|
-
padCh[i] * padGain * duck +
|
|
1337
|
-
plate[i] * 0.1 +
|
|
1338
|
-
glue[i];
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
const masterEq = makeEq3(sampleRate);
|
|
1342
|
-
const mEq = RIPPEL.mixer.masterEq;
|
|
1343
|
-
for (let i = 0; i < n; i++) {
|
|
1344
|
-
mix[i] = masterEq.step(mix[i], mEq.low, mEq.mid, mEq.high) * RIPPEL.mixer.masterVolume;
|
|
1345
|
-
}
|
|
1346
|
-
let glued = compress(mix, dbLin(-16), g.id === "phonk" ? 5 : 2.6, sampleRate, 0.008, 0.16);
|
|
1347
|
-
const shaped = masterEnvelope(glued, sampleRate, seconds, lock);
|
|
1348
|
-
const peaked = normalize(shaped, CRYSTAL.peakTarget);
|
|
1349
|
-
const samples = limiter(peaked, dbLin(RIPPEL.mixer.limiterDb));
|
|
1350
|
-
|
|
1351
|
-
return {
|
|
1352
|
-
samples,
|
|
1353
|
-
sampleRate,
|
|
1354
|
-
seconds,
|
|
1355
|
-
seed: seedHex,
|
|
1356
|
-
genre: g,
|
|
1357
|
-
grid: { ...grid, syncopate: lock },
|
|
1358
|
-
engine: ENGINE,
|
|
1359
|
-
mix: MIX,
|
|
1360
|
-
ssot: SSOT,
|
|
1361
|
-
toneOffline: TONE_OFFLINE_BLOCKER,
|
|
1362
|
-
topology: TOPOLOGY,
|
|
1363
|
-
};
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
function bandEnergy(samples, sampleRate, loHz, hiHz) {
|
|
1367
|
-
const hp = highpass(loHz, sampleRate);
|
|
1368
|
-
const lp = lowpass(hiHz, sampleRate);
|
|
1369
|
-
let acc = 0;
|
|
1370
|
-
for (let i = 0; i < samples.length; i++) {
|
|
1371
|
-
const x = lp.step(hp.step(samples[i]));
|
|
1372
|
-
acc += x * x;
|
|
1373
|
-
}
|
|
1374
|
-
return Math.sqrt(acc / samples.length);
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
function highpassEnergy(samples, sampleRate, cutoffHz) {
|
|
1378
|
-
const hp = highpass(cutoffHz, sampleRate);
|
|
1379
|
-
let acc = 0;
|
|
1380
|
-
for (let i = 0; i < samples.length; i++) {
|
|
1381
|
-
const x = hp.step(samples[i]);
|
|
1382
|
-
acc += x * x;
|
|
1383
|
-
}
|
|
1384
|
-
return Math.sqrt(acc / samples.length);
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
function zeroCrossRate(samples) {
|
|
1388
|
-
let crosses = 0;
|
|
1389
|
-
for (let i = 1; i < samples.length; i++) {
|
|
1390
|
-
if (samples[i] === 0 || samples[i] * samples[i - 1] < 0) crosses += 1;
|
|
1391
|
-
}
|
|
1392
|
-
return crosses / samples.length;
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
function crestFactor(samples) {
|
|
1396
|
-
let peak = 0;
|
|
1397
|
-
let acc = 0;
|
|
1398
|
-
for (let i = 0; i < samples.length; i++) {
|
|
1399
|
-
const a = Math.abs(samples[i]);
|
|
1400
|
-
if (a > peak) peak = a;
|
|
1401
|
-
acc += samples[i] * samples[i];
|
|
1402
|
-
}
|
|
1403
|
-
const rms = Math.sqrt(acc / samples.length);
|
|
1404
|
-
return rms > 0 ? peak / rms : 0;
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
module.exports = {
|
|
1408
|
-
TONE_OFFLINE_BLOCKER,
|
|
1409
|
-
ENGINE,
|
|
1410
|
-
MIX,
|
|
1411
|
-
TOPOLOGY,
|
|
1412
|
-
SSOT,
|
|
1413
|
-
RIPPEL,
|
|
1414
|
-
CRYSTAL,
|
|
1415
|
-
GENRE_ALIASES,
|
|
1416
|
-
GENRES,
|
|
1417
|
-
SCALES,
|
|
1418
|
-
resolveGenre,
|
|
1419
|
-
tempoFromSeed,
|
|
1420
|
-
motionGrid,
|
|
1421
|
-
shortformPhrase,
|
|
1422
|
-
padTones,
|
|
1423
|
-
degreeLine,
|
|
1424
|
-
gridTime,
|
|
1425
|
-
renderRippelBed,
|
|
1426
|
-
renderMembrane,
|
|
1427
|
-
renderMetal,
|
|
1428
|
-
renderDuoBass,
|
|
1429
|
-
renderCowbell,
|
|
1430
|
-
bandEnergy,
|
|
1431
|
-
highpassEnergy,
|
|
1432
|
-
zeroCrossRate,
|
|
1433
|
-
crestFactor,
|
|
1434
|
-
};
|
|
1
|
+
/** Shim — mill drawers live in @0xray/blip. */
|
|
2
|
+
module.exports = require("@0xray/blip/sound-rippel");
|