@kernel.chat/kbot 3.52.0 → 3.55.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.
- package/dist/agents/replit.js +1 -1
- package/dist/behaviour.d.ts +30 -0
- package/dist/behaviour.d.ts.map +1 -0
- package/dist/behaviour.js +191 -0
- package/dist/behaviour.js.map +1 -0
- package/dist/bootstrap.js +1 -1
- package/dist/bootstrap.js.map +1 -1
- package/dist/integrations/ableton-m4l.d.ts +124 -0
- package/dist/integrations/ableton-m4l.d.ts.map +1 -0
- package/dist/integrations/ableton-m4l.js +338 -0
- package/dist/integrations/ableton-m4l.js.map +1 -0
- package/dist/integrations/ableton-osc.d.ts.map +1 -1
- package/dist/integrations/ableton-osc.js +6 -2
- package/dist/integrations/ableton-osc.js.map +1 -1
- package/dist/music-learning.d.ts +181 -0
- package/dist/music-learning.d.ts.map +1 -0
- package/dist/music-learning.js +340 -0
- package/dist/music-learning.js.map +1 -0
- package/dist/skill-system.d.ts +68 -0
- package/dist/skill-system.d.ts.map +1 -0
- package/dist/skill-system.js +386 -0
- package/dist/skill-system.js.map +1 -0
- package/dist/tools/ableton.d.ts.map +1 -1
- package/dist/tools/ableton.js +24 -8
- package/dist/tools/ableton.js.map +1 -1
- package/dist/tools/arrangement-engine.d.ts +2 -0
- package/dist/tools/arrangement-engine.d.ts.map +1 -0
- package/dist/tools/arrangement-engine.js +644 -0
- package/dist/tools/arrangement-engine.js.map +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +5 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/music-theory.d.ts.map +1 -1
- package/dist/tools/music-theory.js +61 -1
- package/dist/tools/music-theory.js.map +1 -1
- package/dist/tools/producer-engine.d.ts +71 -0
- package/dist/tools/producer-engine.d.ts.map +1 -0
- package/dist/tools/producer-engine.js +1859 -0
- package/dist/tools/producer-engine.js.map +1 -0
- package/dist/tools/sound-designer.d.ts +2 -0
- package/dist/tools/sound-designer.d.ts.map +1 -0
- package/dist/tools/sound-designer.js +896 -0
- package/dist/tools/sound-designer.js.map +1 -0
- 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
|