@kernel.chat/kbot 3.49.0 → 3.50.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/dist/agent-teams.d.ts.map +1 -1
- package/dist/agent-teams.js +7 -0
- package/dist/agent-teams.js.map +1 -1
- package/dist/agents/producer.d.ts +21 -0
- package/dist/agents/producer.d.ts.map +1 -0
- package/dist/agents/producer.js +139 -0
- package/dist/agents/producer.js.map +1 -0
- package/dist/agents/specialists.d.ts.map +1 -1
- package/dist/agents/specialists.js +19 -0
- package/dist/agents/specialists.js.map +1 -1
- package/dist/completions.d.ts.map +1 -1
- package/dist/completions.js +1 -0
- package/dist/completions.js.map +1 -1
- package/dist/integrations/ableton-osc.d.ts +146 -0
- package/dist/integrations/ableton-osc.d.ts.map +1 -0
- package/dist/integrations/ableton-osc.js +590 -0
- package/dist/integrations/ableton-osc.js.map +1 -0
- package/dist/learned-router.d.ts.map +1 -1
- package/dist/learned-router.js +20 -0
- package/dist/learned-router.js.map +1 -1
- package/dist/tools/ableton-knowledge.d.ts +2 -0
- package/dist/tools/ableton-knowledge.d.ts.map +1 -0
- package/dist/tools/ableton-knowledge.js +419 -0
- package/dist/tools/ableton-knowledge.js.map +1 -0
- package/dist/tools/ableton.d.ts +2 -0
- package/dist/tools/ableton.d.ts.map +1 -0
- package/dist/tools/ableton.js +769 -0
- package/dist/tools/ableton.js.map +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/music-theory.d.ts +175 -0
- package/dist/tools/music-theory.d.ts.map +1 -0
- package/dist/tools/music-theory.js +1020 -0
- package/dist/tools/music-theory.js.map +1 -0
- 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
|