@williamzujkowski/live-coding-music-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1735 -0
  3. package/dist/AudioAnalyzer.js +678 -0
  4. package/dist/PatternStore.js +169 -0
  5. package/dist/StrudelController.js +676 -0
  6. package/dist/__tests__/MusicTheory.test.js +245 -0
  7. package/dist/__tests__/PatternGenerator.test.js +337 -0
  8. package/dist/__tests__/PatternStore.test.js +153 -0
  9. package/dist/__tests__/utils/MockPlaywright.js +130 -0
  10. package/dist/index.js +21 -0
  11. package/dist/server/EnhancedMCPServer.js +654 -0
  12. package/dist/server/EnhancedMCPServerFixed.js +2506 -0
  13. package/dist/server/server.js +580 -0
  14. package/dist/server/tools/ai.js +530 -0
  15. package/dist/server/tools/analysis.js +132 -0
  16. package/dist/server/tools/capture.js +184 -0
  17. package/dist/server/tools/diagnostics.js +96 -0
  18. package/dist/server/tools/editor.js +128 -0
  19. package/dist/server/tools/generate.js +218 -0
  20. package/dist/server/tools/history.js +196 -0
  21. package/dist/server/tools/playback.js +41 -0
  22. package/dist/server/tools/session.js +115 -0
  23. package/dist/server/tools/storage.js +77 -0
  24. package/dist/server/tools/transform.js +399 -0
  25. package/dist/server/tools/types.js +9 -0
  26. package/dist/services/AudioCaptureService.js +279 -0
  27. package/dist/services/GeminiService.js +790 -0
  28. package/dist/services/MIDIExportService.js +412 -0
  29. package/dist/services/MusicTheory.js +186 -0
  30. package/dist/services/PatternGenerator.js +647 -0
  31. package/dist/services/SessionManager.js +316 -0
  32. package/dist/services/StrudelEngine.js +374 -0
  33. package/dist/services/__mocks__/StrudelEngine.js +157 -0
  34. package/dist/types/AudioAnalysis.js +4 -0
  35. package/dist/types/index.js +1 -0
  36. package/dist/utils/ErrorRecovery.js +248 -0
  37. package/dist/utils/InputValidator.js +281 -0
  38. package/dist/utils/Logger.js +19 -0
  39. package/dist/utils/PatternValidator.js +241 -0
  40. package/dist/utils/PerformanceMonitor.js +128 -0
  41. package/package.json +86 -0
@@ -0,0 +1,412 @@
1
+ /**
2
+ * MIDI Export Service
3
+ *
4
+ * Converts Strudel patterns to MIDI files using @tonejs/midi.
5
+ * Parses note() functions, MIDI numbers, and chord names from patterns.
6
+ *
7
+ * @example
8
+ * const service = new MIDIExportService();
9
+ * const notes = service.parsePatternNotes('note("c4 e4 g4")');
10
+ * const midi = service.convertToMidi(notes, { bpm: 120 });
11
+ * const base64 = service.exportToBase64(midi);
12
+ */
13
+ // @tonejs/midi is CommonJS - use dynamic import approach
14
+ import * as midiModule from '@tonejs/midi';
15
+ // Handle both ESM and CJS interop
16
+ const Midi = midiModule.Midi || midiModule.default?.Midi;
17
+ import { writeFileSync } from 'fs';
18
+ import { resolve } from 'path';
19
+ /**
20
+ * Note name to MIDI number mapping.
21
+ * C4 = 60 (middle C)
22
+ */
23
+ const NOTE_NAMES = {
24
+ 'c': 0, 'c#': 1, 'db': 1,
25
+ 'd': 2, 'd#': 3, 'eb': 3,
26
+ 'e': 4, 'fb': 4, 'e#': 5,
27
+ 'f': 5, 'f#': 6, 'gb': 6,
28
+ 'g': 7, 'g#': 8, 'ab': 8,
29
+ 'a': 9, 'a#': 10, 'bb': 10,
30
+ 'b': 11, 'cb': 11, 'b#': 0
31
+ };
32
+ /**
33
+ * Chord intervals from root note.
34
+ * Used to expand chord names to individual notes.
35
+ */
36
+ const CHORD_INTERVALS = {
37
+ '': [0, 4, 7], // major
38
+ 'm': [0, 3, 7], // minor
39
+ '7': [0, 4, 7, 10], // dominant 7
40
+ 'maj7': [0, 4, 7, 11], // major 7
41
+ 'm7': [0, 3, 7, 10], // minor 7
42
+ 'dim': [0, 3, 6], // diminished
43
+ 'aug': [0, 4, 8], // augmented
44
+ 'dim7': [0, 3, 6, 9], // diminished 7
45
+ 'sus2': [0, 2, 7], // suspended 2
46
+ 'sus4': [0, 5, 7], // suspended 4
47
+ '9': [0, 4, 7, 10, 14], // dominant 9
48
+ 'maj9': [0, 4, 7, 11, 14], // major 9
49
+ 'm9': [0, 3, 7, 10, 14], // minor 9
50
+ 'add9': [0, 4, 7, 14], // add 9
51
+ '6': [0, 4, 7, 9], // major 6
52
+ 'm6': [0, 3, 7, 9] // minor 6
53
+ };
54
+ export class MIDIExportService {
55
+ /**
56
+ * Converts a note name (e.g., "C4", "D#5", "Bb3") to MIDI number
57
+ * @param noteName - Note name with optional accidental and octave
58
+ * @returns MIDI note number (0-127) or null if invalid
59
+ */
60
+ noteNameToMidi(noteName) {
61
+ if (!noteName || typeof noteName !== 'string') {
62
+ return null;
63
+ }
64
+ const cleaned = noteName.toLowerCase().trim();
65
+ // Check for pure MIDI number
66
+ const midiNum = parseInt(cleaned, 10);
67
+ if (!isNaN(midiNum) && midiNum >= 0 && midiNum <= 127 && cleaned === midiNum.toString()) {
68
+ return midiNum;
69
+ }
70
+ // Parse note name with regex
71
+ // Matches: c, c#, db, c4, c#4, db4, etc.
72
+ const match = cleaned.match(/^([a-g])([#b]?)(-?\d+)?$/);
73
+ if (!match) {
74
+ return null;
75
+ }
76
+ const [, letter, accidental, octaveStr] = match;
77
+ const noteKey = letter + (accidental || '');
78
+ const semitone = NOTE_NAMES[noteKey];
79
+ if (semitone === undefined) {
80
+ return null;
81
+ }
82
+ // Default octave is 4 (middle C area)
83
+ const octave = octaveStr !== undefined ? parseInt(octaveStr, 10) : 4;
84
+ // MIDI note = (octave + 1) * 12 + semitone
85
+ // C4 = 60, so octave 4 base = 60, but 60 = (4+1)*12 + 0
86
+ const midi = (octave + 1) * 12 + semitone;
87
+ // Clamp to valid MIDI range
88
+ if (midi < 0 || midi > 127) {
89
+ return null;
90
+ }
91
+ return midi;
92
+ }
93
+ /**
94
+ * Converts a MIDI number to note name
95
+ * @param midi - MIDI note number (0-127)
96
+ * @returns Note name with octave (e.g., "C4")
97
+ */
98
+ midiToNoteName(midi) {
99
+ const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
100
+ const octave = Math.floor(midi / 12) - 1;
101
+ const note = noteNames[midi % 12];
102
+ return `${note}${octave}`;
103
+ }
104
+ /**
105
+ * Expands a chord name to individual MIDI note numbers
106
+ * @param chordName - Chord name (e.g., "Cmaj7", "Am", "G7")
107
+ * @param octave - Base octave (default: 4)
108
+ * @returns Array of MIDI note numbers or empty array if invalid
109
+ */
110
+ expandChord(chordName, octave = 4) {
111
+ if (!chordName || typeof chordName !== 'string') {
112
+ return [];
113
+ }
114
+ const cleaned = chordName.trim();
115
+ // Parse chord: root note + optional accidental + chord type
116
+ const match = cleaned.match(/^([A-Ga-g])([#b]?)(.*)$/);
117
+ if (!match) {
118
+ return [];
119
+ }
120
+ const [, letter, accidental, chordType] = match;
121
+ const rootKey = letter.toLowerCase() + (accidental || '');
122
+ const rootSemitone = NOTE_NAMES[rootKey];
123
+ if (rootSemitone === undefined) {
124
+ return [];
125
+ }
126
+ // Get chord intervals, default to major triad
127
+ const intervals = CHORD_INTERVALS[chordType.toLowerCase()] || CHORD_INTERVALS[''];
128
+ const rootMidi = (octave + 1) * 12 + rootSemitone;
129
+ return intervals.map(interval => {
130
+ const midi = rootMidi + interval;
131
+ return midi >= 0 && midi <= 127 ? midi : -1;
132
+ }).filter(n => n >= 0);
133
+ }
134
+ /**
135
+ * Parses a Strudel pattern and extracts note events
136
+ * @param pattern - Strudel pattern code
137
+ * @returns Array of NoteEvent objects
138
+ */
139
+ parsePatternNotes(pattern) {
140
+ if (!pattern || typeof pattern !== 'string') {
141
+ return [];
142
+ }
143
+ const notes = [];
144
+ let currentTime = 0;
145
+ // Extract note() function calls
146
+ // Matches: note("c4 e4 g4"), note("c4", "e4"), note(`c4`)
147
+ const noteRegex = /note\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/gi;
148
+ let noteMatch;
149
+ while ((noteMatch = noteRegex.exec(pattern)) !== null) {
150
+ const noteContent = noteMatch[1];
151
+ const parsedNotes = this.parseNoteString(noteContent, currentTime);
152
+ notes.push(...parsedNotes);
153
+ currentTime += parsedNotes.length > 0 ? 1 : 0;
154
+ }
155
+ // Extract n() function calls (Strudel shorthand)
156
+ const nRegex = /\bn\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/gi;
157
+ let nMatch;
158
+ while ((nMatch = nRegex.exec(pattern)) !== null) {
159
+ const noteContent = nMatch[1];
160
+ // n() uses MIDI numbers directly
161
+ const parsedNotes = this.parseNoteString(noteContent, currentTime, true);
162
+ notes.push(...parsedNotes);
163
+ currentTime += parsedNotes.length > 0 ? 1 : 0;
164
+ }
165
+ // Extract chord patterns
166
+ const chordRegex = /chord\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/gi;
167
+ let chordMatch;
168
+ while ((chordMatch = chordRegex.exec(pattern)) !== null) {
169
+ const chordContent = chordMatch[1];
170
+ const parsedChords = this.parseChordString(chordContent, currentTime);
171
+ notes.push(...parsedChords);
172
+ currentTime += parsedChords.length > 0 ? 1 : 0;
173
+ }
174
+ // Extract s() sound patterns with n() modifier for samples
175
+ // e.g., s("piano").n("0 2 4 7")
176
+ const soundNRegex = /s\s*\([^)]+\)\s*\.n\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/gi;
177
+ let soundNMatch;
178
+ while ((soundNMatch = soundNRegex.exec(pattern)) !== null) {
179
+ const noteContent = soundNMatch[1];
180
+ const parsedNotes = this.parseNoteString(noteContent, currentTime, true);
181
+ notes.push(...parsedNotes);
182
+ currentTime += parsedNotes.length > 0 ? 1 : 0;
183
+ }
184
+ // If no notes found through function parsing, try to find inline notes
185
+ if (notes.length === 0) {
186
+ const inlineNotes = this.parseInlineNotes(pattern);
187
+ notes.push(...inlineNotes);
188
+ }
189
+ return notes;
190
+ }
191
+ /**
192
+ * Parses a space/comma-separated note string
193
+ * @param noteString - String of notes (e.g., "c4 e4 g4" or "60 64 67")
194
+ * @param startTime - Starting time in beats
195
+ * @param asMidiNumbers - Treat values as MIDI numbers
196
+ * @returns Array of NoteEvent objects
197
+ */
198
+ parseNoteString(noteString, startTime, asMidiNumbers = false) {
199
+ const notes = [];
200
+ const parts = noteString.split(/[\s,]+/).filter(p => p.length > 0);
201
+ const noteDuration = parts.length > 0 ? 1 / parts.length : 1;
202
+ parts.forEach((part, index) => {
203
+ // Check for rest
204
+ if (part === '~' || part === '-' || part === 'r') {
205
+ return;
206
+ }
207
+ // Handle sub-patterns in brackets [c4 e4]
208
+ if (part.startsWith('[')) {
209
+ const subContent = part.replace(/[\[\]]/g, '');
210
+ const subParts = subContent.split(/[\s,]+/);
211
+ subParts.forEach(subPart => {
212
+ const midi = asMidiNumbers
213
+ ? parseInt(subPart, 10)
214
+ : this.noteNameToMidi(subPart);
215
+ if (midi !== null && !isNaN(midi) && midi >= 0 && midi <= 127) {
216
+ notes.push({
217
+ note: midi,
218
+ time: startTime + index * noteDuration,
219
+ duration: noteDuration / subParts.length,
220
+ velocity: 100
221
+ });
222
+ }
223
+ });
224
+ return;
225
+ }
226
+ const midi = asMidiNumbers
227
+ ? parseInt(part, 10)
228
+ : this.noteNameToMidi(part);
229
+ if (midi !== null && !isNaN(midi) && midi >= 0 && midi <= 127) {
230
+ notes.push({
231
+ note: midi,
232
+ time: startTime + index * noteDuration,
233
+ duration: noteDuration,
234
+ velocity: 100
235
+ });
236
+ }
237
+ });
238
+ return notes;
239
+ }
240
+ /**
241
+ * Parses a chord string and expands to note events
242
+ * @param chordString - String of chord names (e.g., "Cmaj7 Am Dm G7")
243
+ * @param startTime - Starting time in beats
244
+ * @returns Array of NoteEvent objects
245
+ */
246
+ parseChordString(chordString, startTime) {
247
+ const notes = [];
248
+ const chords = chordString.split(/[\s,]+/).filter(c => c.length > 0);
249
+ const chordDuration = chords.length > 0 ? 1 / chords.length : 1;
250
+ chords.forEach((chord, index) => {
251
+ if (chord === '~' || chord === '-' || chord === 'r') {
252
+ return;
253
+ }
254
+ const midiNotes = this.expandChord(chord);
255
+ midiNotes.forEach(midi => {
256
+ notes.push({
257
+ note: midi,
258
+ time: startTime + index * chordDuration,
259
+ duration: chordDuration,
260
+ velocity: 100
261
+ });
262
+ });
263
+ });
264
+ return notes;
265
+ }
266
+ /**
267
+ * Attempts to find note-like patterns inline in the pattern code
268
+ * @param pattern - Full pattern string
269
+ * @returns Array of NoteEvent objects
270
+ */
271
+ parseInlineNotes(pattern) {
272
+ const notes = [];
273
+ // Find quoted strings that look like note sequences
274
+ const quotedRegex = /["'`]([a-g][#b]?\d[\s,a-g#b0-9~\-\[\]]+)["'`]/gi;
275
+ let match;
276
+ while ((match = quotedRegex.exec(pattern)) !== null) {
277
+ const noteSequence = match[1];
278
+ const parsed = this.parseNoteString(noteSequence, notes.length);
279
+ notes.push(...parsed);
280
+ }
281
+ return notes;
282
+ }
283
+ /**
284
+ * Converts parsed note events to a MIDI object
285
+ * @param notes - Array of NoteEvent objects
286
+ * @param options - MIDI export options
287
+ * @returns Midi object
288
+ */
289
+ convertToMidi(notes, options = {}) {
290
+ const { bpm = 120, bars = 4, trackName = 'Strudel Pattern', timeSignatureNumerator = 4, timeSignatureDenominator = 4 } = options;
291
+ const midi = new Midi();
292
+ // Set tempo
293
+ midi.header.setTempo(bpm);
294
+ // Set time signature
295
+ midi.header.timeSignatures.push({
296
+ ticks: 0,
297
+ timeSignature: [timeSignatureNumerator, timeSignatureDenominator],
298
+ measures: 0
299
+ });
300
+ // Create track
301
+ const track = midi.addTrack();
302
+ track.name = trackName;
303
+ // Calculate PPQ (pulses per quarter note) from the header
304
+ const ppq = midi.header.ppq;
305
+ // Add notes to track
306
+ notes.forEach(noteEvent => {
307
+ // Only add notes within the specified bar range
308
+ const maxTime = bars * timeSignatureNumerator;
309
+ if (noteEvent.time >= maxTime) {
310
+ return;
311
+ }
312
+ // Convert beat time to seconds at the given BPM
313
+ const timeInSeconds = (noteEvent.time / (bpm / 60));
314
+ const durationInSeconds = (noteEvent.duration / (bpm / 60));
315
+ track.addNote({
316
+ midi: noteEvent.note,
317
+ time: timeInSeconds,
318
+ duration: durationInSeconds,
319
+ velocity: noteEvent.velocity / 127
320
+ });
321
+ });
322
+ return midi;
323
+ }
324
+ /**
325
+ * Exports a Strudel pattern to a MIDI file
326
+ * @param pattern - Strudel pattern code
327
+ * @param filename - Output filename (default: 'pattern.mid')
328
+ * @param options - MIDI export options
329
+ * @returns Export result
330
+ */
331
+ exportToFile(pattern, filename = 'pattern.mid', options = {}) {
332
+ try {
333
+ const notes = this.parsePatternNotes(pattern);
334
+ if (notes.length === 0) {
335
+ return {
336
+ success: false,
337
+ output: '',
338
+ noteCount: 0,
339
+ bars: options.bars || 4,
340
+ bpm: options.bpm || 120,
341
+ error: 'No notes found in pattern. Use note(), n(), or chord() functions.'
342
+ };
343
+ }
344
+ const midi = this.convertToMidi(notes, options);
345
+ // Ensure filename has .mid extension
346
+ const finalFilename = filename.endsWith('.mid') ? filename : `${filename}.mid`;
347
+ const outputPath = resolve(finalFilename);
348
+ // Write MIDI data
349
+ const midiArray = midi.toArray();
350
+ writeFileSync(outputPath, Buffer.from(midiArray));
351
+ return {
352
+ success: true,
353
+ output: outputPath,
354
+ noteCount: notes.length,
355
+ bars: options.bars || 4,
356
+ bpm: options.bpm || 120
357
+ };
358
+ }
359
+ catch (error) {
360
+ return {
361
+ success: false,
362
+ output: '',
363
+ noteCount: 0,
364
+ bars: options.bars || 4,
365
+ bpm: options.bpm || 120,
366
+ error: `MIDI export failed: ${error.message}`
367
+ };
368
+ }
369
+ }
370
+ /**
371
+ * Exports a Strudel pattern to base64-encoded MIDI data
372
+ * @param pattern - Strudel pattern code
373
+ * @param options - MIDI export options
374
+ * @returns Export result with base64 data
375
+ */
376
+ exportToBase64(pattern, options = {}) {
377
+ try {
378
+ const notes = this.parsePatternNotes(pattern);
379
+ if (notes.length === 0) {
380
+ return {
381
+ success: false,
382
+ output: '',
383
+ noteCount: 0,
384
+ bars: options.bars || 4,
385
+ bpm: options.bpm || 120,
386
+ error: 'No notes found in pattern. Use note(), n(), or chord() functions.'
387
+ };
388
+ }
389
+ const midi = this.convertToMidi(notes, options);
390
+ const midiArray = midi.toArray();
391
+ const buffer = Buffer.from(midiArray);
392
+ const base64 = buffer.toString('base64');
393
+ return {
394
+ success: true,
395
+ output: base64,
396
+ noteCount: notes.length,
397
+ bars: options.bars || 4,
398
+ bpm: options.bpm || 120
399
+ };
400
+ }
401
+ catch (error) {
402
+ return {
403
+ success: false,
404
+ output: '',
405
+ noteCount: 0,
406
+ bars: options.bars || 4,
407
+ bpm: options.bpm || 120,
408
+ error: `MIDI export failed: ${error.message}`
409
+ };
410
+ }
411
+ }
412
+ }
@@ -0,0 +1,186 @@
1
+ export class MusicTheory {
2
+ scales = {
3
+ major: [0, 2, 4, 5, 7, 9, 11],
4
+ minor: [0, 2, 3, 5, 7, 8, 10],
5
+ dorian: [0, 2, 3, 5, 7, 9, 10],
6
+ phrygian: [0, 1, 3, 5, 7, 8, 10],
7
+ lydian: [0, 2, 4, 6, 7, 9, 11],
8
+ mixolydian: [0, 2, 4, 5, 7, 9, 10],
9
+ aeolian: [0, 2, 3, 5, 7, 8, 10],
10
+ locrian: [0, 1, 3, 5, 6, 8, 10],
11
+ pentatonic: [0, 2, 4, 7, 9],
12
+ blues: [0, 3, 5, 6, 7, 10],
13
+ chromatic: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
14
+ wholetone: [0, 2, 4, 6, 8, 10],
15
+ harmonic_minor: [0, 2, 3, 5, 7, 8, 11],
16
+ melodic_minor: [0, 2, 3, 5, 7, 9, 11]
17
+ };
18
+ chordProgressions = {
19
+ pop: ['I', 'V', 'vi', 'IV'],
20
+ jazz: ['IIM7', 'V7', 'IM7'],
21
+ blues: ['I7', 'I7', 'I7', 'I7', 'IV7', 'IV7', 'I7', 'I7', 'V7', 'IV7', 'I7', 'V7'],
22
+ folk: ['I', 'IV', 'I', 'V'],
23
+ rock: ['I', 'bVII', 'IV', 'I'],
24
+ classical: ['I', 'IV', 'V', 'I'],
25
+ modal: ['i', 'bVII', 'IV', 'i'],
26
+ edm: ['i', 'VI', 'III', 'VII']
27
+ };
28
+ /**
29
+ * Generates a musical scale from a root note
30
+ * @param root - Root note (e.g., 'C', 'D#', 'F')
31
+ * @param scaleName - Scale type (e.g., 'major', 'minor', 'pentatonic')
32
+ * @returns Array of note names in the scale
33
+ * @throws {Error} When root note or scale name is invalid
34
+ */
35
+ generateScale(root, scaleName) {
36
+ const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
37
+ const rootIndex = noteNames.indexOf(root.toUpperCase());
38
+ if (rootIndex === -1) {
39
+ throw new Error(`Invalid root note: ${root}`);
40
+ }
41
+ const scale = this.scales[scaleName];
42
+ if (!scale) {
43
+ throw new Error(`Invalid scale: ${scaleName}`);
44
+ }
45
+ return scale.map(interval => {
46
+ const noteIndex = (rootIndex + interval) % 12;
47
+ return noteNames[noteIndex];
48
+ });
49
+ }
50
+ /**
51
+ * Generates a chord progression for a given key and style
52
+ * @param key - Musical key (e.g., 'C', 'Am')
53
+ * @param style - Progression style (e.g., 'pop', 'jazz', 'blues')
54
+ * @returns Space-separated string of chord names
55
+ * @throws {Error} When style is invalid
56
+ */
57
+ generateChordProgression(key, style) {
58
+ const progression = this.chordProgressions[style];
59
+ if (!progression) {
60
+ throw new Error(`Invalid progression style: ${style}`);
61
+ }
62
+ const chordMap = {
63
+ 'I': key,
64
+ 'I7': `${key}7`,
65
+ 'i': `${key.toLowerCase()}m`,
66
+ 'ii': `${this.getNote(key, 2)}m`,
67
+ 'IIM7': `${this.getNote(key, 2)}m7`,
68
+ 'iii': `${this.getNote(key, 4)}m`,
69
+ 'III': this.getNote(key, 4),
70
+ 'IV': this.getNote(key, 5),
71
+ 'IV7': `${this.getNote(key, 5)}7`,
72
+ 'V': this.getNote(key, 7),
73
+ 'V7': `${this.getNote(key, 7)}7`,
74
+ 'vi': `${this.getNote(key, 9)}m`,
75
+ 'VI': this.getNote(key, 9),
76
+ 'VII': this.getNote(key, 11),
77
+ 'bVII': this.getNote(key, 10),
78
+ 'IM7': `${key}maj7`
79
+ };
80
+ return progression
81
+ .map(chord => chordMap[chord] || key)
82
+ .join(' ');
83
+ }
84
+ /**
85
+ * Calculates a note at a given interval from the root
86
+ * @param root - Root note name
87
+ * @param semitones - Number of semitones to transpose
88
+ * @returns Transposed note name
89
+ */
90
+ getNote(root, semitones) {
91
+ const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
92
+ const rootIndex = noteNames.indexOf(root.toUpperCase());
93
+ if (rootIndex === -1)
94
+ return root;
95
+ return noteNames[(rootIndex + semitones) % 12];
96
+ }
97
+ /**
98
+ * Generates a Euclidean rhythm pattern
99
+ * @param hits - Number of hits/beats in the pattern
100
+ * @param steps - Total number of steps in the pattern
101
+ * @returns Space-separated string of '1' (hit) and '~' (rest)
102
+ * @throws {Error} When hits exceed steps
103
+ */
104
+ generateEuclideanRhythm(hits, steps) {
105
+ if (hits > steps) {
106
+ throw new Error('Hits cannot exceed steps');
107
+ }
108
+ const pattern = new Array(steps).fill(false);
109
+ const interval = steps / hits;
110
+ for (let i = 0; i < hits; i++) {
111
+ const index = Math.floor(i * interval);
112
+ pattern[index] = true;
113
+ }
114
+ return pattern.map(hit => hit ? '1' : '~').join(' ');
115
+ }
116
+ /**
117
+ * Generates a polyrhythm from two Euclidean patterns
118
+ * @param pattern1 - Number of hits for first rhythm
119
+ * @param pattern2 - Number of hits for second rhythm
120
+ * @param length - Total length of patterns (default: 16)
121
+ * @returns Formatted polyrhythm string
122
+ */
123
+ generatePolyrhythm(pattern1, pattern2, length = 16) {
124
+ const rhythm1 = this.generateEuclideanRhythm(pattern1, length);
125
+ const rhythm2 = this.generateEuclideanRhythm(pattern2, length);
126
+ return `[${rhythm1}], [${rhythm2}]`;
127
+ }
128
+ /**
129
+ * Gets scale notes with octave numbers
130
+ * @param root - Root note of the scale
131
+ * @param scaleName - Type of scale to generate
132
+ * @param octave - Octave number (default: 3)
133
+ * @returns Array of notes with octave numbers (e.g., ['c3', 'd3', 'e3'])
134
+ */
135
+ getScaleNotes(root, scaleName, octave = 3) {
136
+ const scale = this.generateScale(root, scaleName);
137
+ return scale.map(note => `${note.toLowerCase()}${octave}`);
138
+ }
139
+ /**
140
+ * Generates an arpeggio pattern from a chord
141
+ * @param chord - Chord name to arpeggiate
142
+ * @param pattern - Arpeggio pattern ('up', 'down', 'updown', 'random')
143
+ * @returns Space-separated string of notes in arpeggio order
144
+ */
145
+ generateArpeggio(chord, pattern = 'up') {
146
+ const patterns = {
147
+ 'up': [0, 1, 2, 3],
148
+ 'down': [3, 2, 1, 0],
149
+ 'updown': [0, 1, 2, 3, 2, 1],
150
+ 'random': [0, 2, 1, 3, 0, 3, 1, 2]
151
+ };
152
+ const arpPattern = patterns[pattern] || patterns['up'];
153
+ const notes = this.getChordNotes(chord);
154
+ return arpPattern
155
+ .map(index => notes[index % notes.length])
156
+ .join(' ');
157
+ }
158
+ /**
159
+ * Extracts individual notes from a chord name
160
+ * Parses chord symbols and returns note names with octave numbers
161
+ * @param chord - Chord name to parse (e.g., 'Cmaj7', 'Am', 'G7')
162
+ * @returns Array of note names with octave numbers (e.g., ['c3', 'e3', 'g3'])
163
+ * @example
164
+ * const notes = this.getChordNotes('Cmaj7');
165
+ * // Returns: ['c3', 'e3', 'g3', 'b3']
166
+ */
167
+ getChordNotes(chord) {
168
+ // Simplified chord parsing - would need more sophisticated implementation
169
+ const root = chord.replace(/[^A-G#]/g, '');
170
+ const type = chord.replace(/[A-G#]/g, '');
171
+ const intervals = {
172
+ '': [0, 4, 7], // major
173
+ 'm': [0, 3, 7], // minor
174
+ '7': [0, 4, 7, 10], // dominant 7
175
+ 'maj7': [0, 4, 7, 11], // major 7
176
+ 'm7': [0, 3, 7, 10], // minor 7
177
+ 'dim': [0, 3, 6], // diminished
178
+ 'aug': [0, 4, 8] // augmented
179
+ };
180
+ const chordIntervals = intervals[type] || intervals[''];
181
+ return chordIntervals.map(interval => {
182
+ const note = this.getNote(root, interval);
183
+ return `${note.toLowerCase()}3`;
184
+ });
185
+ }
186
+ }