@williamzujkowski/live-coding-music-mcp 2.0.0 → 3.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.
@@ -1,2506 +0,0 @@
1
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
4
- import { StrudelController } from '../StrudelController.js';
5
- import { PatternStore } from '../PatternStore.js';
6
- import { MusicTheory } from '../services/MusicTheory.js';
7
- import { PatternGenerator } from '../services/PatternGenerator.js';
8
- import { GeminiService } from '../services/GeminiService.js';
9
- import { AudioCaptureService } from '../services/AudioCaptureService.js';
10
- import { MIDIExportService } from '../services/MIDIExportService.js';
11
- import { SessionManager } from '../services/SessionManager.js';
12
- import { readFileSync, existsSync } from 'fs';
13
- import { Logger } from '../utils/Logger.js';
14
- import { PerformanceMonitor } from '../utils/PerformanceMonitor.js';
15
- import { InputValidator } from '../utils/InputValidator.js';
16
- import { StrudelEngine } from '../services/StrudelEngine.js';
17
- const configPath = './config.json';
18
- const config = existsSync(configPath)
19
- ? JSON.parse(readFileSync(configPath, 'utf-8'))
20
- : { headless: false };
21
- /** Energy level presets for pattern energy adjustments (#81) */
22
- const ENERGY_LEVELS = {
23
- 0: { tempoAdjust: -20, roomAmount: 0.5, densityAdjust: '.slow(4)', description: 'minimal/ambient' },
24
- 1: { tempoAdjust: -15, roomAmount: 0.4, densityAdjust: '.slow(3)', description: 'very sparse' },
25
- 2: { tempoAdjust: -10, roomAmount: 0.3, densityAdjust: '.slow(2)', description: 'sparse' },
26
- 3: { tempoAdjust: -5, roomAmount: 0.2, densityAdjust: '.slow(1.5)', description: 'light' },
27
- 4: { tempoAdjust: 0, roomAmount: 0.15, densityAdjust: '', description: 'relaxed' },
28
- 5: { tempoAdjust: 0, roomAmount: 0.1, densityAdjust: '', description: 'normal' },
29
- 6: { tempoAdjust: 5, roomAmount: 0.08, densityAdjust: '', description: 'moderate' },
30
- 7: { tempoAdjust: 10, roomAmount: 0.05, densityAdjust: '.fast(1.25)', description: 'driving' },
31
- 8: { tempoAdjust: 15, roomAmount: 0.03, densityAdjust: '.fast(1.5)', description: 'intense' },
32
- 9: { tempoAdjust: 18, roomAmount: 0.02, densityAdjust: '.fast(1.75)', description: 'very intense' },
33
- 10: { tempoAdjust: 20, roomAmount: 0.01, densityAdjust: '.fast(2)', description: 'maximum' }
34
- };
35
- /** Mood profiles for emotional transformations (#80) */
36
- const MOOD_PROFILES = {
37
- dark: { preferMinor: true, tempoMod: -0.1, cutoffMod: -200, roomMod: 0.2, gainMod: -0.1, noteShift: -12 },
38
- euphoric: { preferMinor: false, tempoMod: 0.1, cutoffMod: 400, roomMod: 0.1, gainMod: 0.1, noteShift: 12 },
39
- melancholic: { preferMinor: true, tempoMod: -0.15, cutoffMod: -100, roomMod: 0.3, gainMod: -0.05, noteShift: 0 },
40
- aggressive: { preferMinor: false, tempoMod: 0.15, cutoffMod: 600, roomMod: -0.1, gainMod: 0.15, noteShift: 0 },
41
- dreamy: { preferMinor: false, tempoMod: -0.2, cutoffMod: -300, roomMod: 0.4, delayMod: 0.3, gainMod: -0.1, noteShift: 0 },
42
- peaceful: { preferMinor: false, tempoMod: -0.25, cutoffMod: -200, roomMod: 0.25, gainMod: -0.15, noteShift: 0 },
43
- energetic: { preferMinor: false, tempoMod: 0.2, cutoffMod: 300, roomMod: 0, gainMod: 0.1, noteShift: 0 }
44
- };
45
- export class EnhancedMCPServerFixed {
46
- server;
47
- controller;
48
- store;
49
- theory;
50
- generator;
51
- geminiService;
52
- audioCaptureService = null;
53
- midiExportService;
54
- sessionManager;
55
- logger;
56
- perfMonitor;
57
- strudelEngine;
58
- sessionHistory = [];
59
- undoStack = [];
60
- redoStack = [];
61
- /** Pattern history with metadata for browsing (#41) */
62
- historyStack = [];
63
- historyIdCounter = 0;
64
- /** Maximum history entries to prevent memory leaks */
65
- MAX_HISTORY = 100;
66
- isInitialized = false;
67
- generatedPatterns = new Map();
68
- constructor() {
69
- this.server = new Server({
70
- name: 'strudel-mcp-enhanced',
71
- version: '2.0.1',
72
- }, {
73
- capabilities: {
74
- tools: {},
75
- },
76
- });
77
- this.controller = new StrudelController(config.headless);
78
- this.store = new PatternStore('./patterns');
79
- this.theory = new MusicTheory();
80
- this.generator = new PatternGenerator();
81
- this.geminiService = new GeminiService();
82
- this.midiExportService = new MIDIExportService();
83
- this.sessionManager = new SessionManager(config.headless);
84
- this.logger = new Logger();
85
- this.perfMonitor = new PerformanceMonitor();
86
- this.strudelEngine = new StrudelEngine();
87
- this.setupHandlers();
88
- }
89
- getTools() {
90
- // Same tools as before - keeping the same structure
91
- return [
92
- // Core Control Tools (10)
93
- {
94
- name: 'init',
95
- description: 'Initialize Strudel in browser',
96
- inputSchema: { type: 'object', properties: {} }
97
- },
98
- {
99
- name: 'write',
100
- description: 'Write pattern to editor with optional auto-play and validation',
101
- inputSchema: {
102
- type: 'object',
103
- properties: {
104
- pattern: { type: 'string', description: 'Pattern code' },
105
- auto_play: { type: 'boolean', description: 'Start playback immediately after writing (default: false)' },
106
- validate: { type: 'boolean', description: 'Validate pattern before writing (default: true)' }
107
- },
108
- required: ['pattern']
109
- }
110
- },
111
- {
112
- name: 'append',
113
- description: 'Append code to current pattern',
114
- inputSchema: {
115
- type: 'object',
116
- properties: {
117
- code: { type: 'string', description: 'Code to append' }
118
- },
119
- required: ['code']
120
- }
121
- },
122
- {
123
- name: 'insert',
124
- description: 'Insert code at specific line',
125
- inputSchema: {
126
- type: 'object',
127
- properties: {
128
- position: { type: 'number', description: 'Line number' },
129
- code: { type: 'string', description: 'Code to insert' }
130
- },
131
- required: ['position', 'code']
132
- }
133
- },
134
- {
135
- name: 'replace',
136
- description: 'Replace pattern section',
137
- inputSchema: {
138
- type: 'object',
139
- properties: {
140
- search: { type: 'string', description: 'Text to replace' },
141
- replace: { type: 'string', description: 'Replacement text' }
142
- },
143
- required: ['search', 'replace']
144
- }
145
- },
146
- {
147
- name: 'play',
148
- description: 'Start playing pattern',
149
- inputSchema: { type: 'object', properties: {} }
150
- },
151
- {
152
- name: 'pause',
153
- description: 'Pause playback',
154
- inputSchema: { type: 'object', properties: {} }
155
- },
156
- {
157
- name: 'stop',
158
- description: 'Stop playback',
159
- inputSchema: { type: 'object', properties: {} }
160
- },
161
- {
162
- name: 'clear',
163
- description: 'Clear the editor',
164
- inputSchema: { type: 'object', properties: {} }
165
- },
166
- {
167
- name: 'get_pattern',
168
- description: 'Get current pattern code',
169
- inputSchema: { type: 'object', properties: {} }
170
- },
171
- // Pattern Manipulation (10)
172
- {
173
- name: 'transpose',
174
- description: 'Transpose notes by semitones',
175
- inputSchema: {
176
- type: 'object',
177
- properties: {
178
- semitones: { type: 'number', description: 'Semitones to transpose' }
179
- },
180
- required: ['semitones']
181
- }
182
- },
183
- {
184
- name: 'reverse',
185
- description: 'Reverse pattern',
186
- inputSchema: { type: 'object', properties: {} }
187
- },
188
- {
189
- name: 'stretch',
190
- description: 'Time stretch pattern',
191
- inputSchema: {
192
- type: 'object',
193
- properties: {
194
- factor: { type: 'number', description: 'Stretch factor' }
195
- },
196
- required: ['factor']
197
- }
198
- },
199
- {
200
- name: 'quantize',
201
- description: 'Quantize to grid',
202
- inputSchema: {
203
- type: 'object',
204
- properties: {
205
- grid: { type: 'string', description: 'Grid size (e.g., "1/16")' }
206
- },
207
- required: ['grid']
208
- }
209
- },
210
- {
211
- name: 'humanize',
212
- description: 'Add human timing variation',
213
- inputSchema: {
214
- type: 'object',
215
- properties: {
216
- amount: { type: 'number', description: 'Humanization amount (0-1)' }
217
- }
218
- }
219
- },
220
- {
221
- name: 'generate_variation',
222
- description: 'Create pattern variations',
223
- inputSchema: {
224
- type: 'object',
225
- properties: {
226
- type: { type: 'string', description: 'Variation type (subtle/moderate/extreme/glitch/evolving)' }
227
- }
228
- }
229
- },
230
- {
231
- name: 'generate_pattern',
232
- description: 'Generate complete pattern from style with optional auto-play',
233
- inputSchema: {
234
- type: 'object',
235
- properties: {
236
- style: { type: 'string', description: 'Music style (techno/house/dnb/ambient/etc)' },
237
- key: { type: 'string', description: 'Musical key' },
238
- bpm: { type: 'number', description: 'Tempo in BPM' },
239
- auto_play: { type: 'boolean', description: 'Start playback immediately (default: false)' }
240
- },
241
- required: ['style']
242
- }
243
- },
244
- {
245
- name: 'generate_drums',
246
- description: 'Generate drum pattern',
247
- inputSchema: {
248
- type: 'object',
249
- properties: {
250
- style: { type: 'string', description: 'Drum style' },
251
- complexity: { type: 'number', description: 'Complexity (0-1)' }
252
- },
253
- required: ['style']
254
- }
255
- },
256
- {
257
- name: 'generate_bassline',
258
- description: 'Generate bassline',
259
- inputSchema: {
260
- type: 'object',
261
- properties: {
262
- key: { type: 'string', description: 'Musical key' },
263
- style: { type: 'string', description: 'Bass style' }
264
- },
265
- required: ['key', 'style']
266
- }
267
- },
268
- {
269
- name: 'generate_melody',
270
- description: 'Generate melody from scale',
271
- inputSchema: {
272
- type: 'object',
273
- properties: {
274
- scale: { type: 'string', description: 'Scale name' },
275
- root: { type: 'string', description: 'Root note' },
276
- length: { type: 'number', description: 'Number of notes' }
277
- },
278
- required: ['scale', 'root']
279
- }
280
- },
281
- // Audio Analysis (5)
282
- {
283
- name: 'analyze',
284
- description: 'Complete audio analysis',
285
- inputSchema: { type: 'object', properties: {} }
286
- },
287
- {
288
- name: 'analyze_spectrum',
289
- description: 'FFT spectrum analysis',
290
- inputSchema: { type: 'object', properties: {} }
291
- },
292
- {
293
- name: 'analyze_rhythm',
294
- description: 'Rhythm analysis',
295
- inputSchema: { type: 'object', properties: {} }
296
- },
297
- {
298
- name: 'detect_tempo',
299
- description: 'BPM detection',
300
- inputSchema: { type: 'object', properties: {} }
301
- },
302
- {
303
- name: 'detect_key',
304
- description: 'Key detection',
305
- inputSchema: { type: 'object', properties: {} }
306
- },
307
- {
308
- name: 'validate_pattern_runtime',
309
- description: 'Validate pattern with runtime error checking (monitors Strudel console for errors)',
310
- inputSchema: {
311
- type: 'object',
312
- properties: {
313
- pattern: { type: 'string', description: 'Pattern code to validate' },
314
- waitMs: { type: 'number', description: 'How long to wait for errors (default 500ms)' }
315
- },
316
- required: ['pattern']
317
- }
318
- },
319
- // Effects & Processing (5)
320
- {
321
- name: 'add_effect',
322
- description: 'Add effect to pattern',
323
- inputSchema: {
324
- type: 'object',
325
- properties: {
326
- effect: { type: 'string', description: 'Effect name' },
327
- params: { type: 'string', description: 'Effect parameters' }
328
- },
329
- required: ['effect']
330
- }
331
- },
332
- {
333
- name: 'remove_effect',
334
- description: 'Remove effect',
335
- inputSchema: {
336
- type: 'object',
337
- properties: {
338
- effect: { type: 'string', description: 'Effect to remove' }
339
- },
340
- required: ['effect']
341
- }
342
- },
343
- {
344
- name: 'set_tempo',
345
- description: 'Set BPM',
346
- inputSchema: {
347
- type: 'object',
348
- properties: {
349
- bpm: { type: 'number', description: 'Tempo in BPM' }
350
- },
351
- required: ['bpm']
352
- }
353
- },
354
- {
355
- name: 'add_swing',
356
- description: 'Add swing to pattern',
357
- inputSchema: {
358
- type: 'object',
359
- properties: {
360
- amount: { type: 'number', description: 'Swing amount (0-1)' }
361
- },
362
- required: ['amount']
363
- }
364
- },
365
- {
366
- name: 'apply_scale',
367
- description: 'Apply scale to notes',
368
- inputSchema: {
369
- type: 'object',
370
- properties: {
371
- scale: { type: 'string', description: 'Scale name' },
372
- root: { type: 'string', description: 'Root note' }
373
- },
374
- required: ['scale', 'root']
375
- }
376
- },
377
- // Mood Transformation Tool (#80)
378
- {
379
- name: 'shift_mood',
380
- description: 'Transform current pattern to match a different emotional mood by adjusting tempo, effects, and note choices. Moods: dark, euphoric, melancholic, aggressive, dreamy, peaceful, energetic.',
381
- inputSchema: {
382
- type: 'object',
383
- properties: {
384
- target_mood: {
385
- type: 'string',
386
- enum: ['dark', 'euphoric', 'melancholic', 'aggressive', 'dreamy', 'peaceful', 'energetic'],
387
- description: 'Target mood'
388
- },
389
- intensity: {
390
- type: 'number',
391
- minimum: 0,
392
- maximum: 1,
393
- description: 'How strongly to apply the mood transformation (0-1, default: 0.5)'
394
- },
395
- auto_play: {
396
- type: 'boolean',
397
- description: 'Start playback after transformation (default: true)'
398
- }
399
- },
400
- required: ['target_mood']
401
- }
402
- },
403
- // Session Management (5)
404
- {
405
- name: 'save',
406
- description: 'Save pattern with metadata',
407
- inputSchema: {
408
- type: 'object',
409
- properties: {
410
- name: { type: 'string', description: 'Pattern name' },
411
- tags: { type: 'array', items: { type: 'string' } }
412
- },
413
- required: ['name']
414
- }
415
- },
416
- {
417
- name: 'load',
418
- description: 'Load saved pattern',
419
- inputSchema: {
420
- type: 'object',
421
- properties: {
422
- name: { type: 'string', description: 'Pattern name' }
423
- },
424
- required: ['name']
425
- }
426
- },
427
- {
428
- name: 'list',
429
- description: 'List saved patterns',
430
- inputSchema: {
431
- type: 'object',
432
- properties: {
433
- tag: { type: 'string', description: 'Filter by tag' }
434
- }
435
- }
436
- },
437
- {
438
- name: 'undo',
439
- description: 'Undo last action',
440
- inputSchema: { type: 'object', properties: {} }
441
- },
442
- {
443
- name: 'redo',
444
- description: 'Redo action',
445
- inputSchema: { type: 'object', properties: {} }
446
- },
447
- // Pattern History Tools (#41)
448
- {
449
- name: 'list_history',
450
- description: 'List recent pattern history with timestamps and previews',
451
- inputSchema: {
452
- type: 'object',
453
- properties: {
454
- limit: { type: 'number', description: 'Maximum entries to return (default: 10)' }
455
- }
456
- }
457
- },
458
- {
459
- name: 'restore_history',
460
- description: 'Restore a previous pattern from history by ID',
461
- inputSchema: {
462
- type: 'object',
463
- properties: {
464
- id: { type: 'number', description: 'History entry ID to restore' }
465
- },
466
- required: ['id']
467
- }
468
- },
469
- {
470
- name: 'compare_patterns',
471
- description: 'Compare two patterns from history showing differences',
472
- inputSchema: {
473
- type: 'object',
474
- properties: {
475
- id1: { type: 'number', description: 'First pattern ID' },
476
- id2: { type: 'number', description: 'Second pattern ID (default: current pattern)' }
477
- },
478
- required: ['id1']
479
- }
480
- },
481
- // Additional Music Theory Tools (5)
482
- {
483
- name: 'generate_scale',
484
- description: 'Generate scale notes',
485
- inputSchema: {
486
- type: 'object',
487
- properties: {
488
- root: { type: 'string', description: 'Root note' },
489
- scale: { type: 'string', description: 'Scale type' }
490
- },
491
- required: ['root', 'scale']
492
- }
493
- },
494
- {
495
- name: 'generate_chord_progression',
496
- description: 'Generate chord progression',
497
- inputSchema: {
498
- type: 'object',
499
- properties: {
500
- key: { type: 'string', description: 'Key' },
501
- style: { type: 'string', description: 'Style (pop/jazz/blues/etc)' }
502
- },
503
- required: ['key', 'style']
504
- }
505
- },
506
- {
507
- name: 'generate_euclidean',
508
- description: 'Generate Euclidean rhythm',
509
- inputSchema: {
510
- type: 'object',
511
- properties: {
512
- hits: { type: 'number', description: 'Number of hits' },
513
- steps: { type: 'number', description: 'Total steps' },
514
- sound: { type: 'string', description: 'Sound to use' }
515
- },
516
- required: ['hits', 'steps']
517
- }
518
- },
519
- {
520
- name: 'generate_polyrhythm',
521
- description: 'Generate polyrhythm',
522
- inputSchema: {
523
- type: 'object',
524
- properties: {
525
- sounds: { type: 'array', items: { type: 'string' }, description: 'Sounds to use' },
526
- patterns: { type: 'array', items: { type: 'number' }, description: 'Pattern numbers' }
527
- },
528
- required: ['sounds', 'patterns']
529
- }
530
- },
531
- {
532
- name: 'generate_fill',
533
- description: 'Generate drum fill',
534
- inputSchema: {
535
- type: 'object',
536
- properties: {
537
- style: { type: 'string', description: 'Fill style' },
538
- bars: { type: 'number', description: 'Number of bars' }
539
- },
540
- required: ['style']
541
- }
542
- },
543
- // Performance Monitoring (2)
544
- {
545
- name: 'performance_report',
546
- description: 'Get performance metrics and bottlenecks',
547
- inputSchema: { type: 'object', properties: {} }
548
- },
549
- {
550
- name: 'memory_usage',
551
- description: 'Get current memory usage statistics',
552
- inputSchema: { type: 'object', properties: {} }
553
- },
554
- // UX Tools - Browser Control (#37)
555
- {
556
- name: 'show_browser',
557
- description: 'Bring browser window to foreground for visual feedback',
558
- inputSchema: { type: 'object', properties: {} }
559
- },
560
- {
561
- name: 'screenshot',
562
- description: 'Take a screenshot of the current Strudel editor state',
563
- inputSchema: {
564
- type: 'object',
565
- properties: {
566
- filename: { type: 'string', description: 'Optional filename for screenshot' }
567
- }
568
- }
569
- },
570
- // UX Tools - Status & Diagnostics (#39)
571
- {
572
- name: 'status',
573
- description: 'Get current browser and playback status (quick state check)',
574
- inputSchema: { type: 'object', properties: {} }
575
- },
576
- {
577
- name: 'diagnostics',
578
- description: 'Get detailed browser diagnostics including cache, errors, and performance',
579
- inputSchema: { type: 'object', properties: {} }
580
- },
581
- {
582
- name: 'show_errors',
583
- description: 'Display captured console errors and warnings from Strudel',
584
- inputSchema: { type: 'object', properties: {} }
585
- },
586
- // UX Tools - High-level Compose (#42, #73)
587
- {
588
- name: 'compose',
589
- description: 'Generate, write, and play a complete pattern in one step. Auto-initializes browser if needed.',
590
- inputSchema: {
591
- type: 'object',
592
- properties: {
593
- style: { type: 'string', description: 'Genre: techno, house, dnb, ambient, trap, jungle, jazz, experimental' },
594
- tempo: { type: 'number', description: 'BPM (default: genre-appropriate)' },
595
- key: { type: 'string', description: 'Musical key (default: C)' },
596
- auto_play: { type: 'boolean', description: 'Start playback immediately (default: true)' },
597
- get_feedback: { type: 'boolean', description: 'Get AI feedback on the generated pattern (default: false)' }
598
- },
599
- required: ['style']
600
- }
601
- },
602
- // AI Feedback Tools (#67)
603
- {
604
- name: 'get_pattern_feedback',
605
- description: 'Get AI-powered creative feedback on the current pattern using Google Gemini. Analyzes pattern structure and optionally audio.',
606
- inputSchema: {
607
- type: 'object',
608
- properties: {
609
- includeAudio: { type: 'boolean', description: 'Include audio analysis (plays pattern briefly). Default: false' },
610
- style: { type: 'string', description: 'Optional style hint for context (e.g., "techno", "ambient")' }
611
- }
612
- }
613
- },
614
- // Audio Capture Tools (#72)
615
- {
616
- name: 'start_audio_capture',
617
- description: 'Start capturing audio from Strudel output. Audio must be playing for capture to work.',
618
- inputSchema: {
619
- type: 'object',
620
- properties: {
621
- format: { type: 'string', enum: ['webm', 'opus'], description: 'Audio format (default: webm)' },
622
- maxDuration: { type: 'number', description: 'Maximum capture duration in milliseconds' }
623
- }
624
- }
625
- },
626
- {
627
- name: 'stop_audio_capture',
628
- description: 'Stop audio capture and return the recorded audio as base64-encoded data.',
629
- inputSchema: { type: 'object', properties: {} }
630
- },
631
- {
632
- name: 'capture_audio_sample',
633
- description: 'Capture a fixed-duration audio sample from Strudel output. Audio must be playing.',
634
- inputSchema: {
635
- type: 'object',
636
- properties: {
637
- duration: { type: 'number', description: 'Duration in milliseconds (default: 5000)' }
638
- }
639
- }
640
- },
641
- // MIDI Export Tools (#74)
642
- {
643
- name: 'export_midi',
644
- description: 'Export current pattern to MIDI file. Parses note(), n(), and chord() functions.',
645
- inputSchema: {
646
- type: 'object',
647
- properties: {
648
- filename: { type: 'string', description: 'Output filename (optional, default: pattern.mid)' },
649
- duration: { type: 'number', description: 'Export duration in bars (default: 4)' },
650
- bpm: { type: 'number', description: 'Tempo in BPM (default: 120)' },
651
- format: { type: 'string', enum: ['file', 'base64'], description: 'Output format: file or base64 (default: base64)' }
652
- }
653
- }
654
- },
655
- // Multi-Session Management Tools (#75)
656
- {
657
- name: 'create_session',
658
- description: 'Create a new isolated Strudel browser session. Sessions share one browser but have isolated contexts.',
659
- inputSchema: {
660
- type: 'object',
661
- properties: {
662
- session_id: { type: 'string', description: 'Unique identifier for the session' }
663
- },
664
- required: ['session_id']
665
- }
666
- },
667
- {
668
- name: 'destroy_session',
669
- description: 'Close and destroy a Strudel session, releasing its resources.',
670
- inputSchema: {
671
- type: 'object',
672
- properties: {
673
- session_id: { type: 'string', description: 'Session identifier to destroy' }
674
- },
675
- required: ['session_id']
676
- }
677
- },
678
- {
679
- name: 'list_sessions',
680
- description: 'List all active Strudel sessions with their metadata.',
681
- inputSchema: { type: 'object', properties: {} }
682
- },
683
- {
684
- name: 'switch_session',
685
- description: 'Change the default session used by other tools.',
686
- inputSchema: {
687
- type: 'object',
688
- properties: {
689
- session_id: { type: 'string', description: 'Session identifier to set as default' }
690
- },
691
- required: ['session_id']
692
- }
693
- },
694
- // Pattern Refinement Tools (#78, #81)
695
- {
696
- name: 'refine',
697
- description: 'Incrementally refine the current pattern with simple directional commands. Supports: faster/slower (tempo), louder/quieter (gain), brighter/darker (filter cutoff), "more reverb"/drier (reverb). Auto-plays after applying refinement.',
698
- inputSchema: {
699
- type: 'object',
700
- properties: {
701
- direction: {
702
- type: 'string',
703
- description: 'Refinement direction: faster, slower, louder, quieter, brighter, darker, "more reverb", or drier'
704
- }
705
- },
706
- required: ['direction']
707
- }
708
- },
709
- {
710
- name: 'set_energy',
711
- description: 'Adjust the overall energy level of the current pattern on a 0-10 scale. 0: minimal/ambient, 1-2: sparse, 3-4: light/relaxed, 5-6: normal/moderate, 7-8: driving/intense, 9-10: maximum. Auto-plays after applying energy level.',
712
- inputSchema: {
713
- type: 'object',
714
- properties: {
715
- level: {
716
- type: 'number',
717
- description: 'Energy level from 0 (minimal) to 10 (maximum)'
718
- }
719
- },
720
- required: ['level']
721
- }
722
- },
723
- // AI Collaborative Jamming (#82)
724
- {
725
- name: 'jam_with',
726
- description: 'AI generates a complementary layer to jam with your pattern. Analyzes current pattern to detect tempo, key, and existing layers, then generates a matching layer that fits musically.',
727
- inputSchema: {
728
- type: 'object',
729
- properties: {
730
- layer: {
731
- type: 'string',
732
- enum: ['drums', 'bass', 'melody', 'pad', 'texture'],
733
- description: 'Type of layer to add: drums, bass, melody, pad, or texture'
734
- },
735
- style_hint: {
736
- type: 'string',
737
- description: 'Optional style guidance (e.g., "funky", "minimal", "atmospheric")'
738
- },
739
- auto_play: {
740
- type: 'boolean',
741
- description: 'Start playback after adding layer (default: true)'
742
- }
743
- },
744
- required: ['layer']
745
- }
746
- }
747
- ];
748
- }
749
- setupHandlers() {
750
- this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
751
- tools: this.getTools()
752
- }));
753
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
754
- const { name, arguments: args } = request.params;
755
- try {
756
- this.logger.info(`Executing tool: ${name}`, args);
757
- // Measure performance
758
- const result = await this.perfMonitor.measureAsync(name, () => this.executeTool(name, args));
759
- return {
760
- content: [{
761
- type: 'text',
762
- text: typeof result === 'string' ? result : JSON.stringify(result, null, 2)
763
- }],
764
- };
765
- }
766
- catch (error) {
767
- this.logger.error(`Tool execution failed: ${name}`, error);
768
- return {
769
- content: [{
770
- type: 'text',
771
- text: `Error: ${error.message}`
772
- }],
773
- };
774
- }
775
- });
776
- }
777
- requiresInitialization(toolName) {
778
- const toolsRequiringInit = [
779
- 'write', 'append', 'insert', 'replace', 'play', 'pause', 'stop',
780
- 'clear', 'get_pattern', 'analyze', 'analyze_spectrum', 'analyze_rhythm',
781
- 'transpose', 'reverse', 'stretch', 'humanize', 'generate_variation',
782
- 'add_effect', 'add_swing', 'set_tempo', 'save', 'undo', 'redo',
783
- 'validate_pattern_runtime'
784
- ];
785
- const toolsRequiringWrite = [
786
- 'generate_pattern', 'generate_drums', 'generate_bassline', 'generate_melody',
787
- 'generate_chord_progression', 'generate_euclidean', 'generate_polyrhythm',
788
- 'generate_fill'
789
- ];
790
- return toolsRequiringInit.includes(toolName) || toolsRequiringWrite.includes(toolName);
791
- }
792
- async getCurrentPatternSafe() {
793
- if (!this.isInitialized) {
794
- // Return the last generated pattern if available
795
- const lastPattern = Array.from(this.generatedPatterns.values()).pop();
796
- return lastPattern || '';
797
- }
798
- try {
799
- return await this.controller.getCurrentPattern();
800
- }
801
- catch (e) {
802
- return '';
803
- }
804
- }
805
- async writePatternSafe(pattern) {
806
- if (!this.isInitialized) {
807
- // Store the pattern for later use
808
- const id = `pattern_${Date.now()}`;
809
- this.generatedPatterns.set(id, pattern);
810
- return `Pattern generated (initialize Strudel to use it): ${pattern.substring(0, 50)}...`;
811
- }
812
- return await this.controller.writePattern(pattern);
813
- }
814
- /**
815
- * Gets a StrudelController for the specified session, or the default session.
816
- * Falls back to the legacy single controller if no sessions exist.
817
- * @param sessionId - Optional session ID. Uses default session if not specified.
818
- * @returns StrudelController for the session
819
- * @throws {Error} If session doesn't exist
820
- */
821
- getControllerForSession(sessionId) {
822
- // If session_id is specified, use the SessionManager
823
- if (sessionId) {
824
- const controller = this.sessionManager.getSession(sessionId);
825
- if (!controller) {
826
- throw new Error(`Session '${sessionId}' not found. Create it first with create_session.`);
827
- }
828
- return controller;
829
- }
830
- // If sessions exist and there's a default, use it
831
- const defaultController = this.sessionManager.getDefaultSession();
832
- if (defaultController) {
833
- return defaultController;
834
- }
835
- // Fall back to legacy single controller for backwards compatibility
836
- return this.controller;
837
- }
838
- /**
839
- * Checks if a session exists (or default/legacy controller is initialized)
840
- * @param sessionId - Optional session ID
841
- * @returns True if controller is available
842
- */
843
- hasSession(sessionId) {
844
- if (sessionId) {
845
- return this.sessionManager.getSession(sessionId) !== undefined;
846
- }
847
- // Check if we have a default session or the legacy controller is initialized
848
- return this.sessionManager.getDefaultSession() !== undefined || this.isInitialized;
849
- }
850
- async executeTool(name, args) {
851
- // Check if tool needs initialization
852
- if (this.requiresInitialization(name) && !this.isInitialized && name !== 'init') {
853
- // For generation tools that don't require browser, handle them specially
854
- const generationTools = [
855
- 'generate_pattern', 'generate_drums', 'generate_bassline', 'generate_melody',
856
- 'generate_chord_progression', 'generate_euclidean', 'generate_polyrhythm', 'generate_fill'
857
- ];
858
- if (!generationTools.includes(name)) {
859
- return `Browser not initialized. Run 'init' first to use ${name}.`;
860
- }
861
- }
862
- // Save current state for undo and history (#41) (only if initialized)
863
- if (['write', 'append', 'insert', 'replace', 'clear'].includes(name) && this.isInitialized) {
864
- try {
865
- const current = await this.controller.getCurrentPattern();
866
- this.undoStack.push(current);
867
- // Add to history stack with metadata (#41)
868
- this.historyIdCounter++;
869
- this.historyStack.push({
870
- id: this.historyIdCounter,
871
- pattern: current,
872
- timestamp: new Date(),
873
- action: name
874
- });
875
- // Enforce bounds to prevent memory leaks
876
- if (this.undoStack.length > this.MAX_HISTORY) {
877
- this.undoStack.shift();
878
- }
879
- if (this.historyStack.length > this.MAX_HISTORY) {
880
- this.historyStack.shift();
881
- }
882
- this.redoStack = [];
883
- }
884
- catch (e) {
885
- // Controller might not be initialized yet
886
- }
887
- }
888
- switch (name) {
889
- // Core Control
890
- case 'init':
891
- const initResult = await this.controller.initialize();
892
- this.isInitialized = true;
893
- // Write any pending patterns
894
- if (this.generatedPatterns.size > 0) {
895
- const lastPattern = Array.from(this.generatedPatterns.values()).pop();
896
- if (lastPattern) {
897
- await this.controller.writePattern(lastPattern);
898
- return `${initResult}. Loaded generated pattern.`;
899
- }
900
- }
901
- return initResult;
902
- case 'write':
903
- InputValidator.validateStringLength(args.pattern, 'pattern', 10000, true);
904
- // Validate pattern if requested (default: true) - Issue #40
905
- if (args.validate !== false && this.isInitialized && typeof this.controller.validatePattern === 'function') {
906
- try {
907
- const validation = await this.controller.validatePattern(args.pattern);
908
- if (validation && !validation.valid) {
909
- return {
910
- success: false,
911
- errors: validation.errors,
912
- warnings: validation.warnings,
913
- suggestions: validation.suggestions,
914
- message: `Pattern validation failed: ${validation.errors.join('; ')}`
915
- };
916
- }
917
- }
918
- catch (e) {
919
- // Validation failed, but we can still try to write
920
- this.logger.warn('Pattern validation threw error, continuing with write');
921
- }
922
- }
923
- const writeResult = await this.writePatternSafe(args.pattern);
924
- // Auto-play if requested - Issue #38
925
- if (args.auto_play && this.isInitialized) {
926
- await this.controller.play();
927
- return `${writeResult}. Playing.`;
928
- }
929
- return writeResult;
930
- case 'append':
931
- InputValidator.validateStringLength(args.code, 'code', 10000, true);
932
- const current = await this.getCurrentPatternSafe();
933
- return await this.writePatternSafe(current + '\n' + args.code);
934
- case 'insert':
935
- InputValidator.validatePositiveInteger(args.position, 'position');
936
- InputValidator.validateStringLength(args.code, 'code', 10000, true);
937
- const lines = (await this.getCurrentPatternSafe()).split('\n');
938
- lines.splice(args.position, 0, args.code);
939
- return await this.writePatternSafe(lines.join('\n'));
940
- case 'replace':
941
- InputValidator.validateStringLength(args.search, 'search', 1000, true);
942
- InputValidator.validateStringLength(args.replace, 'replace', 10000, true);
943
- const pattern = await this.getCurrentPatternSafe();
944
- // Escape $ in replacement to prevent special sequence injection ($&, $1, $', etc.)
945
- const safeReplacement = args.replace.replace(/\$/g, '$$$$');
946
- const replaced = pattern.replace(args.search, safeReplacement);
947
- return await this.writePatternSafe(replaced);
948
- case 'play':
949
- return await this.controller.play();
950
- case 'pause':
951
- case 'stop':
952
- return await this.controller.stop();
953
- case 'clear':
954
- return await this.writePatternSafe('');
955
- case 'get_pattern':
956
- return await this.getCurrentPatternSafe();
957
- // Pattern Generation - These can work without browser
958
- case 'generate_pattern':
959
- InputValidator.validateStringLength(args.style, 'style', 100, false);
960
- if (args.key) {
961
- InputValidator.validateRootNote(args.key);
962
- }
963
- if (args.bpm !== undefined) {
964
- InputValidator.validateBPM(args.bpm);
965
- }
966
- const generated = this.generator.generateCompletePattern(args.style, args.key || 'C', args.bpm || 120);
967
- await this.writePatternSafe(generated);
968
- // Auto-play if requested - Issue #38
969
- if (args.auto_play && this.isInitialized) {
970
- await this.controller.play();
971
- return `Generated ${args.style} pattern. Playing.`;
972
- }
973
- return `Generated ${args.style} pattern`;
974
- case 'generate_drums':
975
- InputValidator.validateStringLength(args.style, 'style', 100, false);
976
- if (args.complexity !== undefined) {
977
- InputValidator.validateNormalizedValue(args.complexity, 'complexity');
978
- }
979
- const drums = this.generator.generateDrumPattern(args.style, args.complexity || 0.5);
980
- const currentDrum = await this.getCurrentPatternSafe();
981
- const newDrumPattern = currentDrum ? currentDrum + '\n' + drums : drums;
982
- await this.writePatternSafe(newDrumPattern);
983
- return `Generated ${args.style} drums`;
984
- case 'generate_bassline':
985
- InputValidator.validateRootNote(args.key);
986
- InputValidator.validateStringLength(args.style, 'style', 100, false);
987
- const bass = this.generator.generateBassline(args.key, args.style);
988
- const currentBass = await this.getCurrentPatternSafe();
989
- const newBassPattern = currentBass ? currentBass + '\n' + bass : bass;
990
- await this.writePatternSafe(newBassPattern);
991
- return `Generated ${args.style} bassline in ${args.key}`;
992
- case 'generate_melody':
993
- InputValidator.validateRootNote(args.root);
994
- InputValidator.validateScaleName(args.scale);
995
- if (args.length !== undefined) {
996
- InputValidator.validatePositiveInteger(args.length, 'length');
997
- }
998
- const scale = this.theory.generateScale(args.root, args.scale);
999
- const melody = this.generator.generateMelody(scale, args.length || 8);
1000
- const currentMelody = await this.getCurrentPatternSafe();
1001
- const newMelodyPattern = currentMelody ? currentMelody + '\n' + melody : melody;
1002
- await this.writePatternSafe(newMelodyPattern);
1003
- return `Generated melody in ${args.root} ${args.scale}`;
1004
- // Music Theory - These don't require browser
1005
- case 'generate_scale':
1006
- InputValidator.validateRootNote(args.root);
1007
- InputValidator.validateScaleName(args.scale);
1008
- const scaleNotes = this.theory.generateScale(args.root, args.scale);
1009
- return `${args.root} ${args.scale} scale: ${scaleNotes.join(', ')}`;
1010
- case 'generate_chord_progression':
1011
- InputValidator.validateRootNote(args.key);
1012
- InputValidator.validateChordStyle(args.style);
1013
- const progression = this.theory.generateChordProgression(args.key, args.style);
1014
- const chordPattern = this.generator.generateChords(progression);
1015
- const currentChords = await this.getCurrentPatternSafe();
1016
- const newChordPattern = currentChords ? currentChords + '\n' + chordPattern : chordPattern;
1017
- await this.writePatternSafe(newChordPattern);
1018
- return `Generated ${args.style} progression in ${args.key}: ${progression}`;
1019
- case 'generate_euclidean':
1020
- InputValidator.validateEuclidean(args.hits, args.steps);
1021
- if (args.sound) {
1022
- InputValidator.validateStringLength(args.sound, 'sound', 100, false);
1023
- }
1024
- const euclidean = this.generator.generateEuclideanPattern(args.hits, args.steps, args.sound || 'bd');
1025
- const currentEuc = await this.getCurrentPatternSafe();
1026
- const newEucPattern = currentEuc ? currentEuc + '\n' + euclidean : euclidean;
1027
- await this.writePatternSafe(newEucPattern);
1028
- return `Generated Euclidean rhythm (${args.hits}/${args.steps})`;
1029
- case 'generate_polyrhythm':
1030
- args.sounds.forEach((sound) => {
1031
- InputValidator.validateStringLength(sound, 'sound', 100, false);
1032
- });
1033
- args.patterns.forEach((pattern) => {
1034
- InputValidator.validatePositiveInteger(pattern, 'pattern');
1035
- });
1036
- const poly = this.generator.generatePolyrhythm(args.sounds, args.patterns);
1037
- const currentPoly = await this.getCurrentPatternSafe();
1038
- const newPolyPattern = currentPoly ? currentPoly + '\n' + poly : poly;
1039
- await this.writePatternSafe(newPolyPattern);
1040
- return `Generated polyrhythm`;
1041
- case 'generate_fill':
1042
- InputValidator.validateStringLength(args.style, 'style', 100, false);
1043
- if (args.bars !== undefined) {
1044
- InputValidator.validatePositiveInteger(args.bars, 'bars');
1045
- }
1046
- const fill = this.generator.generateFill(args.style, args.bars || 1);
1047
- const currentFill = await this.getCurrentPatternSafe();
1048
- const newFillPattern = currentFill ? currentFill + '\n' + fill : fill;
1049
- await this.writePatternSafe(newFillPattern);
1050
- return `Generated ${args.bars || 1} bar fill`;
1051
- // Pattern Manipulation - These require browser
1052
- case 'transpose':
1053
- // Semitones can be positive or negative, just validate it's a number
1054
- if (typeof args.semitones !== 'number' || !Number.isInteger(args.semitones)) {
1055
- throw new Error('Semitones must be an integer');
1056
- }
1057
- const toTranspose = await this.getCurrentPatternSafe();
1058
- const transposed = this.transposePattern(toTranspose, args.semitones);
1059
- await this.writePatternSafe(transposed);
1060
- return `Transposed ${args.semitones} semitones`;
1061
- case 'reverse':
1062
- const toReverse = await this.getCurrentPatternSafe();
1063
- const reversed = toReverse + '.rev';
1064
- await this.writePatternSafe(reversed);
1065
- return 'Pattern reversed';
1066
- case 'stretch':
1067
- InputValidator.validateGain(args.factor); // Positive number, use gain validator for simplicity
1068
- const toStretch = await this.getCurrentPatternSafe();
1069
- const stretched = toStretch + `.slow(${args.factor})`;
1070
- await this.writePatternSafe(stretched);
1071
- return `Stretched by factor of ${args.factor}`;
1072
- case 'humanize':
1073
- if (args.amount !== undefined) {
1074
- InputValidator.validateNormalizedValue(args.amount, 'amount');
1075
- }
1076
- const toHumanize = await this.getCurrentPatternSafe();
1077
- const humanized = toHumanize + `.nudge(rand.range(-${args.amount || 0.01}, ${args.amount || 0.01}))`;
1078
- await this.writePatternSafe(humanized);
1079
- return 'Added human timing';
1080
- case 'generate_variation':
1081
- const toVary = await this.getCurrentPatternSafe();
1082
- const varied = this.generator.generateVariation(toVary, args.type || 'subtle');
1083
- await this.writePatternSafe(varied);
1084
- return `Added ${args.type || 'subtle'} variation`;
1085
- // Effects - These require browser
1086
- case 'add_effect':
1087
- InputValidator.validateStringLength(args.effect, 'effect', 100, false);
1088
- if (args.params) {
1089
- InputValidator.validateStringLength(args.params, 'params', 1000, true);
1090
- }
1091
- const currentEffect = await this.getCurrentPatternSafe();
1092
- const withEffect = args.params
1093
- ? currentEffect + `.${args.effect}(${args.params})`
1094
- : currentEffect + `.${args.effect}()`;
1095
- await this.writePatternSafe(withEffect);
1096
- return `Added ${args.effect} effect`;
1097
- case 'add_swing':
1098
- InputValidator.validateNormalizedValue(args.amount, 'amount');
1099
- const currentSwing = await this.getCurrentPatternSafe();
1100
- const withSwing = currentSwing + `.swing(${args.amount})`;
1101
- await this.writePatternSafe(withSwing);
1102
- return `Added swing: ${args.amount}`;
1103
- case 'set_tempo':
1104
- InputValidator.validateBPM(args.bpm);
1105
- const currentTempo = await this.getCurrentPatternSafe();
1106
- const withTempo = `setcpm(${args.bpm})\n${currentTempo}`;
1107
- await this.writePatternSafe(withTempo);
1108
- return `Set tempo to ${args.bpm} BPM`;
1109
- // Audio Analysis - Requires browser
1110
- case 'analyze':
1111
- if (!this.isInitialized) {
1112
- return 'Browser not initialized. Run init first.';
1113
- }
1114
- return await this.controller.analyzeAudio();
1115
- case 'analyze_spectrum':
1116
- if (!this.isInitialized) {
1117
- return 'Browser not initialized. Run init first.';
1118
- }
1119
- const spectrum = await this.controller.analyzeAudio();
1120
- return spectrum.features || spectrum;
1121
- case 'analyze_rhythm':
1122
- if (!this.isInitialized) {
1123
- return 'Browser not initialized. Run init first.';
1124
- }
1125
- const analysis = await this.controller.analyzeAudio();
1126
- return {
1127
- isPlaying: analysis.features?.isPlaying,
1128
- tempo: 'Analysis pending implementation',
1129
- pattern: 'Rhythm pattern analysis'
1130
- };
1131
- case 'detect_tempo':
1132
- if (!this.isInitialized) {
1133
- return 'Browser not initialized. Run init first.';
1134
- }
1135
- try {
1136
- const tempoAnalysis = await this.controller.detectTempo();
1137
- if (!tempoAnalysis || tempoAnalysis.bpm === 0) {
1138
- return {
1139
- bpm: 0,
1140
- confidence: 0,
1141
- message: 'No tempo detected. Ensure audio is playing and has a clear rhythmic pattern.'
1142
- };
1143
- }
1144
- return {
1145
- bpm: tempoAnalysis.bpm,
1146
- confidence: Math.round(tempoAnalysis.confidence * 100) / 100,
1147
- method: tempoAnalysis.method,
1148
- message: `Detected ${tempoAnalysis.bpm} BPM with ${Math.round(tempoAnalysis.confidence * 100)}% confidence`
1149
- };
1150
- }
1151
- catch (error) {
1152
- return {
1153
- bpm: 0,
1154
- confidence: 0,
1155
- error: error.message || 'Tempo detection failed'
1156
- };
1157
- }
1158
- case 'detect_key':
1159
- if (!this.isInitialized) {
1160
- return 'Browser not initialized. Run init first.';
1161
- }
1162
- try {
1163
- const keyAnalysis = await this.controller.detectKey();
1164
- if (!keyAnalysis || keyAnalysis.confidence < 0.1) {
1165
- return {
1166
- key: 'Unknown',
1167
- scale: 'unknown',
1168
- confidence: 0,
1169
- message: 'No clear key detected. Ensure audio is playing and has tonal content.'
1170
- };
1171
- }
1172
- const result = {
1173
- key: keyAnalysis.key,
1174
- scale: keyAnalysis.scale,
1175
- confidence: Math.round(keyAnalysis.confidence * 100) / 100,
1176
- message: `Detected ${keyAnalysis.key} ${keyAnalysis.scale} with ${Math.round(keyAnalysis.confidence * 100)}% confidence`
1177
- };
1178
- // Include alternatives if available and confidence is moderate
1179
- if (keyAnalysis.alternatives && keyAnalysis.alternatives.length > 0) {
1180
- result.alternatives = keyAnalysis.alternatives.map((alt) => ({
1181
- key: alt.key,
1182
- scale: alt.scale,
1183
- confidence: Math.round(alt.confidence * 100) / 100
1184
- }));
1185
- }
1186
- return result;
1187
- }
1188
- catch (error) {
1189
- return {
1190
- key: 'Unknown',
1191
- scale: 'unknown',
1192
- confidence: 0,
1193
- error: error.message || 'Key detection failed'
1194
- };
1195
- }
1196
- case 'validate_pattern_runtime':
1197
- if (!this.isInitialized) {
1198
- return 'Browser not initialized. Run init first.';
1199
- }
1200
- InputValidator.validateStringLength(args.pattern, 'pattern', 10000, false);
1201
- const validation = await this.controller.validatePatternRuntime(args.pattern, args.waitMs || 500);
1202
- if (validation.valid) {
1203
- return `✅ Pattern valid - no runtime errors detected`;
1204
- }
1205
- else {
1206
- return `❌ Pattern has runtime errors:\n${validation.errors.join('\n')}\n` +
1207
- (validation.warnings.length > 0 ? `\nWarnings:\n${validation.warnings.join('\n')}` : '');
1208
- }
1209
- // Local Pattern Tools (#83) - No browser required
1210
- case 'validate_pattern_local':
1211
- InputValidator.validateStringLength(args.pattern, 'pattern', 10000, false);
1212
- const localValidation = this.strudelEngine.validate(args.pattern);
1213
- return {
1214
- valid: localValidation.valid,
1215
- errors: localValidation.errors,
1216
- warnings: localValidation.warnings,
1217
- suggestions: localValidation.suggestions,
1218
- errorLocation: localValidation.errorLocation,
1219
- message: localValidation.valid
1220
- ? '✅ Pattern is valid'
1221
- : `❌ Pattern has ${localValidation.errors.length} error(s)`
1222
- };
1223
- case 'analyze_pattern_local':
1224
- InputValidator.validateStringLength(args.pattern, 'pattern', 10000, false);
1225
- const patternMetadata = this.strudelEngine.analyzePattern(args.pattern);
1226
- return {
1227
- ...patternMetadata,
1228
- message: `Pattern analysis: ${patternMetadata.eventsPerCycle} events/cycle, ` +
1229
- `complexity ${(patternMetadata.complexity * 100).toFixed(0)}%` +
1230
- (patternMetadata.bpm ? `, ${patternMetadata.bpm} BPM` : '')
1231
- };
1232
- case 'query_pattern_events':
1233
- InputValidator.validateStringLength(args.pattern, 'pattern', 10000, false);
1234
- const startCycle = args.start ?? 0;
1235
- const endCycle = args.end ?? 1;
1236
- if (startCycle >= endCycle) {
1237
- return { error: 'Start must be less than end' };
1238
- }
1239
- if (endCycle - startCycle > 16) {
1240
- return { error: 'Maximum range is 16 cycles to prevent excessive output' };
1241
- }
1242
- try {
1243
- const events = this.strudelEngine.queryEvents(args.pattern, startCycle, endCycle);
1244
- return {
1245
- count: events.length,
1246
- range: { start: startCycle, end: endCycle },
1247
- events: events.map((e) => ({
1248
- value: e.value,
1249
- start: e.start,
1250
- end: e.end,
1251
- duration: e.end - e.start
1252
- }))
1253
- };
1254
- }
1255
- catch (error) {
1256
- return {
1257
- error: error.message,
1258
- suggestion: 'Check pattern syntax with validate_pattern_local first'
1259
- };
1260
- }
1261
- case 'transpile_pattern':
1262
- InputValidator.validateStringLength(args.pattern, 'pattern', 10000, false);
1263
- const transpileResult = this.strudelEngine.transpile(args.pattern);
1264
- if (transpileResult.success) {
1265
- return {
1266
- success: true,
1267
- transpiledCode: transpileResult.transpiledCode,
1268
- message: 'Pattern transpiled successfully'
1269
- };
1270
- }
1271
- else {
1272
- return {
1273
- success: false,
1274
- error: transpileResult.error,
1275
- errorLocation: transpileResult.errorLocation,
1276
- message: 'Transpilation failed'
1277
- };
1278
- }
1279
- // Mood Transformation (#80)
1280
- case 'shift_mood':
1281
- const mood = args.target_mood?.toLowerCase()?.trim();
1282
- const moodProfile = MOOD_PROFILES[mood];
1283
- if (!moodProfile) {
1284
- return {
1285
- success: false,
1286
- error: `Unknown mood: ${args.target_mood}. Valid moods: ${Object.keys(MOOD_PROFILES).join(', ')}.`
1287
- };
1288
- }
1289
- const moodPattern = await this.getCurrentPatternSafe();
1290
- if (!moodPattern || moodPattern.trim().length === 0) {
1291
- return {
1292
- success: false,
1293
- error: 'No pattern to transform. Write a pattern first.'
1294
- };
1295
- }
1296
- const moodIntensity = args.intensity ?? 0.5;
1297
- if (moodIntensity < 0 || moodIntensity > 1) {
1298
- return {
1299
- success: false,
1300
- error: 'Intensity must be between 0 and 1.'
1301
- };
1302
- }
1303
- const appliedEffects = [];
1304
- let transformedPattern = moodPattern;
1305
- // Apply tempo modification
1306
- if (moodProfile.tempoMod !== 0) {
1307
- const tempoAdjust = 1 + (moodProfile.tempoMod * moodIntensity);
1308
- if (tempoAdjust > 1) {
1309
- transformedPattern += `.fast(${tempoAdjust.toFixed(2)})`;
1310
- appliedEffects.push(`tempo +${Math.round(moodProfile.tempoMod * 100 * moodIntensity)}%`);
1311
- }
1312
- else {
1313
- transformedPattern += `.slow(${(1 / tempoAdjust).toFixed(2)})`;
1314
- appliedEffects.push(`tempo ${Math.round(moodProfile.tempoMod * 100 * moodIntensity)}%`);
1315
- }
1316
- }
1317
- // Apply cutoff modification
1318
- if (moodProfile.cutoffMod !== 0) {
1319
- const baseCutoff = 1000;
1320
- const newCutoff = Math.max(200, baseCutoff + (moodProfile.cutoffMod * moodIntensity));
1321
- transformedPattern += `.lpf(${Math.round(newCutoff)})`;
1322
- appliedEffects.push(`cutoff ${moodProfile.cutoffMod > 0 ? '+' : ''}${Math.round(moodProfile.cutoffMod * moodIntensity)}Hz`);
1323
- }
1324
- // Apply reverb modification
1325
- if (moodProfile.roomMod !== 0) {
1326
- const roomAmount = Math.max(0, Math.min(1, moodProfile.roomMod * moodIntensity));
1327
- transformedPattern += `.room(${roomAmount.toFixed(2)})`;
1328
- appliedEffects.push(`reverb ${roomAmount.toFixed(2)}`);
1329
- }
1330
- // Apply delay if specified
1331
- if (moodProfile.delayMod && moodProfile.delayMod > 0) {
1332
- const delayAmount = moodProfile.delayMod * moodIntensity;
1333
- transformedPattern += `.delay(${delayAmount.toFixed(2)})`;
1334
- appliedEffects.push(`delay ${delayAmount.toFixed(2)}`);
1335
- }
1336
- // Apply gain modification
1337
- if (moodProfile.gainMod !== 0) {
1338
- const gainAdjust = 1 + (moodProfile.gainMod * moodIntensity);
1339
- transformedPattern += `.gain(${gainAdjust.toFixed(2)})`;
1340
- appliedEffects.push(`gain ${moodProfile.gainMod > 0 ? '+' : ''}${Math.round(moodProfile.gainMod * 100 * moodIntensity)}%`);
1341
- }
1342
- await this.writePatternSafe(transformedPattern);
1343
- const shouldPlayMood = args.auto_play !== false;
1344
- if (shouldPlayMood && this.isInitialized) {
1345
- await this.controller.play();
1346
- }
1347
- return {
1348
- success: true,
1349
- target_mood: mood,
1350
- intensity: moodIntensity,
1351
- applied_effects: appliedEffects
1352
- };
1353
- // Session Management
1354
- case 'save':
1355
- InputValidator.validateStringLength(args.name, 'name', 255, false);
1356
- const toSave = await this.getCurrentPatternSafe();
1357
- if (!toSave) {
1358
- return 'No pattern to save';
1359
- }
1360
- await this.store.save(args.name, toSave, args.tags || []);
1361
- return `Pattern saved as "${args.name}"`;
1362
- case 'load':
1363
- InputValidator.validateStringLength(args.name, 'name', 255, false);
1364
- const saved = await this.store.load(args.name);
1365
- if (saved) {
1366
- await this.writePatternSafe(saved.content);
1367
- return `Loaded pattern "${args.name}"`;
1368
- }
1369
- return `Pattern "${args.name}" not found`;
1370
- case 'list':
1371
- if (args?.tag) {
1372
- InputValidator.validateStringLength(args.tag, 'tag', 100, false);
1373
- }
1374
- const patterns = await this.store.list(args?.tag);
1375
- return patterns.map(p => `• ${p.name} [${p.tags.join(', ')}] - ${p.timestamp}`).join('\n') || 'No patterns found';
1376
- case 'undo':
1377
- if (!this.isInitialized) {
1378
- return 'Browser not initialized. Run init first.';
1379
- }
1380
- if (this.undoStack.length > 0) {
1381
- const currentUndo = await this.controller.getCurrentPattern();
1382
- this.redoStack.push(currentUndo);
1383
- // Enforce bounds to prevent memory leaks
1384
- if (this.redoStack.length > this.MAX_HISTORY) {
1385
- this.redoStack.shift();
1386
- }
1387
- const previous = this.undoStack.pop();
1388
- await this.controller.writePattern(previous);
1389
- return 'Undone';
1390
- }
1391
- return 'Nothing to undo';
1392
- case 'redo':
1393
- if (!this.isInitialized) {
1394
- return 'Browser not initialized. Run init first.';
1395
- }
1396
- if (this.redoStack.length > 0) {
1397
- const currentRedo = await this.controller.getCurrentPattern();
1398
- this.undoStack.push(currentRedo);
1399
- // Enforce bounds to prevent memory leaks
1400
- if (this.undoStack.length > this.MAX_HISTORY) {
1401
- this.undoStack.shift();
1402
- }
1403
- const next = this.redoStack.pop();
1404
- await this.controller.writePattern(next);
1405
- return 'Redone';
1406
- }
1407
- return 'Nothing to redo';
1408
- // Pattern History (#41)
1409
- case 'list_history':
1410
- if (this.historyStack.length === 0) {
1411
- return 'No pattern history yet. Make some edits to build history.';
1412
- }
1413
- const limit = args?.limit || 10;
1414
- const recentHistory = this.historyStack.slice(-limit).reverse();
1415
- return {
1416
- count: this.historyStack.length,
1417
- showing: recentHistory.length,
1418
- entries: recentHistory.map(entry => ({
1419
- id: entry.id,
1420
- preview: entry.pattern.substring(0, 60) + (entry.pattern.length > 60 ? '...' : ''),
1421
- chars: entry.pattern.length,
1422
- action: entry.action,
1423
- timestamp: this.formatTimeAgo(entry.timestamp)
1424
- }))
1425
- };
1426
- case 'restore_history':
1427
- if (!this.isInitialized) {
1428
- return 'Browser not initialized. Run init first.';
1429
- }
1430
- const entryToRestore = this.historyStack.find(e => e.id === args.id);
1431
- if (!entryToRestore) {
1432
- return `History entry #${args.id} not found. Use list_history to see available entries.`;
1433
- }
1434
- // Save current state before restoring
1435
- const currentBeforeRestore = await this.controller.getCurrentPattern();
1436
- this.undoStack.push(currentBeforeRestore);
1437
- if (this.undoStack.length > this.MAX_HISTORY) {
1438
- this.undoStack.shift();
1439
- }
1440
- await this.controller.writePattern(entryToRestore.pattern);
1441
- return `Restored pattern from history #${args.id} (${this.formatTimeAgo(entryToRestore.timestamp)})`;
1442
- case 'compare_patterns':
1443
- const entry1 = this.historyStack.find(e => e.id === args.id1);
1444
- if (!entry1) {
1445
- return `History entry #${args.id1} not found.`;
1446
- }
1447
- let pattern2;
1448
- let label2;
1449
- if (args.id2) {
1450
- const entry2 = this.historyStack.find(e => e.id === args.id2);
1451
- if (!entry2) {
1452
- return `History entry #${args.id2} not found.`;
1453
- }
1454
- pattern2 = entry2.pattern;
1455
- label2 = `#${args.id2}`;
1456
- }
1457
- else {
1458
- pattern2 = await this.getCurrentPatternSafe();
1459
- label2 = 'current';
1460
- }
1461
- const diff = this.generateDiff(entry1.pattern, pattern2);
1462
- return {
1463
- pattern1: { id: args.id1, chars: entry1.pattern.length },
1464
- pattern2: { id: label2, chars: pattern2.length },
1465
- diff: diff,
1466
- summary: this.summarizeDiff(entry1.pattern, pattern2)
1467
- };
1468
- // Performance Monitoring
1469
- case 'performance_report':
1470
- const report = this.perfMonitor.getReport();
1471
- const bottlenecks = this.perfMonitor.getBottlenecks(5);
1472
- return `${report}\n\nTop 5 Bottlenecks:\n${JSON.stringify(bottlenecks, null, 2)}`;
1473
- case 'memory_usage':
1474
- const memory = this.perfMonitor.getMemoryUsage();
1475
- return memory ? JSON.stringify(memory, null, 2) : 'Memory usage not available';
1476
- // UX Tools - Browser Control (#37)
1477
- case 'show_browser':
1478
- if (!this.isInitialized) {
1479
- return 'Browser not initialized. Run init first.';
1480
- }
1481
- return await this.controller.showBrowser();
1482
- case 'screenshot':
1483
- if (!this.isInitialized) {
1484
- return 'Browser not initialized. Run init first.';
1485
- }
1486
- return await this.controller.takeScreenshot(args?.filename);
1487
- // UX Tools - Status & Diagnostics (#39)
1488
- case 'status':
1489
- return this.controller.getStatus();
1490
- case 'diagnostics':
1491
- if (!this.isInitialized) {
1492
- return {
1493
- initialized: false,
1494
- message: 'Browser not initialized. Run init first for full diagnostics.'
1495
- };
1496
- }
1497
- return await this.controller.getDiagnostics();
1498
- case 'show_errors':
1499
- const errors = this.controller.getConsoleErrors();
1500
- const warnings = this.controller.getConsoleWarnings();
1501
- if (errors.length === 0 && warnings.length === 0) {
1502
- return 'No errors or warnings captured.';
1503
- }
1504
- let result = '';
1505
- if (errors.length > 0) {
1506
- result += `❌ Errors (${errors.length}):\n${errors.map(e => ` • ${e}`).join('\n')}\n`;
1507
- }
1508
- if (warnings.length > 0) {
1509
- result += `⚠️ Warnings (${warnings.length}):\n${warnings.map(w => ` • ${w}`).join('\n')}`;
1510
- }
1511
- return result.trim();
1512
- // UX Tools - High-level Compose (#42, #73)
1513
- case 'compose':
1514
- InputValidator.validateStringLength(args.style, 'style', 100, false);
1515
- if (args.key) {
1516
- InputValidator.validateRootNote(args.key);
1517
- }
1518
- if (args.tempo !== undefined) {
1519
- InputValidator.validateBPM(args.tempo);
1520
- }
1521
- // Auto-initialize if needed
1522
- if (!this.isInitialized) {
1523
- await this.controller.initialize();
1524
- this.isInitialized = true;
1525
- }
1526
- // Generate pattern
1527
- const composedPattern = this.generator.generateCompletePattern(args.style, args.key || 'C', args.tempo || this.getDefaultTempo(args.style));
1528
- // Write pattern
1529
- await this.controller.writePattern(composedPattern);
1530
- // Auto-play by default (unless explicitly set to false)
1531
- const shouldPlay = args.auto_play !== false;
1532
- if (shouldPlay) {
1533
- await this.controller.play();
1534
- }
1535
- // Build response
1536
- const composeResponse = {
1537
- success: true,
1538
- pattern: composedPattern.substring(0, 200) + (composedPattern.length > 200 ? '...' : ''),
1539
- metadata: {
1540
- style: args.style,
1541
- bpm: args.tempo || this.getDefaultTempo(args.style),
1542
- key: args.key || 'C'
1543
- },
1544
- status: shouldPlay ? 'playing' : 'ready',
1545
- message: `Created ${args.style} pattern in ${args.key || 'C'}${shouldPlay ? ' - now playing' : ''}`
1546
- };
1547
- // Get AI feedback if requested (#73)
1548
- if (args.get_feedback) {
1549
- if (this.geminiService.isAvailable()) {
1550
- try {
1551
- const feedback = await this.geminiService.getCreativeFeedback(composedPattern);
1552
- composeResponse.feedback = feedback;
1553
- composeResponse.message += ` (AI feedback: ${feedback.complexity} complexity, estimated ${feedback.estimatedStyle})`;
1554
- }
1555
- catch (error) {
1556
- this.logger.warn('Failed to get AI feedback for compose', error);
1557
- // Don't fail the whole operation, just note the feedback failure
1558
- composeResponse.message += ' (AI feedback unavailable)';
1559
- }
1560
- }
1561
- else {
1562
- composeResponse.message += ' (AI feedback requires GEMINI_API_KEY)';
1563
- }
1564
- }
1565
- return composeResponse;
1566
- // AI Feedback Tools (#67)
1567
- case 'get_pattern_feedback':
1568
- return await this.getPatternFeedback(args?.includeAudio || false, args?.style);
1569
- // Audio Capture Tools (#72)
1570
- case 'start_audio_capture':
1571
- return await this.startAudioCapture(args?.format, args?.maxDuration);
1572
- case 'stop_audio_capture':
1573
- return await this.stopAudioCapture();
1574
- case 'capture_audio_sample':
1575
- return await this.captureAudioSampleTool(args?.duration);
1576
- // MIDI Export Tools (#74)
1577
- case 'export_midi':
1578
- return await this.exportMidi(args?.filename, args?.duration, args?.bpm, args?.format);
1579
- // Multi-Session Management Tools (#75)
1580
- case 'create_session':
1581
- InputValidator.validateStringLength(args.session_id, 'session_id', 100, false);
1582
- try {
1583
- await this.sessionManager.createSession(args.session_id);
1584
- return {
1585
- success: true,
1586
- session_id: args.session_id,
1587
- message: `Session '${args.session_id}' created successfully`,
1588
- total_sessions: this.sessionManager.getSessionCount(),
1589
- max_sessions: this.sessionManager.getMaxSessions()
1590
- };
1591
- }
1592
- catch (error) {
1593
- return {
1594
- success: false,
1595
- error: error.message
1596
- };
1597
- }
1598
- case 'destroy_session':
1599
- InputValidator.validateStringLength(args.session_id, 'session_id', 100, false);
1600
- try {
1601
- await this.sessionManager.destroySession(args.session_id);
1602
- return {
1603
- success: true,
1604
- session_id: args.session_id,
1605
- message: `Session '${args.session_id}' destroyed`,
1606
- remaining_sessions: this.sessionManager.getSessionCount()
1607
- };
1608
- }
1609
- catch (error) {
1610
- return {
1611
- success: false,
1612
- error: error.message
1613
- };
1614
- }
1615
- case 'list_sessions':
1616
- const sessionsInfo = this.sessionManager.getSessionsInfo();
1617
- return {
1618
- count: sessionsInfo.length,
1619
- max_sessions: this.sessionManager.getMaxSessions(),
1620
- default_session: this.sessionManager.getDefaultSessionId(),
1621
- sessions: sessionsInfo.map(s => ({
1622
- id: s.id,
1623
- created: s.created.toISOString(),
1624
- last_activity: s.lastActivity.toISOString(),
1625
- is_playing: s.isPlaying,
1626
- is_default: s.id === this.sessionManager.getDefaultSessionId()
1627
- }))
1628
- };
1629
- case 'switch_session':
1630
- InputValidator.validateStringLength(args.session_id, 'session_id', 100, false);
1631
- try {
1632
- this.sessionManager.setDefaultSession(args.session_id);
1633
- return {
1634
- success: true,
1635
- default_session: args.session_id,
1636
- message: `Default session switched to '${args.session_id}'`
1637
- };
1638
- }
1639
- catch (error) {
1640
- return {
1641
- success: false,
1642
- error: error.message
1643
- };
1644
- }
1645
- // AI Collaborative Jamming (#82)
1646
- case 'jam_with':
1647
- return await this.jamWith(args.layer, args.style_hint, args.auto_play);
1648
- // Pattern Refinement Tools (#78, #81)
1649
- case 'refine':
1650
- return await this.refinePattern(args.direction);
1651
- case 'set_energy':
1652
- return await this.setEnergyLevel(args.level);
1653
- default:
1654
- throw new Error(`Unknown tool: ${name}`);
1655
- }
1656
- }
1657
- /**
1658
- * Incrementally refines the current pattern with simple directional commands.
1659
- * Supports: faster/slower, louder/quieter, brighter/darker, more reverb/drier.
1660
- * @param direction - Refinement direction
1661
- * @returns Result object with success status and applied refinement
1662
- */
1663
- async refinePattern(direction) {
1664
- const currentPattern = await this.getCurrentPatternSafe();
1665
- if (!currentPattern || currentPattern.trim().length === 0) {
1666
- return {
1667
- success: false,
1668
- error: 'No pattern to refine. Write a pattern first.'
1669
- };
1670
- }
1671
- let modification = '';
1672
- const dir = direction.toLowerCase().trim();
1673
- switch (dir) {
1674
- case 'faster':
1675
- modification = '.fast(1.1)';
1676
- break;
1677
- case 'slower':
1678
- modification = '.slow(1.1)';
1679
- break;
1680
- case 'louder':
1681
- modification = '.gain(1.1)';
1682
- break;
1683
- case 'quieter':
1684
- modification = '.gain(0.9)';
1685
- break;
1686
- case 'brighter':
1687
- modification = '.lpf(2000)';
1688
- break;
1689
- case 'darker':
1690
- modification = '.lpf(800)';
1691
- break;
1692
- case 'more reverb':
1693
- modification = '.room(0.5)';
1694
- break;
1695
- case 'drier':
1696
- modification = '.room(0.1)';
1697
- break;
1698
- default:
1699
- return {
1700
- success: false,
1701
- error: `Unknown refinement direction: ${direction}. Supported: faster, slower, louder, quieter, brighter, darker, "more reverb", drier.`
1702
- };
1703
- }
1704
- const newPattern = currentPattern + modification;
1705
- await this.writePatternSafe(newPattern);
1706
- if (this.isInitialized) {
1707
- await this.controller.play();
1708
- }
1709
- return {
1710
- success: true,
1711
- direction: dir,
1712
- applied: modification
1713
- };
1714
- }
1715
- /**
1716
- * Adjusts the overall energy level of the current pattern.
1717
- * @param level - Energy level from 0 (minimal) to 10 (maximum)
1718
- * @returns Result object with success status
1719
- */
1720
- async setEnergyLevel(level) {
1721
- if (level < 0 || level > 10 || !Number.isInteger(level)) {
1722
- return {
1723
- success: false,
1724
- error: 'Energy level must be an integer from 0 to 10.'
1725
- };
1726
- }
1727
- const currentPattern = await this.getCurrentPatternSafe();
1728
- if (!currentPattern || currentPattern.trim().length === 0) {
1729
- return {
1730
- success: false,
1731
- error: 'No pattern to adjust. Write a pattern first.'
1732
- };
1733
- }
1734
- const config = ENERGY_LEVELS[level];
1735
- let newPattern = currentPattern;
1736
- // Apply density adjustment if specified
1737
- if (config.densityAdjust) {
1738
- newPattern += config.densityAdjust;
1739
- }
1740
- // Apply room/reverb
1741
- newPattern += `.room(${config.roomAmount})`;
1742
- await this.writePatternSafe(newPattern);
1743
- if (this.isInitialized) {
1744
- await this.controller.play();
1745
- }
1746
- return {
1747
- success: true,
1748
- level,
1749
- description: config.description
1750
- };
1751
- }
1752
- transposePattern(pattern, semitones) {
1753
- // Simple transpose implementation - would need more sophisticated parsing
1754
- return pattern.replace(/([a-g][#b]?)(\d)/gi, (match, note, octave) => {
1755
- const noteMap = {
1756
- 'c': 0, 'c#': 1, 'd': 2, 'd#': 3, 'e': 4, 'f': 5,
1757
- 'f#': 6, 'g': 7, 'g#': 8, 'a': 9, 'a#': 10, 'b': 11
1758
- };
1759
- const currentNote = note.toLowerCase();
1760
- const noteValue = noteMap[currentNote] || 0;
1761
- const newNoteValue = (noteValue + semitones + 12) % 12;
1762
- const noteNames = ['c', 'c#', 'd', 'd#', 'e', 'f', 'f#', 'g', 'g#', 'a', 'a#', 'b'];
1763
- const newOctave = parseInt(octave) + Math.floor((noteValue + semitones) / 12);
1764
- return noteNames[newNoteValue] + newOctave;
1765
- });
1766
- }
1767
- /**
1768
- * Gets the default tempo for a given music style
1769
- * @param style - Music style/genre
1770
- * @returns Default BPM for the style
1771
- */
1772
- getDefaultTempo(style) {
1773
- const tempoMap = {
1774
- 'techno': 130,
1775
- 'house': 125,
1776
- 'dnb': 174,
1777
- 'drum and bass': 174,
1778
- 'ambient': 80,
1779
- 'trap': 140,
1780
- 'jungle': 160,
1781
- 'jazz': 110,
1782
- 'experimental': 120,
1783
- 'dubstep': 140,
1784
- 'trance': 138,
1785
- 'breakbeat': 130,
1786
- 'garage': 130,
1787
- 'electro': 128,
1788
- 'downtempo': 90,
1789
- 'idm': 115
1790
- };
1791
- return tempoMap[style.toLowerCase()] || 120;
1792
- }
1793
- /**
1794
- * Formats a timestamp as human-readable "time ago" string
1795
- * @param date - Date to format
1796
- * @returns Human-readable string like "2m ago" or "1h ago"
1797
- */
1798
- formatTimeAgo(date) {
1799
- const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000);
1800
- if (seconds < 60)
1801
- return `${seconds}s ago`;
1802
- if (seconds < 3600)
1803
- return `${Math.floor(seconds / 60)}m ago`;
1804
- if (seconds < 86400)
1805
- return `${Math.floor(seconds / 3600)}h ago`;
1806
- return `${Math.floor(seconds / 86400)}d ago`;
1807
- }
1808
- /**
1809
- * Generates a simple line-by-line diff between two patterns
1810
- * @param pattern1 - First pattern
1811
- * @param pattern2 - Second pattern
1812
- * @returns Diff output showing additions and removals
1813
- */
1814
- generateDiff(pattern1, pattern2) {
1815
- const lines1 = pattern1.split('\n');
1816
- const lines2 = pattern2.split('\n');
1817
- const diff = [];
1818
- const maxLines = Math.max(lines1.length, lines2.length);
1819
- for (let i = 0; i < maxLines; i++) {
1820
- const line1 = lines1[i] || '';
1821
- const line2 = lines2[i] || '';
1822
- if (line1 === line2) {
1823
- diff.push(` ${line1}`);
1824
- }
1825
- else {
1826
- if (line1)
1827
- diff.push(`- ${line1}`);
1828
- if (line2)
1829
- diff.push(`+ ${line2}`);
1830
- }
1831
- }
1832
- return diff;
1833
- }
1834
- /**
1835
- * Summarizes differences between two patterns
1836
- * @param pattern1 - First pattern
1837
- * @param pattern2 - Second pattern
1838
- * @returns Summary of differences
1839
- */
1840
- summarizeDiff(pattern1, pattern2) {
1841
- const lines1 = pattern1.split('\n');
1842
- const lines2 = pattern2.split('\n');
1843
- let linesAdded = 0;
1844
- let linesRemoved = 0;
1845
- let linesChanged = 0;
1846
- const maxLines = Math.max(lines1.length, lines2.length);
1847
- for (let i = 0; i < maxLines; i++) {
1848
- const line1 = lines1[i];
1849
- const line2 = lines2[i];
1850
- if (line1 === undefined && line2 !== undefined) {
1851
- linesAdded++;
1852
- }
1853
- else if (line1 !== undefined && line2 === undefined) {
1854
- linesRemoved++;
1855
- }
1856
- else if (line1 !== line2) {
1857
- linesChanged++;
1858
- }
1859
- }
1860
- return {
1861
- linesAdded,
1862
- linesRemoved,
1863
- linesChanged,
1864
- charsDiff: pattern2.length - pattern1.length
1865
- };
1866
- }
1867
- /**
1868
- * Gets AI-powered creative feedback on the current pattern
1869
- * Uses Google Gemini for pattern analysis and optionally audio analysis
1870
- * @param includeAudio - Whether to include audio analysis (plays pattern briefly)
1871
- * @param style - Optional style hint for context
1872
- * @returns Feedback object with pattern_analysis and optionally audio_analysis
1873
- */
1874
- async getPatternFeedback(includeAudio, style) {
1875
- // Check if Gemini is available
1876
- if (!this.geminiService.isAvailable()) {
1877
- return {
1878
- gemini_available: false,
1879
- error: 'Gemini API not configured. Set GEMINI_API_KEY environment variable to enable AI feedback.'
1880
- };
1881
- }
1882
- // Get current pattern
1883
- const pattern = await this.getCurrentPatternSafe();
1884
- if (!pattern || pattern.trim().length === 0) {
1885
- return {
1886
- gemini_available: true,
1887
- error: 'No pattern to analyze. Write a pattern first.'
1888
- };
1889
- }
1890
- const result = {
1891
- gemini_available: true
1892
- };
1893
- // Get pattern analysis
1894
- try {
1895
- result.pattern_analysis = await this.geminiService.getCreativeFeedback(pattern);
1896
- }
1897
- catch (error) {
1898
- this.logger.error('Pattern feedback failed', error);
1899
- // Handle rate limit errors gracefully - pass through the detailed error message
1900
- if (error.message?.includes('rate limit') || error.message?.includes('Rate limit')) {
1901
- return {
1902
- gemini_available: true,
1903
- error: error.message // Pass through the detailed rate limit message with wait time
1904
- };
1905
- }
1906
- result.error = `Pattern analysis failed: ${error.message}`;
1907
- }
1908
- // Get audio analysis if requested
1909
- if (includeAudio && this.isInitialized) {
1910
- try {
1911
- // Capture audio by playing the pattern briefly
1912
- const audioBlob = await this.captureAudioSample();
1913
- if (audioBlob) {
1914
- result.audio_analysis = await this.geminiService.analyzeAudio(audioBlob, {
1915
- style: style,
1916
- duration: 5
1917
- });
1918
- }
1919
- else {
1920
- this.logger.warn('Audio capture returned no data');
1921
- }
1922
- }
1923
- catch (error) {
1924
- this.logger.error('Audio analysis failed', error);
1925
- // Don't fail the whole request if audio analysis fails
1926
- if (!result.error) {
1927
- result.error = `Audio analysis failed: ${error.message}`;
1928
- }
1929
- }
1930
- }
1931
- else if (includeAudio && !this.isInitialized) {
1932
- if (!result.error) {
1933
- result.error = 'Audio analysis requires browser initialization. Run init first or set includeAudio to false.';
1934
- }
1935
- }
1936
- return result;
1937
- }
1938
- /**
1939
- * Captures a brief audio sample from the playing pattern
1940
- * Plays the pattern for ~5 seconds and captures audio using MediaRecorder
1941
- * @returns Audio blob or null if capture failed
1942
- */
1943
- async captureAudioSample() {
1944
- if (!this._page) {
1945
- this.logger.warn('Cannot capture audio: page not available');
1946
- return null;
1947
- }
1948
- const page = this.controller.page;
1949
- if (!page) {
1950
- this.logger.warn('Cannot capture audio: controller page not available');
1951
- return null;
1952
- }
1953
- try {
1954
- // Inject audio capture code and start recording
1955
- const audioData = await page.evaluate(async () => {
1956
- return new Promise((resolve) => {
1957
- const analyzer = window.strudelAudioAnalyzer;
1958
- if (!analyzer || !analyzer.analyser) {
1959
- resolve(null);
1960
- return;
1961
- }
1962
- try {
1963
- const audioCtx = analyzer.analyser.context;
1964
- const destination = audioCtx.createMediaStreamDestination();
1965
- // Connect analyzer to destination for recording
1966
- analyzer.analyser.connect(destination);
1967
- const mediaRecorder = new MediaRecorder(destination.stream, {
1968
- mimeType: 'audio/webm;codecs=opus'
1969
- });
1970
- const chunks = [];
1971
- mediaRecorder.ondataavailable = (e) => {
1972
- if (e.data.size > 0) {
1973
- chunks.push(e.data);
1974
- }
1975
- };
1976
- mediaRecorder.onstop = async () => {
1977
- // Disconnect to avoid audio routing issues
1978
- try {
1979
- analyzer.analyser.disconnect(destination);
1980
- }
1981
- catch (e) {
1982
- // Ignore disconnect errors
1983
- }
1984
- if (chunks.length === 0) {
1985
- resolve(null);
1986
- return;
1987
- }
1988
- const blob = new Blob(chunks, { type: 'audio/webm' });
1989
- // Convert to base64 for transport
1990
- const reader = new FileReader();
1991
- reader.onloadend = () => {
1992
- const base64 = reader.result;
1993
- resolve(base64.split(',')[1] || null);
1994
- };
1995
- reader.onerror = () => resolve(null);
1996
- reader.readAsDataURL(blob);
1997
- };
1998
- // Start recording
1999
- mediaRecorder.start();
2000
- // Record for 5 seconds
2001
- setTimeout(() => {
2002
- if (mediaRecorder.state === 'recording') {
2003
- mediaRecorder.stop();
2004
- }
2005
- }, 5000);
2006
- }
2007
- catch (e) {
2008
- resolve(null);
2009
- }
2010
- });
2011
- });
2012
- if (!audioData) {
2013
- return null;
2014
- }
2015
- // Convert base64 back to Blob
2016
- const binaryString = atob(audioData);
2017
- const bytes = new Uint8Array(binaryString.length);
2018
- for (let i = 0; i < binaryString.length; i++) {
2019
- bytes[i] = binaryString.charCodeAt(i);
2020
- }
2021
- return new Blob([bytes], { type: 'audio/webm' });
2022
- }
2023
- catch (error) {
2024
- this.logger.error('Audio capture failed', error);
2025
- return null;
2026
- }
2027
- }
2028
- /**
2029
- * Getter for page access in audio capture
2030
- */
2031
- get _page() {
2032
- return this.controller.page;
2033
- }
2034
- /**
2035
- * Lazily initializes and returns the AudioCaptureService
2036
- * Injects recorder into page on first use
2037
- */
2038
- async getAudioCaptureService() {
2039
- if (!this.isInitialized || !this._page) {
2040
- throw new Error('Browser not initialized. Run init first.');
2041
- }
2042
- if (!this.audioCaptureService) {
2043
- this.audioCaptureService = new AudioCaptureService();
2044
- await this.audioCaptureService.injectRecorder(this._page);
2045
- }
2046
- return this.audioCaptureService;
2047
- }
2048
- /**
2049
- * Starts audio capture from Strudel output
2050
- * @param format - Audio format ('webm' or 'opus')
2051
- * @param maxDuration - Maximum capture duration in ms (optional)
2052
- * @returns Status message
2053
- */
2054
- async startAudioCapture(format, maxDuration) {
2055
- try {
2056
- const captureService = await this.getAudioCaptureService();
2057
- if (captureService.isCapturing()) {
2058
- return {
2059
- success: false,
2060
- message: 'Audio capture already in progress. Stop it first.'
2061
- };
2062
- }
2063
- await captureService.startCapture(this._page, { format });
2064
- return {
2065
- success: true,
2066
- message: 'Audio capture started. Use stop_audio_capture to get the recorded audio.',
2067
- format: format || 'webm'
2068
- };
2069
- }
2070
- catch (error) {
2071
- return {
2072
- success: false,
2073
- message: `Failed to start audio capture: ${error.message}`
2074
- };
2075
- }
2076
- }
2077
- /**
2078
- * Stops audio capture and returns base64-encoded audio data
2079
- * @returns Captured audio as base64 with metadata
2080
- */
2081
- async stopAudioCapture() {
2082
- try {
2083
- const captureService = await this.getAudioCaptureService();
2084
- if (!captureService.isCapturing()) {
2085
- return {
2086
- success: false,
2087
- message: 'No audio capture in progress. Start capture first.'
2088
- };
2089
- }
2090
- const result = await captureService.stopCapture(this._page);
2091
- // Convert Blob to base64
2092
- const arrayBuffer = await result.blob.arrayBuffer();
2093
- const bytes = new Uint8Array(arrayBuffer);
2094
- let binary = '';
2095
- for (let i = 0; i < bytes.length; i++) {
2096
- binary += String.fromCharCode(bytes[i]);
2097
- }
2098
- const base64 = btoa(binary);
2099
- return {
2100
- success: true,
2101
- message: `Captured ${result.duration}ms of audio`,
2102
- audio: base64,
2103
- duration: result.duration,
2104
- format: result.format
2105
- };
2106
- }
2107
- catch (error) {
2108
- return {
2109
- success: false,
2110
- message: `Failed to stop audio capture: ${error.message}`
2111
- };
2112
- }
2113
- }
2114
- /**
2115
- * Captures a fixed-duration audio sample (MCP tool handler)
2116
- * @param duration - Duration in milliseconds (default: 5000)
2117
- * @returns Captured audio as base64 with metadata
2118
- */
2119
- async captureAudioSampleTool(duration) {
2120
- const durationMs = duration || 5000;
2121
- // Validate duration
2122
- if (durationMs < 100 || durationMs > 60000) {
2123
- return {
2124
- success: false,
2125
- message: 'Duration must be between 100ms and 60000ms (1 minute)'
2126
- };
2127
- }
2128
- try {
2129
- const captureService = await this.getAudioCaptureService();
2130
- if (captureService.isCapturing()) {
2131
- return {
2132
- success: false,
2133
- message: 'Audio capture already in progress. Stop it first.'
2134
- };
2135
- }
2136
- const result = await captureService.captureForDuration(this._page, durationMs);
2137
- // Convert Blob to base64
2138
- const arrayBuffer = await result.blob.arrayBuffer();
2139
- const bytes = new Uint8Array(arrayBuffer);
2140
- let binary = '';
2141
- for (let i = 0; i < bytes.length; i++) {
2142
- binary += String.fromCharCode(bytes[i]);
2143
- }
2144
- const base64 = btoa(binary);
2145
- return {
2146
- success: true,
2147
- message: `Captured ${result.duration}ms audio sample`,
2148
- audio: base64,
2149
- duration: result.duration,
2150
- format: result.format
2151
- };
2152
- }
2153
- catch (error) {
2154
- return {
2155
- success: false,
2156
- message: `Failed to capture audio sample: ${error.message}`
2157
- };
2158
- }
2159
- }
2160
- /**
2161
- * Exports current pattern to MIDI format
2162
- * @param filename - Output filename (optional)
2163
- * @param bars - Duration in bars (default: 4)
2164
- * @param bpm - Tempo in BPM (default: 120)
2165
- * @param format - Output format: 'file' or 'base64' (default: 'base64')
2166
- * @returns Export result with MIDI data or file path
2167
- */
2168
- async exportMidi(filename, bars, bpm, format) {
2169
- // Validate inputs
2170
- if (bpm !== undefined) {
2171
- InputValidator.validateBPM(bpm);
2172
- }
2173
- if (bars !== undefined && (bars < 1 || bars > 128)) {
2174
- return {
2175
- success: false,
2176
- message: 'Bars must be between 1 and 128'
2177
- };
2178
- }
2179
- // Get current pattern
2180
- const pattern = await this.getCurrentPatternSafe();
2181
- if (!pattern || pattern.trim().length === 0) {
2182
- return {
2183
- success: false,
2184
- message: 'No pattern to export. Write a pattern first.'
2185
- };
2186
- }
2187
- const exportOptions = {
2188
- bpm: bpm || 120,
2189
- bars: bars || 4
2190
- };
2191
- // Export based on format
2192
- const outputFormat = format || 'base64';
2193
- if (outputFormat === 'file') {
2194
- const result = this.midiExportService.exportToFile(pattern, filename, exportOptions);
2195
- return {
2196
- success: result.success,
2197
- message: result.success
2198
- ? `Exported ${result.noteCount} notes to ${result.output}`
2199
- : result.error || 'Export failed',
2200
- output: result.output,
2201
- noteCount: result.noteCount,
2202
- bars: result.bars,
2203
- bpm: result.bpm,
2204
- error: result.error
2205
- };
2206
- }
2207
- else {
2208
- const result = this.midiExportService.exportToBase64(pattern, exportOptions);
2209
- return {
2210
- success: result.success,
2211
- message: result.success
2212
- ? `Exported ${result.noteCount} notes as base64 MIDI data`
2213
- : result.error || 'Export failed',
2214
- output: result.output,
2215
- noteCount: result.noteCount,
2216
- bars: result.bars,
2217
- bpm: result.bpm,
2218
- error: result.error
2219
- };
2220
- }
2221
- }
2222
- /**
2223
- * AI Collaborative Jamming - generates a complementary layer to jam with the current pattern (#82)
2224
- * @param layer - Type of layer to add (drums, bass, melody, pad, texture)
2225
- * @param styleHint - Optional style guidance
2226
- * @param autoPlay - Whether to auto-play after adding layer (default: true)
2227
- * @returns Result with merged pattern and analysis info
2228
- */
2229
- async jamWith(layer, styleHint, autoPlay = true) {
2230
- const validLayers = ['drums', 'bass', 'melody', 'pad', 'texture'];
2231
- if (!validLayers.includes(layer)) {
2232
- return {
2233
- success: false,
2234
- message: `Invalid layer type: ${layer}. Must be one of: ${validLayers.join(', ')}`,
2235
- layer,
2236
- detected: { tempo: 120, key: 'C', existingLayers: [] },
2237
- newLayer: ''
2238
- };
2239
- }
2240
- const currentPattern = await this.getCurrentPatternSafe();
2241
- if (!currentPattern || currentPattern.trim().length === 0) {
2242
- return {
2243
- success: false,
2244
- message: 'No pattern to jam with. Write a pattern first.',
2245
- layer,
2246
- detected: { tempo: 120, key: 'C', existingLayers: [] },
2247
- newLayer: ''
2248
- };
2249
- }
2250
- const tempo = this.detectTempoFromPattern(currentPattern);
2251
- const key = this.detectKeyFromPattern(currentPattern);
2252
- const existingLayers = this.detectExistingLayers(currentPattern);
2253
- const detectedStyle = this.detectStyleFromPattern(currentPattern, styleHint);
2254
- if (existingLayers.includes(layer) && layer !== 'texture') {
2255
- this.logger.warn(`Pattern already contains ${layer} layer, adding anyway`);
2256
- }
2257
- let newLayer;
2258
- try {
2259
- newLayer = this.generateComplementaryLayer(layer, key, tempo, detectedStyle, existingLayers);
2260
- }
2261
- catch (error) {
2262
- return {
2263
- success: false,
2264
- message: `Failed to generate ${layer} layer: ${error.message}`,
2265
- layer,
2266
- detected: { tempo, key, existingLayers },
2267
- newLayer: '',
2268
- error: error.message
2269
- };
2270
- }
2271
- const mergedPattern = this.mergeLayerIntoPattern(currentPattern, newLayer, layer);
2272
- try {
2273
- await this.writePatternSafe(mergedPattern);
2274
- if (autoPlay && this.isInitialized) {
2275
- await this.controller.play();
2276
- }
2277
- return {
2278
- success: true,
2279
- message: `Added ${layer} layer${styleHint ? ` (${styleHint} style)` : ''} to jam with your pattern`,
2280
- layer,
2281
- detected: { tempo, key, existingLayers },
2282
- newLayer,
2283
- pattern: mergedPattern.substring(0, 300) + (mergedPattern.length > 300 ? '...' : '')
2284
- };
2285
- }
2286
- catch (error) {
2287
- return {
2288
- success: false,
2289
- message: `Failed to write merged pattern: ${error.message}`,
2290
- layer,
2291
- detected: { tempo, key, existingLayers },
2292
- newLayer,
2293
- error: error.message
2294
- };
2295
- }
2296
- }
2297
- detectTempoFromPattern(pattern) {
2298
- const cpmMatch = pattern.match(/setcpm\s*\(\s*(\d+(?:\.\d+)?)\s*\)/i);
2299
- if (cpmMatch)
2300
- return Math.round(parseFloat(cpmMatch[1]));
2301
- const bpmMatch = pattern.match(/setbpm\s*\(\s*(\d+(?:\.\d+)?)\s*\)/i);
2302
- if (bpmMatch)
2303
- return Math.round(parseFloat(bpmMatch[1]));
2304
- const cpsMatch = pattern.match(/setcps\s*\(\s*(\d+(?:\.\d+)?)\s*(?:\/\s*60)?\s*\)/i);
2305
- if (cpsMatch) {
2306
- const cps = parseFloat(cpsMatch[1]);
2307
- if (pattern.includes(`setcps(${cpsMatch[1]}/60`))
2308
- return Math.round(cps);
2309
- return Math.round(cps * 60);
2310
- }
2311
- if (pattern.toLowerCase().includes('dnb'))
2312
- return 174;
2313
- if (pattern.toLowerCase().includes('techno'))
2314
- return 130;
2315
- if (pattern.toLowerCase().includes('house'))
2316
- return 125;
2317
- return 120;
2318
- }
2319
- detectKeyFromPattern(pattern) {
2320
- const noteMatches = pattern.match(/note\s*\(\s*["']([^"']+)["']\s*\)/gi) || [];
2321
- const nMatches = pattern.match(/\.n\s*\(\s*["']([^"']+)["']\s*\)/gi) || [];
2322
- const allNotes = [];
2323
- for (const match of noteMatches) {
2324
- const notesInMatch = match.match(/[a-g][#b]?\d?/gi) || [];
2325
- allNotes.push(...notesInMatch.map(n => n.toLowerCase().replace(/\d/g, '')));
2326
- }
2327
- for (const match of nMatches) {
2328
- const notesInMatch = match.match(/[a-g][#b]?\d?/gi) || [];
2329
- allNotes.push(...notesInMatch.map(n => n.toLowerCase().replace(/\d/g, '')));
2330
- }
2331
- const chordMatches = pattern.match(/chord\s*\(\s*["']<([^>]+)>/gi) || [];
2332
- for (const match of chordMatches) {
2333
- const rootMatch = match.match(/[a-g][#b]?/i);
2334
- if (rootMatch)
2335
- allNotes.push(rootMatch[0].toLowerCase());
2336
- }
2337
- if (allNotes.length === 0)
2338
- return 'C';
2339
- const noteCounts = {};
2340
- for (const note of allNotes) {
2341
- const normalizedNote = note.charAt(0).toUpperCase() + note.slice(1);
2342
- noteCounts[normalizedNote] = (noteCounts[normalizedNote] || 0) + 1;
2343
- }
2344
- let mostCommonNote = 'C';
2345
- let maxCount = 0;
2346
- for (const [note, count] of Object.entries(noteCounts)) {
2347
- if (count > maxCount) {
2348
- maxCount = count;
2349
- mostCommonNote = note;
2350
- }
2351
- }
2352
- return mostCommonNote;
2353
- }
2354
- detectExistingLayers(pattern) {
2355
- const layers = [];
2356
- const lowerPattern = pattern.toLowerCase();
2357
- if (lowerPattern.includes('bd') || lowerPattern.includes('cp') ||
2358
- lowerPattern.includes('hh') || lowerPattern.includes('sd') ||
2359
- lowerPattern.includes('sn') || lowerPattern.includes('oh') ||
2360
- lowerPattern.includes('breaks') || lowerPattern.includes('drum')) {
2361
- layers.push('drums');
2362
- }
2363
- if (pattern.match(/note\s*\([^)]*[12]\s*["']/i) || lowerPattern.includes('bass')) {
2364
- layers.push('bass');
2365
- }
2366
- if (pattern.match(/note\s*\([^)]*[34567]\s*["']/i) ||
2367
- lowerPattern.includes('melody') || lowerPattern.includes('lead')) {
2368
- layers.push('melody');
2369
- }
2370
- if (lowerPattern.includes('chord(') || lowerPattern.includes('pad') ||
2371
- lowerPattern.includes('strings') || lowerPattern.includes('.voicing')) {
2372
- layers.push('pad');
2373
- }
2374
- if (lowerPattern.includes('noise') || lowerPattern.includes('fx') ||
2375
- lowerPattern.includes('perlin') || lowerPattern.includes('rand')) {
2376
- layers.push('texture');
2377
- }
2378
- return layers;
2379
- }
2380
- detectStyleFromPattern(pattern, styleHint) {
2381
- if (styleHint)
2382
- return styleHint.toLowerCase();
2383
- const lowerPattern = pattern.toLowerCase();
2384
- const tempo = this.detectTempoFromPattern(pattern);
2385
- if (tempo >= 160 && lowerPattern.includes('breaks'))
2386
- return 'jungle';
2387
- if (tempo >= 165 && tempo <= 180)
2388
- return 'dnb';
2389
- if (tempo >= 125 && tempo <= 135 && lowerPattern.includes('bd*4')) {
2390
- return lowerPattern.includes('cp') ? 'techno' : 'house';
2391
- }
2392
- if (tempo <= 100 && lowerPattern.includes('room'))
2393
- return 'ambient';
2394
- if (lowerPattern.includes('trap'))
2395
- return 'trap';
2396
- return 'techno';
2397
- }
2398
- generateComplementaryLayer(layer, key, tempo, style, existingLayers) {
2399
- switch (layer) {
2400
- case 'drums':
2401
- if (existingLayers.includes('drums')) {
2402
- const percOptions = {
2403
- 'techno': 's("~ hh ~ hh, ~ ~ oh ~").gain(0.4).hpf(5000)',
2404
- 'house': 's("[~ hh]*4, ~ ~ oh ~").gain(0.35).room(0.2)',
2405
- 'dnb': 's("hh*16").gain(perlin.range(0.2, 0.4)).hpf(6000)',
2406
- 'ambient': 's("~ ~ ~ hh:8").room(0.8).gain(0.2).slow(2)',
2407
- 'trap': 's("hh*16").gain(perlin.range(0.15, 0.35)).hpf(5000)',
2408
- 'jungle': 's("hh*32").gain(perlin.range(0.2, 0.4)).hpf(4000)',
2409
- 'jazz': 's("~ ride ~ ride, ~ ~ ~ hh").gain(0.3).room(0.3)'
2410
- };
2411
- return percOptions[style] || percOptions['techno'];
2412
- }
2413
- return this.generator.generateDrumPattern(style, 0.6);
2414
- case 'bass':
2415
- return this.generator.generateBassline(key, style);
2416
- case 'melody': {
2417
- let scaleName = 'minor';
2418
- let octaveRange = [4, 5];
2419
- if (style === 'jazz') {
2420
- scaleName = 'dorian';
2421
- octaveRange = [3, 5];
2422
- }
2423
- if (style === 'ambient') {
2424
- scaleName = 'major';
2425
- octaveRange = [4, 6];
2426
- }
2427
- if (existingLayers.includes('bass'))
2428
- octaveRange = [4, 6];
2429
- const scale = this.theory.generateScale(key, scaleName);
2430
- const effects = {
2431
- 'techno': '.delay(0.25).room(0.2)', 'house': '.room(0.3).gain(0.6)',
2432
- 'dnb': '.delay(0.125).room(0.2).gain(0.5)', 'ambient': '.room(0.7).delay(0.5).gain(0.4)',
2433
- 'trap': '.gain(0.5).room(0.15)', 'jungle': '.delay(0.125).room(0.25).gain(0.55)',
2434
- 'jazz': '.room(0.4).gain(0.5)'
2435
- };
2436
- return this.generator.generateMelody(scale, 8, octaveRange) + (effects[style] || '.room(0.3).gain(0.5)');
2437
- }
2438
- case 'pad': {
2439
- const safeKey = key.toLowerCase();
2440
- const fourth = this.theory.getNote(key, 5).toLowerCase();
2441
- const fifth = this.theory.getNote(key, 7).toLowerCase();
2442
- const padPatterns = {
2443
- 'techno': `chord("<${safeKey}m7 ${fourth}m7>/4").dict('ireal').voicing().s("sawtooth").attack(0.5).release(2).lpf(2000).gain(0.2).room(0.4)`,
2444
- 'house': `chord("<${safeKey}m9 ${fourth}7 ${fifth}m7>/2").dict('ireal').voicing().s("gm_epiano1").gain(0.3).room(0.4)`,
2445
- 'dnb': `chord("<${safeKey}m9 ${fourth}m9>/8").dict('ireal').voicing().s("gm_strings").attack(1).release(2).gain(0.2).room(0.5).lpf(3500)`,
2446
- 'ambient': `chord("<${safeKey}maj7 ${fourth}maj7 ${fifth}m7>/8").dict('ireal').voicing().s("sawtooth").attack(3).release(5).lpf(sine.range(400, 1200).slow(16)).gain(0.15).room(0.9)`,
2447
- 'trap': `chord("<${safeKey}m7>/4").dict('ireal').voicing().s("sawtooth").attack(0.1).release(0.5).lpf(1500).gain(0.25).room(0.3)`,
2448
- 'jungle': `chord("<${safeKey}m9 ${fourth}m9>/8").dict('ireal').voicing().s("gm_epiano1").gain(0.25).room(0.4).delay(0.25)`,
2449
- 'jazz': `chord("<${safeKey}m9 ${fourth}m9 ${fifth}7>/4").dict('ireal').voicing().s("gm_epiano1").gain(0.3).room(0.5)`
2450
- };
2451
- return padPatterns[style] || padPatterns['techno'];
2452
- }
2453
- case 'texture': {
2454
- const texturePatterns = {
2455
- 'techno': `s("hh:8*16").gain(perlin.range(0.02, 0.06)).hpf(8000).room(0.6).pan(perlin.range(0.2, 0.8).slow(8))`,
2456
- 'house': `s("~ noise:2 ~ noise:2").gain(0.04).hpf(6000).room(0.4)`,
2457
- 'dnb': `s("~ ~ ~ noise:4").gain(perlin.range(0.02, 0.05)).hpf(7000).room(0.5).pan(perlin.range(0.3, 0.7))`,
2458
- 'ambient': `s("pad:1").n(perlin.range(0, 8).floor()).gain(0.08).room(0.95).lpf(sine.range(500, 2000).slow(32)).slow(4)`,
2459
- 'trap': `s("~ ~ noise:3 ~").gain(0.03).hpf(10000).room(0.3)`,
2460
- 'jungle': `s("breaks125:8").fit().chop(32).gain(0.05).hpf(5000).room(0.4).pan(perlin.range(0.2, 0.8))`,
2461
- 'jazz': `s("brush:1").struct("~ 1 ~ 1 ~ 1 ~ ~").gain(0.1).room(0.5)`
2462
- };
2463
- return texturePatterns[style] || texturePatterns['techno'];
2464
- }
2465
- default:
2466
- throw new Error(`Unknown layer type: ${layer}`);
2467
- }
2468
- }
2469
- mergeLayerIntoPattern(currentPattern, newLayer, layerType) {
2470
- const trimmedPattern = currentPattern.trim();
2471
- const trimmedLayer = newLayer.trim();
2472
- const stackMatch = trimmedPattern.match(/^([\s\S]*?)stack\s*\(\s*([\s\S]*?)\s*\)([\s\S]*)$/);
2473
- if (stackMatch) {
2474
- const prefix = stackMatch[1];
2475
- const stackContents = stackMatch[2].trimEnd().replace(/,\s*$/, '');
2476
- const suffix = stackMatch[3];
2477
- return `${prefix}stack(
2478
- ${stackContents},
2479
-
2480
- // Jam ${layerType} layer
2481
- ${trimmedLayer}
2482
- )${suffix}`;
2483
- }
2484
- const tempoMatch = trimmedPattern.match(/^(\s*(?:setcp[ms]|setbpm)\s*\([^)]+\)\s*\n?)/);
2485
- const tempoPrefix = tempoMatch ? tempoMatch[1] : '';
2486
- const patternBody = tempoMatch ? trimmedPattern.slice(tempoMatch[0].length) : trimmedPattern;
2487
- return `${tempoPrefix}stack(
2488
- // Original pattern
2489
- ${patternBody},
2490
-
2491
- // Jam ${layerType} layer
2492
- ${trimmedLayer}
2493
- )`;
2494
- }
2495
- async run() {
2496
- const transport = new StdioServerTransport();
2497
- await this.server.connect(transport);
2498
- this.logger.info('Enhanced Strudel MCP server v2.0.1 running (fixed)');
2499
- process.on('SIGINT', async () => {
2500
- this.logger.info('Shutting down...');
2501
- await this.controller.cleanup();
2502
- await this.sessionManager.destroyAll();
2503
- process.exit(0);
2504
- });
2505
- }
2506
- }