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