@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.
- package/README.md +4 -4
- package/dist/cli.js +1 -1
- package/dist/coordinator.d.ts +32 -0
- package/dist/coordinator.js +157 -0
- package/dist/doctor.js +5 -4
- package/dist/share.js +1 -1
- package/dist/streaming.js +1 -1
- package/dist/tools/ableton.js +176 -42
- package/dist/tools/audit.js +2 -2
- package/dist/tools/containers.js +75 -14
- package/dist/tools/estimation.d.ts +2 -0
- package/dist/tools/estimation.js +21 -0
- package/dist/tools/idempotency-checker.d.ts +2 -0
- package/dist/tools/idempotency-checker.js +23 -0
- package/dist/tools/image-variation.d.ts +2 -0
- package/dist/tools/image-variation.js +31 -0
- package/dist/tools/index.js +1 -0
- package/dist/tools/one-prompt-producer.d.ts +2 -0
- package/dist/tools/one-prompt-producer.js +723 -0
- package/dist/tools/sound-designer.js +278 -3
- package/dist/tools/stream-brain.js +1 -1
- package/dist/tools/stream-character.js +4 -4
- package/dist/tools/stream-intelligence.js +2 -2
- package/dist/tools/stream-renderer.js +30 -30
- package/dist/ui.js +23 -21
- package/dist/watcher.d.ts +16 -0
- package/dist/watcher.js +111 -0
- package/package.json +5 -4
|
@@ -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
|