@leafo/lml 0.2.0 → 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 +31 -17
- 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 +374 -246
- package/dist/grammar.js.map +1 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/music.d.ts +501 -5
- package/dist/music.d.ts.map +1 -1
- package/dist/music.js +495 -52
- 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 +1 -0
- package/dist/noteUtils.d.ts.map +1 -1
- package/dist/noteUtils.js +8 -2
- package/dist/noteUtils.js.map +1 -1
- package/dist/parser.d.ts +135 -2
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +137 -36
- package/dist/parser.js.map +1 -1
- package/dist/song.d.ts +197 -1
- package/dist/song.d.ts.map +1 -1
- package/dist/song.js +213 -13
- package/dist/song.js.map +1 -1
- package/package.json +5 -2
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,6 +66,16 @@ 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
80
|
const octave = Math.floor(pitch / OCTAVE_SIZE) - 1;
|
|
39
81
|
const offset = pitch - (octave + 1) * OCTAVE_SIZE;
|
|
@@ -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) {
|
|
@@ -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,11 +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
|
+
*/
|
|
196
361
|
noteName(pitch) {
|
|
197
362
|
return noteName(pitch, !this.isFlat());
|
|
198
363
|
}
|
|
199
|
-
|
|
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
|
+
*/
|
|
200
371
|
accidentalNotes() {
|
|
201
372
|
const fifths = KeySignature.FIFTHS_TRUNCATED;
|
|
202
373
|
if (this.count > 0) {
|
|
@@ -206,7 +377,12 @@ export class KeySignature {
|
|
|
206
377
|
return fifths.slice(fifths.length + this.count).reverse();
|
|
207
378
|
}
|
|
208
379
|
}
|
|
209
|
-
|
|
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
|
+
*/
|
|
210
386
|
unconvertNote(note) {
|
|
211
387
|
if (this.count == 0) {
|
|
212
388
|
return typeof note === "number" ? noteName(note) : note;
|
|
@@ -226,12 +402,45 @@ export class KeySignature {
|
|
|
226
402
|
}
|
|
227
403
|
return note;
|
|
228
404
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
+
*/
|
|
235
444
|
accidentalsForNote(note) {
|
|
236
445
|
if (typeof note == "number") {
|
|
237
446
|
note = noteName(note);
|
|
@@ -254,8 +463,13 @@ export class KeySignature {
|
|
|
254
463
|
}
|
|
255
464
|
return null;
|
|
256
465
|
}
|
|
257
|
-
|
|
258
|
-
|
|
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
|
+
*/
|
|
259
473
|
notesInRange(min, max) {
|
|
260
474
|
if (this.count == 0) {
|
|
261
475
|
return [];
|
|
@@ -297,34 +511,60 @@ export class KeySignature {
|
|
|
297
511
|
});
|
|
298
512
|
}
|
|
299
513
|
}
|
|
514
|
+
/** Circle of fifths note names */
|
|
300
515
|
KeySignature.FIFTHS = [
|
|
301
516
|
"F", "C", "G", "D", "A", "E", "B", "Gb", "Db", "Ab", "Eb", "Bb"
|
|
302
517
|
];
|
|
518
|
+
/** Natural note names in order of fifths (for accidental calculation) */
|
|
303
519
|
KeySignature.FIFTHS_TRUNCATED = [
|
|
304
520
|
"F", "C", "G", "D", "A", "E", "B"
|
|
305
521
|
];
|
|
306
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
|
+
*/
|
|
307
527
|
export class ChromaticKeySignature extends KeySignature {
|
|
308
528
|
constructor() {
|
|
309
529
|
super(0); // render as c major
|
|
310
530
|
}
|
|
531
|
+
/** Returns true (this is always a chromatic key signature). */
|
|
311
532
|
isChromatic() {
|
|
312
533
|
return true;
|
|
313
534
|
}
|
|
535
|
+
/** Returns "Chromatic" as the key name. */
|
|
314
536
|
name() {
|
|
315
537
|
return "Chromatic";
|
|
316
538
|
}
|
|
539
|
+
/** Returns "C" as the scale root. */
|
|
317
540
|
scaleRoot() {
|
|
318
541
|
return "C";
|
|
319
542
|
}
|
|
543
|
+
/** Returns a ChromaticScale as the default scale. */
|
|
320
544
|
defaultScale() {
|
|
321
545
|
return new ChromaticScale(this);
|
|
322
546
|
}
|
|
323
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
|
+
*/
|
|
324
557
|
export class Scale {
|
|
558
|
+
/**
|
|
559
|
+
* Creates a new Scale.
|
|
560
|
+
* @param root - Root note as string (e.g., "C") or KeySignature
|
|
561
|
+
*/
|
|
325
562
|
constructor(root) {
|
|
563
|
+
/** Interval pattern in semitones (e.g., [2,2,1,2,2,2,1] for major) */
|
|
326
564
|
this.steps = [];
|
|
565
|
+
/** True if this is a minor scale (affects enharmonic spelling) */
|
|
327
566
|
this.minor = false;
|
|
567
|
+
/** True if this is a chromatic scale */
|
|
328
568
|
this.chromatic = false;
|
|
329
569
|
if (root instanceof KeySignature) {
|
|
330
570
|
root = root.scaleRoot();
|
|
@@ -334,9 +574,19 @@ export class Scale {
|
|
|
334
574
|
}
|
|
335
575
|
this.root = root;
|
|
336
576
|
}
|
|
577
|
+
/**
|
|
578
|
+
* Returns all notes in the scale across 8 octaves.
|
|
579
|
+
* @returns Array of note strings covering the full playable range
|
|
580
|
+
*/
|
|
337
581
|
getFullRange() {
|
|
338
582
|
return this.getRange(0, (this.steps.length + 1) * 8);
|
|
339
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
|
+
*/
|
|
340
590
|
getLooseRange(min, max) {
|
|
341
591
|
const fullRange = this.getFullRange();
|
|
342
592
|
const minPitch = parseNote(min);
|
|
@@ -346,6 +596,16 @@ export class Scale {
|
|
|
346
596
|
return pitch >= minPitch && pitch <= maxPitch;
|
|
347
597
|
});
|
|
348
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
|
+
*/
|
|
349
609
|
getRange(octave, count = this.steps.length + 1, offset = 0) {
|
|
350
610
|
let current = parseNote(`${this.root}${octave}`);
|
|
351
611
|
const isFlat = this.isFlat();
|
|
@@ -367,6 +627,11 @@ export class Scale {
|
|
|
367
627
|
}
|
|
368
628
|
return range;
|
|
369
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
|
+
*/
|
|
370
635
|
isFlat() {
|
|
371
636
|
let idx = KeySignature.FIFTHS.indexOf(this.root);
|
|
372
637
|
if (idx == -1) {
|
|
@@ -386,6 +651,14 @@ export class Scale {
|
|
|
386
651
|
}
|
|
387
652
|
return idx < 1 || idx > 6;
|
|
388
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
|
+
*/
|
|
389
662
|
containsNote(note) {
|
|
390
663
|
let pitch = parseNoteOffset(note);
|
|
391
664
|
const rootPitch = parseNoteOffset(this.root);
|
|
@@ -408,7 +681,15 @@ export class Scale {
|
|
|
408
681
|
}
|
|
409
682
|
return false;
|
|
410
683
|
}
|
|
411
|
-
|
|
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
|
+
*/
|
|
412
693
|
degreeToName(degree) {
|
|
413
694
|
// truncate to reasonable range
|
|
414
695
|
degree = (degree - 1) % this.steps.length + 1;
|
|
@@ -417,7 +698,16 @@ export class Scale {
|
|
|
417
698
|
const m = note.match(/^[^\d]+/);
|
|
418
699
|
return m ? m[0] : note;
|
|
419
700
|
}
|
|
420
|
-
|
|
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
|
+
*/
|
|
421
711
|
getDegree(note) {
|
|
422
712
|
let pitch = parseNoteOffset(note);
|
|
423
713
|
const rootPitch = parseNoteOffset(this.root);
|
|
@@ -445,8 +735,15 @@ export class Scale {
|
|
|
445
735
|
}
|
|
446
736
|
throw new Error(`${note} is not in scale ${this.root}`);
|
|
447
737
|
}
|
|
448
|
-
|
|
449
|
-
|
|
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
|
+
*/
|
|
450
747
|
buildChordSteps(degree, count) {
|
|
451
748
|
let idx = degree - 1;
|
|
452
749
|
const out = [];
|
|
@@ -463,7 +760,13 @@ export class Scale {
|
|
|
463
760
|
}
|
|
464
761
|
return out;
|
|
465
762
|
}
|
|
466
|
-
|
|
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
|
+
*/
|
|
467
770
|
allChords(noteCount = 3) {
|
|
468
771
|
const out = [];
|
|
469
772
|
for (let i = 0; i < this.steps.length; i++) {
|
|
@@ -475,13 +778,22 @@ export class Scale {
|
|
|
475
778
|
return out;
|
|
476
779
|
}
|
|
477
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
|
+
*/
|
|
478
786
|
export class MajorScale extends Scale {
|
|
479
787
|
constructor(root) {
|
|
480
788
|
super(root);
|
|
481
789
|
this.steps = [2, 2, 1, 2, 2, 2, 1];
|
|
482
790
|
}
|
|
483
791
|
}
|
|
484
|
-
|
|
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
|
+
*/
|
|
485
797
|
export class MinorScale extends Scale {
|
|
486
798
|
constructor(root) {
|
|
487
799
|
super(root);
|
|
@@ -489,6 +801,10 @@ export class MinorScale extends Scale {
|
|
|
489
801
|
this.steps = [2, 1, 2, 2, 1, 2, 2];
|
|
490
802
|
}
|
|
491
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
|
+
*/
|
|
492
808
|
export class HarmonicMinorScale extends Scale {
|
|
493
809
|
constructor(root) {
|
|
494
810
|
super(root);
|
|
@@ -496,6 +812,10 @@ export class HarmonicMinorScale extends Scale {
|
|
|
496
812
|
this.steps = [2, 1, 2, 2, 1, 3, 1];
|
|
497
813
|
}
|
|
498
814
|
}
|
|
815
|
+
/**
|
|
816
|
+
* Ascending melodic minor scale with raised 6th and 7th degrees.
|
|
817
|
+
* Pattern: W-H-W-W-W-W-H.
|
|
818
|
+
*/
|
|
499
819
|
export class AscendingMelodicMinorScale extends Scale {
|
|
500
820
|
constructor(root) {
|
|
501
821
|
super(root);
|
|
@@ -503,21 +823,30 @@ export class AscendingMelodicMinorScale extends Scale {
|
|
|
503
823
|
this.steps = [2, 1, 2, 2, 2, 2, 1];
|
|
504
824
|
}
|
|
505
825
|
}
|
|
826
|
+
/**
|
|
827
|
+
* Major blues scale (6 notes).
|
|
828
|
+
* Notes in C: C, D, Eb, E, G, A.
|
|
829
|
+
*/
|
|
506
830
|
export class MajorBluesScale extends Scale {
|
|
507
831
|
constructor(root) {
|
|
508
832
|
super(root);
|
|
509
|
-
// C, D, D#/Eb, E, G, A
|
|
510
833
|
this.steps = [2, 1, 1, 3, 2, 3];
|
|
511
834
|
}
|
|
512
835
|
}
|
|
836
|
+
/**
|
|
837
|
+
* Minor blues scale (6 notes).
|
|
838
|
+
* Notes in C: C, Eb, F, Gb, G, Bb.
|
|
839
|
+
*/
|
|
513
840
|
export class MinorBluesScale extends Scale {
|
|
514
841
|
constructor(root) {
|
|
515
842
|
super(root);
|
|
516
843
|
this.minor = true;
|
|
517
|
-
// C, D#/Eb, F, F#/Gb, G, Bb
|
|
518
844
|
this.steps = [3, 2, 1, 1, 3, 2];
|
|
519
845
|
}
|
|
520
846
|
}
|
|
847
|
+
/**
|
|
848
|
+
* Chromatic scale containing all 12 semitones.
|
|
849
|
+
*/
|
|
521
850
|
export class ChromaticScale extends Scale {
|
|
522
851
|
constructor(root) {
|
|
523
852
|
super(root);
|
|
@@ -525,8 +854,27 @@ export class ChromaticScale extends Scale {
|
|
|
525
854
|
this.steps = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
|
|
526
855
|
}
|
|
527
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
|
+
*/
|
|
528
865
|
export class Chord extends Scale {
|
|
529
|
-
|
|
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
|
+
*/
|
|
530
878
|
static notes(note, chordName, inversion = 0, notesCount = 0) {
|
|
531
879
|
const match = note.match(/^([^\d]+)(\d+)$/);
|
|
532
880
|
if (!match) {
|
|
@@ -538,21 +886,33 @@ export class Chord extends Scale {
|
|
|
538
886
|
if (notesCount == 0) {
|
|
539
887
|
notesCount = intervals.length + 1;
|
|
540
888
|
}
|
|
541
|
-
return new Chord(root, intervals).getRange(octave, notesCount, inversion);
|
|
542
|
-
}
|
|
889
|
+
return new Chord(root, [...intervals]).getRange(octave, notesCount, inversion);
|
|
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
|
+
*/
|
|
543
899
|
constructor(root, intervals) {
|
|
544
900
|
super(root);
|
|
901
|
+
let steps;
|
|
545
902
|
if (typeof intervals === "string") {
|
|
546
903
|
const shape = Chord.SHAPES[intervals];
|
|
547
904
|
if (!shape) {
|
|
548
905
|
throw new Error(`Unknown chord shape: ${intervals}`);
|
|
549
906
|
}
|
|
550
|
-
|
|
907
|
+
steps = [...shape];
|
|
551
908
|
}
|
|
552
|
-
|
|
909
|
+
else {
|
|
910
|
+
steps = [...intervals];
|
|
911
|
+
}
|
|
912
|
+
if (!steps.length) {
|
|
553
913
|
throw new Error("Missing intervals for chord");
|
|
554
914
|
}
|
|
555
|
-
this.steps =
|
|
915
|
+
this.steps = steps;
|
|
556
916
|
// add wrapping interval to get back to octave
|
|
557
917
|
let sum = 0;
|
|
558
918
|
for (const i of this.steps) {
|
|
@@ -564,7 +924,10 @@ export class Chord extends Scale {
|
|
|
564
924
|
}
|
|
565
925
|
this.steps.push(rest);
|
|
566
926
|
}
|
|
567
|
-
|
|
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
|
+
*/
|
|
568
931
|
isDominant() {
|
|
569
932
|
const shapeName = this.chordShapeName();
|
|
570
933
|
if (shapeName == "M" || shapeName == "7") {
|
|
@@ -572,8 +935,13 @@ export class Chord extends Scale {
|
|
|
572
935
|
}
|
|
573
936
|
return false;
|
|
574
937
|
}
|
|
575
|
-
|
|
576
|
-
|
|
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
|
+
*/
|
|
577
945
|
getSecondaryDominantTargets(noteCount = 3) {
|
|
578
946
|
if (!this.isDominant()) {
|
|
579
947
|
throw new Error(`chord is not dominant to begin with: ${this.chordShapeName()}`);
|
|
@@ -594,8 +962,12 @@ export class Chord extends Scale {
|
|
|
594
962
|
}
|
|
595
963
|
throw new Error(`don't know how to get secondary dominant for note count: ${noteCount}`);
|
|
596
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
|
+
*/
|
|
597
969
|
chordShapeName() {
|
|
598
|
-
for (const shape
|
|
970
|
+
for (const shape of Object.keys(Chord.SHAPES)) {
|
|
599
971
|
const intervals = Chord.SHAPES[shape];
|
|
600
972
|
if (this.steps.length - 1 != intervals.length) {
|
|
601
973
|
continue;
|
|
@@ -612,7 +984,14 @@ export class Chord extends Scale {
|
|
|
612
984
|
}
|
|
613
985
|
}
|
|
614
986
|
}
|
|
615
|
-
|
|
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
|
+
*/
|
|
616
995
|
containsNotes(notes) {
|
|
617
996
|
if (!notes.length) {
|
|
618
997
|
return false;
|
|
@@ -624,7 +1003,11 @@ export class Chord extends Scale {
|
|
|
624
1003
|
}
|
|
625
1004
|
return true;
|
|
626
1005
|
}
|
|
627
|
-
|
|
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
|
+
*/
|
|
628
1011
|
countSharedNotes(otherChord) {
|
|
629
1012
|
const myNotes = this.getRange(5, this.steps.length);
|
|
630
1013
|
const theirNotes = otherChord.getRange(5, this.steps.length);
|
|
@@ -643,22 +1026,37 @@ export class Chord extends Scale {
|
|
|
643
1026
|
}
|
|
644
1027
|
return count;
|
|
645
1028
|
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Returns the chord name as a string (e.g., "C", "Am7", "Bdim").
|
|
1031
|
+
* @returns Chord name with root and quality
|
|
1032
|
+
*/
|
|
646
1033
|
toString() {
|
|
647
|
-
|
|
648
|
-
if (!
|
|
1034
|
+
const shapeName = this.chordShapeName();
|
|
1035
|
+
if (!shapeName) {
|
|
649
1036
|
console.warn("don't know name of chord", this.root, this.steps, this.getRange(5, 3));
|
|
650
|
-
|
|
1037
|
+
return this.root;
|
|
651
1038
|
}
|
|
652
|
-
if (
|
|
653
|
-
|
|
1039
|
+
if (shapeName == "M") {
|
|
1040
|
+
return this.root;
|
|
654
1041
|
}
|
|
655
|
-
return `${this.root}${
|
|
1042
|
+
return `${this.root}${shapeName}`;
|
|
656
1043
|
}
|
|
657
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
|
+
*/
|
|
658
1056
|
Chord.SHAPES = {
|
|
659
1057
|
"M": [4, 3],
|
|
660
1058
|
"m": [3, 4],
|
|
661
|
-
"dim": [3, 3],
|
|
1059
|
+
"dim": [3, 3],
|
|
662
1060
|
"dimM7": [3, 3, 5],
|
|
663
1061
|
"dim7": [3, 3, 3],
|
|
664
1062
|
"aug": [4, 4],
|
|
@@ -670,32 +1068,57 @@ Chord.SHAPES = {
|
|
|
670
1068
|
"m7": [3, 4, 3],
|
|
671
1069
|
"m7b5": [3, 3, 4],
|
|
672
1070
|
"mM7": [3, 4, 4],
|
|
673
|
-
|
|
674
|
-
"Q": [5, 5], // quartal
|
|
1071
|
+
"Q": [5, 5],
|
|
675
1072
|
"Qb4": [4, 5],
|
|
676
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
|
+
*/
|
|
677
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
|
+
*/
|
|
678
1088
|
static forName(name) {
|
|
679
1089
|
if (!this.cache) {
|
|
680
1090
|
this.cache = Object.fromEntries(this.allStaves().map(s => [s.name, s]));
|
|
681
1091
|
}
|
|
682
1092
|
return this.cache[name];
|
|
683
1093
|
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Returns all available staff types.
|
|
1096
|
+
* @returns Array of Staff instances
|
|
1097
|
+
*/
|
|
684
1098
|
static allStaves() {
|
|
1099
|
+
// TODO: alto, middle C center
|
|
685
1100
|
return [
|
|
686
1101
|
new Staff("treble", "E5", "F6", "G5"),
|
|
687
1102
|
new Staff("bass", "G3", "A4", "F4")
|
|
688
|
-
// TODO: alto, middle C center
|
|
689
1103
|
];
|
|
690
1104
|
}
|
|
691
|
-
|
|
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
|
+
*/
|
|
692
1112
|
constructor(name, lowerNote, upperNote, clefNote) {
|
|
693
1113
|
this.name = name;
|
|
694
1114
|
this.lowerNote = lowerNote;
|
|
695
1115
|
this.upperNote = upperNote;
|
|
696
1116
|
this.clefNote = clefNote;
|
|
697
1117
|
}
|
|
698
|
-
|
|
1118
|
+
/**
|
|
1119
|
+
* Gets the letter name of the clef (e.g., "G" for treble, "F" for bass).
|
|
1120
|
+
* @returns Single letter clef name
|
|
1121
|
+
*/
|
|
699
1122
|
clefName() {
|
|
700
1123
|
const match = this.clefNote.match(/^([A-G])/);
|
|
701
1124
|
if (!match) {
|
|
@@ -705,4 +1128,24 @@ export class Staff {
|
|
|
705
1128
|
}
|
|
706
1129
|
}
|
|
707
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
|
+
}
|
|
708
1151
|
//# sourceMappingURL=music.js.map
|