@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,896 @@
1
+ // kbot Sound Designer — AI-powered synthesizer programming from text descriptions
2
+ // "dark 808 with tape distortion" → loads synth + sets all parameters
3
+ //
4
+ // Tools:
5
+ // design_sound — Parse a text description and program a synth on a track
6
+ //
7
+ // Requires: AbletonOSC loaded in Ableton Live
8
+ import { registerTool } from './index.js';
9
+ import { ensureAbleton } from '../integrations/ableton-osc.js';
10
+ // ── Helpers ──────────────────────────────────────────────────────────────────
11
+ function extractArgs(args) {
12
+ return args.map(a => {
13
+ if (a.type === 'b')
14
+ return '[blob]';
15
+ return a.value;
16
+ });
17
+ }
18
+ function userTrack(track) {
19
+ const n = Number(track);
20
+ return Math.max(0, n - 1);
21
+ }
22
+ /**
23
+ * Set a device parameter by name. Fetches the parameter list, finds the index
24
+ * matching `paramName`, then sets by index.
25
+ */
26
+ async function setParamByName(osc, track, device, paramName, value) {
27
+ const paramNames = await osc.query('/live/device/get/parameters/name', track, device);
28
+ const names = extractArgs(paramNames).slice(2); // skip track + device idx echo
29
+ const idx = names.findIndex(n => String(n).toLowerCase() === paramName.toLowerCase());
30
+ if (idx === -1)
31
+ return false;
32
+ osc.send('/live/device/set/parameter/value', track, device, idx, value);
33
+ return true;
34
+ }
35
+ /**
36
+ * Batch-set multiple parameters by name. Returns which ones succeeded/failed.
37
+ */
38
+ async function setParamsBatch(osc, track, device, params) {
39
+ // Fetch all parameter names once
40
+ const paramNames = await osc.query('/live/device/get/parameters/name', track, device);
41
+ const names = extractArgs(paramNames).slice(2);
42
+ const nameMap = new Map();
43
+ for (let i = 0; i < names.length; i++) {
44
+ nameMap.set(String(names[i]).toLowerCase(), i);
45
+ }
46
+ const set = [];
47
+ const failed = [];
48
+ for (const [name, value] of Object.entries(params)) {
49
+ const idx = nameMap.get(name.toLowerCase());
50
+ if (idx !== undefined) {
51
+ osc.send('/live/device/set/parameter/value', track, device, idx, value);
52
+ set.push(name);
53
+ }
54
+ else {
55
+ failed.push(name);
56
+ }
57
+ }
58
+ return { set, failed };
59
+ }
60
+ // ── Sound Recipes ────────────────────────────────────────────────────────────
61
+ // Each recipe maps to a synth + parameter settings. Parameter names match
62
+ // Ableton's exposed parameter names through AbletonOSC.
63
+ const SOUND_RECIPES = {
64
+ // ─── 808 Bass ──────────────────────────────────────────────────────────
65
+ '808_basic': {
66
+ synth: 'Operator',
67
+ params: {
68
+ 'Osc-A Wave': 0, // Sine
69
+ 'Osc-A Level': 1.0,
70
+ 'Osc-A Coarse': 0,
71
+ 'Algorithm': 0, // Single carrier
72
+ 'Ae Attack': 0,
73
+ 'Ae Decay': 3.0,
74
+ 'Ae Sustain': 0,
75
+ 'Ae Release': 0.5,
76
+ 'Osc-B On': 0,
77
+ 'Osc-C On': 0,
78
+ 'Osc-D On': 0,
79
+ 'Volume': 0.85,
80
+ },
81
+ description: 'Pure sine 808 sub bass — clean, round, deep',
82
+ },
83
+ '808_dirty': {
84
+ synth: 'Operator',
85
+ params: {
86
+ 'Osc-A Wave': 0, // Sine
87
+ 'Osc-A Level': 1.0,
88
+ 'Algorithm': 2, // B modulates A
89
+ 'Osc-B On': 1,
90
+ 'Osc-B Wave': 0, // Sine
91
+ 'Osc-B Level': 0.35,
92
+ 'Osc-B Coarse': 1,
93
+ 'Be Decay': 0.8,
94
+ 'Be Sustain': 0,
95
+ 'Ae Attack': 0,
96
+ 'Ae Decay': 2.5,
97
+ 'Ae Sustain': 0,
98
+ 'Ae Release': 0.4,
99
+ 'Osc-C On': 0,
100
+ 'Osc-D On': 0,
101
+ 'Volume': 0.85,
102
+ },
103
+ description: 'Dirty/dark 808 — FM distortion on the attack, gritty harmonics',
104
+ effects: [{
105
+ name: 'Saturator',
106
+ params: { 'Drive': 12, 'Type': 2, 'Output': -3, 'Dry/Wet': 0.6 },
107
+ }],
108
+ },
109
+ '808_slide': {
110
+ synth: 'Operator',
111
+ params: {
112
+ 'Osc-A Wave': 0,
113
+ 'Osc-A Level': 1.0,
114
+ 'Algorithm': 0,
115
+ 'Ae Attack': 0,
116
+ 'Ae Decay': 3.5,
117
+ 'Ae Sustain': 0.15,
118
+ 'Ae Release': 0.8,
119
+ 'Osc-B On': 0,
120
+ 'Osc-C On': 0,
121
+ 'Osc-D On': 0,
122
+ 'Glide Time': 0.15,
123
+ 'Volume': 0.85,
124
+ },
125
+ description: '808 with glide/portamento for slides between notes',
126
+ },
127
+ '808_hardclip': {
128
+ synth: 'Operator',
129
+ params: {
130
+ 'Osc-A Wave': 0,
131
+ 'Osc-A Level': 1.0,
132
+ 'Algorithm': 0,
133
+ 'Ae Attack': 0,
134
+ 'Ae Decay': 2.0,
135
+ 'Ae Sustain': 0,
136
+ 'Ae Release': 0.3,
137
+ 'Osc-B On': 0,
138
+ 'Osc-C On': 0,
139
+ 'Osc-D On': 0,
140
+ 'Volume': 1.0,
141
+ },
142
+ description: 'Hard-clipped 808 — slammed into a limiter for aggressive punch',
143
+ effects: [{
144
+ name: 'Saturator',
145
+ params: { 'Drive': 20, 'Type': 4, 'Output': -6, 'Dry/Wet': 1.0 },
146
+ }],
147
+ },
148
+ // ─── Sub Bass ──────────────────────────────────────────────────────────
149
+ 'sub_sine': {
150
+ synth: 'Operator',
151
+ params: {
152
+ 'Osc-A Wave': 0, // Sine
153
+ 'Osc-A Level': 1.0,
154
+ 'Algorithm': 0,
155
+ 'Ae Attack': 0.01,
156
+ 'Ae Decay': 0,
157
+ 'Ae Sustain': 1.0,
158
+ 'Ae Release': 0.15,
159
+ 'Osc-B On': 0,
160
+ 'Osc-C On': 0,
161
+ 'Osc-D On': 0,
162
+ 'Volume': 0.9,
163
+ },
164
+ description: 'Pure sine sub bass — sustained, fundamental-only',
165
+ },
166
+ 'sub_triangle': {
167
+ synth: 'Analog',
168
+ params: {
169
+ 'Osc1 Shape': 0.0, // Triangle
170
+ 'Osc1 Octave': -1,
171
+ 'Osc1 Level': 1.0,
172
+ 'Osc2 On/Off': 0,
173
+ 'F1 Freq': 200,
174
+ 'F1 Res': 0.1,
175
+ 'F1 Type': 0, // LP
176
+ 'Amp1 Attack': 0.01,
177
+ 'Amp1 Decay': 0,
178
+ 'Amp1 Sustain': 1.0,
179
+ 'Amp1 Release': 0.15,
180
+ 'Volume': 0.9,
181
+ },
182
+ description: 'Triangle sub bass — slightly more overtones than sine',
183
+ },
184
+ 'sub_warm': {
185
+ synth: 'Analog',
186
+ params: {
187
+ 'Osc1 Shape': 0.5, // Saw-ish
188
+ 'Osc1 Octave': -1,
189
+ 'Osc1 Level': 1.0,
190
+ 'Osc2 On/Off': 0,
191
+ 'F1 Freq': 120,
192
+ 'F1 Res': 0.2,
193
+ 'F1 Type': 0, // LP
194
+ 'Amp1 Attack': 0.02,
195
+ 'Amp1 Decay': 0.3,
196
+ 'Amp1 Sustain': 0.85,
197
+ 'Amp1 Release': 0.2,
198
+ 'Volume': 0.85,
199
+ },
200
+ description: 'Warm sub bass — filtered saw, round low end',
201
+ },
202
+ // ─── Leads ─────────────────────────────────────────────────────────────
203
+ 'dark_bell': {
204
+ synth: 'Operator',
205
+ params: {
206
+ 'Algorithm': 4, // FM: B→A, D→C
207
+ 'Osc-A Wave': 0, // Sine
208
+ 'Osc-A Level': 1.0,
209
+ 'Osc-B On': 1,
210
+ 'Osc-B Wave': 0,
211
+ 'Osc-B Level': 0.5,
212
+ 'Osc-B Coarse': 3.5, // Inharmonic ratio
213
+ 'Osc-B Fine': 0.07,
214
+ 'Be Attack': 0,
215
+ 'Be Decay': 2.0,
216
+ 'Be Sustain': 0,
217
+ 'Ae Attack': 0,
218
+ 'Ae Decay': 3.0,
219
+ 'Ae Sustain': 0,
220
+ 'Ae Release': 1.0,
221
+ 'Osc-C On': 0,
222
+ 'Osc-D On': 0,
223
+ 'Volume': 0.7,
224
+ },
225
+ description: 'Dark FM bell — inharmonic ratios, metallic decay',
226
+ },
227
+ 'bright_lead': {
228
+ synth: 'Analog',
229
+ params: {
230
+ 'Osc1 Shape': 1.0, // Saw
231
+ 'Osc1 Octave': 0,
232
+ 'Osc1 Level': 0.8,
233
+ 'Osc2 On/Off': 1,
234
+ 'Osc2 Shape': 1.0, // Saw
235
+ 'Osc2 Octave': 0,
236
+ 'Osc2 Level': 0.8,
237
+ 'Osc2 Detune': 0.12,
238
+ 'F1 Freq': 3500,
239
+ 'F1 Res': 0.3,
240
+ 'F1 Type': 0, // LP
241
+ 'FE1 Attack': 0.01,
242
+ 'FE1 Decay': 0.3,
243
+ 'FE1 Sustain': 0.5,
244
+ 'FE1 Amount': 0.4,
245
+ 'Amp1 Attack': 0.01,
246
+ 'Amp1 Decay': 0,
247
+ 'Amp1 Sustain': 0.9,
248
+ 'Amp1 Release': 0.15,
249
+ 'Volume': 0.75,
250
+ },
251
+ description: 'Bright detuned saw lead — punchy, present, slightly wide',
252
+ },
253
+ 'acid_lead': {
254
+ synth: 'Analog',
255
+ params: {
256
+ 'Osc1 Shape': 1.0, // Saw
257
+ 'Osc1 Octave': 0,
258
+ 'Osc1 Level': 1.0,
259
+ 'Osc2 On/Off': 0,
260
+ 'F1 Freq': 800,
261
+ 'F1 Res': 0.7, // High resonance = acid squelch
262
+ 'F1 Type': 0, // LP
263
+ 'FE1 Attack': 0.001,
264
+ 'FE1 Decay': 0.25,
265
+ 'FE1 Sustain': 0,
266
+ 'FE1 Amount': 0.8, // Heavy filter envelope
267
+ 'Amp1 Attack': 0.001,
268
+ 'Amp1 Decay': 0.3,
269
+ 'Amp1 Sustain': 0,
270
+ 'Amp1 Release': 0.1,
271
+ 'Volume': 0.75,
272
+ },
273
+ description: 'Acid lead — resonant filter sweep, 303-style squelch',
274
+ },
275
+ 'supersaw_lead': {
276
+ synth: 'Wavetable',
277
+ params: {
278
+ 'Osc 1 Pos': 0.8, // Saw-like position
279
+ 'Osc 1 Effect Type': 1, // FM
280
+ 'Osc 1 Effect Amount': 0.3,
281
+ 'Sub On': 1,
282
+ 'Sub Gain': -12,
283
+ 'Sub Tone': 0.5,
284
+ 'Filter Type': 0, // LP
285
+ 'Filter Freq': 5000,
286
+ 'Filter Res': 0.15,
287
+ 'Amp Attack': 0.01,
288
+ 'Amp Decay': 0,
289
+ 'Amp Sustain': 1.0,
290
+ 'Amp Release': 0.2,
291
+ 'Unison Amount': 0.5,
292
+ 'Unison Voices': 4,
293
+ 'Volume': 0.75,
294
+ },
295
+ description: 'Supersaw lead — wide unison, thick and full',
296
+ },
297
+ // ─── Pads ──────────────────────────────────────────────────────────────
298
+ 'dark_pad': {
299
+ synth: 'Drift',
300
+ params: {
301
+ 'Osc Shape': 0.6, // Between triangle and saw
302
+ 'Drift Amount': 0.4, // Analog drift
303
+ 'Filter Freq': 600,
304
+ 'Filter Res': 0.15,
305
+ 'Filter Type': 0, // LP
306
+ 'Amp Attack': 1.5,
307
+ 'Amp Decay': 2.0,
308
+ 'Amp Sustain': 0.6,
309
+ 'Amp Release': 2.0,
310
+ 'LFO Rate': 0.3,
311
+ 'LFO Amount': 0.15,
312
+ 'Volume': 0.7,
313
+ },
314
+ description: 'Dark ambient pad — slow drift, filtered, brooding',
315
+ },
316
+ 'warm_pad': {
317
+ synth: 'Analog',
318
+ params: {
319
+ 'Osc1 Shape': 1.0, // Saw
320
+ 'Osc1 Octave': 0,
321
+ 'Osc1 Level': 0.7,
322
+ 'Osc2 On/Off': 1,
323
+ 'Osc2 Shape': 1.0, // Saw
324
+ 'Osc2 Octave': 0,
325
+ 'Osc2 Level': 0.7,
326
+ 'Osc2 Detune': 0.08,
327
+ 'F1 Freq': 1500,
328
+ 'F1 Res': 0.1,
329
+ 'F1 Type': 0, // LP
330
+ 'Amp1 Attack': 0.8,
331
+ 'Amp1 Decay': 1.0,
332
+ 'Amp1 Sustain': 0.7,
333
+ 'Amp1 Release': 1.5,
334
+ 'Volume': 0.7,
335
+ },
336
+ description: 'Warm pad — detuned saws, low-pass filtered, lush',
337
+ },
338
+ 'ambient_pad': {
339
+ synth: 'Wavetable',
340
+ params: {
341
+ 'Osc 1 Pos': 0.4,
342
+ 'Osc 1 Effect Type': 2, // Classic
343
+ 'Osc 1 Effect Amount': 0.2,
344
+ 'Filter Type': 0, // LP
345
+ 'Filter Freq': 2000,
346
+ 'Filter Res': 0.1,
347
+ 'Amp Attack': 2.5,
348
+ 'Amp Decay': 3.0,
349
+ 'Amp Sustain': 0.5,
350
+ 'Amp Release': 4.0,
351
+ 'Mod Amount A': 0.2, // Slow modulation
352
+ 'Volume': 0.65,
353
+ },
354
+ description: 'Ambient pad — evolving wavetable, very slow attack, spacious',
355
+ effects: [{
356
+ name: 'Reverb',
357
+ params: { 'Decay Time': 6.0, 'Room Size': 0.8, 'Dry/Wet': 0.5 },
358
+ }],
359
+ },
360
+ 'shimmer_pad': {
361
+ synth: 'Wavetable',
362
+ params: {
363
+ 'Osc 1 Pos': 0.6,
364
+ 'Osc 1 Effect Type': 1, // FM
365
+ 'Osc 1 Effect Amount': 0.15,
366
+ 'Filter Type': 1, // HP
367
+ 'Filter Freq': 800,
368
+ 'Filter Res': 0.2,
369
+ 'Amp Attack': 1.5,
370
+ 'Amp Decay': 2.0,
371
+ 'Amp Sustain': 0.6,
372
+ 'Amp Release': 3.5,
373
+ 'Unison Amount': 0.4,
374
+ 'Unison Voices': 3,
375
+ 'Volume': 0.6,
376
+ },
377
+ description: 'Shimmer pad — bright, airy, high-pass filtered with unison spread',
378
+ effects: [
379
+ { name: 'Chorus', params: { 'Rate 1': 0.5, 'Amount 1': 0.3, 'Dry/Wet': 0.4 } },
380
+ { name: 'Reverb', params: { 'Decay Time': 5.0, 'Room Size': 0.85, 'Dry/Wet': 0.45 } },
381
+ ],
382
+ },
383
+ 'strings_pad': {
384
+ synth: 'Analog',
385
+ params: {
386
+ 'Osc1 Shape': 1.0, // Saw
387
+ 'Osc1 Octave': 0,
388
+ 'Osc1 Level': 0.6,
389
+ 'Osc2 On/Off': 1,
390
+ 'Osc2 Shape': 1.0, // Saw
391
+ 'Osc2 Octave': 1,
392
+ 'Osc2 Level': 0.4,
393
+ 'Osc2 Detune': 0.06,
394
+ 'F1 Freq': 2500,
395
+ 'F1 Res': 0.05,
396
+ 'F1 Type': 0, // LP
397
+ 'Amp1 Attack': 0.6,
398
+ 'Amp1 Decay': 0.5,
399
+ 'Amp1 Sustain': 0.8,
400
+ 'Amp1 Release': 1.0,
401
+ 'Volume': 0.7,
402
+ },
403
+ description: 'String ensemble pad — layered octave saws, gentle filter',
404
+ effects: [{
405
+ name: 'Chorus',
406
+ params: { 'Rate 1': 0.8, 'Amount 1': 0.25, 'Dry/Wet': 0.35 },
407
+ }],
408
+ },
409
+ // ─── Keys ──────────────────────────────────────────────────────────────
410
+ 'rhodes': {
411
+ synth: 'Electric',
412
+ params: {
413
+ 'M Stiffness': 0.4,
414
+ 'M Force': 0.5,
415
+ 'F Tine Color': 0.5,
416
+ 'F Tine Decay': 0.6,
417
+ 'F Tine Vol': 0.8,
418
+ 'F Tone Color': 0.4,
419
+ 'F Tone Decay': 0.5,
420
+ 'F Tone Vol': 0.6,
421
+ 'P Pickup Sym': 0.5,
422
+ 'P Pickup Dist': 0.3,
423
+ 'Damper On': 1,
424
+ 'D Damper Tone': 0.5,
425
+ 'Volume': 0.75,
426
+ },
427
+ description: 'Rhodes electric piano — warm, magnetic pickup, classic tone',
428
+ },
429
+ 'wurlitzer': {
430
+ synth: 'Electric',
431
+ params: {
432
+ 'M Stiffness': 0.55,
433
+ 'M Force': 0.6,
434
+ 'F Tine Color': 0.65,
435
+ 'F Tine Decay': 0.5,
436
+ 'F Tine Vol': 0.7,
437
+ 'F Tone Color': 0.6,
438
+ 'F Tone Decay': 0.4,
439
+ 'F Tone Vol': 0.5,
440
+ 'P Pickup Sym': 0.6,
441
+ 'P Pickup Dist': 0.4,
442
+ 'Damper On': 1,
443
+ 'D Damper Tone': 0.6,
444
+ 'Volume': 0.75,
445
+ },
446
+ description: 'Wurlitzer — brighter, more bark, piezo-style character',
447
+ },
448
+ 'organ': {
449
+ synth: 'Operator',
450
+ params: {
451
+ 'Algorithm': 10, // Additive — all carriers
452
+ 'Osc-A Wave': 0, // Sine
453
+ 'Osc-A Level': 1.0,
454
+ 'Osc-A Coarse': 0, // 8 foot
455
+ 'Osc-B On': 1,
456
+ 'Osc-B Wave': 0,
457
+ 'Osc-B Level': 0.7,
458
+ 'Osc-B Coarse': 12, // 4 foot
459
+ 'Osc-C On': 1,
460
+ 'Osc-C Wave': 0,
461
+ 'Osc-C Level': 0.5,
462
+ 'Osc-C Coarse': 19, // 2 2/3 foot
463
+ 'Osc-D On': 1,
464
+ 'Osc-D Wave': 0,
465
+ 'Osc-D Level': 0.4,
466
+ 'Osc-D Coarse': 24, // 2 foot
467
+ 'Ae Attack': 0.005,
468
+ 'Ae Decay': 0,
469
+ 'Ae Sustain': 1.0,
470
+ 'Ae Release': 0.08,
471
+ 'Volume': 0.7,
472
+ },
473
+ description: 'Organ — additive sine drawbars (8\', 4\', 2 2/3\', 2\')',
474
+ },
475
+ // ─── Plucks ────────────────────────────────────────────────────────────
476
+ 'pluck_acoustic': {
477
+ synth: 'Wavetable',
478
+ params: {
479
+ 'Osc 1 Pos': 0.3,
480
+ 'Filter Type': 0, // LP
481
+ 'Filter Freq': 4000,
482
+ 'Filter Res': 0.1,
483
+ 'Fe Attack': 0,
484
+ 'Fe Decay': 0.15,
485
+ 'Fe Sustain': 0,
486
+ 'Fe Amount': 0.5,
487
+ 'Amp Attack': 0.001,
488
+ 'Amp Decay': 0.4,
489
+ 'Amp Sustain': 0,
490
+ 'Amp Release': 0.2,
491
+ 'Volume': 0.75,
492
+ },
493
+ description: 'Acoustic pluck — short filter envelope, natural decay',
494
+ },
495
+ 'pluck_digital': {
496
+ synth: 'Wavetable',
497
+ params: {
498
+ 'Osc 1 Pos': 0.7, // Digital/harmonic-rich position
499
+ 'Osc 1 Effect Type': 1, // FM
500
+ 'Osc 1 Effect Amount': 0.25,
501
+ 'Filter Type': 0, // LP
502
+ 'Filter Freq': 6000,
503
+ 'Filter Res': 0.2,
504
+ 'Fe Attack': 0,
505
+ 'Fe Decay': 0.1,
506
+ 'Fe Sustain': 0,
507
+ 'Fe Amount': 0.6,
508
+ 'Amp Attack': 0,
509
+ 'Amp Decay': 0.3,
510
+ 'Amp Sustain': 0,
511
+ 'Amp Release': 0.15,
512
+ 'Volume': 0.7,
513
+ },
514
+ description: 'Digital pluck — FM-modulated wavetable, crisp and synthetic',
515
+ },
516
+ 'pluck_metallic': {
517
+ synth: 'Operator',
518
+ params: {
519
+ 'Algorithm': 2,
520
+ 'Osc-A Wave': 0,
521
+ 'Osc-A Level': 1.0,
522
+ 'Osc-B On': 1,
523
+ 'Osc-B Wave': 0,
524
+ 'Osc-B Level': 0.6,
525
+ 'Osc-B Coarse': 7, // Inharmonic
526
+ 'Osc-B Fine': 0.03,
527
+ 'Be Attack': 0,
528
+ 'Be Decay': 0.08,
529
+ 'Be Sustain': 0,
530
+ 'Ae Attack': 0,
531
+ 'Ae Decay': 0.5,
532
+ 'Ae Sustain': 0,
533
+ 'Ae Release': 0.3,
534
+ 'Osc-C On': 0,
535
+ 'Osc-D On': 0,
536
+ 'Volume': 0.7,
537
+ },
538
+ description: 'Metallic pluck — inharmonic FM, bell-like attack, short decay',
539
+ },
540
+ // ─── FX ────────────────────────────────────────────────────────────────
541
+ 'fx_riser': {
542
+ synth: 'Wavetable',
543
+ params: {
544
+ 'Osc 1 Pos': 0.5,
545
+ 'Osc 1 Effect Type': 2, // Classic
546
+ 'Osc 1 Effect Amount': 0.3,
547
+ 'Filter Type': 0, // LP
548
+ 'Filter Freq': 200,
549
+ 'Filter Res': 0.3,
550
+ 'Fe Attack': 4.0, // Slow filter open = riser
551
+ 'Fe Decay': 0,
552
+ 'Fe Sustain': 1.0,
553
+ 'Fe Amount': 0.9,
554
+ 'Amp Attack': 0.5,
555
+ 'Amp Decay': 0,
556
+ 'Amp Sustain': 1.0,
557
+ 'Amp Release': 0.3,
558
+ 'Unison Amount': 0.6,
559
+ 'Unison Voices': 4,
560
+ 'Volume': 0.7,
561
+ },
562
+ description: 'Riser — slow filter sweep upward, building tension',
563
+ },
564
+ 'fx_noise_sweep': {
565
+ synth: 'Operator',
566
+ params: {
567
+ 'Algorithm': 0,
568
+ 'Osc-A Wave': 5, // Noise
569
+ 'Osc-A Level': 1.0,
570
+ 'Ae Attack': 2.0,
571
+ 'Ae Decay': 3.0,
572
+ 'Ae Sustain': 0,
573
+ 'Ae Release': 1.0,
574
+ 'Filter On': 1,
575
+ 'Filter Freq': 400,
576
+ 'Filter Res': 0.4,
577
+ 'Fe Attack': 3.0,
578
+ 'Fe Decay': 0,
579
+ 'Fe Sustain': 1.0,
580
+ 'Fe Amount': 0.8,
581
+ 'Osc-B On': 0,
582
+ 'Osc-C On': 0,
583
+ 'Osc-D On': 0,
584
+ 'Volume': 0.6,
585
+ },
586
+ description: 'Noise sweep — filtered noise rising and fading, transition FX',
587
+ },
588
+ 'fx_impact': {
589
+ synth: 'Operator',
590
+ params: {
591
+ 'Algorithm': 2,
592
+ 'Osc-A Wave': 0, // Sine
593
+ 'Osc-A Level': 1.0,
594
+ 'Osc-A Coarse': -12, // Very low
595
+ 'Osc-B On': 1,
596
+ 'Osc-B Wave': 5, // Noise
597
+ 'Osc-B Level': 0.7,
598
+ 'Be Attack': 0,
599
+ 'Be Decay': 0.05,
600
+ 'Be Sustain': 0,
601
+ 'Ae Attack': 0,
602
+ 'Ae Decay': 1.5,
603
+ 'Ae Sustain': 0,
604
+ 'Ae Release': 0.5,
605
+ 'Osc-C On': 0,
606
+ 'Osc-D On': 0,
607
+ 'Volume': 0.85,
608
+ },
609
+ description: 'Impact — low boom + noise burst, cinematic hit',
610
+ effects: [{
611
+ name: 'Reverb',
612
+ params: { 'Decay Time': 3.0, 'Room Size': 0.7, 'Dry/Wet': 0.35 },
613
+ }],
614
+ },
615
+ };
616
+ // ── Keyword Matching Rules ───────────────────────────────────────────────────
617
+ // Ordered by specificity — first match wins. More specific patterns first.
618
+ const MATCH_RULES = [
619
+ // 808 variants (most specific first)
620
+ { keywords: ['808', 'slide', 'glide'], recipe: '808_slide' },
621
+ { keywords: ['808', 'hard', 'clip'], recipe: '808_hardclip' },
622
+ { keywords: ['808', 'clip'], recipe: '808_hardclip' },
623
+ { keywords: ['808', 'dirty'], recipe: '808_dirty' },
624
+ { keywords: ['808', 'dark'], recipe: '808_dirty' },
625
+ { keywords: ['808', 'distort'], recipe: '808_dirty' },
626
+ { keywords: ['808', 'tape'], recipe: '808_dirty' },
627
+ { keywords: ['808', 'grit'], recipe: '808_dirty' },
628
+ { keywords: ['808', 'saturate'], recipe: '808_dirty' },
629
+ { keywords: ['808', 'clean'], recipe: '808_basic' },
630
+ { keywords: ['808', 'sub'], recipe: '808_basic' },
631
+ { keywords: ['808', 'pure'], recipe: '808_basic' },
632
+ { keywords: ['808'], recipe: '808_basic' },
633
+ // Sub bass
634
+ { keywords: ['sub', 'warm'], recipe: 'sub_warm' },
635
+ { keywords: ['sub', 'triangle'], recipe: 'sub_triangle' },
636
+ { keywords: ['sub', 'tri'], recipe: 'sub_triangle' },
637
+ { keywords: ['sub', 'sine'], recipe: 'sub_sine' },
638
+ { keywords: ['sub', 'bass'], recipe: 'sub_sine' },
639
+ { keywords: ['sub'], recipe: 'sub_sine' },
640
+ // Leads
641
+ { keywords: ['acid'], recipe: 'acid_lead' },
642
+ { keywords: ['303'], recipe: 'acid_lead' },
643
+ { keywords: ['supersaw'], recipe: 'supersaw_lead' },
644
+ { keywords: ['super', 'saw'], recipe: 'supersaw_lead' },
645
+ { keywords: ['bell', 'dark'], recipe: 'dark_bell' },
646
+ { keywords: ['bell', 'metal'], recipe: 'dark_bell' },
647
+ { keywords: ['bell'], recipe: 'dark_bell' },
648
+ { keywords: ['lead', 'bright'], recipe: 'bright_lead' },
649
+ { keywords: ['lead', 'saw'], recipe: 'bright_lead' },
650
+ { keywords: ['lead', 'dark'], recipe: 'acid_lead' },
651
+ { keywords: ['lead', 'acid'], recipe: 'acid_lead' },
652
+ { keywords: ['lead'], recipe: 'bright_lead' },
653
+ // Pads
654
+ { keywords: ['pad', 'shimmer'], recipe: 'shimmer_pad' },
655
+ { keywords: ['pad', 'sparkle'], recipe: 'shimmer_pad' },
656
+ { keywords: ['pad', 'bright'], recipe: 'shimmer_pad' },
657
+ { keywords: ['pad', 'string'], recipe: 'strings_pad' },
658
+ { keywords: ['pad', 'ensemble'], recipe: 'strings_pad' },
659
+ { keywords: ['pad', 'ambient'], recipe: 'ambient_pad' },
660
+ { keywords: ['pad', 'evolve'], recipe: 'ambient_pad' },
661
+ { keywords: ['pad', 'space'], recipe: 'ambient_pad' },
662
+ { keywords: ['pad', 'warm'], recipe: 'warm_pad' },
663
+ { keywords: ['pad', 'lush'], recipe: 'warm_pad' },
664
+ { keywords: ['pad', 'dark'], recipe: 'dark_pad' },
665
+ { keywords: ['pad', 'moody'], recipe: 'dark_pad' },
666
+ { keywords: ['pad'], recipe: 'warm_pad' },
667
+ { keywords: ['string'], recipe: 'strings_pad' },
668
+ { keywords: ['ambient'], recipe: 'ambient_pad' },
669
+ // Keys
670
+ { keywords: ['rhodes'], recipe: 'rhodes' },
671
+ { keywords: ['wurlitzer'], recipe: 'wurlitzer' },
672
+ { keywords: ['wurli'], recipe: 'wurlitzer' },
673
+ { keywords: ['organ'], recipe: 'organ' },
674
+ { keywords: ['electric', 'piano'], recipe: 'rhodes' },
675
+ { keywords: ['ep'], recipe: 'rhodes' },
676
+ { keywords: ['keys', 'warm'], recipe: 'rhodes' },
677
+ { keywords: ['keys', 'bright'], recipe: 'wurlitzer' },
678
+ { keywords: ['keys'], recipe: 'rhodes' },
679
+ { keywords: ['piano'], recipe: 'rhodes' },
680
+ // Plucks
681
+ { keywords: ['pluck', 'metal'], recipe: 'pluck_metallic' },
682
+ { keywords: ['pluck', 'digital'], recipe: 'pluck_digital' },
683
+ { keywords: ['pluck', 'synth'], recipe: 'pluck_digital' },
684
+ { keywords: ['pluck', 'acoustic'], recipe: 'pluck_acoustic' },
685
+ { keywords: ['pluck', 'natural'], recipe: 'pluck_acoustic' },
686
+ { keywords: ['pluck'], recipe: 'pluck_acoustic' },
687
+ // FX
688
+ { keywords: ['riser'], recipe: 'fx_riser' },
689
+ { keywords: ['rise'], recipe: 'fx_riser' },
690
+ { keywords: ['build', 'up'], recipe: 'fx_riser' },
691
+ { keywords: ['sweep', 'noise'], recipe: 'fx_noise_sweep' },
692
+ { keywords: ['noise', 'sweep'], recipe: 'fx_noise_sweep' },
693
+ { keywords: ['whoosh'], recipe: 'fx_noise_sweep' },
694
+ { keywords: ['impact'], recipe: 'fx_impact' },
695
+ { keywords: ['hit'], recipe: 'fx_impact' },
696
+ { keywords: ['boom'], recipe: 'fx_impact' },
697
+ ];
698
+ // Map user-facing synth names to Ableton instrument names
699
+ const SYNTH_NAME_MAP = {
700
+ 'operator': 'Operator',
701
+ 'wavetable': 'Wavetable',
702
+ 'drift': 'Drift',
703
+ 'analog': 'Analog',
704
+ 'electric': 'Electric',
705
+ 'serum2': 'Serum 2',
706
+ 'serum': 'Serum 2',
707
+ 'vital': 'Vital',
708
+ };
709
+ // ── Matching Engine ──────────────────────────────────────────────────────────
710
+ /**
711
+ * Normalize a description into lowercase tokens with stemming-like suffix stripping.
712
+ */
713
+ function tokenize(description) {
714
+ return description
715
+ .toLowerCase()
716
+ .replace(/[^a-z0-9\s]/g, ' ')
717
+ .split(/\s+/)
718
+ .filter(Boolean)
719
+ .flatMap(token => {
720
+ // Also include stemmed variants for fuzzy matching
721
+ const stems = [token];
722
+ if (token.endsWith('ed'))
723
+ stems.push(token.slice(0, -2));
724
+ if (token.endsWith('ing'))
725
+ stems.push(token.slice(0, -3));
726
+ if (token.endsWith('tion'))
727
+ stems.push(token.slice(0, -4));
728
+ if (token.endsWith('ly'))
729
+ stems.push(token.slice(0, -2));
730
+ if (token.endsWith('y'))
731
+ stems.push(token.slice(0, -1));
732
+ return stems;
733
+ });
734
+ }
735
+ /**
736
+ * Match a description against the rules. Returns the best recipe key or null.
737
+ */
738
+ function matchDescription(description) {
739
+ const tokens = tokenize(description);
740
+ const tokenSet = new Set(tokens);
741
+ for (const rule of MATCH_RULES) {
742
+ const allMatch = rule.keywords.every(kw => {
743
+ // Check if any token starts with the keyword (prefix match for stemming)
744
+ return tokenSet.has(kw) || tokens.some(t => t.startsWith(kw));
745
+ });
746
+ if (!allMatch)
747
+ continue;
748
+ if (rule.exclude) {
749
+ const excluded = rule.exclude.some(kw => tokenSet.has(kw) || tokens.some(t => t.startsWith(kw)));
750
+ if (excluded)
751
+ continue;
752
+ }
753
+ return rule.recipe;
754
+ }
755
+ return null;
756
+ }
757
+ // ── Tool Registration ────────────────────────────────────────────────────────
758
+ export function registerSoundDesignerTools() {
759
+ registerTool({
760
+ name: 'design_sound',
761
+ description: 'AI Sound Designer — program a synthesizer from a text description. ' +
762
+ 'Describe the sound you want (e.g. "dark 808 with tape distortion", "warm shimmer pad", ' +
763
+ '"acid lead", "metallic pluck") and this tool loads the right synth and sets all parameters. ' +
764
+ 'Supports 808s, sub bass, leads, pads, keys, plucks, and FX. Requires AbletonOSC.',
765
+ parameters: {
766
+ description: {
767
+ type: 'string',
768
+ description: 'Text description of the desired sound (e.g. "dark 808 with tape distortion", "warm lush pad", "acid lead")',
769
+ required: true,
770
+ },
771
+ track: {
772
+ type: 'number',
773
+ description: 'Track number (1-based) to load the synth onto',
774
+ required: true,
775
+ },
776
+ synth: {
777
+ type: 'string',
778
+ description: 'Override synth choice: "operator", "wavetable", "drift", "analog", "electric", "serum2". If omitted, the best synth is chosen automatically from the recipe.',
779
+ },
780
+ },
781
+ tier: 'free',
782
+ timeout: 30_000,
783
+ async execute(args) {
784
+ const description = String(args.description).trim();
785
+ const t = userTrack(args.track);
786
+ const synthOverride = args.synth ? String(args.synth).toLowerCase() : null;
787
+ if (!description)
788
+ return 'Error: description is required';
789
+ if (isNaN(t) || t < 0)
790
+ return 'Error: valid track number required';
791
+ try {
792
+ const osc = await ensureAbleton();
793
+ // ── 1. Match description to recipe ─────────────────────────────────
794
+ const recipeKey = matchDescription(description);
795
+ if (!recipeKey || !SOUND_RECIPES[recipeKey]) {
796
+ // List available sound categories to help the user
797
+ const categories = [
798
+ '**808 bass**: "808", "dark 808", "808 slide", "808 hard clip"',
799
+ '**Sub bass**: "sub bass", "warm sub", "triangle sub"',
800
+ '**Leads**: "bright lead", "acid lead", "supersaw", "dark bell"',
801
+ '**Pads**: "dark pad", "warm pad", "ambient pad", "shimmer pad", "string pad"',
802
+ '**Keys**: "rhodes", "wurlitzer", "organ", "electric piano"',
803
+ '**Plucks**: "pluck", "digital pluck", "metallic pluck"',
804
+ '**FX**: "riser", "noise sweep", "impact"',
805
+ ];
806
+ return [
807
+ `Could not match "${description}" to a sound recipe.`,
808
+ '',
809
+ 'Try describing your sound with these keywords:',
810
+ ...categories.map(c => `- ${c}`),
811
+ ].join('\n');
812
+ }
813
+ const recipe = SOUND_RECIPES[recipeKey];
814
+ // ── 2. Determine synth to load ─────────────────────────────────────
815
+ let synthName = recipe.synth;
816
+ if (synthOverride) {
817
+ synthName = SYNTH_NAME_MAP[synthOverride] || synthOverride;
818
+ }
819
+ // ── 3. Load the synth ──────────────────────────────────────────────
820
+ const loadResult = await osc.query('/live/kbot/load_plugin', t, synthName, '');
821
+ const loadStatus = extractArgs(loadResult);
822
+ if (loadStatus[0] !== 'ok') {
823
+ // Fallback: try with name as both manufacturer and plugin
824
+ const retry = await osc.query('/live/kbot/load_plugin', t, synthName, synthName);
825
+ const retryStatus = extractArgs(retry);
826
+ if (retryStatus[0] !== 'ok') {
827
+ return `Failed to load **${synthName}** on track ${args.track}: ${retryStatus.join(', ')}`;
828
+ }
829
+ }
830
+ // Brief pause to let the plugin initialize its parameters
831
+ await new Promise(resolve => setTimeout(resolve, 500));
832
+ // ── 4. Set synth parameters ────────────────────────────────────────
833
+ const { set, failed } = await setParamsBatch(osc, t, 0, recipe.params);
834
+ // ── 5. Load and configure effects ──────────────────────────────────
835
+ const effectResults = [];
836
+ if (recipe.effects) {
837
+ for (const fx of recipe.effects) {
838
+ // Load effect after the synth (it will be device index 1, 2, etc.)
839
+ const fxResult = await osc.query('/live/kbot/load_plugin', t, fx.name, '');
840
+ const fxStatus = extractArgs(fxResult);
841
+ if (fxStatus[0] === 'ok') {
842
+ await new Promise(resolve => setTimeout(resolve, 300));
843
+ // Get the device count to find the newly loaded effect's index
844
+ const devCount = await osc.query('/live/device/get/num_devices', t);
845
+ const numDevices = Number(extractArgs(devCount)[1] || 1);
846
+ const fxDeviceIdx = numDevices - 1;
847
+ const fxParams = await setParamsBatch(osc, t, fxDeviceIdx, fx.params);
848
+ effectResults.push(`${fx.name} (${fxParams.set.length} params set)`);
849
+ }
850
+ else {
851
+ effectResults.push(`${fx.name} (failed to load)`);
852
+ }
853
+ }
854
+ }
855
+ // ── 6. Report ──────────────────────────────────────────────────────
856
+ const lines = [
857
+ `## Sound Designer: ${recipe.description}`,
858
+ '',
859
+ `**Recipe**: \`${recipeKey}\``,
860
+ `**Synth**: ${synthName} on track ${args.track}`,
861
+ `**Parameters set**: ${set.length}`,
862
+ ];
863
+ if (failed.length > 0) {
864
+ lines.push(`**Parameters not found**: ${failed.join(', ')}`);
865
+ lines.push('_(These may have different names in your Ableton version)_');
866
+ }
867
+ if (effectResults.length > 0) {
868
+ lines.push(`**Effects**: ${effectResults.join(', ')}`);
869
+ }
870
+ lines.push('', `> Matched "${description}" → \`${recipeKey}\``);
871
+ if (set.length > 0) {
872
+ lines.push('', '| Parameter | Value |');
873
+ lines.push('|-----------|-------|');
874
+ for (const name of set) {
875
+ lines.push(`| ${name} | ${recipe.params[name]} |`);
876
+ }
877
+ }
878
+ return lines.join('\n');
879
+ }
880
+ catch (err) {
881
+ const msg = err instanceof Error ? err.message : String(err);
882
+ if (msg.includes('ECONNREFUSED') || msg.includes('timeout')) {
883
+ return [
884
+ 'Cannot connect to AbletonOSC.',
885
+ '',
886
+ '1. Open Ableton Live',
887
+ '2. Ensure AbletonOSC is loaded (Preferences > Link/Tempo/MIDI > Control Surface)',
888
+ '3. AbletonOSC should be listening on UDP 11000',
889
+ ].join('\n');
890
+ }
891
+ return `Sound Designer error: ${msg}`;
892
+ }
893
+ },
894
+ });
895
+ }
896
+ //# sourceMappingURL=sound-designer.js.map