@kernel.chat/kbot 3.49.0 → 3.51.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 (36) hide show
  1. package/dist/agent-teams.d.ts.map +1 -1
  2. package/dist/agent-teams.js +7 -0
  3. package/dist/agent-teams.js.map +1 -1
  4. package/dist/agents/producer.d.ts +21 -0
  5. package/dist/agents/producer.d.ts.map +1 -0
  6. package/dist/agents/producer.js +139 -0
  7. package/dist/agents/producer.js.map +1 -0
  8. package/dist/agents/specialists.d.ts.map +1 -1
  9. package/dist/agents/specialists.js +19 -0
  10. package/dist/agents/specialists.js.map +1 -1
  11. package/dist/completions.d.ts.map +1 -1
  12. package/dist/completions.js +1 -0
  13. package/dist/completions.js.map +1 -1
  14. package/dist/integrations/ableton-osc.d.ts +146 -0
  15. package/dist/integrations/ableton-osc.d.ts.map +1 -0
  16. package/dist/integrations/ableton-osc.js +590 -0
  17. package/dist/integrations/ableton-osc.js.map +1 -0
  18. package/dist/learned-router.d.ts.map +1 -1
  19. package/dist/learned-router.js +20 -0
  20. package/dist/learned-router.js.map +1 -1
  21. package/dist/tools/ableton-knowledge.d.ts +2 -0
  22. package/dist/tools/ableton-knowledge.d.ts.map +1 -0
  23. package/dist/tools/ableton-knowledge.js +419 -0
  24. package/dist/tools/ableton-knowledge.js.map +1 -0
  25. package/dist/tools/ableton.d.ts +2 -0
  26. package/dist/tools/ableton.d.ts.map +1 -0
  27. package/dist/tools/ableton.js +1023 -0
  28. package/dist/tools/ableton.js.map +1 -0
  29. package/dist/tools/index.d.ts.map +1 -1
  30. package/dist/tools/index.js +2 -0
  31. package/dist/tools/index.js.map +1 -1
  32. package/dist/tools/music-theory.d.ts +175 -0
  33. package/dist/tools/music-theory.d.ts.map +1 -0
  34. package/dist/tools/music-theory.js +1020 -0
  35. package/dist/tools/music-theory.js.map +1 -0
  36. package/package.json +1 -1
@@ -0,0 +1,1020 @@
1
+ // kbot Music Theory Engine
2
+ // Shared foundation for ableton.ts, magenta-plugin.ts, and creative.ts.
3
+ // All music theory primitives: scales, chords, MIDI, progressions, rhythm, voice leading.
4
+ // Zero external dependencies.
5
+ // ─── Constants ─────────────────────────────────────────────────────
6
+ const NOTE_NAMES_SHARP = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
7
+ const NOTE_NAMES_FLAT = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
8
+ /** Prefer flats for keys commonly written with flats */
9
+ const FLAT_KEYS = new Set(['F', 'Bb', 'Eb', 'Ab', 'Db', 'Gb', 'Dm', 'Gm', 'Cm', 'Fm', 'Bbm', 'Ebm']);
10
+ // ─── Scales (20+) ──────────────────────────────────────────────────
11
+ // Intervals from root, ascending within one octave.
12
+ export const SCALES = {
13
+ // Diatonic modes
14
+ major: [0, 2, 4, 5, 7, 9, 11],
15
+ natural_minor: [0, 2, 3, 5, 7, 8, 10],
16
+ harmonic_minor: [0, 2, 3, 5, 7, 8, 11],
17
+ melodic_minor: [0, 2, 3, 5, 7, 9, 11],
18
+ // Church modes
19
+ dorian: [0, 2, 3, 5, 7, 9, 10],
20
+ phrygian: [0, 1, 3, 5, 7, 8, 10],
21
+ lydian: [0, 2, 4, 6, 7, 9, 11],
22
+ mixolydian: [0, 2, 4, 5, 7, 9, 10],
23
+ locrian: [0, 1, 3, 5, 6, 8, 10],
24
+ // Pentatonic / blues
25
+ pentatonic_major: [0, 2, 4, 7, 9],
26
+ pentatonic_minor: [0, 3, 5, 7, 10],
27
+ blues: [0, 3, 5, 6, 7, 10],
28
+ // Symmetric
29
+ whole_tone: [0, 2, 4, 6, 8, 10],
30
+ diminished: [0, 1, 3, 4, 6, 7, 9, 10], // half-whole
31
+ diminished_whole_half: [0, 2, 3, 5, 6, 8, 9, 11], // whole-half
32
+ // Bebop
33
+ bebop_dominant: [0, 2, 4, 5, 7, 9, 10, 11],
34
+ bebop_major: [0, 2, 4, 5, 7, 8, 9, 11],
35
+ // Exotic
36
+ hungarian_minor: [0, 2, 3, 6, 7, 8, 11],
37
+ phrygian_dominant: [0, 1, 4, 5, 7, 8, 10],
38
+ double_harmonic: [0, 1, 4, 5, 7, 8, 11],
39
+ enigmatic: [0, 1, 4, 6, 8, 10, 11],
40
+ neapolitan_minor: [0, 1, 3, 5, 7, 8, 11],
41
+ neapolitan_major: [0, 1, 3, 5, 7, 9, 11],
42
+ // Chromatic
43
+ chromatic: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
44
+ };
45
+ // ─── Chords (25+) ──────────────────────────────────────────────────
46
+ // Intervals from root.
47
+ export const CHORDS = {
48
+ // Triads
49
+ major: [0, 4, 7],
50
+ minor: [0, 3, 7],
51
+ dim: [0, 3, 6],
52
+ aug: [0, 4, 8],
53
+ // Seventh chords
54
+ dom7: [0, 4, 7, 10],
55
+ maj7: [0, 4, 7, 11],
56
+ min7: [0, 3, 7, 10],
57
+ dim7: [0, 3, 6, 9],
58
+ m7b5: [0, 3, 6, 10], // half-diminished
59
+ aug7: [0, 4, 8, 10],
60
+ minmaj7: [0, 3, 7, 11],
61
+ // Extended chords
62
+ dom9: [0, 4, 7, 10, 14],
63
+ maj9: [0, 4, 7, 11, 14],
64
+ min9: [0, 3, 7, 10, 14],
65
+ dom11: [0, 4, 7, 10, 14, 17],
66
+ min11: [0, 3, 7, 10, 14, 17],
67
+ dom13: [0, 4, 7, 10, 14, 17, 21],
68
+ min13: [0, 3, 7, 10, 14, 17, 21],
69
+ // Added-tone / suspended
70
+ add9: [0, 4, 7, 14],
71
+ '6': [0, 4, 7, 9],
72
+ m6: [0, 3, 7, 9],
73
+ sus2: [0, 2, 7],
74
+ sus4: [0, 5, 7],
75
+ '7sus4': [0, 5, 7, 10],
76
+ // Altered dominants
77
+ '7sharp9': [0, 4, 7, 10, 15], // Hendrix chord
78
+ '7flat9': [0, 4, 7, 10, 13],
79
+ '7sharp5': [0, 4, 8, 10],
80
+ '7flat5': [0, 4, 6, 10],
81
+ '9sharp11': [0, 4, 7, 10, 14, 18],
82
+ // Power / cluster
83
+ power: [0, 7],
84
+ power8: [0, 7, 12],
85
+ };
86
+ // ─── MIDI Conversion ───────────────────────────────────────────────
87
+ const NAME_TO_PC = {
88
+ 'C': 0, 'C#': 1, 'Db': 1,
89
+ 'D': 2, 'D#': 3, 'Eb': 3,
90
+ 'E': 4, 'Fb': 4, 'E#': 5,
91
+ 'F': 5, 'F#': 6, 'Gb': 6,
92
+ 'G': 7, 'G#': 8, 'Ab': 8,
93
+ 'A': 9, 'A#': 10, 'Bb': 10,
94
+ 'B': 11, 'Cb': 11, 'B#': 0,
95
+ };
96
+ /**
97
+ * Convert a note name to MIDI number.
98
+ * Accepts: "C4", "F#3", "Bb5", "Eb2", etc.
99
+ * Uses scientific pitch notation where C4 = 60 (middle C).
100
+ */
101
+ export function noteNameToMidi(name) {
102
+ const match = name.match(/^([A-Ga-g][#b]?)(-?\d)$/);
103
+ if (!match)
104
+ throw new Error(`Invalid note name: "${name}"`);
105
+ const pc = NAME_TO_PC[match[1].charAt(0).toUpperCase() + match[1].slice(1)];
106
+ if (pc === undefined)
107
+ throw new Error(`Unknown pitch class: "${match[1]}"`);
108
+ const octave = parseInt(match[2], 10);
109
+ return (octave + 1) * 12 + pc;
110
+ }
111
+ /**
112
+ * Convert MIDI number to note name.
113
+ * Returns sharps by default. Pass `preferFlats: true` for flat names.
114
+ */
115
+ export function midiToNoteName(midi, preferFlats = false) {
116
+ const names = preferFlats ? NOTE_NAMES_FLAT : NOTE_NAMES_SHARP;
117
+ const pc = ((midi % 12) + 12) % 12;
118
+ const octave = Math.floor(midi / 12) - 1;
119
+ return `${names[pc]}${octave}`;
120
+ }
121
+ /**
122
+ * Convert MIDI number to frequency in Hz (A4 = 440 Hz).
123
+ */
124
+ export function midiToFrequency(midi) {
125
+ return 440 * Math.pow(2, (midi - 69) / 12);
126
+ }
127
+ /**
128
+ * Convert frequency in Hz to the nearest MIDI number.
129
+ */
130
+ export function frequencyToMidi(freq) {
131
+ return Math.round(12 * Math.log2(freq / 440) + 69);
132
+ }
133
+ // ─── Internal Helpers ──────────────────────────────────────────────
134
+ /** Parse a root note name (without octave) to pitch class 0-11. */
135
+ function parseRoot(name) {
136
+ const cleaned = name.charAt(0).toUpperCase() + name.slice(1);
137
+ const pc = NAME_TO_PC[cleaned];
138
+ if (pc === undefined)
139
+ throw new Error(`Unknown root note: "${name}"`);
140
+ return pc;
141
+ }
142
+ /** Build MIDI notes from a root MIDI number and interval array. */
143
+ function buildChordMidi(rootMidi, intervals) {
144
+ return intervals.map(i => rootMidi + i);
145
+ }
146
+ // ─── Chord Symbol Parser ───────────────────────────────────────────
147
+ /**
148
+ * Quality alias mapping. Order matters — longer matches first.
149
+ * Maps common chord symbol suffixes to CHORDS keys.
150
+ */
151
+ const QUALITY_ALIASES = [
152
+ // Extended altered
153
+ [/^(?:dom)?9#11$/i, '9sharp11'],
154
+ [/^(?:dom)?13$/i, 'dom13'],
155
+ [/^m(?:in)?13$/i, 'min13'],
156
+ [/^(?:dom)?11$/i, 'dom11'],
157
+ [/^m(?:in)?11$/i, 'min11'],
158
+ [/^(?:dom)?9$/i, 'dom9'],
159
+ [/^maj9$/i, 'maj9'],
160
+ [/^m(?:in)?9$/i, 'min9'],
161
+ // Altered 7ths
162
+ [/^7[#\+]9$/i, '7sharp9'],
163
+ [/^7b9$/i, '7flat9'],
164
+ [/^7[#\+]5$/i, '7sharp5'],
165
+ [/^7b5$/i, '7flat5'],
166
+ [/^7sus4$/i, '7sus4'],
167
+ // 7ths
168
+ [/^maj7$/i, 'maj7'],
169
+ [/^(?:M7|ma7|Maj7|\u0394 ?7)$/, 'maj7'],
170
+ [/^m(?:in)?(?:maj|M)7$/, 'minmaj7'],
171
+ [/^m(?:in)?7b5$/i, 'm7b5'],
172
+ [/^(?:half)?dim7$/i, 'm7b5'],
173
+ [/^dim7$/i, 'dim7'],
174
+ [/^m(?:in)?7$/i, 'min7'],
175
+ [/^(?:dom)?7$/i, 'dom7'],
176
+ [/^aug7$/i, 'aug7'],
177
+ // 6ths
178
+ [/^m6$/i, 'm6'],
179
+ [/^6$/i, '6'],
180
+ // Triads & sus
181
+ [/^add9$/i, 'add9'],
182
+ [/^sus2$/i, 'sus2'],
183
+ [/^sus4$/i, 'sus4'],
184
+ [/^sus$/i, 'sus4'],
185
+ [/^dim$/i, 'dim'],
186
+ [/^[o°]$/i, 'dim'],
187
+ [/^aug$/i, 'aug'],
188
+ [/^[+]$/i, 'aug'],
189
+ [/^m(?:in)?$/i, 'minor'],
190
+ [/^[-]$/i, 'minor'],
191
+ [/^(?:maj|M)?$/i, 'major'], // empty suffix or "maj" = major triad
192
+ // Power
193
+ [/^5$/i, 'power'],
194
+ ];
195
+ /**
196
+ * Parse a chord symbol string into MIDI note numbers.
197
+ *
198
+ * @param symbol - e.g. "Cmaj7", "F#m7", "Bb7", "Dadd9", "G7#9"
199
+ * @param octave - root octave (default 4)
200
+ * @returns Array of MIDI note numbers
201
+ *
202
+ * Examples:
203
+ * parseChordSymbol("Cmaj7") → [60, 64, 67, 71]
204
+ * parseChordSymbol("F#m7") → [66, 69, 73, 76]
205
+ * parseChordSymbol("Bb7") → [58, 62, 65, 68]
206
+ */
207
+ export function parseChordSymbol(symbol, octave = 4) {
208
+ const match = symbol.match(/^([A-Ga-g][#b]?)(.*)$/);
209
+ if (!match)
210
+ throw new Error(`Cannot parse chord symbol: "${symbol}"`);
211
+ const rootPc = parseRoot(match[1]);
212
+ const qualitySuffix = match[2];
213
+ const rootMidi = (octave + 1) * 12 + rootPc;
214
+ // Find matching quality
215
+ for (const [pattern, chordKey] of QUALITY_ALIASES) {
216
+ if (pattern.test(qualitySuffix)) {
217
+ const intervals = CHORDS[chordKey];
218
+ if (!intervals)
219
+ throw new Error(`Unknown chord quality key: "${chordKey}"`);
220
+ return buildChordMidi(rootMidi, intervals);
221
+ }
222
+ }
223
+ throw new Error(`Unknown chord quality: "${qualitySuffix}" in "${symbol}"`);
224
+ }
225
+ // ─── Roman Numeral Parser ──────────────────────────────────────────
226
+ /** Roman numeral values */
227
+ const ROMAN_VALUES = {
228
+ 'i': 0, 'ii': 1, 'iii': 2, 'iv': 3, 'v': 4, 'vi': 5, 'vii': 6,
229
+ };
230
+ /** Map from scale degree to default triad quality in major scale */
231
+ const MAJOR_TRIAD_QUALITIES = ['major', 'minor', 'minor', 'major', 'major', 'minor', 'dim'];
232
+ /** Map from scale degree to default triad quality in natural minor scale */
233
+ const MINOR_TRIAD_QUALITIES = ['minor', 'dim', 'major', 'minor', 'minor', 'major', 'major'];
234
+ /**
235
+ * Parse a Roman numeral chord notation.
236
+ *
237
+ * @param numeral - e.g. "I", "ii", "V7", "bVII", "iv", "#IV", "viidim7"
238
+ * @param key - e.g. "C", "F#", "Bb"
239
+ * @param scale - scale name (default "major")
240
+ * @returns Object with root MIDI number, quality name, and MIDI notes
241
+ *
242
+ * Conventions:
243
+ * - Uppercase = major, lowercase = minor
244
+ * - "dim" suffix = diminished, "aug" = augmented
245
+ * - "7" = dominant 7th (on uppercase) or minor 7th (on lowercase)
246
+ * - "maj7" = major 7th
247
+ * - "b" prefix = lowered a half step, "#" prefix = raised a half step
248
+ */
249
+ export function parseRomanNumeral(numeral, key, scale = 'major') {
250
+ const scaleIntervals = SCALES[scale] || SCALES.major;
251
+ // Extract accidental prefix (b or #)
252
+ let accidental = 0;
253
+ let rest = numeral;
254
+ while (rest.startsWith('b') || rest.startsWith('#')) {
255
+ if (rest.startsWith('b')) {
256
+ accidental -= 1;
257
+ rest = rest.slice(1);
258
+ }
259
+ else {
260
+ accidental += 1;
261
+ rest = rest.slice(1);
262
+ }
263
+ }
264
+ // Extract the roman numeral (case-sensitive)
265
+ const romanMatch = rest.match(/^(I{1,3}V?|IV|V?I{0,3}|i{1,3}v?|iv|v?i{0,3})/i);
266
+ if (!romanMatch || !romanMatch[0])
267
+ throw new Error(`Cannot parse Roman numeral: "${numeral}"`);
268
+ const romanPart = romanMatch[0];
269
+ const suffix = rest.slice(romanPart.length);
270
+ const isUpper = romanPart === romanPart.toUpperCase();
271
+ const degree = ROMAN_VALUES[romanPart.toLowerCase()];
272
+ if (degree === undefined)
273
+ throw new Error(`Unknown Roman numeral: "${romanPart}"`);
274
+ // Calculate root pitch class
275
+ const keyPc = parseRoot(key);
276
+ const scaleDegreeInterval = scaleIntervals[degree % scaleIntervals.length] || 0;
277
+ const rootPc = (keyPc + scaleDegreeInterval + accidental + 12) % 12;
278
+ // Root MIDI in octave 4
279
+ const rootMidi = 48 + rootPc; // octave 3 base for Roman numerals (sounds better)
280
+ // Determine quality from suffix and case
281
+ let quality;
282
+ if (suffix) {
283
+ // Parse explicit suffix
284
+ const suffixLower = suffix.toLowerCase();
285
+ if (suffixLower === 'dim7')
286
+ quality = 'dim7';
287
+ else if (suffixLower === 'dim')
288
+ quality = 'dim';
289
+ else if (suffixLower === 'aug7')
290
+ quality = 'aug7';
291
+ else if (suffixLower === 'aug')
292
+ quality = 'aug';
293
+ else if (suffixLower === 'maj7' || suffixLower === 'M7')
294
+ quality = 'maj7';
295
+ else if (suffixLower === '7')
296
+ quality = isUpper ? 'dom7' : 'min7';
297
+ else if (suffixLower === '9')
298
+ quality = isUpper ? 'dom9' : 'min9';
299
+ else if (suffixLower === '11')
300
+ quality = isUpper ? 'dom11' : 'min11';
301
+ else if (suffixLower === '13')
302
+ quality = isUpper ? 'dom13' : 'min13';
303
+ else if (suffixLower === 'sus2')
304
+ quality = 'sus2';
305
+ else if (suffixLower === 'sus4' || suffixLower === 'sus')
306
+ quality = 'sus4';
307
+ else if (suffixLower === '7sus4')
308
+ quality = '7sus4';
309
+ else if (suffixLower === 'm7b5' || suffixLower === 'ø' || suffixLower === 'halfdim')
310
+ quality = 'm7b5';
311
+ else
312
+ quality = isUpper ? 'major' : 'minor';
313
+ }
314
+ else {
315
+ // Default quality from scale degree
316
+ const qualities = scale === 'natural_minor' || scale === 'harmonic_minor' || scale === 'melodic_minor'
317
+ ? MINOR_TRIAD_QUALITIES
318
+ : MAJOR_TRIAD_QUALITIES;
319
+ quality = isUpper
320
+ ? (qualities[degree] === 'minor' ? 'major' : qualities[degree])
321
+ : (qualities[degree] === 'major' ? 'minor' : qualities[degree]);
322
+ // But respect the case convention: uppercase = major, lowercase = minor
323
+ if (isUpper && quality !== 'dim' && quality !== 'aug')
324
+ quality = 'major';
325
+ if (!isUpper && quality !== 'dim' && quality !== 'aug')
326
+ quality = 'minor';
327
+ }
328
+ const intervals = CHORDS[quality];
329
+ if (!intervals)
330
+ throw new Error(`Unknown chord quality: "${quality}"`);
331
+ const notes = buildChordMidi(rootMidi, intervals);
332
+ return { root: rootMidi, quality, notes };
333
+ }
334
+ // ─── Progression Parser ────────────────────────────────────────────
335
+ /**
336
+ * Parse a chord progression string into arrays of MIDI notes.
337
+ *
338
+ * Accepts two formats:
339
+ * 1. Roman numerals: "I vi IV V"
340
+ * 2. Chord symbols: "Cmaj7 Am7 Fmaj7 G7"
341
+ *
342
+ * @param progression - space-separated chord tokens
343
+ * @param key - key root note (e.g. "C", "F#")
344
+ * @param scale - scale name (default "major")
345
+ * @param octave - root octave for chord symbols (default 4)
346
+ * @returns Array of MIDI note arrays, one per chord
347
+ */
348
+ export function parseProgression(progression, key, scale = 'major', octave = 4) {
349
+ const tokens = progression.trim().split(/\s+/);
350
+ return tokens.map(token => {
351
+ // Detect Roman numeral: starts with optional accidental + roman chars
352
+ if (/^[#b]*[IiVv]/.test(token)) {
353
+ return parseRomanNumeral(token, key, scale).notes;
354
+ }
355
+ // Otherwise treat as chord symbol
356
+ return parseChordSymbol(token, octave);
357
+ });
358
+ }
359
+ // ─── Voice Leading / Voicings ──────────────────────────────────────
360
+ /**
361
+ * Apply a voicing to a set of MIDI notes.
362
+ *
363
+ * @param notes - Input chord tones (root position, ascending)
364
+ * @param voicing - Voicing strategy
365
+ * @returns Re-voiced MIDI notes
366
+ *
367
+ * Voicings:
368
+ * - close: stack notes in ascending order within one octave
369
+ * - open: drop the second note from bottom down an octave
370
+ * - drop2: drop the second note from the top down an octave
371
+ * - drop3: drop the third note from the top down an octave
372
+ * - spread: distribute notes across 2-3 octaves
373
+ * - shell: root + 3rd (or sus) + 7th only
374
+ */
375
+ export function voiceChord(notes, voicing) {
376
+ if (notes.length === 0)
377
+ return [];
378
+ const sorted = [...notes].sort((a, b) => a - b);
379
+ switch (voicing) {
380
+ case 'close':
381
+ return sorted;
382
+ case 'open': {
383
+ if (sorted.length < 3)
384
+ return sorted;
385
+ const result = [...sorted];
386
+ // Drop the second note from bottom down an octave
387
+ result[1] = result[1] - 12;
388
+ return result.sort((a, b) => a - b);
389
+ }
390
+ case 'drop2': {
391
+ if (sorted.length < 3)
392
+ return sorted;
393
+ const result = [...sorted];
394
+ // Second from the top drops an octave
395
+ const dropIdx = result.length - 2;
396
+ result[dropIdx] = result[dropIdx] - 12;
397
+ return result.sort((a, b) => a - b);
398
+ }
399
+ case 'drop3': {
400
+ if (sorted.length < 4)
401
+ return voiceChord(sorted, 'drop2'); // fallback
402
+ const result = [...sorted];
403
+ // Third from the top drops an octave
404
+ const dropIdx = result.length - 3;
405
+ result[dropIdx] = result[dropIdx] - 12;
406
+ return result.sort((a, b) => a - b);
407
+ }
408
+ case 'spread': {
409
+ // Distribute across 2-3 octaves from the root
410
+ const root = sorted[0];
411
+ const intervals = sorted.map(n => n - root);
412
+ const result = [root];
413
+ for (let i = 1; i < intervals.length; i++) {
414
+ // Alternate between adding 12 and keeping, spreading upward
415
+ const octaveBoost = i <= 1 ? 0 : (i <= 3 ? 12 : 24);
416
+ result.push(root + intervals[i] + octaveBoost);
417
+ }
418
+ return result.sort((a, b) => a - b);
419
+ }
420
+ case 'shell': {
421
+ // Root + 3rd (or 2nd/4th for sus) + 7th
422
+ if (sorted.length < 3)
423
+ return sorted;
424
+ const root = sorted[0];
425
+ const intervals = sorted.map(n => ((n - root) % 12 + 12) % 12);
426
+ // Find the "3rd" voice (interval 2-5 semitones from root)
427
+ const third = sorted.find((_, i) => intervals[i] >= 2 && intervals[i] <= 5);
428
+ // Find the "7th" voice (interval 9-11 semitones from root)
429
+ const seventh = sorted.find((_, i) => intervals[i] >= 9 && intervals[i] <= 11);
430
+ const shell = [root];
431
+ if (third !== undefined)
432
+ shell.push(third);
433
+ if (seventh !== undefined)
434
+ shell.push(seventh);
435
+ // If no 7th found, include the 5th as fallback
436
+ if (seventh === undefined) {
437
+ const fifth = sorted.find((_, i) => intervals[i] >= 6 && intervals[i] <= 8);
438
+ if (fifth !== undefined)
439
+ shell.push(fifth);
440
+ }
441
+ return shell.sort((a, b) => a - b);
442
+ }
443
+ default:
444
+ return sorted;
445
+ }
446
+ }
447
+ // ─── Arpeggiator ───────────────────────────────────────────────────
448
+ /**
449
+ * Arpeggiate a chord into a sequence of MidiNote events.
450
+ *
451
+ * @param notes - MIDI note numbers to arpeggiate
452
+ * @param pattern - Direction pattern
453
+ * @param divisions - Number of notes to generate
454
+ * @param durationBeats - Total duration in beats
455
+ * @returns Array of MidiNote events
456
+ */
457
+ export function arpeggiate(notes, pattern, divisions, durationBeats) {
458
+ if (notes.length === 0 || divisions <= 0)
459
+ return [];
460
+ const sorted = [...notes].sort((a, b) => a - b);
461
+ const stepDuration = durationBeats / divisions;
462
+ // Build the pitch sequence for one cycle
463
+ let sequence;
464
+ switch (pattern) {
465
+ case 'up':
466
+ sequence = sorted;
467
+ break;
468
+ case 'down':
469
+ sequence = [...sorted].reverse();
470
+ break;
471
+ case 'updown': {
472
+ // Up then down, excluding endpoints to avoid double-hits
473
+ const up = [...sorted];
474
+ const down = sorted.length > 2
475
+ ? [...sorted].reverse().slice(1, -1)
476
+ : [...sorted].reverse();
477
+ sequence = [...up, ...down];
478
+ break;
479
+ }
480
+ case 'random':
481
+ // Deterministic pseudo-shuffle based on index
482
+ sequence = sorted;
483
+ break;
484
+ }
485
+ const result = [];
486
+ for (let i = 0; i < divisions; i++) {
487
+ let pitch;
488
+ if (pattern === 'random') {
489
+ // Simple deterministic "random" using modular arithmetic
490
+ const idx = (i * 7 + 3) % sorted.length;
491
+ pitch = sorted[idx];
492
+ }
493
+ else {
494
+ pitch = sequence[i % sequence.length];
495
+ }
496
+ // Slight velocity variation for musicality
497
+ const baseVelocity = 80;
498
+ const accentBoost = (i % sequence.length === 0) ? 20 : 0;
499
+ const velocity = Math.min(127, baseVelocity + accentBoost);
500
+ result.push({
501
+ pitch,
502
+ start: i * stepDuration,
503
+ duration: stepDuration * 0.9, // 90% gate for legato feel
504
+ velocity,
505
+ });
506
+ }
507
+ return result;
508
+ }
509
+ // ─── Named Progressions (50+) ──────────────────────────────────────
510
+ export const NAMED_PROGRESSIONS = {
511
+ // ── Pop / Rock ──
512
+ axis: { name: 'Axis of Awesome', numerals: 'I V vi IV', description: 'Most popular pop progression (Let It Be, No Woman No Cry, etc.)' },
513
+ fifties: { name: '50s Doo-Wop', numerals: 'I vi IV V', description: '50s rock and doo-wop standard' },
514
+ sensitive: { name: 'Sensitive Female', numerals: 'vi IV I V', description: 'Emotional pop ballad (Safe & Sound, Zombie)' },
515
+ pop_punk: { name: 'Pop Punk', numerals: 'I V vi IV', description: 'Standard pop-punk and rock anthem' },
516
+ pachelbel: { name: 'Pachelbel Canon', numerals: 'I V vi iii IV I IV V', description: 'Pachelbel\'s Canon in D — endlessly borrowed' },
517
+ creep: { name: 'Creep (Radiohead)', numerals: 'I III IV iv', description: 'Chromatic major-to-minor IV movement' },
518
+ wonderwall: { name: 'Wonderwall', numerals: 'vi IV I V', description: 'Oasis-style Britpop (vi starting)' },
519
+ dont_stop: { name: 'Don\'t Stop Believin\'', numerals: 'I V vi IV', description: 'Classic arena-rock anthem progression' },
520
+ despacito: { name: 'Despacito', numerals: 'vi IV I V', description: 'Reggaeton / Latin pop staple' },
521
+ let_it_be: { name: 'Let It Be', numerals: 'I V vi IV', description: 'Beatles classic — the axis progression' },
522
+ // ── Blues ──
523
+ twelve_bar_blues: { name: '12-Bar Blues', numerals: 'I I I I IV IV I I V IV I V', description: 'Standard 12-bar blues form' },
524
+ minor_blues: { name: 'Minor Blues', numerals: 'i i i i iv iv i i v iv i v', description: '12-bar minor blues' },
525
+ quick_change_blues: { name: 'Quick Change Blues', numerals: 'I IV I I IV IV I I V IV I V', description: '12-bar with quick IV in bar 2' },
526
+ eight_bar_blues: { name: '8-Bar Blues', numerals: 'I I IV IV I V I V', description: 'Shorter 8-bar blues form' },
527
+ // ── Jazz ──
528
+ jazz_ii_v_i: { name: 'Jazz ii-V-I', numerals: 'ii7 V7 Imaj7', description: 'The foundational jazz cadence' },
529
+ jazz_turnaround: { name: 'Jazz Turnaround', numerals: 'Imaj7 vi7 ii7 V7', description: 'Standard jazz turnaround / rhythm changes A' },
530
+ rhythm_changes: { name: 'Rhythm Changes', numerals: 'Imaj7 vi7 ii7 V7', description: 'I Got Rhythm — Gershwin / bebop standard' },
531
+ coltrane_changes: { name: 'Coltrane Changes', numerals: 'Imaj7 bIIImaj7 Vmaj7', description: 'Giant Steps — Coltrane\'s symmetric cycle' },
532
+ backdoor: { name: 'Backdoor ii-V', numerals: 'iv bVII7 I', description: 'Backdoor resolution, softer than V7-I' },
533
+ bird_blues: { name: 'Bird Blues', numerals: 'Imaj7 iv7 bVII7 Imaj7 ii7 V7', description: 'Charlie Parker\'s reharmonized blues' },
534
+ minor_ii_v_i: { name: 'Minor ii-V-i', numerals: 'ii7 V7 i', description: 'Jazz minor cadence with half-dim ii' },
535
+ lady_bird: { name: 'Lady Bird', numerals: 'Imaj7 bIIImaj7 IVmaj7 bVImaj7', description: 'Tadd Dameron — chromatic turnaround' },
536
+ so_what: { name: 'So What', numerals: 'i i i i i i i i bII bII bII bII bII bII bII i', description: 'Miles Davis modal jazz — Dm to Ebm' },
537
+ autumn_leaves: { name: 'Autumn Leaves', numerals: 'ii7 V7 Imaj7 IVmaj7 vii7 III7 vi', description: 'Jazz standard — descending cycle' },
538
+ // ── Classical ──
539
+ classical_cadence: { name: 'Classical Cadence', numerals: 'I IV V I', description: 'Simple authentic cadence (classical)' },
540
+ deceptive: { name: 'Deceptive Cadence', numerals: 'I IV V vi', description: 'Expected resolution to I, goes to vi instead' },
541
+ plagal: { name: 'Plagal Cadence', numerals: 'I IV I', description: 'Amen cadence — IV to I' },
542
+ andalusian: { name: 'Andalusian Cadence', numerals: 'i bVII bVI V', description: 'Flamenco / Mediterranean descending cadence' },
543
+ neapolitan: { name: 'Neapolitan', numerals: 'i bII V i', description: 'Neapolitan 6th — bII approach to V' },
544
+ picardy: { name: 'Picardy Third', numerals: 'iv V I', description: 'Minor piece resolving to major I' },
545
+ circle_of_fifths: { name: 'Circle of Fifths', numerals: 'vi ii V I', description: 'Descending fifths — Baroque standard' },
546
+ romanesca: { name: 'Romanesca', numerals: 'III bVII i V', description: 'Renaissance ground bass pattern' },
547
+ lament: { name: 'Lament Bass', numerals: 'i i7 bVII bVI', description: 'Descending chromatic bass — passacaglia' },
548
+ // ── Modal / Film ──
549
+ modal_interchange: { name: 'Modal Interchange', numerals: 'I bVII IV iv', description: 'Borrowing bVII and iv from parallel minor' },
550
+ epic_film: { name: 'Epic Film', numerals: 'i bVI bIII bVII', description: 'Cinematic minor progression (Hans Zimmer feel)' },
551
+ dorian_vamp: { name: 'Dorian Vamp', numerals: 'i IV', description: 'Simple Dorian two-chord groove (So What, Oye Como Va)' },
552
+ mixolydian_vamp: { name: 'Mixolydian Vamp', numerals: 'I bVII', description: 'Two-chord rock/folk groove (Sweet Home Alabama feel)' },
553
+ lydian_float: { name: 'Lydian Float', numerals: 'I II', description: 'Lydian two-chord — dreamy, ambiguous' },
554
+ phrygian_dark: { name: 'Phrygian Dark', numerals: 'i bII', description: 'Dark Phrygian two-chord (metal / flamenco)' },
555
+ // ── Japanese / K-Pop / Anime ──
556
+ royal_road: { name: 'Royal Road (Ouji)', numerals: 'IV V iii vi', description: 'J-pop / anime staple — the "ouji-shiki" progression' },
557
+ jpop_classic: { name: 'J-Pop Classic', numerals: 'IV V I vi', description: 'Standard J-pop cadence' },
558
+ kpop_vi: { name: 'K-Pop Minor Start', numerals: 'vi IV I V', description: 'K-pop and anime emotional opening' },
559
+ anime_sad: { name: 'Anime Sadness', numerals: 'vi V IV V', description: 'Sad anime scene — stepwise minor descent' },
560
+ // ── EDM / Electronic ──
561
+ edm_anthem: { name: 'EDM Anthem', numerals: 'vi IV I V', description: 'Festival EDM build-and-drop' },
562
+ trance_gate: { name: 'Trance Gate', numerals: 'i bVII bVI V', description: 'Trance uplifter — Andalusian variant' },
563
+ house_vamp: { name: 'House Vamp', numerals: 'i bVII bVI bVII', description: 'Deep house hypnotic minor loop' },
564
+ // ── Reggae / Latin ──
565
+ reggae_one_drop: { name: 'Reggae One-Drop', numerals: 'I IV V IV', description: 'Bob Marley style — relaxed reggae groove' },
566
+ bossa_nova: { name: 'Bossa Nova', numerals: 'Imaj7 ii7 iii7 bIIImaj7 ii7 V7 Imaj7', description: 'Girl from Ipanema — bossa standard' },
567
+ son_montuno: { name: 'Son Montuno', numerals: 'I IV V IV', description: 'Cuban son / salsa groove' },
568
+ // ── R&B / Soul / Gospel ──
569
+ soul_turnaround: { name: 'Soul Turnaround', numerals: 'I vi ii V', description: 'Classic Motown / soul turnaround' },
570
+ gospel_shout: { name: 'Gospel Shout', numerals: 'I I7 IV iv I V I', description: 'Gospel with chromatic iv — church shout' },
571
+ neo_soul: { name: 'Neo-Soul', numerals: 'Imaj7 iii7 vi7 ii7 V7', description: 'Erykah Badu / D\'Angelo smooth cycle' },
572
+ // ── Ragtime / Stride ──
573
+ ragtime: { name: 'Ragtime', numerals: 'I I IV iv I V I', description: 'Classic ragtime with chromatic passing iv' },
574
+ entertainer: { name: 'The Entertainer', numerals: 'I V I IV I V I', description: 'Scott Joplin stride pattern' },
575
+ // ── Metal / Prog ──
576
+ metal_power: { name: 'Metal Power', numerals: 'i bVII bVI bVII', description: 'Power-chord metal riff pattern' },
577
+ prog_chromatic: { name: 'Prog Chromatic', numerals: 'I bII I bVII', description: 'Progressive rock chromatic movement' },
578
+ djent: { name: 'Djent', numerals: 'i bII bVII i', description: 'Modern prog-metal — Meshuggah/Periphery feel' },
579
+ // ── Country / Folk ──
580
+ country_walk: { name: 'Country Walk', numerals: 'I I IV IV V IV I V', description: 'Nashville-style 8-bar walking progression' },
581
+ folk_circle: { name: 'Folk Circle', numerals: 'I V vi iii IV I ii V', description: 'Folk — extended circle of fifths' },
582
+ three_chord_wonder: { name: 'Three-Chord Wonder', numerals: 'I IV V I', description: 'The simplest rock/country/punk progression' },
583
+ // ── Funk ──
584
+ funk_one_chord: { name: 'Funk One-Chord', numerals: 'I7', description: 'James Brown — one-chord funk (rhythm is everything)' },
585
+ funk_cycle: { name: 'Funk Cycle', numerals: 'I7 IV7 I7 V7', description: 'Funk with dominant 7ths throughout' },
586
+ };
587
+ // ─── Rhythm Patterns ───────────────────────────────────────────────
588
+ // Beat positions within a bar (in quarter notes, 4/4 time).
589
+ // 0 = beat 1, 1 = beat 2, 2 = beat 3, 3 = beat 4.
590
+ export const RHYTHM_PATTERNS = {
591
+ // Standard divisions
592
+ whole: [0],
593
+ half: [0, 2],
594
+ quarter: [0, 1, 2, 3],
595
+ eighth: [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5],
596
+ sixteenth: [0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3, 3.25, 3.5, 3.75],
597
+ triplet: [0, 1 / 3, 2 / 3, 1, 4 / 3, 5 / 3, 2, 7 / 3, 8 / 3, 3, 10 / 3, 11 / 3],
598
+ // Dotted patterns
599
+ dotted_quarter: [0, 1.5, 3],
600
+ dotted_eighth: [0, 0.75, 1.5, 2.25, 3],
601
+ // Syncopation
602
+ offbeat_eighth: [0.5, 1.5, 2.5, 3.5],
603
+ anticipated: [0, 1, 1.5, 2.5, 3],
604
+ charleston: [0, 1.5],
605
+ habanera: [0, 1.5, 2, 3],
606
+ // Swing / shuffle
607
+ swing: [0, 2 / 3, 1, 5 / 3, 2, 8 / 3, 3, 11 / 3], // triplet feel
608
+ shuffle: [0, 2 / 3, 1, 5 / 3, 2, 8 / 3, 3, 11 / 3], // same as swing
609
+ half_time_shuffle: [0, 2 / 3, 2, 8 / 3], // Purdie shuffle, half as dense
610
+ // Latin / world
611
+ clave_son: [0, 1.5, 2.5, 3, 3.5], // 3-2 son clave (within one bar approximation)
612
+ clave_rumba: [0, 1.5, 2.75, 3, 3.5], // 3-2 rumba clave
613
+ tresillo: [0, 1.5, 3], // 3-3-2 feel
614
+ cinquillo: [0, 0.5, 1.5, 2, 3], // Afro-Cuban five-stroke
615
+ // Montuno / reggaeton
616
+ reggaeton: [0, 0.75, 1.5, 2.25, 3], // dembow-ish
617
+ dembow: [0, 0.75, 1.5, 2.25, 3],
618
+ };
619
+ // ─── Scale Quantization ────────────────────────────────────────────
620
+ /**
621
+ * Snap a MIDI note to the nearest note in the given scale and key.
622
+ *
623
+ * @param note - MIDI note number to quantize
624
+ * @param key - Root pitch class (0-11) or note name
625
+ * @param scale - Scale name from SCALES
626
+ * @returns Nearest MIDI note in the scale
627
+ */
628
+ export function quantizeToScale(note, key, scale) {
629
+ const keyPc = typeof key === 'string' ? parseRoot(key) : key;
630
+ const scaleIntervals = SCALES[scale] || SCALES.major;
631
+ // Build the set of valid pitch classes
632
+ const validPcs = new Set(scaleIntervals.map(i => (keyPc + i) % 12));
633
+ const pc = ((note % 12) + 12) % 12;
634
+ if (validPcs.has(pc))
635
+ return note;
636
+ // Find nearest valid pitch class
637
+ let bestDist = Infinity;
638
+ let bestOffset = 0;
639
+ for (let offset = 1; offset <= 6; offset++) {
640
+ if (validPcs.has((pc + offset) % 12)) {
641
+ if (offset < bestDist) {
642
+ bestDist = offset;
643
+ bestOffset = offset;
644
+ }
645
+ break;
646
+ }
647
+ }
648
+ for (let offset = 1; offset <= 6; offset++) {
649
+ if (validPcs.has(((pc - offset) % 12 + 12) % 12)) {
650
+ if (offset < bestDist) {
651
+ bestDist = offset;
652
+ bestOffset = -offset;
653
+ }
654
+ break;
655
+ }
656
+ }
657
+ // Tie-break: prefer rounding down
658
+ if (bestDist === Infinity)
659
+ return note; // chromatic or empty scale, no change
660
+ return note + bestOffset;
661
+ }
662
+ // ─── Key Detection ─────────────────────────────────────────────────
663
+ /**
664
+ * Detect the most likely key and scale for a set of MIDI notes.
665
+ * Uses the Krumhansl-Schmuckler key-finding algorithm (simplified).
666
+ *
667
+ * @param notes - Array of MIDI note numbers
668
+ * @returns Best-fit key, scale, and confidence (0-1)
669
+ */
670
+ export function detectKey(notes) {
671
+ if (notes.length === 0)
672
+ return { key: 'C', scale: 'major', confidence: 0 };
673
+ // Count pitch class occurrences
674
+ const pcCounts = new Array(12).fill(0);
675
+ for (const n of notes) {
676
+ pcCounts[((n % 12) + 12) % 12]++;
677
+ }
678
+ const total = notes.length;
679
+ // Krumhansl-Kessler profiles (empirical key profiles)
680
+ const majorProfile = [6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88];
681
+ const minorProfile = [6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17];
682
+ /** Pearson correlation between two arrays */
683
+ function correlate(a, b) {
684
+ const n = a.length;
685
+ const meanA = a.reduce((s, v) => s + v, 0) / n;
686
+ const meanB = b.reduce((s, v) => s + v, 0) / n;
687
+ let num = 0, denA = 0, denB = 0;
688
+ for (let i = 0; i < n; i++) {
689
+ const da = a[i] - meanA;
690
+ const db = b[i] - meanB;
691
+ num += da * db;
692
+ denA += da * da;
693
+ denB += db * db;
694
+ }
695
+ const den = Math.sqrt(denA * denB);
696
+ return den === 0 ? 0 : num / den;
697
+ }
698
+ let bestKey = 0;
699
+ let bestScale = 'major';
700
+ let bestScore = -Infinity;
701
+ for (let root = 0; root < 12; root++) {
702
+ // Rotate pitch class counts so that `root` is index 0
703
+ const rotated = Array.from({ length: 12 }, (_, i) => pcCounts[(root + i) % 12]);
704
+ const majScore = correlate(rotated, majorProfile);
705
+ const minScore = correlate(rotated, minorProfile);
706
+ if (majScore > bestScore) {
707
+ bestScore = majScore;
708
+ bestKey = root;
709
+ bestScale = 'major';
710
+ }
711
+ if (minScore > bestScore) {
712
+ bestScore = minScore;
713
+ bestKey = root;
714
+ bestScale = 'natural_minor';
715
+ }
716
+ }
717
+ // Confidence: map correlation from [0,1] range
718
+ const confidence = Math.max(0, Math.min(1, (bestScore + 1) / 2));
719
+ const keyName = NOTE_NAMES_SHARP[bestKey];
720
+ return { key: keyName, scale: bestScale, confidence: Math.round(confidence * 1000) / 1000 };
721
+ }
722
+ // ─── General MIDI Drum Map ─────────────────────────────────────────
723
+ export const GM_DRUMS = {
724
+ // Bass drums
725
+ kick: 36,
726
+ kick_alt: 35,
727
+ bass_drum: 36,
728
+ // Snares
729
+ snare: 38,
730
+ snare_alt: 40,
731
+ electric_snare: 40,
732
+ rim: 37,
733
+ rimshot: 37,
734
+ side_stick: 37,
735
+ cross_stick: 37,
736
+ // Hi-hats
737
+ closed_hihat: 42,
738
+ hihat_closed: 42,
739
+ pedal_hihat: 44,
740
+ open_hihat: 46,
741
+ hihat_open: 46,
742
+ // Toms
743
+ low_floor_tom: 41,
744
+ high_floor_tom: 43,
745
+ low_tom: 45,
746
+ low_mid_tom: 47,
747
+ hi_mid_tom: 48,
748
+ high_tom: 50,
749
+ // Cymbals
750
+ crash: 49,
751
+ crash_1: 49,
752
+ crash_2: 57,
753
+ ride: 51,
754
+ ride_bell: 53,
755
+ splash: 55,
756
+ china: 52,
757
+ // Percussion
758
+ clap: 39,
759
+ handclap: 39,
760
+ tambourine: 54,
761
+ cowbell: 56,
762
+ vibraslap: 58,
763
+ // Latin
764
+ bongo_high: 60,
765
+ bongo_low: 61,
766
+ conga_muted: 62,
767
+ conga_open: 63,
768
+ conga_low: 64,
769
+ timbale_high: 65,
770
+ timbale_low: 66,
771
+ agogo_high: 67,
772
+ agogo_low: 68,
773
+ cabasa: 69,
774
+ maracas: 70,
775
+ guiro_short: 73,
776
+ guiro_long: 74,
777
+ claves: 75,
778
+ // Woodblock / triangle
779
+ woodblock_high: 76,
780
+ woodblock_low: 77,
781
+ triangle_muted: 80,
782
+ triangle_open: 81,
783
+ shaker: 82,
784
+ };
785
+ // ─── Genre Drum Patterns ───────────────────────────────────────────
786
+ // Pattern arrays indicate which 16th-note subdivisions (0-15) have hits.
787
+ // 0 = beat 1, 4 = beat 2, 8 = beat 3, 12 = beat 4.
788
+ export const GENRE_DRUM_PATTERNS = {
789
+ house: {
790
+ bpm: [120, 130],
791
+ pattern: {
792
+ kick: [0, 4, 8, 12], // four on the floor
793
+ clap: [4, 12], // clap on 2 & 4
794
+ closed_hihat: [0, 2, 4, 6, 8, 10, 12, 14], // steady 8ths
795
+ open_hihat: [3, 7, 11, 15], // offbeat opens
796
+ },
797
+ },
798
+ techno: {
799
+ bpm: [130, 145],
800
+ pattern: {
801
+ kick: [0, 4, 8, 12],
802
+ clap: [4, 12],
803
+ closed_hihat: [0, 2, 4, 6, 8, 10, 12, 14],
804
+ ride: [2, 6, 10, 14],
805
+ },
806
+ },
807
+ hiphop: {
808
+ bpm: [85, 100],
809
+ pattern: {
810
+ kick: [0, 5, 8, 13], // syncopated boom-bap
811
+ snare: [4, 12],
812
+ closed_hihat: [0, 2, 4, 6, 8, 10, 12, 14],
813
+ open_hihat: [7],
814
+ },
815
+ },
816
+ trap: {
817
+ bpm: [130, 170],
818
+ pattern: {
819
+ kick: [0, 7, 8], // sparse kick, half-time feel
820
+ snare: [8], // snare on beat 3 (half-time)
821
+ closed_hihat: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], // 16th hi-hats
822
+ open_hihat: [6, 14],
823
+ clap: [8],
824
+ },
825
+ },
826
+ dnb: {
827
+ bpm: [160, 180],
828
+ pattern: {
829
+ kick: [0, 10], // syncopated kick
830
+ snare: [4, 12], // backbeat
831
+ closed_hihat: [0, 2, 4, 6, 8, 10, 12, 14],
832
+ ride: [1, 3, 5, 7, 9, 11, 13, 15],
833
+ },
834
+ },
835
+ reggaeton: {
836
+ bpm: [88, 100],
837
+ pattern: {
838
+ kick: [0, 3, 4, 7, 8, 11, 12, 15], // dembow kick
839
+ snare: [3, 7, 11, 15], // dembow snare
840
+ closed_hihat: [0, 2, 4, 6, 8, 10, 12, 14],
841
+ rim: [3, 7, 11, 15],
842
+ },
843
+ },
844
+ jazz: {
845
+ bpm: [100, 180],
846
+ pattern: {
847
+ kick: [0, 10], // feathered bass drum
848
+ snare: [7, 14], // ghost notes / comping
849
+ ride: [0, 3, 4, 7, 8, 11, 12, 15], // swing ride pattern
850
+ closed_hihat: [4, 12], // hi-hat on 2 & 4
851
+ },
852
+ },
853
+ rock: {
854
+ bpm: [100, 140],
855
+ pattern: {
856
+ kick: [0, 8], // beats 1 & 3
857
+ snare: [4, 12], // beats 2 & 4
858
+ closed_hihat: [0, 2, 4, 6, 8, 10, 12, 14], // straight 8ths
859
+ crash: [0], // crash on 1 (start of pattern)
860
+ },
861
+ },
862
+ pop: {
863
+ bpm: [100, 130],
864
+ pattern: {
865
+ kick: [0, 6, 8], // kick with a push
866
+ snare: [4, 12], // standard backbeat
867
+ closed_hihat: [0, 2, 4, 6, 8, 10, 12, 14],
868
+ tambourine: [4, 12],
869
+ },
870
+ },
871
+ lofi: {
872
+ bpm: [70, 90],
873
+ pattern: {
874
+ kick: [0, 5, 8, 13], // boom bap, slightly loose
875
+ snare: [4, 12],
876
+ closed_hihat: [0, 2, 4, 6, 8, 10, 12, 14],
877
+ open_hihat: [6, 14],
878
+ rim: [3, 11], // ghost rimshot
879
+ },
880
+ },
881
+ ambient: {
882
+ bpm: [60, 90],
883
+ pattern: {
884
+ kick: [0], // minimal — one kick per bar
885
+ ride: [0, 8], // sparse ride
886
+ shaker: [0, 4, 8, 12], // gentle pulse
887
+ },
888
+ },
889
+ funk: {
890
+ bpm: [95, 115],
891
+ pattern: {
892
+ kick: [0, 3, 6, 8, 11], // syncopated funk kick
893
+ snare: [4, 12], // backbeat
894
+ closed_hihat: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], // 16th hats
895
+ open_hihat: [7, 15],
896
+ },
897
+ },
898
+ bossa_nova: {
899
+ bpm: [120, 145],
900
+ pattern: {
901
+ kick: [0, 5, 8, 13],
902
+ rim: [3, 6, 9, 12, 15], // cross-stick pattern
903
+ closed_hihat: [0, 2, 4, 6, 8, 10, 12, 14],
904
+ shaker: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
905
+ },
906
+ },
907
+ reggae: {
908
+ bpm: [65, 85],
909
+ pattern: {
910
+ kick: [0, 10], // one-drop kick
911
+ snare: [12], // rim on beat 3 (one-drop)
912
+ closed_hihat: [0, 2, 4, 6, 8, 10, 12, 14],
913
+ rim: [12],
914
+ },
915
+ },
916
+ afrobeat: {
917
+ bpm: [100, 130],
918
+ pattern: {
919
+ kick: [0, 6, 8, 14],
920
+ snare: [4, 12],
921
+ closed_hihat: [0, 2, 4, 6, 8, 10, 12, 14],
922
+ open_hihat: [3, 11],
923
+ shaker: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
924
+ claves: [0, 3, 6, 10, 12], // Afrobeat bell pattern
925
+ },
926
+ },
927
+ drill: {
928
+ bpm: [140, 150],
929
+ pattern: {
930
+ kick: [0, 3, 8, 11],
931
+ snare: [6, 14], // displaced snare
932
+ closed_hihat: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
933
+ open_hihat: [5, 13],
934
+ },
935
+ },
936
+ };
937
+ // ─── Utility Exports ───────────────────────────────────────────────
938
+ /**
939
+ * Get scale note names for a given key and scale.
940
+ * Useful for display and reference.
941
+ */
942
+ export function getScaleNotes(key, scale, octave = 4) {
943
+ const intervals = SCALES[scale] || SCALES.major;
944
+ const keyPc = parseRoot(key);
945
+ const preferFlats = FLAT_KEYS.has(key);
946
+ const rootMidi = (octave + 1) * 12 + keyPc;
947
+ const midi = intervals.map(i => rootMidi + i);
948
+ const names = midi.map(m => midiToNoteName(m, preferFlats));
949
+ return { names, midi };
950
+ }
951
+ /**
952
+ * Transpose an array of MIDI notes by a given number of semitones.
953
+ */
954
+ export function transpose(notes, semitones) {
955
+ return notes.map(n => n + semitones);
956
+ }
957
+ /**
958
+ * Invert a chord (rotate the lowest note up an octave).
959
+ * @param notes - MIDI notes (sorted ascending)
960
+ * @param inversion - Number of inversions (1 = first, 2 = second, etc.)
961
+ */
962
+ export function invertChord(notes, inversion) {
963
+ const result = [...notes].sort((a, b) => a - b);
964
+ for (let i = 0; i < inversion && i < result.length; i++) {
965
+ result.push(result.shift() + 12);
966
+ }
967
+ return result;
968
+ }
969
+ /**
970
+ * Calculate the interval name between two MIDI notes.
971
+ */
972
+ export function intervalName(semitones) {
973
+ const names = {
974
+ 0: 'unison', 1: 'minor 2nd', 2: 'major 2nd', 3: 'minor 3rd',
975
+ 4: 'major 3rd', 5: 'perfect 4th', 6: 'tritone', 7: 'perfect 5th',
976
+ 8: 'minor 6th', 9: 'major 6th', 10: 'minor 7th', 11: 'major 7th',
977
+ 12: 'octave',
978
+ };
979
+ const mod = ((semitones % 12) + 12) % 12;
980
+ const octaves = Math.floor(Math.abs(semitones) / 12);
981
+ const base = names[mod] || `${mod} semitones`;
982
+ if (octaves > 1)
983
+ return `${base} + ${octaves - 1} octave${octaves > 2 ? 's' : ''}`;
984
+ if (semitones === 12)
985
+ return 'octave';
986
+ if (semitones > 12)
987
+ return `${base} + octave`;
988
+ return base;
989
+ }
990
+ /**
991
+ * Get the available scale names.
992
+ */
993
+ export function listScales() {
994
+ return Object.keys(SCALES);
995
+ }
996
+ /**
997
+ * Get the available chord quality names.
998
+ */
999
+ export function listChords() {
1000
+ return Object.keys(CHORDS);
1001
+ }
1002
+ /**
1003
+ * Get the available named progressions.
1004
+ */
1005
+ export function listProgressions() {
1006
+ return Object.keys(NAMED_PROGRESSIONS);
1007
+ }
1008
+ /**
1009
+ * Get the available drum pattern genres.
1010
+ */
1011
+ export function listDrumPatterns() {
1012
+ return Object.keys(GENRE_DRUM_PATTERNS);
1013
+ }
1014
+ /**
1015
+ * Get the available rhythm patterns.
1016
+ */
1017
+ export function listRhythmPatterns() {
1018
+ return Object.keys(RHYTHM_PATTERNS);
1019
+ }
1020
+ //# sourceMappingURL=music-theory.js.map