@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/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
- // 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,11 +348,26 @@ export class KeySignature {
192
348
  }
193
349
  return note;
194
350
  }
195
- // Convert MIDI pitch to note name with correct enharmonic spelling for 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
+ */
196
361
  noteName(pitch) {
197
362
  return noteName(pitch, !this.isFlat());
198
363
  }
199
- // which notes have accidentals in this key
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
- // 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
+ */
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
- // how many accidentals should display on note for this key
230
- // null: nothing
231
- // 0: a natural
232
- // 1: a sharp
233
- // -1: a flat
234
- // 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
+ */
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
- // the notes to give accidentals to within the range [min, max], the returned
258
- // 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
+ */
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
- // 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
+ */
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
- // 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
+ */
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
- // degree is one indexed
449
- // 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
+ */
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
- // 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
+ */
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
- // 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
+ */
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
- // 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
+ */
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
- intervals = shape;
907
+ steps = [...shape];
551
908
  }
552
- if (!intervals) {
909
+ else {
910
+ steps = [...intervals];
911
+ }
912
+ if (!steps.length) {
553
913
  throw new Error("Missing intervals for chord");
554
914
  }
555
- this.steps = [...intervals];
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
- // 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
+ */
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
- // can point to a chord that's a 4th below (third above)
576
- // 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
+ */
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 in Chord.SHAPES) {
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
- // 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
+ */
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
- // 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
+ */
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
- let name = this.chordShapeName();
648
- if (!name) {
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
- name = "";
1037
+ return this.root;
651
1038
  }
652
- if (name == "M") {
653
- name = "";
1039
+ if (shapeName == "M") {
1040
+ return this.root;
654
1041
  }
655
- return `${this.root}${name}`;
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], // diminished
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
- // exotic
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
- // 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
+ */
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
- // 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
+ */
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