@kernel.chat/kbot 3.52.0 → 3.54.0

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.
Files changed (41) hide show
  1. package/dist/agents/replit.js +1 -1
  2. package/dist/behaviour.d.ts +30 -0
  3. package/dist/behaviour.d.ts.map +1 -0
  4. package/dist/behaviour.js +191 -0
  5. package/dist/behaviour.js.map +1 -0
  6. package/dist/bootstrap.js +1 -1
  7. package/dist/bootstrap.js.map +1 -1
  8. package/dist/integrations/ableton-m4l.d.ts +124 -0
  9. package/dist/integrations/ableton-m4l.d.ts.map +1 -0
  10. package/dist/integrations/ableton-m4l.js +338 -0
  11. package/dist/integrations/ableton-m4l.js.map +1 -0
  12. package/dist/integrations/ableton-osc.d.ts.map +1 -1
  13. package/dist/integrations/ableton-osc.js +6 -2
  14. package/dist/integrations/ableton-osc.js.map +1 -1
  15. package/dist/music-learning.d.ts +181 -0
  16. package/dist/music-learning.d.ts.map +1 -0
  17. package/dist/music-learning.js +340 -0
  18. package/dist/music-learning.js.map +1 -0
  19. package/dist/skill-system.d.ts +68 -0
  20. package/dist/skill-system.d.ts.map +1 -0
  21. package/dist/skill-system.js +386 -0
  22. package/dist/skill-system.js.map +1 -0
  23. package/dist/tools/ableton.d.ts.map +1 -1
  24. package/dist/tools/ableton.js +24 -8
  25. package/dist/tools/ableton.js.map +1 -1
  26. package/dist/tools/arrangement-engine.d.ts +2 -0
  27. package/dist/tools/arrangement-engine.d.ts.map +1 -0
  28. package/dist/tools/arrangement-engine.js +644 -0
  29. package/dist/tools/arrangement-engine.js.map +1 -0
  30. package/dist/tools/index.d.ts.map +1 -1
  31. package/dist/tools/index.js +5 -0
  32. package/dist/tools/index.js.map +1 -1
  33. package/dist/tools/producer-engine.d.ts +71 -0
  34. package/dist/tools/producer-engine.d.ts.map +1 -0
  35. package/dist/tools/producer-engine.js +1859 -0
  36. package/dist/tools/producer-engine.js.map +1 -0
  37. package/dist/tools/sound-designer.d.ts +2 -0
  38. package/dist/tools/sound-designer.d.ts.map +1 -0
  39. package/dist/tools/sound-designer.js +896 -0
  40. package/dist/tools/sound-designer.js.map +1 -0
  41. package/package.json +3 -3
@@ -0,0 +1,1859 @@
1
+ // kbot Producer Engine — One-shot beat production + auto-mix
2
+ //
3
+ // The unified Producer + Sound Engineer engine. Given a genre (trap, drill,
4
+ // lofi, house, rnb, phonk, pluggnb, ambient), this engine:
5
+ //
6
+ // 1. Resolves all creative decisions from genre presets + randomization
7
+ // 2. Creates tracks, loads instruments, writes drums/bass/melody/chords/pads
8
+ // 3. Auto-mixes: volumes, panning, sends to return tracks
9
+ // 4. Fires all clips and starts playback
10
+ //
11
+ // One prompt. One tool call. One complete, mixed, playing beat.
12
+ //
13
+ // Uses existing primitives:
14
+ // - music-theory.ts: SCALES, CHORDS, parseProgression, voiceChord, etc.
15
+ // - ableton-osc.ts: ensureAbleton(), AbletonOSC send/query
16
+ // - index.ts: registerTool
17
+ import { registerTool } from './index.js';
18
+ import { ensureAbleton, formatAbletonError } from '../integrations/ableton-osc.js';
19
+ import { parseProgression, voiceChord, NAMED_PROGRESSIONS, GENRE_DRUM_PATTERNS, GM_DRUMS, getScaleNotes, quantizeToScale, } from './music-theory.js';
20
+ // ── Helpers ─────────────────────────────────────────────────────────────────
21
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
22
+ function randomInRange(min, max) {
23
+ return Math.floor(Math.random() * (max - min + 1)) + min;
24
+ }
25
+ function pick(arr) {
26
+ return arr[Math.floor(Math.random() * arr.length)];
27
+ }
28
+ function extractArgs(args) {
29
+ return args.map(a => {
30
+ if (a.type === 'b')
31
+ return '[blob]';
32
+ return a.value;
33
+ });
34
+ }
35
+ /**
36
+ * Parse a key string like "Cm", "F#m", "Bb", "Em" into root name and scale.
37
+ * Returns { root: "C", scale: "natural_minor" } or { root: "F#", scale: "natural_minor" }, etc.
38
+ */
39
+ function parseKeyString(keyStr) {
40
+ const match = keyStr.match(/^([A-Ga-g][#b]?)(m|min|minor)?$/i);
41
+ if (!match)
42
+ return { root: 'C', scale: 'natural_minor' };
43
+ const root = match[1].charAt(0).toUpperCase() + match[1].slice(1);
44
+ const isMinor = !!match[2];
45
+ return { root, scale: isMinor ? 'natural_minor' : 'major' };
46
+ }
47
+ // ── Genre Presets ───────────────────────────────────────────────────────────
48
+ export const GENRE_PRESETS = {
49
+ trap: {
50
+ id: 'trap',
51
+ name: 'Trap',
52
+ bpmRange: [138, 148],
53
+ preferredKeys: ['Cm', 'Em', 'Am', 'F#m', 'Dm'],
54
+ preferredScales: ['natural_minor', 'phrygian', 'harmonic_minor'],
55
+ timeSignature: [4, 4],
56
+ feel: 'halftime',
57
+ tracks: [
58
+ {
59
+ name: '808',
60
+ role: 'bass',
61
+ instrument: {
62
+ primary: 'Operator',
63
+ presetHint: 'Sine wave sub with pitch envelope. Algorithm 1, Op A sine, pitch env down 12st over 50ms for attack click.',
64
+ rolandCloud: 'TR-808 Bass Drum into Simpler, pitched',
65
+ rationale: 'Operator sine gives clean 808 sub. Pitch envelope gives the characteristic attack transient.',
66
+ },
67
+ midiContent: 'bass_line',
68
+ color: 1,
69
+ },
70
+ {
71
+ name: 'Kick',
72
+ role: 'drums',
73
+ instrument: {
74
+ primary: 'Drum Rack',
75
+ presetHint: 'Short punchy acoustic kick. High transient, fast decay. This provides the click/attack that the 808 lacks.',
76
+ rationale: 'Layered with 808 — this provides attack, 808 provides sustain.',
77
+ },
78
+ midiContent: 'drum_pattern',
79
+ color: 3,
80
+ },
81
+ {
82
+ name: 'Snare/Clap',
83
+ role: 'drums',
84
+ instrument: {
85
+ primary: 'Drum Rack',
86
+ presetHint: 'Layered snare + clap. C1 = snare, D1 = clap. Both trigger on beat 3 (half-time).',
87
+ rationale: 'Half-time snare is the anchor of trap rhythm.',
88
+ },
89
+ midiContent: 'drum_pattern',
90
+ color: 4,
91
+ },
92
+ {
93
+ name: 'Hi-Hats',
94
+ role: 'perc',
95
+ instrument: {
96
+ primary: 'Drum Rack',
97
+ presetHint: 'C1 = closed hat, D1 = open hat. 16th-note patterns with rolls on 32nds. Velocity variation 60-127.',
98
+ rolandCloud: 'TR-808 HiHat',
99
+ rationale: 'Trap hats are the genre signature — fast rolls, velocity ramps, open hat accents.',
100
+ },
101
+ midiContent: 'perc_pattern',
102
+ color: 5,
103
+ },
104
+ {
105
+ name: 'Melody',
106
+ role: 'melody',
107
+ instrument: {
108
+ primary: 'Wavetable',
109
+ presetHint: 'Dark bell/pluck. Short attack, medium decay, no sustain. Wavetable position swept slightly. Chorus for width.',
110
+ rolandCloud: 'JD-800 Digital Bell or JUPITER-8 Brass',
111
+ rationale: 'Trap melodies are sparse, dark, often bell-like or flute-like timbres.',
112
+ },
113
+ midiContent: 'melody',
114
+ color: 7,
115
+ },
116
+ {
117
+ name: 'Pad',
118
+ role: 'pad',
119
+ instrument: {
120
+ primary: 'Drift',
121
+ presetHint: 'Dark ambient pad. Slow attack (500ms+), long release. Low-pass filtered. Drift 40% for organic movement.',
122
+ rolandCloud: 'JUPITER-8 Pad',
123
+ rationale: 'Subtle background texture. Fills frequency space without competing with melody.',
124
+ },
125
+ midiContent: 'pad_chords',
126
+ color: 9,
127
+ },
128
+ ],
129
+ progressionStyle: {
130
+ namedProgressions: ['house_vamp', 'phrygian_dark', 'epic_film', 'andalusian'],
131
+ romanTemplates: ['i bVI bVII i', 'i bVII bVI V', 'i iv bVI bVII', 'i i bVI bVII'],
132
+ voicing: 'spread',
133
+ chordRhythm: 'whole',
134
+ barsPerSection: 4,
135
+ octave: 4,
136
+ },
137
+ drumStyle: {
138
+ basePattern: 'trap',
139
+ hihatVelocityCurve: 'crescendo_roll',
140
+ ghostNotes: false,
141
+ rollProbability: 0.3,
142
+ swing: 0,
143
+ layers: [
144
+ { instrument: 'rim', positions: [3, 11], velocity: 60, probability: 0.5 },
145
+ ],
146
+ },
147
+ mixTemplate: {
148
+ volumes: { bass: 0.80, drums: 0.75, 'drums.snare': 0.78, perc: 0.55, melody: 0.60, pad: 0.35 },
149
+ panning: { bass: 0, drums: 0, 'drums.snare': 0, perc: 0.05, melody: -0.10, pad: 0 },
150
+ sends: [
151
+ { fromRole: 'melody', toReturn: 0, level: 0.25 },
152
+ { fromRole: 'melody', toReturn: 1, level: 0.15 },
153
+ { fromRole: 'pad', toReturn: 0, level: 0.40 },
154
+ { fromRole: 'drums.snare', toReturn: 0, level: 0.20 },
155
+ ],
156
+ returns: [
157
+ { name: 'Reverb', device: 'Reverb', presetHint: 'Dark plate. Decay 2s. HP 200Hz on return. Predelay 20ms.' },
158
+ { name: 'Delay', device: 'Delay', presetHint: '1/4 note ping-pong. Feedback 30%. LP 3kHz on feedback. Dry/Wet 100% (send only).' },
159
+ ],
160
+ masterChain: ['EQ Eight (HP 30Hz)', 'Glue Compressor (2:1, attack 10ms, auto release, -2dB GR)', 'Limiter (ceiling -0.3dB)'],
161
+ targetLUFS: -14,
162
+ },
163
+ productionNotes: [
164
+ '808 IS the bass — tune it to the key. Glide between notes for slides.',
165
+ 'Half-time feel: snare on beat 3 only (position 8 in 16th grid).',
166
+ 'Hi-hat rolls: velocity ramps from 60 to 120 over 4-6 32nd notes.',
167
+ 'Melody: sparse, 4-8 notes per bar max. Leave space.',
168
+ 'Sidechain the 808 from the kick layer for clarity.',
169
+ 'Dark reverb on the snare — long tail, HP filtered.',
170
+ ],
171
+ },
172
+ drill: {
173
+ id: 'drill',
174
+ name: 'Drill',
175
+ bpmRange: [138, 145],
176
+ preferredKeys: ['Cm', 'Bbm', 'F#m', 'Gm'],
177
+ preferredScales: ['natural_minor', 'harmonic_minor', 'phrygian'],
178
+ timeSignature: [4, 4],
179
+ feel: 'halftime',
180
+ tracks: [
181
+ {
182
+ name: '808',
183
+ role: 'bass',
184
+ instrument: {
185
+ primary: 'Operator',
186
+ presetHint: 'Slide 808. Glide ON, glide time 80ms. Sine sub with saturation. Portamento for the signature drill slides.',
187
+ rationale: 'Drill 808s MUST slide. Glide/portamento is non-negotiable.',
188
+ },
189
+ midiContent: 'bass_line',
190
+ color: 1,
191
+ },
192
+ {
193
+ name: 'Drums',
194
+ role: 'drums',
195
+ instrument: {
196
+ primary: 'Drum Rack',
197
+ presetHint: 'UK drill kit: punchy kick, tight snare, rimshot on the ghost notes. Kick and snare in same rack for choke interaction.',
198
+ rationale: 'Drill drums are tight and punchy, displaced from the grid for that sliding feel.',
199
+ },
200
+ midiContent: 'drum_pattern',
201
+ color: 3,
202
+ },
203
+ {
204
+ name: 'Hi-Hats',
205
+ role: 'perc',
206
+ instrument: {
207
+ primary: 'Drum Rack',
208
+ presetHint: 'Fast closed hats with triplet rolls. Similar to trap but with more displaced rhythms.',
209
+ rationale: 'Drill hats borrow from trap but add more syncopation and triplet feel.',
210
+ },
211
+ midiContent: 'perc_pattern',
212
+ color: 5,
213
+ },
214
+ {
215
+ name: 'Melody',
216
+ role: 'melody',
217
+ instrument: {
218
+ primary: 'Wavetable',
219
+ presetHint: 'Dark piano or string stab. Minor key, haunting. Could also use Simpler with a piano sample.',
220
+ rolandCloud: 'JD-800 Dark Piano',
221
+ rationale: 'Drill melodies are dark, minor, often piano or orchestral.',
222
+ },
223
+ midiContent: 'melody',
224
+ color: 7,
225
+ },
226
+ {
227
+ name: 'Strings',
228
+ role: 'pad',
229
+ instrument: {
230
+ primary: 'Wavetable',
231
+ presetHint: 'Dark orchestral strings. Slow attack, sustained. Creates the cinematic drill atmosphere.',
232
+ rationale: 'Strings are signature drill texture — UK drill especially.',
233
+ },
234
+ midiContent: 'pad_chords',
235
+ color: 9,
236
+ },
237
+ ],
238
+ progressionStyle: {
239
+ namedProgressions: ['andalusian', 'phrygian_dark', 'epic_film'],
240
+ romanTemplates: ['i bVII bVI V', 'i bII bVII i', 'i iv v i'],
241
+ voicing: 'drop2',
242
+ chordRhythm: 'whole',
243
+ barsPerSection: 4,
244
+ octave: 3,
245
+ },
246
+ drumStyle: {
247
+ basePattern: 'drill',
248
+ hihatVelocityCurve: 'crescendo_roll',
249
+ ghostNotes: true,
250
+ rollProbability: 0.25,
251
+ swing: 0,
252
+ layers: [
253
+ { instrument: 'rim', positions: [3, 7, 11, 15], velocity: 50, probability: 0.4 },
254
+ ],
255
+ },
256
+ mixTemplate: {
257
+ volumes: { bass: 0.82, drums: 0.75, perc: 0.50, melody: 0.58, pad: 0.30 },
258
+ panning: { bass: 0, drums: 0, perc: 0.05, melody: 0, pad: 0 },
259
+ sends: [
260
+ { fromRole: 'melody', toReturn: 0, level: 0.20 },
261
+ { fromRole: 'pad', toReturn: 0, level: 0.35 },
262
+ { fromRole: 'drums', toReturn: 0, level: 0.10 },
263
+ ],
264
+ returns: [
265
+ { name: 'Reverb', device: 'Hybrid Reverb', presetHint: 'Dark Hall algorithm. Decay 3s. HP 250Hz.' },
266
+ { name: 'Delay', device: 'Echo', presetHint: 'Tape delay. 1/4 dotted. Noise + modulation for grit.' },
267
+ ],
268
+ masterChain: ['EQ Eight (HP 30Hz)', 'Glue Compressor (2:1, auto release)', 'Limiter (ceiling -0.3dB)'],
269
+ targetLUFS: -14,
270
+ },
271
+ productionNotes: [
272
+ 'SLIDES ARE EVERYTHING. Use glide/portamento on the 808.',
273
+ 'Displaced snare: not on 2 and 4, but on the and-of-2 and and-of-4.',
274
+ 'Hi-hat triplet rolls are more prominent than trap.',
275
+ 'Dark, cinematic strings in the background.',
276
+ 'Bass note patterns: lots of octave jumps with slides between.',
277
+ ],
278
+ },
279
+ lofi: {
280
+ id: 'lofi',
281
+ name: 'Lo-Fi Hip-Hop',
282
+ bpmRange: [72, 86],
283
+ preferredKeys: ['C', 'F', 'Bb', 'Eb', 'Ab'],
284
+ preferredScales: ['major', 'dorian', 'mixolydian'],
285
+ timeSignature: [4, 4],
286
+ feel: 'swing',
287
+ tracks: [
288
+ {
289
+ name: 'Drums',
290
+ role: 'drums',
291
+ instrument: {
292
+ primary: 'Drum Rack',
293
+ presetHint: 'Vintage/dusty samples. Bit-crushed slightly. SP-404 aesthetic. Boom-bap pattern with swing.',
294
+ rationale: 'Lo-fi drums should sound like they came off a cassette tape.',
295
+ },
296
+ midiContent: 'drum_pattern',
297
+ color: 3,
298
+ },
299
+ {
300
+ name: 'Bass',
301
+ role: 'bass',
302
+ instrument: {
303
+ primary: 'Analog',
304
+ presetHint: 'Warm, round sub bass. Low-pass filtered at 400Hz. Slight saturation for warmth.',
305
+ rationale: 'Analog gives the warmest sub. Keep it simple and deep.',
306
+ },
307
+ midiContent: 'bass_line',
308
+ color: 1,
309
+ },
310
+ {
311
+ name: 'Keys',
312
+ role: 'harmony',
313
+ instrument: {
314
+ primary: 'Electric',
315
+ presetHint: 'Rhodes tone. Magnetic pickup, mid position. Warm and slightly detuned. THE signature lo-fi instrument.',
316
+ rolandCloud: 'RD-88 Vintage Rhodes',
317
+ rationale: 'Electric piano is the soul of lo-fi. Jazzy extended chords.',
318
+ },
319
+ midiContent: 'chord_progression',
320
+ color: 7,
321
+ },
322
+ {
323
+ name: 'Guitar',
324
+ role: 'melody',
325
+ instrument: {
326
+ primary: 'Wavetable',
327
+ presetHint: 'Jazz guitar sample, chopped. Warp mode: Texture. Filtered, warm. Could be a Nujabes-style sample chop.',
328
+ rationale: 'Sampled guitar gives authenticity. Wavetable as fallback for pluck/guitar tone.',
329
+ },
330
+ midiContent: 'melody',
331
+ color: 5,
332
+ },
333
+ {
334
+ name: 'Vinyl',
335
+ role: 'fx',
336
+ instrument: {
337
+ primary: 'Simpler',
338
+ presetHint: 'Vinyl crackle loop. One-shot mode, looped. Very low volume — texture only.',
339
+ rationale: 'Vinyl noise is essential lo-fi texture.',
340
+ },
341
+ midiContent: 'none',
342
+ color: 11,
343
+ },
344
+ ],
345
+ progressionStyle: {
346
+ namedProgressions: ['jazz_ii_v_i', 'neo_soul', 'jazz_turnaround', 'bossa_nova'],
347
+ romanTemplates: ['Imaj7 vi7 ii7 V7', 'ii7 V7 Imaj7 IVmaj7', 'Imaj7 iii7 vi7 V7'],
348
+ voicing: 'drop2',
349
+ chordRhythm: 'half',
350
+ barsPerSection: 4,
351
+ octave: 4,
352
+ },
353
+ drumStyle: {
354
+ basePattern: 'lofi',
355
+ hihatVelocityCurve: 'accent_downbeat',
356
+ ghostNotes: true,
357
+ rollProbability: 0,
358
+ swing: 65,
359
+ layers: [
360
+ { instrument: 'rim', positions: [3, 11], velocity: 50, probability: 0.6 },
361
+ { instrument: 'shaker', positions: [0, 2, 4, 6, 8, 10, 12, 14], velocity: 35, probability: 0.4 },
362
+ ],
363
+ },
364
+ mixTemplate: {
365
+ volumes: { drums: 0.70, bass: 0.72, harmony: 0.65, melody: 0.55, fx: 0.15 },
366
+ panning: { drums: 0, bass: 0, harmony: 0.05, melody: -0.15, fx: 0 },
367
+ sends: [
368
+ { fromRole: 'harmony', toReturn: 0, level: 0.25 },
369
+ { fromRole: 'melody', toReturn: 0, level: 0.30 },
370
+ { fromRole: 'melody', toReturn: 1, level: 0.15 },
371
+ { fromRole: 'drums', toReturn: 0, level: 0.10 },
372
+ ],
373
+ returns: [
374
+ { name: 'Reverb', device: 'Reverb', presetHint: 'Warm room. Decay 1.2s. High damp 3kHz. Low diffusion.' },
375
+ { name: 'Delay', device: 'Echo', presetHint: 'Tape echo. 1/8 note. Noise 30%. Modulation for wobble. Ducking ON.' },
376
+ ],
377
+ masterChain: [
378
+ 'EQ Eight (LP 15kHz gentle roll-off for lo-fi character, HP 35Hz)',
379
+ 'Glue Compressor (2:1, slow attack, -2dB GR)',
380
+ 'Redux (bit depth 12, downsample slight for texture)',
381
+ 'Limiter (ceiling -0.5dB)',
382
+ ],
383
+ targetLUFS: -16,
384
+ },
385
+ productionNotes: [
386
+ 'Swing is MANDATORY. 60-70% swing on drums.',
387
+ 'Everything should sound slightly detuned and warm.',
388
+ 'Extended jazz chords: maj7, m7, 9ths, add9.',
389
+ 'Master chain: subtle bit reduction + LP filter for tape character.',
390
+ 'Vinyl crackle layer at very low volume for ambience.',
391
+ 'Keep it mellow — if anything sounds aggressive, filter it down.',
392
+ ],
393
+ },
394
+ house: {
395
+ id: 'house',
396
+ name: 'House',
397
+ bpmRange: [122, 128],
398
+ preferredKeys: ['C', 'F', 'G', 'Am', 'Dm'],
399
+ preferredScales: ['major', 'dorian', 'mixolydian'],
400
+ timeSignature: [4, 4],
401
+ feel: 'straight',
402
+ tracks: [
403
+ {
404
+ name: 'Kick',
405
+ role: 'drums',
406
+ instrument: {
407
+ primary: 'Drum Rack',
408
+ presetHint: 'Punchy house kick. Four-on-the-floor. Transient shaping for punch.',
409
+ rolandCloud: 'TR-909 Kick',
410
+ rationale: '909-style kick is the foundation of house.',
411
+ },
412
+ midiContent: 'drum_pattern',
413
+ color: 3,
414
+ },
415
+ {
416
+ name: 'Hats/Perc',
417
+ role: 'perc',
418
+ instrument: {
419
+ primary: 'Drum Rack',
420
+ presetHint: 'Crisp closed hats on 8ths, open hats on offbeats. Shaker layer.',
421
+ rolandCloud: 'TR-909 HiHat',
422
+ rationale: 'Classic house hat pattern with offbeat opens.',
423
+ },
424
+ midiContent: 'perc_pattern',
425
+ color: 5,
426
+ },
427
+ {
428
+ name: 'Clap',
429
+ role: 'drums',
430
+ instrument: {
431
+ primary: 'Drum Rack',
432
+ presetHint: 'Tight clap on 2 and 4. Layered with subtle snare for body.',
433
+ rationale: 'Clap drives the backbeat in house.',
434
+ },
435
+ midiContent: 'drum_pattern',
436
+ color: 4,
437
+ },
438
+ {
439
+ name: 'Bass',
440
+ role: 'bass',
441
+ instrument: {
442
+ primary: 'Analog',
443
+ presetHint: 'Funky bass line. Saw wave, low-pass filtered with envelope. Groovy, syncopated.',
444
+ rolandCloud: 'SH-101 Bass',
445
+ rationale: 'SH-101 style mono bass is classic house.',
446
+ },
447
+ midiContent: 'bass_line',
448
+ color: 1,
449
+ },
450
+ {
451
+ name: 'Chords',
452
+ role: 'harmony',
453
+ instrument: {
454
+ primary: 'Electric',
455
+ presetHint: 'Stab chords. Rhodes or organ-like. Pumping from sidechain.',
456
+ rolandCloud: 'JUNO-106 Pad',
457
+ rationale: 'Warm chord stabs that pump with the kick.',
458
+ },
459
+ midiContent: 'chord_progression',
460
+ color: 7,
461
+ },
462
+ {
463
+ name: 'Pad',
464
+ role: 'pad',
465
+ instrument: {
466
+ primary: 'Wavetable',
467
+ presetHint: 'Lush filtered pad. Slow LFO on filter cutoff. Wide stereo. Background wash.',
468
+ rationale: 'Fills the frequency spectrum behind the chords.',
469
+ },
470
+ midiContent: 'pad_chords',
471
+ color: 9,
472
+ },
473
+ ],
474
+ progressionStyle: {
475
+ namedProgressions: ['jazz_ii_v_i', 'house_vamp', 'dorian_vamp', 'neo_soul'],
476
+ romanTemplates: ['i bVII bVI bVII', 'ii7 V7 Imaj7', 'vi IV I V'],
477
+ voicing: 'open',
478
+ chordRhythm: 'quarter',
479
+ barsPerSection: 8,
480
+ octave: 4,
481
+ },
482
+ drumStyle: {
483
+ basePattern: 'house',
484
+ hihatVelocityCurve: 'accent_downbeat',
485
+ ghostNotes: false,
486
+ rollProbability: 0,
487
+ swing: 0,
488
+ layers: [
489
+ { instrument: 'tambourine', positions: [2, 6, 10, 14], velocity: 45, probability: 0.7 },
490
+ { instrument: 'shaker', positions: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], velocity: 30, probability: 0.5 },
491
+ ],
492
+ },
493
+ mixTemplate: {
494
+ volumes: { drums: 0.78, 'drums.clap': 0.72, perc: 0.50, bass: 0.75, harmony: 0.55, pad: 0.35 },
495
+ panning: { drums: 0, 'drums.clap': 0, perc: 0.10, bass: 0, harmony: 0, pad: 0 },
496
+ sends: [
497
+ { fromRole: 'harmony', toReturn: 0, level: 0.20 },
498
+ { fromRole: 'pad', toReturn: 0, level: 0.35 },
499
+ { fromRole: 'perc', toReturn: 0, level: 0.10 },
500
+ { fromRole: 'harmony', toReturn: 1, level: 0.15 },
501
+ ],
502
+ returns: [
503
+ { name: 'Reverb', device: 'Reverb', presetHint: 'Plate reverb. Decay 1.8s. HP 200Hz. Bright and clean.' },
504
+ { name: 'Delay', device: 'Delay', presetHint: 'Ping-pong 1/8 note. Feedback 25%. LP 4kHz.' },
505
+ ],
506
+ masterChain: ['EQ Eight (HP 30Hz)', 'Glue Compressor (2:1, attack 10ms, auto release)', 'Limiter (ceiling -0.3dB)'],
507
+ targetLUFS: -14,
508
+ },
509
+ productionNotes: [
510
+ 'Four-on-the-floor kick is sacred. Never skip a beat.',
511
+ 'Sidechain EVERYTHING (bass, chords, pad) from the kick.',
512
+ 'Bass should be syncopated and funky, not just root notes.',
513
+ 'Open hats on the offbeat give the groove.',
514
+ 'Filter sweeps for builds — automate LP frequency over 8-16 bars.',
515
+ ],
516
+ },
517
+ rnb: {
518
+ id: 'rnb',
519
+ name: 'R&B',
520
+ bpmRange: [68, 82],
521
+ preferredKeys: ['Db', 'Ab', 'Eb', 'Bb', 'Gb'],
522
+ preferredScales: ['major', 'dorian', 'mixolydian'],
523
+ timeSignature: [4, 4],
524
+ feel: 'straight',
525
+ tracks: [
526
+ {
527
+ name: 'Drums',
528
+ role: 'drums',
529
+ instrument: {
530
+ primary: 'Drum Rack',
531
+ presetHint: 'Tight, crisp drums. Acoustic-leaning samples. Subtle and pocket-focused.',
532
+ rationale: 'R&B drums serve the groove — never overpower the vocal space.',
533
+ },
534
+ midiContent: 'drum_pattern',
535
+ color: 3,
536
+ },
537
+ {
538
+ name: '808',
539
+ role: 'bass',
540
+ instrument: {
541
+ primary: 'Operator',
542
+ presetHint: 'Deep sustained 808. Sine wave, long decay, subtle saturation for warmth.',
543
+ rationale: 'Modern R&B lives on 808 bass — sustained, warm, melodic.',
544
+ },
545
+ midiContent: 'bass_line',
546
+ color: 1,
547
+ },
548
+ {
549
+ name: 'Keys',
550
+ role: 'harmony',
551
+ instrument: {
552
+ primary: 'Electric',
553
+ presetHint: 'Neo-soul Rhodes. Warm, slightly overdriven. Magnetic pickup. Extended chords.',
554
+ rolandCloud: 'RD-88 Neo Soul',
555
+ uaAlternative: 'Neve channel on the Rhodes for warmth',
556
+ rationale: 'Rhodes is the defining R&B keyboard sound. Neo-soul voicings.',
557
+ },
558
+ midiContent: 'chord_progression',
559
+ color: 7,
560
+ },
561
+ {
562
+ name: 'Pad',
563
+ role: 'pad',
564
+ instrument: {
565
+ primary: 'Drift',
566
+ presetHint: 'Warm lush pad. Very subtle, background only. Fills gaps between chord changes.',
567
+ rationale: 'Drift warmth suits R&B perfectly. Keep it subliminal.',
568
+ },
569
+ midiContent: 'pad_chords',
570
+ color: 9,
571
+ },
572
+ {
573
+ name: 'Lead',
574
+ role: 'melody',
575
+ instrument: {
576
+ primary: 'Wavetable',
577
+ presetHint: 'Smooth synth lead or bell. Will be replaced by vocal in a real track. Placeholder melody.',
578
+ rationale: 'Topline melody that shows where the vocal would sit.',
579
+ },
580
+ midiContent: 'melody',
581
+ color: 6,
582
+ },
583
+ ],
584
+ progressionStyle: {
585
+ namedProgressions: ['neo_soul', 'jazz_turnaround', 'soul_turnaround'],
586
+ romanTemplates: ['Imaj7 iii7 vi7 ii7 V7', 'Imaj7 vi7 ii7 V7', 'IVmaj7 iii7 vi7 ii7'],
587
+ voicing: 'drop2',
588
+ chordRhythm: 'half',
589
+ barsPerSection: 4,
590
+ octave: 4,
591
+ },
592
+ drumStyle: {
593
+ basePattern: 'hiphop',
594
+ hihatVelocityCurve: 'accent_downbeat',
595
+ ghostNotes: true,
596
+ rollProbability: 0,
597
+ swing: 40,
598
+ layers: [
599
+ { instrument: 'rim', positions: [3, 7, 11], velocity: 45, probability: 0.5 },
600
+ { instrument: 'shaker', positions: [0, 2, 4, 6, 8, 10, 12, 14], velocity: 30, probability: 0.3 },
601
+ ],
602
+ },
603
+ mixTemplate: {
604
+ volumes: { drums: 0.65, bass: 0.75, harmony: 0.60, pad: 0.30, melody: 0.55 },
605
+ panning: { drums: 0, bass: 0, harmony: 0.05, pad: 0, melody: 0 },
606
+ sends: [
607
+ { fromRole: 'harmony', toReturn: 0, level: 0.25 },
608
+ { fromRole: 'pad', toReturn: 0, level: 0.30 },
609
+ { fromRole: 'melody', toReturn: 0, level: 0.20 },
610
+ { fromRole: 'melody', toReturn: 1, level: 0.15 },
611
+ ],
612
+ returns: [
613
+ { name: 'Reverb', device: 'Reverb', presetHint: 'Smooth plate. Decay 2.5s. HP 150Hz. Predelay 25ms. Silky.' },
614
+ { name: 'Delay', device: 'Delay', presetHint: '1/4 note stereo. Feedback 20%. LP 3kHz. Subtle.' },
615
+ ],
616
+ masterChain: ['EQ Eight (HP 30Hz, gentle presence boost at 3kHz)', 'Glue Compressor (2:1, smooth)', 'Limiter (ceiling -0.3dB)'],
617
+ targetLUFS: -14,
618
+ },
619
+ productionNotes: [
620
+ 'Warm and smooth is the goal. Nothing harsh.',
621
+ 'Neo-soul chord voicings: 9ths, 11ths, add9, chromatic passing chords.',
622
+ 'Ghost notes on drums for pocket groove.',
623
+ 'Bass should be melodic — follow the chord tones, not just roots.',
624
+ 'Leave a LOT of space for vocals. The beat should breathe.',
625
+ ],
626
+ },
627
+ phonk: {
628
+ id: 'phonk',
629
+ name: 'Phonk',
630
+ bpmRange: [130, 145],
631
+ preferredKeys: ['Cm', 'Fm', 'Gm', 'Bbm'],
632
+ preferredScales: ['natural_minor', 'blues', 'phrygian'],
633
+ timeSignature: [4, 4],
634
+ feel: 'halftime',
635
+ tracks: [
636
+ {
637
+ name: 'Kick',
638
+ role: 'drums',
639
+ instrument: {
640
+ primary: 'Drum Rack',
641
+ presetHint: 'Distorted kick. Heavy, saturated. Cowbell on top.',
642
+ rationale: 'Phonk kicks are aggressive and distorted.',
643
+ },
644
+ midiContent: 'drum_pattern',
645
+ color: 1,
646
+ },
647
+ {
648
+ name: 'Clap/Snare',
649
+ role: 'drums',
650
+ instrument: {
651
+ primary: 'Drum Rack',
652
+ presetHint: 'Layered clap + snare. Distorted, heavy reverb. Memphis-style.',
653
+ rationale: 'Drenched in reverb and distortion.',
654
+ },
655
+ midiContent: 'drum_pattern',
656
+ color: 3,
657
+ },
658
+ {
659
+ name: 'Cowbell',
660
+ role: 'perc',
661
+ instrument: {
662
+ primary: 'Drum Rack',
663
+ presetHint: 'TR-808 cowbell. THE phonk signature sound. Pitched down slightly.',
664
+ rolandCloud: 'TR-808 Cowbell',
665
+ rationale: 'Cowbell is the single most identifiable phonk element.',
666
+ },
667
+ midiContent: 'perc_pattern',
668
+ color: 4,
669
+ },
670
+ {
671
+ name: 'Bass',
672
+ role: 'bass',
673
+ instrument: {
674
+ primary: 'Operator',
675
+ presetHint: 'Distorted 808 bass. Saturator or Overdrive after Operator. Aggressive.',
676
+ rationale: 'Phonk bass is 808 with heavy distortion.',
677
+ },
678
+ midiContent: 'bass_line',
679
+ color: 1,
680
+ },
681
+ {
682
+ name: 'Sample',
683
+ role: 'melody',
684
+ instrument: {
685
+ primary: 'Wavetable',
686
+ presetHint: 'Chopped soul/Memphis rap vocal sample. Dark, filtered. If no sample available, use Wavetable with a dark bell/stab.',
687
+ rationale: 'Phonk is built on samples — chopped Memphis vocals and soul.',
688
+ },
689
+ midiContent: 'melody',
690
+ color: 7,
691
+ },
692
+ ],
693
+ progressionStyle: {
694
+ namedProgressions: ['house_vamp', 'phrygian_dark', 'metal_power'],
695
+ romanTemplates: ['i bVII bVI V', 'i i bVI bVII', 'i bII bVII i'],
696
+ voicing: 'close',
697
+ chordRhythm: 'whole',
698
+ barsPerSection: 4,
699
+ octave: 3,
700
+ },
701
+ drumStyle: {
702
+ basePattern: 'trap',
703
+ hihatVelocityCurve: 'flat',
704
+ ghostNotes: false,
705
+ rollProbability: 0.15,
706
+ swing: 0,
707
+ layers: [
708
+ { instrument: 'cowbell', positions: [0, 4, 8, 12], velocity: 90, probability: 1.0 },
709
+ { instrument: 'open_hihat', positions: [2, 6, 10, 14], velocity: 70, probability: 0.6 },
710
+ ],
711
+ },
712
+ mixTemplate: {
713
+ volumes: { drums: 0.78, 'drums.snare': 0.75, perc: 0.65, bass: 0.80, melody: 0.55 },
714
+ panning: { drums: 0, 'drums.snare': 0, perc: 0, bass: 0, melody: 0 },
715
+ sends: [
716
+ { fromRole: 'drums.snare', toReturn: 0, level: 0.35 },
717
+ { fromRole: 'melody', toReturn: 0, level: 0.25 },
718
+ ],
719
+ returns: [
720
+ { name: 'Reverb', device: 'Reverb', presetHint: 'HUGE reverb. Decay 4s+. The reverb IS the sound for phonk. Dark.' },
721
+ { name: 'Delay', device: 'Echo', presetHint: 'Tape delay. Distorted feedback. Lo-fi character.' },
722
+ ],
723
+ masterChain: ['EQ Eight (HP 30Hz)', 'Saturator (for overall grit)', 'Glue Compressor (4:1, aggressive)', 'Limiter (ceiling -0.3dB)'],
724
+ targetLUFS: -12,
725
+ },
726
+ productionNotes: [
727
+ 'COWBELL. If it does not have a cowbell it is not phonk.',
728
+ 'Everything distorted — bass, drums, even the master.',
729
+ 'Reverb on the clap/snare: 3-5 seconds, dark, wet.',
730
+ 'Based on Memphis rap: chopped vocal samples, dark atmosphere.',
731
+ 'Bass should be aggressive — saturated, clipping is OK.',
732
+ ],
733
+ },
734
+ pluggnb: {
735
+ id: 'pluggnb',
736
+ name: 'Pluggnb (Plugg + R&B)',
737
+ bpmRange: [145, 160],
738
+ preferredKeys: ['C', 'F', 'Bb', 'Eb', 'Ab'],
739
+ preferredScales: ['major', 'lydian', 'mixolydian'],
740
+ timeSignature: [4, 4],
741
+ feel: 'halftime',
742
+ tracks: [
743
+ {
744
+ name: 'Drums',
745
+ role: 'drums',
746
+ instrument: {
747
+ primary: 'Drum Rack',
748
+ presetHint: 'Soft, pillowy drums. NOT aggressive. Gentle kick, soft snare/clap. Half-time.',
749
+ rationale: 'Pluggnb drums are ethereal and soft — opposite of trap aggression.',
750
+ },
751
+ midiContent: 'drum_pattern',
752
+ color: 3,
753
+ },
754
+ {
755
+ name: 'Bass',
756
+ role: 'bass',
757
+ instrument: {
758
+ primary: 'Operator',
759
+ presetHint: '808 but clean and warm. No distortion. Rounded, melodic. Follows the melody.',
760
+ rationale: 'Pluggnb bass is clean and melodic — more R&B than trap.',
761
+ },
762
+ midiContent: 'bass_line',
763
+ color: 1,
764
+ },
765
+ {
766
+ name: 'Melody',
767
+ role: 'melody',
768
+ instrument: {
769
+ primary: 'Wavetable',
770
+ presetHint: 'Dreamy bells or pluck. Bright, airy, ethereal. Chorus + reverb for sparkle. MAJOR key.',
771
+ rationale: 'Pluggnb melodies are bright, dreamy, positive — think Summrs/Autumn.',
772
+ },
773
+ midiContent: 'melody',
774
+ color: 6,
775
+ },
776
+ {
777
+ name: 'Pad',
778
+ role: 'pad',
779
+ instrument: {
780
+ primary: 'Drift',
781
+ presetHint: 'Airy, bright pad. Slow attack, long release. Dreamy, ethereal.',
782
+ rationale: 'Background atmosphere — keeps the dreamy vibe.',
783
+ },
784
+ midiContent: 'pad_chords',
785
+ color: 9,
786
+ },
787
+ {
788
+ name: 'Hi-Hats',
789
+ role: 'perc',
790
+ instrument: {
791
+ primary: 'Drum Rack',
792
+ presetHint: 'Soft hi-hats. Less aggressive than trap. Some rolls but gentler.',
793
+ rationale: 'Hats should be present but not dominating.',
794
+ },
795
+ midiContent: 'perc_pattern',
796
+ color: 5,
797
+ },
798
+ ],
799
+ progressionStyle: {
800
+ namedProgressions: ['axis', 'jpop_classic', 'royal_road', 'lydian_float'],
801
+ romanTemplates: ['I V vi IV', 'IV V iii vi', 'I V vi iii IV I IV V'],
802
+ voicing: 'spread',
803
+ chordRhythm: 'whole',
804
+ barsPerSection: 4,
805
+ octave: 5,
806
+ },
807
+ drumStyle: {
808
+ basePattern: 'trap',
809
+ hihatVelocityCurve: 'accent_downbeat',
810
+ ghostNotes: false,
811
+ rollProbability: 0.2,
812
+ swing: 0,
813
+ layers: [],
814
+ },
815
+ mixTemplate: {
816
+ volumes: { drums: 0.60, bass: 0.72, melody: 0.65, pad: 0.35, perc: 0.45 },
817
+ panning: { drums: 0, bass: 0, melody: 0, pad: 0, perc: 0.05 },
818
+ sends: [
819
+ { fromRole: 'melody', toReturn: 0, level: 0.40 },
820
+ { fromRole: 'pad', toReturn: 0, level: 0.45 },
821
+ { fromRole: 'melody', toReturn: 1, level: 0.20 },
822
+ ],
823
+ returns: [
824
+ { name: 'Reverb', device: 'Hybrid Reverb', presetHint: 'Shimmer algorithm. Decay 4s. Bright, ethereal. Defines the genre.' },
825
+ { name: 'Delay', device: 'Delay', presetHint: 'Ping-pong 1/8 dotted. Feedback 35%. HP 300Hz. Dreamy.' },
826
+ ],
827
+ masterChain: ['EQ Eight (HP 30Hz, gentle air boost 12kHz)', 'Glue Compressor (2:1, gentle)', 'Limiter (ceiling -0.3dB)'],
828
+ targetLUFS: -14,
829
+ },
830
+ productionNotes: [
831
+ 'MAJOR KEYS. Pluggnb is bright, dreamy, ethereal — not dark.',
832
+ 'Drums should be SOFT. Opposite of trap energy.',
833
+ 'Melody is the star. Should be high-register, bell-like, reverbed heavily.',
834
+ 'Shimmer reverb is essential — it defines the pluggnb sound.',
835
+ 'Bass is clean and melodic. No distortion.',
836
+ 'Think: Summrs, Autumn, SeptembersRich.',
837
+ ],
838
+ },
839
+ ambient: {
840
+ id: 'ambient',
841
+ name: 'Ambient',
842
+ bpmRange: [60, 85],
843
+ preferredKeys: ['C', 'Am', 'Em', 'D', 'F'],
844
+ preferredScales: ['major', 'lydian', 'mixolydian', 'pentatonic_major'],
845
+ timeSignature: [4, 4],
846
+ feel: 'straight',
847
+ tracks: [
848
+ {
849
+ name: 'Pad 1',
850
+ role: 'pad',
851
+ instrument: {
852
+ primary: 'Wavetable',
853
+ presetHint: 'Evolving pad. Very slow attack (2s+). Wavetable position modulated by LFO. Wide, immersive.',
854
+ rationale: 'Primary texture layer. Should feel like it is always there.',
855
+ },
856
+ midiContent: 'pad_chords',
857
+ color: 9,
858
+ },
859
+ {
860
+ name: 'Pad 2',
861
+ role: 'pad',
862
+ instrument: {
863
+ primary: 'Wavetable',
864
+ presetHint: 'Granular texture from field recording or tonal sample. Freeze + slow scan. Ethereal.',
865
+ rationale: 'Second texture layer. Granular gives organic, evolving quality.',
866
+ },
867
+ midiContent: 'pad_chords',
868
+ color: 10,
869
+ },
870
+ {
871
+ name: 'Melody',
872
+ role: 'melody',
873
+ instrument: {
874
+ primary: 'Wavetable',
875
+ presetHint: 'Soft mallet on beam resonator. Like a distant marimba or singing bowl. Sparse notes.',
876
+ rationale: 'Physical modeling gives organic resonance. Sparse melody creates focal points.',
877
+ },
878
+ midiContent: 'melody',
879
+ color: 7,
880
+ },
881
+ {
882
+ name: 'Texture',
883
+ role: 'fx',
884
+ instrument: {
885
+ primary: 'Simpler',
886
+ presetHint: 'Field recording loop — rain, ocean, forest. Very low volume. Environmental context.',
887
+ rationale: 'Grounds the ambient piece in a physical space.',
888
+ },
889
+ midiContent: 'none',
890
+ color: 11,
891
+ },
892
+ {
893
+ name: 'Sub',
894
+ role: 'bass',
895
+ instrument: {
896
+ primary: 'Analog',
897
+ presetHint: 'Deep sub drone. Sine wave. Barely audible. Provides physical weight.',
898
+ rationale: 'Subsonic foundation that you feel more than hear.',
899
+ },
900
+ midiContent: 'bass_line',
901
+ color: 1,
902
+ },
903
+ ],
904
+ progressionStyle: {
905
+ namedProgressions: ['lydian_float', 'dorian_vamp', 'modal_interchange'],
906
+ romanTemplates: ['I II', 'I bVII', 'I IV', 'Imaj7'],
907
+ voicing: 'spread',
908
+ chordRhythm: 'whole',
909
+ barsPerSection: 8,
910
+ octave: 4,
911
+ },
912
+ drumStyle: {
913
+ basePattern: 'ambient',
914
+ hihatVelocityCurve: 'flat',
915
+ ghostNotes: false,
916
+ rollProbability: 0,
917
+ swing: 0,
918
+ layers: [],
919
+ },
920
+ mixTemplate: {
921
+ volumes: { pad: 0.55, 'pad.2': 0.40, melody: 0.45, fx: 0.20, bass: 0.50 },
922
+ panning: { pad: 0, 'pad.2': 0, melody: 0, fx: 0, bass: 0 },
923
+ sends: [
924
+ { fromRole: 'melody', toReturn: 0, level: 0.60 },
925
+ { fromRole: 'pad', toReturn: 0, level: 0.40 },
926
+ { fromRole: 'melody', toReturn: 1, level: 0.30 },
927
+ ],
928
+ returns: [
929
+ { name: 'Reverb', device: 'Hybrid Reverb', presetHint: 'Shimmer algorithm. Decay 8s+. Huge, infinite. This IS the sound.' },
930
+ { name: 'Grain Delay', device: 'Spectral Time', presetHint: 'Spectral freeze + delay. For otherworldly textures.' },
931
+ ],
932
+ masterChain: ['EQ Eight (HP 25Hz, gentle overall shaping)', 'Limiter (ceiling -0.5dB, very gentle)'],
933
+ targetLUFS: -20,
934
+ },
935
+ productionNotes: [
936
+ 'Less is always more. One note can fill an entire bar.',
937
+ 'Reverb tails ARE the music. Let everything sustain and decay.',
938
+ 'No drums required — or minimal: one kick per bar, distant ride.',
939
+ 'Automate EVERYTHING slowly — filter sweeps over 32 bars.',
940
+ 'Think: Brian Eno, Stars of the Lid, Tim Hecker.',
941
+ 'The piece should feel like it has no beginning or end.',
942
+ ],
943
+ },
944
+ };
945
+ // ── Preset Resolution ───────────────────────────────────────────────────────
946
+ function resolvePreset(genre, overrides) {
947
+ const preset = GENRE_PRESETS[genre] || GENRE_PRESETS.trap;
948
+ const genreId = GENRE_PRESETS[genre] ? genre : 'trap';
949
+ // Resolve key
950
+ let root;
951
+ let scale;
952
+ if (overrides?.key) {
953
+ const parsed = parseKeyString(overrides.key);
954
+ root = parsed.root;
955
+ scale = parsed.scale;
956
+ }
957
+ else {
958
+ const keyStr = pick(preset.preferredKeys);
959
+ const parsed = parseKeyString(keyStr);
960
+ root = parsed.root;
961
+ // Use a preferred scale from the preset if the key doesn't dictate minor
962
+ scale = parsed.scale === 'natural_minor'
963
+ ? pick(preset.preferredScales.filter(s => s.includes('minor') || s === 'phrygian' || s === 'blues') || ['natural_minor'])
964
+ : pick(preset.preferredScales.filter(s => !s.includes('minor')) || ['major']);
965
+ if (!scale)
966
+ scale = pick(preset.preferredScales);
967
+ }
968
+ // Resolve BPM
969
+ const bpm = overrides?.bpm || randomInRange(preset.bpmRange[0], preset.bpmRange[1]);
970
+ // Resolve bars
971
+ const bars = overrides?.bars || preset.progressionStyle.barsPerSection;
972
+ // Resolve chord progression
973
+ let progressionNumerals;
974
+ if (overrides?.progression) {
975
+ // Check if it's a named progression
976
+ const named = NAMED_PROGRESSIONS[overrides.progression.toLowerCase().replace(/[\s-]/g, '_')];
977
+ progressionNumerals = named ? named.numerals : overrides.progression;
978
+ }
979
+ else {
980
+ // Random: 50% named, 50% roman template
981
+ if (Math.random() > 0.5 && preset.progressionStyle.namedProgressions.length > 0) {
982
+ const progName = pick(preset.progressionStyle.namedProgressions);
983
+ const named = NAMED_PROGRESSIONS[progName];
984
+ progressionNumerals = named ? named.numerals : pick(preset.progressionStyle.romanTemplates);
985
+ }
986
+ else {
987
+ progressionNumerals = pick(preset.progressionStyle.romanTemplates);
988
+ }
989
+ }
990
+ // Parse the progression into MIDI note arrays
991
+ let resolvedChords;
992
+ try {
993
+ resolvedChords = parseProgression(progressionNumerals, root, scale, preset.progressionStyle.octave);
994
+ // Apply voicing
995
+ resolvedChords = resolvedChords.map(notes => voiceChord(notes, preset.progressionStyle.voicing));
996
+ }
997
+ catch {
998
+ // Fallback: simple i bVI bVII i
999
+ resolvedChords = parseProgression('i bVI bVII i', root, 'natural_minor', preset.progressionStyle.octave);
1000
+ progressionNumerals = 'i bVI bVII i';
1001
+ }
1002
+ // Determine melody octave based on genre
1003
+ const melodyOctave = genreId === 'pluggnb' ? 6
1004
+ : genreId === 'ambient' ? 5
1005
+ : genreId === 'lofi' ? 4
1006
+ : 5;
1007
+ return {
1008
+ ...preset,
1009
+ id: genreId,
1010
+ key: root + (scale.includes('minor') || scale === 'phrygian' || scale === 'blues' ? 'm' : ''),
1011
+ root,
1012
+ scale,
1013
+ bpm,
1014
+ bars,
1015
+ resolvedChords,
1016
+ progressionNumerals,
1017
+ melodyOctave,
1018
+ };
1019
+ }
1020
+ // ── Velocity Curves ─────────────────────────────────────────────────────────
1021
+ function applyVelocityCurve(baseVelocity, sixteenthPosition, instrument, curve) {
1022
+ switch (curve) {
1023
+ case 'flat':
1024
+ return baseVelocity;
1025
+ case 'accent_downbeat':
1026
+ if (sixteenthPosition % 4 === 0)
1027
+ return Math.min(127, baseVelocity + 20);
1028
+ if (sixteenthPosition % 2 === 0)
1029
+ return baseVelocity;
1030
+ return Math.max(40, baseVelocity - 15);
1031
+ case 'crescendo_roll':
1032
+ if (instrument.includes('hihat') || instrument.includes('hat')) {
1033
+ const posInBeat = sixteenthPosition % 4;
1034
+ return Math.min(127, 60 + posInBeat * 20);
1035
+ }
1036
+ return baseVelocity;
1037
+ case 'random_humanize':
1038
+ return Math.max(40, Math.min(127, baseVelocity + Math.floor(Math.random() * 30 - 15)));
1039
+ }
1040
+ }
1041
+ // ── Melody Density ──────────────────────────────────────────────────────────
1042
+ function getMelodyDensity(genre) {
1043
+ switch (genre) {
1044
+ case 'trap':
1045
+ case 'drill':
1046
+ return { notesPerBar: 6, restProbability: 0.3, maxInterval: 5 };
1047
+ case 'house':
1048
+ return { notesPerBar: 8, restProbability: 0.15, maxInterval: 7 };
1049
+ case 'lofi':
1050
+ return { notesPerBar: 5, restProbability: 0.35, maxInterval: 4 };
1051
+ case 'rnb':
1052
+ return { notesPerBar: 6, restProbability: 0.25, maxInterval: 5 };
1053
+ case 'phonk':
1054
+ return { notesPerBar: 4, restProbability: 0.4, maxInterval: 3 };
1055
+ case 'pluggnb':
1056
+ return { notesPerBar: 8, restProbability: 0.15, maxInterval: 7 };
1057
+ case 'ambient':
1058
+ return { notesPerBar: 2, restProbability: 0.6, maxInterval: 7 };
1059
+ default:
1060
+ return { notesPerBar: 6, restProbability: 0.2, maxInterval: 5 };
1061
+ }
1062
+ }
1063
+ // ── Melody Duration Picker ──────────────────────────────────────────────────
1064
+ function pickMelodyDuration(genre, currentBeat) {
1065
+ switch (genre) {
1066
+ case 'trap':
1067
+ case 'drill':
1068
+ // Sparse: mostly half and quarter notes
1069
+ return pick([0.5, 0.5, 1, 1, 2]);
1070
+ case 'house':
1071
+ // Active: eighths and quarters
1072
+ return pick([0.25, 0.5, 0.5, 1]);
1073
+ case 'lofi':
1074
+ // Relaxed: quarters and halves with jazz feel
1075
+ return pick([0.5, 1, 1, 1.5, 2]);
1076
+ case 'rnb':
1077
+ // Smooth: mixed durations, longer notes
1078
+ return pick([0.5, 1, 1, 1.5, 2]);
1079
+ case 'phonk':
1080
+ // Very sparse: long notes
1081
+ return pick([1, 1, 2, 2, 4]);
1082
+ case 'pluggnb':
1083
+ // Bright and active: eighths and quarters
1084
+ return pick([0.25, 0.5, 0.5, 1]);
1085
+ case 'ambient':
1086
+ // Extremely long, sustained
1087
+ return pick([2, 4, 4, 8]);
1088
+ default:
1089
+ return pick([0.5, 1, 1, 2]);
1090
+ }
1091
+ }
1092
+ // ── Melody Note Generator ───────────────────────────────────────────────────
1093
+ function generateNextMelodyNote(prevPitch, scaleMidi, maxInterval) {
1094
+ // Build all scale notes within range across octaves
1095
+ const allScaleNotes = [];
1096
+ for (let octaveOffset = -12; octaveOffset <= 12; octaveOffset += 12) {
1097
+ for (const n of scaleMidi) {
1098
+ allScaleNotes.push(n + octaveOffset);
1099
+ }
1100
+ }
1101
+ const candidates = allScaleNotes.filter(n => Math.abs(n - prevPitch) <= maxInterval &&
1102
+ Math.abs(n - prevPitch) > 0);
1103
+ if (candidates.length === 0)
1104
+ return prevPitch;
1105
+ // Prefer stepwise motion (80%) over leaps (20%)
1106
+ const stepwise = candidates.filter(n => Math.abs(n - prevPitch) <= 2);
1107
+ if (stepwise.length > 0 && Math.random() < 0.8) {
1108
+ return pick(stepwise);
1109
+ }
1110
+ return pick(candidates);
1111
+ }
1112
+ // ── Bass Line Generator ─────────────────────────────────────────────────────
1113
+ function generateBassLine(preset) {
1114
+ const chords = preset.resolvedChords;
1115
+ const barsPerChord = preset.bars / chords.length;
1116
+ const beatsPerChord = barsPerChord * 4;
1117
+ const notes = [];
1118
+ for (let i = 0; i < chords.length; i++) {
1119
+ const chordRoot = chords[i][0];
1120
+ const chordStart = i * beatsPerChord;
1121
+ // Bass octave: one octave below chord root
1122
+ const bassRoot = chordRoot - 12;
1123
+ switch (preset.id) {
1124
+ case 'trap':
1125
+ case 'pluggnb': {
1126
+ // 808 style: long sustained notes with occasional rhythmic hits
1127
+ notes.push({
1128
+ pitch: bassRoot,
1129
+ start: chordStart,
1130
+ duration: beatsPerChord * 0.9,
1131
+ velocity: 110,
1132
+ });
1133
+ // Occasional octave hit for energy
1134
+ if (Math.random() > 0.5) {
1135
+ notes.push({
1136
+ pitch: bassRoot + 12,
1137
+ start: chordStart + beatsPerChord * 0.75,
1138
+ duration: 0.5,
1139
+ velocity: 90,
1140
+ });
1141
+ }
1142
+ break;
1143
+ }
1144
+ case 'drill': {
1145
+ // Slide 808: octave jumps with overlapping notes for glide effect
1146
+ notes.push({
1147
+ pitch: bassRoot,
1148
+ start: chordStart,
1149
+ duration: beatsPerChord * 0.5,
1150
+ velocity: 110,
1151
+ });
1152
+ // Octave jump
1153
+ notes.push({
1154
+ pitch: bassRoot + 12,
1155
+ start: chordStart + beatsPerChord * 0.5,
1156
+ duration: beatsPerChord * 0.3,
1157
+ velocity: 100,
1158
+ });
1159
+ // Slide back down (overlapping for portamento trigger)
1160
+ if (Math.random() > 0.4) {
1161
+ const fifthAbove = quantizeToScale(bassRoot + 7, preset.root, preset.scale);
1162
+ notes.push({
1163
+ pitch: fifthAbove,
1164
+ start: chordStart + beatsPerChord * 0.8,
1165
+ duration: beatsPerChord * 0.2,
1166
+ velocity: 95,
1167
+ });
1168
+ }
1169
+ break;
1170
+ }
1171
+ case 'phonk': {
1172
+ // Aggressive 808 with distortion feel
1173
+ notes.push({
1174
+ pitch: bassRoot,
1175
+ start: chordStart,
1176
+ duration: beatsPerChord * 0.85,
1177
+ velocity: 120,
1178
+ });
1179
+ // Add sub hit
1180
+ if (Math.random() > 0.6) {
1181
+ notes.push({
1182
+ pitch: bassRoot + 12,
1183
+ start: chordStart + beatsPerChord * 0.5,
1184
+ duration: 0.5,
1185
+ velocity: 100,
1186
+ });
1187
+ }
1188
+ break;
1189
+ }
1190
+ case 'house': {
1191
+ // Funky syncopated bass
1192
+ const houseBassRhythm = [0, 1.5, 2, 3, 3.5];
1193
+ for (const offset of houseBassRhythm) {
1194
+ if (offset >= beatsPerChord)
1195
+ break;
1196
+ const pitch = offset === 0 ? bassRoot
1197
+ : Math.random() > 0.5
1198
+ ? quantizeToScale(bassRoot + 7, preset.root, preset.scale)
1199
+ : quantizeToScale(bassRoot + 5, preset.root, preset.scale);
1200
+ notes.push({
1201
+ pitch,
1202
+ start: chordStart + offset,
1203
+ duration: 0.4,
1204
+ velocity: offset === 0 ? 100 : 80,
1205
+ });
1206
+ }
1207
+ break;
1208
+ }
1209
+ case 'lofi':
1210
+ case 'rnb': {
1211
+ // Walking / melodic bass following chord tones
1212
+ const chordTones = chords[i].map(n => n - 12);
1213
+ const walkRhythm = [0, 1, 2, 3];
1214
+ for (let j = 0; j < walkRhythm.length; j++) {
1215
+ if (walkRhythm[j] >= beatsPerChord)
1216
+ break;
1217
+ const pitch = chordTones[j % chordTones.length];
1218
+ notes.push({
1219
+ pitch,
1220
+ start: chordStart + walkRhythm[j],
1221
+ duration: 0.8,
1222
+ velocity: j === 0 ? 95 : 75,
1223
+ });
1224
+ }
1225
+ break;
1226
+ }
1227
+ case 'ambient': {
1228
+ // Drone: one long note per chord, very low velocity
1229
+ notes.push({
1230
+ pitch: bassRoot,
1231
+ start: chordStart,
1232
+ duration: beatsPerChord,
1233
+ velocity: 50,
1234
+ });
1235
+ break;
1236
+ }
1237
+ default: {
1238
+ // Default: root notes on the beat
1239
+ notes.push({
1240
+ pitch: bassRoot,
1241
+ start: chordStart,
1242
+ duration: beatsPerChord * 0.8,
1243
+ velocity: 90,
1244
+ });
1245
+ }
1246
+ }
1247
+ }
1248
+ return notes;
1249
+ }
1250
+ // ── Melody Generator ────────────────────────────────────────────────────────
1251
+ function generateMelody(preset) {
1252
+ const scaleData = getScaleNotes(preset.root, preset.scale, preset.melodyOctave);
1253
+ const totalBeats = preset.bars * 4;
1254
+ const density = getMelodyDensity(preset.id);
1255
+ const notes = [];
1256
+ let currentBeat = 0;
1257
+ // Use pentatonic for trap, full scale for others
1258
+ let scaleMidi = scaleData.midi;
1259
+ if (preset.id === 'trap' || preset.id === 'drill' || preset.id === 'phonk') {
1260
+ // Get pentatonic minor for dark genres
1261
+ const pentatonic = getScaleNotes(preset.root, 'pentatonic_minor', preset.melodyOctave);
1262
+ scaleMidi = pentatonic.midi;
1263
+ }
1264
+ else if (preset.id === 'pluggnb') {
1265
+ // Major pentatonic for bright genres
1266
+ const pentatonic = getScaleNotes(preset.root, 'pentatonic_major', preset.melodyOctave);
1267
+ scaleMidi = pentatonic.midi;
1268
+ }
1269
+ while (currentBeat < totalBeats) {
1270
+ // Rest probability
1271
+ if (Math.random() < density.restProbability) {
1272
+ currentBeat += 0.5;
1273
+ continue;
1274
+ }
1275
+ // Pick the next pitch
1276
+ const prevPitch = notes.length > 0 ? notes[notes.length - 1].pitch : scaleMidi[0];
1277
+ const nextPitch = generateNextMelodyNote(prevPitch, scaleMidi, density.maxInterval);
1278
+ // Duration varies by genre
1279
+ const duration = pickMelodyDuration(preset.id, currentBeat);
1280
+ // Don't go past the end
1281
+ const clampedDuration = Math.min(duration, totalBeats - currentBeat);
1282
+ if (clampedDuration <= 0)
1283
+ break;
1284
+ notes.push({
1285
+ pitch: nextPitch,
1286
+ start: currentBeat,
1287
+ duration: clampedDuration,
1288
+ velocity: 70 + Math.floor(Math.random() * 30),
1289
+ });
1290
+ currentBeat += duration + (Math.random() > 0.7 ? 0.25 : 0);
1291
+ }
1292
+ return notes;
1293
+ }
1294
+ // ── Pad Chord Generator ─────────────────────────────────────────────────────
1295
+ function generatePadChords(preset) {
1296
+ const chords = preset.resolvedChords;
1297
+ const barsPerChord = preset.bars / chords.length;
1298
+ const beatsPerChord = barsPerChord * 4;
1299
+ const notes = [];
1300
+ for (let i = 0; i < chords.length; i++) {
1301
+ const chordStart = i * beatsPerChord;
1302
+ const chordNotes = chords[i];
1303
+ for (const pitch of chordNotes) {
1304
+ notes.push({
1305
+ pitch,
1306
+ start: chordStart,
1307
+ duration: beatsPerChord * 0.95, // nearly full sustain
1308
+ velocity: 60,
1309
+ });
1310
+ }
1311
+ }
1312
+ return notes;
1313
+ }
1314
+ // ── Hi-Hat Roll Writer ──────────────────────────────────────────────────────
1315
+ function generateHihatRolls(bars, rollProbability) {
1316
+ const rollPitch = GM_DRUMS.closed_hihat;
1317
+ const notes = [];
1318
+ for (let bar = 0; bar < bars; bar++) {
1319
+ if (Math.random() > rollProbability)
1320
+ continue;
1321
+ // Roll before beat 3 or end of bar
1322
+ const rollStartSixteenth = bar * 16 + (Math.random() > 0.5 ? 6 : 14);
1323
+ const rollLength = Math.random() > 0.5 ? 4 : 6;
1324
+ for (let i = 0; i < rollLength; i++) {
1325
+ const pos = rollStartSixteenth + (i * 0.5); // 32nd notes
1326
+ const beatPos = pos / 4;
1327
+ const velocity = Math.min(127, 60 + Math.floor((i / rollLength) * 60));
1328
+ notes.push({
1329
+ pitch: rollPitch,
1330
+ start: beatPos,
1331
+ duration: 0.1,
1332
+ velocity: Math.floor(velocity),
1333
+ });
1334
+ }
1335
+ }
1336
+ return notes;
1337
+ }
1338
+ // ── Execution Pipeline ──────────────────────────────────────────────────────
1339
+ async function executeProductionPipeline(genre, overrides) {
1340
+ const report = {
1341
+ genre,
1342
+ key: '',
1343
+ scale: '',
1344
+ bpm: 0,
1345
+ bars: 0,
1346
+ progression: '',
1347
+ tracksCreated: [],
1348
+ instrumentsLoaded: [],
1349
+ errors: [],
1350
+ mixApplied: false,
1351
+ playing: false,
1352
+ };
1353
+ // Step 1: Resolve preset
1354
+ const preset = resolvePreset(genre, overrides);
1355
+ report.key = preset.key;
1356
+ report.scale = preset.scale;
1357
+ report.bpm = preset.bpm;
1358
+ report.bars = preset.bars;
1359
+ report.progression = preset.progressionNumerals;
1360
+ let osc;
1361
+ try {
1362
+ osc = await ensureAbleton();
1363
+ }
1364
+ catch (err) {
1365
+ report.errors.push(`Ableton connection failed: ${err.message}`);
1366
+ return report;
1367
+ }
1368
+ // Step 2: Stop playback, set tempo + time signature
1369
+ try {
1370
+ osc.send('/live/song/stop_playing');
1371
+ await sleep(100);
1372
+ osc.send('/live/song/set/tempo', preset.bpm);
1373
+ osc.send('/live/song/set/signature_numerator', preset.timeSignature[0]);
1374
+ osc.send('/live/song/set/signature_denominator', preset.timeSignature[1]);
1375
+ await sleep(150);
1376
+ }
1377
+ catch (err) {
1378
+ report.errors.push(`Transport setup failed: ${err.message}`);
1379
+ }
1380
+ // Step 3: Create tracks
1381
+ const trackMappings = [];
1382
+ try {
1383
+ // Get current track count
1384
+ const countResult = await osc.query('/live/song/get/num_tracks');
1385
+ // The return includes the track count as the first arg
1386
+ let nextTrack = Number(extractArgs(countResult)[0]) || 0;
1387
+ for (const spec of preset.tracks) {
1388
+ osc.send('/live/kbot/create_midi_track', -1);
1389
+ await sleep(500);
1390
+ // Re-query to get the actual new track index
1391
+ const newCount = await osc.query('/live/song/get/num_tracks');
1392
+ const newIdx = Number(extractArgs(newCount)[0]) - 1;
1393
+ // Name and color the track
1394
+ osc.send('/live/track/set/name', newIdx, spec.name);
1395
+ osc.send('/live/track/set/color', newIdx, spec.color);
1396
+ trackMappings.push({ role: spec.role, trackIndex: newIdx, spec });
1397
+ report.tracksCreated.push(spec.name);
1398
+ nextTrack = newIdx + 1;
1399
+ await sleep(150);
1400
+ }
1401
+ }
1402
+ catch (err) {
1403
+ report.errors.push(`Track creation failed: ${err.message}`);
1404
+ if (trackMappings.length === 0)
1405
+ return report;
1406
+ }
1407
+ // Step 4: Load instruments on each track
1408
+ for (const mapping of trackMappings) {
1409
+ try {
1410
+ const result = await osc.query('/live/kbot/load_plugin', mapping.trackIndex, mapping.spec.instrument.primary, '');
1411
+ const status = extractArgs(result);
1412
+ if (status[0] === 'ok') {
1413
+ report.instrumentsLoaded.push(`${mapping.spec.name}: ${status[1]}`);
1414
+ }
1415
+ else {
1416
+ // Fallback: try load_device
1417
+ try {
1418
+ const fallback = await osc.query('/live/kbot/load_device', mapping.trackIndex, mapping.spec.instrument.primary);
1419
+ const fbStatus = extractArgs(fallback);
1420
+ if (fbStatus[0] === 'ok') {
1421
+ report.instrumentsLoaded.push(`${mapping.spec.name}: ${fbStatus[1]}`);
1422
+ }
1423
+ else {
1424
+ report.errors.push(`Could not load ${mapping.spec.instrument.primary} on ${mapping.spec.name} — track will use default instrument`);
1425
+ }
1426
+ }
1427
+ catch {
1428
+ report.errors.push(`Could not load ${mapping.spec.instrument.primary} on ${mapping.spec.name} — track will use default instrument`);
1429
+ }
1430
+ }
1431
+ }
1432
+ catch (err) {
1433
+ report.errors.push(`Plugin load error on ${mapping.spec.name}: ${err.message}`);
1434
+ }
1435
+ await sleep(150);
1436
+ }
1437
+ // Step 5: Create clips on each track
1438
+ const clipSlot = 0;
1439
+ const clipLengthBeats = preset.bars * 4;
1440
+ for (const mapping of trackMappings) {
1441
+ try {
1442
+ osc.send('/live/clip_slot/create_clip', mapping.trackIndex, clipSlot, clipLengthBeats);
1443
+ await sleep(200);
1444
+ osc.send('/live/clip/set/name', mapping.trackIndex, clipSlot, `${preset.name} - ${mapping.spec.name}`);
1445
+ }
1446
+ catch (err) {
1447
+ report.errors.push(`Clip creation failed on ${mapping.spec.name}: ${err.message}`);
1448
+ }
1449
+ }
1450
+ await sleep(150);
1451
+ // Step 6: Write drum patterns
1452
+ const drumTracks = trackMappings.filter(m => m.spec.midiContent === 'drum_pattern');
1453
+ const percTracks = trackMappings.filter(m => m.spec.midiContent === 'perc_pattern');
1454
+ const drumPattern = GENRE_DRUM_PATTERNS[preset.drumStyle.basePattern];
1455
+ if (drumPattern) {
1456
+ // Write main drum pattern to the first drum track
1457
+ const mainDrumTrack = drumTracks[0];
1458
+ if (mainDrumTrack) {
1459
+ try {
1460
+ for (const [instrumentName, positions] of Object.entries(drumPattern.pattern)) {
1461
+ const midiPitch = GM_DRUMS[instrumentName];
1462
+ if (midiPitch === undefined)
1463
+ continue;
1464
+ // Skip hi-hat instruments for the main drum track (they go on perc track)
1465
+ if (instrumentName.includes('hihat') || instrumentName.includes('hat'))
1466
+ continue;
1467
+ for (let bar = 0; bar < preset.bars; bar++) {
1468
+ for (const pos of positions) {
1469
+ const absolutePos = bar * 16 + pos;
1470
+ const beatPos = absolutePos / 4;
1471
+ let velocity = 80;
1472
+ velocity = applyVelocityCurve(velocity, pos, instrumentName, preset.drumStyle.hihatVelocityCurve);
1473
+ osc.send('/live/clip/add/notes', mainDrumTrack.trackIndex, clipSlot, midiPitch, beatPos, 0.2, velocity, 0);
1474
+ }
1475
+ }
1476
+ }
1477
+ // Write drum layers
1478
+ for (const layer of preset.drumStyle.layers) {
1479
+ // Skip hi-hat / percussion layers — they go on perc track
1480
+ if (layer.instrument.includes('hihat') || layer.instrument.includes('hat') ||
1481
+ layer.instrument === 'cowbell' || layer.instrument === 'tambourine' ||
1482
+ layer.instrument === 'shaker')
1483
+ continue;
1484
+ const pitch = GM_DRUMS[layer.instrument];
1485
+ if (pitch === undefined)
1486
+ continue;
1487
+ for (let bar = 0; bar < preset.bars; bar++) {
1488
+ for (const pos of layer.positions) {
1489
+ if (Math.random() > layer.probability)
1490
+ continue;
1491
+ const beatPos = (bar * 16 + pos) / 4;
1492
+ osc.send('/live/clip/add/notes', mainDrumTrack.trackIndex, clipSlot, pitch, beatPos, 0.2, layer.velocity, 0);
1493
+ }
1494
+ }
1495
+ }
1496
+ }
1497
+ catch (err) {
1498
+ report.errors.push(`Drum pattern write error: ${err.message}`);
1499
+ }
1500
+ }
1501
+ // Write snare/clap on second drum track if it exists
1502
+ const snareDrumTrack = drumTracks[1];
1503
+ if (snareDrumTrack && drumPattern.pattern) {
1504
+ try {
1505
+ // Write snare hits
1506
+ const snarePositions = drumPattern.pattern.snare || drumPattern.pattern.clap || [];
1507
+ const snarePitch = GM_DRUMS.snare;
1508
+ const clapPitch = GM_DRUMS.clap;
1509
+ for (let bar = 0; bar < preset.bars; bar++) {
1510
+ for (const pos of snarePositions) {
1511
+ const beatPos = (bar * 16 + pos) / 4;
1512
+ osc.send('/live/clip/add/notes', snareDrumTrack.trackIndex, clipSlot, snarePitch, beatPos, 0.2, 90, 0);
1513
+ // Layer a clap on top
1514
+ osc.send('/live/clip/add/notes', snareDrumTrack.trackIndex, clipSlot, clapPitch, beatPos, 0.2, 80, 0);
1515
+ }
1516
+ }
1517
+ }
1518
+ catch (err) {
1519
+ report.errors.push(`Snare track write error: ${err.message}`);
1520
+ }
1521
+ }
1522
+ }
1523
+ // Step 7: Write hi-hat / percussion patterns
1524
+ const percTrack = percTracks[0];
1525
+ if (percTrack && drumPattern) {
1526
+ try {
1527
+ // Write hi-hat pattern
1528
+ for (const [instrumentName, positions] of Object.entries(drumPattern.pattern)) {
1529
+ if (!instrumentName.includes('hihat') && !instrumentName.includes('hat'))
1530
+ continue;
1531
+ const midiPitch = GM_DRUMS[instrumentName];
1532
+ if (midiPitch === undefined)
1533
+ continue;
1534
+ for (let bar = 0; bar < preset.bars; bar++) {
1535
+ for (const pos of positions) {
1536
+ const beatPos = (bar * 16 + pos) / 4;
1537
+ let velocity = 80;
1538
+ velocity = applyVelocityCurve(velocity, pos, instrumentName, preset.drumStyle.hihatVelocityCurve);
1539
+ osc.send('/live/clip/add/notes', percTrack.trackIndex, clipSlot, midiPitch, beatPos, 0.15, velocity, 0);
1540
+ }
1541
+ }
1542
+ }
1543
+ // Write percussion layers (cowbell, tambourine, shaker)
1544
+ for (const layer of preset.drumStyle.layers) {
1545
+ const pitch = GM_DRUMS[layer.instrument];
1546
+ if (pitch === undefined)
1547
+ continue;
1548
+ for (let bar = 0; bar < preset.bars; bar++) {
1549
+ for (const pos of layer.positions) {
1550
+ if (Math.random() > layer.probability)
1551
+ continue;
1552
+ const beatPos = (bar * 16 + pos) / 4;
1553
+ osc.send('/live/clip/add/notes', percTrack.trackIndex, clipSlot, pitch, beatPos, 0.15, layer.velocity, 0);
1554
+ }
1555
+ }
1556
+ }
1557
+ // Hi-hat rolls for trap/drill/phonk
1558
+ if (preset.drumStyle.rollProbability > 0) {
1559
+ const rollNotes = generateHihatRolls(preset.bars, preset.drumStyle.rollProbability);
1560
+ for (const note of rollNotes) {
1561
+ osc.send('/live/clip/add/notes', percTrack.trackIndex, clipSlot, note.pitch, note.start, note.duration, note.velocity, 0);
1562
+ }
1563
+ }
1564
+ }
1565
+ catch (err) {
1566
+ report.errors.push(`Hi-hat/perc write error: ${err.message}`);
1567
+ }
1568
+ }
1569
+ // Step 8: Write bass line
1570
+ const bassTrack = trackMappings.find(m => m.spec.midiContent === 'bass_line');
1571
+ if (bassTrack) {
1572
+ try {
1573
+ const bassNotes = generateBassLine(preset);
1574
+ for (const note of bassNotes) {
1575
+ osc.send('/live/clip/add/notes', bassTrack.trackIndex, clipSlot, note.pitch, note.start, note.duration, note.velocity, 0);
1576
+ }
1577
+ }
1578
+ catch (err) {
1579
+ report.errors.push(`Bass line write error: ${err.message}`);
1580
+ }
1581
+ }
1582
+ // Step 9: Write melody
1583
+ const melodyTrack = trackMappings.find(m => m.spec.midiContent === 'melody');
1584
+ if (melodyTrack) {
1585
+ try {
1586
+ const melodyNotes = generateMelody(preset);
1587
+ for (const note of melodyNotes) {
1588
+ osc.send('/live/clip/add/notes', melodyTrack.trackIndex, clipSlot, note.pitch, note.start, note.duration, note.velocity, 0);
1589
+ }
1590
+ }
1591
+ catch (err) {
1592
+ report.errors.push(`Melody write error: ${err.message}`);
1593
+ }
1594
+ }
1595
+ // Step 10: Write chord progression (harmony tracks)
1596
+ const harmonyTrack = trackMappings.find(m => m.spec.midiContent === 'chord_progression');
1597
+ if (harmonyTrack) {
1598
+ try {
1599
+ const chords = preset.resolvedChords;
1600
+ const barsPerChord = preset.bars / chords.length;
1601
+ const beatsPerChord = barsPerChord * 4;
1602
+ for (let i = 0; i < chords.length; i++) {
1603
+ const chordStart = i * beatsPerChord;
1604
+ const chordNotes = chords[i];
1605
+ // Write chord using the preset's rhythm style
1606
+ const rhythmKey = preset.progressionStyle.chordRhythm;
1607
+ if (rhythmKey === 'quarter') {
1608
+ // Stabs on every beat
1609
+ for (let beat = 0; beat < beatsPerChord; beat++) {
1610
+ for (const pitch of chordNotes) {
1611
+ osc.send('/live/clip/add/notes', harmonyTrack.trackIndex, clipSlot, pitch, chordStart + beat, 0.8, 80, 0);
1612
+ }
1613
+ }
1614
+ }
1615
+ else if (rhythmKey === 'half') {
1616
+ // Two hits per chord
1617
+ for (let hit = 0; hit < 2; hit++) {
1618
+ const hitStart = chordStart + hit * 2;
1619
+ if (hitStart >= chordStart + beatsPerChord)
1620
+ break;
1621
+ for (const pitch of chordNotes) {
1622
+ osc.send('/live/clip/add/notes', harmonyTrack.trackIndex, clipSlot, pitch, hitStart, 1.8, 80, 0);
1623
+ }
1624
+ }
1625
+ }
1626
+ else {
1627
+ // Whole note: sustain through the chord
1628
+ for (const pitch of chordNotes) {
1629
+ osc.send('/live/clip/add/notes', harmonyTrack.trackIndex, clipSlot, pitch, chordStart, beatsPerChord * 0.95, 80, 0);
1630
+ }
1631
+ }
1632
+ }
1633
+ }
1634
+ catch (err) {
1635
+ report.errors.push(`Chord progression write error: ${err.message}`);
1636
+ }
1637
+ }
1638
+ // Step 11: Write pad chords
1639
+ const padTracks = trackMappings.filter(m => m.spec.midiContent === 'pad_chords');
1640
+ if (padTracks.length > 0) {
1641
+ try {
1642
+ const padNotes = generatePadChords(preset);
1643
+ for (const padTrack of padTracks) {
1644
+ for (const note of padNotes) {
1645
+ osc.send('/live/clip/add/notes', padTrack.trackIndex, clipSlot, note.pitch, note.start, note.duration, note.velocity, 0);
1646
+ }
1647
+ }
1648
+ }
1649
+ catch (err) {
1650
+ report.errors.push(`Pad chord write error: ${err.message}`);
1651
+ }
1652
+ }
1653
+ await sleep(150);
1654
+ // Step 12: Apply mix — volumes, panning, sends
1655
+ try {
1656
+ // Build a lookup: role -> trackIndex, with fallback for dotted roles like 'drums.snare'
1657
+ const roleToIndex = new Map();
1658
+ for (const mapping of trackMappings) {
1659
+ roleToIndex.set(mapping.role, mapping.trackIndex);
1660
+ }
1661
+ // Also set dotted keys (e.g. drums.snare = second drums track, drums.clap = second drums track)
1662
+ const drumTrackList = trackMappings.filter(m => m.spec.role === 'drums');
1663
+ if (drumTrackList.length > 1) {
1664
+ roleToIndex.set('drums.snare', drumTrackList[1].trackIndex);
1665
+ roleToIndex.set('drums.clap', drumTrackList[1].trackIndex);
1666
+ }
1667
+ const padTrackList = trackMappings.filter(m => m.spec.role === 'pad');
1668
+ if (padTrackList.length > 1) {
1669
+ roleToIndex.set('pad.2', padTrackList[1].trackIndex);
1670
+ }
1671
+ // Apply volumes
1672
+ for (const [role, volume] of Object.entries(preset.mixTemplate.volumes)) {
1673
+ const trackIdx = roleToIndex.get(role);
1674
+ if (trackIdx === undefined)
1675
+ continue;
1676
+ osc.send('/live/track/set/volume', trackIdx, volume);
1677
+ }
1678
+ // Apply panning
1679
+ for (const [role, pan] of Object.entries(preset.mixTemplate.panning)) {
1680
+ const trackIdx = roleToIndex.get(role);
1681
+ if (trackIdx === undefined)
1682
+ continue;
1683
+ osc.send('/live/track/set/panning', trackIdx, pan);
1684
+ }
1685
+ // Apply sends
1686
+ for (const send of preset.mixTemplate.sends) {
1687
+ const trackIdx = roleToIndex.get(send.fromRole);
1688
+ if (trackIdx === undefined)
1689
+ continue;
1690
+ osc.send('/live/track/set/send', trackIdx, send.toReturn, send.level);
1691
+ }
1692
+ report.mixApplied = true;
1693
+ }
1694
+ catch (err) {
1695
+ report.errors.push(`Mix application failed: ${err.message}`);
1696
+ }
1697
+ // Step 13: Fire all clips and start playback
1698
+ try {
1699
+ // Set clip trigger quantization to 0 (immediate)
1700
+ osc.send('/live/song/set/clip_trigger_quantization', 0);
1701
+ await sleep(100);
1702
+ // Fire clip on every track (clip/fire auto-starts transport)
1703
+ for (const mapping of trackMappings) {
1704
+ osc.send('/live/clip/fire', mapping.trackIndex, clipSlot);
1705
+ }
1706
+ report.playing = true;
1707
+ }
1708
+ catch (err) {
1709
+ report.errors.push(`Clip fire failed: ${err.message}`);
1710
+ }
1711
+ return report;
1712
+ }
1713
+ // ── Report Formatter ────────────────────────────────────────────────────────
1714
+ function formatProductionReport(report) {
1715
+ const preset = GENRE_PRESETS[report.genre] || GENRE_PRESETS.trap;
1716
+ const lines = [];
1717
+ lines.push(`## ${preset.name} Beat — ${report.key} at ${report.bpm} BPM`);
1718
+ lines.push('');
1719
+ if (report.playing) {
1720
+ lines.push('**Status: Playing**');
1721
+ }
1722
+ else {
1723
+ lines.push('**Status: Built (not playing — check errors below)**');
1724
+ }
1725
+ lines.push('');
1726
+ // Musical info
1727
+ lines.push('### Musical Foundation');
1728
+ lines.push(`- **Key**: ${report.key}`);
1729
+ lines.push(`- **Scale**: ${report.scale}`);
1730
+ lines.push(`- **Tempo**: ${report.bpm} BPM`);
1731
+ lines.push(`- **Bars**: ${report.bars}`);
1732
+ lines.push(`- **Progression**: \`${report.progression}\``);
1733
+ lines.push(`- **Feel**: ${preset.feel}`);
1734
+ lines.push('');
1735
+ // Tracks
1736
+ lines.push('### Tracks Created');
1737
+ for (const track of report.tracksCreated) {
1738
+ lines.push(`- ${track}`);
1739
+ }
1740
+ lines.push('');
1741
+ // Instruments
1742
+ if (report.instrumentsLoaded.length > 0) {
1743
+ lines.push('### Instruments Loaded');
1744
+ for (const inst of report.instrumentsLoaded) {
1745
+ lines.push(`- ${inst}`);
1746
+ }
1747
+ lines.push('');
1748
+ }
1749
+ // Mix
1750
+ if (report.mixApplied) {
1751
+ lines.push('### Mix Applied');
1752
+ lines.push(`- Volumes, panning, and sends set to ${preset.name} template`);
1753
+ lines.push(`- Target: ${preset.mixTemplate.targetLUFS} LUFS`);
1754
+ if (preset.mixTemplate.returns.length > 0) {
1755
+ lines.push(`- Returns: ${preset.mixTemplate.returns.map(r => r.name).join(', ')}`);
1756
+ }
1757
+ lines.push('');
1758
+ }
1759
+ // Production notes
1760
+ lines.push('### Production Notes');
1761
+ for (const note of preset.productionNotes.slice(0, 3)) {
1762
+ lines.push(`- ${note}`);
1763
+ }
1764
+ lines.push('');
1765
+ // Recommended next steps
1766
+ lines.push('### Recommended Next Steps');
1767
+ lines.push(`- Add ${preset.mixTemplate.masterChain.join(' -> ')} to the master channel`);
1768
+ if (preset.mixTemplate.returns.length > 0) {
1769
+ for (const ret of preset.mixTemplate.returns) {
1770
+ lines.push(`- Set up ${ret.name} return: ${ret.presetHint}`);
1771
+ }
1772
+ }
1773
+ lines.push('- Adjust individual instrument presets to taste');
1774
+ lines.push('- Fine-tune the mix with `ableton_mixer` or `ableton_device`');
1775
+ lines.push('');
1776
+ // Errors
1777
+ if (report.errors.length > 0) {
1778
+ lines.push('### Warnings');
1779
+ for (const err of report.errors) {
1780
+ lines.push(`- ${err}`);
1781
+ }
1782
+ lines.push('');
1783
+ }
1784
+ return lines.join('\n');
1785
+ }
1786
+ // ── Tool Registration ───────────────────────────────────────────────────────
1787
+ export function registerProducerEngine() {
1788
+ registerTool({
1789
+ name: 'produce_beat',
1790
+ description: 'One-shot beat production. Specify a genre and get a complete, mixed, playing beat in Ableton Live. Creates tracks, loads instruments, writes drums, bass, chords, melody, pads, applies mix settings, and fires all clips. Supports: trap, drill, lofi, house, rnb, phonk, pluggnb, ambient.',
1791
+ parameters: {
1792
+ genre: {
1793
+ type: 'string',
1794
+ description: 'Genre: trap, drill, lofi, house, rnb, phonk, pluggnb, ambient',
1795
+ required: true,
1796
+ },
1797
+ key: {
1798
+ type: 'string',
1799
+ description: 'Musical key override (e.g. "Cm", "Em", "F#m", "Bb"). Random from genre defaults if omitted.',
1800
+ },
1801
+ bpm: {
1802
+ type: 'number',
1803
+ description: 'BPM override. Random from genre range if omitted.',
1804
+ },
1805
+ bars: {
1806
+ type: 'number',
1807
+ description: 'Bars per section (default: from genre preset, usually 4 or 8).',
1808
+ },
1809
+ instruments: {
1810
+ type: 'string',
1811
+ description: 'Instrument preference override (e.g. "roland" to prefer Roland Cloud, "ua" for UA plugins). Default uses Ableton native.',
1812
+ },
1813
+ },
1814
+ tier: 'free',
1815
+ timeout: 60_000,
1816
+ async execute(args) {
1817
+ const genre = String(args.genre).toLowerCase().replace(/[- ]/g, '');
1818
+ // Normalize genre aliases
1819
+ const genreMap = {
1820
+ 'trap': 'trap',
1821
+ 'drill': 'drill',
1822
+ 'ukdrill': 'drill',
1823
+ 'lofi': 'lofi',
1824
+ 'lo-fi': 'lofi',
1825
+ 'chillhop': 'lofi',
1826
+ 'house': 'house',
1827
+ 'deephouse': 'house',
1828
+ 'rnb': 'rnb',
1829
+ 'r&b': 'rnb',
1830
+ 'phonk': 'phonk',
1831
+ 'pluggnb': 'pluggnb',
1832
+ 'plugg': 'pluggnb',
1833
+ 'ambient': 'ambient',
1834
+ 'chill': 'lofi',
1835
+ 'hiphop': 'lofi',
1836
+ 'boombap': 'lofi',
1837
+ };
1838
+ const normalizedGenre = genreMap[genre] || 'trap';
1839
+ const overrides = {};
1840
+ if (args.key)
1841
+ overrides.key = String(args.key);
1842
+ if (args.bpm)
1843
+ overrides.bpm = Number(args.bpm);
1844
+ if (args.bars)
1845
+ overrides.bars = Number(args.bars);
1846
+ try {
1847
+ const report = await executeProductionPipeline(normalizedGenre, overrides);
1848
+ return formatProductionReport(report);
1849
+ }
1850
+ catch (err) {
1851
+ if (err.message?.includes('AbletonOSC') || err.message?.includes('not connected')) {
1852
+ return `Ableton connection failed.\n\n${formatAbletonError()}`;
1853
+ }
1854
+ return `Production failed: ${err.message}`;
1855
+ }
1856
+ },
1857
+ });
1858
+ }
1859
+ //# sourceMappingURL=producer-engine.js.map