@williamzujkowski/live-coding-music-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1735 -0
  3. package/dist/AudioAnalyzer.js +678 -0
  4. package/dist/PatternStore.js +169 -0
  5. package/dist/StrudelController.js +676 -0
  6. package/dist/__tests__/MusicTheory.test.js +245 -0
  7. package/dist/__tests__/PatternGenerator.test.js +337 -0
  8. package/dist/__tests__/PatternStore.test.js +153 -0
  9. package/dist/__tests__/utils/MockPlaywright.js +130 -0
  10. package/dist/index.js +21 -0
  11. package/dist/server/EnhancedMCPServer.js +654 -0
  12. package/dist/server/EnhancedMCPServerFixed.js +2506 -0
  13. package/dist/server/server.js +580 -0
  14. package/dist/server/tools/ai.js +530 -0
  15. package/dist/server/tools/analysis.js +132 -0
  16. package/dist/server/tools/capture.js +184 -0
  17. package/dist/server/tools/diagnostics.js +96 -0
  18. package/dist/server/tools/editor.js +128 -0
  19. package/dist/server/tools/generate.js +218 -0
  20. package/dist/server/tools/history.js +196 -0
  21. package/dist/server/tools/playback.js +41 -0
  22. package/dist/server/tools/session.js +115 -0
  23. package/dist/server/tools/storage.js +77 -0
  24. package/dist/server/tools/transform.js +399 -0
  25. package/dist/server/tools/types.js +9 -0
  26. package/dist/services/AudioCaptureService.js +279 -0
  27. package/dist/services/GeminiService.js +790 -0
  28. package/dist/services/MIDIExportService.js +412 -0
  29. package/dist/services/MusicTheory.js +186 -0
  30. package/dist/services/PatternGenerator.js +647 -0
  31. package/dist/services/SessionManager.js +316 -0
  32. package/dist/services/StrudelEngine.js +374 -0
  33. package/dist/services/__mocks__/StrudelEngine.js +157 -0
  34. package/dist/types/AudioAnalysis.js +4 -0
  35. package/dist/types/index.js +1 -0
  36. package/dist/utils/ErrorRecovery.js +248 -0
  37. package/dist/utils/InputValidator.js +281 -0
  38. package/dist/utils/Logger.js +19 -0
  39. package/dist/utils/PatternValidator.js +241 -0
  40. package/dist/utils/PerformanceMonitor.js +128 -0
  41. package/package.json +86 -0
@@ -0,0 +1,647 @@
1
+ import { MusicTheory } from './MusicTheory.js';
2
+ export class PatternGenerator {
3
+ theory = new MusicTheory();
4
+ // Note interval lookup for chord calculations
5
+ notes = ['c', 'db', 'd', 'eb', 'e', 'f', 'gb', 'g', 'ab', 'a', 'bb', 'b'];
6
+ /**
7
+ * Get a note at a specific interval from the root
8
+ */
9
+ getInterval(root, semitones) {
10
+ const normalizedRoot = root.toLowerCase().replace('#', 'b');
11
+ const sharpToFlat = {
12
+ 'c#': 'db', 'd#': 'eb', 'f#': 'gb', 'g#': 'ab', 'a#': 'bb'
13
+ };
14
+ const searchRoot = sharpToFlat[normalizedRoot] || normalizedRoot;
15
+ const idx = this.notes.indexOf(searchRoot);
16
+ if (idx === -1)
17
+ return root;
18
+ return this.notes[(idx + semitones) % 12];
19
+ }
20
+ getFourth(root) { return this.getInterval(root, 5); }
21
+ getFifth(root) { return this.getInterval(root, 7); }
22
+ getMinorThird(root) { return this.getInterval(root, 3); }
23
+ getMinorSeventh(root) { return this.getInterval(root, 10); }
24
+ /**
25
+ * Generates a drum pattern for a given style
26
+ * @param style - Music style (e.g., 'techno', 'house', 'dnb', 'ambient')
27
+ * @param complexity - Pattern complexity from 0 to 1 (default: 1)
28
+ * @returns Strudel drum pattern code
29
+ */
30
+ generateDrumPattern(style, complexity = 1) {
31
+ const patterns = {
32
+ techno: [
33
+ 's("bd*4")',
34
+ 's("bd*4, ~ cp ~ cp")',
35
+ 's("bd*4, ~ cp ~ cp, hh*8")',
36
+ 's("bd*4, ~ cp ~ cp, [~ hh]*4, oh ~ ~ ~").swing(0.05)'
37
+ ],
38
+ house: [
39
+ 's("bd*4, hh*8")',
40
+ 's("bd*4, hh*8, ~ cp ~ cp")',
41
+ 's("bd*4, [~ hh]*4, ~ cp ~ cp, oh ~ oh ~")',
42
+ 's("bd*4, [~ hh]*4, ~ cp ~ cp").every(4, x => x.fast(2))'
43
+ ],
44
+ dnb: [
45
+ 's("bd ~ ~ bd ~ ~ bd ~, ~ ~ cp ~ ~ cp ~ ~")',
46
+ 's("bd ~ ~ [bd bd] ~ ~ bd ~, ~ ~ cp ~ ~ cp ~ ~, hh*16")',
47
+ 's("bd ~ ~ [bd bd] ~ ~ bd ~, ~ ~ cp ~ [~ cp] ~ cp ~ ~, hh*16").fast(2)'
48
+ ],
49
+ breakbeat: [
50
+ 's("bd ~ ~ bd ~ ~ ~ bd, ~ cp ~ ~ cp ~")',
51
+ 's("bd ~ ~ bd ~ [~ bd] ~ bd, ~ cp ~ ~ cp ~, hh*8")',
52
+ 's("bd ~ [~ bd] bd ~ [~ bd] ~ bd, ~ cp ~ ~ cp [~ cp], hh*8").swing(0.1)'
53
+ ],
54
+ trap: [
55
+ 's("bd*2, ~ cp ~ cp")',
56
+ 's("bd*2, ~ cp ~ cp, hh*8").every(2, x => x.fast(2))',
57
+ 's("bd [bd bd] ~ bd, ~ cp ~ cp, hh*16").swing(0.2)'
58
+ ],
59
+ jungle: [
60
+ 's("bd ~ [~ bd] bd ~ ~ bd ~, ~ cp ~ ~ cp ~").fast(2)',
61
+ 's("bd ~ [~ bd] bd ~ [bd bd] bd ~, ~ cp ~ [~ cp] cp ~, hh*32").fast(2)'
62
+ ],
63
+ ambient: [
64
+ 's("bd ~ ~ ~")',
65
+ 's("bd ~ ~ ~, ~ ~ ~ hh:8").room(0.9)',
66
+ 's("bd ~ ~ ~, ~ ~ ~ hh:8, ~ ~ oh:5 ~").room(0.9).gain(0.5)'
67
+ ],
68
+ experimental: [
69
+ 's("bd").euclid(5, 8)',
70
+ 's("bd cp").euclid(7, 16)',
71
+ 's("bd cp hh").euclid(choose([3, 5, 7]), 16)'
72
+ ],
73
+ // === NEW GENRES ===
74
+ intelligent_dnb: [
75
+ // Minimal - sparse break
76
+ `s("breaks165").fit().slice(8, "0 ~ ~ 3 ~ ~ 6 ~").gain(0.5).room(0.3).lpf(5000)`,
77
+ // Medium - classic rolling break
78
+ `s("breaks165").fit().slice(8, "0 0 6 3 0 2 6 7").gain(0.7).room(0.2)`,
79
+ // Complex - layered breaks
80
+ `stack(
81
+ s("breaks165").fit().slice(8, "0 0 6 3 0 2 6 7").gain(0.65).room(0.2),
82
+ s("breaks165").fit().chop(16).gain(0.12).hpf(3000).room(0.4),
83
+ s("bd ~ ~ ~ [~ bd] ~ bd ~").bank("RolandTR909").gain(0.45).lpf(100)
84
+ )`
85
+ ],
86
+ trip_hop: [
87
+ // Minimal - sparse and moody
88
+ `s("bd ~ ~ ~, ~ ~ ~ sd:3").room(0.5).gain(0.7)`,
89
+ // Medium - classic trip hop groove
90
+ `stack(
91
+ s("bd ~ ~ bd ~ ~ bd ~").gain(0.8),
92
+ s("~ ~ ~ ~ sd ~ ~ ~").bank("RolandTR808").gain(0.7).room(0.4),
93
+ s("hh*8").gain(0.25).hpf(6000).pan(sine.range(0.3, 0.7))
94
+ )`,
95
+ // Complex - layered with vinyl texture
96
+ `stack(
97
+ s("bd ~ [~ bd] ~ bd ~ ~ ~").gain(0.8),
98
+ s("~ ~ ~ ~ sd ~ ~ sd:2?").bank("RolandTR808").gain(0.7).room(0.5),
99
+ s("hh*8").gain(perlin.range(0.15, 0.3)).hpf(5000),
100
+ s("~ oh ~ ~ ~ oh ~ ~").bank("RolandTR808").gain(0.2).room(0.6)
101
+ ).slow(2)`
102
+ ],
103
+ boom_bap: [
104
+ // Minimal - hard kick and snare
105
+ `s("bd ~ ~ ~ sd ~ ~ ~, bd ~ ~ ~ sd ~ bd ~")`,
106
+ // Medium - classic boom bap with hats
107
+ `stack(
108
+ s("bd ~ ~ ~ sd ~ ~ ~, bd ~ ~ bd sd ~ bd ~").gain(0.9),
109
+ s("hh*8").gain(0.4).hpf(5000),
110
+ s("~ ~ oh ~ ~ ~ oh ~").gain(0.3)
111
+ ).swing(0.08)`,
112
+ // Complex - layered golden era style
113
+ `stack(
114
+ s("bd ~ ~ [~ bd] sd ~ ~ ~, bd ~ ~ bd sd ~ [bd bd] ~").gain(0.9),
115
+ s("~ ~ ~ ~ sd:3 ~ ~ ~").gain(0.3).room(0.2),
116
+ s("[~ hh]*8").gain(0.45).hpf(4000),
117
+ s("~ ~ oh ~ ~ ~ ~ oh:2").gain(0.25).room(0.3)
118
+ ).swing(0.1)`
119
+ ]
120
+ };
121
+ // Handle aliases
122
+ const styleMap = {
123
+ 'liquid_dnb': 'intelligent_dnb',
124
+ 'atmospheric_dnb': 'intelligent_dnb',
125
+ 'intelligent': 'intelligent_dnb',
126
+ 'liquid': 'intelligent_dnb',
127
+ 'atmospheric': 'intelligent_dnb',
128
+ 'bukem': 'intelligent_dnb',
129
+ 'triphop': 'trip_hop',
130
+ 'portishead': 'trip_hop',
131
+ 'massive_attack': 'trip_hop',
132
+ 'flying_lotus': 'trip_hop',
133
+ 'boombap': 'boom_bap',
134
+ 'golden_era': 'boom_bap',
135
+ 'premier': 'boom_bap',
136
+ 'alchemist': 'boom_bap',
137
+ 'daringer': 'boom_bap',
138
+ 'hitboy': 'boom_bap'
139
+ };
140
+ const resolvedStyle = styleMap[style.toLowerCase()] || style.toLowerCase();
141
+ const stylePatterns = patterns[resolvedStyle] || patterns.techno;
142
+ const index = Math.min(Math.floor(complexity * stylePatterns.length), stylePatterns.length - 1);
143
+ return stylePatterns[index];
144
+ }
145
+ /**
146
+ * Generates a bassline pattern for a given key and style
147
+ * @param key - Musical key (e.g., 'C', 'D', 'F#')
148
+ * @param style - Music style (e.g., 'techno', 'house', 'acid', 'dub')
149
+ * @returns Strudel bassline pattern code
150
+ */
151
+ generateBassline(key, style) {
152
+ const safeKey = key.toLowerCase();
153
+ const fourth = this.getFourth(safeKey);
154
+ const fifth = this.getFifth(safeKey);
155
+ const minorSeventh = this.getMinorSeventh(safeKey);
156
+ const patterns = {
157
+ techno: `note("${key}2 ${key}2 ${key}2 ${key}2").s("sawtooth").cutoff(800)`,
158
+ house: `note("${key}2 ~ ${key}2 ~").s("sine").gain(0.8)`,
159
+ dnb: `note("${key}1 ~ ~ ${key}2 ~ ${key}1 ~ ~").s("square").cutoff(400)`,
160
+ acid: `note("${key}2 ${key}3 ${key}2 ${this.theory.getNote(key, 3)}2").s("sawtooth").cutoff(sine.range(200, 2000).slow(4))`,
161
+ dub: `note("${key}1 ~ ~ ~ ${key}1 ~ ${this.theory.getNote(key, 5)}1 ~").s("sine:2").room(0.5)`,
162
+ funk: `note("${key}2 ${key}2 ~ ${this.theory.getNote(key, 5)}2 ~ ${key}2 ${this.theory.getNote(key, 7)}2 ~").s("square").cutoff(1200)`,
163
+ jazz: `note("${key}2 ~ ${this.theory.getNote(key, 4)}2 ~ ${this.theory.getNote(key, 7)}2 ~").s("sine").gain(0.7)`,
164
+ ambient: `note("${key}1").s("sine").attack(2).release(4).gain(0.6)`,
165
+ // === NEW GENRES ===
166
+ intelligent_dnb: `note("<${safeKey}1 ~ ${safeKey}1 ~> <${fourth}1 ~ ~ ${fourth}1> <${minorSeventh}0 ~ ${minorSeventh}0 ~> <${this.getMinorThird(safeKey)}1 ~ ~ ~>")
167
+ .s("sine")
168
+ .gain(0.65)
169
+ .lpf(80)
170
+ .attack(0.01)
171
+ .decay(0.2)
172
+ .sustain(0.5)
173
+ .release(0.4)`,
174
+ trip_hop: `note("<${safeKey}1 ~ ~ ${safeKey}1> <~ ${fourth}1 ~ ~> <${fifth}1 ~ ${safeKey}1 ~> <~ ~ ${fourth}1 ~>")
175
+ .s("sine")
176
+ .gain(0.7)
177
+ .lpf(120)
178
+ .attack(0.02)
179
+ .decay(0.3)
180
+ .sustain(0.6)
181
+ .release(0.8)
182
+ .room(0.3)`,
183
+ boom_bap: `note("<${safeKey}1 ~ ${safeKey}1 ~> <${safeKey}1 ${fourth}1 ~ ~> <${safeKey}1 ~ ${fifth}1 ~> <${fourth}1 ~ ${safeKey}1 ~>")
184
+ .s("sawtooth")
185
+ .gain(0.7)
186
+ .lpf(200)
187
+ .attack(0.01)
188
+ .decay(0.15)
189
+ .sustain(0.4)
190
+ .release(0.3)`
191
+ };
192
+ // Handle aliases
193
+ const styleMap = {
194
+ 'liquid_dnb': 'intelligent_dnb',
195
+ 'atmospheric_dnb': 'intelligent_dnb',
196
+ 'intelligent': 'intelligent_dnb',
197
+ 'liquid': 'intelligent_dnb',
198
+ 'bukem': 'intelligent_dnb',
199
+ 'triphop': 'trip_hop',
200
+ 'portishead': 'trip_hop',
201
+ 'massive_attack': 'trip_hop',
202
+ 'boombap': 'boom_bap',
203
+ 'golden_era': 'boom_bap',
204
+ 'premier': 'boom_bap'
205
+ };
206
+ const resolvedStyle = styleMap[style.toLowerCase()] || style.toLowerCase();
207
+ return patterns[resolvedStyle] || patterns.techno;
208
+ }
209
+ /**
210
+ * Generates a melodic pattern from a scale
211
+ * @param scale - Array of note names to use
212
+ * @param length - Number of notes in the melody (default: 8)
213
+ * @param octaveRange - Tuple of min and max octave numbers (default: [3, 5])
214
+ * @returns Strudel melody pattern code
215
+ */
216
+ generateMelody(scale, length = 8, octaveRange = [3, 5]) {
217
+ const notes = [];
218
+ let lastNoteIndex = Math.floor(Math.random() * scale.length);
219
+ for (let i = 0; i < length; i++) {
220
+ // Create more musical intervals (prefer steps over leaps)
221
+ const stepProbability = 0.7;
222
+ const useStep = Math.random() < stepProbability;
223
+ let noteIndex;
224
+ if (useStep) {
225
+ // Move by step (1 or 2 scale degrees)
226
+ const step = Math.random() < 0.5 ? 1 : -1;
227
+ noteIndex = (lastNoteIndex + step + scale.length) % scale.length;
228
+ }
229
+ else {
230
+ // Leap to any note
231
+ noteIndex = Math.floor(Math.random() * scale.length);
232
+ }
233
+ const note = scale[noteIndex];
234
+ const octave = octaveRange[0] + Math.floor(Math.random() * (octaveRange[1] - octaveRange[0] + 1));
235
+ notes.push(`${note.toLowerCase()}${octave}`);
236
+ lastNoteIndex = noteIndex;
237
+ }
238
+ return `note("${notes.join(' ')}").s("triangle")`;
239
+ }
240
+ /**
241
+ * Generates a chord pattern from a progression
242
+ * @param progression - Chord progression string
243
+ * @param voicing - Chord voicing style (default: 'triad')
244
+ * @returns Strudel chord pattern code
245
+ */
246
+ generateChords(progression, voicing = 'triad') {
247
+ const voicings = {
248
+ triad: '.struct("1 ~ ~ ~")',
249
+ seventh: '.struct("1 ~ ~ ~").add(note("7"))',
250
+ sustained: '.attack(0.5).release(2)',
251
+ stab: '.struct("1 ~ 1 ~").release(0.1)',
252
+ pad: '.attack(2).release(4).room(0.8)'
253
+ };
254
+ return `note("<${progression}>").s("sawtooth")${voicings[voicing] || voicings.triad}`;
255
+ }
256
+ /**
257
+ * Generates a complete multi-layer musical pattern
258
+ * @param style - Music style (e.g., 'techno', 'house', 'jazz', 'ambient')
259
+ * @param key - Musical key (default: 'C')
260
+ * @param bpm - Tempo in beats per minute (default: 120)
261
+ * @returns Complete Strudel pattern with drums, bass, chords, and melody
262
+ */
263
+ generateCompletePattern(style, key = 'C', bpm = 120) {
264
+ // Handle aliases and special genres
265
+ const styleMap = {
266
+ 'liquid_dnb': 'intelligent_dnb',
267
+ 'atmospheric_dnb': 'intelligent_dnb',
268
+ 'intelligent': 'intelligent_dnb',
269
+ 'liquid': 'intelligent_dnb',
270
+ 'atmospheric': 'intelligent_dnb',
271
+ 'bukem': 'intelligent_dnb',
272
+ 'triphop': 'trip_hop',
273
+ 'portishead': 'trip_hop',
274
+ 'massive_attack': 'trip_hop',
275
+ 'flying_lotus': 'trip_hop',
276
+ 'boombap': 'boom_bap',
277
+ 'golden_era': 'boom_bap',
278
+ 'premier': 'boom_bap',
279
+ 'alchemist': 'boom_bap',
280
+ 'daringer': 'boom_bap',
281
+ 'hitboy': 'boom_bap'
282
+ };
283
+ const resolvedStyle = styleMap[style.toLowerCase()] || style.toLowerCase();
284
+ // Use specialized generators for new genres
285
+ switch (resolvedStyle) {
286
+ case 'intelligent_dnb':
287
+ return this.generateIntelligentDnB(key, bpm || 170);
288
+ case 'trip_hop':
289
+ return this.generateTripHop(key, bpm || 90);
290
+ case 'boom_bap':
291
+ return this.generateBoomBap(key, bpm || 92);
292
+ }
293
+ // Default generation for other styles
294
+ const drums = this.generateDrumPattern(resolvedStyle, 0.7);
295
+ const bass = this.generateBassline(key, resolvedStyle);
296
+ const scale = this.theory.generateScale(key, resolvedStyle === 'jazz' ? 'dorian' : 'minor');
297
+ const melody = this.generateMelody(scale);
298
+ const chordStyle = resolvedStyle === 'jazz' ? 'jazz' :
299
+ resolvedStyle === 'house' ? 'pop' :
300
+ resolvedStyle === 'techno' ? 'edm' : 'pop';
301
+ const progression = this.theory.generateChordProgression(key, chordStyle);
302
+ const chords = this.generateChords(progression, resolvedStyle === 'ambient' ? 'pad' : 'stab');
303
+ return `// ${resolvedStyle} pattern in ${key} at ${bpm} BPM
304
+ setcpm(${bpm})
305
+
306
+ stack(
307
+ // Drums
308
+ ${drums},
309
+
310
+ // Bass
311
+ ${bass},
312
+
313
+ // Chords
314
+ ${chords}.gain(0.6),
315
+
316
+ // Melody
317
+ ${melody}.struct("~ 1 ~ 1 1 ~ 1 ~").delay(0.25).room(0.3).gain(0.5)
318
+ ).gain(0.8)`;
319
+ }
320
+ // ========================================
321
+ // INTELLIGENT DNB GENERATOR
322
+ // Style: LTJ Bukem, Good Looking Records
323
+ // ========================================
324
+ generateIntelligentDnB(key, tempo) {
325
+ const safeKey = key.toLowerCase();
326
+ const fourth = this.getFourth(safeKey);
327
+ const seventh = this.getMinorSeventh(safeKey);
328
+ const third = this.getMinorThird(safeKey);
329
+ return `// Intelligent DnB in ${key} at ${tempo} BPM
330
+ // Style: LTJ Bukem / Good Looking Records
331
+ setcps(${tempo}/60)
332
+ samples('github:tidalcycles/dirt-samples')
333
+
334
+ let chords = chord("<${safeKey}m9 ${fourth}m9 ${seventh}m9 ${third}maj7>/4").dict('ireal')
335
+
336
+ stack(
337
+ // === DRUMS - Rolling break with ghost notes ===
338
+ s("breaks165")
339
+ .fit()
340
+ .slice(8, "0 0 6 3 0 2 6 7")
341
+ .gain(0.7)
342
+ .room(0.2),
343
+
344
+ // Ghost break layer
345
+ s("breaks165")
346
+ .fit()
347
+ .chop(16)
348
+ .gain(0.12)
349
+ .hpf(3000)
350
+ .room(0.4)
351
+ .pan(perlin.range(0.3, 0.7)),
352
+
353
+ // Kick reinforcement
354
+ s("bd ~ ~ ~ [~ bd] ~ bd ~")
355
+ .bank("RolandTR909")
356
+ .gain(0.45)
357
+ .lpf(100),
358
+
359
+ // Open hat accents
360
+ s("~ ~ ~ oh ~ ~ oh ~")
361
+ .bank("RolandTR909")
362
+ .gain(0.28)
363
+ .cut(1)
364
+ .room(0.35),
365
+
366
+ // === BASS - Deep sine sub ===
367
+ note("<${safeKey}1 ~ ${safeKey}1 ~> <${fourth}1 ~ ~ ${fourth}1> <${seventh}0 ~ ${seventh}0 ~> <${third}1 ~ ~ ~>")
368
+ .s("sine")
369
+ .gain(0.65)
370
+ .lpf(80)
371
+ .attack(0.01)
372
+ .decay(0.2)
373
+ .sustain(0.5)
374
+ .release(0.4),
375
+
376
+ // === CHORDS - Jazz voicings ===
377
+ chords.voicing()
378
+ .s("gm_epiano1")
379
+ .gain(0.3)
380
+ .room(0.5)
381
+ .delay(0.3)
382
+ .delaytime(0.375)
383
+ .delayfeedback(0.32)
384
+ .lpf(4500),
385
+
386
+ // === ATMOSPHERE - Ethereal strings ===
387
+ chords.voicing()
388
+ .s("gm_strings")
389
+ .attack(2)
390
+ .sustain(0.7)
391
+ .release(3)
392
+ .gain(0.18)
393
+ .room(0.7)
394
+ .size(0.85)
395
+ .lpf(3000)
396
+ .hpf(300)
397
+ )`;
398
+ }
399
+ // ========================================
400
+ // TRIP HOP GENERATOR
401
+ // Style: Portishead, Massive Attack, Flying Lotus
402
+ // ========================================
403
+ generateTripHop(key, tempo) {
404
+ const safeKey = key.toLowerCase();
405
+ const fourth = this.getFourth(safeKey);
406
+ const fifth = this.getFifth(safeKey);
407
+ const seventh = this.getMinorSeventh(safeKey);
408
+ return `// Trip Hop in ${key} at ${tempo} BPM
409
+ // Style: Portishead / Massive Attack
410
+ setcps(${tempo}/60/2)
411
+
412
+ stack(
413
+ // === DRUMS - Slow, heavy, dusty ===
414
+
415
+ // Main beat - half time feel
416
+ s("bd ~ [~ bd] ~ bd ~ ~ ~")
417
+ .gain(0.85)
418
+ .lpf(150)
419
+ .room(0.3),
420
+
421
+ // Snare - heavy with room
422
+ s("~ ~ ~ ~ sd ~ ~ ~")
423
+ .bank("RolandTR808")
424
+ .gain(0.75)
425
+ .room(0.5)
426
+ .size(0.6),
427
+
428
+ // Ghost snares
429
+ s("~ ~ ~ sd:3? ~ ~ sd:2? ~")
430
+ .bank("RolandTR808")
431
+ .gain(0.2)
432
+ .room(0.4),
433
+
434
+ // Dusty hats
435
+ s("hh*8")
436
+ .gain(perlin.range(0.15, 0.3))
437
+ .hpf(5000)
438
+ .room(0.3)
439
+ .pan(perlin.range(0.3, 0.7).slow(4)),
440
+
441
+ // === BASS - Deep and moody ===
442
+ note("<${safeKey}1 ~ ~ ${safeKey}1> <~ ${fourth}1 ~ ~> <${fifth}1 ~ ${safeKey}1 ~> <~ ~ ${fourth}1 ~>")
443
+ .s("sine")
444
+ .gain(0.7)
445
+ .lpf(120)
446
+ .attack(0.02)
447
+ .decay(0.3)
448
+ .sustain(0.6)
449
+ .release(0.8)
450
+ .room(0.3),
451
+
452
+ // === KEYS - Dark Rhodes ===
453
+ chord("<${safeKey}m7 ${fourth}m7 ${fifth}m7 ${seventh}maj7>/4")
454
+ .dict('ireal')
455
+ .voicing()
456
+ .s("gm_epiano1")
457
+ .gain(0.25)
458
+ .room(0.5)
459
+ .lpf(2500)
460
+ .delay(0.4)
461
+ .delaytime(0.666)
462
+ .delayfeedback(0.35),
463
+
464
+ // === ATMOSPHERE - Dark pad ===
465
+ chord("<${safeKey}m7 ${fourth}m7>/8")
466
+ .dict('ireal')
467
+ .voicing()
468
+ .s("sawtooth")
469
+ .attack(3)
470
+ .sustain(0.6)
471
+ .release(4)
472
+ .lpf(sine.range(300, 800).slow(16))
473
+ .lpq(0.5)
474
+ .gain(0.12)
475
+ .room(0.8)
476
+ .size(0.9),
477
+
478
+ // === TEXTURE - Vinyl crackle vibe ===
479
+ s("hh:8*16")
480
+ .gain(0.03)
481
+ .hpf(8000)
482
+ .room(0.2)
483
+ )`;
484
+ }
485
+ // ========================================
486
+ // BOOM BAP GENERATOR
487
+ // Style: DJ Premier, Alchemist, Daringer, Hit-Boy
488
+ // ========================================
489
+ generateBoomBap(key, tempo) {
490
+ const safeKey = key.toLowerCase();
491
+ const fourth = this.getFourth(safeKey);
492
+ const fifth = this.getFifth(safeKey);
493
+ const seventh = this.getMinorSeventh(safeKey);
494
+ const minorThird = this.getMinorThird(safeKey);
495
+ return `// Boom Bap in ${key} at ${tempo} BPM
496
+ // Style: DJ Premier / Alchemist / Daringer
497
+ setcps(${tempo}/60/2)
498
+
499
+ stack(
500
+ // === DRUMS - Hard hitting, swing ===
501
+
502
+ // Main kick pattern
503
+ s("bd ~ ~ [~ bd] sd ~ ~ ~, bd ~ ~ bd sd ~ [bd bd] ~")
504
+ .gain(0.9)
505
+ .lpf(200)
506
+ .room(0.15),
507
+
508
+ // Layered snare for crack
509
+ s("~ ~ ~ ~ sd:3 ~ ~ ~")
510
+ .gain(0.35)
511
+ .hpf(200)
512
+ .room(0.25),
513
+
514
+ // Crispy hats
515
+ s("[~ hh]*8")
516
+ .gain(0.5)
517
+ .hpf(4000)
518
+ .room(0.2),
519
+
520
+ // Open hat accents
521
+ s("~ ~ oh ~ ~ ~ ~ oh:2")
522
+ .gain(0.3)
523
+ .room(0.35)
524
+ .cut(1),
525
+
526
+ // === BASS - Punchy and warm ===
527
+ note("<${safeKey}1 ~ ${safeKey}1 ~> <${safeKey}1 ${fourth}1 ~ ~> <${safeKey}1 ~ ${fifth}1 ~> <${fourth}1 ~ ${safeKey}1 ~>")
528
+ .s("sawtooth")
529
+ .gain(0.65)
530
+ .lpf(180)
531
+ .attack(0.01)
532
+ .decay(0.15)
533
+ .sustain(0.4)
534
+ .release(0.3),
535
+
536
+ // === CHOPS - Soul sample style ===
537
+ chord("<${safeKey}m7 ~ ${fourth}m7 ~> <${seventh}maj7 ~ ${minorThird}maj7 ~>/2")
538
+ .dict('ireal')
539
+ .voicing()
540
+ .s("gm_epiano1")
541
+ .gain(0.4)
542
+ .room(0.3)
543
+ .lpf(3000)
544
+ .hpf(200),
545
+
546
+ // === STRINGS - Cinematic stabs ===
547
+ chord("<${safeKey}m7 ~ ~ ~> <~ ~ ${fourth}m7 ~>/4")
548
+ .dict('ireal')
549
+ .voicing()
550
+ .s("gm_strings")
551
+ .struct("1 ~ ~ ~ ~ 1? ~ ~")
552
+ .attack(0.1)
553
+ .release(0.8)
554
+ .gain(0.25)
555
+ .room(0.4)
556
+ .lpf(4000)
557
+ .hpf(300),
558
+
559
+ // === HORNS - Optional stab ===
560
+ note("<${safeKey}4 ${minorThird}4 ${fourth}4 ${safeKey}4>/8")
561
+ .s("gm_brass")
562
+ .struct("~ ~ ~ ~ 1 ~ ~ ~")
563
+ .gain(0.2)
564
+ .room(0.3)
565
+ .lpf(3500)
566
+ ).swing(0.08)`;
567
+ }
568
+ /**
569
+ * Generates variations on an existing pattern
570
+ * @param pattern - Original pattern code
571
+ * @param variationType - Type of variation ('subtle', 'moderate', 'extreme', 'glitch', 'evolving')
572
+ * @returns Pattern with variation modifiers applied
573
+ */
574
+ generateVariation(pattern, variationType = 'subtle') {
575
+ const variations = {
576
+ subtle: '.sometimes(x => x.fast(2))',
577
+ moderate: '.every(4, x => x.rev).sometimes(x => x.fast(2))',
578
+ extreme: '.every(2, x => x.jux(rev)).sometimes(x => x.iter(4))',
579
+ glitch: '.sometimes(x => x.chop(8).rev).rarely(x => x.speed(-1))',
580
+ evolving: '.slow(4).every(8, x => x.fast(2)).every(16, x => x.palindrome)'
581
+ };
582
+ return pattern + (variations[variationType] || variations.subtle);
583
+ }
584
+ /**
585
+ * Generates a drum fill for transitions
586
+ * @param style - Music style for the fill
587
+ * @param bars - Length of the fill in bars (default: 1)
588
+ * @returns Strudel fill pattern code
589
+ */
590
+ generateFill(style, bars = 1) {
591
+ const fills = {
592
+ techno: `s("bd*8, cp*4").fast(${bars})`,
593
+ house: `s("bd*4, cp*2, hh*16").fast(${bars})`,
594
+ dnb: `s("bd*8, sn*8").fast(${bars * 2})`,
595
+ trap: `s("bd*4, hh*32").fast(${bars})`,
596
+ breakbeat: `s("bd cp bd cp, hh*8").iter(4).fast(${bars})`,
597
+ intelligent_dnb: `s("breaks165").fit().slice(16, "0 2 4 6 8 10 12 14 1 3 5 7 9 11 13 15").gain(0.7).fast(${bars})`,
598
+ trip_hop: `s("bd ~ sd ~, hh*4").room(0.5).fast(${bars})`,
599
+ boom_bap: `s("bd sd bd sd, hh*8").swing(0.1).fast(${bars})`
600
+ };
601
+ return fills[style] || fills.techno;
602
+ }
603
+ /**
604
+ * Generates a transition between two musical styles
605
+ * @param fromStyle - Starting music style
606
+ * @param toStyle - Target music style
607
+ * @param bars - Length of transition in bars (default: 4)
608
+ * @returns Strudel transition pattern with crossfade
609
+ */
610
+ generateTransition(fromStyle, toStyle, bars = 4) {
611
+ return `// Transition from ${fromStyle} to ${toStyle}
612
+ stack(
613
+ // Fade out ${fromStyle}
614
+ ${this.generateDrumPattern(fromStyle, 0.5)}.gain(perlin.range(0.8, 0).slow(${bars})),
615
+
616
+ // Fade in ${toStyle}
617
+ ${this.generateDrumPattern(toStyle, 0.5)}.gain(perlin.range(0, 0.8).slow(${bars}))
618
+ )`;
619
+ }
620
+ /**
621
+ * Generates a Euclidean rhythm pattern
622
+ * @param hits - Number of hits in the pattern
623
+ * @param steps - Total number of steps
624
+ * @param sound - Sound name to use (default: "bd")
625
+ * @returns Strudel Euclidean pattern code
626
+ */
627
+ generateEuclideanPattern(hits, steps, sound = "bd") {
628
+ const rhythm = this.theory.generateEuclideanRhythm(hits, steps);
629
+ return `s("${sound}").struct("${rhythm}")`;
630
+ }
631
+ /**
632
+ * Generates a polyrhythmic pattern with multiple Euclidean rhythms
633
+ * @param sounds - Array of sound names to use
634
+ * @param patterns - Array of hit counts for each rhythm
635
+ * @returns Strudel polyrhythm pattern code
636
+ * @throws {Error} When sounds and patterns arrays have different lengths
637
+ */
638
+ generatePolyrhythm(sounds, patterns) {
639
+ if (sounds.length !== patterns.length) {
640
+ throw new Error('Number of sounds must match number of patterns');
641
+ }
642
+ const rhythms = sounds.map((sound, i) => {
643
+ return `s("${sound}").euclid(${patterns[i]}, 16)`;
644
+ });
645
+ return `stack(\n ${rhythms.join(',\n ')}\n)`;
646
+ }
647
+ }