@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.
- package/LICENSE +21 -0
- package/README.md +1735 -0
- package/dist/AudioAnalyzer.js +678 -0
- package/dist/PatternStore.js +169 -0
- package/dist/StrudelController.js +676 -0
- package/dist/__tests__/MusicTheory.test.js +245 -0
- package/dist/__tests__/PatternGenerator.test.js +337 -0
- package/dist/__tests__/PatternStore.test.js +153 -0
- package/dist/__tests__/utils/MockPlaywright.js +130 -0
- package/dist/index.js +21 -0
- package/dist/server/EnhancedMCPServer.js +654 -0
- package/dist/server/EnhancedMCPServerFixed.js +2506 -0
- package/dist/server/server.js +580 -0
- package/dist/server/tools/ai.js +530 -0
- package/dist/server/tools/analysis.js +132 -0
- package/dist/server/tools/capture.js +184 -0
- package/dist/server/tools/diagnostics.js +96 -0
- package/dist/server/tools/editor.js +128 -0
- package/dist/server/tools/generate.js +218 -0
- package/dist/server/tools/history.js +196 -0
- package/dist/server/tools/playback.js +41 -0
- package/dist/server/tools/session.js +115 -0
- package/dist/server/tools/storage.js +77 -0
- package/dist/server/tools/transform.js +399 -0
- package/dist/server/tools/types.js +9 -0
- package/dist/services/AudioCaptureService.js +279 -0
- package/dist/services/GeminiService.js +790 -0
- package/dist/services/MIDIExportService.js +412 -0
- package/dist/services/MusicTheory.js +186 -0
- package/dist/services/PatternGenerator.js +647 -0
- package/dist/services/SessionManager.js +316 -0
- package/dist/services/StrudelEngine.js +374 -0
- package/dist/services/__mocks__/StrudelEngine.js +157 -0
- package/dist/types/AudioAnalysis.js +4 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/ErrorRecovery.js +248 -0
- package/dist/utils/InputValidator.js +281 -0
- package/dist/utils/Logger.js +19 -0
- package/dist/utils/PatternValidator.js +241 -0
- package/dist/utils/PerformanceMonitor.js +128 -0
- 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
|
+
}
|