@kernel.chat/kbot 3.97.0 → 3.97.4

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.
@@ -0,0 +1,723 @@
1
+ // kbot One-Prompt Producer — Natural language to complete musical arrangement
2
+ //
3
+ // The killer feature: one sentence -> full track.
4
+ // Input: "dark trap beat, 140bpm, F minor, 808-heavy"
5
+ // Output: Complete arrangement with drums, bass, chords, melody, structure, synth suggestions.
6
+ //
7
+ // Uses music-theory.ts for all theory computations — zero AI needed for generation.
8
+ // Deterministic output: same genre + key + tempo = reproducible musical patterns.
9
+ import { registerTool } from './index.js';
10
+ import { GENRE_DRUM_PATTERNS, GM_DRUMS, getScaleNotes, midiToNoteName, parseProgression, quantizeToScale, voiceChord, } from './music-theory.js';
11
+ // ── Helpers ───────────────────────────────────────────────────────────────
12
+ function pick(arr) {
13
+ return arr[Math.floor(Math.random() * arr.length)];
14
+ }
15
+ function clamp(v, lo, hi) {
16
+ return Math.max(lo, Math.min(hi, v));
17
+ }
18
+ function randInt(lo, hi) {
19
+ return Math.floor(Math.random() * (hi - lo + 1)) + lo;
20
+ }
21
+ const GENRE_BLUEPRINTS = {
22
+ trap: {
23
+ tempoRange: [130, 170],
24
+ defaultKey: 'F',
25
+ defaultScale: 'natural_minor',
26
+ progressions: ['i bVI bVII i', 'i bVII bVI V', 'i iv bVI bVII'],
27
+ drumPattern: 'trap',
28
+ bassStyle: 'slides',
29
+ bassOctave: 1,
30
+ melodyDensity: 'sparse',
31
+ melodyOctave: 4,
32
+ chordVoicing: 'spread',
33
+ chordRhythm: 'pads',
34
+ structure: [
35
+ { name: 'Intro', bars: 4, instruments: ['drums'], energy: 0.3 },
36
+ { name: 'Verse 1', bars: 8, instruments: ['drums', 'bass'], energy: 0.5 },
37
+ { name: 'Hook', bars: 8, instruments: ['drums', 'bass', 'chords', 'melody'], energy: 0.9 },
38
+ { name: 'Verse 2', bars: 8, instruments: ['drums', 'bass', 'melody'], energy: 0.6 },
39
+ { name: 'Hook 2', bars: 8, instruments: ['drums', 'bass', 'chords', 'melody'], energy: 0.95 },
40
+ { name: 'Bridge', bars: 4, instruments: ['chords', 'melody'], energy: 0.3 },
41
+ { name: 'Final Hook', bars: 8, instruments: ['drums', 'bass', 'chords', 'melody'], energy: 1.0 },
42
+ { name: 'Outro', bars: 4, instruments: ['chords'], energy: 0.15 },
43
+ ],
44
+ synthSuggestions: [
45
+ { instrument: 'bass', description: 'heavy 808 sub bass with long decay and pitch glide', synth_type: '2027_sub', character: 'deep, booming, distorted low end' },
46
+ { instrument: 'lead', description: 'dark bell melody with reverb and delay', synth_type: '2027_pluck', character: 'metallic, sparse, haunting' },
47
+ { instrument: 'pad', description: 'dark atmospheric pad, low-passed, wide stereo', synth_type: '2027_pad', character: 'ominous, evolving, cinematic' },
48
+ { instrument: 'drums', description: 'tight 808 kit: punchy kick, sharp snare, rapid hi-hats', synth_type: 'drum_machine', character: 'crisp, aggressive, modern' },
49
+ ],
50
+ titlePrefixes: ['Midnight', 'Shadow', 'Void', 'Neon', 'Phantom', 'Eclipse'],
51
+ },
52
+ house: {
53
+ tempoRange: [120, 130],
54
+ defaultKey: 'A',
55
+ defaultScale: 'natural_minor',
56
+ progressions: ['i bVII bVI bVII', 'i iv bVI V', 'i bVII iv bVII'],
57
+ drumPattern: 'house',
58
+ bassStyle: 'eighth',
59
+ bassOctave: 2,
60
+ melodyDensity: 'moderate',
61
+ melodyOctave: 4,
62
+ chordVoicing: 'open',
63
+ chordRhythm: 'stabs',
64
+ structure: [
65
+ { name: 'Intro', bars: 8, instruments: ['drums'], energy: 0.3 },
66
+ { name: 'Build', bars: 8, instruments: ['drums', 'bass'], energy: 0.5 },
67
+ { name: 'Main A', bars: 16, instruments: ['drums', 'bass', 'chords', 'melody'], energy: 0.8 },
68
+ { name: 'Breakdown', bars: 8, instruments: ['chords', 'melody'], energy: 0.3 },
69
+ { name: 'Build 2', bars: 4, instruments: ['drums', 'bass'], energy: 0.6 },
70
+ { name: 'Main B', bars: 16, instruments: ['drums', 'bass', 'chords', 'melody'], energy: 0.9 },
71
+ { name: 'Outro', bars: 8, instruments: ['drums'], energy: 0.2 },
72
+ ],
73
+ synthSuggestions: [
74
+ { instrument: 'bass', description: 'deep round sub bass with slight saturation', synth_type: '2027_sub', character: 'warm, round, punchy' },
75
+ { instrument: 'lead', description: 'bright piano stab or vocal chop melody', synth_type: '2027_keys', character: 'rhythmic, soulful, uplifting' },
76
+ { instrument: 'pad', description: 'warm filtered pad with slow LFO modulation', synth_type: '2027_pad', character: 'warm, deep, groovy' },
77
+ { instrument: 'drums', description: 'classic house kit: punchy kick, crisp clap, shimmering hats', synth_type: 'drum_machine', character: 'tight, clean, danceable' },
78
+ ],
79
+ titlePrefixes: ['Groove', 'Deep', 'Velvet', 'Pulse', 'Echo', 'Dusk'],
80
+ },
81
+ lofi: {
82
+ tempoRange: [70, 90],
83
+ defaultKey: 'E',
84
+ defaultScale: 'natural_minor',
85
+ progressions: ['i iv bVI V', 'i bVII bVI bVII', 'i iv v i'],
86
+ drumPattern: 'lofi',
87
+ bassStyle: 'walking',
88
+ bassOctave: 2,
89
+ melodyDensity: 'sparse',
90
+ melodyOctave: 4,
91
+ chordVoicing: 'drop2',
92
+ chordRhythm: 'half',
93
+ structure: [
94
+ { name: 'Intro', bars: 4, instruments: ['chords'], energy: 0.2 },
95
+ { name: 'Verse', bars: 8, instruments: ['drums', 'bass', 'chords'], energy: 0.4 },
96
+ { name: 'Hook', bars: 8, instruments: ['drums', 'bass', 'chords', 'melody'], energy: 0.6 },
97
+ { name: 'Verse 2', bars: 8, instruments: ['drums', 'bass', 'chords'], energy: 0.45 },
98
+ { name: 'Hook 2', bars: 8, instruments: ['drums', 'bass', 'chords', 'melody'], energy: 0.65 },
99
+ { name: 'Outro', bars: 4, instruments: ['chords', 'melody'], energy: 0.15 },
100
+ ],
101
+ synthSuggestions: [
102
+ { instrument: 'bass', description: 'mellow upright bass or muted electric bass', synth_type: '2027_bass', character: 'warm, woody, rounded' },
103
+ { instrument: 'lead', description: 'Rhodes electric piano with chorus and vinyl crackle', synth_type: '2027_keys', character: 'nostalgic, warm, gentle' },
104
+ { instrument: 'pad', description: 'tape-saturated pad with slow filter sweep', synth_type: '2027_pad', character: 'dusty, warm, intimate' },
105
+ { instrument: 'drums', description: 'vinyl-sampled drums: soft kick, brushed snare, loose hats', synth_type: 'drum_machine', character: 'dusty, swung, lo-fi' },
106
+ ],
107
+ titlePrefixes: ['Rainy', 'Dusty', 'Moonlit', 'Faded', 'Sleepy', 'Golden'],
108
+ },
109
+ dnb: {
110
+ tempoRange: [160, 180],
111
+ defaultKey: 'A',
112
+ defaultScale: 'natural_minor',
113
+ progressions: ['i bVII bVI V', 'i bVI bIII bVII', 'i iv bVI bVII'],
114
+ drumPattern: 'dnb',
115
+ bassStyle: 'arp',
116
+ bassOctave: 1,
117
+ melodyDensity: 'moderate',
118
+ melodyOctave: 4,
119
+ chordVoicing: 'spread',
120
+ chordRhythm: 'pads',
121
+ structure: [
122
+ { name: 'Intro', bars: 8, instruments: ['chords'], energy: 0.2 },
123
+ { name: 'Build', bars: 8, instruments: ['drums', 'chords'], energy: 0.5 },
124
+ { name: 'Drop', bars: 16, instruments: ['drums', 'bass', 'chords', 'melody'], energy: 0.95 },
125
+ { name: 'Breakdown', bars: 8, instruments: ['chords', 'melody'], energy: 0.3 },
126
+ { name: 'Drop 2', bars: 16, instruments: ['drums', 'bass', 'chords', 'melody'], energy: 1.0 },
127
+ { name: 'Outro', bars: 8, instruments: ['drums'], energy: 0.2 },
128
+ ],
129
+ synthSuggestions: [
130
+ { instrument: 'bass', description: 'reese bass: detuned saw waves with movement and grit', synth_type: '2027_bass', character: 'growling, wide, aggressive' },
131
+ { instrument: 'lead', description: 'bright supersaw lead with reverb tail', synth_type: '2027_lead', character: 'soaring, emotional, energetic' },
132
+ { instrument: 'pad', description: 'atmospheric pad with granular texture', synth_type: '2027_pad', character: 'ethereal, wide, evolving' },
133
+ { instrument: 'drums', description: 'jungle break: tight snare, punchy kick, fast rides', synth_type: 'drum_machine', character: 'snappy, fast, syncopated' },
134
+ ],
135
+ titlePrefixes: ['Liquid', 'Fracture', 'Neural', 'Voltage', 'Horizon', 'Cascade'],
136
+ },
137
+ drill: {
138
+ tempoRange: [140, 150],
139
+ defaultKey: 'C',
140
+ defaultScale: 'natural_minor',
141
+ progressions: ['i bVI bVII i', 'i bII bVII i', 'i bVI V i'],
142
+ drumPattern: 'drill',
143
+ bassStyle: 'slides',
144
+ bassOctave: 1,
145
+ melodyDensity: 'sparse',
146
+ melodyOctave: 4,
147
+ chordVoicing: 'close',
148
+ chordRhythm: 'stabs',
149
+ structure: [
150
+ { name: 'Intro', bars: 4, instruments: ['chords'], energy: 0.2 },
151
+ { name: 'Verse 1', bars: 8, instruments: ['drums', 'bass'], energy: 0.5 },
152
+ { name: 'Hook', bars: 8, instruments: ['drums', 'bass', 'chords', 'melody'], energy: 0.85 },
153
+ { name: 'Verse 2', bars: 8, instruments: ['drums', 'bass', 'melody'], energy: 0.55 },
154
+ { name: 'Hook 2', bars: 8, instruments: ['drums', 'bass', 'chords', 'melody'], energy: 0.9 },
155
+ { name: 'Outro', bars: 4, instruments: ['chords'], energy: 0.1 },
156
+ ],
157
+ synthSuggestions: [
158
+ { instrument: 'bass', description: 'sliding 808 with heavy distortion and pitch bend', synth_type: '2027_sub', character: 'menacing, sliding, deep' },
159
+ { instrument: 'lead', description: 'dark piano melody with reverb', synth_type: '2027_keys', character: 'cinematic, eerie, minimal' },
160
+ { instrument: 'pad', description: 'dark string ensemble, slow attack', synth_type: '2027_pad', character: 'ominous, orchestral, tense' },
161
+ { instrument: 'drums', description: 'drill kit: bouncy kick, sharp clap, rapid hi-hats', synth_type: 'drum_machine', character: 'aggressive, bouncy, relentless' },
162
+ ],
163
+ titlePrefixes: ['Block', 'Concrete', 'Grime', 'Frost', 'Iron', 'Smoke'],
164
+ },
165
+ ambient: {
166
+ tempoRange: [60, 90],
167
+ defaultKey: 'C',
168
+ defaultScale: 'major',
169
+ progressions: ['I II', 'I IVmaj7', 'i bVII', 'i bVI'],
170
+ drumPattern: 'ambient',
171
+ bassStyle: 'sustained',
172
+ bassOctave: 2,
173
+ melodyDensity: 'sparse',
174
+ melodyOctave: 5,
175
+ chordVoicing: 'spread',
176
+ chordRhythm: 'pads',
177
+ structure: [
178
+ { name: 'Opening', bars: 8, instruments: ['chords'], energy: 0.1 },
179
+ { name: 'Evolution A', bars: 16, instruments: ['chords', 'bass'], energy: 0.3 },
180
+ { name: 'Peak', bars: 16, instruments: ['chords', 'bass', 'melody', 'drums'], energy: 0.6 },
181
+ { name: 'Dissolution', bars: 16, instruments: ['chords', 'melody'], energy: 0.3 },
182
+ { name: 'Coda', bars: 8, instruments: ['chords'], energy: 0.05 },
183
+ ],
184
+ synthSuggestions: [
185
+ { instrument: 'bass', description: 'sub drone with slow filter modulation', synth_type: '2027_drone', character: 'deep, still, immersive' },
186
+ { instrument: 'lead', description: 'granular piano with long reverb and shimmer', synth_type: '2027_granular', character: 'ethereal, delicate, crystalline' },
187
+ { instrument: 'pad', description: 'evolving wavetable pad with spectral morphing', synth_type: '2027_pad', character: 'vast, celestial, slowly shifting' },
188
+ { instrument: 'drums', description: 'textural percussion: soft mallets, distant shakers, field recordings', synth_type: 'textural', character: 'organic, subtle, ambient' },
189
+ ],
190
+ titlePrefixes: ['Drift', 'Aurora', 'Stillness', 'Horizon', 'Ether', 'Vapor'],
191
+ },
192
+ industrial: {
193
+ tempoRange: [130, 160],
194
+ defaultKey: 'A',
195
+ defaultScale: 'phrygian',
196
+ progressions: ['i bII', 'i bII bVII i', 'i v bII i'],
197
+ drumPattern: 'techno',
198
+ bassStyle: 'pulse',
199
+ bassOctave: 1,
200
+ melodyDensity: 'sparse',
201
+ melodyOctave: 3,
202
+ chordVoicing: 'close',
203
+ chordRhythm: 'stabs',
204
+ structure: [
205
+ { name: 'Intro', bars: 8, instruments: ['drums'], energy: 0.4 },
206
+ { name: 'Build', bars: 8, instruments: ['drums', 'bass'], energy: 0.6 },
207
+ { name: 'Assault', bars: 16, instruments: ['drums', 'bass', 'chords', 'melody'], energy: 0.95 },
208
+ { name: 'Breakdown', bars: 8, instruments: ['chords'], energy: 0.3 },
209
+ { name: 'Assault 2', bars: 16, instruments: ['drums', 'bass', 'chords', 'melody'], energy: 1.0 },
210
+ { name: 'Outro', bars: 8, instruments: ['drums', 'bass'], energy: 0.4 },
211
+ ],
212
+ synthSuggestions: [
213
+ { instrument: 'bass', description: 'heavily distorted square wave bass with bitcrushing', synth_type: '2027_distorted', character: 'brutal, crushing, abrasive' },
214
+ { instrument: 'lead', description: 'metallic FM synthesis with ring modulation', synth_type: '2027_fm', character: 'harsh, atonal, mechanical' },
215
+ { instrument: 'pad', description: 'noise-based texture with resonant filter sweeps', synth_type: '2027_noise', character: 'chaotic, aggressive, industrial' },
216
+ { instrument: 'drums', description: 'distorted 909 kit: overdriven kick, noisy snare, metallic hats', synth_type: 'drum_machine', character: 'punishing, mechanical, relentless' },
217
+ ],
218
+ titlePrefixes: ['Rust', 'Machine', 'Collapse', 'Grind', 'Furnace', 'Wreck'],
219
+ },
220
+ techno: {
221
+ tempoRange: [130, 145],
222
+ defaultKey: 'A',
223
+ defaultScale: 'natural_minor',
224
+ progressions: ['i bVII', 'i iv', 'i bVI bVII i'],
225
+ drumPattern: 'techno',
226
+ bassStyle: 'pulse',
227
+ bassOctave: 1,
228
+ melodyDensity: 'sparse',
229
+ melodyOctave: 3,
230
+ chordVoicing: 'shell',
231
+ chordRhythm: 'stabs',
232
+ structure: [
233
+ { name: 'Intro', bars: 8, instruments: ['drums'], energy: 0.3 },
234
+ { name: 'Build', bars: 8, instruments: ['drums', 'bass'], energy: 0.5 },
235
+ { name: 'Peak A', bars: 16, instruments: ['drums', 'bass', 'chords', 'melody'], energy: 0.85 },
236
+ { name: 'Breakdown', bars: 8, instruments: ['chords'], energy: 0.25 },
237
+ { name: 'Peak B', bars: 16, instruments: ['drums', 'bass', 'chords', 'melody'], energy: 0.95 },
238
+ { name: 'Outro', bars: 8, instruments: ['drums'], energy: 0.2 },
239
+ ],
240
+ synthSuggestions: [
241
+ { instrument: 'bass', description: 'acid 303-style resonant bass with filter envelope', synth_type: '2027_acid', character: 'squelchy, hypnotic, driving' },
242
+ { instrument: 'lead', description: 'detuned saw stab with delay feedback', synth_type: '2027_stab', character: 'sharp, metallic, percussive' },
243
+ { instrument: 'pad', description: 'dark evolving pad with slow phaser', synth_type: '2027_pad', character: 'hypnotic, dark, cavernous' },
244
+ { instrument: 'drums', description: '909 kit: booming kick, sharp clap, crisp ride', synth_type: 'drum_machine', character: 'driving, tight, mechanical' },
245
+ ],
246
+ titlePrefixes: ['Reactor', 'Axis', 'Strobe', 'Cipher', 'Monolith', 'Signal'],
247
+ },
248
+ phonk: {
249
+ tempoRange: [130, 145],
250
+ defaultKey: 'D',
251
+ defaultScale: 'natural_minor',
252
+ progressions: ['i bVI bVII i', 'i bII i bVII', 'i iv i V'],
253
+ drumPattern: 'phonk',
254
+ bassStyle: 'slides',
255
+ bassOctave: 1,
256
+ melodyDensity: 'moderate',
257
+ melodyOctave: 4,
258
+ chordVoicing: 'close',
259
+ chordRhythm: 'stabs',
260
+ structure: [
261
+ { name: 'Intro', bars: 4, instruments: ['melody'], energy: 0.2 },
262
+ { name: 'Verse 1', bars: 8, instruments: ['drums', 'bass', 'melody'], energy: 0.6 },
263
+ { name: 'Hook', bars: 8, instruments: ['drums', 'bass', 'chords', 'melody'], energy: 0.9 },
264
+ { name: 'Verse 2', bars: 8, instruments: ['drums', 'bass', 'melody'], energy: 0.65 },
265
+ { name: 'Hook 2', bars: 8, instruments: ['drums', 'bass', 'chords', 'melody'], energy: 0.95 },
266
+ { name: 'Outro', bars: 4, instruments: ['melody'], energy: 0.15 },
267
+ ],
268
+ synthSuggestions: [
269
+ { instrument: 'bass', description: 'distorted 808 with hard clipping and pitch slides', synth_type: '2027_sub', character: 'dark, gritty, Memphis' },
270
+ { instrument: 'lead', description: 'chopped soul vocal sample or dark bell', synth_type: '2027_pluck', character: 'lo-fi, eerie, chopped' },
271
+ { instrument: 'pad', description: 'vinyl-textured dark pad with wow and flutter', synth_type: '2027_pad', character: 'gritty, nostalgic, haunted' },
272
+ { instrument: 'drums', description: 'Memphis kit: punchy kick, snappy snare, cowbell, rapid hats', synth_type: 'drum_machine', character: 'aggressive, lo-fi, cowbell-heavy' },
273
+ ],
274
+ titlePrefixes: ['Hellcat', 'Demon', 'Drift', 'Burnout', 'Reaper', 'Phantom'],
275
+ },
276
+ };
277
+ // Alias mappings for common genre variants
278
+ const GENRE_ALIASES = {
279
+ 'lo-fi': 'lofi', 'lo fi': 'lofi', 'lofi hip hop': 'lofi', 'lofi hiphop': 'lofi',
280
+ 'drum and bass': 'dnb', 'drum & bass': 'dnb', 'jungle': 'dnb',
281
+ 'uk drill': 'drill', 'ny drill': 'drill',
282
+ 'deep house': 'house', 'tech house': 'house',
283
+ 'hip hop': 'trap', 'hiphop': 'trap', 'hip-hop': 'trap', 'rap': 'trap',
284
+ 'dark ambient': 'ambient', 'atmospheric': 'ambient', 'dreamy': 'ambient',
285
+ 'noise': 'industrial', 'ebm': 'industrial', 'dark techno': 'industrial',
286
+ 'detroit techno': 'techno', 'acid': 'techno', 'minimal techno': 'techno',
287
+ 'drift phonk': 'phonk', 'memphis': 'phonk',
288
+ };
289
+ // ── Prompt Parser ─────────────────────────────────────────────────────────
290
+ function parsePrompt(raw) {
291
+ const input = raw.toLowerCase().trim();
292
+ // Extract tempo
293
+ let tempo = 0;
294
+ const bpmMatch = input.match(/(\d{2,3})\s*bpm/);
295
+ if (bpmMatch)
296
+ tempo = parseInt(bpmMatch[1], 10);
297
+ // Extract key
298
+ let keyRoot = '';
299
+ let keyScale = '';
300
+ const keyMatch = input.match(/\b([a-g][#b]?)\s*(minor|major|min|maj|m(?:in)?)?\b/i);
301
+ if (keyMatch) {
302
+ const rawRoot = keyMatch[1];
303
+ keyRoot = rawRoot.charAt(0).toUpperCase() + rawRoot.slice(1);
304
+ const qualifier = (keyMatch[2] || '').toLowerCase();
305
+ if (qualifier.startsWith('maj'))
306
+ keyScale = 'major';
307
+ else if (qualifier === 'm' || qualifier.startsWith('min'))
308
+ keyScale = 'natural_minor';
309
+ }
310
+ // Detect genre
311
+ let genre = '';
312
+ const knownGenres = Object.keys(GENRE_BLUEPRINTS);
313
+ // Check direct matches first
314
+ for (const g of knownGenres) {
315
+ if (input.includes(g)) {
316
+ genre = g;
317
+ break;
318
+ }
319
+ }
320
+ // Check aliases
321
+ if (!genre) {
322
+ for (const [alias, target] of Object.entries(GENRE_ALIASES)) {
323
+ if (input.includes(alias)) {
324
+ genre = target;
325
+ break;
326
+ }
327
+ }
328
+ }
329
+ if (!genre)
330
+ genre = 'trap'; // default
331
+ const bp = GENRE_BLUEPRINTS[genre];
332
+ // Apply defaults from genre if not specified
333
+ if (!tempo)
334
+ tempo = Math.round((bp.tempoRange[0] + bp.tempoRange[1]) / 2);
335
+ tempo = clamp(tempo, 40, 300);
336
+ if (!keyRoot)
337
+ keyRoot = bp.defaultKey;
338
+ if (!keyScale)
339
+ keyScale = bp.defaultScale;
340
+ // Extract mood keywords (anything that isn't genre/bpm/key)
341
+ const moodWords = ['dark', 'bright', 'aggressive', 'chill', 'dreamy', 'heavy', 'light',
342
+ 'warm', 'cold', 'distorted', 'clean', 'chaotic', 'minimal', 'lush', 'sparse',
343
+ 'energetic', 'melancholic', 'uplifting', 'gritty', 'smooth', 'ethereal',
344
+ 'punchy', 'soft', 'hard', 'deep', 'gentle', 'intense', 'hypnotic', 'bouncy'];
345
+ const mood = moodWords.filter(w => input.includes(w));
346
+ // Extract instrument hints
347
+ const instrumentWords = ['808', 'piano', 'guitar', 'strings', 'synth', 'pad', 'bells',
348
+ 'rhodes', 'organ', 'pluck', 'lead', 'vocal', 'brass', 'flute', 'sax', 'violin'];
349
+ const instrumentHints = instrumentWords.filter(w => input.includes(w));
350
+ return { genre, tempo, key: `${keyRoot}${keyScale.includes('minor') ? 'm' : ''}`, scale: keyScale, root: keyRoot, mood, instrumentHints };
351
+ }
352
+ // ── Drum Pattern Generator ────────────────────────────────────────────────
353
+ function generateDrums(genre, _mood) {
354
+ const patternKey = GENRE_BLUEPRINTS[genre]?.drumPattern || genre;
355
+ const genrePattern = GENRE_DRUM_PATTERNS[patternKey] || GENRE_DRUM_PATTERNS.house;
356
+ const hits = [];
357
+ for (const [voice, steps] of Object.entries(genrePattern.pattern)) {
358
+ const gmNote = GM_DRUMS[voice];
359
+ if (gmNote === undefined)
360
+ continue;
361
+ for (const step of steps) {
362
+ // Genre-appropriate velocity shaping
363
+ let velocity;
364
+ if (voice === 'kick' || voice === 'bass_drum') {
365
+ velocity = randInt(100, 127);
366
+ }
367
+ else if (voice === 'snare' || voice === 'clap' || voice === 'handclap') {
368
+ velocity = randInt(90, 120);
369
+ }
370
+ else if (voice === 'closed_hihat') {
371
+ // Create velocity pattern for hi-hats (accented, ghost, normal)
372
+ const isAccent = step % 4 === 0;
373
+ const isGhost = step % 2 === 1;
374
+ velocity = isAccent ? randInt(90, 110) : isGhost ? randInt(40, 65) : randInt(65, 85);
375
+ }
376
+ else if (voice === 'open_hihat') {
377
+ velocity = randInt(75, 100);
378
+ }
379
+ else {
380
+ velocity = randInt(60, 95);
381
+ }
382
+ hits.push({ step, voice, velocity: clamp(velocity, 1, 127) });
383
+ }
384
+ }
385
+ return hits;
386
+ }
387
+ // ── Bass Generator ────────────────────────────────────────────────────────
388
+ function generateBass(chords, parsed, bp) {
389
+ const notes = [];
390
+ const { root, scale } = parsed;
391
+ const octave = bp.bassOctave;
392
+ for (const chord of chords) {
393
+ // Root note of chord in bass octave
394
+ const chordRoot = chord.midi[0];
395
+ const bassRootPc = ((chordRoot % 12) + 12) % 12;
396
+ const bassMidi = (octave + 1) * 12 + bassRootPc;
397
+ switch (bp.bassStyle) {
398
+ case 'sustained': {
399
+ // Whole note sustained bass
400
+ notes.push({
401
+ bar: chord.bar, beat: 0,
402
+ note: midiToNoteName(bassMidi), midi: bassMidi,
403
+ duration: chord.duration, velocity: randInt(85, 105),
404
+ });
405
+ break;
406
+ }
407
+ case 'eighth': {
408
+ // Eighth note pulse (house bass)
409
+ const beatsInChord = chord.duration;
410
+ for (let beat = 0; beat < beatsInChord; beat += 0.5) {
411
+ const isDownbeat = beat % 1 === 0;
412
+ const vel = isDownbeat ? randInt(90, 110) : randInt(70, 90);
413
+ notes.push({
414
+ bar: chord.bar, beat,
415
+ note: midiToNoteName(bassMidi), midi: bassMidi,
416
+ duration: 0.4, velocity: vel,
417
+ });
418
+ }
419
+ break;
420
+ }
421
+ case 'walking': {
422
+ // Walking bass: root, 3rd, 5th, approach note
423
+ const scaleNotes = getScaleNotes(root, scale, octave);
424
+ const rootIdx = scaleNotes.midi.findIndex(n => n % 12 === bassRootPc);
425
+ const walkNotes = [0, 2, 4, 3].map(offset => {
426
+ const idx = clamp((rootIdx >= 0 ? rootIdx : 0) + offset, 0, scaleNotes.midi.length - 1);
427
+ return scaleNotes.midi[idx];
428
+ });
429
+ for (let i = 0; i < 4 && i < chord.duration; i++) {
430
+ const midi = walkNotes[i % walkNotes.length];
431
+ notes.push({
432
+ bar: chord.bar, beat: i,
433
+ note: midiToNoteName(midi), midi,
434
+ duration: 0.9, velocity: randInt(75, 100),
435
+ });
436
+ }
437
+ break;
438
+ }
439
+ case 'slides': {
440
+ // 808-style: long notes on beats 1 and 3, pitch slides implied
441
+ notes.push({
442
+ bar: chord.bar, beat: 0,
443
+ note: midiToNoteName(bassMidi), midi: bassMidi,
444
+ duration: 2.0, velocity: randInt(100, 127),
445
+ });
446
+ // Occasional second hit
447
+ if (Math.random() > 0.4) {
448
+ const slideMidi = quantizeToScale(bassMidi + randInt(2, 5), root, scale);
449
+ notes.push({
450
+ bar: chord.bar, beat: 2.5,
451
+ note: midiToNoteName(slideMidi), midi: slideMidi,
452
+ duration: 1.0, velocity: randInt(85, 110),
453
+ });
454
+ }
455
+ break;
456
+ }
457
+ case 'arp': {
458
+ // Arpeggiated bass (dnb style)
459
+ const scaleNotes = getScaleNotes(root, scale, octave);
460
+ const rootIdx = scaleNotes.midi.findIndex(n => n % 12 === bassRootPc);
461
+ const arpNotes = [0, 2, 4, 2].map(offset => {
462
+ const idx = clamp((rootIdx >= 0 ? rootIdx : 0) + offset, 0, scaleNotes.midi.length - 1);
463
+ return scaleNotes.midi[idx];
464
+ });
465
+ for (let i = 0; i < 8 && i * 0.5 < chord.duration; i++) {
466
+ const midi = arpNotes[i % arpNotes.length];
467
+ notes.push({
468
+ bar: chord.bar, beat: i * 0.5,
469
+ note: midiToNoteName(midi), midi,
470
+ duration: 0.4, velocity: randInt(80, 110),
471
+ });
472
+ }
473
+ break;
474
+ }
475
+ case 'pulse': {
476
+ // Steady pulse (techno/industrial)
477
+ for (let beat = 0; beat < chord.duration; beat += 1) {
478
+ notes.push({
479
+ bar: chord.bar, beat,
480
+ note: midiToNoteName(bassMidi), midi: bassMidi,
481
+ duration: 0.8, velocity: randInt(90, 115),
482
+ });
483
+ }
484
+ break;
485
+ }
486
+ case 'sparse':
487
+ default: {
488
+ // Just the root on beat 1
489
+ notes.push({
490
+ bar: chord.bar, beat: 0,
491
+ note: midiToNoteName(bassMidi), midi: bassMidi,
492
+ duration: 3.5, velocity: randInt(80, 100),
493
+ });
494
+ break;
495
+ }
496
+ }
497
+ }
498
+ return notes;
499
+ }
500
+ // ── Chord Generator ───────────────────────────────────────────────────────
501
+ function generateChords(parsed, bp, totalBars) {
502
+ const { root, scale } = parsed;
503
+ const progression = pick(bp.progressions);
504
+ const chordMidi = parseProgression(progression, root, scale);
505
+ const chordTokens = progression.trim().split(/\s+/);
506
+ const events = [];
507
+ // How many bars per chord
508
+ const chordsPerCycle = chordMidi.length;
509
+ const barsPerChord = Math.max(1, Math.round(4 / chordsPerCycle)); // default: 4-bar cycle
510
+ let bar = 0;
511
+ while (bar < totalBars) {
512
+ for (let ci = 0; ci < chordsPerCycle && bar < totalBars; ci++) {
513
+ const rawNotes = chordMidi[ci];
514
+ const voicedNotes = voiceChord(rawNotes, bp.chordVoicing);
515
+ let duration;
516
+ switch (bp.chordRhythm) {
517
+ case 'whole':
518
+ duration = 4;
519
+ break;
520
+ case 'half':
521
+ duration = 2;
522
+ break;
523
+ case 'stabs':
524
+ duration = 0.5;
525
+ break;
526
+ case 'pads':
527
+ duration = barsPerChord * 4;
528
+ break;
529
+ case 'arp':
530
+ duration = 4;
531
+ break;
532
+ default: duration = 4;
533
+ }
534
+ events.push({
535
+ bar,
536
+ chord_name: chordTokens[ci],
537
+ notes: voicedNotes.map(n => midiToNoteName(n)),
538
+ midi: voicedNotes,
539
+ duration: Math.min(duration, (totalBars - bar) * 4),
540
+ });
541
+ bar += barsPerChord;
542
+ }
543
+ }
544
+ return events;
545
+ }
546
+ // ── Melody Generator ──────────────────────────────────────────────────────
547
+ function generateMelody(chords, parsed, bp, totalBars) {
548
+ const { root, scale } = parsed;
549
+ const octave = bp.melodyOctave;
550
+ const scaleData = getScaleNotes(root, scale, octave);
551
+ const scaleMidi = [...scaleData.midi, ...scaleData.midi.map(n => n + 12)];
552
+ const notes = [];
553
+ // Generate a 2-4 bar motif then repeat with variation
554
+ const motifBars = parsed.genre === 'ambient' ? 4 : 2;
555
+ const motif = [];
556
+ // Build motif
557
+ let prevIdx = Math.floor(scaleMidi.length / 3); // start in lower third
558
+ const density = bp.melodyDensity;
559
+ const notesPerBar = density === 'sparse' ? 2 : density === 'moderate' ? 4 : 8;
560
+ const stepDuration = 4 / notesPerBar;
561
+ for (let bar = 0; bar < motifBars; bar++) {
562
+ for (let n = 0; n < notesPerBar; n++) {
563
+ // Rest probability
564
+ const restChance = density === 'sparse' ? 0.4 : density === 'moderate' ? 0.2 : 0.1;
565
+ if (Math.random() < restChance)
566
+ continue;
567
+ // Step-wise motion with occasional leaps
568
+ const step = Math.random() < 0.7
569
+ ? (Math.random() < 0.5 ? 1 : -1)
570
+ : (Math.random() < 0.5 ? randInt(2, 4) : randInt(-4, -2));
571
+ prevIdx = clamp(prevIdx + step, 0, scaleMidi.length - 1);
572
+ const isDownbeat = n === 0;
573
+ let velocity = randInt(60, 90);
574
+ if (isDownbeat)
575
+ velocity = randInt(85, 110);
576
+ velocity = clamp(velocity, 1, 127);
577
+ motif.push({
578
+ beatOffset: bar * 4 + n * stepDuration,
579
+ scaleIdx: prevIdx,
580
+ duration: stepDuration * (0.6 + Math.random() * 0.6),
581
+ velocity,
582
+ });
583
+ }
584
+ }
585
+ // Repeat motif across total bars with slight variation
586
+ const motifBeats = motifBars * 4;
587
+ for (let bar = 0; bar < totalBars; bar += motifBars) {
588
+ for (const m of motif) {
589
+ const absoluteBar = bar + Math.floor(m.beatOffset / 4);
590
+ if (absoluteBar >= totalBars)
591
+ break;
592
+ // Slight variation on repeats
593
+ let idx = m.scaleIdx;
594
+ if (bar > 0 && Math.random() < 0.25) {
595
+ idx = clamp(idx + (Math.random() < 0.5 ? 1 : -1), 0, scaleMidi.length - 1);
596
+ }
597
+ const pitch = quantizeToScale(scaleMidi[idx], root, scale);
598
+ const beatInBar = m.beatOffset % 4;
599
+ notes.push({
600
+ bar: absoluteBar,
601
+ beat: Math.round(beatInBar * 1000) / 1000,
602
+ note: midiToNoteName(pitch),
603
+ midi: pitch,
604
+ duration: Math.round(m.duration * 1000) / 1000,
605
+ velocity: clamp(m.velocity + randInt(-5, 5), 1, 127),
606
+ });
607
+ }
608
+ }
609
+ return notes;
610
+ }
611
+ // ── Arrangement Builder ───────────────────────────────────────────────────
612
+ function buildArrangement(bp) {
613
+ let bar = 0;
614
+ return bp.structure.map(section => {
615
+ const s = {
616
+ section: section.name,
617
+ start_bar: bar,
618
+ end_bar: bar + section.bars,
619
+ active_instruments: section.instruments,
620
+ energy: section.energy,
621
+ };
622
+ bar += section.bars;
623
+ return s;
624
+ });
625
+ }
626
+ // ── Title Generator ───────────────────────────────────────────────────────
627
+ function generateTitle(parsed, bp) {
628
+ const suffixes = ['Nights', 'Dreams', 'State', 'Wave', 'Zone', 'Mode',
629
+ 'District', 'Ritual', 'Code', 'Frequency', 'Protocol', 'Dimension'];
630
+ const prefix = pick(bp.titlePrefixes);
631
+ const suffix = pick(suffixes);
632
+ return `${prefix} ${suffix}`;
633
+ }
634
+ // ── Main Producer ─────────────────────────────────────────────────────────
635
+ function produceTrack(prompt) {
636
+ const parsed = parsePrompt(prompt);
637
+ const bp = GENRE_BLUEPRINTS[parsed.genre];
638
+ // Calculate total bars from arrangement
639
+ const totalBars = bp.structure.reduce((sum, s) => sum + s.bars, 0);
640
+ // Generate all elements
641
+ const chords = generateChords(parsed, bp, totalBars);
642
+ const drums = generateDrums(parsed.genre, parsed.mood);
643
+ const bass = generateBass(chords, parsed, bp);
644
+ const melody = generateMelody(chords, parsed, bp, totalBars);
645
+ const arrangement = buildArrangement(bp);
646
+ const title = generateTitle(parsed, bp);
647
+ // Duration calculation
648
+ const beatsPerMinute = parsed.tempo;
649
+ const totalBeats = totalBars * 4;
650
+ const durationSeconds = Math.round((totalBeats / beatsPerMinute) * 60);
651
+ return {
652
+ metadata: {
653
+ title,
654
+ genre: parsed.genre,
655
+ tempo: parsed.tempo,
656
+ key: parsed.key,
657
+ scale: parsed.scale,
658
+ time_signature: '4/4',
659
+ total_bars: totalBars,
660
+ duration_seconds: durationSeconds,
661
+ },
662
+ drums,
663
+ bass,
664
+ chords,
665
+ melody,
666
+ arrangement,
667
+ synth_suggestions: bp.synthSuggestions,
668
+ };
669
+ }
670
+ // ── Tool Registration ─────────────────────────────────────────────────────
671
+ export function registerOnePromptTools() {
672
+ registerTool({
673
+ name: 'produce_track',
674
+ description: '[One-Prompt Producer] Generate a complete musical arrangement from a single natural language description. ' +
675
+ 'Input a prompt like "dark trap beat, 140bpm, F minor, 808-heavy" and get back a full track: ' +
676
+ 'drum pattern, bass line, chord progression, melody, arrangement map, and 2027 synth suggestions. ' +
677
+ 'Supports genres: trap, house, lofi, dnb, drill, ambient, industrial, techno, phonk. ' +
678
+ 'Output is structured JSON ready for MIDI export or Ableton Live integration.',
679
+ parameters: {
680
+ prompt: {
681
+ type: 'string',
682
+ description: 'Natural language description of the track. Include any of: genre, tempo (e.g. "140bpm"), ' +
683
+ 'key (e.g. "F minor"), mood (e.g. "dark", "dreamy", "aggressive"), instrument preferences ' +
684
+ '(e.g. "808-heavy", "piano"). Examples: "dark trap beat, 140bpm, F minor", ' +
685
+ '"ambient lo-fi, 85bpm, gentle and dreamy", "aggressive industrial, 160bpm, distorted"',
686
+ required: true,
687
+ },
688
+ },
689
+ tier: 'free',
690
+ timeout: 30_000,
691
+ async execute(args) {
692
+ const prompt = String(args.prompt || '');
693
+ if (!prompt.trim()) {
694
+ return JSON.stringify({ error: 'Please provide a track description. Example: "dark trap beat, 140bpm, F minor, 808-heavy"' });
695
+ }
696
+ try {
697
+ const track = produceTrack(prompt);
698
+ // Build a summary header for readability
699
+ const summary = [
700
+ `## ${track.metadata.title}`,
701
+ `**Genre**: ${track.metadata.genre} | **Tempo**: ${track.metadata.tempo} BPM | **Key**: ${track.metadata.key} (${track.metadata.scale})`,
702
+ `**Duration**: ${Math.floor(track.metadata.duration_seconds / 60)}:${String(track.metadata.duration_seconds % 60).padStart(2, '0')} | **Bars**: ${track.metadata.total_bars}`,
703
+ '',
704
+ `**Arrangement**: ${track.arrangement.map(s => `${s.section} (${s.start_bar}-${s.end_bar})`).join(' -> ')}`,
705
+ '',
706
+ `**Drum pattern** (1 bar, 16th grid): ${track.drums.length} hits`,
707
+ `**Bass line**: ${track.bass.length} notes`,
708
+ `**Chord progression**: ${track.chords.slice(0, 8).map(c => c.chord_name).join(' | ')} (${track.chords.length} total events)`,
709
+ `**Melody**: ${track.melody.length} notes`,
710
+ '',
711
+ '**2027 Synth Suggestions**:',
712
+ ...track.synth_suggestions.map(s => ` - ${s.instrument}: ${s.description} (${s.character})`),
713
+ ].join('\n');
714
+ return JSON.stringify({ summary, track }, null, 2);
715
+ }
716
+ catch (err) {
717
+ const msg = err instanceof Error ? err.message : String(err);
718
+ return JSON.stringify({ error: `Track generation failed: ${msg}` });
719
+ }
720
+ },
721
+ });
722
+ }
723
+ //# sourceMappingURL=one-prompt-producer.js.map