@libraz/libcantus 1.0.1 → 1.2.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.
Files changed (142) hide show
  1. package/README.md +27 -65
  2. package/README_ja.md +28 -66
  3. package/dist/analyze/index.cjs +11 -7
  4. package/dist/analyze/index.cjs.map +1 -1
  5. package/dist/analyze/index.d.cts +79 -144
  6. package/dist/analyze/index.d.ts +79 -144
  7. package/dist/analyze/index.js +20 -16
  8. package/dist/chunk-35NBV2FT.cjs +145 -0
  9. package/dist/chunk-35NBV2FT.cjs.map +1 -0
  10. package/dist/{chunk-UADYAYAT.cjs → chunk-3R3QVKY5.cjs} +1353 -580
  11. package/dist/chunk-3R3QVKY5.cjs.map +1 -0
  12. package/dist/{chunk-WWURE4XT.cjs → chunk-76ZBTRG2.cjs} +624 -729
  13. package/dist/chunk-76ZBTRG2.cjs.map +1 -0
  14. package/dist/{chunk-FVSI36UV.js → chunk-7KG3RTYS.js} +197 -110
  15. package/dist/chunk-7KG3RTYS.js.map +1 -0
  16. package/dist/{chunk-VTI6FOLB.js → chunk-AGOVW7U6.js} +709 -133
  17. package/dist/chunk-AGOVW7U6.js.map +1 -0
  18. package/dist/{chunk-E3F4AHAH.cjs → chunk-CGBADIO5.cjs} +221 -134
  19. package/dist/chunk-CGBADIO5.cjs.map +1 -0
  20. package/dist/{chunk-6XH6QZYQ.cjs → chunk-DSMJRXCZ.cjs} +1455 -849
  21. package/dist/chunk-DSMJRXCZ.cjs.map +1 -0
  22. package/dist/{chunk-PKFNUHDY.js → chunk-FBH75DAY.js} +1218 -612
  23. package/dist/chunk-FBH75DAY.js.map +1 -0
  24. package/dist/chunk-H2KJHBW4.js +3329 -0
  25. package/dist/chunk-H2KJHBW4.js.map +1 -0
  26. package/dist/chunk-HPVSH7SF.cjs +2294 -0
  27. package/dist/chunk-HPVSH7SF.cjs.map +1 -0
  28. package/dist/{chunk-MXO72PLY.cjs → chunk-HWHGOVO6.cjs} +21 -19
  29. package/dist/chunk-HWHGOVO6.cjs.map +1 -0
  30. package/dist/chunk-HYJJ2V2I.js +2294 -0
  31. package/dist/chunk-HYJJ2V2I.js.map +1 -0
  32. package/dist/{chunk-AFM2ULJH.cjs → chunk-J7FHVZII.cjs} +1567 -1067
  33. package/dist/chunk-J7FHVZII.cjs.map +1 -0
  34. package/dist/chunk-K6OZXIR6.js +1 -0
  35. package/dist/{chunk-H3VORETI.js → chunk-M5RYMAFI.js} +487 -592
  36. package/dist/chunk-M5RYMAFI.js.map +1 -0
  37. package/dist/chunk-NB25P3EH.js +363 -0
  38. package/dist/chunk-NB25P3EH.js.map +1 -0
  39. package/dist/{chunk-TTCLJX6D.js → chunk-NFJ4P6ZZ.js} +10 -8
  40. package/dist/chunk-NFJ4P6ZZ.js.map +1 -0
  41. package/dist/chunk-NM7UAMC3.cjs +457 -0
  42. package/dist/chunk-NM7UAMC3.cjs.map +1 -0
  43. package/dist/chunk-PM4XJE2Y.cjs +363 -0
  44. package/dist/chunk-PM4XJE2Y.cjs.map +1 -0
  45. package/dist/chunk-PUKJP2Z5.js +145 -0
  46. package/dist/chunk-PUKJP2Z5.js.map +1 -0
  47. package/dist/{chunk-VOHZB45S.js → chunk-Q6LFSAKV.js} +1321 -821
  48. package/dist/chunk-Q6LFSAKV.js.map +1 -0
  49. package/dist/{chunk-INHVXP6Z.js → chunk-RFIBF2RR.js} +1203 -430
  50. package/dist/chunk-RFIBF2RR.js.map +1 -0
  51. package/dist/{chunk-Q2WGKJNN.cjs → chunk-S5BKZZGM.cjs} +699 -123
  52. package/dist/chunk-S5BKZZGM.cjs.map +1 -0
  53. package/dist/chunk-SGLEQVJH.cjs +3329 -0
  54. package/dist/chunk-SGLEQVJH.cjs.map +1 -0
  55. package/dist/chunk-UGLFDG7L.cjs +1 -0
  56. package/dist/chunk-UGLFDG7L.cjs.map +1 -0
  57. package/dist/{chunk-YH3BIKHH.js → chunk-UMWZ4ZET.js} +146 -49
  58. package/dist/chunk-UMWZ4ZET.js.map +1 -0
  59. package/dist/{index-szBv3496.d.ts → coerce-DP0vYu-x.d.ts} +286 -199
  60. package/dist/{index-C_8o--VE.d.cts → coerce-DRERzdve.d.cts} +286 -199
  61. package/dist/core/index.cjs +9 -5
  62. package/dist/core/index.cjs.map +1 -1
  63. package/dist/core/index.d.cts +122 -108
  64. package/dist/core/index.d.ts +122 -108
  65. package/dist/core/index.js +32 -28
  66. package/dist/generate/index.cjs +8 -8
  67. package/dist/generate/index.d.cts +168 -84
  68. package/dist/generate/index.d.ts +168 -84
  69. package/dist/generate/index.js +7 -7
  70. package/dist/{index-CGpRv5sl.d.cts → index-9eoQs2C1.d.ts} +34 -19
  71. package/dist/{index-CCNJRG-P.d.cts → index-B9QLziDe.d.cts} +293 -56
  72. package/dist/{index-Dzr13q3d.d.cts → index-BBWNZS0f.d.cts} +135 -58
  73. package/dist/{index-9wcMhG73.d.ts → index-BHIUyjhZ.d.ts} +293 -56
  74. package/dist/{index-BGFW-8P2.d.cts → index-BJgP97N1.d.ts} +196 -114
  75. package/dist/{index-CunYKLiu.d.ts → index-BL5N2Oyz.d.cts} +1 -1
  76. package/dist/{index-Cj49F892.d.ts → index-BMweUlm5.d.cts} +63 -23
  77. package/dist/{index-CgHqxdps.d.ts → index-BNSM-DSk.d.cts} +196 -114
  78. package/dist/{index-D57QRbVn.d.ts → index-BRMbPkHE.d.ts} +480 -254
  79. package/dist/{index-Chbst3Jx.d.ts → index-CJBBiAmU.d.ts} +135 -58
  80. package/dist/{index-BkGmlyI3.d.cts → index-CL4b4gUQ.d.ts} +63 -23
  81. package/dist/{index-B9MNZboQ.d.ts → index-Cy15oTvh.d.cts} +52 -22
  82. package/dist/{index-B9MNZboQ.d.cts → index-Cy15oTvh.d.ts} +52 -22
  83. package/dist/{index-BO6xE9YU.d.cts → index-DGCNKAUr.d.cts} +480 -254
  84. package/dist/{index-B4o55NDr.d.cts → index-Dw2DP7vm.d.ts} +1 -1
  85. package/dist/{index-G1uwGGWQ.d.ts → index-cHvbYgJI.d.cts} +34 -19
  86. package/dist/index.cjs +42 -14
  87. package/dist/index.cjs.map +1 -1
  88. package/dist/index.d.cts +14 -14
  89. package/dist/index.d.ts +14 -14
  90. package/dist/index.js +82 -54
  91. package/dist/{masks-DYbE485J.d.cts → masks-Cc7-MzzY.d.ts} +49 -2
  92. package/dist/{masks-DYbE485J.d.ts → masks-pADfTOiD.d.cts} +49 -2
  93. package/dist/model/index.cjs +19 -12
  94. package/dist/model/index.cjs.map +1 -1
  95. package/dist/model/index.d.cts +504 -182
  96. package/dist/model/index.d.ts +504 -182
  97. package/dist/model/index.js +18 -11
  98. package/dist/{positional-Cptlj-xs.d.ts → positional-DJgs6ltD.d.ts} +85 -8
  99. package/dist/{positional-DvH_sd8u.d.cts → positional-DMZer-FM.d.cts} +85 -8
  100. package/dist/theory/index.cjs +35 -6
  101. package/dist/theory/index.cjs.map +1 -1
  102. package/dist/theory/index.d.cts +257 -62
  103. package/dist/theory/index.d.ts +257 -62
  104. package/dist/theory/index.js +41 -12
  105. package/package.json +7 -5
  106. package/dist/chunk-2LCPM3EG.js +0 -3224
  107. package/dist/chunk-2LCPM3EG.js.map +0 -1
  108. package/dist/chunk-6XH6QZYQ.cjs.map +0 -1
  109. package/dist/chunk-6XPTBE7Y.cjs +0 -49
  110. package/dist/chunk-6XPTBE7Y.cjs.map +0 -1
  111. package/dist/chunk-AFM2ULJH.cjs.map +0 -1
  112. package/dist/chunk-B6XNKWZV.cjs +0 -3224
  113. package/dist/chunk-B6XNKWZV.cjs.map +0 -1
  114. package/dist/chunk-E3F4AHAH.cjs.map +0 -1
  115. package/dist/chunk-ELCTZUJJ.js +0 -379
  116. package/dist/chunk-ELCTZUJJ.js.map +0 -1
  117. package/dist/chunk-FVSI36UV.js.map +0 -1
  118. package/dist/chunk-H3VORETI.js.map +0 -1
  119. package/dist/chunk-H42UHPYW.js +0 -1577
  120. package/dist/chunk-H42UHPYW.js.map +0 -1
  121. package/dist/chunk-HFPS7JFD.js +0 -1
  122. package/dist/chunk-INHVXP6Z.js.map +0 -1
  123. package/dist/chunk-K4ZJUGF4.cjs +0 -379
  124. package/dist/chunk-K4ZJUGF4.cjs.map +0 -1
  125. package/dist/chunk-KNJEKLKV.js +0 -49
  126. package/dist/chunk-KNJEKLKV.js.map +0 -1
  127. package/dist/chunk-MXO72PLY.cjs.map +0 -1
  128. package/dist/chunk-NLEU4QSE.cjs +0 -360
  129. package/dist/chunk-NLEU4QSE.cjs.map +0 -1
  130. package/dist/chunk-OE6WK2Y2.cjs +0 -1577
  131. package/dist/chunk-OE6WK2Y2.cjs.map +0 -1
  132. package/dist/chunk-PKFNUHDY.js.map +0 -1
  133. package/dist/chunk-Q2WGKJNN.cjs.map +0 -1
  134. package/dist/chunk-RHIJ3S4I.cjs +0 -1
  135. package/dist/chunk-RHIJ3S4I.cjs.map +0 -1
  136. package/dist/chunk-TTCLJX6D.js.map +0 -1
  137. package/dist/chunk-UADYAYAT.cjs.map +0 -1
  138. package/dist/chunk-VOHZB45S.js.map +0 -1
  139. package/dist/chunk-VTI6FOLB.js.map +0 -1
  140. package/dist/chunk-WWURE4XT.cjs.map +0 -1
  141. package/dist/chunk-YH3BIKHH.js.map +0 -1
  142. /package/dist/{chunk-HFPS7JFD.js.map → chunk-K6OZXIR6.js.map} +0 -0
@@ -1,33 +1,43 @@
1
1
  import {
2
2
  InvalidInputError,
3
+ MAX_NAME_ACCIDENTALS,
4
+ assertArray,
3
5
  assertDegree,
4
6
  assertFiniteNumber,
5
7
  assertFiniteSemitones,
8
+ assertFlag,
6
9
  assertGenerationBudget,
7
10
  assertInteger,
8
11
  assertMidiPitch,
9
12
  assertOneOf,
13
+ assertOptions,
14
+ assertPitchClass,
15
+ assertRecord,
10
16
  clampToMidi,
17
+ describeRejected,
11
18
  diatonicLetterOf,
12
19
  formatNote,
13
20
  isConsonantInterval,
21
+ maxNoteNameLength,
14
22
  midiToNote,
15
23
  naturalPitchClassOf,
16
24
  noteToMidi,
17
25
  noteToPitchClass,
18
26
  parseFailure,
19
- parseKeyName,
20
27
  parseNote,
21
28
  pitchClassOf,
22
29
  spelledInterval,
23
30
  toNoteData,
31
+ transposeByInterval,
24
32
  transposeNote,
33
+ tryParseKeyName,
25
34
  tryParseNote,
26
35
  unwrapParse
27
- } from "./chunk-VTI6FOLB.js";
36
+ } from "./chunk-AGOVW7U6.js";
28
37
 
29
38
  // src/theory/scale/masks.ts
30
39
  function maskFromOffsets(offsets) {
40
+ assertArray(offsets, "offsets");
31
41
  let mask = 1;
32
42
  for (const [index, offset] of offsets.entries()) {
33
43
  assertInteger(offset, `offsets[${index}]`);
@@ -51,6 +61,12 @@ var WHOLE_TONE_MASK = maskFromOffsets([0, 2, 4, 6, 8, 10]);
51
61
  var OCTATONIC_HALF_WHOLE_MASK = maskFromOffsets([0, 1, 3, 4, 6, 7, 9, 10]);
52
62
  var OCTATONIC_WHOLE_HALF_MASK = maskFromOffsets([0, 2, 3, 5, 6, 8, 9, 11]);
53
63
  var CHROMATIC_MASK = 4095;
64
+ function assertModeMask(modeMask12, name = "modeMask12") {
65
+ return assertInteger(modeMask12, name, 1, CHROMATIC_MASK);
66
+ }
67
+ function maskOf(scale, name = "key") {
68
+ return assertModeMask(assertRecord(scale, name).modeMask12, `${name}.modeMask12`);
69
+ }
54
70
  var LYDIAN_DOMINANT_MASK = maskFromOffsets([0, 2, 4, 6, 7, 9, 10]);
55
71
  var MIXOLYDIAN_B13_MASK = maskFromOffsets([0, 2, 4, 5, 7, 8, 10]);
56
72
  var LOCRIAN_NATURAL2_MASK = maskFromOffsets([0, 2, 3, 5, 6, 8, 10]);
@@ -156,6 +172,9 @@ var SCALE_ALIASES = Object.freeze({
156
172
  poorvi: "purvi"
157
173
  });
158
174
  function resolveScaleName(name) {
175
+ if (typeof name !== "string") {
176
+ throw new InvalidInputError(`scale name must be a string; received ${describeRejected(name)}`);
177
+ }
159
178
  if (Object.hasOwn(NAMED_SCALES, name)) {
160
179
  return name;
161
180
  }
@@ -178,21 +197,119 @@ function requireScaleMask(name, label = "scale") {
178
197
  }
179
198
  return mask;
180
199
  }
200
+ var VARIANT_MASKS = {
201
+ major: MAJOR_MASK,
202
+ natural: NATURAL_MINOR_MASK,
203
+ harmonic: HARMONIC_MINOR_MASK,
204
+ melodic: MELODIC_MINOR_MASK
205
+ };
206
+ function isMinorMask(modeMask12) {
207
+ return (modeMask12 >> 3 & 1) === 1 && (modeMask12 >> 4 & 1) === 0;
208
+ }
209
+ function variantOfMask(modeMask12) {
210
+ assertModeMask(modeMask12);
211
+ for (const [variant, mask] of Object.entries(VARIANT_MASKS)) {
212
+ if (mask === modeMask12) {
213
+ return variant;
214
+ }
215
+ }
216
+ return "modal";
217
+ }
218
+ var KEY_VARIANTS = [
219
+ ...Object.keys(VARIANT_MASKS),
220
+ "modal"
221
+ ];
222
+ function assertKeyVariant(variant, modeMask12) {
223
+ assertOneOf(variant, KEY_VARIANTS, "variant");
224
+ assertModeMask(modeMask12);
225
+ const named = VARIANT_MASKS[variant];
226
+ const matches = named === void 0 ? !Object.values(VARIANT_MASKS).includes(modeMask12) : named === modeMask12;
227
+ if (!matches) {
228
+ throw new InvalidInputError(
229
+ `variant ${variant} does not match the scale mask ${modeMask12}; pass the scale that variant names, or omit the variant`
230
+ );
231
+ }
232
+ }
181
233
 
182
- // src/theory/scale/key.ts
183
- function majorKey(rootPc) {
234
+ // src/theory/scale/scales.ts
235
+ function majorScale(rootPc) {
184
236
  return { rootPc: pitchClassOf(rootPc), modeMask12: MAJOR_MASK };
185
237
  }
186
- function minorKey(rootPc) {
238
+ function minorScale(rootPc) {
187
239
  return { rootPc: pitchClassOf(rootPc), modeMask12: NATURAL_MINOR_MASK };
188
240
  }
189
- function scaleByName(name, rootPc) {
241
+ function namedScale(name, rootPc) {
190
242
  return { rootPc: pitchClassOf(rootPc), modeMask12: requireScaleMask(name) };
191
243
  }
192
244
 
245
+ // src/theory/scale/name.ts
246
+ function scaleWordKey(text) {
247
+ return text.replace(/[^a-z0-9]/gi, "").toLowerCase();
248
+ }
249
+ var BUILT_IN_SCALE_NAMES = [
250
+ ...Object.keys(NAMED_SCALES),
251
+ ...Object.keys(WORLD_SCALES),
252
+ ...Object.keys(SCALE_ALIASES)
253
+ ];
254
+ var SCALE_BY_WORD = new Map(
255
+ BUILT_IN_SCALE_NAMES.map((name) => [scaleWordKey(name), name])
256
+ );
257
+ function scaleNameOfMask(mask) {
258
+ for (const name of Object.keys(NAMED_SCALES)) {
259
+ if (NAMED_SCALES[name] === mask) {
260
+ return name;
261
+ }
262
+ }
263
+ for (const name of Object.keys(WORLD_SCALES)) {
264
+ if (WORLD_SCALES[name] === mask) {
265
+ return name;
266
+ }
267
+ }
268
+ return void 0;
269
+ }
270
+ function bareTonic(note) {
271
+ return { letter: note.letter, alter: note.alter };
272
+ }
273
+ function tryNamedScaleKey(text) {
274
+ const trimmed = text.trim();
275
+ const separator = trimmed.search(/\s/);
276
+ if (separator < 0) {
277
+ return null;
278
+ }
279
+ const name = SCALE_BY_WORD.get(scaleWordKey(trimmed.slice(separator)));
280
+ if (name === void 0) {
281
+ return null;
282
+ }
283
+ const tonic = tryParseNote(trimmed.slice(0, separator));
284
+ if (!tonic.ok) {
285
+ return null;
286
+ }
287
+ const scale = namedScale(name, noteToPitchClass(tonic.value));
288
+ return { scale, tonic: bareTonic(tonic.value), variant: variantOfMask(scale.modeMask12) };
289
+ }
290
+ function tryResolveKeyName(text, opts) {
291
+ const asked = assertOptions(opts, "opts");
292
+ if (typeof text === "string" && (asked.system === void 0 || asked.system === "english")) {
293
+ const named = tryNamedScaleKey(text);
294
+ if (named !== null) {
295
+ return { ok: true, value: named };
296
+ }
297
+ }
298
+ const parsed = tryParseKeyName(text, asked);
299
+ if (!parsed.ok) {
300
+ return parsed;
301
+ }
302
+ const tonic = bareTonic(parsed.value.tonic);
303
+ const rootPc = noteToPitchClass(tonic);
304
+ return parsed.value.mode === "major" ? { ok: true, value: { scale: majorScale(rootPc), tonic, variant: "major" } } : { ok: true, value: { scale: minorScale(rootPc), tonic, variant: "natural" } };
305
+ }
306
+ function resolveKeyName(text, opts) {
307
+ return unwrapParse(tryResolveKeyName(text, opts));
308
+ }
309
+
193
310
  // src/theory/scale/coerce.ts
194
311
  function normalizedKeyScale(scale) {
195
- assertInteger(scale.modeMask12, "key.modeMask12", 1, CHROMATIC_MASK);
312
+ assertModeMask(scale.modeMask12, "key.modeMask12");
196
313
  if ((scale.modeMask12 & 1) === 0) {
197
314
  throw new InvalidInputError(
198
315
  `key.modeMask12 must include its root; received ${scale.modeMask12}`
@@ -203,12 +320,10 @@ function normalizedKeyScale(scale) {
203
320
  }
204
321
  function toKeyScale(value) {
205
322
  if (typeof value === "string") {
206
- const name = parseKeyName(value);
207
- const rootPc = noteToPitchClass(name.tonic);
208
- return name.mode === "major" ? majorKey(rootPc) : minorKey(rootPc);
323
+ return resolveKeyName(value).scale;
209
324
  }
210
325
  if (typeof value === "object" && value !== null) {
211
- const data = "toJSON" in value && typeof value.toJSON === "function" ? value.toJSON().scale : value;
326
+ const data = "toJSON" in value && typeof value.toJSON === "function" ? value.toJSON().scale : "scale" in value ? value.scale : value;
212
327
  if (typeof data === "object" && data !== null) {
213
328
  return normalizedKeyScale(data);
214
329
  }
@@ -224,10 +339,11 @@ function inScale(pitch, scale) {
224
339
  return (scale.modeMask12 >> scaleOffset(pitch, scale) & 1) === 1;
225
340
  }
226
341
  function isScaleTone(pitch, key) {
227
- return inScale(pitch, key);
342
+ return inScale(pitch, toKeyScale(key));
228
343
  }
229
344
  function nearestScaleTone(pitch, key) {
230
345
  const scale = toKeyScale(key);
346
+ assertFiniteNumber(pitch, "pitch");
231
347
  const base = clampToMidi(Math.round(pitch), "pitch");
232
348
  for (let distance = 0; distance < 12; distance += 1) {
233
349
  const lower = base - distance;
@@ -251,6 +367,10 @@ function scaleOffsets(key) {
251
367
  return offsets;
252
368
  }
253
369
  function scaleLadderPosition(pitch, key) {
370
+ return ladderPosition(pitch, toKeyScale(key));
371
+ }
372
+ function ladderPosition(pitch, key) {
373
+ assertFiniteNumber(pitch, "pitch");
254
374
  const offsets = scaleOffsets(key);
255
375
  const size = offsets.length;
256
376
  if (size === 0) {
@@ -268,6 +388,10 @@ function scaleLadderPosition(pitch, key) {
268
388
  return { rung: octave * size + degree, offset: within - (offsets[degree] ?? 0) };
269
389
  }
270
390
  function scaleLadderPitch(rung, key) {
391
+ return ladderPitch(rung, toKeyScale(key));
392
+ }
393
+ function ladderPitch(rung, key) {
394
+ assertFiniteNumber(rung, "rung");
271
395
  const offsets = scaleOffsets(key);
272
396
  const size = offsets.length;
273
397
  if (size === 0) {
@@ -278,12 +402,13 @@ function scaleLadderPitch(rung, key) {
278
402
  return pitchClassOf(key.rootPc) + octave * 12 + (offsets[degree] ?? 0);
279
403
  }
280
404
  function shiftByScaleDegrees(pitch, degrees, key) {
281
- const steps = Math.trunc(degrees);
405
+ const scale = toKeyScale(key);
406
+ const steps = Math.trunc(assertFiniteNumber(degrees, "degrees"));
282
407
  if (steps === 0) {
283
408
  return pitch;
284
409
  }
285
- const { rung, offset } = scaleLadderPosition(pitch, key);
286
- return scaleLadderPitch(rung + steps, key) + offset;
410
+ const { rung, offset } = ladderPosition(pitch, scale);
411
+ return ladderPitch(rung + steps, scale) + offset;
287
412
  }
288
413
  function pitchToScaleDegree(pitch, key) {
289
414
  const scale = toKeyScale(key);
@@ -300,18 +425,10 @@ function pitchToScaleDegree(pitch, key) {
300
425
  return degree;
301
426
  }
302
427
  function diatonicPitchClasses(key) {
303
- const scale = key;
304
- const root = pitchClassOf(scale.rootPc);
305
- const pcs = [];
306
- for (let n = 0; n < 12; n += 1) {
307
- if ((scale.modeMask12 >> n & 1) === 1) {
308
- pcs.push((root + n) % 12);
309
- }
310
- }
311
- return pcs.sort((a, b) => a - b);
428
+ return scaleTonesInDegreeOrder(key).sort((a, b) => a - b);
312
429
  }
313
430
  function scaleTonesInDegreeOrder(key) {
314
- const scale = key;
431
+ const scale = toKeyScale(key);
315
432
  const root = pitchClassOf(scale.rootPc);
316
433
  const pcs = [];
317
434
  for (let n = 0; n < 12; n += 1) {
@@ -348,22 +465,20 @@ var DIATONIC_MODE_OFFSETS = /* @__PURE__ */ new Map([
348
465
  [rotateMask(MAJOR_MASK, 11), -5]
349
466
  // locrian
350
467
  ]);
351
- function hasMinorThird(modeMask12) {
352
- return (modeMask12 >> 3 & 1) === 1 && (modeMask12 >> 4 & 1) === 0;
353
- }
354
468
  function modeOffset(key) {
355
469
  const diatonic = DIATONIC_MODE_OFFSETS.get(key.modeMask12);
356
470
  if (diatonic !== void 0) {
357
471
  return diatonic;
358
472
  }
359
- return hasMinorThird(key.modeMask12) ? MINOR_MODE_OFFSET : 0;
473
+ return isMinorMask(key.modeMask12) ? MINOR_MODE_OFFSET : 0;
360
474
  }
361
475
  function isSignatureKey(key) {
362
- return DIATONIC_MODE_OFFSETS.has(key.modeMask12) || key.modeMask12 === HARMONIC_MINOR_MASK || key.modeMask12 === MELODIC_MINOR_MASK;
476
+ const mask = maskOf(key);
477
+ return DIATONIC_MODE_OFFSETS.has(mask) || mask === HARMONIC_MINOR_MASK || mask === MELODIC_MINOR_MASK;
363
478
  }
364
479
  function keySignatureFifths(tonic, key) {
365
480
  const note = toNoteData(tonic);
366
- const scale = key;
481
+ const scale = toKeyScale(key);
367
482
  const letter = diatonicLetterOf(note.letter);
368
483
  assertInteger(note.alter, "tonic.alter");
369
484
  assertInteger(scale.modeMask12, "key.modeMask12", 1, 4095);
@@ -376,33 +491,28 @@ function keyFromFifths(fifths, mode = "major") {
376
491
  const letter = FIFTHS_LETTERS[(index % 7 + 7) % 7] ?? 0;
377
492
  const alter = Math.floor(index / 7);
378
493
  const rootPc = pitchClassOf(naturalPitchClassOf(letter) + alter);
494
+ const scale = which === "minor" ? minorScale(rootPc) : majorScale(rootPc);
379
495
  return {
380
496
  tonic: { letter, alter },
381
- key: which === "minor" ? minorKey(rootPc) : majorKey(rootPc)
497
+ scale,
498
+ variant: variantOfMask(scale.modeMask12)
382
499
  };
383
500
  }
384
501
 
385
502
  // src/theory/scale/relations.ts
386
503
  var ENHARMONIC_FIFTHS = 12;
387
504
  var MAX_CONVENTIONAL_FIFTHS = 7;
388
- var DOUBLE_ACCIDENTAL = 2;
389
505
  var MINOR_TONIC_FIFTHS = 3;
390
506
  var FLATTEST_TONIC_FIFTHS = -MAX_CONVENTIONAL_FIFTHS;
391
507
  var SHARPEST_TONIC_FIFTHS = MAX_CONVENTIONAL_FIFTHS + MINOR_TONIC_FIFTHS;
392
- function isMinorMode(modeMask12) {
393
- return (modeMask12 >> 3 & 1) === 1 && (modeMask12 >> 4 & 1) === 0;
394
- }
395
- function assertModeMask(key, name = "key") {
396
- return assertInteger(key.modeMask12, `${name}.modeMask12`, 1, CHROMATIC_MASK);
397
- }
398
508
  function modeOf(key) {
399
- return isMinorMode(assertModeMask(key)) ? "minor" : "major";
509
+ return isMinorMask(maskOf(key)) ? "minor" : "major";
400
510
  }
401
511
  function oppositeMode(mode) {
402
512
  return mode === "major" ? "minor" : "major";
403
513
  }
404
514
  function soundsLike(a, b) {
405
- return pitchClassOf(a.rootPc) === pitchClassOf(b.rootPc) && isMinorMode(a.modeMask12) === isMinorMode(b.modeMask12);
515
+ return pitchClassOf(a.rootPc) === pitchClassOf(b.rootPc) && isMinorMask(a.modeMask12) === isMinorMask(b.modeMask12);
406
516
  }
407
517
  function spelledLike(a, b) {
408
518
  return diatonicLetterOf(a.letter) === diatonicLetterOf(b.letter) && a.alter === b.alter;
@@ -413,7 +523,7 @@ function tonicCost(tonic, key) {
413
523
  for (const note of spellScale(tonic, key)) {
414
524
  const alter = Math.abs(note.alter);
415
525
  accidentals += alter;
416
- if (alter >= DOUBLE_ACCIDENTAL) {
526
+ if (alter >= MAX_NAME_ACCIDENTALS) {
417
527
  doubles += 1;
418
528
  }
419
529
  }
@@ -429,7 +539,7 @@ function spellsBetter(a, b, signatureKey) {
429
539
  return false;
430
540
  }
431
541
  function spelledKeyOf(key) {
432
- const scale = key;
542
+ const scale = assertRecord(key, "key");
433
543
  const rootPc = pitchClassOf(scale.rootPc);
434
544
  const signatureKey = isSignatureKey(scale);
435
545
  let tonic = keyFromFifths(0).tonic;
@@ -445,33 +555,35 @@ function spelledKeyOf(key) {
445
555
  best = cost;
446
556
  }
447
557
  }
448
- return { tonic, key };
558
+ return { tonic, scale: key, variant: variantOfMask(key.modeMask12) };
449
559
  }
450
560
  function relativeKeyOf(tonic, key) {
451
- const scale = toKeyScale(key);
452
- return keyFromFifths(keySignatureFifths(tonic, scale), oppositeMode(modeOf(scale)));
561
+ const mode = modeOf(toKeyScale(key));
562
+ return keyFromFifths(tonicFifths(toNoteData(tonic), mode), oppositeMode(mode));
453
563
  }
454
564
  function parallelKeyOf(tonic, key) {
455
565
  const note = toNoteData(tonic);
456
566
  const mode = oppositeMode(modeOf(toKeyScale(key)));
457
567
  const rootPc = noteToPitchClass(note);
568
+ const scale = mode === "minor" ? minorScale(rootPc) : majorScale(rootPc);
458
569
  return {
459
570
  tonic: { letter: diatonicLetterOf(note.letter), alter: note.alter },
460
- key: mode === "minor" ? minorKey(rootPc) : majorKey(rootPc)
571
+ scale,
572
+ variant: variantOfMask(scale.modeMask12)
461
573
  };
462
574
  }
463
575
  function tonicPosition(tonic) {
464
- return keySignatureFifths(tonic, majorKey(noteToPitchClass(tonic)));
576
+ return keySignatureFifths(tonic, majorScale(noteToPitchClass(tonic)));
465
577
  }
466
578
  function tonicFifths(tonic, mode) {
467
579
  return tonicPosition(tonic) - (mode === "minor" ? MINOR_TONIC_FIFTHS : 0);
468
580
  }
469
581
  function isWrittenTonic(tonic, key) {
470
582
  const position = tonicPosition(tonic);
471
- if (position < FLATTEST_TONIC_FIFTHS || position > SHARPEST_TONIC_FIFTHS) {
472
- return false;
583
+ if (isSignatureKey(key)) {
584
+ return Math.abs(position) <= MAX_FIFTHS && Math.abs(keySignatureFifths(tonic, key)) <= MAX_CONVENTIONAL_FIFTHS;
473
585
  }
474
- return isSignatureKey(key) ? Math.abs(keySignatureFifths(tonic, key)) <= MAX_CONVENTIONAL_FIFTHS : tonicCost(tonic, key).doubles === 0;
586
+ return position >= FLATTEST_TONIC_FIFTHS && position <= SHARPEST_TONIC_FIFTHS && tonicCost(tonic, { rootPc: noteToPitchClass(tonic), modeMask12: key.modeMask12 }).doubles === 0;
475
587
  }
476
588
  function dominantKeyOf(tonic, key) {
477
589
  const mode = modeOf(toKeyScale(key));
@@ -484,34 +596,35 @@ function subdominantKeyOf(tonic, key) {
484
596
  function enharmonicKeyOf(tonic, key) {
485
597
  const note = toNoteData(tonic);
486
598
  const scale = toKeyScale(key);
487
- assertModeMask(scale);
599
+ maskOf(scale);
488
600
  for (const position of [
489
601
  tonicPosition(note) - ENHARMONIC_FIFTHS,
490
602
  tonicPosition(note) + ENHARMONIC_FIFTHS
491
603
  ]) {
492
- if (position < FLATTEST_TONIC_FIFTHS || position > SHARPEST_TONIC_FIFTHS) {
604
+ if (Math.abs(position) > MAX_FIFTHS) {
493
605
  continue;
494
606
  }
495
- const spelled = keyFromFifths(position).tonic;
496
- if (!isWrittenTonic(spelled, scale)) {
607
+ const spelled2 = keyFromFifths(position).tonic;
608
+ if (!isWrittenTonic(spelled2, scale)) {
497
609
  continue;
498
610
  }
499
611
  return {
500
- tonic: spelled,
612
+ tonic: spelled2,
501
613
  // The mask is the caller's; only the root is re-read, so a tonic that did
502
614
  // not spell the mask's own root still comes back consistent.
503
- key: { rootPc: noteToPitchClass(spelled), modeMask12: scale.modeMask12 }
615
+ scale: { rootPc: noteToPitchClass(spelled2), modeMask12: scale.modeMask12 },
616
+ variant: variantOfMask(scale.modeMask12)
504
617
  };
505
618
  }
506
619
  return null;
507
620
  }
508
621
  function relativeOfDominantKeyOf(tonic, key) {
509
622
  const dominant = dominantKeyOf(tonic, key);
510
- return relativeKeyOf(dominant.tonic, dominant.key);
623
+ return relativeKeyOf(dominant.tonic, dominant.scale);
511
624
  }
512
625
  function relativeOfSubdominantKeyOf(tonic, key) {
513
626
  const subdominant = subdominantKeyOf(tonic, key);
514
- return relativeKeyOf(subdominant.tonic, subdominant.key);
627
+ return relativeKeyOf(subdominant.tonic, subdominant.scale);
515
628
  }
516
629
  var CLOSELY_RELATED = [
517
630
  { relation: "relative", of: relativeKeyOf },
@@ -527,19 +640,68 @@ function relatedKeysOf(tonic, key) {
527
640
  return CLOSELY_RELATED.map(({ relation, of }) => ({ ...of(note, scale), relation }));
528
641
  }
529
642
  function keyRelationBetween(a, b) {
530
- assertModeMask(a.key, "a.key");
531
- assertModeMask(b.key, "b.key");
532
- if (soundsLike(a.key, b.key)) {
533
- return spelledLike(a.tonic, b.tonic) ? "same" : "enharmonic";
643
+ const from = assertRecord(a, "a");
644
+ const to = assertRecord(b, "b");
645
+ maskOf(from.scale, "a.scale");
646
+ maskOf(to.scale, "b.scale");
647
+ if (soundsLike(from.scale, to.scale)) {
648
+ return spelledLike(from.tonic, to.tonic) ? "same" : "enharmonic";
534
649
  }
535
650
  for (const { relation, of } of CLOSELY_RELATED) {
536
- if (soundsLike(of(a.tonic, a.key).key, b.key)) {
651
+ if (soundsLike(of(from.tonic, from.scale).scale, to.scale)) {
537
652
  return relation;
538
653
  }
539
654
  }
540
655
  return null;
541
656
  }
542
657
 
658
+ // src/theory/scale/identity.ts
659
+ function carriesIdentity(value) {
660
+ return typeof value === "object" && value !== null;
661
+ }
662
+ function carriedTonic(tonic, scale) {
663
+ if (noteToPitchClass(tonic) !== scale.rootPc) {
664
+ throw new InvalidInputError(
665
+ `tonic ${formatNote(tonic)} does not match the scale root pitch class ${scale.rootPc}; pass a tonic that spells the scale root, or omit it to have one chosen`
666
+ );
667
+ }
668
+ return { letter: tonic.letter, alter: tonic.alter };
669
+ }
670
+ function carriedVariant(variant, scale) {
671
+ assertKeyVariant(variant, scale.modeMask12);
672
+ return variant;
673
+ }
674
+ function resolveKey(value) {
675
+ if (typeof value === "string") {
676
+ return resolveKeyName(value);
677
+ }
678
+ const scale = toKeyScale(value);
679
+ const carried = carriesIdentity(value) && "toJSON" in value && typeof value.toJSON === "function" ? value.toJSON() : value;
680
+ const tonic = carriesIdentity(carried) && carried.tonic !== void 0 ? carriedTonic(carried.tonic, scale) : spelledKeyOf(scale).tonic;
681
+ const variant = carriesIdentity(carried) && carried.variant !== void 0 ? carriedVariant(carried.variant, scale) : variantOfMask(scale.modeMask12);
682
+ return { scale, tonic, variant };
683
+ }
684
+ function scaleOf(key) {
685
+ const scale = assertRecord(key, "key").scale;
686
+ maskOf(scale, "key.scale");
687
+ return scale;
688
+ }
689
+
690
+ // src/theory/scale/key.ts
691
+ function spelled(scale) {
692
+ const { tonic, variant } = spelledKeyOf(scale);
693
+ return { ...scale, tonic, variant };
694
+ }
695
+ function majorKey(rootPc) {
696
+ return spelled(majorScale(rootPc));
697
+ }
698
+ function minorKey(rootPc) {
699
+ return spelled(minorScale(rootPc));
700
+ }
701
+ function scaleByName(name, rootPc) {
702
+ return spelled(namedScale(name, rootPc));
703
+ }
704
+
543
705
  // src/theory/scale/system.ts
544
706
  var SCALE_SYSTEMS = Object.freeze({
545
707
  major: "common-practice",
@@ -607,7 +769,7 @@ function scaleSystemOf(scale) {
607
769
  }
608
770
  return SCALE_SYSTEMS[canonical];
609
771
  }
610
- return SYSTEM_BY_MASK.get(scale.modeMask12);
772
+ return SYSTEM_BY_MASK.get(maskOf(scale, "scale"));
611
773
  }
612
774
  function supportsFunctionalHarmony(scale) {
613
775
  return scaleSystemOf(scale) !== "non-functional";
@@ -653,6 +815,7 @@ var BASE_TONES = {
653
815
  var SEVENTH_SEMITONES = { maj7: 11, min7: 10, dim7: 9 };
654
816
  var DEGREE_SEMITONES = { 6: 9, 9: 14, 11: 17, 13: 21 };
655
817
  var PERFECT_FIFTH = 7;
818
+ var FIFTH_KEY = "5";
656
819
  var OMITTABLE_DEGREES = [1, 3, 5];
657
820
  var BASE_QUALITY = {
658
821
  maj: "maj",
@@ -676,114 +839,149 @@ function parts(base, spec = {}) {
676
839
  }
677
840
  return built;
678
841
  }
679
- var QUALITY_SPECS = {
680
- maj: parts("maj"),
681
- min: parts("min"),
682
- dim: parts("dim"),
683
- aug: parts("aug"),
684
- majb5: parts("maj", { alt: [[5, -1]] }),
685
- maj7: parts("maj", { seventh: "maj7" }),
686
- min7: parts("min", { seventh: "min7" }),
687
- dom7: parts("maj", { seventh: "min7" }),
688
- dim7: parts("dim", { seventh: "dim7" }),
689
- m7b5: parts("dim", { seventh: "min7" }),
690
- minMaj7: parts("min", { seventh: "maj7" }),
691
- minMaj9: parts("min", { seventh: "maj7", alt: [[9, 0]] }),
692
- minMaj11: parts("min", {
693
- seventh: "maj7",
694
- alt: [
695
- [9, 0],
696
- [11, 0]
697
- ]
698
- }),
699
- minMaj13: parts("min", {
700
- seventh: "maj7",
701
- alt: [
702
- [9, 0],
703
- [13, 0]
704
- ]
705
- }),
706
- aug7: parts("aug", { seventh: "min7" }),
707
- augMaj7: parts("aug", { seventh: "maj7" }),
708
- "6": parts("maj", { add: [6] }),
709
- min6: parts("min", { add: [6] }),
710
- "6/9": parts("maj", { add: [6, 9] }),
711
- sus2: parts("sus2"),
712
- sus4: parts("sus4"),
713
- add9: parts("maj", { add: [9] }),
714
- add11: parts("maj", { add: [11] }),
715
- maj9: parts("maj", { seventh: "maj7", alt: [[9, 0]] }),
716
- min9: parts("min", { seventh: "min7", alt: [[9, 0]] }),
717
- dom9: parts("maj", { seventh: "min7", alt: [[9, 0]] }),
718
- "7b9": parts("maj", { seventh: "min7", alt: [[9, -1]] }),
719
- "7#9": parts("maj", { seventh: "min7", alt: [[9, 1]] }),
720
- "7#11": parts("maj", { seventh: "min7", alt: [[11, 1]] }),
721
- "7b13": parts("maj", { seventh: "min7", alt: [[13, -1]] }),
842
+ var QUALITY_SPECS = [
843
+ ["maj", parts("maj")],
844
+ ["min", parts("min")],
845
+ ["dim", parts("dim")],
846
+ ["aug", parts("aug")],
847
+ ["majb5", parts("maj", { alt: [[5, -1]] })],
848
+ ["maj7", parts("maj", { seventh: "maj7" })],
849
+ ["min7", parts("min", { seventh: "min7" })],
850
+ ["dom7", parts("maj", { seventh: "min7" })],
851
+ ["dim7", parts("dim", { seventh: "dim7" })],
852
+ ["m7b5", parts("dim", { seventh: "min7" })],
853
+ ["minMaj7", parts("min", { seventh: "maj7" })],
854
+ ["minMaj9", parts("min", { seventh: "maj7", alt: [[9, 0]] })],
855
+ [
856
+ "minMaj11",
857
+ parts("min", {
858
+ seventh: "maj7",
859
+ alt: [
860
+ [9, 0],
861
+ [11, 0]
862
+ ]
863
+ })
864
+ ],
865
+ [
866
+ "minMaj13",
867
+ parts("min", {
868
+ seventh: "maj7",
869
+ alt: [
870
+ [9, 0],
871
+ [13, 0]
872
+ ]
873
+ })
874
+ ],
875
+ ["aug7", parts("aug", { seventh: "min7" })],
876
+ ["augMaj7", parts("aug", { seventh: "maj7" })],
877
+ ["6", parts("maj", { add: [6] })],
878
+ ["min6", parts("min", { add: [6] })],
879
+ ["6/9", parts("maj", { add: [6, 9] })],
880
+ ["sus2", parts("sus2")],
881
+ ["sus4", parts("sus4")],
882
+ ["add9", parts("maj", { add: [9] })],
883
+ ["add11", parts("maj", { add: [11] })],
884
+ ["maj9", parts("maj", { seventh: "maj7", alt: [[9, 0]] })],
885
+ ["min9", parts("min", { seventh: "min7", alt: [[9, 0]] })],
886
+ ["dom9", parts("maj", { seventh: "min7", alt: [[9, 0]] })],
887
+ ["7b9", parts("maj", { seventh: "min7", alt: [[9, -1]] })],
888
+ ["7#9", parts("maj", { seventh: "min7", alt: [[9, 1]] })],
889
+ ["7#11", parts("maj", { seventh: "min7", alt: [[11, 1]] })],
890
+ ["7b13", parts("maj", { seventh: "min7", alt: [[13, -1]] })],
722
891
  // The eleventh chord as a chart voices it: the third gives way to the
723
892
  // eleventh a semitone above it rather than sounding against it.
724
- "11": parts("maj", {
725
- seventh: "min7",
726
- alt: [
727
- [9, 0],
728
- [11, 0]
729
- ],
730
- omit: [3]
731
- }),
732
- "13": parts("maj", {
733
- seventh: "min7",
734
- alt: [
735
- [9, 0],
736
- [13, 0]
737
- ]
738
- }),
739
- "5": parts("power"),
740
- "7sus4": parts("sus4", { seventh: "min7" }),
741
- "7b5": parts("maj", { seventh: "min7", alt: [[5, -1]] }),
893
+ [
894
+ "11",
895
+ parts("maj", {
896
+ seventh: "min7",
897
+ alt: [
898
+ [9, 0],
899
+ [11, 0]
900
+ ],
901
+ omit: [3]
902
+ })
903
+ ],
904
+ [
905
+ "13",
906
+ parts("maj", {
907
+ seventh: "min7",
908
+ alt: [
909
+ [9, 0],
910
+ [13, 0]
911
+ ]
912
+ })
913
+ ],
914
+ ["5", parts("power")],
915
+ ["7sus4", parts("sus4", { seventh: "min7" })],
916
+ ["7b5", parts("maj", { seventh: "min7", alt: [[5, -1]] })],
742
917
  // The altered dominant as it is voiced from a lead sheet: a dominant seventh
743
918
  // with a raised fifth and a raised ninth. The full seven-note altered stack
744
919
  // has no seven-letter spelling, so it is not what this symbol denotes.
745
- "7alt": parts("aug", { seventh: "min7", alt: [[9, 1]] }),
746
- "13b9": parts("maj", {
747
- seventh: "min7",
748
- alt: [
749
- [9, -1],
750
- [13, 0]
751
- ]
752
- }),
753
- maj13: parts("maj", {
754
- seventh: "maj7",
755
- alt: [
756
- [9, 0],
757
- [13, 0]
758
- ]
759
- }),
760
- "maj7#11": parts("maj", { seventh: "maj7", alt: [[11, 1]] }),
761
- min11: parts("min", {
762
- seventh: "min7",
763
- alt: [
764
- [9, 0],
765
- [11, 0]
766
- ]
767
- }),
768
- min13: parts("min", {
769
- seventh: "min7",
770
- alt: [
771
- [9, 0],
772
- [13, 0]
773
- ]
774
- }),
775
- minAdd9: parts("min", { add: [9] }),
776
- "min6/9": parts("min", { add: [6, 9] })
777
- };
920
+ ["7alt", parts("aug", { seventh: "min7", alt: [[9, 1]] })],
921
+ [
922
+ "13b9",
923
+ parts("maj", {
924
+ seventh: "min7",
925
+ alt: [
926
+ [9, -1],
927
+ [13, 0]
928
+ ]
929
+ })
930
+ ],
931
+ [
932
+ "maj13",
933
+ parts("maj", {
934
+ seventh: "maj7",
935
+ alt: [
936
+ [9, 0],
937
+ [13, 0]
938
+ ]
939
+ })
940
+ ],
941
+ ["maj7#11", parts("maj", { seventh: "maj7", alt: [[11, 1]] })],
942
+ [
943
+ "min11",
944
+ parts("min", {
945
+ seventh: "min7",
946
+ alt: [
947
+ [9, 0],
948
+ [11, 0]
949
+ ]
950
+ })
951
+ ],
952
+ [
953
+ "min13",
954
+ parts("min", {
955
+ seventh: "min7",
956
+ alt: [
957
+ [9, 0],
958
+ [13, 0]
959
+ ]
960
+ })
961
+ ],
962
+ ["minAdd9", parts("min", { add: [9] })],
963
+ ["min6/9", parts("min", { add: [6, 9] })]
964
+ ];
965
+ var QUALITY_ORDER = QUALITY_SPECS.map(([quality]) => quality);
966
+ var SPEC_BY_QUALITY = new Map(QUALITY_SPECS);
778
967
  function normalizeChordSpec(spec) {
779
968
  let base = spec.base;
780
969
  const alterations = /* @__PURE__ */ new Map();
781
970
  for (const alteration of spec.alterations ?? []) {
782
- alterations.set(alteration.degree, alteration.alter);
971
+ const { degree, alter } = alteration;
972
+ if (degree === 5) {
973
+ const current = alterations.get(FIFTH_KEY);
974
+ if (current !== void 0 && current.alter <= alter) {
975
+ continue;
976
+ }
977
+ alterations.set(FIFTH_KEY, { degree, alter });
978
+ continue;
979
+ }
980
+ alterations.set(`${degree}:${alter}`, { degree, alter });
783
981
  }
784
982
  const additions = new Set(spec.additions ?? []);
785
983
  const omissions = new Set(spec.omissions ?? []);
786
- const fifth = alterations.get(5);
984
+ const fifth = alterations.get(FIFTH_KEY)?.alter;
787
985
  if (fifth !== void 0) {
788
986
  if (base === "aug") {
789
987
  base = "maj";
@@ -791,19 +989,19 @@ function normalizeChordSpec(spec) {
791
989
  base = "min";
792
990
  }
793
991
  if (fifth === 0) {
794
- alterations.delete(5);
992
+ alterations.delete(FIFTH_KEY);
795
993
  } else if (base === "maj" && fifth === 1) {
796
994
  base = "aug";
797
- alterations.delete(5);
995
+ alterations.delete(FIFTH_KEY);
798
996
  } else if (base === "min" && fifth === -1) {
799
997
  base = "dim";
800
- alterations.delete(5);
998
+ alterations.delete(FIFTH_KEY);
801
999
  }
802
1000
  }
803
1001
  if (spec.seventh === void 0) {
804
- for (const [degree, alter] of [...alterations]) {
1002
+ for (const [key, { degree, alter }] of [...alterations]) {
805
1003
  if (degree !== 5 && alter === 0) {
806
- alterations.delete(degree);
1004
+ alterations.delete(key);
807
1005
  additions.add(degree);
808
1006
  }
809
1007
  }
@@ -811,7 +1009,7 @@ function normalizeChordSpec(spec) {
811
1009
  const normalized = {
812
1010
  rootPc: spec.rootPc,
813
1011
  base,
814
- alterations: [...alterations].sort((a, b) => a[0] - b[0]).map(([degree, alter]) => ({ degree, alter })),
1012
+ alterations: [...alterations.values()].sort((a, b) => a.degree - b.degree || a.alter - b.alter),
815
1013
  additions: [...additions].sort((a, b) => a - b),
816
1014
  omissions: [...omissions].sort((a, b) => a - b)
817
1015
  };
@@ -851,10 +1049,7 @@ function chordSpecIntervals(spec) {
851
1049
  return specIntervals(assertChordSpec(spec));
852
1050
  }
853
1051
  var QUALITY_INTERVALS = Object.fromEntries(
854
- Object.keys(QUALITY_SPECS).map((quality) => [
855
- quality,
856
- specIntervals(normalizeChordSpec(QUALITY_SPECS[quality]))
857
- ])
1052
+ QUALITY_SPECS.map(([quality, spec]) => [quality, specIntervals(normalizeChordSpec(spec))])
858
1053
  );
859
1054
  function specKey(spec) {
860
1055
  const normalized = normalizeChordSpec(spec);
@@ -867,16 +1062,14 @@ function specKey(spec) {
867
1062
  ].join("|");
868
1063
  }
869
1064
  var QUALITY_BY_KEY = new Map(
870
- Object.keys(QUALITY_SPECS).map((quality) => [
871
- specKey(QUALITY_SPECS[quality]),
872
- quality
873
- ])
1065
+ QUALITY_SPECS.map(([quality, spec]) => [specKey(spec), quality])
874
1066
  );
875
1067
  function chordSpecForQuality(quality, rootPc, bassPc) {
876
- if (!Object.hasOwn(QUALITY_SPECS, quality)) {
1068
+ const named = SPEC_BY_QUALITY.get(quality);
1069
+ if (named === void 0) {
877
1070
  throw new InvalidInputError(`Unknown chord quality: ${String(quality)}`);
878
1071
  }
879
- const spec = normalizeChordSpec(QUALITY_SPECS[quality]);
1072
+ const spec = normalizeChordSpec(named);
880
1073
  spec.rootPc = rootPc;
881
1074
  if (bassPc !== void 0) {
882
1075
  spec.bassPc = bassPc;
@@ -892,10 +1085,10 @@ function chordSpecQuality(spec) {
892
1085
  if (exact !== void 0) {
893
1086
  return exact;
894
1087
  }
895
- const offsets = new Set(specIntervals(checked));
1088
+ const offsets = new Set(specIntervals({ ...checked, omissions: [] }));
896
1089
  let nearest;
897
1090
  let size = 0;
898
- for (const quality of Object.keys(QUALITY_INTERVALS)) {
1091
+ for (const quality of QUALITY_ORDER) {
899
1092
  const template = QUALITY_INTERVALS[quality];
900
1093
  if (template.length > size && template.every((offset) => offsets.has(offset))) {
901
1094
  nearest = quality;
@@ -982,7 +1175,7 @@ function chordSpecFromIntervals(intervals) {
982
1175
  function assertPart(value, allowed, name) {
983
1176
  if (!allowed.includes(value)) {
984
1177
  throw new InvalidInputError(
985
- `${name} must be one of ${allowed.join(", ")}; received ${JSON.stringify(value)}`
1178
+ `${name} must be one of ${allowed.join(", ")}; received ${describeRejected(value)}`
986
1179
  );
987
1180
  }
988
1181
  return value;
@@ -1034,13 +1227,23 @@ function assertChordSpec(spec) {
1034
1227
  // src/theory/chord/index.ts
1035
1228
  var MAX_CHORD_INTERVAL = 127;
1036
1229
  function chordQualities() {
1037
- return Object.keys(QUALITY_INTERVALS);
1230
+ return [...QUALITY_ORDER];
1231
+ }
1232
+ function assertChordQuality(quality) {
1233
+ if (!Object.hasOwn(QUALITY_INTERVALS, quality)) {
1234
+ throw new InvalidInputError(`Unknown chord quality: ${String(quality)}`);
1235
+ }
1236
+ return quality;
1237
+ }
1238
+ function assertChord(chord, name = "chord") {
1239
+ const read = assertRecord(chord, name);
1240
+ assertFiniteNumber(read.rootPc, `${name}.rootPc`);
1241
+ assertChordQuality(read.quality);
1242
+ return read;
1038
1243
  }
1039
1244
  function chordFromDegree(degree, ext, key) {
1040
1245
  assertDegree(degree, "chord degree");
1041
- if (!Object.hasOwn(QUALITY_INTERVALS, ext)) {
1042
- throw new InvalidInputError(`Unknown chord quality: ${String(ext)}`);
1043
- }
1246
+ assertChordQuality(ext);
1044
1247
  const scale = toKeyScale(key);
1045
1248
  const tones = scaleTonesInDegreeOrder(scale);
1046
1249
  const length = tones.length;
@@ -1053,9 +1256,7 @@ function makeChord(rootPc, quality, bassPc) {
1053
1256
  if (bassPc !== void 0) {
1054
1257
  assertFiniteNumber(bassPc, "chord bassPc");
1055
1258
  }
1056
- if (!Object.hasOwn(QUALITY_INTERVALS, quality)) {
1057
- throw new InvalidInputError(`Unknown chord quality: ${String(quality)}`);
1058
- }
1259
+ assertChordQuality(quality);
1059
1260
  const chord = {
1060
1261
  rootPc: pitchClassOf(rootPc),
1061
1262
  quality,
@@ -1079,6 +1280,7 @@ function chordFromSpec(spec) {
1079
1280
  return chord;
1080
1281
  }
1081
1282
  function chordSpecOf(chord) {
1283
+ assertRecord(chord, "chord");
1082
1284
  assertFiniteNumber(chord.rootPc, "chord rootPc");
1083
1285
  const rootPc = pitchClassOf(chord.rootPc);
1084
1286
  const bassPc = chord.bassPc === void 0 ? void 0 : pitchClassOf(chord.bassPc);
@@ -1119,6 +1321,7 @@ function hasCustomIntervals(chord) {
1119
1321
  return !matchesQualityIntervals(chord.quality, chord.intervals);
1120
1322
  }
1121
1323
  function chordFromSpan(span) {
1324
+ assertRecord(span, "chord span");
1122
1325
  const chord = makeChord(span.rootPc, span.quality, span.bassPc);
1123
1326
  if (span.intervals !== void 0) {
1124
1327
  chord.intervals = copySpanIntervals(span.intervals);
@@ -1127,9 +1330,7 @@ function chordFromSpan(span) {
1127
1330
  }
1128
1331
  function spanFromChord(chord, startBeat) {
1129
1332
  assertFiniteNumber(startBeat, "chord span startBeat");
1130
- if (!Object.hasOwn(QUALITY_INTERVALS, chord.quality)) {
1131
- throw new InvalidInputError(`Unknown chord quality: ${String(chord.quality)}`);
1132
- }
1333
+ assertChord(chord);
1133
1334
  const span = { rootPc: chord.rootPc, quality: chord.quality, startBeat };
1134
1335
  if (chord.bassPc !== void 0) {
1135
1336
  span.bassPc = chord.bassPc;
@@ -1140,6 +1341,7 @@ function spanFromChord(chord, startBeat) {
1140
1341
  return span;
1141
1342
  }
1142
1343
  function transposeChord(chord, semitones) {
1344
+ assertChord(chord);
1143
1345
  assertFiniteNumber(semitones, "semitones");
1144
1346
  const steps = Math.round(semitones);
1145
1347
  const moved = {
@@ -1171,59 +1373,145 @@ function chordToneSpellings(chord) {
1171
1373
  }
1172
1374
  return usable;
1173
1375
  }
1174
- function chordPitchClasses(chord, opts = {}) {
1175
- const set = /* @__PURE__ */ new Set();
1376
+ function chordToneLetterOffsets(chord) {
1377
+ const hints = chordToneSpellings(chord);
1378
+ const rootHint = hints?.[chord.intervals.findIndex((interval) => pitchClassOf(interval) === 0)];
1379
+ if (hints === void 0 || rootHint === void 0) {
1380
+ return void 0;
1381
+ }
1382
+ return hints.map((hint) => diatonicLetterOf(hint.letter - rootHint.letter));
1383
+ }
1384
+ var ROLE_BY_LETTER_OFFSET = {
1385
+ 0: "root",
1386
+ 2: "third",
1387
+ 4: "fifth",
1388
+ 5: "sixth",
1389
+ 6: "seventh"
1390
+ };
1391
+ function spelledToneRole(pitch, chord) {
1392
+ const offsets = chordToneLetterOffsets(chord);
1393
+ if (offsets === void 0) {
1394
+ return void 0;
1395
+ }
1396
+ const index = chord.intervals.findIndex(
1397
+ (interval) => pitchClassOf(chord.rootPc + interval) === pitchClassOf(pitch)
1398
+ );
1399
+ const offset = offsets[index];
1400
+ return offset === void 0 ? void 0 : ROLE_BY_LETTER_OFFSET[offset] ?? null;
1401
+ }
1402
+ function chordPcMask(chord, opts = {}) {
1403
+ assertRecord(chord, "chord");
1404
+ assertArray(chord.intervals, "chord intervals");
1405
+ let mask = 0;
1176
1406
  for (const interval of chord.intervals) {
1177
- set.add(pitchClassOf(chord.rootPc + interval));
1407
+ mask |= 1 << pitchClassOf(chord.rootPc + interval);
1178
1408
  }
1179
1409
  if (opts.includeBass !== false && chord.bassPc !== void 0) {
1180
- set.add(pitchClassOf(chord.bassPc));
1410
+ mask |= 1 << pitchClassOf(chord.bassPc);
1411
+ }
1412
+ return mask;
1413
+ }
1414
+ function hasPitchClass(mask, pc) {
1415
+ return (mask >> pc & 1) === 1;
1416
+ }
1417
+ function chordIntervalMask(chord) {
1418
+ let mask = 0;
1419
+ for (const interval of chord.intervals) {
1420
+ mask |= 1 << (interval % 12 + 12) % 12;
1421
+ }
1422
+ return mask;
1423
+ }
1424
+ function chordPitchClasses(chord, opts = {}) {
1425
+ const mask = chordPcMask(assertChord(chord), assertOptions(opts, "opts"));
1426
+ const pcs = [];
1427
+ for (let pc = 0; pc < 12; pc += 1) {
1428
+ if (hasPitchClass(mask, pc)) {
1429
+ pcs.push(pc);
1430
+ }
1181
1431
  }
1182
- return [...set].sort((a, b) => a - b);
1432
+ return pcs;
1183
1433
  }
1184
1434
  function intervalAboveRoot(pitch, chord) {
1185
- return ((pitchClassOf(pitch) - pitchClassOf(chord.rootPc)) % 12 + 12) % 12;
1435
+ return ((pitchClassOf(pitch) - pitchClassOf(assertChord(chord).rootPc)) % 12 + 12) % 12;
1186
1436
  }
1187
1437
  function isChordMember(pitch, chord) {
1188
- return chord === null ? false : chordPitchClasses(chord).includes(pitchClassOf(pitch));
1189
- }
1190
- function chordToneOffsets(chord) {
1191
- return new Set(chord.intervals.map((i) => (i % 12 + 12) % 12));
1438
+ return chord === null ? false : hasPitchClass(chordPcMask(chord), pitchClassOf(pitch));
1192
1439
  }
1193
1440
  function chordToneRole(pitch, chord) {
1441
+ assertRecord(chord, "chord");
1442
+ const spelled2 = spelledToneRole(pitch, chord);
1443
+ if (spelled2 !== void 0) {
1444
+ return spelled2;
1445
+ }
1194
1446
  const interval = (pitchClassOf(pitch) - pitchClassOf(chord.rootPc) + 12) % 12;
1195
- const tones = chordToneOffsets(chord);
1447
+ const tones = chordIntervalMask(chord);
1448
+ const sounds = (ic) => hasPitchClass(tones, ic);
1196
1449
  if (interval === 0) {
1197
- return "root";
1450
+ return sounds(0) ? "root" : null;
1198
1451
  }
1199
1452
  if (interval === 3 || interval === 4) {
1200
- return tones.has(interval) && (interval !== 3 || !tones.has(4)) ? "third" : null;
1453
+ return sounds(interval) && (interval !== 3 || !sounds(4)) ? "third" : null;
1201
1454
  }
1202
1455
  if (interval === 7) {
1203
- return tones.has(7) ? "fifth" : null;
1456
+ return sounds(7) ? "fifth" : null;
1204
1457
  }
1205
1458
  if (interval === 6) {
1206
- return tones.has(6) && !tones.has(7) ? "fifth" : null;
1459
+ return sounds(6) && !sounds(7) ? "fifth" : null;
1207
1460
  }
1208
1461
  if (interval === 8) {
1209
- return tones.has(8) && !tones.has(7) ? "fifth" : null;
1462
+ return sounds(8) && !sounds(7) ? "fifth" : null;
1210
1463
  }
1211
1464
  if (interval === 9) {
1212
- const hasHigherSeventh = tones.has(10) || tones.has(11);
1213
- const isDiminishedSeventh = tones.has(3) && tones.has(6) && !hasHigherSeventh;
1214
- if (tones.has(9) && isDiminishedSeventh) {
1465
+ const hasHigherSeventh = sounds(10) || sounds(11);
1466
+ const isDiminishedSeventh = sounds(3) && sounds(6) && !hasHigherSeventh;
1467
+ if (sounds(9) && isDiminishedSeventh) {
1215
1468
  return "seventh";
1216
1469
  }
1217
- if (tones.has(9) && !hasHigherSeventh) {
1470
+ if (sounds(9) && !hasHigherSeventh) {
1218
1471
  return "sixth";
1219
1472
  }
1220
1473
  return null;
1221
1474
  }
1222
1475
  if (interval === 10 || interval === 11) {
1223
- return tones.has(interval) ? "seventh" : null;
1476
+ return sounds(interval) ? "seventh" : null;
1224
1477
  }
1225
1478
  return null;
1226
1479
  }
1480
+ var DEGREE_BY_ROLE = {
1481
+ root: 1,
1482
+ third: 3,
1483
+ fifth: 5,
1484
+ sixth: 6,
1485
+ seventh: 7
1486
+ };
1487
+ var TENSION_DEGREE = [1, 2, 2, 2, 3, 4, 4, 5, 6, 6, 7, 7];
1488
+ function degreeOfInterval(interval, chord) {
1489
+ const role = chordToneRole(pitchClassOf(chord.rootPc + interval), chord);
1490
+ if (role !== null) {
1491
+ return DEGREE_BY_ROLE[role];
1492
+ }
1493
+ return TENSION_DEGREE[(interval % 12 + 12) % 12];
1494
+ }
1495
+ function thirdSlotOf(chord) {
1496
+ const tones = chordIntervalMask(chord);
1497
+ if (hasPitchClass(tones, 4)) {
1498
+ return { kind: "third", interval: 4 };
1499
+ }
1500
+ if (hasPitchClass(tones, 3)) {
1501
+ return { kind: "third", interval: 3 };
1502
+ }
1503
+ const spec = chordSpecOf(chord);
1504
+ if (spec.base === "sus2") {
1505
+ return hasPitchClass(tones, 2) ? { kind: "suspension", interval: 2 } : { kind: "none" };
1506
+ }
1507
+ if (spec.base === "sus4" || spec.omissions.includes(3)) {
1508
+ return hasPitchClass(tones, 5) ? { kind: "suspension", interval: 5 } : { kind: "none" };
1509
+ }
1510
+ return { kind: "none" };
1511
+ }
1512
+ function displacedThirds(chord) {
1513
+ return thirdSlotOf(chord).kind === "suspension" ? [pitchClassOf(chord.rootPc + 3), pitchClassOf(chord.rootPc + 4)] : [];
1514
+ }
1227
1515
  function classifyTriad(thirdIc, fifthIc) {
1228
1516
  if (thirdIc === 4 && fifthIc === 8) {
1229
1517
  return "aug";
@@ -1396,16 +1684,32 @@ function capChromaticAccidentals(preferred, alternative) {
1396
1684
  return alternative !== void 0 && Math.abs(preferred.alter) - Math.abs(alternative.alter) >= 2 ? alternative : preferred;
1397
1685
  }
1398
1686
  var DIATONIC_LETTER_COUNT = 7;
1687
+ var MAX_JOINT_TONE_COUNT = DIATONIC_LETTER_COUNT + 1;
1688
+ var MAJOR_SEMITONES = [0, 2, 4, 5, 7, 9, 11];
1689
+ var IMPERFECT_DEGREES = /* @__PURE__ */ new Set([1, 2, 5, 6]);
1690
+ var DIATONIC_MODE_OFFSETS2 = MAJOR_SEMITONES.map(
1691
+ (tonic, mode) => MAJOR_SEMITONES.map((_, degree) => pitchClassOf((MAJOR_SEMITONES[(mode + degree) % 7] ?? 0) - tonic))
1692
+ );
1693
+ function degreeStrain(tonic, note) {
1694
+ const letterOffset = diatonicLetterOf(note.letter - tonic.letter);
1695
+ const offset = pitchClassOf(notePitchClass(note) - notePitchClass(tonic));
1696
+ let deviation = pitchClassOf(offset - (MAJOR_SEMITONES[letterOffset] ?? 0));
1697
+ if (deviation > 6) {
1698
+ deviation -= 12;
1699
+ }
1700
+ const strain = IMPERFECT_DEGREES.has(letterOffset) && deviation < 0 ? Math.abs(deviation + 1) : Math.abs(deviation);
1701
+ return Math.min(strain, 2);
1702
+ }
1399
1703
  function isHeptatonic(key) {
1400
1704
  return scaleTonesInDegreeOrder(key).length === DIATONIC_LETTER_COUNT;
1401
1705
  }
1402
- function hasMinorThird2(key) {
1706
+ function hasMinorThird(key) {
1403
1707
  const tones = scaleTonesInDegreeOrder(key);
1404
1708
  const root = pitchClassOf(key.rootPc);
1405
1709
  return tones.includes(pitchClassOf(root + 3)) && !tones.includes(pitchClassOf(root + 4));
1406
1710
  }
1407
1711
  function leansFlat(tonic, key) {
1408
- return tonic.alter < 0 || hasMinorThird2(key);
1712
+ return tonic.alter < 0 || hasMinorThird(key);
1409
1713
  }
1410
1714
  function gappedCandidates(tonic, offset, pc, preferFlat) {
1411
1715
  const flat = letterFor(tonic, CHROMATIC_SPELLING_FLAT[offset], pc);
@@ -1417,22 +1721,64 @@ function gappedCandidates(tonic, offset, pc, preferFlat) {
1417
1721
  const alternative = preferred === flat ? sharp : flat;
1418
1722
  return alternative === void 0 || alternative.letter === preferred.letter ? [preferred] : [preferred, alternative];
1419
1723
  }
1420
- function assignDistinctLetters(candidates, reserved) {
1724
+ function spellByParentMode(tonic, pcs, preferFlat) {
1725
+ const tonicPc = notePitchClass(tonic);
1726
+ let best;
1727
+ let bestAlter = 0;
1728
+ let bestSigned = 0;
1729
+ for (const offsets of DIATONIC_MODE_OFFSETS2) {
1730
+ const notes = [];
1731
+ let alter = 0;
1732
+ let signed = 0;
1733
+ for (const pc of pcs) {
1734
+ const degree = offsets.indexOf(pitchClassOf(pc - tonicPc));
1735
+ const note = letterFor(tonic, degree < 0 ? void 0 : degree, pc);
1736
+ if (note === void 0) {
1737
+ break;
1738
+ }
1739
+ notes.push(note);
1740
+ alter += Math.abs(note.alter);
1741
+ signed += note.alter;
1742
+ }
1743
+ if (notes.length !== pcs.length) {
1744
+ continue;
1745
+ }
1746
+ const evenlyLight = alter === bestAlter && (preferFlat ? signed < bestSigned : signed > bestSigned);
1747
+ const lighter = alter < bestAlter || evenlyLight;
1748
+ if (best === void 0 || lighter) {
1749
+ best = notes;
1750
+ bestAlter = alter;
1751
+ bestSigned = signed;
1752
+ }
1753
+ }
1754
+ return best;
1755
+ }
1756
+ function assignDistinctLetters(candidates, tonic, duplicates) {
1757
+ const reserved = 1 << diatonicLetterOf(tonic.letter);
1421
1758
  let best;
1759
+ let bestStrain = 0;
1422
1760
  let bestAlter = 0;
1423
1761
  let bestDeviations = 0;
1424
1762
  for (let choice = 0; choice < 1 << candidates.length; choice += 1) {
1425
1763
  const picked = [];
1426
1764
  let letters = reserved;
1765
+ let spare = duplicates;
1766
+ let strain = 0;
1427
1767
  let alter = 0;
1428
1768
  let deviations = 0;
1429
1769
  for (const [index, options] of candidates.entries()) {
1430
1770
  const preferred = options[0];
1431
1771
  const note = options[choice >> index & 1] ?? preferred;
1432
- if (note === void 0 || (letters & 1 << note.letter) !== 0) {
1772
+ if (note === void 0) {
1773
+ break;
1774
+ }
1775
+ const taken = (letters & 1 << note.letter) !== 0;
1776
+ if (taken && (spare === 0 || (reserved & 1 << note.letter) !== 0)) {
1433
1777
  break;
1434
1778
  }
1779
+ spare -= taken ? 1 : 0;
1435
1780
  letters |= 1 << note.letter;
1781
+ strain += degreeStrain(tonic, note);
1436
1782
  alter += Math.abs(note.alter);
1437
1783
  deviations += note === preferred ? 0 : 1;
1438
1784
  picked.push(note);
@@ -1440,8 +1786,10 @@ function assignDistinctLetters(candidates, reserved) {
1440
1786
  if (picked.length !== candidates.length) {
1441
1787
  continue;
1442
1788
  }
1443
- if (best === void 0 || alter < bestAlter || alter === bestAlter && deviations < bestDeviations) {
1789
+ const better = duplicates > 0 && strain !== bestStrain ? strain < bestStrain : alter !== bestAlter ? alter < bestAlter : deviations < bestDeviations;
1790
+ if (best === void 0 || better) {
1444
1791
  best = picked;
1792
+ bestStrain = strain;
1445
1793
  bestAlter = alter;
1446
1794
  bestDeviations = deviations;
1447
1795
  }
@@ -1449,18 +1797,33 @@ function assignDistinctLetters(candidates, reserved) {
1449
1797
  return best;
1450
1798
  }
1451
1799
  function spellGappedScale(tonic, key) {
1800
+ const cacheKey = `${diatonicLetterOf(tonic.letter)}:${tonic.alter}:${key.rootPc}:${key.modeMask12}`;
1801
+ const cached = JOINT_SPELLINGS.get(cacheKey);
1802
+ if (cached !== void 0) {
1803
+ return cached.spellings;
1804
+ }
1805
+ const answer = readGappedScale(tonic, key);
1806
+ if (JOINT_SPELLINGS.size >= MAX_REMEMBERED_SCALES) {
1807
+ JOINT_SPELLINGS.clear();
1808
+ }
1809
+ JOINT_SPELLINGS.set(cacheKey, { spellings: answer });
1810
+ return answer;
1811
+ }
1812
+ var MAX_REMEMBERED_SCALES = 512;
1813
+ var JOINT_SPELLINGS = /* @__PURE__ */ new Map();
1814
+ function readGappedScale(tonic, key) {
1452
1815
  const tones = scaleTonesInDegreeOrder(key);
1453
- if (tones.length > DIATONIC_LETTER_COUNT) {
1816
+ if (tones.length > MAX_JOINT_TONE_COUNT) {
1454
1817
  return void 0;
1455
1818
  }
1456
1819
  const tonicPc = pitchClassOf(naturalPitchClassOf(tonic.letter) + tonic.alter);
1457
1820
  const tonicNote = { letter: diatonicLetterOf(tonic.letter), alter: tonic.alter };
1458
1821
  const preferFlat = leansFlat(tonic, key);
1459
1822
  const rest = tones.filter((pc) => pitchClassOf(pc - tonicPc) !== 0);
1460
- const chosen = assignDistinctLetters(
1461
- rest.map((pc) => gappedCandidates(tonic, pitchClassOf(pc - tonicPc), pc, preferFlat)),
1462
- 1 << tonicNote.letter
1463
- );
1823
+ const candidates = rest.map((pc) => gappedCandidates(tonic, pitchClassOf(pc - tonicPc), pc, preferFlat));
1824
+ const chosen = spellByParentMode(tonicNote, rest, preferFlat) ?? // A scale that can hold a letter per tone does; only one that cannot writes
1825
+ // a letter twice, and then where the doubling falls is what is chosen.
1826
+ assignDistinctLetters(candidates, tonicNote, 0) ?? assignDistinctLetters(candidates, tonicNote, 1);
1464
1827
  if (chosen === void 0) {
1465
1828
  return void 0;
1466
1829
  }
@@ -1498,7 +1861,7 @@ function spellByKey(pc, tonic, key) {
1498
1861
  if (isScaleTone(pc, key)) {
1499
1862
  const assigned = spellGappedScale(tonic, key)?.get(pitchClassOf(pc));
1500
1863
  if (assigned !== void 0) {
1501
- return assigned;
1864
+ return { letter: assigned.letter, alter: assigned.alter };
1502
1865
  }
1503
1866
  }
1504
1867
  const [preferred] = gappedCandidates(tonic, offset, pc, leansFlat(tonic, key));
@@ -1523,13 +1886,39 @@ var CHORD_TONE_LETTER_STEPS = {
1523
1886
  11: 6
1524
1887
  // major seventh
1525
1888
  };
1889
+ var LETTER_STEP_BY_ROLE = {
1890
+ root: 0,
1891
+ third: 2,
1892
+ fifth: 4,
1893
+ sixth: 5,
1894
+ seventh: 6
1895
+ };
1526
1896
  var MAX_CONTEXT_ALTER = 2;
1527
- function chordToneSpelling(pc, chordRoot, tonic, key) {
1897
+ function chordToneLetterStep(pc, rootPc, chord) {
1898
+ if (chord !== void 0) {
1899
+ const offsets = chordToneLetterOffsets(chord);
1900
+ const index = chord.intervals.findIndex(
1901
+ (interval) => pitchClassOf(chord.rootPc + interval) === pitchClassOf(pc)
1902
+ );
1903
+ const offset = offsets?.[index];
1904
+ if (offset !== void 0) {
1905
+ return offset;
1906
+ }
1907
+ const role = chordToneRole(pc, chord);
1908
+ if (role !== null) {
1909
+ return LETTER_STEP_BY_ROLE[role];
1910
+ }
1911
+ }
1912
+ return CHORD_TONE_LETTER_STEPS[pitchClassOf(pc - rootPc)];
1913
+ }
1914
+ function spellChordTone(pc, context, tonic, key) {
1915
+ const { chord } = context;
1916
+ const chordRoot = chord?.rootPc ?? context.chordRoot;
1528
1917
  if (chordRoot === void 0) {
1529
1918
  return void 0;
1530
1919
  }
1531
- const root = spellByKey(pitchClassOf(chordRoot), tonic, key);
1532
- return letterFor(root, CHORD_TONE_LETTER_STEPS[pitchClassOf(pc - chordRoot)], pc);
1920
+ const root = chord === void 0 ? spellByKey(pitchClassOf(chordRoot), tonic, key) : chordRootSpelling(chord, tonic, key);
1921
+ return letterFor(root, chordToneLetterStep(pc, chordRoot, chord), pc);
1533
1922
  }
1534
1923
  function neighbourSpelling(neighbour, gap, letterStep, pc, tonic, key) {
1535
1924
  if (neighbour === void 0 || pitchClassOf(neighbour - pc) !== pitchClassOf(gap)) {
@@ -1542,10 +1931,10 @@ function refineByContext(pc, tonic, key, context, keySpelling) {
1542
1931
  if (pitchClassOf(pc) === tonicPc || isScaleTone(pc, key)) {
1543
1932
  return keySpelling;
1544
1933
  }
1545
- const { chordRoot, previous, next } = context;
1934
+ const { previous, next } = context;
1546
1935
  const candidates = [
1547
- // The sounding chord names the pitch by its interval above the root.
1548
- chordToneSpelling(pc, chordRoot, tonic, key),
1936
+ // The sounding chord names the pitch by the degree it writes it as.
1937
+ spellChordTone(pc, context, tonic, key),
1549
1938
  // A semitone below what follows: the pitch is its leading tone, a letter down.
1550
1939
  neighbourSpelling(next, 1, -1, pc, tonic, key),
1551
1940
  // A semitone above what preceded: the pitch is that note inflected upward,
@@ -1569,6 +1958,7 @@ function spellPitchClass(pc, tonic, key, context) {
1569
1958
  const note = toNoteData(tonic);
1570
1959
  const scale = toKeyScale(key);
1571
1960
  assertTonicOf(note, scale, "spellPitchClass");
1961
+ assertOptions(context, "context");
1572
1962
  return spelledPitchClass(pc, note, scale, context);
1573
1963
  }
1574
1964
  function spelledPitchClass(pc, tonic, key, context) {
@@ -1577,15 +1967,18 @@ function spelledPitchClass(pc, tonic, key, context) {
1577
1967
  }
1578
1968
  function spellScale(tonic, key) {
1579
1969
  const note = toNoteData(tonic);
1580
- const scale = key;
1970
+ const scale = toKeyScale(key);
1581
1971
  assertTonicOf(note, scale, "spellScale");
1582
1972
  return scaleTonesInDegreeOrder(scale).map((pc) => spelledPitchClass(pc, note, scale));
1583
1973
  }
1584
1974
  function spellPitchClasses(pcs, tonic, key) {
1975
+ const classes = assertArray(pcs, "pcs");
1585
1976
  const note = toNoteData(tonic);
1586
1977
  const scale = toKeyScale(key);
1587
1978
  assertTonicOf(note, scale, "spellPitchClasses");
1588
- return pcs.map((pc) => spelledPitchClass(pc, note, scale));
1979
+ return classes.map(
1980
+ (pc, index) => spelledPitchClass(assertPitchClass(pc, `pcs[${index}]`), note, scale)
1981
+ );
1589
1982
  }
1590
1983
  function chordLetterOffset(interval, chord) {
1591
1984
  switch (interval) {
@@ -1599,11 +1992,11 @@ function chordLetterOffset(interval, chord) {
1599
1992
  return 1;
1600
1993
  case 3:
1601
1994
  case 4:
1602
- case 16:
1603
1995
  return 2;
1604
1996
  case 15:
1605
1997
  return chord.intervals.some((i) => i === 3 || i === 4) ? 1 : 2;
1606
1998
  case 5:
1999
+ case 16:
1607
2000
  case 17:
1608
2001
  case 18:
1609
2002
  return 3;
@@ -1616,10 +2009,10 @@ function chordLetterOffset(interval, chord) {
1616
2009
  return chord.quality === "dim7" ? 6 : 5;
1617
2010
  case 20:
1618
2011
  case 21:
2012
+ case 22:
1619
2013
  return 5;
1620
2014
  case 10:
1621
2015
  case 11:
1622
- case 22:
1623
2016
  case 23:
1624
2017
  return 6;
1625
2018
  default: {
@@ -1630,22 +2023,14 @@ function chordLetterOffset(interval, chord) {
1630
2023
  }
1631
2024
  }
1632
2025
  }
1633
- function hintedLetterOffsets(chord) {
1634
- const hints = chordToneSpellings(chord);
1635
- const rootHint = hints?.[chord.intervals.findIndex((interval) => pitchClassOf(interval) === 0)];
1636
- if (hints === void 0 || rootHint === void 0) {
1637
- return void 0;
1638
- }
1639
- return hints.map((hint) => diatonicLetterOf(hint.letter - rootHint.letter));
1640
- }
1641
2026
  function spellChordFromRoot(chord, root) {
1642
- return chordTonesFromRoot(chord, toNoteData(root));
2027
+ return chordTonesFromRoot(assertChord(chord), toNoteData(root));
1643
2028
  }
1644
2029
  function chordTonesFromRoot(chord, root) {
1645
2030
  if (pitchClassOf(naturalPitchClassOf(root.letter) + root.alter) !== pitchClassOf(chord.rootPc)) {
1646
2031
  throw new InvalidInputError("chord root spelling must match chord.rootPc");
1647
2032
  }
1648
- const hinted = hintedLetterOffsets(chord);
2033
+ const hinted = chordToneLetterOffsets(chord);
1649
2034
  return chord.intervals.map((interval, index) => {
1650
2035
  const letter = diatonicLetterOf(root.letter + (hinted?.[index] ?? chordLetterOffset(interval, chord)));
1651
2036
  const pc = pitchClassOf(chord.rootPc + interval);
@@ -1653,6 +2038,7 @@ function chordTonesFromRoot(chord, root) {
1653
2038
  });
1654
2039
  }
1655
2040
  function spellChord(chord, tonic, key) {
2041
+ assertChord(chord);
1656
2042
  const note = toNoteData(tonic);
1657
2043
  const scale = toKeyScale(key);
1658
2044
  assertTonicOf(note, scale, "spellChord");
@@ -1669,16 +2055,26 @@ function spellPitch(pitch, tonic, key, context) {
1669
2055
  const note = toNoteData(tonic);
1670
2056
  const scale = toKeyScale(key);
1671
2057
  assertTonicOf(note, scale, "spellPitch");
1672
- const rounded = Math.round(pitch);
1673
- const spelled = spelledPitchClass(pitchClassOf(rounded), note, scale, context);
1674
- const octave = (rounded - naturalPitchClassOf(spelled.letter) - spelled.alter) / 12 - 1;
1675
- return { letter: spelled.letter, alter: spelled.alter, octave };
2058
+ assertOptions(context, "context");
2059
+ const rounded = Math.round(assertFiniteNumber(pitch, "pitch"));
2060
+ const spelled2 = spelledPitchClass(pitchClassOf(rounded), note, scale, context);
2061
+ const octave = (rounded - naturalPitchClassOf(spelled2.letter) - spelled2.alter) / 12 - 1;
2062
+ return { letter: spelled2.letter, alter: spelled2.alter, octave };
1676
2063
  }
1677
2064
  function noteNames(notes) {
2065
+ assertArray(notes, "notes");
1678
2066
  return notes.map((note) => formatNote(toNoteData(note)));
1679
2067
  }
1680
2068
 
1681
2069
  // src/theory/counterpoint/internal.ts
2070
+ function assertQualityLabel(quality) {
2071
+ if (typeof quality !== "string" || !/^(P|M|m|A+|d+)$/.test(quality)) {
2072
+ throw new InvalidInputError(
2073
+ `interval.quality must be an interval quality; received ${describeRejected(quality)}`
2074
+ );
2075
+ }
2076
+ return quality;
2077
+ }
1682
2078
  function pitchOf(value) {
1683
2079
  return typeof value === "number" ? value : noteToMidi(value);
1684
2080
  }
@@ -1686,8 +2082,10 @@ function simpleIntervalNumber(numberValue) {
1686
2082
  return (Math.abs(numberValue) - 1) % 7 + 1;
1687
2083
  }
1688
2084
  function classifySpelledInterval(interval, twoVoice = true) {
1689
- const simple = simpleIntervalNumber(interval.number);
1690
- const quality = interval.quality;
2085
+ assertFlag(twoVoice, "twoVoice");
2086
+ const read = assertRecord(interval, "interval");
2087
+ const simple = simpleIntervalNumber(assertDegree(read.number, "interval.number"));
2088
+ const quality = assertQualityLabel(read.quality);
1691
2089
  if (quality === "P") {
1692
2090
  if (simple === 1 || simple === 5) {
1693
2091
  return 0 /* PerfectConsonance */;
@@ -1738,23 +2136,26 @@ function motionBetween(from, to) {
1738
2136
  return before === after ? "parallel" : "similar";
1739
2137
  }
1740
2138
  function voiceIndependence(lead, counter, opts) {
1741
- if (lead.length !== counter.length) {
2139
+ assertOptions(opts, "opts");
2140
+ const upper = assertArray(lead, "lead");
2141
+ const lower = assertArray(counter, "counter");
2142
+ if (upper.length !== lower.length) {
1742
2143
  throw new InvalidInputError(
1743
- `voiceIndependence needs the two lines aligned slot for slot; received ${lead.length} and ${counter.length}`
2144
+ `voiceIndependence needs the two lines aligned slot for slot; received ${upper.length} and ${lower.length}`
1744
2145
  );
1745
2146
  }
1746
2147
  for (const [name, flags] of [
1747
2148
  ["leadAttacks", opts?.leadAttacks],
1748
2149
  ["counterAttacks", opts?.counterAttacks]
1749
2150
  ]) {
1750
- if (flags !== void 0 && flags.length !== lead.length) {
2151
+ if (flags !== void 0 && flags.length !== upper.length) {
1751
2152
  throw new InvalidInputError(
1752
- `voiceIndependence needs ${name} aligned slot for slot with the lines; received ${flags.length} and ${lead.length}`
2153
+ `voiceIndependence needs ${name} aligned slot for slot with the lines; received ${flags.length} and ${upper.length}`
1753
2154
  );
1754
2155
  }
1755
2156
  }
1756
- const slots = lead.map((leadNote, index) => {
1757
- const counterNote = counter[index] ?? null;
2157
+ const slots = upper.map((leadNote, index) => {
2158
+ const counterNote = lower[index] ?? null;
1758
2159
  return {
1759
2160
  lead: leadNote,
1760
2161
  counter: counterNote,
@@ -1840,6 +2241,7 @@ function voiceIndependence(lead, counter, opts) {
1840
2241
  }
1841
2242
 
1842
2243
  // src/theory/counterpoint/index.ts
2244
+ var HIDDEN_PARALLEL_READINGS = Object.freeze(["fourPart", "twoVoice"]);
1843
2245
  function createsVoiceCrossing(upper, lower) {
1844
2246
  return pitchOf(upper) < pitchOf(lower);
1845
2247
  }
@@ -1917,7 +2319,8 @@ function createsParallelUnison(aPrev, aCur, bPrev, bCur) {
1917
2319
  }
1918
2320
  return a1 === b1 && a0 === b0;
1919
2321
  }
1920
- function createsHiddenParallelPerfect(aPrev, aCur, bPrev, bCur) {
2322
+ function createsHiddenParallelPerfect(aPrev, aCur, bPrev, bCur, strictness = "fourPart") {
2323
+ assertOneOf(strictness, HIDDEN_PARALLEL_READINGS, "strictness");
1921
2324
  const a0 = pitchOf(aPrev);
1922
2325
  const a1 = pitchOf(aCur);
1923
2326
  const b0 = pitchOf(bPrev);
@@ -1933,7 +2336,7 @@ function createsHiddenParallelPerfect(aPrev, aCur, bPrev, bCur) {
1933
2336
  return false;
1934
2337
  }
1935
2338
  const upperMove = a1 >= b1 ? aMove : bMove;
1936
- if (Math.abs(upperMove) <= 2) {
2339
+ if (strictness === "fourPart" && Math.abs(upperMove) <= 2) {
1937
2340
  return false;
1938
2341
  }
1939
2342
  return true;
@@ -1948,7 +2351,12 @@ function createsBattuta(aPrev, aCur, bPrev, bCur) {
1948
2351
  if (!bothVoicesMove(aMove, bMove) || similarMotion(aMove, bMove)) {
1949
2352
  return false;
1950
2353
  }
1951
- if (simpleClass(a1 - b1) !== 0) {
2354
+ const nowClass = simpleClass(a1 - b1);
2355
+ if (nowClass !== 0) {
2356
+ return false;
2357
+ }
2358
+ const prevClass = simpleClass(a0 - b0);
2359
+ if (prevClass === nowClass) {
1952
2360
  return false;
1953
2361
  }
1954
2362
  const upperMove = a1 >= b1 ? aMove : bMove;
@@ -1962,12 +2370,13 @@ function createsVoiceOverlap(upperPrev, upperCur, lowerPrev, lowerCur) {
1962
2370
  return u1 < l0 || l1 > u0;
1963
2371
  }
1964
2372
  function exceedsSpacing(upper, lower, maxSemitones = 12) {
2373
+ assertFiniteNumber(maxSemitones, "maxSemitones");
1965
2374
  return Math.abs(pitchOf(upper) - pitchOf(lower)) > maxSemitones;
1966
2375
  }
1967
2376
  function isLeadingToneResolution(prev, cur, key) {
1968
2377
  const prevPitch = pitchOf(prev);
1969
2378
  const curPitch = pitchOf(cur);
1970
- const tonic = pitchClassOf(key.rootPc);
2379
+ const tonic = pitchClassOf(toKeyScale(key).rootPc);
1971
2380
  const leading = (tonic + 11) % 12;
1972
2381
  if (pitchClassOf(prevPitch) !== leading || pitchClassOf(curPitch) !== tonic) {
1973
2382
  return false;
@@ -2009,7 +2418,18 @@ var PROFILE_WEIGHTS = Object.freeze({
2009
2418
  var SAFETY_PROFILES = Object.keys(PROFILE_WEIGHTS);
2010
2419
  function profileWeights(profile, overrides) {
2011
2420
  const base = PROFILE_WEIGHTS[assertOneOf(profile, SAFETY_PROFILES, "safety profile")];
2012
- return { ...base, ...overrides };
2421
+ if (overrides === void 0) {
2422
+ return { ...base };
2423
+ }
2424
+ assertRecord(overrides, "overrides");
2425
+ const weights = { ...base };
2426
+ for (const [field, value] of Object.entries(overrides)) {
2427
+ if (value === void 0) {
2428
+ continue;
2429
+ }
2430
+ weights[field] = assertFiniteNumber(value, `weight ${field}`);
2431
+ }
2432
+ return weights;
2013
2433
  }
2014
2434
  var NoteSafety = /* @__PURE__ */ ((NoteSafety2) => {
2015
2435
  NoteSafety2[NoteSafety2["Safe"] = 0] = "Safe";
@@ -2017,6 +2437,7 @@ var NoteSafety = /* @__PURE__ */ ((NoteSafety2) => {
2017
2437
  NoteSafety2[NoteSafety2["Dissonant"] = 2] = "Dissonant";
2018
2438
  return NoteSafety2;
2019
2439
  })(NoteSafety || {});
2440
+ Object.freeze(NoteSafety);
2020
2441
  var ReasonFlag = /* @__PURE__ */ ((ReasonFlag2) => {
2021
2442
  ReasonFlag2[ReasonFlag2["ChordTone"] = 1] = "ChordTone";
2022
2443
  ReasonFlag2[ReasonFlag2["Tension"] = 2] = "Tension";
@@ -2037,6 +2458,7 @@ var ReasonFlag = /* @__PURE__ */ ((ReasonFlag2) => {
2037
2458
  ReasonFlag2[ReasonFlag2["MelodicTritone"] = 65536] = "MelodicTritone";
2038
2459
  return ReasonFlag2;
2039
2460
  })(ReasonFlag || {});
2461
+ Object.freeze(ReasonFlag);
2040
2462
  var SUGGESTION_WINDOW = 12;
2041
2463
  var MAX_SUGGESTIONS = 3;
2042
2464
  var REASON_SEVERITY = [
@@ -2092,9 +2514,11 @@ function stepResolution(pitch, chord) {
2092
2514
  function evaluateSafety(q, opts = {}) {
2093
2515
  assertSafetyContext(q);
2094
2516
  assertMidiPitch(q.candidatePitch, "candidatePitch");
2095
- return evaluateInternal(q, opts.suggestions ?? true);
2517
+ const options = assertOptions(opts, "opts");
2518
+ return evaluateInternal(q, options.suggestions ?? true, options.partners ?? false);
2096
2519
  }
2097
2520
  function assertSafetyContext(q) {
2521
+ assertRecord(q, "safety query");
2098
2522
  assertOneOf(q.profile, SAFETY_PROFILES, "safety profile");
2099
2523
  if (q.prevPitch !== void 0) assertMidiPitch(q.prevPitch, "prevPitch");
2100
2524
  if (q.vocalLow !== void 0) assertMidiPitch(q.vocalLow, "vocalLow");
@@ -2115,7 +2539,7 @@ function assertSafetyContext(q) {
2115
2539
  }
2116
2540
  }
2117
2541
  }
2118
- function evaluateInternal(q, collectSuggestions) {
2542
+ function evaluateInternal(q, collectSuggestions, collectPartners) {
2119
2543
  let reasons = 0;
2120
2544
  let level = 0 /* Safe */;
2121
2545
  let resolveTo;
@@ -2186,41 +2610,58 @@ function evaluateInternal(q, collectSuggestions) {
2186
2610
  reasons |= 512 /* MajorSeventh */;
2187
2611
  }
2188
2612
  }
2613
+ let bass = pitch;
2614
+ let top = pitch;
2615
+ for (const ov of q.otherVoices) {
2616
+ bass = Math.min(bass, ov.pitch);
2617
+ top = Math.max(top, ov.pitch);
2618
+ }
2619
+ const pairReasons = collectPartners ? new Array(q.otherVoices.length).fill(0) : void 0;
2620
+ const flagPair = (index, flag) => {
2621
+ reasons |= flag;
2622
+ if (pairReasons !== void 0) {
2623
+ pairReasons[index] = (pairReasons[index] ?? 0) | flag;
2624
+ }
2625
+ };
2189
2626
  if (q.strongBeat) {
2190
- const twoVoice = q.otherVoices.length === 1;
2191
2627
  const prev = q.prevPitch;
2192
- for (const ov of q.otherVoices) {
2193
- if (!createsVerticalDissonance(pitch, ov.pitch, twoVoice)) {
2628
+ for (const [index, ov] of q.otherVoices.entries()) {
2629
+ const againstBass = pitch === bass || ov.pitch === bass;
2630
+ if (!createsVerticalDissonance(pitch, ov.pitch, againstBass)) {
2194
2631
  continue;
2195
2632
  }
2196
2633
  if (chord && isStructuralChordDissonance(pitch, ov.pitch, chord)) {
2197
2634
  continue;
2198
2635
  }
2199
- reasons |= 1024 /* VerticalDissonance */;
2636
+ if (chord && q.profile === "pop" && pitchClassOf(Math.abs(pitch - ov.pitch)) === 5 && isChordMember(pitch, chord) && isChordMember(ov.pitch, chord)) {
2637
+ continue;
2638
+ }
2639
+ flagPair(index, 1024 /* VerticalDissonance */);
2200
2640
  raise(2 /* Dissonant */);
2201
- if (prev !== void 0 && pitch === prev && ov.prevPitch !== void 0 && !createsVerticalDissonance(prev, ov.prevPitch, twoVoice)) {
2202
- reasons |= 16384 /* Suspension */;
2641
+ if (prev !== void 0 && pitch === prev && ov.prevPitch !== void 0 && !createsVerticalDissonance(prev, ov.prevPitch, againstBass)) {
2642
+ flagPair(index, 16384 /* Suspension */);
2203
2643
  }
2204
2644
  }
2205
2645
  }
2206
2646
  if (q.prevPitch !== void 0) {
2207
2647
  const prev = q.prevPitch;
2208
- for (const ov of q.otherVoices) {
2648
+ for (const [index, ov] of q.otherVoices.entries()) {
2209
2649
  if (ov.prevPitch === void 0) {
2210
2650
  continue;
2211
2651
  }
2212
2652
  if (createsParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {
2213
- reasons |= 2048 /* ParallelPerfect */;
2653
+ flagPair(index, 2048 /* ParallelPerfect */);
2214
2654
  raise(parallelSeverity);
2215
2655
  }
2216
- if (createsHiddenParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {
2217
- reasons |= 4096 /* HiddenParallel */;
2656
+ const outerPair = pitch === top && ov.pitch === bass || pitch === bass && ov.pitch === top;
2657
+ if (outerPair && createsHiddenParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {
2658
+ flagPair(index, 4096 /* HiddenParallel */);
2218
2659
  raise(parallelSeverity);
2219
2660
  }
2220
2661
  const crossedNow = pitch - ov.pitch;
2221
2662
  const crossedPrev = prev - ov.prevPitch;
2222
2663
  if (crossedNow !== 0 && crossedPrev !== 0 && crossedNow > 0 !== crossedPrev > 0) {
2223
- reasons |= 8192 /* VoiceCrossing */;
2664
+ flagPair(index, 8192 /* VoiceCrossing */);
2224
2665
  raise(parallelSeverity);
2225
2666
  }
2226
2667
  }
@@ -2236,6 +2677,17 @@ function evaluateInternal(q, collectSuggestions) {
2236
2677
  result.suggestions = suggestions;
2237
2678
  }
2238
2679
  }
2680
+ if (pairReasons !== void 0) {
2681
+ const partners = [];
2682
+ pairReasons.forEach((pair, index) => {
2683
+ if (pair !== 0) {
2684
+ partners.push({ index, reasons: pair });
2685
+ }
2686
+ });
2687
+ if (partners.length > 0) {
2688
+ result.partners = partners;
2689
+ }
2690
+ }
2239
2691
  return result;
2240
2692
  }
2241
2693
  function findSafeNearby(q, candidate) {
@@ -2245,7 +2697,7 @@ function findSafeNearby(q, candidate) {
2245
2697
  if (p < 0 || p > 127) {
2246
2698
  continue;
2247
2699
  }
2248
- if (evaluateInternal({ ...q, candidatePitch: p }, false).safety === 0 /* Safe */) {
2700
+ if (evaluateInternal({ ...q, candidatePitch: p }, false, false).safety === 0 /* Safe */) {
2249
2701
  out.push(p);
2250
2702
  if (out.length >= MAX_SUGGESTIONS) {
2251
2703
  return out;
@@ -2269,7 +2721,7 @@ var REASON_TEXT = [
2269
2721
  [2048 /* ParallelPerfect */, "Parallel perfect interval with another voice"],
2270
2722
  [4096 /* HiddenParallel */, "Hidden perfect interval by similar motion"],
2271
2723
  [8192 /* VoiceCrossing */, "Crosses another voice"],
2272
- [128 /* LargeLeap */, "Leap wider than the profile allows"],
2724
+ [128 /* LargeLeap */, "Forbidden melodic leap \u2014 a seventh or wider than an octave"],
2273
2725
  [32768 /* NeedsResolution */, "Dissonance awaiting a stepwise resolution"],
2274
2726
  [2 /* Tension */, "Chord tension"],
2275
2727
  [8 /* ScaleTone */, "Scale tone, not in the chord"],
@@ -2314,7 +2766,7 @@ function enumerateSafePitches(q, pitchLow, pitchHigh) {
2314
2766
  const low = Math.max(pitchLow, q.vocalLow ?? pitchLow);
2315
2767
  const high = Math.min(pitchHigh, q.vocalHigh ?? pitchHigh);
2316
2768
  for (let pitch = high; pitch >= low; pitch -= 1) {
2317
- const result = evaluateInternal({ ...q, candidatePitch: pitch }, false);
2769
+ const result = evaluateInternal({ ...q, candidatePitch: pitch }, false, false);
2318
2770
  if (result.safety === 2 /* Dissonant */) {
2319
2771
  continue;
2320
2772
  }
@@ -2327,7 +2779,7 @@ function enumerateSafePitches(q, pitchLow, pitchHigh) {
2327
2779
  return [...chordTones, ...others];
2328
2780
  }
2329
2781
 
2330
- // src/theory/symbol/index.ts
2782
+ // src/theory/symbol/grammar.ts
2331
2783
  var ROOT_RE = /^[A-Ga-g](?:#{1,2}|b{1,2}|x)?/;
2332
2784
  var BASS_RE = /^[A-Ga-g](?:#{1,2}|b{1,2}|x)?$/;
2333
2785
  var DEFAULT_SYSTEM = "english";
@@ -2430,51 +2882,6 @@ var QUALITY_MAP = {
2430
2882
  h: "m7b5",
2431
2883
  h7: "m7b5"
2432
2884
  };
2433
- var CANONICAL_SUFFIX = {
2434
- maj: "",
2435
- min: "m",
2436
- dim: "dim",
2437
- aug: "aug",
2438
- maj7: "maj7",
2439
- min7: "m7",
2440
- dom7: "7",
2441
- dim7: "dim7",
2442
- m7b5: "m7b5",
2443
- minMaj7: "mMaj7",
2444
- minMaj9: "mMaj9",
2445
- minMaj11: "mMaj11",
2446
- minMaj13: "mMaj13",
2447
- aug7: "aug7",
2448
- augMaj7: "augMaj7",
2449
- majb5: "maj(b5)",
2450
- "6": "6",
2451
- min6: "m6",
2452
- "6/9": "6/9",
2453
- sus2: "sus2",
2454
- sus4: "sus4",
2455
- add9: "add9",
2456
- add11: "add11",
2457
- maj9: "maj9",
2458
- min9: "m9",
2459
- dom9: "9",
2460
- "7b9": "7b9",
2461
- "7#9": "7#9",
2462
- "7#11": "7#11",
2463
- "7b13": "7b13",
2464
- "11": "11",
2465
- "13": "13",
2466
- "5": "5",
2467
- "7sus4": "7sus4",
2468
- "7b5": "7b5",
2469
- "7alt": "7alt",
2470
- "13b9": "13b9",
2471
- maj13: "maj13",
2472
- "maj7#11": "maj7#11",
2473
- min11: "m11",
2474
- min13: "m13",
2475
- minAdd9: "madd9",
2476
- "min6/9": "m6/9"
2477
- };
2478
2885
  function knownQuality(suffix) {
2479
2886
  return Object.hasOwn(QUALITY_MAP, suffix) ? QUALITY_MAP[suffix] : void 0;
2480
2887
  }
@@ -2486,6 +2893,7 @@ var BASE_TOKENS = [
2486
2893
  ["dim", "dim"],
2487
2894
  ["aug", "aug"],
2488
2895
  ["maj", "major"],
2896
+ ["Maj", "major"],
2489
2897
  ["m", "min"],
2490
2898
  ["-", "min"],
2491
2899
  ["o", "dim"],
@@ -2495,6 +2903,10 @@ var BASE_TOKENS = [
2495
2903
  ["\u0394", "major"],
2496
2904
  ["^", "major"]
2497
2905
  ];
2906
+ var COMPOUND_TOKENS = [
2907
+ ["\xF8", { base: "dim", seventh: "min7" }],
2908
+ ["h", { base: "dim", seventh: "min7" }]
2909
+ ];
2498
2910
  var MAJOR_MARKERS = [
2499
2911
  ["maj", "word"],
2500
2912
  ["Maj", "word"],
@@ -2513,6 +2925,27 @@ var ADDED_DEGREES = {
2513
2925
  };
2514
2926
  var ALTERABLE = { "5": 5, "9": 9, "11": 11, "13": 13 };
2515
2927
  var OMITTABLE = { "1": 1, "3": 3, "5": 5 };
2928
+ function alterationKey(degree, alter) {
2929
+ return `${degree}:${alter}`;
2930
+ }
2931
+ function writeAlteration(draft, degree, alter) {
2932
+ if (draft.implied.delete(degree)) {
2933
+ draft.alterations.delete(alterationKey(degree, 0));
2934
+ }
2935
+ draft.alterations.set(alterationKey(degree, alter), { degree, alter });
2936
+ }
2937
+ function implyAlteration(draft, degree) {
2938
+ draft.implied.add(degree);
2939
+ draft.alterations.set(alterationKey(degree, 0), { degree, alter: 0 });
2940
+ }
2941
+ function altersDegree(draft, degree) {
2942
+ for (const alteration of draft.alterations.values()) {
2943
+ if (alteration.degree === degree) {
2944
+ return true;
2945
+ }
2946
+ }
2947
+ return false;
2948
+ }
2516
2949
  function matchToken(text, at, table) {
2517
2950
  let found;
2518
2951
  for (const [token, value] of table) {
@@ -2536,15 +2969,18 @@ function stackTo(draft, top) {
2536
2969
  draft.additions.add(6);
2537
2970
  return;
2538
2971
  }
2539
- draft.seventh = draft.major ? "maj7" : draft.base === "dim" ? "dim7" : "min7";
2972
+ draft.seventh = draft.major ? "maj7" : draft.seventhOverBase ?? (draft.base === "dim" ? "dim7" : "min7");
2540
2973
  if (top >= 9) {
2541
- draft.alterations.set(9, 0);
2974
+ implyAlteration(draft, 9);
2542
2975
  }
2543
2976
  if (top === 11) {
2544
- draft.alterations.set(11, 0);
2977
+ implyAlteration(draft, 11);
2978
+ if (!draft.major && draft.base === "maj") {
2979
+ draft.omissions.add(3);
2980
+ }
2545
2981
  }
2546
2982
  if (top === 13) {
2547
- draft.alterations.set(13, 0);
2983
+ implyAlteration(draft, 13);
2548
2984
  }
2549
2985
  }
2550
2986
  function applyFigure(draft, text, at) {
@@ -2560,7 +2996,7 @@ function applyFigure(draft, text, at) {
2560
2996
  if (text.startsWith("alt", at)) {
2561
2997
  draft.base = "aug";
2562
2998
  draft.seventh = "min7";
2563
- draft.alterations.set(9, 1);
2999
+ writeAlteration(draft, 9, 1);
2564
3000
  return at + 3;
2565
3001
  }
2566
3002
  if (text.startsWith("add", at)) {
@@ -2592,13 +3028,37 @@ function applyFigure(draft, text, at) {
2592
3028
  if (degree === void 0) {
2593
3029
  return void 0;
2594
3030
  }
2595
- draft.alterations.set(degree.value, accidental?.value ?? 0);
3031
+ if (degree.value === 5 && (accidental === void 0 || writesFifth(draft))) {
3032
+ return void 0;
3033
+ }
3034
+ writeAlteration(draft, degree.value, accidental?.value ?? 0);
2596
3035
  return degree.next;
2597
3036
  }
3037
+ function writesFifth(draft) {
3038
+ return draft.base === "dim" || draft.base === "aug" || altersDegree(draft, 5);
3039
+ }
2598
3040
  function matchNumberToken(text, at, table) {
2599
3041
  const entries = Object.entries(table).map(([token, value]) => [token, value]);
2600
3042
  return matchToken(text, at, entries);
2601
3043
  }
3044
+ function openings(core) {
3045
+ const found = [];
3046
+ for (const [token, value] of COMPOUND_TOKENS) {
3047
+ if (core.startsWith(token)) {
3048
+ found.push({ length: token.length, base: value.base, seventh: value.seventh });
3049
+ }
3050
+ }
3051
+ for (const [token, value] of BASE_TOKENS) {
3052
+ if (!core.startsWith(token)) {
3053
+ continue;
3054
+ }
3055
+ found.push(
3056
+ value === "major" ? { length: token.length, major: true, glyph: token === "\u0394" || token === "^" } : { length: token.length, base: value }
3057
+ );
3058
+ }
3059
+ found.sort((a, b) => b.length - a.length);
3060
+ return [...found, void 0];
3061
+ }
2602
3062
  function structuralSpec(text) {
2603
3063
  let core = "";
2604
3064
  const groups = [];
@@ -2626,24 +3086,37 @@ function structuralSpec(text) {
2626
3086
  if (inside) {
2627
3087
  return void 0;
2628
3088
  }
3089
+ for (const opening of openings(core)) {
3090
+ const spec = readCore(core, groups, opening);
3091
+ if (spec !== void 0) {
3092
+ return spec;
3093
+ }
3094
+ }
3095
+ return void 0;
3096
+ }
3097
+ function readCore(core, groups, opening) {
2629
3098
  const draft = {
2630
3099
  base: "maj",
2631
3100
  alterations: /* @__PURE__ */ new Map(),
2632
3101
  additions: /* @__PURE__ */ new Set(),
2633
3102
  omissions: /* @__PURE__ */ new Set(),
3103
+ implied: /* @__PURE__ */ new Set(),
2634
3104
  major: false
2635
3105
  };
2636
3106
  let at = 0;
2637
- const opening = matchToken(core, at, BASE_TOKENS);
2638
3107
  let markerGlyph = false;
2639
3108
  let markerAfterBase = false;
2640
3109
  if (opening !== void 0) {
2641
- at = opening.next;
2642
- if (opening.value === "major") {
3110
+ at = opening.length;
3111
+ if (opening.major === true) {
2643
3112
  draft.major = true;
2644
- markerGlyph = core.startsWith("\u0394") || core.startsWith("^");
2645
- } else {
2646
- draft.base = opening.value;
3113
+ markerGlyph = opening.glyph === true;
3114
+ } else if (opening.base !== void 0) {
3115
+ draft.base = opening.base;
3116
+ if (opening.seventh !== void 0) {
3117
+ draft.seventh = opening.seventh;
3118
+ draft.seventhOverBase = opening.seventh;
3119
+ }
2647
3120
  }
2648
3121
  }
2649
3122
  if (!draft.major) {
@@ -2688,7 +3161,7 @@ function structuralSpec(text) {
2688
3161
  rootPc: 0,
2689
3162
  base: draft.base,
2690
3163
  ...draft.seventh === void 0 ? {} : { seventh: draft.seventh },
2691
- alterations: [...draft.alterations].map(([degree, alter]) => ({ degree, alter })),
3164
+ alterations: [...draft.alterations.values()],
2692
3165
  additions: [...draft.additions],
2693
3166
  omissions: [...draft.omissions]
2694
3167
  };
@@ -2715,7 +3188,8 @@ function rootSplits(text, system) {
2715
3188
  }
2716
3189
  assertNamingSystem(system);
2717
3190
  const splits = [];
2718
- for (let end = text.length; end > 0; end -= 1) {
3191
+ const longest = Math.min(text.length, maxNoteNameLength(system));
3192
+ for (let end = longest; end > 0; end -= 1) {
2719
3193
  const root = bareNote(text.slice(0, end), system);
2720
3194
  if (root !== void 0) {
2721
3195
  splits.push({ root, rest: text.slice(end) });
@@ -2753,82 +3227,53 @@ function chordAfterRoot(root, rest, system) {
2753
3227
  const spec = suffixSpec(rest);
2754
3228
  return spec === void 0 ? void 0 : makeSpelledChord(root, spec);
2755
3229
  }
2756
- function parseChordSymbol(text, opts) {
2757
- return unwrapParse(tryParseChordSymbol(text, opts));
2758
- }
2759
- function tryParseChordSymbol(text, opts) {
2760
- try {
2761
- if (typeof text !== "string") {
2762
- throw new InvalidInputError(`chord symbol must be a string; received ${typeof text}`);
2763
- }
2764
- const system = opts?.system ?? DEFAULT_SYSTEM;
2765
- const trimmed = text.trim().replaceAll("\u266F", "#").replaceAll("\u266D", "b").replaceAll("\u{1D12A}", "x").replaceAll("\u{1D12B}", "bb").replaceAll("\u2206", "\u0394");
2766
- const splits = rootSplits(trimmed, system);
2767
- if (splits.length === 0) {
2768
- throw new InvalidInputError(`Invalid chord symbol: ${text}`);
2769
- }
2770
- for (const { root, rest } of splits) {
2771
- const chord = chordAfterRoot(root, rest, system);
2772
- if (chord !== void 0) {
2773
- return { ok: true, value: chord };
2774
- }
2775
- }
2776
- throw new InvalidInputError(`Unrecognized chord quality: ${text}`);
2777
- } catch (error) {
2778
- return parseFailure(error);
2779
- }
2780
- }
2781
- function normalizedChord(data) {
2782
- if (!Array.isArray(data.intervals)) {
2783
- throw new InvalidInputError("chord.intervals must be an array of semitone offsets");
2784
- }
2785
- const copy = {
2786
- rootPc: pitchClassOf(assertFiniteNumber(data.rootPc, "chord.rootPc")),
2787
- quality: data.quality,
2788
- // Each offset is checked rather than copied blind: a chord holding a NaN
2789
- // interval voices, spells and formats as a chord that looks real, and the
2790
- // failure surfaces wherever the number is finally used.
2791
- intervals: data.intervals.map(
2792
- (interval, index) => assertFiniteNumber(interval, `chord.intervals[${index}]`)
2793
- )
2794
- };
2795
- if (data.bassPc !== void 0) {
2796
- copy.bassPc = pitchClassOf(assertFiniteNumber(data.bassPc, "chord.bassPc"));
2797
- }
2798
- if (data.rootSpelling !== void 0) {
2799
- copy.rootSpelling = data.rootSpelling;
2800
- }
2801
- if (data.bassSpelling !== void 0) {
2802
- copy.bassSpelling = data.bassSpelling;
2803
- }
2804
- if (data.toneSpellings !== void 0) {
2805
- copy.toneSpellings = data.toneSpellings;
2806
- }
2807
- return copy;
2808
- }
2809
- function toChordData(value) {
2810
- if (typeof value === "string") {
2811
- return parseChordSymbol(value);
2812
- }
2813
- if (typeof value === "object" && value !== null) {
2814
- const data = "toJSON" in value && typeof value.toJSON === "function" ? value.toJSON() : value;
2815
- return normalizedChord(data);
2816
- }
2817
- throw new InvalidInputError(
2818
- `chord must be a chord symbol or chord data; received ${typeof value}`
2819
- );
2820
- }
2821
- function symbolName(note, system) {
2822
- const name = formatNote(note, { system });
2823
- return system === "german" ? `${name.slice(0, 1).toUpperCase()}${name.slice(1)}` : name;
2824
- }
2825
- function pitchClassName(pc, hint, system, flats, inheritFlats) {
2826
- if (flats === void 0 && hint !== void 0 && noteToPitchClass(hint) === pc) {
2827
- return symbolName(hint, system);
2828
- }
2829
- const note = midiToNote(60 + pc, flats ?? inheritFlats ? "flat" : "sharp");
2830
- return symbolName({ letter: note.letter, alter: note.alter }, system);
2831
- }
3230
+
3231
+ // src/theory/symbol/suffix.ts
3232
+ var CANONICAL_SUFFIX = {
3233
+ maj: "",
3234
+ min: "m",
3235
+ dim: "dim",
3236
+ aug: "aug",
3237
+ maj7: "maj7",
3238
+ min7: "m7",
3239
+ dom7: "7",
3240
+ dim7: "dim7",
3241
+ m7b5: "m7b5",
3242
+ minMaj7: "mMaj7",
3243
+ minMaj9: "mMaj9",
3244
+ minMaj11: "mMaj11",
3245
+ minMaj13: "mMaj13",
3246
+ aug7: "aug7",
3247
+ augMaj7: "augMaj7",
3248
+ majb5: "maj(b5)",
3249
+ "6": "6",
3250
+ min6: "m6",
3251
+ "6/9": "6/9",
3252
+ sus2: "sus2",
3253
+ sus4: "sus4",
3254
+ add9: "add9",
3255
+ add11: "add11",
3256
+ maj9: "maj9",
3257
+ min9: "m9",
3258
+ dom9: "9",
3259
+ "7b9": "7b9",
3260
+ "7#9": "7#9",
3261
+ "7#11": "7#11",
3262
+ "7b13": "7b13",
3263
+ "11": "11",
3264
+ "13": "13",
3265
+ "5": "5",
3266
+ "7sus4": "7sus4",
3267
+ "7b5": "7b5",
3268
+ "7alt": "7alt",
3269
+ "13b9": "13b9",
3270
+ maj13: "maj13",
3271
+ "maj7#11": "maj7#11",
3272
+ min11: "m11",
3273
+ min13: "m13",
3274
+ minAdd9: "madd9",
3275
+ "min6/9": "m6/9"
3276
+ };
2832
3277
  var CORE_CANDIDATES = chordQualities().map((quality) => [quality, chordSpecForQuality(quality, 0)]).filter(([, spec]) => spec.alterations.every(({ degree, alter }) => degree === 5 || alter === 0));
2833
3278
  function carriesAlteration(spec, part) {
2834
3279
  return spec.alterations.some(
@@ -2900,42 +3345,338 @@ function specSuffix(spec) {
2900
3345
  const figures = figureText(spec, coreSpec);
2901
3346
  return figures.length === 0 ? text : `${text}(${figures.join(",")})`;
2902
3347
  }
3348
+
3349
+ // src/theory/symbol/spelling.ts
3350
+ function bareSpelling(note) {
3351
+ return { letter: note.letter, alter: note.alter };
3352
+ }
3353
+ function plainSpelling(pc, asFlat) {
3354
+ return bareSpelling(midiToNote(60 + pc, asFlat ? "flat" : "sharp"));
3355
+ }
3356
+ function symbolName(note, system) {
3357
+ const name = formatNote(note, { system });
3358
+ return system === "german" ? `${name.slice(0, 1).toUpperCase()}${name.slice(1)}` : name;
3359
+ }
3360
+ function isWrittenSpelling(spelling) {
3361
+ const written = midiToNote(
3362
+ 60 + noteToPitchClass(spelling),
3363
+ spelling.alter < 0 ? "flat" : "sharp"
3364
+ );
3365
+ return written.letter === spelling.letter && written.alter === spelling.alter;
3366
+ }
3367
+ function pitchClassName(pc, hint, system, flats, inheritFlats) {
3368
+ if (flats === void 0 && hint !== void 0 && noteToPitchClass(hint) === pc) {
3369
+ return symbolName(hint, system);
3370
+ }
3371
+ return symbolName(plainSpelling(pc, flats ?? inheritFlats ?? false), system);
3372
+ }
3373
+ function onAccidentalSide(spelling, flats) {
3374
+ return flats === void 0 || spelling.alter === 0 || spelling.alter < 0 === flats;
3375
+ }
3376
+ function bySameStep(spelling, from, to) {
3377
+ return bareSpelling(transposeByInterval(spelling, spelledInterval(from, to)));
3378
+ }
3379
+ function spellingAbove(root, letters, pc) {
3380
+ const letter = ((root.letter + letters) % 7 + 7) % 7;
3381
+ const natural = noteToPitchClass({ letter, alter: 0 });
3382
+ return { letter, alter: ((pc - natural) % 12 + 18) % 12 - 6 };
3383
+ }
3384
+ function carriedBass(spelling, from, to, asFlat, isOwnSpelling) {
3385
+ const carried = bySameStep(spelling, from, to);
3386
+ return isOwnSpelling(carried) || isWrittenSpelling(carried) ? carried : plainSpelling(noteToPitchClass(carried), asFlat);
3387
+ }
3388
+ function hintFor(hint, pc) {
3389
+ return hint !== void 0 && noteToPitchClass(hint) === pc ? hint : void 0;
3390
+ }
3391
+ function rootSpellingFor(pc, hint, flats) {
3392
+ return hint !== void 0 && onAccidentalSide(hint, flats) ? hint : plainSpelling(pc, flats ?? false);
3393
+ }
3394
+
3395
+ // src/theory/symbol/index.ts
3396
+ function parseChordSymbol(text, opts) {
3397
+ return unwrapParse(tryParseChordSymbol(text, opts));
3398
+ }
3399
+ function tryParseChordSymbol(text, opts) {
3400
+ const asked = assertOptions(opts, "opts");
3401
+ try {
3402
+ if (typeof text !== "string") {
3403
+ throw new InvalidInputError(`chord symbol must be a string; received ${typeof text}`);
3404
+ }
3405
+ const system = asked.system ?? DEFAULT_SYSTEM;
3406
+ const trimmed = text.trim().replaceAll("\u266F", "#").replaceAll("\u266D", "b").replaceAll("\u{1D12A}", "x").replaceAll("\u{1D12B}", "bb").replaceAll("\u2206", "\u0394");
3407
+ const splits = rootSplits(trimmed, system);
3408
+ if (splits.length === 0) {
3409
+ throw new InvalidInputError(`Invalid chord symbol: ${describeRejected(text)}`);
3410
+ }
3411
+ for (const { root, rest } of splits) {
3412
+ const chord = chordAfterRoot(root, rest, system);
3413
+ if (chord !== void 0) {
3414
+ return { ok: true, value: chord };
3415
+ }
3416
+ }
3417
+ throw new InvalidInputError(`Unrecognized chord quality: ${describeRejected(text)}`);
3418
+ } catch (error) {
3419
+ return parseFailure(error);
3420
+ }
3421
+ }
2903
3422
  function formatChordSymbol(chord, opts) {
2904
3423
  const data = toChordData(chord);
2905
3424
  if (!Object.hasOwn(CANONICAL_SUFFIX, data.quality)) {
2906
3425
  throw new InvalidInputError(`Unknown chord quality: ${String(data.quality)}`);
2907
3426
  }
2908
- const system = opts?.system ?? DEFAULT_SYSTEM;
3427
+ const asked = assertOptions(opts, "opts");
3428
+ const system = asked.system ?? DEFAULT_SYSTEM;
3429
+ const flats = asked.flats;
2909
3430
  const rootPc = pitchClassOf(data.rootPc);
2910
- const rootHint = data.rootSpelling;
2911
- const rootName = pitchClassName(rootPc, rootHint, system, opts?.flats);
3431
+ const rootHint = hintFor(data.rootSpelling, rootPc);
3432
+ const rootSpelling = rootSpellingFor(rootPc, rootHint, flats);
2912
3433
  const suffix = specSuffix(chordSpecOf(data));
2913
- let symbol = `${rootName}${suffix}`;
3434
+ let symbol = `${symbolName(rootSpelling, system)}${suffix}`;
2914
3435
  if (data.bassPc !== void 0 && pitchClassOf(data.bassPc) !== rootPc) {
2915
- const inheritFlats = rootHint !== void 0 && noteToPitchClass(rootHint) === rootPc ? rootHint.alter < 0 : void 0;
2916
3436
  const bassPc = pitchClassOf(data.bassPc);
2917
- symbol += `/${pitchClassName(bassPc, data.bassSpelling, system, opts?.flats, inheritFlats)}`;
3437
+ const bassHint = hintFor(data.bassSpelling, bassPc);
3438
+ const tonePcs = new Set(chordPitchClasses(data, { includeBass: false }));
3439
+ const ownSpelling = tonePcs.has(bassPc) ? spellingAbove(
3440
+ rootSpelling,
3441
+ (degreeOfInterval(pitchClassOf(bassPc - rootPc), data) ?? 1) - 1,
3442
+ bassPc
3443
+ ) : void 0;
3444
+ const bassName = rootHint !== void 0 && bassHint !== void 0 ? symbolName(
3445
+ carriedBass(
3446
+ bassHint,
3447
+ rootHint,
3448
+ rootSpelling,
3449
+ flats ?? rootSpelling.alter < 0,
3450
+ (spelling) => ownSpelling !== void 0 && spelling.letter === ownSpelling.letter && spelling.alter === ownSpelling.alter
3451
+ ),
3452
+ system
3453
+ ) : pitchClassName(bassPc, bassHint, system, flats, rootSpelling.alter < 0);
3454
+ symbol += `/${bassName}`;
2918
3455
  }
2919
3456
  return symbol;
2920
3457
  }
3458
+ function transposeChordSpellings(spellings, semitones, flats) {
3459
+ const byLetter = (spelling) => spelling === void 0 ? void 0 : bareSpelling(transposeNote(spelling, semitones));
3460
+ const movedRoot = byLetter(spellings.rootSpelling);
3461
+ const movedBass = byLetter(spellings.bassSpelling);
3462
+ const side = [movedRoot, spellings.rootSpelling, movedBass, spellings.bassSpelling].find(
3463
+ (spelling) => spelling !== void 0 && spelling.alter !== 0
3464
+ );
3465
+ const preferFlats = flats ?? (side !== void 0 && side.alter < 0);
3466
+ const octaveOnly = pitchClassOf(semitones) === 0;
3467
+ const plainName = (spelling, asFlat) => plainSpelling(noteToPitchClass(spelling), asFlat);
3468
+ const writable = (spelling) => octaveOnly || isWrittenSpelling(spelling) ? spelling : plainName(spelling, preferFlats);
3469
+ const root = spellings.rootSpelling;
3470
+ const bass = spellings.bassSpelling;
3471
+ const carriesBass = (candidate) => {
3472
+ if (root === void 0 || bass === void 0 || octaveOnly || !isWrittenSpelling(bass)) {
3473
+ return true;
3474
+ }
3475
+ const carried = bySameStep(bass, root, candidate);
3476
+ return isWrittenSpelling(carried) && onAccidentalSide(carried, flats);
3477
+ };
3478
+ const moved = {};
3479
+ if (movedRoot !== void 0) {
3480
+ const asked = flats !== void 0 && !octaveOnly && !isWrittenSpelling(movedRoot);
3481
+ const candidates = [
3482
+ ...octaveOnly || isWrittenSpelling(movedRoot) ? [movedRoot] : [],
3483
+ plainName(movedRoot, preferFlats),
3484
+ ...asked ? [movedRoot] : [],
3485
+ plainName(movedRoot, !preferFlats)
3486
+ ];
3487
+ moved.rootSpelling = candidates.find(carriesBass) ?? plainName(movedRoot, preferFlats);
3488
+ }
3489
+ const step = root !== void 0 && moved.rootSpelling !== void 0 ? spelledInterval(root, moved.rootSpelling) : void 0;
3490
+ const byStep = (spelling) => bareSpelling(
3491
+ step === void 0 ? transposeNote(spelling, semitones) : transposeByInterval(spelling, step)
3492
+ );
3493
+ if (bass !== void 0) {
3494
+ moved.bassSpelling = writable(byStep(bass));
3495
+ }
3496
+ if (spellings.toneSpellings !== void 0) {
3497
+ moved.toneSpellings = spellings.toneSpellings.map(byStep);
3498
+ }
3499
+ return moved;
3500
+ }
2921
3501
  function transposeChordSymbol(text, semitones, opts) {
2922
3502
  assertFiniteSemitones(semitones);
2923
3503
  const chord = parseChordSymbol(text, opts);
2924
3504
  const transposed = { ...chord, rootPc: pitchClassOf(chord.rootPc + semitones) };
2925
- if (chord.rootSpelling !== void 0) {
2926
- const hint = transposeNote(chord.rootSpelling, semitones);
2927
- transposed.rootSpelling = { letter: hint.letter, alter: hint.alter };
2928
- }
2929
3505
  if (chord.bassPc !== void 0) {
2930
3506
  transposed.bassPc = pitchClassOf(chord.bassPc + semitones);
2931
3507
  }
2932
- if (chord.bassSpelling !== void 0) {
2933
- const hint = transposeNote(chord.bassSpelling, semitones);
2934
- transposed.bassSpelling = { letter: hint.letter, alter: hint.alter };
3508
+ const spellings = transposeChordSpellings(chord, semitones, opts?.flats);
3509
+ if (spellings.rootSpelling !== void 0) {
3510
+ transposed.rootSpelling = spellings.rootSpelling;
3511
+ }
3512
+ if (spellings.bassSpelling !== void 0) {
3513
+ transposed.bassSpelling = spellings.bassSpelling;
3514
+ }
3515
+ if (spellings.toneSpellings !== void 0) {
3516
+ transposed.toneSpellings = spellings.toneSpellings;
2935
3517
  }
2936
3518
  return formatChordSymbol(transposed, opts);
2937
3519
  }
2938
3520
 
3521
+ // src/theory/symbol/coerce.ts
3522
+ function normalizedChord(data) {
3523
+ if (!Array.isArray(data.intervals)) {
3524
+ throw new InvalidInputError("chord.intervals must be an array of semitone offsets");
3525
+ }
3526
+ const copy = {
3527
+ rootPc: pitchClassOf(assertFiniteNumber(data.rootPc, "chord.rootPc")),
3528
+ quality: data.quality,
3529
+ // Each offset is checked rather than copied blind: a chord holding a NaN
3530
+ // interval voices, spells and formats as a chord that looks real, and the
3531
+ // failure surfaces wherever the number is finally used.
3532
+ intervals: data.intervals.map(
3533
+ (interval, index) => assertFiniteNumber(interval, `chord.intervals[${index}]`)
3534
+ )
3535
+ };
3536
+ if (data.bassPc !== void 0) {
3537
+ copy.bassPc = pitchClassOf(assertFiniteNumber(data.bassPc, "chord.bassPc"));
3538
+ }
3539
+ if (data.rootSpelling !== void 0) {
3540
+ copy.rootSpelling = data.rootSpelling;
3541
+ }
3542
+ if (data.bassSpelling !== void 0) {
3543
+ copy.bassSpelling = data.bassSpelling;
3544
+ }
3545
+ if (data.toneSpellings !== void 0) {
3546
+ copy.toneSpellings = data.toneSpellings;
3547
+ }
3548
+ return copy;
3549
+ }
3550
+ function toChordData(value) {
3551
+ if (typeof value === "string") {
3552
+ return parseChordSymbol(value);
3553
+ }
3554
+ if (typeof value === "object" && value !== null) {
3555
+ const data = "toJSON" in value && typeof value.toJSON === "function" ? value.toJSON() : value;
3556
+ return normalizedChord(data);
3557
+ }
3558
+ throw new InvalidInputError(
3559
+ `chord must be a chord symbol or chord data; received ${typeof value}`
3560
+ );
3561
+ }
3562
+
3563
+ // src/theory/tendency/index.ts
3564
+ function hasInterval(chord, semitones) {
3565
+ return chord.intervals.some((interval) => pitchClassOf(interval) === semitones);
3566
+ }
3567
+ function isDiatonicChord(chord, key) {
3568
+ return chordPitchClasses(chord).every((pc) => isScaleTone(pc, key));
3569
+ }
3570
+ function hasMajorThird(chord) {
3571
+ return hasInterval(chord, 4);
3572
+ }
3573
+ function soundsDominantSeventh(chord) {
3574
+ return hasMajorThird(chord) && hasInterval(chord, 10);
3575
+ }
3576
+ function soundsMajorTriad(chord) {
3577
+ return hasMajorThird(chord) && !hasInterval(chord, 10) && !hasInterval(chord, 11);
3578
+ }
3579
+ function hasDominantSonority(chord) {
3580
+ return soundsMajorTriad(chord) || soundsDominantSeventh(chord);
3581
+ }
3582
+ function isDominantChordOf(chord, key) {
3583
+ if (pitchClassOf(chord.rootPc - key.rootPc) !== 7) {
3584
+ return false;
3585
+ }
3586
+ if (hasMajorThird(chord)) {
3587
+ return true;
3588
+ }
3589
+ const suspended = !hasInterval(chord, 3) && (hasInterval(chord, 5) || hasInterval(chord, 2));
3590
+ return suspended && hasInterval(chord, 10);
3591
+ }
3592
+ function isNeapolitanChordOf(chord, key) {
3593
+ return pitchClassOf(chord.rootPc - key.rootPc) === 1 && soundsMajorTriad(chord) && !isDiatonicChord(chord, key);
3594
+ }
3595
+ var DOMINANT_DEGREE = 5;
3596
+ function heptatonicFrameOf(key) {
3597
+ return scaleTonesInDegreeOrder(key).length === 7 ? key : majorKey(pitchClassOf(key.rootPc));
3598
+ }
3599
+ function degreeTriad(tones, index) {
3600
+ const root = tones[index] ?? 0;
3601
+ return {
3602
+ third: pitchClassOf((tones[(index + 2) % tones.length] ?? 0) - root),
3603
+ fifth: pitchClassOf((tones[(index + 4) % tones.length] ?? 0) - root)
3604
+ };
3605
+ }
3606
+ function tonicizableDegrees(key) {
3607
+ const tones = scaleTonesInDegreeOrder(heptatonicFrameOf(key));
3608
+ const minor = isMinorMask(key.modeMask12);
3609
+ const out = [];
3610
+ for (let index = 0; index < tones.length; index += 1) {
3611
+ const rootPc = tones[index];
3612
+ if (rootPc === void 0) {
3613
+ continue;
3614
+ }
3615
+ const triad = degreeTriad(tones, index);
3616
+ if (triad.fifth !== 7) {
3617
+ continue;
3618
+ }
3619
+ const dominantDegree = minor && index + 1 === DOMINANT_DEGREE;
3620
+ out.push({ degreeNumber: index + 1, rootPc, lower: triad.third === 3 && !dominantDegree });
3621
+ }
3622
+ return out;
3623
+ }
3624
+ function appliedDominantTarget(rootPc, key) {
3625
+ const target = pitchClassOf(rootPc);
3626
+ return tonicizableDegrees(key).find(
3627
+ (degree) => degree.degreeNumber !== 1 && degree.rootPc === target
3628
+ ) ?? null;
3629
+ }
3630
+ function leadingTonePcOf(key) {
3631
+ return pitchClassOf(key.rootPc - 1);
3632
+ }
3633
+ var LEADING_TONE_QUALITIES = /* @__PURE__ */ new Set(["dim", "dim7", "m7b5"]);
3634
+ function soundsDiminishedTriad(chord) {
3635
+ return hasInterval(chord, 3) && hasInterval(chord, 6) && !hasInterval(chord, 7);
3636
+ }
3637
+ function isLeadingToneChordOf(chord, key) {
3638
+ return pitchClassOf(chord.rootPc) === leadingTonePcOf(key) && soundsDiminishedTriad(chord);
3639
+ }
3640
+ function isFunctioningLeadingTone(chord, key) {
3641
+ const role = chordToneRole(leadingTonePcOf(key), chord);
3642
+ if (role === "third") {
3643
+ return pitchClassOf(chord.rootPc) === pitchClassOf(key.rootPc + 7);
3644
+ }
3645
+ if (role === "root") {
3646
+ return soundsDiminishedTriad(chord);
3647
+ }
3648
+ return false;
3649
+ }
3650
+ function seventhPcOf(chord) {
3651
+ for (const interval of chord.intervals) {
3652
+ const pc = pitchClassOf(chord.rootPc + interval);
3653
+ if (chordToneRole(pc, chord) === "seventh") {
3654
+ return pc;
3655
+ }
3656
+ }
3657
+ return void 0;
3658
+ }
3659
+ function fifthPcOf(chord) {
3660
+ for (const interval of chord.intervals) {
3661
+ const pc = pitchClassOf(chord.rootPc + interval);
3662
+ if (chordToneRole(pc, chord) === "fifth") {
3663
+ return pc;
3664
+ }
3665
+ }
3666
+ return void 0;
3667
+ }
3668
+ function isDescendingStep(interval) {
3669
+ return interval.number === 2 && (interval.semitones === -1 || interval.semitones === -2);
3670
+ }
3671
+ var THIRD_SEMITONES = [3, 4];
3672
+ function isFrustratedLeadingTone(fromPitch, toPitch, nextChord) {
3673
+ if (!THIRD_SEMITONES.includes(fromPitch - toPitch)) {
3674
+ return false;
3675
+ }
3676
+ const fifthPc = fifthPcOf(nextChord);
3677
+ return fifthPc !== void 0 && pitchClassOf(toPitch) === fifthPc;
3678
+ }
3679
+
2939
3680
  export {
2940
3681
  maskFromOffsets,
2941
3682
  MAJOR_MASK,
@@ -2954,6 +3695,7 @@ export {
2954
3695
  OCTATONIC_HALF_WHOLE_MASK,
2955
3696
  OCTATONIC_WHOLE_HALF_MASK,
2956
3697
  CHROMATIC_MASK,
3698
+ assertModeMask,
2957
3699
  LYDIAN_DOMINANT_MASK,
2958
3700
  MIXOLYDIAN_B13_MASK,
2959
3701
  LOCRIAN_NATURAL2_MASK,
@@ -2972,9 +3714,11 @@ export {
2972
3714
  resolveScaleName,
2973
3715
  namedScaleMask,
2974
3716
  requireScaleMask,
2975
- majorKey,
2976
- minorKey,
2977
- scaleByName,
3717
+ isMinorMask,
3718
+ variantOfMask,
3719
+ assertKeyVariant,
3720
+ scaleNameOfMask,
3721
+ tryResolveKeyName,
2978
3722
  toKeyScale,
2979
3723
  isScaleTone,
2980
3724
  nearestScaleTone,
@@ -2998,17 +3742,25 @@ export {
2998
3742
  spelledKeyOf,
2999
3743
  relativeKeyOf,
3000
3744
  parallelKeyOf,
3745
+ isWrittenTonic,
3001
3746
  dominantKeyOf,
3002
3747
  subdominantKeyOf,
3003
3748
  enharmonicKeyOf,
3004
3749
  relatedKeysOf,
3005
3750
  keyRelationBetween,
3751
+ resolveKey,
3752
+ scaleOf,
3753
+ majorKey,
3754
+ minorKey,
3755
+ scaleByName,
3006
3756
  SCALE_SYSTEMS,
3007
3757
  scaleSystemOf,
3008
3758
  supportsFunctionalHarmony,
3009
3759
  chordSpecIntervals,
3010
3760
  chordSpecQuality,
3011
3761
  chordQualities,
3762
+ assertChordQuality,
3763
+ assertChord,
3012
3764
  chordFromDegree,
3013
3765
  makeChord,
3014
3766
  chordFromSpec,
@@ -3021,6 +3773,9 @@ export {
3021
3773
  intervalAboveRoot,
3022
3774
  isChordMember,
3023
3775
  chordToneRole,
3776
+ degreeOfInterval,
3777
+ thirdSlotOf,
3778
+ displacedThirds,
3024
3779
  diatonicTriad,
3025
3780
  diatonicSeventh,
3026
3781
  simpleIntervalNumber,
@@ -3044,10 +3799,28 @@ export {
3044
3799
  ReasonFlag,
3045
3800
  evaluateSafety,
3046
3801
  enumerateSafePitches,
3802
+ toChordData,
3047
3803
  parseChordSymbol,
3048
3804
  tryParseChordSymbol,
3049
- toChordData,
3050
3805
  formatChordSymbol,
3051
- transposeChordSymbol
3806
+ transposeChordSpellings,
3807
+ transposeChordSymbol,
3808
+ isDiatonicChord,
3809
+ hasMajorThird,
3810
+ soundsDominantSeventh,
3811
+ hasDominantSonority,
3812
+ isDominantChordOf,
3813
+ isNeapolitanChordOf,
3814
+ heptatonicFrameOf,
3815
+ tonicizableDegrees,
3816
+ appliedDominantTarget,
3817
+ leadingTonePcOf,
3818
+ LEADING_TONE_QUALITIES,
3819
+ isLeadingToneChordOf,
3820
+ isFunctioningLeadingTone,
3821
+ seventhPcOf,
3822
+ fifthPcOf,
3823
+ isDescendingStep,
3824
+ isFrustratedLeadingTone
3052
3825
  };
3053
- //# sourceMappingURL=chunk-INHVXP6Z.js.map
3826
+ //# sourceMappingURL=chunk-RFIBF2RR.js.map