@pepperhorn/chordl-core 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/audio/midi-export.d.ts +23 -0
- package/dist/audio/midi-export.js +155 -0
- package/dist/audio/midi-export.js.map +1 -0
- package/dist/chord-sheet/codec.d.ts +14 -0
- package/dist/chord-sheet/codec.js +119 -0
- package/dist/chord-sheet/codec.js.map +1 -0
- package/dist/chord-sheet/defaults.d.ts +13 -0
- package/dist/chord-sheet/defaults.js +48 -0
- package/dist/chord-sheet/defaults.js.map +1 -0
- package/dist/chord-sheet/index.d.ts +3 -0
- package/dist/chord-sheet/index.js +4 -0
- package/dist/chord-sheet/index.js.map +1 -0
- package/dist/chord-sheet/schema.d.ts +6 -0
- package/dist/chord-sheet/schema.js +17 -0
- package/dist/chord-sheet/schema.js.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +15 -0
- package/dist/config.js.map +1 -0
- package/dist/engine/auto-fingering.d.ts +36 -0
- package/dist/engine/auto-fingering.js +344 -0
- package/dist/engine/auto-fingering.js.map +1 -0
- package/dist/engine/highlight-mapper.d.ts +3 -0
- package/dist/engine/highlight-mapper.js +46 -0
- package/dist/engine/highlight-mapper.js.map +1 -0
- package/dist/engine/keyboard-layout.d.ts +6 -0
- package/dist/engine/keyboard-layout.js +65 -0
- package/dist/engine/keyboard-layout.js.map +1 -0
- package/dist/engine/note-spelling.d.ts +54 -0
- package/dist/engine/note-spelling.js +103 -0
- package/dist/engine/note-spelling.js.map +1 -0
- package/dist/engine/scale-fingering.d.ts +21 -0
- package/dist/engine/scale-fingering.js +130 -0
- package/dist/engine/scale-fingering.js.map +1 -0
- package/dist/engine/staff-constants.d.ts +16 -0
- package/dist/engine/staff-constants.js +20 -0
- package/dist/engine/staff-constants.js.map +1 -0
- package/dist/engine/staff-glyphs.d.ts +17 -0
- package/dist/engine/staff-glyphs.js +59 -0
- package/dist/engine/staff-glyphs.js.map +1 -0
- package/dist/engine/staff-layout.d.ts +28 -0
- package/dist/engine/staff-layout.js +238 -0
- package/dist/engine/staff-layout.js.map +1 -0
- package/dist/engine/svg-constants.d.ts +19 -0
- package/dist/engine/svg-constants.js +40 -0
- package/dist/engine/svg-constants.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/logging.d.ts +70 -0
- package/dist/logging.js +24 -0
- package/dist/logging.js.map +1 -0
- package/dist/parser/natural-language.d.ts +9 -0
- package/dist/parser/natural-language.js +406 -0
- package/dist/parser/natural-language.js.map +1 -0
- package/dist/parser/progression-parser.d.ts +16 -0
- package/dist/parser/progression-parser.js +97 -0
- package/dist/parser/progression-parser.js.map +1 -0
- package/dist/pipeline.d.ts +33 -0
- package/dist/pipeline.js +203 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/progression/form-templates.d.ts +11 -0
- package/dist/progression/form-templates.js +105 -0
- package/dist/progression/form-templates.js.map +1 -0
- package/dist/progression/index.d.ts +5 -0
- package/dist/progression/index.js +4 -0
- package/dist/progression/index.js.map +1 -0
- package/dist/progression/progression-resolver.d.ts +29 -0
- package/dist/progression/progression-resolver.js +135 -0
- package/dist/progression/progression-resolver.js.map +1 -0
- package/dist/progression/roman-numeral.d.ts +14 -0
- package/dist/progression/roman-numeral.js +78 -0
- package/dist/progression/roman-numeral.js.map +1 -0
- package/dist/resolver/auto-layout.d.ts +20 -0
- package/dist/resolver/auto-layout.js +195 -0
- package/dist/resolver/auto-layout.js.map +1 -0
- package/dist/resolver/chord-resolver.d.ts +8 -0
- package/dist/resolver/chord-resolver.js +172 -0
- package/dist/resolver/chord-resolver.js.map +1 -0
- package/dist/resolver/scale-resolver.d.ts +9 -0
- package/dist/resolver/scale-resolver.js +45 -0
- package/dist/resolver/scale-resolver.js.map +1 -0
- package/dist/themes/boomwhacker.d.ts +2 -0
- package/dist/themes/boomwhacker.js +35 -0
- package/dist/themes/boomwhacker.js.map +1 -0
- package/dist/themes/crf.d.ts +19 -0
- package/dist/themes/crf.js +34 -0
- package/dist/themes/crf.js.map +1 -0
- package/dist/themes/index.d.ts +3 -0
- package/dist/themes/index.js +25 -0
- package/dist/themes/index.js.map +1 -0
- package/dist/themes/simple.d.ts +2 -0
- package/dist/themes/simple.js +13 -0
- package/dist/themes/simple.js.map +1 -0
- package/dist/theory/chord-tones.d.ts +56 -0
- package/dist/theory/chord-tones.js +215 -0
- package/dist/theory/chord-tones.js.map +1 -0
- package/dist/theory/degree-labels.d.ts +31 -0
- package/dist/theory/degree-labels.js +74 -0
- package/dist/theory/degree-labels.js.map +1 -0
- package/dist/types.d.ts +123 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +37 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { normalizeToSharps, PC_SEMITONES as NOTE_SEMITONE } from "./note-spelling";
|
|
2
|
+
/** Max playable span for a single hand: a 12th = 19 semitones */
|
|
3
|
+
const MAX_HAND_SPAN = 19;
|
|
4
|
+
/** Max notes a single hand can play */
|
|
5
|
+
const MAX_HAND_NOTES = 5;
|
|
6
|
+
const BLACK_NOTES = new Set(["C#", "D#", "F#", "G#", "A#"]);
|
|
7
|
+
function normalize(note) {
|
|
8
|
+
return normalizeToSharps(note);
|
|
9
|
+
}
|
|
10
|
+
function isBlack(note) {
|
|
11
|
+
return BLACK_NOTES.has(normalize(note));
|
|
12
|
+
}
|
|
13
|
+
/** Ascending semitone interval between two pitch classes (mod 12). */
|
|
14
|
+
function interval(a, b) {
|
|
15
|
+
const sa = NOTE_SEMITONE[normalize(a)];
|
|
16
|
+
const sb = NOTE_SEMITONE[normalize(b)];
|
|
17
|
+
if (sa == null || sb == null)
|
|
18
|
+
return 0;
|
|
19
|
+
return ((sb - sa) % 12 + 12) % 12;
|
|
20
|
+
}
|
|
21
|
+
/** Total ascending span of a note sequence in semitones (mod 12 per step). */
|
|
22
|
+
function totalSpanMod12(notes) {
|
|
23
|
+
let span = 0;
|
|
24
|
+
for (let i = 1; i < notes.length; i++) {
|
|
25
|
+
span += interval(notes[i - 1], notes[i]);
|
|
26
|
+
}
|
|
27
|
+
return span;
|
|
28
|
+
}
|
|
29
|
+
/** Check if a set of notes is playable by one hand using mod-12 intervals. */
|
|
30
|
+
function isOneHandPlayable(notes) {
|
|
31
|
+
return notes.length <= MAX_HAND_NOTES && totalSpanMod12(notes) <= MAX_HAND_SPAN;
|
|
32
|
+
}
|
|
33
|
+
/** Check if a set of MIDI values is playable by one hand. */
|
|
34
|
+
function isOneHandPlayableMidi(midis) {
|
|
35
|
+
if (midis.length > MAX_HAND_NOTES)
|
|
36
|
+
return false;
|
|
37
|
+
if (midis.length <= 1)
|
|
38
|
+
return true;
|
|
39
|
+
const sorted = [...midis].sort((a, b) => a - b);
|
|
40
|
+
return sorted[sorted.length - 1] - sorted[0] <= MAX_HAND_SPAN;
|
|
41
|
+
}
|
|
42
|
+
// ── Fingering patterns ────────────────────────────────────────────
|
|
43
|
+
/** Standard RH fingering patterns by note count. */
|
|
44
|
+
const RH_PATTERNS = {
|
|
45
|
+
2: [[1, 5], [1, 3], [2, 5]],
|
|
46
|
+
3: [[1, 3, 5], [1, 2, 5], [1, 2, 4], [1, 3, 4], [2, 3, 5], [1, 2, 3]],
|
|
47
|
+
4: [[1, 2, 3, 5], [1, 2, 4, 5], [1, 2, 3, 4], [1, 3, 4, 5], [2, 3, 4, 5]],
|
|
48
|
+
5: [[1, 2, 3, 4, 5]],
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Score a candidate fingering for a set of notes (RH perspective).
|
|
52
|
+
* Lower is better.
|
|
53
|
+
*/
|
|
54
|
+
function scoreFingering(notes, fingers) {
|
|
55
|
+
let penalty = 0;
|
|
56
|
+
for (let i = 0; i < notes.length; i++) {
|
|
57
|
+
if (fingers[i] === 1 && isBlack(notes[i]) && i !== 0) {
|
|
58
|
+
penalty += 10;
|
|
59
|
+
}
|
|
60
|
+
if (fingers[i] === 5 && isBlack(notes[i])) {
|
|
61
|
+
penalty += 2;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (let i = 0; i < notes.length - 1; i++) {
|
|
65
|
+
const semitones = interval(notes[i], notes[i + 1]);
|
|
66
|
+
const fingerGap = Math.abs(fingers[i + 1] - fingers[i]);
|
|
67
|
+
if (semitones >= 5 && fingerGap <= 1) {
|
|
68
|
+
penalty += 5;
|
|
69
|
+
}
|
|
70
|
+
if (semitones <= 1 && fingerGap >= 3) {
|
|
71
|
+
penalty += 3;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (fingers[0] !== 1)
|
|
75
|
+
penalty += 1;
|
|
76
|
+
const last = fingers[fingers.length - 1];
|
|
77
|
+
if (last !== 5 && last !== 4)
|
|
78
|
+
penalty += 1;
|
|
79
|
+
return penalty;
|
|
80
|
+
}
|
|
81
|
+
/** Pick the best fingering pattern for a single hand. */
|
|
82
|
+
function fingerOneHand(notes, hand) {
|
|
83
|
+
const n = notes.length;
|
|
84
|
+
if (n === 0)
|
|
85
|
+
return [];
|
|
86
|
+
if (n === 1)
|
|
87
|
+
return [3];
|
|
88
|
+
const patterns = RH_PATTERNS[n];
|
|
89
|
+
if (!patterns)
|
|
90
|
+
return Array(n).fill(3);
|
|
91
|
+
let bestPattern = patterns[0];
|
|
92
|
+
let bestScore = Infinity;
|
|
93
|
+
for (const pattern of patterns) {
|
|
94
|
+
const score = scoreFingering(notes, pattern);
|
|
95
|
+
if (score < bestScore) {
|
|
96
|
+
bestScore = score;
|
|
97
|
+
bestPattern = pattern;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// LH: mirror via (5+1)-f so 1↔5, 2↔4, 3 stays
|
|
101
|
+
if (hand === "lh") {
|
|
102
|
+
return bestPattern.map((f) => 6 - f);
|
|
103
|
+
}
|
|
104
|
+
return bestPattern;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Assign fingering to chord notes, splitting across hands when needed.
|
|
108
|
+
*
|
|
109
|
+
* @param notes - Pitch classes in voicing order (e.g. ["B", "D#", "F#", ...])
|
|
110
|
+
* @param handHints - Optional per-note hand assignments from voicing library ("LH"/"RH")
|
|
111
|
+
* @param midiValues - Optional MIDI note numbers for accurate pitch ordering and span calculation.
|
|
112
|
+
* When provided, notes are sorted by actual pitch before splitting.
|
|
113
|
+
*/
|
|
114
|
+
export function assignFingering(notes, handHints, midiValues) {
|
|
115
|
+
const n = notes.length;
|
|
116
|
+
if (n === 0)
|
|
117
|
+
return { fingering: [], hands: [] };
|
|
118
|
+
// If hand hints are provided (from voicing library), use them directly
|
|
119
|
+
if (handHints && handHints.length === n) {
|
|
120
|
+
return fingerWithHints(notes, handHints);
|
|
121
|
+
}
|
|
122
|
+
// When MIDI values are provided, sort by actual pitch to avoid crossing
|
|
123
|
+
if (midiValues && midiValues.length === n) {
|
|
124
|
+
return assignFingeringByMidi(notes, midiValues);
|
|
125
|
+
}
|
|
126
|
+
// Fallback: pitch-class-only mode (mod 12)
|
|
127
|
+
if (isOneHandPlayable(notes)) {
|
|
128
|
+
const fingers = fingerOneHand(notes, "rh");
|
|
129
|
+
return { fingering: fingers, hands: Array(n).fill("rh") };
|
|
130
|
+
}
|
|
131
|
+
const splitIdx = findSplitPointMod12(notes);
|
|
132
|
+
return splitAndFinger(notes, splitIdx);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* MIDI-aware fingering: sorts notes by actual pitch, splits, and maps back
|
|
136
|
+
* to original order.
|
|
137
|
+
*/
|
|
138
|
+
function assignFingeringByMidi(notes, midiValues) {
|
|
139
|
+
const n = notes.length;
|
|
140
|
+
// Create index array sorted by MIDI pitch (ascending)
|
|
141
|
+
const sortedIndices = Array.from({ length: n }, (_, i) => i)
|
|
142
|
+
.sort((a, b) => midiValues[a] - midiValues[b]);
|
|
143
|
+
const sortedNotes = sortedIndices.map((i) => notes[i]);
|
|
144
|
+
const sortedMidis = sortedIndices.map((i) => midiValues[i]);
|
|
145
|
+
// Check if playable by one hand using actual MIDI span
|
|
146
|
+
if (isOneHandPlayableMidi(sortedMidis)) {
|
|
147
|
+
const fingers = fingerOneHand(sortedNotes, "rh");
|
|
148
|
+
// Map back to original order
|
|
149
|
+
const fingering = new Array(n);
|
|
150
|
+
const hands = new Array(n);
|
|
151
|
+
sortedIndices.forEach((origIdx, sortIdx) => {
|
|
152
|
+
fingering[origIdx] = fingers[sortIdx];
|
|
153
|
+
hands[origIdx] = "rh";
|
|
154
|
+
});
|
|
155
|
+
return { fingering, hands };
|
|
156
|
+
}
|
|
157
|
+
// Find best split point using actual MIDI distances
|
|
158
|
+
const splitIdx = findSplitPointMidi(sortedNotes, sortedMidis);
|
|
159
|
+
const lhNotes = sortedNotes.slice(0, splitIdx);
|
|
160
|
+
const rhNotes = sortedNotes.slice(splitIdx);
|
|
161
|
+
const lhFingers = fingerOneHand(lhNotes, "lh");
|
|
162
|
+
const rhFingers = fingerOneHand(rhNotes, "rh");
|
|
163
|
+
// Map back to original order
|
|
164
|
+
const fingering = new Array(n);
|
|
165
|
+
const hands = new Array(n);
|
|
166
|
+
sortedIndices.forEach((origIdx, sortIdx) => {
|
|
167
|
+
if (sortIdx < splitIdx) {
|
|
168
|
+
fingering[origIdx] = lhFingers[sortIdx];
|
|
169
|
+
hands[origIdx] = "lh";
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
fingering[origIdx] = rhFingers[sortIdx - splitIdx];
|
|
173
|
+
hands[origIdx] = "rh";
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
return { fingering, hands };
|
|
177
|
+
}
|
|
178
|
+
/** Split and finger using sorted note order. */
|
|
179
|
+
function splitAndFinger(notes, splitIdx) {
|
|
180
|
+
const lhNotes = notes.slice(0, splitIdx);
|
|
181
|
+
const rhNotes = notes.slice(splitIdx);
|
|
182
|
+
const lhFingers = fingerOneHand(lhNotes, "lh");
|
|
183
|
+
const rhFingers = fingerOneHand(rhNotes, "rh");
|
|
184
|
+
return {
|
|
185
|
+
fingering: [...lhFingers, ...rhFingers],
|
|
186
|
+
hands: [
|
|
187
|
+
...Array(lhNotes.length).fill("lh"),
|
|
188
|
+
...Array(rhNotes.length).fill("rh"),
|
|
189
|
+
],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/** Use voicing library hand hints to finger each hand independently. */
|
|
193
|
+
function fingerWithHints(notes, hints) {
|
|
194
|
+
const lhIndices = [];
|
|
195
|
+
const rhIndices = [];
|
|
196
|
+
for (let i = 0; i < notes.length; i++) {
|
|
197
|
+
if (hints[i] === "LH")
|
|
198
|
+
lhIndices.push(i);
|
|
199
|
+
else
|
|
200
|
+
rhIndices.push(i);
|
|
201
|
+
}
|
|
202
|
+
const lhNotes = lhIndices.map((i) => notes[i]);
|
|
203
|
+
const rhNotes = rhIndices.map((i) => notes[i]);
|
|
204
|
+
const lhFingers = fingerOneHand(lhNotes, "lh");
|
|
205
|
+
const rhFingers = fingerOneHand(rhNotes, "rh");
|
|
206
|
+
const fingering = new Array(notes.length);
|
|
207
|
+
const hands = new Array(notes.length);
|
|
208
|
+
lhIndices.forEach((idx, i) => {
|
|
209
|
+
fingering[idx] = lhFingers[i];
|
|
210
|
+
hands[idx] = "lh";
|
|
211
|
+
});
|
|
212
|
+
rhIndices.forEach((idx, i) => {
|
|
213
|
+
fingering[idx] = rhFingers[i];
|
|
214
|
+
hands[idx] = "rh";
|
|
215
|
+
});
|
|
216
|
+
return { fingering, hands };
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Find split point using actual MIDI values — guaranteed no hand crossing.
|
|
220
|
+
*
|
|
221
|
+
* Levine-style:
|
|
222
|
+
* - LH gets 1-3 lower notes (root + guide tones)
|
|
223
|
+
* - RH gets upper notes (extensions)
|
|
224
|
+
* - Prefer split at the widest MIDI gap
|
|
225
|
+
*/
|
|
226
|
+
/** Largest interval between adjacent notes in a sorted MIDI array. */
|
|
227
|
+
function maxAdjacentStretch(midis) {
|
|
228
|
+
let max = 0;
|
|
229
|
+
for (let i = 1; i < midis.length; i++) {
|
|
230
|
+
max = Math.max(max, midis[i] - midis[i - 1]);
|
|
231
|
+
}
|
|
232
|
+
return max;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Find split point using actual MIDI values — guaranteed no hand crossing.
|
|
236
|
+
*
|
|
237
|
+
* Scoring priorities (highest to lowest):
|
|
238
|
+
* 1. Minimize worst-case stretch — penalize the hand with the biggest
|
|
239
|
+
* inter-note gap. If LH has spare capacity, absorb notes to give RH room.
|
|
240
|
+
* 2. Prefer split at wider MIDI gaps — natural hand separation.
|
|
241
|
+
* 3. Levine-style preferences — LH 1-3 notes, RH 3-4 notes.
|
|
242
|
+
* 4. Balance note count between hands.
|
|
243
|
+
*/
|
|
244
|
+
function findSplitPointMidi(sortedNotes, sortedMidis) {
|
|
245
|
+
const n = sortedNotes.length;
|
|
246
|
+
let bestSplit = Math.min(2, n - 1);
|
|
247
|
+
let bestScore = Infinity;
|
|
248
|
+
for (let split = 1; split < n; split++) {
|
|
249
|
+
const lhMidis = sortedMidis.slice(0, split);
|
|
250
|
+
const rhMidis = sortedMidis.slice(split);
|
|
251
|
+
// Both hands must be playable
|
|
252
|
+
if (split > MAX_HAND_NOTES || (n - split) > MAX_HAND_NOTES)
|
|
253
|
+
continue;
|
|
254
|
+
const lhSpan = lhMidis[lhMidis.length - 1] - lhMidis[0];
|
|
255
|
+
const rhSpan = rhMidis[rhMidis.length - 1] - rhMidis[0];
|
|
256
|
+
if (lhSpan > MAX_HAND_SPAN || rhSpan > MAX_HAND_SPAN)
|
|
257
|
+
continue;
|
|
258
|
+
let score = 0;
|
|
259
|
+
const lhCount = split;
|
|
260
|
+
const rhCount = n - split;
|
|
261
|
+
// 1. STRAIN BALANCE — minimize the worst stretch in either hand.
|
|
262
|
+
const lhStretch = maxAdjacentStretch(lhMidis);
|
|
263
|
+
const rhStretch = maxAdjacentStretch(rhMidis);
|
|
264
|
+
const worstStretch = Math.max(lhStretch, rhStretch);
|
|
265
|
+
score += worstStretch * 5;
|
|
266
|
+
// 2. LOAD BALANCE — penalize hands at max capacity (5 notes = no slack).
|
|
267
|
+
// A hand with spare fingers can accommodate stretches more comfortably.
|
|
268
|
+
// Reward splits where both hands have breathing room.
|
|
269
|
+
const lhLoad = lhCount / MAX_HAND_NOTES; // 0.2 to 1.0
|
|
270
|
+
const rhLoad = rhCount / MAX_HAND_NOTES;
|
|
271
|
+
const loadImbalance = Math.abs(lhLoad - rhLoad);
|
|
272
|
+
score += loadImbalance * 8;
|
|
273
|
+
// Extra penalty when either hand is maxed out at 5 notes
|
|
274
|
+
if (lhCount === MAX_HAND_NOTES || rhCount === MAX_HAND_NOTES) {
|
|
275
|
+
score += 4;
|
|
276
|
+
}
|
|
277
|
+
// 3. Prefer wider MIDI gap at the split point — natural hand separation
|
|
278
|
+
const gap = sortedMidis[split] - sortedMidis[split - 1];
|
|
279
|
+
score -= gap * 3;
|
|
280
|
+
// 4. Levine-style: LH with 1-3 notes (root + guide tones)
|
|
281
|
+
if (lhCount >= 1 && lhCount <= 3)
|
|
282
|
+
score -= 4;
|
|
283
|
+
// 5. Prefer RH with 3-4 notes
|
|
284
|
+
if (rhCount >= 3 && rhCount <= 4)
|
|
285
|
+
score -= 2;
|
|
286
|
+
if (score < bestScore) {
|
|
287
|
+
bestScore = score;
|
|
288
|
+
bestSplit = split;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return bestSplit;
|
|
292
|
+
}
|
|
293
|
+
/** Fallback split point using mod-12 intervals (no MIDI data). */
|
|
294
|
+
function findSplitPointMod12(notes) {
|
|
295
|
+
const n = notes.length;
|
|
296
|
+
let bestSplit = Math.min(2, n - 1);
|
|
297
|
+
let bestScore = Infinity;
|
|
298
|
+
for (let split = 1; split < n; split++) {
|
|
299
|
+
const lhNotes = notes.slice(0, split);
|
|
300
|
+
const rhNotes = notes.slice(split);
|
|
301
|
+
if (split > MAX_HAND_NOTES || (n - split) > MAX_HAND_NOTES)
|
|
302
|
+
continue;
|
|
303
|
+
if (totalSpanMod12(lhNotes) > MAX_HAND_SPAN || totalSpanMod12(rhNotes) > MAX_HAND_SPAN)
|
|
304
|
+
continue;
|
|
305
|
+
let score = 0;
|
|
306
|
+
// Strain balance: penalize the hand with biggest adjacent interval
|
|
307
|
+
let lhMaxGap = 0;
|
|
308
|
+
for (let i = 1; i < split; i++)
|
|
309
|
+
lhMaxGap = Math.max(lhMaxGap, interval(notes[i - 1], notes[i]));
|
|
310
|
+
let rhMaxGap = 0;
|
|
311
|
+
for (let i = split + 1; i < n; i++)
|
|
312
|
+
rhMaxGap = Math.max(rhMaxGap, interval(notes[i - 1], notes[i]));
|
|
313
|
+
score += Math.max(lhMaxGap, rhMaxGap) * 5;
|
|
314
|
+
const gapSemitones = interval(notes[split - 1], notes[split]);
|
|
315
|
+
score -= gapSemitones * 3;
|
|
316
|
+
if (split >= 1 && split <= 3)
|
|
317
|
+
score -= 4;
|
|
318
|
+
if ((n - split) >= 3 && (n - split) <= 4)
|
|
319
|
+
score -= 2;
|
|
320
|
+
score += Math.abs(split - (n - split));
|
|
321
|
+
if (score < bestScore) {
|
|
322
|
+
bestScore = score;
|
|
323
|
+
bestSplit = split;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return bestSplit;
|
|
327
|
+
}
|
|
328
|
+
// ── Legacy API ────────────────────────────────────────────────────
|
|
329
|
+
/**
|
|
330
|
+
* Compute auto-fingering for a chord voicing (legacy API).
|
|
331
|
+
*/
|
|
332
|
+
export function autoFingering(notes, hand = "rh") {
|
|
333
|
+
const n = notes.length;
|
|
334
|
+
if (n === 0)
|
|
335
|
+
return [];
|
|
336
|
+
if (n === 1)
|
|
337
|
+
return [3];
|
|
338
|
+
if (isOneHandPlayable(notes)) {
|
|
339
|
+
return fingerOneHand(notes, hand);
|
|
340
|
+
}
|
|
341
|
+
const result = assignFingering(notes);
|
|
342
|
+
return result.fingering;
|
|
343
|
+
}
|
|
344
|
+
//# sourceMappingURL=auto-fingering.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-fingering.js","sourceRoot":"","sources":["../../src/engine/auto-fingering.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { DEFAULT_WHITE_FILL, DEFAULT_BLACK_FILL } from "./svg-constants";
|
|
2
|
+
import { normalizeToSharps } from "./note-spelling";
|
|
3
|
+
export function normalizeNote(note) {
|
|
4
|
+
return normalizeToSharps(note);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Normalize a highlight key. Supports octave-qualified format "note:octave"
|
|
8
|
+
* (e.g., "A#:0", "C:1") for multi-octave keyboards.
|
|
9
|
+
*/
|
|
10
|
+
function normalizeHighlightKey(key) {
|
|
11
|
+
const colonIdx = key.indexOf(":");
|
|
12
|
+
if (colonIdx !== -1) {
|
|
13
|
+
const note = normalizeNote(key.slice(0, colonIdx));
|
|
14
|
+
const octave = parseInt(key.slice(colonIdx + 1), 10);
|
|
15
|
+
return { note, octave };
|
|
16
|
+
}
|
|
17
|
+
return { note: normalizeNote(key), octave: null };
|
|
18
|
+
}
|
|
19
|
+
export function mapHighlights(keys, highlightKeys, theme) {
|
|
20
|
+
const normalized = highlightKeys.map(normalizeHighlightKey);
|
|
21
|
+
const remaining = [...normalized];
|
|
22
|
+
return keys.map((key) => {
|
|
23
|
+
const keyNote = normalizeNote(key.note);
|
|
24
|
+
const matchIndex = remaining.findIndex((h) => {
|
|
25
|
+
if (h.note !== keyNote)
|
|
26
|
+
return false;
|
|
27
|
+
if (h.octave !== null && h.octave !== key.octave)
|
|
28
|
+
return false;
|
|
29
|
+
return true;
|
|
30
|
+
});
|
|
31
|
+
const isHighlighted = matchIndex !== -1;
|
|
32
|
+
if (isHighlighted) {
|
|
33
|
+
remaining.splice(matchIndex, 1);
|
|
34
|
+
}
|
|
35
|
+
if (theme) {
|
|
36
|
+
return key.isBlack
|
|
37
|
+
? theme.blackKey(keyNote, isHighlighted)
|
|
38
|
+
: theme.whiteKey(keyNote, isHighlighted);
|
|
39
|
+
}
|
|
40
|
+
if (isHighlighted) {
|
|
41
|
+
return key.isBlack ? "#5a8ab5" : "#a0c6e8";
|
|
42
|
+
}
|
|
43
|
+
return key.isBlack ? DEFAULT_BLACK_FILL : DEFAULT_WHITE_FILL;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=highlight-mapper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"highlight-mapper.js","sourceRoot":"","sources":["../../src/engine/highlight-mapper.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEpD,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,iBAAiB,CAAC,IAAI,CAAC,CAAC;AACjC,CAAC;AAED;;;GAGG;AACH,SAAS,qBAAqB,CAAC,GAAW;IACxC,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,QAAQ,KAAK,CAAC,CAAC,EAAE,CAAC;QACpB,MAAM,IAAI,GAAG,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC;QACnD,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACrD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAC1B,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,aAAa,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AACpD,CAAC;AAED,MAAM,UAAU,aAAa,CAC3B,IAAqB,EACrB,aAAuB,EACvB,KAAkB;IAElB,MAAM,UAAU,GAAG,aAAa,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;IAC5D,MAAM,SAAS,GAAG,CAAC,GAAG,UAAU,CAAC,CAAC;IAElC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;QACtB,MAAM,OAAO,GAAG,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACxC,MAAM,UAAU,GAAG,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE;YAC3C,IAAI,CAAC,CAAC,IAAI,KAAK,OAAO;gBAAE,OAAO,KAAK,CAAC;YACrC,IAAI,CAAC,CAAC,MAAM,KAAK,IAAI,IAAI,CAAC,CAAC,MAAM,KAAK,GAAG,CAAC,MAAM;gBAAE,OAAO,KAAK,CAAC;YAC/D,OAAO,IAAI,CAAC;QACd,CAAC,CAAC,CAAC;QACH,MAAM,aAAa,GAAG,UAAU,KAAK,CAAC,CAAC,CAAC;QAExC,IAAI,aAAa,EAAE,CAAC;YAClB,SAAS,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;QAClC,CAAC;QAED,IAAI,KAAK,EAAE,CAAC;YACV,OAAO,GAAG,CAAC,OAAO;gBAChB,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;gBACxC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;QAC7C,CAAC;QAED,IAAI,aAAa,EAAE,CAAC;YAClB,OAAO,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;QAC7C,CAAC;QAED,OAAO,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,kBAAkB,CAAC;IAC/D,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Format, WhiteNote, KeyDescriptor } from "../types";
|
|
2
|
+
export declare function computeKeyboard(startFrom?: WhiteNote, size?: number, format?: Format): KeyDescriptor[];
|
|
3
|
+
export declare function computeSvgDimensions(size: number, format?: Format): {
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { WHITE_KEY_WIDTH, WHITE_KEY_WIDTH_EXACT, WHITE_KEY_HEIGHT_COMPACT, WHITE_KEY_HEIGHT_EXACT, BLACK_KEY_WIDTH, BLACK_KEY_WIDTH_EXACT, BLACK_KEY_HEIGHT_COMPACT, BLACK_KEY_HEIGHT_EXACT, BLACK_KEY_OFFSETS, BLACK_KEY_OFFSETS_EXACT, WHITE_NOTES_WITH_SHARPS, WHITE_NOTE_ORDER, } from "./svg-constants";
|
|
2
|
+
const SHARP_SUFFIX = {
|
|
3
|
+
C: "C#",
|
|
4
|
+
D: "D#",
|
|
5
|
+
F: "F#",
|
|
6
|
+
G: "G#",
|
|
7
|
+
A: "A#",
|
|
8
|
+
};
|
|
9
|
+
export function computeKeyboard(startFrom = "C", size = 8, format = "compact") {
|
|
10
|
+
const isExact = format === "exact";
|
|
11
|
+
const whiteHeight = isExact ? WHITE_KEY_HEIGHT_EXACT : WHITE_KEY_HEIGHT_COMPACT;
|
|
12
|
+
const blackHeight = isExact ? BLACK_KEY_HEIGHT_EXACT : BLACK_KEY_HEIGHT_COMPACT;
|
|
13
|
+
const whiteW = isExact ? WHITE_KEY_WIDTH_EXACT : WHITE_KEY_WIDTH;
|
|
14
|
+
const blackW = isExact ? BLACK_KEY_WIDTH_EXACT : BLACK_KEY_WIDTH;
|
|
15
|
+
const offsets = isExact ? BLACK_KEY_OFFSETS_EXACT : BLACK_KEY_OFFSETS;
|
|
16
|
+
const keys = [];
|
|
17
|
+
const startIndex = WHITE_NOTE_ORDER.indexOf(startFrom);
|
|
18
|
+
// Offset by half stroke width so left border isn't clipped
|
|
19
|
+
let whiteX = 0.5;
|
|
20
|
+
let relativeOctave = 0;
|
|
21
|
+
for (let i = 0; i < size; i++) {
|
|
22
|
+
const noteIndex = (startIndex + i) % 7;
|
|
23
|
+
const whiteNote = WHITE_NOTE_ORDER[noteIndex];
|
|
24
|
+
// Octave increments each time we cross C
|
|
25
|
+
if (i > 0 && whiteNote === "C") {
|
|
26
|
+
relativeOctave++;
|
|
27
|
+
}
|
|
28
|
+
// Add white key
|
|
29
|
+
keys.push({
|
|
30
|
+
note: whiteNote,
|
|
31
|
+
octave: relativeOctave,
|
|
32
|
+
isBlack: false,
|
|
33
|
+
x: whiteX,
|
|
34
|
+
y: 0,
|
|
35
|
+
width: whiteW,
|
|
36
|
+
height: whiteHeight,
|
|
37
|
+
});
|
|
38
|
+
// Add black key if this white note has one
|
|
39
|
+
if (WHITE_NOTES_WITH_SHARPS.has(whiteNote)) {
|
|
40
|
+
const offset = offsets[whiteNote];
|
|
41
|
+
keys.push({
|
|
42
|
+
note: SHARP_SUFFIX[whiteNote],
|
|
43
|
+
octave: relativeOctave,
|
|
44
|
+
isBlack: true,
|
|
45
|
+
x: whiteX + offset,
|
|
46
|
+
y: 0,
|
|
47
|
+
width: blackW,
|
|
48
|
+
height: blackHeight,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
whiteX += whiteW;
|
|
52
|
+
}
|
|
53
|
+
return keys;
|
|
54
|
+
}
|
|
55
|
+
export function computeSvgDimensions(size, format = "compact") {
|
|
56
|
+
const isExact = format === "exact";
|
|
57
|
+
const keyHeight = isExact ? WHITE_KEY_HEIGHT_EXACT : WHITE_KEY_HEIGHT_COMPACT;
|
|
58
|
+
const whiteW = isExact ? WHITE_KEY_WIDTH_EXACT : WHITE_KEY_WIDTH;
|
|
59
|
+
// Add padding so strokes + rounded corners aren't clipped at edges
|
|
60
|
+
return {
|
|
61
|
+
width: size * whiteW + 1,
|
|
62
|
+
height: keyHeight + 2,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=keyboard-layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyboard-layout.js","sourceRoot":"","sources":["../../src/engine/keyboard-layout.ts"],"names":[],"mappings":"AACA,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,wBAAwB,EACxB,sBAAsB,EACtB,eAAe,EACf,qBAAqB,EACrB,wBAAwB,EACxB,sBAAsB,EACtB,iBAAiB,EACjB,uBAAuB,EACvB,uBAAuB,EACvB,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AAEzB,MAAM,YAAY,GAA2B;IAC3C,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;CACR,CAAC;AAEF,MAAM,UAAU,eAAe,CAC7B,YAAuB,GAAG,EAC1B,OAAe,CAAC,EAChB,SAAiB,SAAS;IAE1B,MAAM,OAAO,GAAG,MAAM,KAAK,OAAO,CAAC;IACnC,MAAM,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,wBAAwB,CAAC;IAChF,MAAM,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,wBAAwB,CAAC;IAChF,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,eAAe,CAAC;IACjE,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,eAAe,CAAC;IACjE,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,iBAAiB,CAAC;IAEtE,MAAM,IAAI,GAAoB,EAAE,CAAC;IACjC,MAAM,UAAU,GAAG,gBAAgB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACvD,2DAA2D;IAC3D,IAAI,MAAM,GAAG,GAAG,CAAC;IACjB,IAAI,cAAc,GAAG,CAAC,CAAC;IAEvB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;QAC9B,MAAM,SAAS,GAAG,CAAC,UAAU,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;QACvC,MAAM,SAAS,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;QAE9C,yCAAyC;QACzC,IAAI,CAAC,GAAG,CAAC,IAAI,SAAS,KAAK,GAAG,EAAE,CAAC;YAC/B,cAAc,EAAE,CAAC;QACnB,CAAC;QAED,gBAAgB;QAChB,IAAI,CAAC,IAAI,CAAC;YACR,IAAI,EAAE,SAAS;YACf,MAAM,EAAE,cAAc;YACtB,OAAO,EAAE,KAAK;YACd,CAAC,EAAE,MAAM;YACT,CAAC,EAAE,CAAC;YACJ,KAAK,EAAE,MAAM;YACb,MAAM,EAAE,WAAW;SACpB,CAAC,CAAC;QAEH,2CAA2C;QAC3C,IAAI,uBAAuB,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;YAC3C,MAAM,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;YAClC,IAAI,CAAC,IAAI,CAAC;gBACR,IAAI,EAAE,YAAY,CAAC,SAAS,CAAC;gBAC7B,MAAM,EAAE,cAAc;gBACtB,OAAO,EAAE,IAAI;gBACb,CAAC,EAAE,MAAM,GAAG,MAAM;gBAClB,CAAC,EAAE,CAAC;gBACJ,KAAK,EAAE,MAAM;gBACb,MAAM,EAAE,WAAW;aACpB,CAAC,CAAC;QACL,CAAC;QAED,MAAM,IAAI,MAAM,CAAC;IACnB,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,IAAY,EACZ,SAAiB,SAAS;IAE1B,MAAM,OAAO,GAAG,MAAM,KAAK,OAAO,CAAC;IACnC,MAAM,SAAS,GAAG,OAAO,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,wBAAwB,CAAC;IAC9E,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,eAAe,CAAC;IACjE,mEAAmE;IACnE,OAAO;QACL,KAAK,EAAE,IAAI,GAAG,MAAM,GAAG,CAAC;QACxB,MAAM,EAAE,SAAS,GAAG,CAAC;KACtB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consolidated enharmonic spelling logic.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for flat↔sharp conversion, key-context-aware
|
|
5
|
+
* note spelling, and pitch class normalization.
|
|
6
|
+
*/
|
|
7
|
+
/** Convert flat note names to their sharp equivalents. */
|
|
8
|
+
export declare const FLAT_TO_SHARP: Record<string, string>;
|
|
9
|
+
/** Convert sharp note names to their flat equivalents. */
|
|
10
|
+
export declare const SHARP_TO_FLAT: Record<string, string>;
|
|
11
|
+
/**
|
|
12
|
+
* Keys whose notes should be spelled with flats.
|
|
13
|
+
* Includes both major flat keys and enharmonic equivalents.
|
|
14
|
+
*/
|
|
15
|
+
export declare const FLAT_KEYS: Set<string>;
|
|
16
|
+
/**
|
|
17
|
+
* Normalize a pitch class to sharps for internal matching.
|
|
18
|
+
* e.g. "Bb" → "A#", "Db" → "C#", "C" → "C"
|
|
19
|
+
*/
|
|
20
|
+
export declare function normalizeToSharps(note: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Spell a pitch class to match the key context of the given root.
|
|
23
|
+
* Flat-key roots get flat spelling; sharp-key roots keep sharp spelling.
|
|
24
|
+
*
|
|
25
|
+
* e.g. spellForKey("A#", "Bb") → "Bb"
|
|
26
|
+
* spellForKey("Bb", "E") → "A#"
|
|
27
|
+
*/
|
|
28
|
+
export declare function spellForKey(pc: string, key: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Choose enharmonic spelling with explicit flat preference.
|
|
31
|
+
* Used for roman numeral accidentals (bVII → flat spelling regardless of key).
|
|
32
|
+
*/
|
|
33
|
+
export declare function spellWithPreference(note: string, key: string, preferFlat?: boolean): string;
|
|
34
|
+
/**
|
|
35
|
+
* Pitch class to semitone value (0-11). Handles both sharps and flats.
|
|
36
|
+
* C=0, C#/Db=1, D=2, D#/Eb=3, E=4, F=5, F#/Gb=6, G=7, G#/Ab=8, A=9, A#/Bb=10, B=11
|
|
37
|
+
*/
|
|
38
|
+
export declare const PC_SEMITONES: Record<string, number>;
|
|
39
|
+
/** A3 MIDI value — default floor for scale starting notes. */
|
|
40
|
+
export declare const MIDI_A3 = 57;
|
|
41
|
+
/**
|
|
42
|
+
* Assign ascending octaves to an array of pitch classes.
|
|
43
|
+
* Each note that would descend (lower semitone than previous) bumps the octave.
|
|
44
|
+
* Optionally compacts the result to stay within a maximum span.
|
|
45
|
+
*
|
|
46
|
+
* @param notes - Sharp-normalized pitch classes in voicing/scale order
|
|
47
|
+
* @param baseOctave - Starting octave for the first note
|
|
48
|
+
* @param maxSpan - Maximum semitone span before folding (0 = no limit)
|
|
49
|
+
* @returns Array of { note, octave } pairs
|
|
50
|
+
*/
|
|
51
|
+
export declare function assignAscendingOctaves(notes: string[], baseOctave: number, maxSpan?: number): Array<{
|
|
52
|
+
note: string;
|
|
53
|
+
octave: number;
|
|
54
|
+
}>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consolidated enharmonic spelling logic.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for flat↔sharp conversion, key-context-aware
|
|
5
|
+
* note spelling, and pitch class normalization.
|
|
6
|
+
*/
|
|
7
|
+
/** Convert flat note names to their sharp equivalents. */
|
|
8
|
+
export const FLAT_TO_SHARP = {
|
|
9
|
+
Db: "C#", Eb: "D#", Gb: "F#", Ab: "G#", Bb: "A#",
|
|
10
|
+
};
|
|
11
|
+
/** Convert sharp note names to their flat equivalents. */
|
|
12
|
+
export const SHARP_TO_FLAT = {
|
|
13
|
+
"C#": "Db", "D#": "Eb", "F#": "Gb", "G#": "Ab", "A#": "Bb",
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Keys whose notes should be spelled with flats.
|
|
17
|
+
* Includes both major flat keys and enharmonic equivalents.
|
|
18
|
+
*/
|
|
19
|
+
export const FLAT_KEYS = new Set([
|
|
20
|
+
"F", "Bb", "Eb", "Ab", "Db", "Gb", "Cb",
|
|
21
|
+
]);
|
|
22
|
+
/**
|
|
23
|
+
* Normalize a pitch class to sharps for internal matching.
|
|
24
|
+
* e.g. "Bb" → "A#", "Db" → "C#", "C" → "C"
|
|
25
|
+
*/
|
|
26
|
+
export function normalizeToSharps(note) {
|
|
27
|
+
return FLAT_TO_SHARP[note] ?? note;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Spell a pitch class to match the key context of the given root.
|
|
31
|
+
* Flat-key roots get flat spelling; sharp-key roots keep sharp spelling.
|
|
32
|
+
*
|
|
33
|
+
* e.g. spellForKey("A#", "Bb") → "Bb"
|
|
34
|
+
* spellForKey("Bb", "E") → "A#"
|
|
35
|
+
*/
|
|
36
|
+
export function spellForKey(pc, key) {
|
|
37
|
+
if (FLAT_KEYS.has(key)) {
|
|
38
|
+
return SHARP_TO_FLAT[pc] ?? pc;
|
|
39
|
+
}
|
|
40
|
+
return FLAT_TO_SHARP[pc] ?? pc;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Choose enharmonic spelling with explicit flat preference.
|
|
44
|
+
* Used for roman numeral accidentals (bVII → flat spelling regardless of key).
|
|
45
|
+
*/
|
|
46
|
+
export function spellWithPreference(note, key, preferFlat = false) {
|
|
47
|
+
if (preferFlat) {
|
|
48
|
+
return SHARP_TO_FLAT[note] ?? note;
|
|
49
|
+
}
|
|
50
|
+
return spellForKey(note, key);
|
|
51
|
+
}
|
|
52
|
+
// ── Semitone mapping (canonical, replaces duplicates across codebase) ──
|
|
53
|
+
/**
|
|
54
|
+
* Pitch class to semitone value (0-11). Handles both sharps and flats.
|
|
55
|
+
* C=0, C#/Db=1, D=2, D#/Eb=3, E=4, F=5, F#/Gb=6, G=7, G#/Ab=8, A=9, A#/Bb=10, B=11
|
|
56
|
+
*/
|
|
57
|
+
export const PC_SEMITONES = {
|
|
58
|
+
C: 0, "C#": 1, Db: 1,
|
|
59
|
+
D: 2, "D#": 3, Eb: 3,
|
|
60
|
+
E: 4, Fb: 4, "E#": 5,
|
|
61
|
+
F: 5, "F#": 6, Gb: 6,
|
|
62
|
+
G: 7, "G#": 8, Ab: 8,
|
|
63
|
+
A: 9, "A#": 10, Bb: 10,
|
|
64
|
+
B: 11, Cb: 11, "B#": 0,
|
|
65
|
+
};
|
|
66
|
+
/** A3 MIDI value — default floor for scale starting notes. */
|
|
67
|
+
export const MIDI_A3 = 57;
|
|
68
|
+
/**
|
|
69
|
+
* Assign ascending octaves to an array of pitch classes.
|
|
70
|
+
* Each note that would descend (lower semitone than previous) bumps the octave.
|
|
71
|
+
* Optionally compacts the result to stay within a maximum span.
|
|
72
|
+
*
|
|
73
|
+
* @param notes - Sharp-normalized pitch classes in voicing/scale order
|
|
74
|
+
* @param baseOctave - Starting octave for the first note
|
|
75
|
+
* @param maxSpan - Maximum semitone span before folding (0 = no limit)
|
|
76
|
+
* @returns Array of { note, octave } pairs
|
|
77
|
+
*/
|
|
78
|
+
export function assignAscendingOctaves(notes, baseOctave, maxSpan = 0) {
|
|
79
|
+
if (notes.length === 0)
|
|
80
|
+
return [];
|
|
81
|
+
let octave = baseOctave;
|
|
82
|
+
let prevSemi = PC_SEMITONES[notes[0]] ?? 0;
|
|
83
|
+
const result = notes.map((n, i) => {
|
|
84
|
+
const semi = PC_SEMITONES[n] ?? 0;
|
|
85
|
+
if (i > 0 && semi <= prevSemi) {
|
|
86
|
+
octave++;
|
|
87
|
+
}
|
|
88
|
+
prevSemi = semi;
|
|
89
|
+
return { note: n, octave };
|
|
90
|
+
});
|
|
91
|
+
// Optional compaction: fold notes down if span exceeds limit
|
|
92
|
+
if (maxSpan > 0 && result.length > 1) {
|
|
93
|
+
const baseSemi = (PC_SEMITONES[result[0].note] ?? 0) + result[0].octave * 12;
|
|
94
|
+
for (let i = 1; i < result.length; i++) {
|
|
95
|
+
const noteSemi = (PC_SEMITONES[result[i].note] ?? 0) + result[i].octave * 12;
|
|
96
|
+
if (noteSemi - baseSemi > maxSpan && result[i].octave > result[0].octave) {
|
|
97
|
+
result[i].octave--;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=note-spelling.js.map
|