@leafo/lml 0.1.1 → 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/README.md +273 -40
- package/dist/auto-chords.d.ts +33 -8
- package/dist/auto-chords.d.ts.map +1 -1
- package/dist/auto-chords.js +19 -1
- package/dist/auto-chords.js.map +1 -1
- package/dist/grammar.d.ts.map +1 -1
- package/dist/grammar.js +1371 -283
- package/dist/grammar.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/music.d.ts +502 -5
- package/dist/music.d.ts.map +1 -1
- package/dist/music.js +500 -53
- package/dist/music.js.map +1 -1
- package/dist/note-utils.d.ts +13 -0
- package/dist/note-utils.d.ts.map +1 -0
- package/dist/note-utils.js +62 -0
- package/dist/note-utils.js.map +1 -0
- package/dist/noteUtils.d.ts +25 -0
- package/dist/noteUtils.d.ts.map +1 -0
- package/dist/noteUtils.js +86 -0
- package/dist/noteUtils.js.map +1 -0
- package/dist/parser.d.ts +137 -2
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +259 -36
- package/dist/parser.js.map +1 -1
- package/dist/song.d.ts +207 -2
- package/dist/song.d.ts.map +1 -1
- package/dist/song.js +251 -15
- package/dist/song.js.map +1 -1
- package/package.json +8 -3
package/dist/music.js
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MIDI pitch value for middle C (C4).
|
|
3
|
+
* @example
|
|
4
|
+
* parseNote("C4") === MIDDLE_C_PITCH // true
|
|
5
|
+
*/
|
|
1
6
|
export const MIDDLE_C_PITCH = 60;
|
|
7
|
+
/**
|
|
8
|
+
* Number of semitones in an octave.
|
|
9
|
+
*/
|
|
2
10
|
export const OCTAVE_SIZE = 12;
|
|
11
|
+
/**
|
|
12
|
+
* Bidirectional mapping between note letters and their semitone offsets from C.
|
|
13
|
+
* Maps both directions: number -> letter name, and letter name -> number.
|
|
14
|
+
* Only includes natural notes (no sharps/flats).
|
|
15
|
+
* @example
|
|
16
|
+
* OFFSETS[0] // "C"
|
|
17
|
+
* OFFSETS["C"] // 0
|
|
18
|
+
* OFFSETS[7] // "G"
|
|
19
|
+
* OFFSETS["G"] // 7
|
|
20
|
+
*/
|
|
3
21
|
export const OFFSETS = {
|
|
4
22
|
0: "C",
|
|
5
23
|
2: "D",
|
|
@@ -16,6 +34,13 @@ export const OFFSETS = {
|
|
|
16
34
|
"A": 9,
|
|
17
35
|
"B": 11
|
|
18
36
|
};
|
|
37
|
+
/**
|
|
38
|
+
* Maps semitone offset (from C) to staff line position (0-6).
|
|
39
|
+
* Used for determining vertical placement on sheet music.
|
|
40
|
+
* @example
|
|
41
|
+
* LETTER_OFFSETS[0] // 0 (C)
|
|
42
|
+
* LETTER_OFFSETS[7] // 4 (G)
|
|
43
|
+
*/
|
|
19
44
|
export const LETTER_OFFSETS = {
|
|
20
45
|
0: 0,
|
|
21
46
|
2: 1,
|
|
@@ -25,6 +50,13 @@ export const LETTER_OFFSETS = {
|
|
|
25
50
|
9: 5,
|
|
26
51
|
11: 6
|
|
27
52
|
};
|
|
53
|
+
/**
|
|
54
|
+
* Maps note letter names to staff line position (0-6).
|
|
55
|
+
* C=0, D=1, E=2, F=3, G=4, A=5, B=6.
|
|
56
|
+
* @example
|
|
57
|
+
* NOTE_NAME_OFFSETS["C"] // 0
|
|
58
|
+
* NOTE_NAME_OFFSETS["G"] // 4
|
|
59
|
+
*/
|
|
28
60
|
export const NOTE_NAME_OFFSETS = {
|
|
29
61
|
"C": 0,
|
|
30
62
|
"D": 1,
|
|
@@ -34,9 +66,19 @@ export const NOTE_NAME_OFFSETS = {
|
|
|
34
66
|
"A": 5,
|
|
35
67
|
"B": 6,
|
|
36
68
|
};
|
|
69
|
+
/**
|
|
70
|
+
* Converts a MIDI pitch number to a note name string.
|
|
71
|
+
* @param pitch - MIDI pitch number (60 = middle C)
|
|
72
|
+
* @param sharpen - If true, use sharps for accidentals; if false, use flats
|
|
73
|
+
* @returns Note name with octave (e.g., "C4", "F#5", "Bb3")
|
|
74
|
+
* @example
|
|
75
|
+
* noteName(60) // "C4"
|
|
76
|
+
* noteName(61) // "C#4"
|
|
77
|
+
* noteName(61, false) // "Db4"
|
|
78
|
+
*/
|
|
37
79
|
export function noteName(pitch, sharpen = true) {
|
|
38
|
-
const octave = Math.floor(pitch / OCTAVE_SIZE);
|
|
39
|
-
const offset = pitch - octave * OCTAVE_SIZE;
|
|
80
|
+
const octave = Math.floor(pitch / OCTAVE_SIZE) - 1;
|
|
81
|
+
const offset = pitch - (octave + 1) * OCTAVE_SIZE;
|
|
40
82
|
let name = OFFSETS[offset];
|
|
41
83
|
if (!name) {
|
|
42
84
|
if (sharpen) {
|
|
@@ -82,6 +124,16 @@ function parseNoteOffset(note) {
|
|
|
82
124
|
}
|
|
83
125
|
return (n + 12) % 12; // wrap around for Cb and B#
|
|
84
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Parses a note string into its MIDI pitch number.
|
|
129
|
+
* @param note - Note string with octave (e.g., "C4", "F#5", "Bb3")
|
|
130
|
+
* @returns MIDI pitch number (60 = middle C)
|
|
131
|
+
* @throws Error if note format is invalid
|
|
132
|
+
* @example
|
|
133
|
+
* parseNote("C4") // 60
|
|
134
|
+
* parseNote("A4") // 69
|
|
135
|
+
* parseNote("C#4") // 61
|
|
136
|
+
*/
|
|
85
137
|
export function parseNote(note) {
|
|
86
138
|
const parsed = note.match(/^([A-G])(#|b)?(\d+)$/);
|
|
87
139
|
if (!parsed) {
|
|
@@ -91,7 +143,7 @@ export function parseNote(note) {
|
|
|
91
143
|
if (OFFSETS[letter] === undefined) {
|
|
92
144
|
throw new Error(`Invalid note letter: ${letter}`);
|
|
93
145
|
}
|
|
94
|
-
let n = OFFSETS[letter] + parseInt(octave, 10) * OCTAVE_SIZE;
|
|
146
|
+
let n = OFFSETS[letter] + (parseInt(octave, 10) + 1) * OCTAVE_SIZE;
|
|
95
147
|
if (accidental == "#") {
|
|
96
148
|
n += 1;
|
|
97
149
|
}
|
|
@@ -100,6 +152,16 @@ export function parseNote(note) {
|
|
|
100
152
|
}
|
|
101
153
|
return n;
|
|
102
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Calculates the vertical staff position for a note.
|
|
157
|
+
* Used for positioning notes on sheet music.
|
|
158
|
+
* @param note - Note string with octave (e.g., "C4", "G5"); letter must be A-G
|
|
159
|
+
* @returns Staff offset value (higher = higher on staff)
|
|
160
|
+
* @throws Error if note format is invalid; invalid letters yield NaN
|
|
161
|
+
* @example
|
|
162
|
+
* noteStaffOffset("C4") // 28
|
|
163
|
+
* noteStaffOffset("D4") // 29
|
|
164
|
+
*/
|
|
103
165
|
export function noteStaffOffset(note) {
|
|
104
166
|
const match = note.match(/(\w)[#b]?(\d+)/);
|
|
105
167
|
if (!match) {
|
|
@@ -108,32 +170,94 @@ export function noteStaffOffset(note) {
|
|
|
108
170
|
const [, name, octave] = match;
|
|
109
171
|
return +octave * 7 + NOTE_NAME_OFFSETS[name];
|
|
110
172
|
}
|
|
111
|
-
|
|
173
|
+
/**
|
|
174
|
+
* Compares two notes ignoring octave (enharmonic comparison).
|
|
175
|
+
* @param a - First note string (with or without octave)
|
|
176
|
+
* @param b - Second note string (with or without octave)
|
|
177
|
+
* @returns True if notes are the same pitch class
|
|
178
|
+
* @example
|
|
179
|
+
* notesSame("C4", "C5") // true (same pitch class)
|
|
180
|
+
* notesSame("C#4", "Db4") // true (enharmonic)
|
|
181
|
+
* notesSame("C4", "D4") // false
|
|
182
|
+
*/
|
|
112
183
|
export function notesSame(a, b) {
|
|
113
184
|
return parseNoteOffset(a) == parseNoteOffset(b);
|
|
114
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Transposes a note by a given interval in semitones.
|
|
188
|
+
* @param note - Note string with octave (e.g., "C4")
|
|
189
|
+
* @param halfSteps - Number of semitones to transpose (positive = up, negative = down)
|
|
190
|
+
* @returns Transposed note string
|
|
191
|
+
* @example
|
|
192
|
+
* addInterval("C4", 2) // "D4" (whole step up)
|
|
193
|
+
* addInterval("C4", 12) // "C5" (octave up)
|
|
194
|
+
* addInterval("C4", -1) // "B3" (half step down)
|
|
195
|
+
*/
|
|
115
196
|
export function addInterval(note, halfSteps) {
|
|
116
197
|
return noteName(parseNote(note) + halfSteps);
|
|
117
198
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Compares two notes and returns the difference in semitones.
|
|
201
|
+
* @param a - First note string with octave
|
|
202
|
+
* @param b - Second note string with octave
|
|
203
|
+
* @returns 0 if equal, negative if a < b, positive if a > b
|
|
204
|
+
* @example
|
|
205
|
+
* compareNotes("C4", "C4") // 0
|
|
206
|
+
* compareNotes("C4", "D4") // -2
|
|
207
|
+
* compareNotes("D4", "C4") // 2
|
|
208
|
+
*/
|
|
121
209
|
export function compareNotes(a, b) {
|
|
122
210
|
return parseNote(a) - parseNote(b);
|
|
123
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Checks if the first note is lower than the second.
|
|
214
|
+
* @param a - First note string with octave
|
|
215
|
+
* @param b - Second note string with octave
|
|
216
|
+
* @returns True if a is lower than b
|
|
217
|
+
* @example
|
|
218
|
+
* notesLessThan("C4", "D4") // true
|
|
219
|
+
* notesLessThan("C5", "C4") // false
|
|
220
|
+
*/
|
|
124
221
|
export function notesLessThan(a, b) {
|
|
125
222
|
return compareNotes(a, b) < 0;
|
|
126
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Checks if the first note is higher than the second.
|
|
226
|
+
* @param a - First note string with octave
|
|
227
|
+
* @param b - Second note string with octave
|
|
228
|
+
* @returns True if a is higher than b
|
|
229
|
+
* @example
|
|
230
|
+
* notesGreaterThan("D4", "C4") // true
|
|
231
|
+
* notesGreaterThan("C4", "D4") // false
|
|
232
|
+
*/
|
|
127
233
|
export function notesGreaterThan(a, b) {
|
|
128
234
|
return compareNotes(a, b) > 0;
|
|
129
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* Represents a musical key signature with a given number of sharps or flats.
|
|
238
|
+
* Positive count = sharps, negative count = flats, zero = C major/A minor.
|
|
239
|
+
* @example
|
|
240
|
+
* const gMajor = new KeySignature(1) // G major (1 sharp)
|
|
241
|
+
* const fMajor = new KeySignature(-1) // F major (1 flat)
|
|
242
|
+
* gMajor.name() // "G"
|
|
243
|
+
* gMajor.accidentalNotes() // ["F"]
|
|
244
|
+
*/
|
|
130
245
|
export class KeySignature {
|
|
131
|
-
|
|
246
|
+
/**
|
|
247
|
+
* Returns all standard key signatures (excludes chromatic).
|
|
248
|
+
* Uses flat spellings for 6 accidentals (Gb instead of F#).
|
|
249
|
+
* @returns Array of KeySignature instances for standard major keys
|
|
250
|
+
*/
|
|
132
251
|
static allKeySignatures() {
|
|
133
252
|
return [
|
|
134
253
|
0, 1, 2, 3, 4, 5, -1, -2, -3, -4, -5, -6
|
|
135
254
|
].map(key => new KeySignature(key));
|
|
136
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Gets a cached KeySignature instance for the given accidental count.
|
|
258
|
+
* @param count - Number of accidentals (positive = sharps, negative = flats)
|
|
259
|
+
* @returns KeySignature instance, or undefined if count is out of range
|
|
260
|
+
*/
|
|
137
261
|
static forCount(count) {
|
|
138
262
|
if (!this.cache) {
|
|
139
263
|
this.cache = this.allKeySignatures();
|
|
@@ -144,22 +268,39 @@ export class KeySignature {
|
|
|
144
268
|
}
|
|
145
269
|
}
|
|
146
270
|
}
|
|
147
|
-
|
|
271
|
+
/**
|
|
272
|
+
* Creates a new KeySignature.
|
|
273
|
+
* @param count - Number of accidentals (positive = sharps, negative = flats)
|
|
274
|
+
*/
|
|
148
275
|
constructor(count) {
|
|
149
276
|
this.count = count;
|
|
150
277
|
}
|
|
278
|
+
/** Returns the number of accidentals in this key signature. */
|
|
151
279
|
getCount() {
|
|
152
280
|
return this.count;
|
|
153
281
|
}
|
|
282
|
+
/** Returns true if this is a chromatic key signature. */
|
|
154
283
|
isChromatic() {
|
|
155
284
|
return false;
|
|
156
285
|
}
|
|
286
|
+
/** Returns true if this key has sharps. */
|
|
157
287
|
isSharp() {
|
|
158
288
|
return this.count > 0;
|
|
159
289
|
}
|
|
290
|
+
/** Returns true if this key has flats. */
|
|
160
291
|
isFlat() {
|
|
161
292
|
return this.count < 0;
|
|
162
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* Returns the name of the major key (e.g., "G", "F", "Bb").
|
|
296
|
+
* @returns Key name string
|
|
297
|
+
* @example
|
|
298
|
+
* new KeySignature(0).name() // => "C"
|
|
299
|
+
* new KeySignature(1).name() // => "G" (1 sharp)
|
|
300
|
+
* new KeySignature(2).name() // => "D" (2 sharps)
|
|
301
|
+
* new KeySignature(-1).name() // => "F" (1 flat)
|
|
302
|
+
* new KeySignature(-2).name() // => "Bb" (2 flats)
|
|
303
|
+
*/
|
|
163
304
|
name() {
|
|
164
305
|
let offset = this.count + 1;
|
|
165
306
|
if (offset < 0) {
|
|
@@ -167,18 +308,33 @@ export class KeySignature {
|
|
|
167
308
|
}
|
|
168
309
|
return KeySignature.FIFTHS[offset];
|
|
169
310
|
}
|
|
311
|
+
/** Returns the key name as a string. */
|
|
170
312
|
toString() {
|
|
171
313
|
return this.name();
|
|
172
314
|
}
|
|
173
|
-
|
|
315
|
+
/**
|
|
316
|
+
* Returns the root note for building scales from this key signature.
|
|
317
|
+
* @returns Note name (e.g., "G", "F")
|
|
318
|
+
*/
|
|
174
319
|
scaleRoot() {
|
|
175
320
|
return this.name();
|
|
176
321
|
}
|
|
177
|
-
|
|
322
|
+
/**
|
|
323
|
+
* Returns the default scale for this key signature.
|
|
324
|
+
* @returns A MajorScale rooted on this key
|
|
325
|
+
*/
|
|
178
326
|
defaultScale() {
|
|
179
327
|
return new MajorScale(this);
|
|
180
328
|
}
|
|
181
|
-
|
|
329
|
+
/**
|
|
330
|
+
* Converts a note to its enharmonic equivalent that fits this key signature.
|
|
331
|
+
* Sharp keys convert flats to sharps; flat keys convert sharps to flats.
|
|
332
|
+
* @param note - Note string with octave
|
|
333
|
+
* @returns Enharmonic equivalent note string
|
|
334
|
+
* @example
|
|
335
|
+
* new KeySignature(1).enharmonic("Db4") // "C#4" (G major uses sharps)
|
|
336
|
+
* new KeySignature(-1).enharmonic("C#4") // "Db4" (F major uses flats)
|
|
337
|
+
*/
|
|
182
338
|
enharmonic(note) {
|
|
183
339
|
if (this.isFlat()) {
|
|
184
340
|
if (note.indexOf("#") != -1) {
|
|
@@ -192,7 +348,26 @@ export class KeySignature {
|
|
|
192
348
|
}
|
|
193
349
|
return note;
|
|
194
350
|
}
|
|
195
|
-
|
|
351
|
+
/**
|
|
352
|
+
* Converts a MIDI pitch to a note name with correct enharmonic spelling for this key.
|
|
353
|
+
* Uses sharps for sharp keys and flats for flat keys.
|
|
354
|
+
* @param pitch - MIDI pitch number
|
|
355
|
+
* @returns Note name string with appropriate accidentals for this key
|
|
356
|
+
* @example
|
|
357
|
+
* new KeySignature(1).noteName(61) // => "C#4" (G major uses sharps)
|
|
358
|
+
* new KeySignature(-1).noteName(61) // => "Db4" (F major uses flats)
|
|
359
|
+
* new KeySignature(0).noteName(61) // => "C#4" (C major defaults to sharps)
|
|
360
|
+
*/
|
|
361
|
+
noteName(pitch) {
|
|
362
|
+
return noteName(pitch, !this.isFlat());
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Returns the note letters that have accidentals in this key.
|
|
366
|
+
* @returns Array of note letters (e.g., ["F"] for G major, ["B", "E"] for Bb major)
|
|
367
|
+
* @example
|
|
368
|
+
* new KeySignature(1).accidentalNotes() // ["F"] (F# in G major)
|
|
369
|
+
* new KeySignature(-2).accidentalNotes() // ["B", "E"] (Bb, Eb)
|
|
370
|
+
*/
|
|
196
371
|
accidentalNotes() {
|
|
197
372
|
const fifths = KeySignature.FIFTHS_TRUNCATED;
|
|
198
373
|
if (this.count > 0) {
|
|
@@ -202,7 +377,12 @@ export class KeySignature {
|
|
|
202
377
|
return fifths.slice(fifths.length + this.count).reverse();
|
|
203
378
|
}
|
|
204
379
|
}
|
|
205
|
-
|
|
380
|
+
/**
|
|
381
|
+
* Converts a note without accidentals to its actual pitch in this key.
|
|
382
|
+
* For example, in G major, "F" becomes "F#".
|
|
383
|
+
* @param note - Note string or MIDI pitch
|
|
384
|
+
* @returns Note string with appropriate accidentals applied
|
|
385
|
+
*/
|
|
206
386
|
unconvertNote(note) {
|
|
207
387
|
if (this.count == 0) {
|
|
208
388
|
return typeof note === "number" ? noteName(note) : note;
|
|
@@ -222,12 +402,45 @@ export class KeySignature {
|
|
|
222
402
|
}
|
|
223
403
|
return note;
|
|
224
404
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
405
|
+
/**
|
|
406
|
+
* Converts a note with accidentals to its staff spelling in this key.
|
|
407
|
+
* This is the inverse of unconvertNote - it strips accidentals that are
|
|
408
|
+
* implied by the key signature.
|
|
409
|
+
*
|
|
410
|
+
* Note: This only strips # or b accidentals. Notes without accidentals are
|
|
411
|
+
* returned unchanged. To determine if a natural sign is needed (e.g., F
|
|
412
|
+
* natural in D major), use {@link accidentalsForNote} which returns 0 when
|
|
413
|
+
* a natural is required.
|
|
414
|
+
*
|
|
415
|
+
* @param note - Note string with accidentals
|
|
416
|
+
* @returns Note string as it would appear on a staff with this key signature
|
|
417
|
+
* @example
|
|
418
|
+
* // In G major (1 sharp on F):
|
|
419
|
+
* new KeySignature(1).convertNote("F#4") // "F4" (sharp implied by key)
|
|
420
|
+
* new KeySignature(1).convertNote("C#4") // "C#4" (not in key, keep accidental)
|
|
421
|
+
* // In F major (1 flat on B):
|
|
422
|
+
* new KeySignature(-1).convertNote("Bb4") // "B4" (flat implied by key)
|
|
423
|
+
* // Notes without accidentals are unchanged:
|
|
424
|
+
* new KeySignature(2).convertNote("F4") // "F4" (use accidentalsForNote to check if natural needed)
|
|
425
|
+
*/
|
|
426
|
+
convertNote(note) {
|
|
427
|
+
const accidental = this.accidentalsForNote(note);
|
|
428
|
+
if (accidental === null) {
|
|
429
|
+
// Note matches key signature, strip the accidental
|
|
430
|
+
return note.replace(/[#b]/, "");
|
|
431
|
+
}
|
|
432
|
+
return note;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Determines how many accidentals should display for a note in this key.
|
|
436
|
+
* @param note - Note string or MIDI pitch
|
|
437
|
+
* @returns null if no accidental needed, 0 for natural, 1 for sharp, -1 for flat, etc.
|
|
438
|
+
* @example
|
|
439
|
+
* // In G major (1 sharp on F):
|
|
440
|
+
* new KeySignature(1).accidentalsForNote("F#4") // null (already in key)
|
|
441
|
+
* new KeySignature(1).accidentalsForNote("F4") // 0 (natural needed)
|
|
442
|
+
* new KeySignature(1).accidentalsForNote("C#4") // 1 (sharp not in key)
|
|
443
|
+
*/
|
|
231
444
|
accidentalsForNote(note) {
|
|
232
445
|
if (typeof note == "number") {
|
|
233
446
|
note = noteName(note);
|
|
@@ -250,8 +463,13 @@ export class KeySignature {
|
|
|
250
463
|
}
|
|
251
464
|
return null;
|
|
252
465
|
}
|
|
253
|
-
|
|
254
|
-
|
|
466
|
+
/**
|
|
467
|
+
* Returns the notes that need accidentals within a given pitch range.
|
|
468
|
+
* The returned notes are natural note names at specific octaves.
|
|
469
|
+
* @param min - Minimum note (string or MIDI pitch)
|
|
470
|
+
* @param max - Maximum note (string or MIDI pitch)
|
|
471
|
+
* @returns Array of note strings that need accidentals in this range
|
|
472
|
+
*/
|
|
255
473
|
notesInRange(min, max) {
|
|
256
474
|
if (this.count == 0) {
|
|
257
475
|
return [];
|
|
@@ -293,34 +511,60 @@ export class KeySignature {
|
|
|
293
511
|
});
|
|
294
512
|
}
|
|
295
513
|
}
|
|
514
|
+
/** Circle of fifths note names */
|
|
296
515
|
KeySignature.FIFTHS = [
|
|
297
516
|
"F", "C", "G", "D", "A", "E", "B", "Gb", "Db", "Ab", "Eb", "Bb"
|
|
298
517
|
];
|
|
518
|
+
/** Natural note names in order of fifths (for accidental calculation) */
|
|
299
519
|
KeySignature.FIFTHS_TRUNCATED = [
|
|
300
520
|
"F", "C", "G", "D", "A", "E", "B"
|
|
301
521
|
];
|
|
302
522
|
KeySignature.cache = null;
|
|
523
|
+
/**
|
|
524
|
+
* A special key signature for chromatic contexts where all 12 notes are equally valid.
|
|
525
|
+
* Renders as C major (no accidentals in the key signature) but allows all chromatic notes.
|
|
526
|
+
*/
|
|
303
527
|
export class ChromaticKeySignature extends KeySignature {
|
|
304
528
|
constructor() {
|
|
305
529
|
super(0); // render as c major
|
|
306
530
|
}
|
|
531
|
+
/** Returns true (this is always a chromatic key signature). */
|
|
307
532
|
isChromatic() {
|
|
308
533
|
return true;
|
|
309
534
|
}
|
|
535
|
+
/** Returns "Chromatic" as the key name. */
|
|
310
536
|
name() {
|
|
311
537
|
return "Chromatic";
|
|
312
538
|
}
|
|
539
|
+
/** Returns "C" as the scale root. */
|
|
313
540
|
scaleRoot() {
|
|
314
541
|
return "C";
|
|
315
542
|
}
|
|
543
|
+
/** Returns a ChromaticScale as the default scale. */
|
|
316
544
|
defaultScale() {
|
|
317
545
|
return new ChromaticScale(this);
|
|
318
546
|
}
|
|
319
547
|
}
|
|
548
|
+
/**
|
|
549
|
+
* Base class for musical scales. A scale is defined by a root note and
|
|
550
|
+
* a pattern of intervals (steps) in semitones.
|
|
551
|
+
* @example
|
|
552
|
+
* const cMajor = new MajorScale("C")
|
|
553
|
+
* cMajor.getRange(4, 8) // ["C4", "D4", "E4", "F4", "G4", "A4", "B4", "C5"]
|
|
554
|
+
* cMajor.containsNote("D") // true
|
|
555
|
+
* cMajor.containsNote("Db") // false
|
|
556
|
+
*/
|
|
320
557
|
export class Scale {
|
|
558
|
+
/**
|
|
559
|
+
* Creates a new Scale.
|
|
560
|
+
* @param root - Root note as string (e.g., "C") or KeySignature
|
|
561
|
+
*/
|
|
321
562
|
constructor(root) {
|
|
563
|
+
/** Interval pattern in semitones (e.g., [2,2,1,2,2,2,1] for major) */
|
|
322
564
|
this.steps = [];
|
|
565
|
+
/** True if this is a minor scale (affects enharmonic spelling) */
|
|
323
566
|
this.minor = false;
|
|
567
|
+
/** True if this is a chromatic scale */
|
|
324
568
|
this.chromatic = false;
|
|
325
569
|
if (root instanceof KeySignature) {
|
|
326
570
|
root = root.scaleRoot();
|
|
@@ -330,9 +574,19 @@ export class Scale {
|
|
|
330
574
|
}
|
|
331
575
|
this.root = root;
|
|
332
576
|
}
|
|
577
|
+
/**
|
|
578
|
+
* Returns all notes in the scale across 8 octaves.
|
|
579
|
+
* @returns Array of note strings covering the full playable range
|
|
580
|
+
*/
|
|
333
581
|
getFullRange() {
|
|
334
582
|
return this.getRange(0, (this.steps.length + 1) * 8);
|
|
335
583
|
}
|
|
584
|
+
/**
|
|
585
|
+
* Returns scale notes within a pitch range (inclusive).
|
|
586
|
+
* @param min - Minimum note string (e.g., "C3")
|
|
587
|
+
* @param max - Maximum note string (e.g., "C6")
|
|
588
|
+
* @returns Array of scale notes within the range
|
|
589
|
+
*/
|
|
336
590
|
getLooseRange(min, max) {
|
|
337
591
|
const fullRange = this.getFullRange();
|
|
338
592
|
const minPitch = parseNote(min);
|
|
@@ -342,6 +596,16 @@ export class Scale {
|
|
|
342
596
|
return pitch >= minPitch && pitch <= maxPitch;
|
|
343
597
|
});
|
|
344
598
|
}
|
|
599
|
+
/**
|
|
600
|
+
* Returns a range of notes from the scale starting at a given octave.
|
|
601
|
+
* @param octave - Starting octave number
|
|
602
|
+
* @param count - Number of notes to return (default: one octave)
|
|
603
|
+
* @param offset - Scale degree offset (negative = start below root)
|
|
604
|
+
* @returns Array of note strings
|
|
605
|
+
* @example
|
|
606
|
+
* new MajorScale("C").getRange(4, 8) // ["C4", "D4", "E4", "F4", "G4", "A4", "B4", "C5"]
|
|
607
|
+
* new MajorScale("C").getRange(4, 3, 2) // ["E4", "F4", "G4"] (start from 3rd degree)
|
|
608
|
+
*/
|
|
345
609
|
getRange(octave, count = this.steps.length + 1, offset = 0) {
|
|
346
610
|
let current = parseNote(`${this.root}${octave}`);
|
|
347
611
|
const isFlat = this.isFlat();
|
|
@@ -363,6 +627,11 @@ export class Scale {
|
|
|
363
627
|
}
|
|
364
628
|
return range;
|
|
365
629
|
}
|
|
630
|
+
/**
|
|
631
|
+
* Determines if this scale should use flats for accidentals.
|
|
632
|
+
* Based on the circle of fifths position of the root.
|
|
633
|
+
* @returns True if the scale should use flats
|
|
634
|
+
*/
|
|
366
635
|
isFlat() {
|
|
367
636
|
let idx = KeySignature.FIFTHS.indexOf(this.root);
|
|
368
637
|
if (idx == -1) {
|
|
@@ -382,6 +651,14 @@ export class Scale {
|
|
|
382
651
|
}
|
|
383
652
|
return idx < 1 || idx > 6;
|
|
384
653
|
}
|
|
654
|
+
/**
|
|
655
|
+
* Checks if a note (any octave) belongs to this scale.
|
|
656
|
+
* @param note - Note string (with or without octave)
|
|
657
|
+
* @returns True if the note is in the scale
|
|
658
|
+
* @example
|
|
659
|
+
* new MajorScale("C").containsNote("D") // true
|
|
660
|
+
* new MajorScale("C").containsNote("Db") // false
|
|
661
|
+
*/
|
|
385
662
|
containsNote(note) {
|
|
386
663
|
let pitch = parseNoteOffset(note);
|
|
387
664
|
const rootPitch = parseNoteOffset(this.root);
|
|
@@ -404,7 +681,15 @@ export class Scale {
|
|
|
404
681
|
}
|
|
405
682
|
return false;
|
|
406
683
|
}
|
|
407
|
-
|
|
684
|
+
/**
|
|
685
|
+
* Converts a scale degree number to a note name (without octave).
|
|
686
|
+
* Degrees are 1-indexed: 1 = root, 2 = second, etc.
|
|
687
|
+
* @param degree - Scale degree (1-indexed)
|
|
688
|
+
* @returns Note name string (e.g., "C", "D", "E")
|
|
689
|
+
* @example
|
|
690
|
+
* new MajorScale("C").degreeToName(1) // "C"
|
|
691
|
+
* new MajorScale("C").degreeToName(5) // "G"
|
|
692
|
+
*/
|
|
408
693
|
degreeToName(degree) {
|
|
409
694
|
// truncate to reasonable range
|
|
410
695
|
degree = (degree - 1) % this.steps.length + 1;
|
|
@@ -413,7 +698,16 @@ export class Scale {
|
|
|
413
698
|
const m = note.match(/^[^\d]+/);
|
|
414
699
|
return m ? m[0] : note;
|
|
415
700
|
}
|
|
416
|
-
|
|
701
|
+
/**
|
|
702
|
+
* Gets the scale degree of a note.
|
|
703
|
+
* Degrees are 1-indexed: root = 1, second = 2, etc.
|
|
704
|
+
* @param note - Note string (with or without octave)
|
|
705
|
+
* @returns Scale degree number
|
|
706
|
+
* @throws Error if note is not in the scale
|
|
707
|
+
* @example
|
|
708
|
+
* new MajorScale("C").getDegree("C") // 1
|
|
709
|
+
* new MajorScale("C").getDegree("G") // 5
|
|
710
|
+
*/
|
|
417
711
|
getDegree(note) {
|
|
418
712
|
let pitch = parseNoteOffset(note);
|
|
419
713
|
const rootPitch = parseNoteOffset(this.root);
|
|
@@ -441,8 +735,15 @@ export class Scale {
|
|
|
441
735
|
}
|
|
442
736
|
throw new Error(`${note} is not in scale ${this.root}`);
|
|
443
737
|
}
|
|
444
|
-
|
|
445
|
-
|
|
738
|
+
/**
|
|
739
|
+
* Builds chord intervals by stacking thirds from a scale degree.
|
|
740
|
+
* @param degree - Starting scale degree (1-indexed)
|
|
741
|
+
* @param count - Number of intervals to generate (2 = triad, 3 = seventh chord)
|
|
742
|
+
* @returns Array of intervals in semitones
|
|
743
|
+
* @example
|
|
744
|
+
* new MajorScale("C").buildChordSteps(1, 2) // [4, 3] (C major triad intervals)
|
|
745
|
+
* new MajorScale("C").buildChordSteps(2, 2) // [3, 4] (D minor triad intervals)
|
|
746
|
+
*/
|
|
446
747
|
buildChordSteps(degree, count) {
|
|
447
748
|
let idx = degree - 1;
|
|
448
749
|
const out = [];
|
|
@@ -459,7 +760,13 @@ export class Scale {
|
|
|
459
760
|
}
|
|
460
761
|
return out;
|
|
461
762
|
}
|
|
462
|
-
|
|
763
|
+
/**
|
|
764
|
+
* Generates all diatonic chords in this scale.
|
|
765
|
+
* @param noteCount - Number of notes per chord (3 = triads, 4 = seventh chords)
|
|
766
|
+
* @returns Array of Chord instances built on each scale degree
|
|
767
|
+
* @example
|
|
768
|
+
* new MajorScale("C").allChords(3) // [C, Dm, Em, F, G, Am, Bdim]
|
|
769
|
+
*/
|
|
463
770
|
allChords(noteCount = 3) {
|
|
464
771
|
const out = [];
|
|
465
772
|
for (let i = 0; i < this.steps.length; i++) {
|
|
@@ -471,13 +778,22 @@ export class Scale {
|
|
|
471
778
|
return out;
|
|
472
779
|
}
|
|
473
780
|
}
|
|
781
|
+
/**
|
|
782
|
+
* Major scale with the interval pattern W-W-H-W-W-W-H (whole and half steps).
|
|
783
|
+
* @example
|
|
784
|
+
* new MajorScale("C").getRange(4, 8) // ["C4", "D4", "E4", "F4", "G4", "A4", "B4", "C5"]
|
|
785
|
+
*/
|
|
474
786
|
export class MajorScale extends Scale {
|
|
475
787
|
constructor(root) {
|
|
476
788
|
super(root);
|
|
477
789
|
this.steps = [2, 2, 1, 2, 2, 2, 1];
|
|
478
790
|
}
|
|
479
791
|
}
|
|
480
|
-
|
|
792
|
+
/**
|
|
793
|
+
* Natural minor scale (Aeolian mode) with pattern W-H-W-W-H-W-W.
|
|
794
|
+
* @example
|
|
795
|
+
* new MinorScale("A").getRange(4, 8) // ["A4", "B4", "C5", "D5", "E5", "F5", "G5", "A5"]
|
|
796
|
+
*/
|
|
481
797
|
export class MinorScale extends Scale {
|
|
482
798
|
constructor(root) {
|
|
483
799
|
super(root);
|
|
@@ -485,6 +801,10 @@ export class MinorScale extends Scale {
|
|
|
485
801
|
this.steps = [2, 1, 2, 2, 1, 2, 2];
|
|
486
802
|
}
|
|
487
803
|
}
|
|
804
|
+
/**
|
|
805
|
+
* Harmonic minor scale with raised 7th degree.
|
|
806
|
+
* Pattern: W-H-W-W-H-A2-H (A2 = augmented second, 3 semitones).
|
|
807
|
+
*/
|
|
488
808
|
export class HarmonicMinorScale extends Scale {
|
|
489
809
|
constructor(root) {
|
|
490
810
|
super(root);
|
|
@@ -492,6 +812,10 @@ export class HarmonicMinorScale extends Scale {
|
|
|
492
812
|
this.steps = [2, 1, 2, 2, 1, 3, 1];
|
|
493
813
|
}
|
|
494
814
|
}
|
|
815
|
+
/**
|
|
816
|
+
* Ascending melodic minor scale with raised 6th and 7th degrees.
|
|
817
|
+
* Pattern: W-H-W-W-W-W-H.
|
|
818
|
+
*/
|
|
495
819
|
export class AscendingMelodicMinorScale extends Scale {
|
|
496
820
|
constructor(root) {
|
|
497
821
|
super(root);
|
|
@@ -499,21 +823,30 @@ export class AscendingMelodicMinorScale extends Scale {
|
|
|
499
823
|
this.steps = [2, 1, 2, 2, 2, 2, 1];
|
|
500
824
|
}
|
|
501
825
|
}
|
|
826
|
+
/**
|
|
827
|
+
* Major blues scale (6 notes).
|
|
828
|
+
* Notes in C: C, D, Eb, E, G, A.
|
|
829
|
+
*/
|
|
502
830
|
export class MajorBluesScale extends Scale {
|
|
503
831
|
constructor(root) {
|
|
504
832
|
super(root);
|
|
505
|
-
// C, D, D#/Eb, E, G, A
|
|
506
833
|
this.steps = [2, 1, 1, 3, 2, 3];
|
|
507
834
|
}
|
|
508
835
|
}
|
|
836
|
+
/**
|
|
837
|
+
* Minor blues scale (6 notes).
|
|
838
|
+
* Notes in C: C, Eb, F, Gb, G, Bb.
|
|
839
|
+
*/
|
|
509
840
|
export class MinorBluesScale extends Scale {
|
|
510
841
|
constructor(root) {
|
|
511
842
|
super(root);
|
|
512
843
|
this.minor = true;
|
|
513
|
-
// C, D#/Eb, F, F#/Gb, G, Bb
|
|
514
844
|
this.steps = [3, 2, 1, 1, 3, 2];
|
|
515
845
|
}
|
|
516
846
|
}
|
|
847
|
+
/**
|
|
848
|
+
* Chromatic scale containing all 12 semitones.
|
|
849
|
+
*/
|
|
517
850
|
export class ChromaticScale extends Scale {
|
|
518
851
|
constructor(root) {
|
|
519
852
|
super(root);
|
|
@@ -521,8 +854,27 @@ export class ChromaticScale extends Scale {
|
|
|
521
854
|
this.steps = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
|
|
522
855
|
}
|
|
523
856
|
}
|
|
857
|
+
/**
|
|
858
|
+
* Represents a musical chord as a special kind of scale.
|
|
859
|
+
* Chords are defined by a root note and interval pattern.
|
|
860
|
+
* @example
|
|
861
|
+
* const cMajor = new Chord("C", "M")
|
|
862
|
+
* cMajor.getRange(4, 3) // ["C4", "E4", "G4"]
|
|
863
|
+
* Chord.notes("C4", "m7") // ["C4", "Eb4", "G4", "Bb4"]
|
|
864
|
+
*/
|
|
524
865
|
export class Chord extends Scale {
|
|
525
|
-
|
|
866
|
+
/**
|
|
867
|
+
* Static helper to get chord notes at a specific position.
|
|
868
|
+
* @param note - Root note with octave (e.g., "C4")
|
|
869
|
+
* @param chordName - Chord type from SHAPES (e.g., "M", "m7")
|
|
870
|
+
* @param inversion - Inversion number (0 = root position, 1 = first inversion, etc.)
|
|
871
|
+
* @param notesCount - Number of notes to return (0 = all chord tones)
|
|
872
|
+
* @returns Array of note strings
|
|
873
|
+
* @throws Error if note format is invalid or chordName is unknown
|
|
874
|
+
* @example
|
|
875
|
+
* Chord.notes("C4", "M") // ["C4", "E4", "G4"]
|
|
876
|
+
* Chord.notes("C4", "M", 1) // ["E4", "G4", "C5"] (first inversion)
|
|
877
|
+
*/
|
|
526
878
|
static notes(note, chordName, inversion = 0, notesCount = 0) {
|
|
527
879
|
const match = note.match(/^([^\d]+)(\d+)$/);
|
|
528
880
|
if (!match) {
|
|
@@ -534,21 +886,33 @@ export class Chord extends Scale {
|
|
|
534
886
|
if (notesCount == 0) {
|
|
535
887
|
notesCount = intervals.length + 1;
|
|
536
888
|
}
|
|
537
|
-
return new Chord(root, intervals).getRange(octave, notesCount, inversion);
|
|
889
|
+
return new Chord(root, [...intervals]).getRange(octave, notesCount, inversion);
|
|
538
890
|
}
|
|
891
|
+
/**
|
|
892
|
+
* Creates a new Chord.
|
|
893
|
+
* @param root - Root note as string (e.g., "C") or KeySignature
|
|
894
|
+
* @param intervals - Chord shape name (e.g., "M", "m7") or array of intervals in semitones
|
|
895
|
+
* @example
|
|
896
|
+
* new Chord("C", "M") // C major from shape name
|
|
897
|
+
* new Chord("C", [4, 3]) // C major from intervals
|
|
898
|
+
*/
|
|
539
899
|
constructor(root, intervals) {
|
|
540
900
|
super(root);
|
|
901
|
+
let steps;
|
|
541
902
|
if (typeof intervals === "string") {
|
|
542
903
|
const shape = Chord.SHAPES[intervals];
|
|
543
904
|
if (!shape) {
|
|
544
905
|
throw new Error(`Unknown chord shape: ${intervals}`);
|
|
545
906
|
}
|
|
546
|
-
|
|
907
|
+
steps = [...shape];
|
|
547
908
|
}
|
|
548
|
-
|
|
909
|
+
else {
|
|
910
|
+
steps = [...intervals];
|
|
911
|
+
}
|
|
912
|
+
if (!steps.length) {
|
|
549
913
|
throw new Error("Missing intervals for chord");
|
|
550
914
|
}
|
|
551
|
-
this.steps =
|
|
915
|
+
this.steps = steps;
|
|
552
916
|
// add wrapping interval to get back to octave
|
|
553
917
|
let sum = 0;
|
|
554
918
|
for (const i of this.steps) {
|
|
@@ -560,7 +924,10 @@ export class Chord extends Scale {
|
|
|
560
924
|
}
|
|
561
925
|
this.steps.push(rest);
|
|
562
926
|
}
|
|
563
|
-
|
|
927
|
+
/**
|
|
928
|
+
* Checks if this chord functions as a dominant (major or dominant 7th).
|
|
929
|
+
* @returns True if chord is major triad or dominant 7th
|
|
930
|
+
*/
|
|
564
931
|
isDominant() {
|
|
565
932
|
const shapeName = this.chordShapeName();
|
|
566
933
|
if (shapeName == "M" || shapeName == "7") {
|
|
@@ -568,8 +935,13 @@ export class Chord extends Scale {
|
|
|
568
935
|
}
|
|
569
936
|
return false;
|
|
570
937
|
}
|
|
571
|
-
|
|
572
|
-
|
|
938
|
+
/**
|
|
939
|
+
* Gets possible resolution targets for this chord as a secondary dominant.
|
|
940
|
+
* A secondary dominant resolves down a fifth (up a fourth).
|
|
941
|
+
* @param noteCount - Number of notes in target chords (3 = triads, 4 = sevenths)
|
|
942
|
+
* @returns Array of possible target Chords (major and minor variants)
|
|
943
|
+
* @throws Error if this chord is not a dominant type
|
|
944
|
+
*/
|
|
573
945
|
getSecondaryDominantTargets(noteCount = 3) {
|
|
574
946
|
if (!this.isDominant()) {
|
|
575
947
|
throw new Error(`chord is not dominant to begin with: ${this.chordShapeName()}`);
|
|
@@ -590,8 +962,12 @@ export class Chord extends Scale {
|
|
|
590
962
|
}
|
|
591
963
|
throw new Error(`don't know how to get secondary dominant for note count: ${noteCount}`);
|
|
592
964
|
}
|
|
965
|
+
/**
|
|
966
|
+
* Gets the name of this chord's shape from SHAPES (e.g., "M", "m7", "dim").
|
|
967
|
+
* @returns Shape name string, or undefined if no matching shape found
|
|
968
|
+
*/
|
|
593
969
|
chordShapeName() {
|
|
594
|
-
for (const shape
|
|
970
|
+
for (const shape of Object.keys(Chord.SHAPES)) {
|
|
595
971
|
const intervals = Chord.SHAPES[shape];
|
|
596
972
|
if (this.steps.length - 1 != intervals.length) {
|
|
597
973
|
continue;
|
|
@@ -608,7 +984,14 @@ export class Chord extends Scale {
|
|
|
608
984
|
}
|
|
609
985
|
}
|
|
610
986
|
}
|
|
611
|
-
|
|
987
|
+
/**
|
|
988
|
+
* Checks if all given notes belong to this chord.
|
|
989
|
+
* @param notes - Array of note strings to check
|
|
990
|
+
* @returns True if all notes are chord tones
|
|
991
|
+
* @example
|
|
992
|
+
* new Chord("C", "M").containsNotes(["C4", "E4", "G4"]) // true
|
|
993
|
+
* new Chord("C", "M").containsNotes(["C4", "F4"]) // false
|
|
994
|
+
*/
|
|
612
995
|
containsNotes(notes) {
|
|
613
996
|
if (!notes.length) {
|
|
614
997
|
return false;
|
|
@@ -620,7 +1003,11 @@ export class Chord extends Scale {
|
|
|
620
1003
|
}
|
|
621
1004
|
return true;
|
|
622
1005
|
}
|
|
623
|
-
|
|
1006
|
+
/**
|
|
1007
|
+
* Counts how many notes two chords have in common.
|
|
1008
|
+
* @param otherChord - Chord to compare with
|
|
1009
|
+
* @returns Number of shared notes
|
|
1010
|
+
*/
|
|
624
1011
|
countSharedNotes(otherChord) {
|
|
625
1012
|
const myNotes = this.getRange(5, this.steps.length);
|
|
626
1013
|
const theirNotes = otherChord.getRange(5, this.steps.length);
|
|
@@ -639,22 +1026,37 @@ export class Chord extends Scale {
|
|
|
639
1026
|
}
|
|
640
1027
|
return count;
|
|
641
1028
|
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Returns the chord name as a string (e.g., "C", "Am7", "Bdim").
|
|
1031
|
+
* @returns Chord name with root and quality
|
|
1032
|
+
*/
|
|
642
1033
|
toString() {
|
|
643
|
-
|
|
644
|
-
if (!
|
|
1034
|
+
const shapeName = this.chordShapeName();
|
|
1035
|
+
if (!shapeName) {
|
|
645
1036
|
console.warn("don't know name of chord", this.root, this.steps, this.getRange(5, 3));
|
|
646
|
-
|
|
1037
|
+
return this.root;
|
|
647
1038
|
}
|
|
648
|
-
if (
|
|
649
|
-
|
|
1039
|
+
if (shapeName == "M") {
|
|
1040
|
+
return this.root;
|
|
650
1041
|
}
|
|
651
|
-
return `${this.root}${
|
|
1042
|
+
return `${this.root}${shapeName}`;
|
|
652
1043
|
}
|
|
653
1044
|
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Predefined chord shapes as interval arrays (in semitones).
|
|
1047
|
+
* - M: Major triad [4, 3]
|
|
1048
|
+
* - m: Minor triad [3, 4]
|
|
1049
|
+
* - dim: Diminished triad [3, 3]
|
|
1050
|
+
* - aug: Augmented triad [4, 4]
|
|
1051
|
+
* - 7: Dominant 7th [4, 3, 3]
|
|
1052
|
+
* - M7: Major 7th [4, 3, 4]
|
|
1053
|
+
* - m7: Minor 7th [3, 4, 3]
|
|
1054
|
+
* - And more...
|
|
1055
|
+
*/
|
|
654
1056
|
Chord.SHAPES = {
|
|
655
1057
|
"M": [4, 3],
|
|
656
1058
|
"m": [3, 4],
|
|
657
|
-
"dim": [3, 3],
|
|
1059
|
+
"dim": [3, 3],
|
|
658
1060
|
"dimM7": [3, 3, 5],
|
|
659
1061
|
"dim7": [3, 3, 3],
|
|
660
1062
|
"aug": [4, 4],
|
|
@@ -666,32 +1068,57 @@ Chord.SHAPES = {
|
|
|
666
1068
|
"m7": [3, 4, 3],
|
|
667
1069
|
"m7b5": [3, 3, 4],
|
|
668
1070
|
"mM7": [3, 4, 4],
|
|
669
|
-
|
|
670
|
-
"Q": [5, 5], // quartal
|
|
1071
|
+
"Q": [5, 5],
|
|
671
1072
|
"Qb4": [4, 5],
|
|
672
1073
|
};
|
|
1074
|
+
/**
|
|
1075
|
+
* Represents a musical staff with clef and note range information.
|
|
1076
|
+
* Used for rendering notes on sheet music.
|
|
1077
|
+
* @example
|
|
1078
|
+
* const treble = Staff.forName("treble")
|
|
1079
|
+
* treble.lowerNote // "E5" (bottom line)
|
|
1080
|
+
* treble.upperNote // "F6" (top line)
|
|
1081
|
+
*/
|
|
673
1082
|
export class Staff {
|
|
1083
|
+
/**
|
|
1084
|
+
* Gets a Staff instance by name.
|
|
1085
|
+
* @param name - Staff name ("treble" or "bass")
|
|
1086
|
+
* @returns Staff instance, or undefined if not found
|
|
1087
|
+
*/
|
|
674
1088
|
static forName(name) {
|
|
675
1089
|
if (!this.cache) {
|
|
676
1090
|
this.cache = Object.fromEntries(this.allStaves().map(s => [s.name, s]));
|
|
677
1091
|
}
|
|
678
1092
|
return this.cache[name];
|
|
679
1093
|
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Returns all available staff types.
|
|
1096
|
+
* @returns Array of Staff instances
|
|
1097
|
+
*/
|
|
680
1098
|
static allStaves() {
|
|
1099
|
+
// TODO: alto, middle C center
|
|
681
1100
|
return [
|
|
682
1101
|
new Staff("treble", "E5", "F6", "G5"),
|
|
683
1102
|
new Staff("bass", "G3", "A4", "F4")
|
|
684
|
-
// TODO: alto, middle C center
|
|
685
1103
|
];
|
|
686
1104
|
}
|
|
687
|
-
|
|
1105
|
+
/**
|
|
1106
|
+
* Creates a new Staff.
|
|
1107
|
+
* @param name - Staff identifier
|
|
1108
|
+
* @param lowerNote - Note on bottom line (e.g., "E5" for treble)
|
|
1109
|
+
* @param upperNote - Note on top line (e.g., "F6" for treble)
|
|
1110
|
+
* @param clefNote - Note at clef position (e.g., "G5" for treble G-clef)
|
|
1111
|
+
*/
|
|
688
1112
|
constructor(name, lowerNote, upperNote, clefNote) {
|
|
689
1113
|
this.name = name;
|
|
690
1114
|
this.lowerNote = lowerNote;
|
|
691
1115
|
this.upperNote = upperNote;
|
|
692
1116
|
this.clefNote = clefNote;
|
|
693
1117
|
}
|
|
694
|
-
|
|
1118
|
+
/**
|
|
1119
|
+
* Gets the letter name of the clef (e.g., "G" for treble, "F" for bass).
|
|
1120
|
+
* @returns Single letter clef name
|
|
1121
|
+
*/
|
|
695
1122
|
clefName() {
|
|
696
1123
|
const match = this.clefNote.match(/^([A-G])/);
|
|
697
1124
|
if (!match) {
|
|
@@ -701,4 +1128,24 @@ export class Staff {
|
|
|
701
1128
|
}
|
|
702
1129
|
}
|
|
703
1130
|
Staff.cache = null;
|
|
1131
|
+
/**
|
|
1132
|
+
* Transposes a key signature by a given number of semitones using circle of fifths.
|
|
1133
|
+
* Each semitone = 7 steps on the circle of fifths (mod 12).
|
|
1134
|
+
* @param currentKs - Current key signature count (positive = sharps, negative = flats)
|
|
1135
|
+
* @param semitones - Number of semitones to transpose (positive = up, negative = down)
|
|
1136
|
+
* @returns New key signature count, normalized to -6..5 range
|
|
1137
|
+
* @example
|
|
1138
|
+
* transposeKeySignature(0, 1) // -5 (C major + 1 semitone = Db major)
|
|
1139
|
+
* transposeKeySignature(1, 1) // -4 (G major + 1 semitone = Ab major)
|
|
1140
|
+
* transposeKeySignature(0, 12) // 0 (octave transposition = no change)
|
|
1141
|
+
*/
|
|
1142
|
+
export function transposeKeySignature(currentKs, semitones) {
|
|
1143
|
+
if (semitones % 12 === 0)
|
|
1144
|
+
return currentKs; // Octave = no key change
|
|
1145
|
+
let newKs = currentKs + (semitones * 7);
|
|
1146
|
+
newKs = ((newKs % 12) + 12) % 12; // Normalize to 0..11
|
|
1147
|
+
if (newKs > 6)
|
|
1148
|
+
newKs -= 12; // Normalize to -6..5
|
|
1149
|
+
return newKs;
|
|
1150
|
+
}
|
|
704
1151
|
//# sourceMappingURL=music.js.map
|