@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/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
- // octaveless note comparison
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
- // returns 0 if notes are same
119
- // returns < 0 if a < b
120
- // returns > 0 if a > b
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
- // excludes the chromatic option
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
- // count: the number of accidentals in the key
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
- // the default scale root for building scales from key signature
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
- // the scale used on the random note generator
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
- // convert note to enharmonic equivalent that fits into this key signature
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
- // which notes have accidentals in this key
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
- // key note -> raw note
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
- // how many accidentals should display on note for this key
226
- // null: nothing
227
- // 0: a natural
228
- // 1: a sharp
229
- // -1: a flat
230
- // 2: double sharp, etc.
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
- // the notes to give accidentals to within the range [min, max], the returned
254
- // notes will not be sharp or flat
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
- // degrees are 1 indexed
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
- // degrees are 1 indexed
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
- // degree is one indexed
445
- // new MajorScale().buildChordSteps(1, 2) -> major chord
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
- // all chords with count notes
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
- // natural minor
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
- // Chord.notes("C5", "M", 1) -> first inversion C major chord
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
- intervals = shape;
907
+ steps = [...shape];
547
908
  }
548
- if (!intervals) {
909
+ else {
910
+ steps = [...intervals];
911
+ }
912
+ if (!steps.length) {
549
913
  throw new Error("Missing intervals for chord");
550
914
  }
551
- this.steps = [...intervals];
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
- // is major or dom7 chord
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
- // can point to a chord that's a 4th below (third above)
572
- // the target chord can either be major or minor (2,3,5,6) in new key
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 in Chord.SHAPES) {
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
- // do all the notes fit this chord
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
- // how many notes do the two chords share
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
- let name = this.chordShapeName();
644
- if (!name) {
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
- name = "";
1037
+ return this.root;
647
1038
  }
648
- if (name == "M") {
649
- name = "";
1039
+ if (shapeName == "M") {
1040
+ return this.root;
650
1041
  }
651
- return `${this.root}${name}`;
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], // diminished
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
- // exotic
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
- // upper and lower note are the notes for the lines on the top and bottom
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
- // F, G, etc
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