@libraz/libcantus 1.3.0 → 1.4.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 (130) hide show
  1. package/NOTICE +63 -0
  2. package/README.md +3 -1
  3. package/README_ja.md +13 -11
  4. package/dist/analyze/index.cjs +7 -7
  5. package/dist/analyze/index.cjs.map +1 -1
  6. package/dist/analyze/index.d.cts +14 -12
  7. package/dist/analyze/index.d.ts +14 -12
  8. package/dist/analyze/index.js +6 -6
  9. package/dist/{chunk-NB25P3EH.js → chunk-2G5MCGYU.js} +33 -11
  10. package/dist/chunk-2G5MCGYU.js.map +1 -0
  11. package/dist/{chunk-NM7UAMC3.cjs → chunk-3FDBMIPR.cjs} +64 -64
  12. package/dist/chunk-3FDBMIPR.cjs.map +1 -0
  13. package/dist/{chunk-YFA7X2F6.cjs → chunk-3MDOEFIQ.cjs} +411 -327
  14. package/dist/chunk-3MDOEFIQ.cjs.map +1 -0
  15. package/dist/{chunk-IPEF6NWB.js → chunk-5MWRBYAT.js} +278 -154
  16. package/dist/chunk-5MWRBYAT.js.map +1 -0
  17. package/dist/{chunk-B2PW6EHN.cjs → chunk-6TTBEON3.cjs} +1253 -889
  18. package/dist/chunk-6TTBEON3.cjs.map +1 -0
  19. package/dist/{chunk-UMWZ4ZET.js → chunk-7NQ6AL3H.js} +4 -4
  20. package/dist/chunk-7NQ6AL3H.js.map +1 -0
  21. package/dist/{chunk-PWC4CRWT.cjs → chunk-E3QO3UET.cjs} +31 -23
  22. package/dist/chunk-E3QO3UET.cjs.map +1 -0
  23. package/dist/{chunk-GNVN4PPJ.js → chunk-FJFULJZE.js} +531 -213
  24. package/dist/chunk-FJFULJZE.js.map +1 -0
  25. package/dist/{chunk-CPMB7P75.js → chunk-FK3DGAIZ.js} +608 -244
  26. package/dist/chunk-FK3DGAIZ.js.map +1 -0
  27. package/dist/chunk-GEDTJHAC.cjs.map +1 -1
  28. package/dist/{chunk-RFIBF2RR.js → chunk-H7FVMBUC.js} +138 -65
  29. package/dist/chunk-H7FVMBUC.js.map +1 -0
  30. package/dist/{chunk-4R2LWV7E.cjs → chunk-HYSVSFNI.cjs} +664 -540
  31. package/dist/chunk-HYSVSFNI.cjs.map +1 -0
  32. package/dist/{chunk-35NBV2FT.cjs → chunk-KMYGRFZ6.cjs} +47 -24
  33. package/dist/chunk-KMYGRFZ6.cjs.map +1 -0
  34. package/dist/{chunk-IFZDWIKM.cjs → chunk-LQZQDBCD.cjs} +1048 -730
  35. package/dist/chunk-LQZQDBCD.cjs.map +1 -0
  36. package/dist/{chunk-PM4XJE2Y.cjs → chunk-P3CPXAJ5.cjs} +76 -54
  37. package/dist/chunk-P3CPXAJ5.cjs.map +1 -0
  38. package/dist/{chunk-S5BKZZGM.cjs → chunk-S3QELJUE.cjs} +72 -56
  39. package/dist/chunk-S3QELJUE.cjs.map +1 -0
  40. package/dist/{chunk-PUKJP2Z5.js → chunk-TXFWSU5D.js} +31 -8
  41. package/dist/chunk-TXFWSU5D.js.map +1 -0
  42. package/dist/{chunk-76ZBTRG2.cjs → chunk-U4EQTDGN.cjs} +258 -218
  43. package/dist/chunk-U4EQTDGN.cjs.map +1 -0
  44. package/dist/chunk-UGLFDG7L.cjs.map +1 -1
  45. package/dist/{chunk-MLBPXA4Z.js → chunk-UTE5A36R.js} +198 -114
  46. package/dist/chunk-UTE5A36R.js.map +1 -0
  47. package/dist/{chunk-ZBUROTJQ.js → chunk-UZKQISRF.js} +17 -9
  48. package/dist/chunk-UZKQISRF.js.map +1 -0
  49. package/dist/{chunk-7KG3RTYS.js → chunk-VL5NXVCN.js} +281 -155
  50. package/dist/chunk-VL5NXVCN.js.map +1 -0
  51. package/dist/{chunk-CGBADIO5.cjs → chunk-WOMCXN2W.cjs} +328 -202
  52. package/dist/chunk-WOMCXN2W.cjs.map +1 -0
  53. package/dist/{chunk-3R3QVKY5.cjs → chunk-YDGMWH43.cjs} +397 -324
  54. package/dist/chunk-YDGMWH43.cjs.map +1 -0
  55. package/dist/{chunk-AGOVW7U6.js → chunk-YYVXRV3V.js} +72 -56
  56. package/dist/chunk-YYVXRV3V.js.map +1 -0
  57. package/dist/{chunk-M5RYMAFI.js → chunk-ZLNSH4UL.js} +71 -31
  58. package/dist/chunk-ZLNSH4UL.js.map +1 -0
  59. package/dist/{coerce-DRERzdve.d.cts → coerce-CUhHc3Tu.d.ts} +16 -8
  60. package/dist/{coerce-DP0vYu-x.d.ts → coerce-Ytt2Eiq9.d.cts} +16 -8
  61. package/dist/core/index.cjs +5 -5
  62. package/dist/core/index.cjs.map +1 -1
  63. package/dist/core/index.d.cts +9 -8
  64. package/dist/core/index.d.ts +9 -8
  65. package/dist/core/index.js +4 -4
  66. package/dist/generate/index.cjs +8 -8
  67. package/dist/generate/index.cjs.map +1 -1
  68. package/dist/generate/index.d.cts +28 -18
  69. package/dist/generate/index.d.ts +28 -18
  70. package/dist/generate/index.js +7 -7
  71. package/dist/{index-BMweUlm5.d.cts → index-5VJgKz_Q.d.cts} +6 -2
  72. package/dist/{index-BHIUyjhZ.d.ts → index-BLuDesVG.d.ts} +20 -13
  73. package/dist/{index-B9QLziDe.d.cts → index-BV6APVa6.d.cts} +20 -13
  74. package/dist/{index-CL4b4gUQ.d.ts → index-Bf0Sv8a9.d.ts} +6 -2
  75. package/dist/{index-Dw2DP7vm.d.ts → index-BkT21EMv.d.cts} +3 -2
  76. package/dist/{index-BL5N2Oyz.d.cts → index-C7KCaTRz.d.ts} +3 -2
  77. package/dist/{index-Cy15oTvh.d.ts → index-C9MYbuKS.d.cts} +7 -3
  78. package/dist/{index-Cy15oTvh.d.cts → index-C9MYbuKS.d.ts} +7 -3
  79. package/dist/{index-CY-3NVfy.d.cts → index-DVInzCDS.d.cts} +53 -29
  80. package/dist/{index-9eoQs2C1.d.ts → index-Dg-SAfhz.d.ts} +6 -4
  81. package/dist/{index-zaZdmnU4.d.ts → index-jgeug9EA.d.ts} +53 -29
  82. package/dist/{index-cHvbYgJI.d.cts → index-pQihL4z7.d.cts} +6 -4
  83. package/dist/index.cjs +13 -13
  84. package/dist/index.cjs.map +1 -1
  85. package/dist/index.d.cts +11 -11
  86. package/dist/index.d.ts +11 -11
  87. package/dist/index.js +12 -12
  88. package/dist/{masks-Cc7-MzzY.d.ts → masks-BnzXnBj0.d.cts} +3 -2
  89. package/dist/{masks-pADfTOiD.d.cts → masks-tycmkrgs.d.ts} +3 -2
  90. package/dist/model/index.cjs +12 -12
  91. package/dist/model/index.cjs.map +1 -1
  92. package/dist/model/index.d.cts +63 -48
  93. package/dist/model/index.d.ts +63 -48
  94. package/dist/model/index.js +11 -11
  95. package/dist/{positional-DJgs6ltD.d.ts → positional-CscpxtlE.d.cts} +3 -2
  96. package/dist/{positional-DMZer-FM.d.cts → positional-DLnfUgqG.d.ts} +3 -2
  97. package/dist/{profile-DLdlIpS9.d.ts → profile-B4WqcM4H.d.ts} +25 -17
  98. package/dist/{profile-CQMEqdH9.d.cts → profile-BJd1XkPB.d.cts} +25 -17
  99. package/dist/theory/index.cjs +5 -5
  100. package/dist/theory/index.cjs.map +1 -1
  101. package/dist/theory/index.d.cts +18 -14
  102. package/dist/theory/index.d.ts +18 -14
  103. package/dist/theory/index.js +4 -4
  104. package/dist/{types-D-XgxGvp.d.cts → types-7mGFwvMm.d.ts} +45 -22
  105. package/dist/{types-Dh7C064C.d.ts → types-_ofpb2be.d.cts} +45 -22
  106. package/package.json +13 -13
  107. package/dist/chunk-35NBV2FT.cjs.map +0 -1
  108. package/dist/chunk-3R3QVKY5.cjs.map +0 -1
  109. package/dist/chunk-4R2LWV7E.cjs.map +0 -1
  110. package/dist/chunk-76ZBTRG2.cjs.map +0 -1
  111. package/dist/chunk-7KG3RTYS.js.map +0 -1
  112. package/dist/chunk-AGOVW7U6.js.map +0 -1
  113. package/dist/chunk-B2PW6EHN.cjs.map +0 -1
  114. package/dist/chunk-CGBADIO5.cjs.map +0 -1
  115. package/dist/chunk-CPMB7P75.js.map +0 -1
  116. package/dist/chunk-GNVN4PPJ.js.map +0 -1
  117. package/dist/chunk-IFZDWIKM.cjs.map +0 -1
  118. package/dist/chunk-IPEF6NWB.js.map +0 -1
  119. package/dist/chunk-M5RYMAFI.js.map +0 -1
  120. package/dist/chunk-MLBPXA4Z.js.map +0 -1
  121. package/dist/chunk-NB25P3EH.js.map +0 -1
  122. package/dist/chunk-NM7UAMC3.cjs.map +0 -1
  123. package/dist/chunk-PM4XJE2Y.cjs.map +0 -1
  124. package/dist/chunk-PUKJP2Z5.js.map +0 -1
  125. package/dist/chunk-PWC4CRWT.cjs.map +0 -1
  126. package/dist/chunk-RFIBF2RR.js.map +0 -1
  127. package/dist/chunk-S5BKZZGM.cjs.map +0 -1
  128. package/dist/chunk-UMWZ4ZET.js.map +0 -1
  129. package/dist/chunk-YFA7X2F6.cjs.map +0 -1
  130. package/dist/chunk-ZBUROTJQ.js.map +0 -1
@@ -1,6 +1,7 @@
1
1
  import {
2
- sortedNoteEvents
3
- } from "./chunk-PUKJP2Z5.js";
2
+ sortedNoteEvents,
3
+ soundingIndices
4
+ } from "./chunk-TXFWSU5D.js";
4
5
  import {
5
6
  DORIAN_MASK,
6
7
  HARMONIC_MINOR_MASK,
@@ -12,7 +13,8 @@ import {
12
13
  MIXOLYDIAN_MASK,
13
14
  NATURAL_MINOR_MASK,
14
15
  PHRYGIAN_MASK,
15
- appliedDominantTarget,
16
+ QUALITY_INTERVALS,
17
+ appliedChordTarget,
16
18
  chordFromSpan,
17
19
  chordPitchClasses,
18
20
  chordQualities,
@@ -20,7 +22,6 @@ import {
20
22
  chordToneSpellings,
21
23
  createsVerticalDissonance,
22
24
  diatonicTriad,
23
- hasDominantSonority,
24
25
  hasMajorThird,
25
26
  heptatonicFrameOf,
26
27
  intervalAboveRoot,
@@ -34,8 +35,10 @@ import {
34
35
  isScaleTone,
35
36
  keyRelationBetween,
36
37
  keySignatureFifths,
38
+ keySpelling,
37
39
  majorKey,
38
40
  makeChord,
41
+ makeSpelledChord,
39
42
  minorKey,
40
43
  resolveKey,
41
44
  scaleOf,
@@ -45,10 +48,11 @@ import {
45
48
  spellChord,
46
49
  spellPitchClass,
47
50
  spelledKeyOf,
51
+ spellingAbove,
48
52
  toChordData,
49
53
  toKeyScale,
50
54
  tonicizableDegrees
51
- } from "./chunk-RFIBF2RR.js";
55
+ } from "./chunk-H7FVMBUC.js";
52
56
  import {
53
57
  BEAT_EPS,
54
58
  InvalidInputError,
@@ -78,10 +82,12 @@ import {
78
82
  diatonicLetterOf,
79
83
  formatNote,
80
84
  isCompound,
85
+ isConsonantInterval,
81
86
  meterAt,
82
87
  metricGridUnit,
83
88
  metricWeight,
84
89
  midiToNote,
90
+ naturalPitchClassOf,
85
91
  parseInterval,
86
92
  pitchClassOf,
87
93
  pulseBeats,
@@ -90,7 +96,7 @@ import {
90
96
  toNoteData,
91
97
  transposeByInterval,
92
98
  transposeNote
93
- } from "./chunk-AGOVW7U6.js";
99
+ } from "./chunk-YYVXRV3V.js";
94
100
 
95
101
  // src/analyze/detect/modes.ts
96
102
  var MODAL_CANDIDATES = Object.freeze([
@@ -204,6 +210,30 @@ function windowWeights(notes, windowStart, windowEnd, meter) {
204
210
  return { weights, totalWeight, maxWeight, lowestPitch };
205
211
  }
206
212
 
213
+ // src/analyze/detect/internal.ts
214
+ var MINOR_VARIANTS = [
215
+ { variant: "natural", mask: NATURAL_MINOR_MASK, scaleName: "naturalMinor" },
216
+ { variant: "harmonic", mask: HARMONIC_MINOR_MASK, scaleName: "harmonicMinor" },
217
+ { variant: "melodic", mask: MELODIC_MINOR_MASK, scaleName: "melodicMinor" }
218
+ ];
219
+ var MAJOR_VARIANTS = [
220
+ { variant: "major", mask: MAJOR_MASK, scaleName: "major" }
221
+ ];
222
+ var SCALE_NAME_ORDER = [
223
+ "major",
224
+ "naturalMinor",
225
+ "harmonicMinor",
226
+ "melodicMinor",
227
+ "dorian",
228
+ "phrygian",
229
+ "lydian",
230
+ "mixolydian",
231
+ "locrian"
232
+ ];
233
+ var EXPLAINED_RIVALS = 3;
234
+ var SCORE_DIGITS = 2;
235
+ var MAX_SCORE_DIGITS = 6;
236
+
207
237
  // src/analyze/detect/profiles.ts
208
238
  var KRUMHANSL_KESSLER_PROFILE = {
209
239
  major: [6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88],
@@ -302,6 +332,11 @@ function profileScore(dist, vector, tonic) {
302
332
  }
303
333
 
304
334
  // src/analyze/detect/index.ts
335
+ var CHORD_INPUT_KINDS = [
336
+ "auto",
337
+ "midi",
338
+ "pitchClass"
339
+ ];
305
340
  function uniquePitchClasses(pitches) {
306
341
  return [...new Set(pitches.map(pitchClassOf))].sort((a, b) => a - b);
307
342
  }
@@ -315,12 +350,13 @@ function assertPitches(pitches, name, budget = void 0) {
315
350
  function detectChord(pitches, opts = {}) {
316
351
  const asked = assertOptions(opts, "opts");
317
352
  assertPitches(pitches, "chord detection pitches");
353
+ const askedKind = assertOneOf(asked.input ?? "auto", CHORD_INPUT_KINDS, "input");
318
354
  if (asked.bassPc !== void 0) assertFiniteNumber(asked.bassPc, "bassPc");
319
355
  const input = uniquePitchClasses(pitches);
320
356
  if (input.length === 0) {
321
357
  return [];
322
358
  }
323
- const inputKind = asked.input === void 0 || asked.input === "auto" ? pitches.every((pitch) => Number.isInteger(pitch) && pitch >= 0 && pitch <= 11) ? "pitchClass" : "midi" : asked.input;
359
+ const inputKind = askedKind === "auto" ? pitches.every((pitch) => Number.isInteger(pitch) && pitch >= 0 && pitch <= 11) ? "pitchClass" : "midi" : askedKind;
324
360
  const bassPc = asked.bassPc !== void 0 ? pitchClassOf(asked.bassPc) : inputKind === "midi" ? pitchClassOf(pitches.reduce((lowest, pitch) => Math.min(lowest, pitch), Infinity)) : void 0;
325
361
  const inputSet = new Set(input);
326
362
  const matches = [];
@@ -339,6 +375,9 @@ function detectChord(pitches, opts = {}) {
339
375
  if (missingPcs.length > 1 || missingPcs.length === 1 && missingPcs[0] !== fifthPc) {
340
376
  continue;
341
377
  }
378
+ if (tones.length - missingPcs.length < 2) {
379
+ continue;
380
+ }
342
381
  const extraPcs = input.filter((pc) => !toneSet.has(pc));
343
382
  const bassIndex = bassPc === void 0 ? -1 : chord.intervals.findIndex((iv) => pitchClassOf(rootPc + iv) === bassPc);
344
383
  const inversion = bassPc === void 0 || bassIndex < 0 ? null : bassIndex;
@@ -387,31 +426,9 @@ function detectChordBest(pitches, opts = {}) {
387
426
  }
388
427
  return makeChord(best.rootPc, best.quality, best.bassPc);
389
428
  }
390
- var MINOR_VARIANTS = [
391
- { variant: "natural", mask: NATURAL_MINOR_MASK, scaleName: "naturalMinor" },
392
- { variant: "harmonic", mask: HARMONIC_MINOR_MASK, scaleName: "harmonicMinor" },
393
- { variant: "melodic", mask: MELODIC_MINOR_MASK, scaleName: "melodicMinor" }
394
- ];
395
- var MAJOR_VARIANTS = [
396
- { variant: "major", mask: MAJOR_MASK, scaleName: "major" }
397
- ];
398
- var SCALE_NAME_ORDER = [
399
- "major",
400
- "naturalMinor",
401
- "harmonicMinor",
402
- "melodicMinor",
403
- "dorian",
404
- "phrygian",
405
- "lydian",
406
- "mixolydian",
407
- "locrian"
408
- ];
409
429
  function scaleNameRank(name) {
410
430
  return SCALE_NAME_ORDER.indexOf(name);
411
431
  }
412
- var EXPLAINED_RIVALS = 3;
413
- var SCORE_DIGITS = 2;
414
- var MAX_SCORE_DIGITS = 6;
415
432
  function quoteScores(rival, winner) {
416
433
  let digits = SCORE_DIGITS;
417
434
  while (digits < MAX_SCORE_DIGITS && rival.toFixed(digits) === winner.toFixed(digits)) {
@@ -629,13 +646,14 @@ var CHORD_SHAPE = {
629
646
  french: { rootAboveBass: 6, quality: "7b5", intervals: [0, 4, 6, 10] },
630
647
  german: { rootAboveBass: 0, quality: "dom7", intervals: [0, 4, 7, 10] }
631
648
  };
632
- var KIND_BY_SYMBOL = /* @__PURE__ */ new Map([
649
+ var KIND_SYMBOLS = [
633
650
  ["It6", "italian"],
634
651
  ["Fr6", "french"],
635
652
  ["Ger6", "german"],
636
653
  // The German sixth's figures over its bass are 6/5; both spellings are in use.
637
654
  ["Ger65", "german"]
638
- ]);
655
+ ];
656
+ var KIND_BY_SYMBOL = new Map(KIND_SYMBOLS);
639
657
  var SYMBOL_BY_KIND = {
640
658
  italian: "It6",
641
659
  french: "Fr6",
@@ -660,14 +678,16 @@ function augmentedSixthOn(kind, keyLike) {
660
678
  const shape = CHORD_SHAPE[checked];
661
679
  const { scale: key, tonic } = resolveKey(keyLike);
662
680
  const bassPc = bassPcOf(key);
663
- const chord = makeChord(pitchClassOf(bassPc + shape.rootAboveBass), shape.quality, bassPc);
664
- chord.intervals = [...shape.intervals];
665
681
  const bass = transposeByInterval(tonic, BASS_INTERVAL);
666
682
  const tones = spelledInChordOrder(checked, tonic);
667
683
  const [root = bass] = tones;
668
- chord.rootSpelling = bareSpelling(root);
669
- chord.bassSpelling = bareSpelling(bass);
670
- chord.toneSpellings = tones.map(bareSpelling);
684
+ const chord = makeSpelledChord(pitchClassOf(bassPc + shape.rootAboveBass), shape.quality, {
685
+ bassPc,
686
+ rootSpelling: bareSpelling(root),
687
+ bassSpelling: bareSpelling(bass),
688
+ toneSpellings: tones.map(bareSpelling)
689
+ });
690
+ chord.intervals = [...shape.intervals];
671
691
  return chord;
672
692
  }
673
693
  function kindOfPitchClasses(pcs, bass) {
@@ -1009,14 +1029,6 @@ function borrowedSource(chord, key) {
1009
1029
  function isTonicizableRoot(rootPc, key) {
1010
1030
  return tonicizableDegrees(key).some((degree) => degree.rootPc === pitchClassOf(rootPc));
1011
1031
  }
1012
- function appliedTarget(chord, key) {
1013
- const dominant = hasDominantSonority(chord);
1014
- const leadingTone = LEADING_TONE_QUALITIES.has(chord.quality);
1015
- if (!dominant && !leadingTone) {
1016
- return null;
1017
- }
1018
- return appliedDominantTarget(chord.rootPc + (dominant ? 5 : 1), key);
1019
- }
1020
1032
  function isAppliedDominant(chord, key) {
1021
1033
  if (isDiatonicChord(chord, key)) {
1022
1034
  return false;
@@ -1028,7 +1040,7 @@ function isAppliedDominant(chord, key) {
1028
1040
  return pointsAtTonicizableDegree(chord, key);
1029
1041
  }
1030
1042
  function pointsAtTonicizableDegree(chord, key) {
1031
- if (appliedTarget(chord, key) !== null) {
1043
+ if (appliedChordTarget(chord, key) !== null) {
1032
1044
  return true;
1033
1045
  }
1034
1046
  return soundsDominantSeventh(chord) && isTonicizableRoot(pitchClassOf(chord.rootPc - 1), key);
@@ -1071,7 +1083,8 @@ function seventhQuality(base, halfDim) {
1071
1083
  }
1072
1084
  return base === "maj" ? "dom7" : "min7";
1073
1085
  }
1074
- var INVERSION_FIGURES = /* @__PURE__ */ new Set(["6", "64", "65", "43", "42", "2", "7"]);
1086
+ var INVERSION_FIGURE_LIST = ["6", "64", "65", "43", "42", "2", "7"];
1087
+ var INVERSION_FIGURES = new Set(INVERSION_FIGURE_LIST);
1075
1088
  var ROMAN_STYLE = {
1076
1089
  maj: { lower: false, suffix: "" },
1077
1090
  min: { lower: true, suffix: "" },
@@ -1143,7 +1156,12 @@ function parseInversion(figures) {
1143
1156
  }
1144
1157
  return { inversion: 0, seventh: false };
1145
1158
  }
1146
- function parseSimpleRoman(text, key) {
1159
+ function numeralRootSpelling(tonic, degreeNumber, rootPc) {
1160
+ const spelling = spellingAbove(tonic, degreeNumber - 1, rootPc);
1161
+ return Math.abs(spelling.alter) <= 2 ? spelling : void 0;
1162
+ }
1163
+ function parseSimpleRoman(text, resolved) {
1164
+ const key = resolved.scale;
1147
1165
  const match = /^([b#]?)([iIvV]+)(.*)$/.exec(text.trim());
1148
1166
  if (!match) {
1149
1167
  throw new InvalidInputError(`Invalid Roman numeral: ${text}`);
@@ -1167,8 +1185,17 @@ function parseSimpleRoman(text, key) {
1167
1185
  );
1168
1186
  const rootForQuality = (quality2) => accidental === 0 && degreeNumber === 7 && loweredDegrees(frame).has(7) && LEADING_TONE_QUALITIES.has(quality2) ? pitchClassOf(diatonicRootPc + 1) : diatonicRootPc;
1169
1187
  const canonical = SUFFIX_QUALITY.get(`${isUpper ? "u" : "l"}:${suffix}`);
1188
+ const parsed = (quality2, inversion2) => {
1189
+ const rootPc = rootForQuality(quality2);
1190
+ return {
1191
+ rootPc,
1192
+ rootSpelling: numeralRootSpelling(resolved.tonic, degreeNumber, rootPc),
1193
+ quality: quality2,
1194
+ inversion: inversion2
1195
+ };
1196
+ };
1170
1197
  if (canonical !== void 0) {
1171
- return { rootPc: rootForQuality(canonical), quality: canonical, inversion: 0 };
1198
+ return parsed(canonical, 0);
1172
1199
  }
1173
1200
  const parsedSuffix = /^(o|°|dim|ø|\+|aug)?(maj7|M7)?(65|64|43|42|7|6|2)?$/.exec(suffix);
1174
1201
  if (!parsedSuffix) {
@@ -1207,20 +1234,23 @@ function parseSimpleRoman(text, key) {
1207
1234
  } else {
1208
1235
  quality = base;
1209
1236
  }
1210
- return { rootPc: rootForQuality(quality), quality, inversion };
1237
+ return parsed(quality, inversion);
1211
1238
  }
1212
1239
  var NEAPOLITAN_SIXTH = "N6";
1213
1240
  function chromaticSymbolChord(text, key) {
1214
1241
  if (text === NEAPOLITAN_SIXTH) {
1215
1242
  const rootPc = pitchClassOf(key.scale.rootPc + 1);
1216
- return makeChord(rootPc, "maj", pitchClassOf(rootPc + 4));
1243
+ return makeSpelledChord(rootPc, "maj", {
1244
+ bassPc: pitchClassOf(rootPc + 4),
1245
+ rootSpelling: numeralRootSpelling(key.tonic, 2, rootPc)
1246
+ });
1217
1247
  }
1218
1248
  return augmentedSixthFromSymbol(text, key);
1219
1249
  }
1220
- function tonicizedKey(targetRoot, key) {
1250
+ function tonicizedKey(targetRoot, key, written) {
1221
1251
  return resolveKey({
1222
1252
  scale: majorKey(targetRoot),
1223
- tonic: spellPitchClass(targetRoot, key.tonic, key.scale)
1253
+ tonic: written ?? spellPitchClass(targetRoot, key.tonic, key.scale)
1224
1254
  });
1225
1255
  }
1226
1256
  function augmentedSixthTarget(chord, key) {
@@ -1238,30 +1268,34 @@ function augmentedSixthTarget(chord, key) {
1238
1268
  return null;
1239
1269
  }
1240
1270
  function chordFromParsed(parsed) {
1241
- const chord = makeChord(parsed.rootPc, parsed.quality);
1242
- if (parsed.inversion > 0 && parsed.inversion < chord.intervals.length) {
1243
- chord.bassPc = pitchClassOf(parsed.rootPc + (chord.intervals[parsed.inversion] ?? 0));
1244
- }
1245
- return chord;
1271
+ const bassInterval = parsed.inversion > 0 ? QUALITY_INTERVALS[parsed.quality][parsed.inversion] : void 0;
1272
+ return makeSpelledChord(parsed.rootPc, parsed.quality, {
1273
+ bassPc: bassInterval === void 0 ? void 0 : pitchClassOf(parsed.rootPc + bassInterval),
1274
+ rootSpelling: parsed.rootSpelling
1275
+ });
1246
1276
  }
1247
1277
  function romanToChord(text, key) {
1248
1278
  const resolved = resolveKey(key);
1249
- const scale = resolved.scale;
1279
+ if (typeof text !== "string") {
1280
+ throw new InvalidInputError(
1281
+ `Roman numeral must be a string; received ${describeRejected(text)}`
1282
+ );
1283
+ }
1250
1284
  const trimmed = text.trim().replace(/(\d)\/(?=\d)/g, "$1");
1251
1285
  const slash = trimmed.indexOf("/");
1252
1286
  if (slash >= 0) {
1253
1287
  const applied = trimmed.slice(0, slash);
1254
1288
  const target = trimmed.slice(slash + 1);
1255
- const targetRoot = parseSimpleRoman(target, scale).rootPc;
1256
- const localKey = tonicizedKey(targetRoot, resolved);
1257
- return chromaticSymbolChord(applied, localKey) ?? chordFromParsed(parseSimpleRoman(applied, localKey.scale));
1289
+ const targetNumeral = parseSimpleRoman(target, resolved);
1290
+ const localKey = tonicizedKey(targetNumeral.rootPc, resolved, targetNumeral.rootSpelling);
1291
+ return chromaticSymbolChord(applied, localKey) ?? chordFromParsed(parseSimpleRoman(applied, localKey));
1258
1292
  }
1259
- return chromaticSymbolChord(trimmed, resolved) ?? chordFromParsed(parseSimpleRoman(trimmed, scale));
1293
+ return chromaticSymbolChord(trimmed, resolved) ?? chordFromParsed(parseSimpleRoman(trimmed, resolved));
1260
1294
  }
1261
1295
  function romanStyle(quality) {
1262
1296
  return ROMAN_STYLE[quality];
1263
1297
  }
1264
- function romanSpelling(rootPc, key) {
1298
+ function romanSpelling(rootPc, key, quality) {
1265
1299
  const tones = scaleTonesInDegreeOrder(romanReference(key));
1266
1300
  const diatonic = tones.indexOf(pitchClassOf(rootPc));
1267
1301
  if (diatonic >= 0) {
@@ -1273,7 +1307,7 @@ function romanSpelling(rootPc, key) {
1273
1307
  return { degreeNumber: i + 1, accidental: "#" };
1274
1308
  }
1275
1309
  }
1276
- for (let i = 0; i < tones.length; i += 1) {
1310
+ for (let i = 0; i < tones.length && !LEADING_TONE_QUALITIES.has(quality); i += 1) {
1277
1311
  const degreeNumber2 = i + 1;
1278
1312
  if (degreeNumber2 === 1 || degreeNumber2 === 5) {
1279
1313
  continue;
@@ -1319,8 +1353,9 @@ function baseMarker(quality) {
1319
1353
  }
1320
1354
  var TRIAD_FIGURES = { 1: "6", 2: "64" };
1321
1355
  var SEVENTH_FIGURES = { 1: "65", 2: "43", 3: "42" };
1322
- var TRIAD_FIGURE_QUALITIES = /* @__PURE__ */ new Set(["maj", "min", "dim", "aug"]);
1323
- var SEVENTH_FIGURE_QUALITIES = /* @__PURE__ */ new Set([
1356
+ var TRIAD_FIGURE_QUALITY_NAMES = ["maj", "min", "dim", "aug"];
1357
+ var TRIAD_FIGURE_QUALITIES = new Set(TRIAD_FIGURE_QUALITY_NAMES);
1358
+ var SEVENTH_FIGURE_QUALITY_NAMES = [
1324
1359
  "maj7",
1325
1360
  "min7",
1326
1361
  "dom7",
@@ -1329,7 +1364,8 @@ var SEVENTH_FIGURE_QUALITIES = /* @__PURE__ */ new Set([
1329
1364
  "minMaj7",
1330
1365
  "aug7",
1331
1366
  "augMaj7"
1332
- ]);
1367
+ ];
1368
+ var SEVENTH_FIGURE_QUALITIES = new Set(SEVENTH_FIGURE_QUALITY_NAMES);
1333
1369
  function numeralFor(degreeNumber, lower) {
1334
1370
  const glyph = ROMAN[degreeNumber - 1];
1335
1371
  if (glyph === void 0) {
@@ -1340,6 +1376,47 @@ function numeralFor(degreeNumber, lower) {
1340
1376
  function chordToRoman(chord, key, opts = {}) {
1341
1377
  return renderRoman(toChordData(chord), key, assertOptions(opts, "opts")).roman;
1342
1378
  }
1379
+ function readNumeral(chord, resolved, applied) {
1380
+ const key = resolved.scale;
1381
+ const written = writtenRoot(chord);
1382
+ if (applied) {
1383
+ const target = augmentedSixthTarget(chord, resolved) ?? (isAppliedDominant(chord, key) ? appliedChordTarget(chord, key) : null);
1384
+ if (target !== null && leadsByLetter(chord, written, target, resolved)) {
1385
+ return { kind: "applied", target };
1386
+ }
1387
+ }
1388
+ return {
1389
+ kind: "root",
1390
+ ...writtenSpelling(chord.rootPc, written, resolved) ?? romanSpelling(chord.rootPc, key, chord.quality)
1391
+ };
1392
+ }
1393
+ function writtenRoot(chord) {
1394
+ const hint = chord.rootSpelling;
1395
+ if (typeof hint !== "object" || hint === null || !Number.isInteger(hint.letter) || hint.letter < 0 || hint.letter > 6 || !Number.isInteger(hint.alter) || pitchClassOf(naturalPitchClassOf(hint.letter) + hint.alter) !== pitchClassOf(chord.rootPc)) {
1396
+ return void 0;
1397
+ }
1398
+ return hint;
1399
+ }
1400
+ function writtenSpelling(rootPc, written, resolved) {
1401
+ const tones = scaleTonesInDegreeOrder(romanReference(resolved.scale));
1402
+ if (written === void 0 || tones.includes(pitchClassOf(rootPc))) {
1403
+ return void 0;
1404
+ }
1405
+ const degreeNumber = diatonicLetterOf(written.letter - resolved.tonic.letter) + 1;
1406
+ const degreePc = tones[degreeNumber - 1];
1407
+ if (degreePc === void 0) {
1408
+ return void 0;
1409
+ }
1410
+ const offset = pitchClassOf(rootPc - degreePc + 6) - 6;
1411
+ return Math.abs(offset) > 1 ? void 0 : { degreeNumber, accidental: offset < 0 ? "b" : offset > 0 ? "#" : "" };
1412
+ }
1413
+ function leadsByLetter(chord, written, target, resolved) {
1414
+ if (written === void 0 || !LEADING_TONE_QUALITIES.has(chord.quality)) {
1415
+ return true;
1416
+ }
1417
+ const targetLetter = spellPitchClass(target.rootPc, resolved.tonic, resolved.scale).letter;
1418
+ return diatonicLetterOf(targetLetter - written.letter) === 1;
1419
+ }
1343
1420
  function renderRoman(chord, keyLike, opts) {
1344
1421
  const resolved = resolveKey(keyLike);
1345
1422
  const key = resolved.scale;
@@ -1347,18 +1424,16 @@ function renderRoman(chord, keyLike, opts) {
1347
1424
  if (augmented !== null) {
1348
1425
  return { roman: augmented, derivation: { kind: "augmentedSixth" } };
1349
1426
  }
1350
- if (opts.applied === true) {
1351
- const target = augmentedSixthTarget(chord, resolved) ?? (isDiatonicChord(chord, key) ? null : appliedTarget(chord, key));
1352
- if (target !== null) {
1353
- const local = renderRoman(chord, tonicizedKey(target.rootPc, resolved), {}).roman;
1354
- const numeral = numeralFor(target.degreeNumber, target.lower);
1355
- return {
1356
- roman: `${local}/${numeral}`,
1357
- derivation: { kind: "applied", target: numeral }
1358
- };
1359
- }
1427
+ const reading = readNumeral(chord, resolved, opts.applied === true);
1428
+ if (reading.kind === "applied") {
1429
+ const local = renderRoman(chord, tonicizedKey(reading.target.rootPc, resolved), {}).roman;
1430
+ const numeral = numeralFor(reading.target.degreeNumber, reading.target.lower);
1431
+ return {
1432
+ roman: `${local}/${numeral}`,
1433
+ derivation: { kind: "applied", target: numeral }
1434
+ };
1360
1435
  }
1361
- const { degreeNumber, accidental: spelledAccidental } = romanSpelling(chord.rootPc, key);
1436
+ const { degreeNumber, accidental: spelledAccidental } = reading;
1362
1437
  const { lower, suffix } = romanStyle(chord.quality);
1363
1438
  const cased = numeralFor(degreeNumber, lower);
1364
1439
  const accidental = seventhDegreeAccidental(spelledAccidental, degreeNumber, chord.quality, key);
@@ -1405,7 +1480,15 @@ function renderRoman(chord, keyLike, opts) {
1405
1480
  }
1406
1481
  };
1407
1482
  }
1408
- var DEGREE_NAMES = ["first", "second", "third", "fourth", "fifth", "sixth", "seventh"];
1483
+ var DEGREE_NAMES = [
1484
+ "first",
1485
+ "second",
1486
+ "third",
1487
+ "fourth",
1488
+ "fifth",
1489
+ "sixth",
1490
+ "seventh"
1491
+ ];
1409
1492
  function describeDegree(degreeNumber, accidental, literal) {
1410
1493
  const name = DEGREE_NAMES[degreeNumber - 1] ?? `${degreeNumber}th`;
1411
1494
  if (accidental !== literal) {
@@ -1544,18 +1627,19 @@ function isMinorKey(key) {
1544
1627
  return isMinorScale(toKeyScale(key));
1545
1628
  }
1546
1629
  function functionOf(chord, key) {
1547
- return functionWithReason(toChordData(chord), toKeyScale(key)).function;
1630
+ return functionWithReason(toChordData(chord), resolveKey(key)).function;
1548
1631
  }
1549
1632
  function degreeFunction(chord, key) {
1550
1633
  const functions = isMinorScale(key) ? MINOR_FUNCTION_BY_OFFSET : FUNCTION_BY_OFFSET;
1551
1634
  return functions[pitchClassOf(chord.rootPc - key.rootPc)] ?? "tonic";
1552
1635
  }
1553
- function functionWithReason(chord, key) {
1636
+ function functionWithReason(chord, resolved) {
1637
+ const key = resolved.scale;
1554
1638
  const offset = pitchClassOf(chord.rootPc - key.rootPc);
1555
1639
  if (isNeapolitanChordOf(chord, key)) {
1556
1640
  return { function: "subdominant", reason: "neapolitan" };
1557
1641
  }
1558
- if (augmentedSixthKindOf(chord, key) !== null) {
1642
+ if (augmentedSixthKindOf(chord, resolved) !== null) {
1559
1643
  return { function: "subdominant", reason: "augmentedSixth" };
1560
1644
  }
1561
1645
  if (isAppliedDominant(chord, key)) {
@@ -1581,7 +1665,8 @@ function describeChord(fn, reason, roman, source) {
1581
1665
  const head = reason === "degree" ? `${capitalize(fn)}: ${roman} takes the ${fn} function of its degree in the key` : `${capitalize(fn)}: ${roman} is ${FUNCTION_REASON_PHRASE[reason]}`;
1582
1666
  return source === null || reason === "neapolitan" ? head : `${head}, ${BORROWED_SOURCE_PHRASE[source]}`;
1583
1667
  }
1584
- function functionAlternatives(chord, key, reason, accepted) {
1668
+ function functionAlternatives(chord, resolved, reason, accepted) {
1669
+ const key = resolved.scale;
1585
1670
  const out = [];
1586
1671
  if (reason === "degree") {
1587
1672
  if (pointsAtTonicizableDegree(chord, key) && isDiatonicChord(chord, key)) {
@@ -1599,7 +1684,7 @@ function functionAlternatives(chord, key, reason, accepted) {
1599
1684
  });
1600
1685
  }
1601
1686
  }
1602
- if (augmentedSixthKindOf(chord, key) === "german") {
1687
+ if (augmentedSixthKindOf(chord, resolved) === "german") {
1603
1688
  out.push({
1604
1689
  label: "tritone substitute",
1605
1690
  reason: "The German sixth sounds a dominant seventh, but the lowered submediant under it fixes the reading as an augmented sixth"
@@ -1616,10 +1701,10 @@ function parallelKey(key) {
1616
1701
  function analyzeChord(chord, key, opts = {}) {
1617
1702
  const asked = assertOptions(opts, "opts");
1618
1703
  const data = toChordData(chord);
1619
- const scale = toKeyScale(key);
1620
- const source = borrowedSourceOf(data, scale);
1621
- const { function: fn, reason } = functionWithReason(data, scale);
1622
- const roman = renderRoman(data, scale, asked).roman;
1704
+ const resolved = resolveKey(key);
1705
+ const source = borrowedSourceOf(data, resolved.scale);
1706
+ const { function: fn, reason } = functionWithReason(data, resolved);
1707
+ const roman = renderRoman(data, resolved, asked).roman;
1623
1708
  return {
1624
1709
  function: fn,
1625
1710
  borrowed: source !== null,
@@ -1627,43 +1712,41 @@ function analyzeChord(chord, key, opts = {}) {
1627
1712
  roman,
1628
1713
  rationale: describeChord(fn, reason, roman, source),
1629
1714
  alternatives: asked.alternatives === true ? [
1630
- ...functionAlternatives(data, scale, reason, fn),
1631
- ...romanAlternatives(data, scale, asked)
1715
+ ...functionAlternatives(data, resolved, reason, fn),
1716
+ ...romanAlternatives(data, resolved, asked)
1632
1717
  ] : []
1633
1718
  };
1634
1719
  }
1635
1720
  function secondaryDominant(targetDegree, key) {
1636
- const scale = toKeyScale(key);
1721
+ const resolved = resolveKey(key);
1722
+ const scale = resolved.scale;
1637
1723
  const degreeCount = scaleTonesInDegreeOrder(scale).length;
1638
1724
  assertInteger(targetDegree, "targetDegree", 1, degreeCount);
1639
1725
  const targetRoot = degreeRootPc(targetDegree, scale);
1640
- return makeChord(pitchClassOf(targetRoot + 7), "dom7");
1726
+ const rootSpelling = transposeByInterval(keySpelling(targetRoot, resolved), "P5");
1727
+ return makeSpelledChord(pitchClassOf(targetRoot + 7), "dom7", { rootSpelling });
1641
1728
  }
1642
1729
  function secondaryDominantOf(target) {
1643
1730
  const data = toChordData(target);
1644
1731
  assertFiniteNumber(data.rootPc, "target chord rootPc");
1645
- const dominant = makeChord(pitchClassOf(data.rootPc + 7), "dom7");
1646
- if (data.rootSpelling !== void 0) {
1647
- const moved = transposeNote(data.rootSpelling, 7);
1648
- dominant.rootSpelling = { letter: moved.letter, alter: moved.alter };
1649
- }
1650
- return dominant;
1732
+ const rootSpelling = data.rootSpelling === void 0 ? void 0 : transposeNote(data.rootSpelling, 7);
1733
+ return makeSpelledChord(pitchClassOf(data.rootPc + 7), "dom7", { rootSpelling });
1651
1734
  }
1652
1735
 
1653
1736
  // src/analyze/functional/pivot.ts
1654
1737
  var HEPTATONIC_DEGREES = 7;
1655
1738
  function pivotChords(from, to) {
1656
- return pivotsBetween(toKeyScale(from), toKeyScale(to));
1739
+ return pivotsBetween(resolveKey(from), resolveKey(to));
1657
1740
  }
1658
1741
  function pivotsBetween(from, to) {
1659
- if (scaleTonesInDegreeOrder(from).length !== HEPTATONIC_DEGREES) {
1742
+ if (scaleTonesInDegreeOrder(from.scale).length !== HEPTATONIC_DEGREES) {
1660
1743
  return [];
1661
1744
  }
1662
1745
  const pivots = [];
1663
1746
  const seen = /* @__PURE__ */ new Set();
1664
1747
  for (let degree = 1; degree <= HEPTATONIC_DEGREES; degree += 1) {
1665
1748
  const chord = stackedTriad(degree, from);
1666
- if (chord === null || !isDiatonicChord(chord, to)) {
1749
+ if (chord === null || !isDiatonicChord(chord, to.scale)) {
1667
1750
  continue;
1668
1751
  }
1669
1752
  const identity = `${chord.rootPc}:${chord.quality}`;
@@ -1714,6 +1797,14 @@ function firstSoundingBeat(notes, fallback = 0) {
1714
1797
  }
1715
1798
  return Number.isFinite(first) ? first : fallback;
1716
1799
  }
1800
+ function resolveSpanEnd(totalBeats, startBeat, soundingEnd, name) {
1801
+ const given = totalBeats ?? null;
1802
+ if (given === null) {
1803
+ return Math.max(startBeat, soundingEnd);
1804
+ }
1805
+ assertRange(given, Math.min(0, startBeat), Number.MAX_SAFE_INTEGER, name);
1806
+ return Math.max(startBeat, given);
1807
+ }
1717
1808
  function lastBarOf(meter, spanStart, spanEnd) {
1718
1809
  const firstBar = barIndexAt(spanStart, meter);
1719
1810
  const bar = barIndexAt(Math.max(spanStart, spanEnd), meter);
@@ -1795,6 +1886,16 @@ function gridOriginOf(firstOnset, slotBeats) {
1795
1886
  function gridForNotes(notes, slotBeats) {
1796
1887
  return gridOriginOf(firstSoundingBeat(notes), slotBeats);
1797
1888
  }
1889
+ function latestSoundingEnd(notes, fallback = 0) {
1890
+ if (notes.length === 0) {
1891
+ return fallback;
1892
+ }
1893
+ let end = Number.NEGATIVE_INFINITY;
1894
+ for (const note of notes) {
1895
+ end = Math.max(end, note.startBeat + note.durationBeat);
1896
+ }
1897
+ return end;
1898
+ }
1798
1899
  function barGridStart(firstOnset, meter) {
1799
1900
  return gridOriginOf(firstOnset, beatsPerBarAt(firstOnset, meter)).startBeat;
1800
1901
  }
@@ -2085,9 +2186,13 @@ function keyTimelineFromNotes(notes, opts = {}) {
2085
2186
  const budget = opts.budget;
2086
2187
  assertNoteEvents(notes, "key timeline notes", { allowNonPositiveDuration: true, budget });
2087
2188
  const sounding = notes.filter((note) => note.durationBeat > 0);
2088
- const lastEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
2089
- const totalBeats = opts.totalBeats ?? lastEnd;
2090
- assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, "key timeline totalBeats");
2189
+ const totalBeats = opts.totalBeats ?? latestSoundingEnd(sounding);
2190
+ assertRange(
2191
+ totalBeats,
2192
+ Math.min(0, firstSoundingBeat(sounding)),
2193
+ Number.MAX_SAFE_INTEGER,
2194
+ "key timeline totalBeats"
2195
+ );
2091
2196
  const slotBeats = Math.min(minKeyBeats, expectedKeyBeats);
2092
2197
  const { origin, startBeat: musicStart } = gridForNotes(sounding, slotBeats);
2093
2198
  const slotCount = Math.max(0, Math.ceil((totalBeats - origin) / slotBeats - BEAT_EPS));
@@ -2223,9 +2328,10 @@ function detectModulations(chords, opts = {}) {
2223
2328
  opts.budget
2224
2329
  );
2225
2330
  const sorted = [...chords].filter((segment) => segment.endBeat > segment.startBeat).sort((a, b) => a.startBeat - b.startBeat);
2226
- const lastChordEnd = sorted.reduce((end, segment) => Math.max(end, segment.endBeat), 0);
2331
+ const lastChordEnd = sorted.length === 0 ? 0 : sorted.reduce((end, segment) => Math.max(end, segment.endBeat), Number.NEGATIVE_INFINITY);
2227
2332
  const totalBeats = opts.totalBeats ?? lastChordEnd;
2228
- assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, "modulation totalBeats");
2333
+ const lowest = totalBeats < 0 ? Math.min(0, sorted[0]?.startBeat ?? 0) : 0;
2334
+ assertRange(totalBeats, lowest, Number.MAX_SAFE_INTEGER, "modulation totalBeats");
2229
2335
  const ordered = sorted.map((segment) => ({ ...segment, endBeat: Math.min(segment.endBeat, totalBeats) })).filter((segment) => segment.endBeat > segment.startBeat);
2230
2336
  if (ordered.length === 0) {
2231
2337
  return [];
@@ -2422,7 +2528,7 @@ function prevailingKeyOf(regions) {
2422
2528
  // src/analyze/voice/index.ts
2423
2529
  function keyScaleAt(key) {
2424
2530
  if (typeof key === "function") {
2425
- return key;
2531
+ return (beat) => toKeyScale(key(beat));
2426
2532
  }
2427
2533
  const scale = toKeyScale(key);
2428
2534
  return () => scale;
@@ -2451,7 +2557,8 @@ var TENSION_DEGREE_BY_INTERVAL = {
2451
2557
  8: 13,
2452
2558
  9: 13
2453
2559
  };
2454
- var NATURAL_TENSION_INTERVALS = /* @__PURE__ */ new Set([2, 5, 9]);
2560
+ var NATURAL_TENSION_INTERVAL_LIST = [2, 5, 9];
2561
+ var NATURAL_TENSION_INTERVALS = new Set(NATURAL_TENSION_INTERVAL_LIST);
2455
2562
  function tensionDegree(ic) {
2456
2563
  return TENSION_DEGREE_BY_INTERVAL[ic];
2457
2564
  }
@@ -2466,11 +2573,22 @@ function isStep(a, b) {
2466
2573
  return d === 1 || d === 2;
2467
2574
  }
2468
2575
  var LEAP_SEMITONES = 3;
2469
- function suspensionType(ic, delta) {
2576
+ function suspensionType(ic, delta, resolvedIc) {
2577
+ const figure = suspendedFigure(ic, delta);
2578
+ return figure !== null && SUSPENSION_RESOLUTIONS[figure].includes(resolvedIc) ? figure : null;
2579
+ }
2580
+ var SUSPENSION_RESOLUTIONS = {
2581
+ "sus4-3": [3, 4],
2582
+ "sus6-5": [7],
2583
+ "sus7-6": [8, 9],
2584
+ "sus9-8": [0],
2585
+ "sus2-3": [3, 4]
2586
+ };
2587
+ function suspendedFigure(ic, delta) {
2470
2588
  if (delta > 0) {
2471
2589
  return ic === 1 || ic === 2 ? "sus2-3" : null;
2472
2590
  }
2473
- if (ic === 2) {
2591
+ if (ic === 1 || ic === 2) {
2474
2592
  return "sus9-8";
2475
2593
  }
2476
2594
  if (ic === 10 || ic === 11) {
@@ -2484,6 +2602,18 @@ function suspensionType(ic, delta) {
2484
2602
  }
2485
2603
  return null;
2486
2604
  }
2605
+ function clashesWithChord(pitch, chord) {
2606
+ if (!isConsonantInterval(intervalAboveBass(pitch, [], chord), true)) {
2607
+ return true;
2608
+ }
2609
+ const tones = chord.intervals.map((interval) => chord.rootPc + interval);
2610
+ if (chord.bassPc !== void 0) {
2611
+ tones.push(chord.bassPc);
2612
+ }
2613
+ return tones.some(
2614
+ (tone) => !isConsonantInterval(((pitchClassOf(pitch) - tone) % 12 + 12) % 12, false)
2615
+ );
2616
+ }
2487
2617
  var STEP_RESOLUTION = 2;
2488
2618
  var NEAREST_CHORD_TONE_REACH = 12;
2489
2619
  function nearestChordTone(pitch, chord, maxDistance) {
@@ -2503,17 +2633,20 @@ function analyzeVoice(voice, chordAtBeat2, key, otherVoicesAtBeat = () => []) {
2503
2633
  assertFunction(otherVoicesAtBeat, "otherVoicesAtBeat");
2504
2634
  const keyAt = keyScaleAt(key);
2505
2635
  const result = [];
2636
+ const sounding = soundingIndices(voice);
2637
+ const line = new Map(sounding.map((index, position) => [index, position]));
2506
2638
  for (let i = 0; i < voice.length; i += 1) {
2507
2639
  const note = voice[i];
2508
2640
  if (!note) {
2509
2641
  continue;
2510
2642
  }
2511
- const prevNote = i > 0 ? voice[i - 1] : void 0;
2512
- const nextNote = i + 1 < voice.length ? voice[i + 1] : void 0;
2513
- const prevEnd = prevNote === void 0 ? Number.NEGATIVE_INFINITY : prevNote.startBeat + prevNote.durationBeat;
2514
- const noteEnd = note.startBeat + note.durationBeat;
2515
- const prev = prevNote !== void 0 && adjacent(prevEnd, note.startBeat) ? prevNote : void 0;
2516
- const next = nextNote !== void 0 && adjacent(noteEnd, nextNote.startBeat) ? nextNote : void 0;
2643
+ const position = line.get(i);
2644
+ const prevIndex = position === void 0 ? void 0 : sounding[position - 1];
2645
+ const nextIndex = position === void 0 ? void 0 : sounding[position + 1];
2646
+ const prevNote = prevIndex === void 0 ? void 0 : voice[prevIndex];
2647
+ const nextNote = nextIndex === void 0 ? void 0 : voice[nextIndex];
2648
+ const prev = prevNote !== void 0 && !sameInstant(prevNote.startBeat, note.startBeat) && adjacent(prevNote.startBeat + prevNote.durationBeat, note.startBeat) ? prevNote : void 0;
2649
+ const next = nextNote !== void 0 && !sameInstant(nextNote.startBeat, note.startBeat) && adjacent(note.startBeat + note.durationBeat, nextNote.startBeat) ? nextNote : void 0;
2517
2650
  const chord = chordAtBeat2(note.startBeat);
2518
2651
  const labels = [];
2519
2652
  const member = isChordMember(note.pitch, chord);
@@ -2540,12 +2673,11 @@ function analyzeVoice(voice, chordAtBeat2, key, otherVoicesAtBeat = () => []) {
2540
2673
  const prepared = prev.pitch === note.pitch && isChordMember(prev.pitch, prevChord);
2541
2674
  const resolves = isStep(next.pitch, note.pitch);
2542
2675
  const others = otherVoicesAtBeat(note.startBeat);
2543
- const verticallyDissonant = others.some(
2544
- (ov) => createsVerticalDissonance(note.pitch, ov.pitch, true)
2545
- );
2676
+ const verticallyDissonant = others.length > 0 ? others.some((ov) => createsVerticalDissonance(note.pitch, ov.pitch, true)) : clashesWithChord(note.pitch, chord);
2546
2677
  if (prepared && verticallyDissonant && resolves) {
2547
2678
  const ic = intervalAboveBass(note.pitch, others, chord);
2548
- const type = suspensionType(ic, next.pitch - note.pitch);
2679
+ const resolvedIc = intervalAboveBass(next.pitch, others, chord);
2680
+ const type = suspensionType(ic, next.pitch - note.pitch, resolvedIc);
2549
2681
  if (type !== null) {
2550
2682
  labels.push({ kind: "suspension", type, resolveTo: next.pitch });
2551
2683
  handled = true;
@@ -2675,8 +2807,34 @@ function assertChordTimeline(timeline, name = "timeline") {
2675
2807
  assertFiniteNumber(segment.startBeat, `${name}.segments[${index}].startBeat`);
2676
2808
  assertFiniteNumber(segment.endBeat, `${name}.segments[${index}].endBeat`);
2677
2809
  }
2810
+ assertSpansInOrder(segments, `${name}.segments`);
2678
2811
  return read;
2679
2812
  }
2813
+ function assertSpansInOrder(spans, name) {
2814
+ for (let index = 0; index < spans.length; index += 1) {
2815
+ const span = spans[index];
2816
+ if (span === void 0) {
2817
+ continue;
2818
+ }
2819
+ if (!(span.endBeat > span.startBeat)) {
2820
+ throw new InvalidInputError(
2821
+ `${name}[${index}] must end after it starts; received [${span.startBeat}, ${span.endBeat})`
2822
+ );
2823
+ }
2824
+ const previous = spans[index - 1];
2825
+ if (previous !== void 0 && span.startBeat < previous.endBeat) {
2826
+ throw new InvalidInputError(
2827
+ `${name}[${index}] must not start before ${name}[${index - 1}] ends; received ${span.startBeat} before ${previous.endBeat}`
2828
+ );
2829
+ }
2830
+ }
2831
+ }
2832
+ function statedKeyRegions(key, startBeat, endBeat) {
2833
+ if (!(endBeat > startBeat)) {
2834
+ return [];
2835
+ }
2836
+ return [{ startBeat, endBeat, key, confidence: 1 }];
2837
+ }
2680
2838
  function chordTimelineFromChords(chords, totalBeats) {
2681
2839
  assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, "timeline totalBeats");
2682
2840
  assertArray(chords, "chords");
@@ -2741,6 +2899,7 @@ var EXTRA_TONE_PENALTY = 0.6;
2741
2899
  var OMITTED_TONE_PENALTY = 0.15;
2742
2900
  var BASS_ROOT_STANDING = 0.95;
2743
2901
  var OCTAVE = 12;
2902
+ var CHORD_SEGMENTATIONS = ["dynamic", "grid"];
2744
2903
  function scoreMatch(match, weights, maxWeight, key, pedalPc) {
2745
2904
  const rootWeight = maxWeight > 0 ? (weights[match.rootPc] ?? 0) / maxWeight : 0;
2746
2905
  const rootStanding = match.inversion === 0 ? Math.max(rootWeight, BASS_ROOT_STANDING) : rootWeight;
@@ -2757,7 +2916,10 @@ function scoreMatch(match, weights, maxWeight, key, pedalPc) {
2757
2916
  score -= OMITTED_TONE_PENALTY * account.missing;
2758
2917
  return score;
2759
2918
  }
2760
- var SLASH_UPPER_QUALITIES = /* @__PURE__ */ new Set(["maj", "min"]);
2919
+ var SLASH_UPPER_QUALITY_LIST = ["maj", "min"];
2920
+ var SLASH_UPPER_QUALITIES = new Set(
2921
+ SLASH_UPPER_QUALITY_LIST
2922
+ );
2761
2923
  function isSlashReading(match, pedalPc) {
2762
2924
  return pedalPc !== null && match.inversion === null && match.missingPcs.length === 0 && match.extraPcs.length === 1 && match.extraPcs[0] === pedalPc && SLASH_UPPER_QUALITIES.has(match.quality);
2763
2925
  }
@@ -2789,6 +2951,30 @@ function bassSoundsOnlyBelow(notes, windowStart, windowEnd, bassPc) {
2789
2951
  }
2790
2952
  return highestBass <= upperFloor;
2791
2953
  }
2954
+ function windowBass(notes, windowStart, windowEnd, meters, harmonicRhythm) {
2955
+ const counted = [];
2956
+ let firstSounding = Number.POSITIVE_INFINITY;
2957
+ for (const note of notes) {
2958
+ const overlap = Math.min(note.startBeat + note.durationBeat, windowEnd) - Math.max(note.startBeat, windowStart);
2959
+ if (overlap <= BEAT_EPS) {
2960
+ continue;
2961
+ }
2962
+ const onsetInWindow = note.startBeat >= windowStart - BEAT_EPS && note.startBeat < windowEnd - BEAT_EPS;
2963
+ if (noteWeight(note, overlap, onsetInWindow ? meters : void 0) <= BEAT_EPS) {
2964
+ continue;
2965
+ }
2966
+ counted.push({ note, overlap });
2967
+ firstSounding = Math.min(firstSounding, Math.max(note.startBeat, windowStart));
2968
+ }
2969
+ const half = (windowEnd - windowStart) / 2;
2970
+ let bass = Number.POSITIVE_INFINITY;
2971
+ for (const { note, overlap } of counted) {
2972
+ if (note.startBeat <= firstSounding + BEAT_EPS || overlap > half || overlap >= harmonicRhythm - BEAT_EPS) {
2973
+ bass = Math.min(bass, note.pitch);
2974
+ }
2975
+ }
2976
+ return bass;
2977
+ }
2792
2978
  function chordConfidence(chord, weights, totalWeight, exact) {
2793
2979
  const tones = new Set(chordPitchClasses(chord));
2794
2980
  let chordWeight = 0;
@@ -2801,23 +2987,20 @@ function chordConfidence(chord, weights, totalWeight, exact) {
2801
2987
  const confidence = Math.min(1, Math.max(0, raw));
2802
2988
  return exact ? confidence : confidence * INEXACT_CONFIDENCE_FACTOR;
2803
2989
  }
2804
- function analyzeWindow(notes, windowStart, windowEnd, meters, key) {
2805
- const { weights, totalWeight, maxWeight, lowestPitch } = windowWeights(
2806
- notes,
2807
- windowStart,
2808
- windowEnd,
2809
- meters
2810
- );
2990
+ function analyzeWindow(notes, windowStart, windowEnd, meters, key, harmonicRhythm) {
2991
+ const { weights, totalWeight, maxWeight } = windowWeights(notes, windowStart, windowEnd, meters);
2811
2992
  if (totalWeight <= BEAT_EPS) {
2812
2993
  return null;
2813
2994
  }
2814
2995
  const ranked = weights.map((weight, pc) => ({ pc, weight })).filter(({ weight }) => weight > 0).sort((a, b) => b.weight - a.weight || a.pc - b.pc);
2815
- const selected = ranked.filter(({ weight }) => weight >= maxWeight * NOISE_THRESHOLD_RATIO).slice(0, MAX_DETECTION_PCS).map(({ pc }) => pc);
2816
- const bassPc = pitchClassOf(lowestPitch);
2817
- const hasBass = selected.includes(bassPc);
2818
- const detectionPitches = hasBass ? selected.map((pc) => pc === bassPc ? pc : pc + 12) : selected;
2819
- const pedalPc = hasBass && bassSoundsOnlyBelow(notes, windowStart, windowEnd, bassPc) ? bassPc : null;
2820
- const matches = detectChord(detectionPitches, { input: hasBass ? "midi" : "pitchClass" });
2996
+ const bassPc = pitchClassOf(windowBass(notes, windowStart, windowEnd, meters, harmonicRhythm));
2997
+ const sounding = ranked.slice(0, MAX_DETECTION_PCS).map(({ pc }) => pc);
2998
+ const significant = ranked.filter(({ weight }) => weight >= maxWeight * NOISE_THRESHOLD_RATIO).slice(0, MAX_DETECTION_PCS).map(({ pc }) => pc);
2999
+ let detection = detectWindow(significant, bassPc, notes, windowStart, windowEnd);
3000
+ if (detection.matches.length === 0 && significant.length < sounding.length) {
3001
+ detection = detectWindow(sounding, bassPc, notes, windowStart, windowEnd);
3002
+ }
3003
+ const { selected, hasBass, pedalPc, matches } = detection;
2821
3004
  let bestMatch = null;
2822
3005
  let bestScore = Number.NEGATIVE_INFINITY;
2823
3006
  for (const match of matches) {
@@ -2834,10 +3017,26 @@ function analyzeWindow(notes, windowStart, windowEnd, meters, key) {
2834
3017
  makeChord(bestMatch.rootPc, bestMatch.quality),
2835
3018
  pedalPc
2836
3019
  );
3020
+ const explained = account.exact || isSlashReading(bestMatch, pedalPc);
2837
3021
  const reading = {
2838
3022
  chord: shared(chord),
2839
- confidence: chordConfidence(chord, weights, totalWeight, account.exact)
3023
+ confidence: chordConfidence(chord, weights, totalWeight, explained)
2840
3024
  };
3025
+ if (bestMatch.quality === SYMMETRIC_QUALITY) {
3026
+ reading.rootings = chordPitchClasses(makeChord(bestMatch.rootPc, bestMatch.quality)).map(
3027
+ (rootPc) => {
3028
+ const rooted = makeChord(
3029
+ rootPc,
3030
+ bestMatch.quality,
3031
+ hasBass && bassPc !== rootPc ? bassPc : void 0
3032
+ );
3033
+ return {
3034
+ chord: shared(rooted),
3035
+ confidence: chordConfidence(rooted, weights, totalWeight, explained)
3036
+ };
3037
+ }
3038
+ );
3039
+ }
2841
3040
  const augmented = hasBass ? augmentedSixthFromPitchClasses(selected, bassPc, key) : null;
2842
3041
  if (augmented !== null) {
2843
3042
  reading.augmented = {
@@ -2849,6 +3048,13 @@ function analyzeWindow(notes, windowStart, windowEnd, meters, key) {
2849
3048
  }
2850
3049
  return null;
2851
3050
  }
3051
+ function detectWindow(selected, bassPc, notes, windowStart, windowEnd) {
3052
+ const hasBass = selected.includes(bassPc);
3053
+ const detectionPitches = hasBass ? selected.map((pc) => pc === bassPc ? pc : pc + 12) : selected;
3054
+ const pedalPc = hasBass && bassSoundsOnlyBelow(notes, windowStart, windowEnd, bassPc) ? bassPc : null;
3055
+ const matches = detectChord(detectionPitches, { input: hasBass ? "midi" : "pitchClass" });
3056
+ return { selected, hasBass, pedalPc, matches };
3057
+ }
2852
3058
  function shared(chord) {
2853
3059
  Object.freeze(chord.intervals);
2854
3060
  return Object.freeze(chord);
@@ -2875,6 +3081,29 @@ function resolvesOntoDominant(readings, index, key) {
2875
3081
  }
2876
3082
  return true;
2877
3083
  }
3084
+ var SYMMETRIC_QUALITY = "dim7";
3085
+ function settledDiminishedRoot(readings, index, key) {
3086
+ const reading = readings[index];
3087
+ const rootings = reading?.rootings;
3088
+ if (!reading || rootings === void 0) {
3089
+ return null;
3090
+ }
3091
+ const roots = rootings.map((rooting) => rooting.chord.rootPc);
3092
+ const tones = chordPitchClasses(reading.chord).join();
3093
+ for (let i = index + 1; i < readings.length; i += 1) {
3094
+ const next = readings[i];
3095
+ if (!next || chordPitchClasses(next.chord).join() === tones) {
3096
+ continue;
3097
+ }
3098
+ const leading = pitchClassOf(next.chord.rootPc - 1);
3099
+ if (roots.includes(leading)) {
3100
+ return leading;
3101
+ }
3102
+ break;
3103
+ }
3104
+ const leadingTone = pitchClassOf(key.rootPc - 1);
3105
+ return roots.includes(leadingTone) ? leadingTone : null;
3106
+ }
2878
3107
  function sameChord(a, b) {
2879
3108
  return a.rootPc === b.rootPc && a.quality === b.quality && a.bassPc === b.bassPc;
2880
3109
  }
@@ -3123,7 +3352,11 @@ function analyzeTimeline(notes, opts = {}, previous, dirty) {
3123
3352
  const meters = resolveMeters(opts, "timeline meters");
3124
3353
  const harmonicRhythm = opts.harmonicRhythm ?? beatsPerBarAt(0, meters);
3125
3354
  assertRange(harmonicRhythm, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "harmonic rhythm");
3126
- const segmentation = opts.segmentation ?? "dynamic";
3355
+ const segmentation = assertOneOf(
3356
+ opts.segmentation ?? "dynamic",
3357
+ CHORD_SEGMENTATIONS,
3358
+ "timeline segmentation"
3359
+ );
3127
3360
  const gridUnit = metricGridUnit(meters);
3128
3361
  const minChordBeats = opts.minChordBeats ?? gridUnit;
3129
3362
  assertRange(minChordBeats, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "minChordBeats");
@@ -3135,9 +3368,13 @@ function analyzeTimeline(notes, opts = {}, previous, dirty) {
3135
3368
  }
3136
3369
  assertNoteEvents(notes, "timeline notes", noteOptions);
3137
3370
  const sounding = sortedNoteEvents(notes.filter((note) => note.durationBeat > 0));
3138
- const lastNoteEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
3139
- const totalBeats = opts.totalBeats ?? lastNoteEnd;
3140
- assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, "timeline totalBeats");
3371
+ const totalBeats = opts.totalBeats ?? latestSoundingEnd(sounding);
3372
+ assertRange(
3373
+ totalBeats,
3374
+ Math.min(0, firstSoundingBeat(sounding)),
3375
+ Number.MAX_SAFE_INTEGER,
3376
+ "timeline totalBeats"
3377
+ );
3141
3378
  const slotBeats = segmentation === "grid" ? harmonicRhythm : slotBeatsWithin(gridUnit, minChordBeats, harmonicRhythm);
3142
3379
  const { origin, startBeat: musicStart } = gridForNotes(sounding, slotBeats);
3143
3380
  const grid = { origin, slotBeats };
@@ -3148,14 +3385,7 @@ function analyzeTimeline(notes, opts = {}, previous, dirty) {
3148
3385
  "timeline windows",
3149
3386
  budget
3150
3387
  );
3151
- const keys = opts.key !== void 0 ? [
3152
- {
3153
- startBeat: musicStart,
3154
- endBeat: totalBeats,
3155
- key: resolveKey(opts.key),
3156
- confidence: 1
3157
- }
3158
- ] : keyTimelineFromNotes(sounding, { meters, totalBeats, budget });
3388
+ const keys = opts.key !== void 0 ? statedKeyRegions(resolveKey(opts.key), musicStart, totalBeats) : keyTimelineFromNotes(sounding, { meters, totalBeats, budget });
3159
3389
  const prevailingKey = prevailingKeyOf(keys) ?? resolveKey(majorKey(0));
3160
3390
  const keyAt = keyLookup(keys, prevailingKey);
3161
3391
  const scaleAt = (beat) => scaleOf(keyAt(beat));
@@ -3211,12 +3441,27 @@ function analyzeTimeline(notes, opts = {}, previous, dirty) {
3211
3441
  const spanKeyWhole = keyAt(start);
3212
3442
  const id = windowId(start, end, spanKey);
3213
3443
  const cached = carried !== void 0 && spanIsClean(grid, span, dirtyFrom, dirtyTo) ? carried.windows.get(id) : void 0;
3214
- const inferred = cached !== void 0 ? cached : analyzeWindow(notesOfSpan(slotNotes, span, grid), start, end, meters, spanKeyWhole);
3444
+ const inferred = cached !== void 0 ? cached : analyzeWindow(
3445
+ notesOfSpan(slotNotes, span, grid),
3446
+ start,
3447
+ end,
3448
+ meters,
3449
+ spanKeyWhole,
3450
+ harmonicRhythm
3451
+ );
3215
3452
  windows.set(id, inferred);
3216
3453
  readings.push(inferred);
3217
3454
  readingKeys.push(spanKey);
3218
3455
  readingBounds.push({ start, end });
3219
3456
  }
3457
+ for (let i = readings.length - 1; i >= 0; i -= 1) {
3458
+ const spanKey = readingKeys[i];
3459
+ const root = spanKey === void 0 ? null : settledDiminishedRoot(readings, i, spanKey);
3460
+ const rooting = readings[i]?.rootings?.find((option) => option.chord.rootPc === root);
3461
+ if (rooting !== void 0 && root !== readings[i]?.chord.rootPc) {
3462
+ readings[i] = rooting;
3463
+ }
3464
+ }
3220
3465
  for (let i = 0; i < readings.length; i += 1) {
3221
3466
  const offered = readings[i]?.augmented;
3222
3467
  const spanKey = readingKeys[i];
@@ -3369,11 +3614,7 @@ function roundTo(value, digits) {
3369
3614
  return Math.round(value * scale) / scale;
3370
3615
  }
3371
3616
  function endBeatOf(notes) {
3372
- let end = 0;
3373
- for (const note of notes) {
3374
- end = Math.max(end, note.startBeat + note.durationBeat);
3375
- }
3376
- return end;
3617
+ return latestSoundingEnd(notes);
3377
3618
  }
3378
3619
  function onsetSpan(notes) {
3379
3620
  if (notes.length < 2) {
@@ -3439,7 +3680,7 @@ function melodicContour(notes) {
3439
3680
  peakIndex: peak,
3440
3681
  troughIndex: trough,
3441
3682
  range,
3442
- rationale: `${share}% of the motion runs ${net > 0 ? "up" : "down"}, so the line is ${shape}`
3683
+ rationale: `The net ${net > 0 ? "rise" : "fall"} is ${share}% of the motion, so the line is ${shape}`
3443
3684
  };
3444
3685
  }
3445
3686
  const peakPitch = line[peak]?.pitch ?? 0;
@@ -3460,7 +3701,7 @@ function melodicContour(notes) {
3460
3701
  peakIndex: peak,
3461
3702
  troughIndex: trough,
3462
3703
  range,
3463
- rationale: `Changes direction without settling either way (${share}% of the motion nets out), so the line is a wave`
3704
+ rationale: `Changes direction without settling either way (the net movement is ${share}% of the motion), so the line is a wave`
3464
3705
  };
3465
3706
  }
3466
3707
 
@@ -3640,8 +3881,8 @@ function spellingOf(key) {
3640
3881
  return keySignatureFifths(key.tonic, key.scale) < 0 ? "flat" : "sharp";
3641
3882
  }
3642
3883
  function intervalBetween(from, to, key) {
3643
- const spelling = spellingOf(key);
3644
- return spelledInterval(midiToNote(from, spelling), midiToNote(to, spelling));
3884
+ const start = midiToNote(from, spellingOf(key));
3885
+ return spelledInterval(start, transposeNote(start, to - from));
3645
3886
  }
3646
3887
  function intervalName(interval) {
3647
3888
  return `${interval.descending && interval.semitones !== 0 ? "-" : ""}${interval.quality}${interval.number}`;
@@ -3771,11 +4012,12 @@ function relateMotifs(a, b, keyLike) {
3771
4012
  }
3772
4013
  }
3773
4014
  }
3774
- if (forward && shapes.includes("inversion")) {
4015
+ const literalRetrograde = backward && shapes.includes("retrograde") && answer[0]?.pitch === model[model.length - 1]?.pitch;
4016
+ if (forward && shapes.includes("inversion") && !(literalRetrograde && semitones !== 0)) {
3775
4017
  return {
3776
4018
  ...base,
3777
4019
  kind: "inversion",
3778
- rationale: "Inversion: every interval turned upside down about the first note" + (stretched ? `, in ${stretchPhrase(timeRatio)}` : "")
4020
+ rationale: `Inversion: every interval turned upside down${semitones === 0 ? " about the first note" : ""}` + (stretched ? `, in ${stretchPhrase(timeRatio)}` : "")
3779
4021
  };
3780
4022
  }
3781
4023
  if (backward && shapes.includes("retrograde")) {
@@ -3839,6 +4081,7 @@ function compareMelodies(a, b) {
3839
4081
  const right = orderedNotes(phraseNotes(b), "melody b");
3840
4082
  const leftIntervals = intervalsOf(left);
3841
4083
  const rightIntervals = intervalsOf(right);
4084
+ assertGenerationBudget(leftIntervals.length * rightIntervals.length, "melody alignment");
3842
4085
  const pitchSimilarity = distanceToSimilarity(
3843
4086
  gradedEditDistance(leftIntervals, rightIntervals, intervalCost),
3844
4087
  leftIntervals.length,
@@ -3953,7 +4196,13 @@ function motifGraph(line, motifs, opts = {}) {
3953
4196
 
3954
4197
  // src/analyze/reduction/index.ts
3955
4198
  var REDUCTION_LEVELS = Object.freeze(["structural", "passing", "neighbor"]);
3956
- var TONIC_ARRIVING = /* @__PURE__ */ new Set(["authentic", "plagal", "modal", "deceptive"]);
4199
+ var TONIC_ARRIVING_CADENCES = [
4200
+ "authentic",
4201
+ "plagal",
4202
+ "modal",
4203
+ "deceptive"
4204
+ ];
4205
+ var TONIC_ARRIVING = new Set(TONIC_ARRIVING_CADENCES);
3957
4206
  function signedStep(fromPc, toPc) {
3958
4207
  return (pitchClassOf(toPc) - pitchClassOf(fromPc) + 18) % 12 - 6;
3959
4208
  }
@@ -4104,6 +4353,11 @@ function reduceProgression(timeline, key, opts = {}) {
4104
4353
  return result;
4105
4354
  }
4106
4355
 
4356
+ // src/analyze/reference/types.ts
4357
+ var REFERENCE_PROFILE_VERSION = 1;
4358
+ var PHRASE_OUTLINE_POINTS = 8;
4359
+ var MIDI_MAX = 127;
4360
+
4107
4361
  // src/analyze/rhythm/internal.ts
4108
4362
  function isMultipleOf(value, unit) {
4109
4363
  if (unit <= 0) {
@@ -4122,9 +4376,15 @@ function rhythmLevel(beatInQuarters, meters) {
4122
4376
  const subUnit = pulse / (isCompound(ts) ? 3 : 2);
4123
4377
  return isMultipleOf(offset, subUnit) ? 1 : 0;
4124
4378
  }
4379
+ var IOI_MIN_EXPONENT = -4;
4380
+ var IOI_MAX_EXPONENT = 4;
4381
+ var IOI_EXPONENT_STEP = 0.5;
4125
4382
  function ioiBinIndex(ioi) {
4126
- const exponent = Math.min(4, Math.max(-4, Math.round(2 * Math.log2(ioi)) / 2));
4127
- return Math.round((exponent + 4) / 0.5);
4383
+ const exponent = Math.min(
4384
+ IOI_MAX_EXPONENT,
4385
+ Math.max(IOI_MIN_EXPONENT, Math.round(2 * Math.log2(ioi)) / 2)
4386
+ );
4387
+ return Math.round((exponent - IOI_MIN_EXPONENT) / IOI_EXPONENT_STEP);
4128
4388
  }
4129
4389
 
4130
4390
  // src/analyze/rhythm/index.ts
@@ -4134,6 +4394,8 @@ var RHYTHM_OFF_GRID_TOLERANCE = 1 / 32;
4134
4394
  var RHYTHM_IOI_BINS = Object.freeze(
4135
4395
  Array.from({ length: 17 }, (_, i) => 2 ** (-4 + i * 0.5))
4136
4396
  );
4397
+ var BUDGET_SLOTS_PER_BAR = 48;
4398
+ var RHYTHM_LEVEL_COUNT = 6;
4137
4399
  function emptyAnalysis() {
4138
4400
  return {
4139
4401
  startBeat: 0,
@@ -4142,7 +4404,7 @@ function emptyAnalysis() {
4142
4404
  onsets: 0,
4143
4405
  onsetDensity: 0,
4144
4406
  barOnsets: [],
4145
- onsetLevels: new Array(6).fill(0),
4407
+ onsetLevels: new Array(RHYTHM_LEVEL_COUNT).fill(0),
4146
4408
  barPositions: [],
4147
4409
  interOnsetShares: new Array(RHYTHM_IOI_BINS.length).fill(0),
4148
4410
  restRatio: 0,
@@ -4177,13 +4439,47 @@ function snapOnset(onset, meters) {
4177
4439
  function tsKey(ts) {
4178
4440
  return ts.grouping === void 0 ? `${ts.numerator}/${ts.denominator}` : `${ts.numerator}/${ts.denominator}:${ts.grouping.join("+")}`;
4179
4441
  }
4180
- function barGridBeats(bar, meters) {
4442
+ function pulseKey(bar, index) {
4443
+ return `${bar}/${index}`;
4444
+ }
4445
+ function otherDivisionPulses(snapped, meters) {
4446
+ const pulses = /* @__PURE__ */ new Set();
4447
+ for (const onset of snapped) {
4448
+ const pulse = pulseBeats(onset.ts);
4449
+ const offset = onset.snapped - onset.barStart;
4450
+ const index = Math.floor(offset / pulse + BEAT_EPS);
4451
+ const within = offset - index * pulse;
4452
+ const [used, other] = isCompound(onset.ts) ? [3, 2] : [2, 3];
4453
+ if (isMultipleOf(within, pulse / other) && !isMultipleOf(within, pulse / used)) {
4454
+ pulses.add(pulseKey(barIndexAt(onset.beat, meters), index));
4455
+ }
4456
+ }
4457
+ return pulses;
4458
+ }
4459
+ function barGridPoints(bar, meters, otherPulses) {
4181
4460
  const barStart = barPositionToBeat({ bar, beat: 0 }, meters);
4182
4461
  const ts = meterAt(barStart, meters);
4183
4462
  const barLength = beatsPerBarAt(barStart, meters);
4184
- const subUnit = pulseBeats(ts) / (isCompound(ts) ? 3 : 2);
4463
+ const pulse = pulseBeats(ts);
4464
+ const [used, other] = isCompound(ts) ? [3, 2] : [2, 3];
4465
+ const subUnit = pulse / used;
4185
4466
  const steps = Math.max(1, Math.round(barLength / subUnit));
4186
- return Array.from({ length: steps }, (_, k) => barStart + k * subUnit);
4467
+ const points = [];
4468
+ for (let k = 0; k < steps; k += 1) {
4469
+ const index = Math.floor(k * subUnit / pulse + BEAT_EPS);
4470
+ if (k % used === 0 || !otherPulses.has(pulseKey(bar, index))) {
4471
+ const beat = barStart + k * subUnit;
4472
+ points.push({ beat, level: rhythmLevel(beat, meters) });
4473
+ }
4474
+ }
4475
+ for (let index = 0; index * pulse < steps * subUnit - BEAT_EPS; index += 1) {
4476
+ if (otherPulses.has(pulseKey(bar, index))) {
4477
+ for (let m = 1; m < other; m += 1) {
4478
+ points.push({ beat: barStart + index * pulse + m * pulse / other, level: 1 });
4479
+ }
4480
+ }
4481
+ }
4482
+ return points.sort((a, b) => a.beat - b.beat);
4187
4483
  }
4188
4484
  function restRatioOf(sounding, windowStart, windowEnd) {
4189
4485
  const span = windowEnd - windowStart;
@@ -4219,11 +4515,12 @@ function syncopationOf(snapped, startBeat, endBeat, meters) {
4219
4515
  }
4220
4516
  const firstBar = barIndexAt(startBeat, meters);
4221
4517
  const lastBar = barIndexAt(endBeat, meters);
4518
+ const otherPulses = otherDivisionPulses(snapped, meters);
4222
4519
  const points = [];
4223
4520
  for (let bar = firstBar; bar <= lastBar; bar += 1) {
4224
- for (const beat of barGridBeats(bar, meters)) {
4225
- if (beat > startBeat + BEAT_EPS && beat <= endBeat + BEAT_EPS) {
4226
- points.push({ beat, level: rhythmLevel(beat, meters) });
4521
+ for (const point of barGridPoints(bar, meters, otherPulses)) {
4522
+ if (point.beat > startBeat + BEAT_EPS && point.beat <= endBeat + BEAT_EPS) {
4523
+ points.push(point);
4227
4524
  }
4228
4525
  }
4229
4526
  }
@@ -4243,36 +4540,44 @@ function syncopationOf(snapped, startBeat, endBeat, meters) {
4243
4540
  while (ptr < points.length && (points[ptr]?.beat ?? Number.POSITIVE_INFINITY) <= onset.snapped + BEAT_EPS) {
4244
4541
  ptr += 1;
4245
4542
  }
4543
+ const here = ptr > 0 ? points[ptr - 1] : void 0;
4544
+ const onsetLevel = here !== void 0 && Math.abs(here.beat - onset.snapped) < BEAT_EPS ? Math.max(onset.level, here.level) : onset.level;
4246
4545
  let scan = ptr;
4247
4546
  let maxLevel = 0;
4248
4547
  while (scan < points.length && (points[scan]?.beat ?? Number.POSITIVE_INFINITY) < windowEnd - BEAT_EPS) {
4249
4548
  maxLevel = Math.max(maxLevel, points[scan]?.level ?? 0);
4250
4549
  scan += 1;
4251
4550
  }
4252
- if (maxLevel > onset.level) {
4253
- numerator += maxLevel - onset.level;
4551
+ if (maxLevel > onsetLevel) {
4552
+ numerator += maxLevel - onsetLevel;
4254
4553
  }
4255
4554
  ptr = scan;
4256
4555
  }
4257
4556
  return numerator / normalizer;
4258
4557
  }
4259
4558
  function analyzeRhythm(notes, opts = {}) {
4559
+ return rhythmOver(notes, opts, void 0);
4560
+ }
4561
+ function rhythmOver(notes, opts, windowStart) {
4260
4562
  const meters = resolveMeters(opts, "rhythm meters");
4261
4563
  assertNoteEvents(notes, "rhythm notes", { allowNonPositiveDuration: true, budget: opts.budget });
4262
4564
  const sounding = notes.filter((note) => note.durationBeat > 0);
4565
+ const startBeat = windowStart ?? barGridStart(firstSoundingBeat(sounding), meters);
4566
+ const endBeat = resolveSpanEnd(
4567
+ opts.totalBeats,
4568
+ startBeat,
4569
+ latestSoundingEnd(sounding),
4570
+ "rhythm totalBeats"
4571
+ );
4263
4572
  if (sounding.length === 0) {
4264
4573
  return emptyAnalysis();
4265
4574
  }
4266
- const firstOnset = sounding.reduce(
4267
- (min, n) => Math.min(min, n.startBeat),
4268
- Number.POSITIVE_INFINITY
4269
- );
4270
- const startBeat = barGridStart(firstOnset, meters);
4271
- const lastEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
4272
- const endBeat = Math.max(startBeat, opts.totalBeats ?? lastEnd);
4273
- assertRange(endBeat, 0, Number.MAX_SAFE_INTEGER, "rhythm totalBeats");
4274
4575
  const bars = barSpanOf(startBeat, endBeat, meters);
4275
- assertGenerationBudget(Math.ceil(bars) * 48, "rhythm analysis slots", opts.budget);
4576
+ assertGenerationBudget(
4577
+ Math.ceil(bars) * BUDGET_SLOTS_PER_BAR,
4578
+ "rhythm analysis slots",
4579
+ opts.budget
4580
+ );
4276
4581
  const onsets = foldOnsets(sounding);
4277
4582
  const snapped = onsets.map((onset) => snapOnset(onset, meters));
4278
4583
  const onsetDensity = bars > 0 ? onsets.length / bars : 0;
@@ -4284,7 +4589,7 @@ function analyzeRhythm(notes, opts = {}) {
4284
4589
  barOnsets[index] = (barOnsets[index] ?? 0) + 1;
4285
4590
  }
4286
4591
  }
4287
- const onsetLevels = new Array(6).fill(0);
4592
+ const onsetLevels = new Array(RHYTHM_LEVEL_COUNT).fill(0);
4288
4593
  let offGridCount = 0;
4289
4594
  for (const onset of snapped) {
4290
4595
  onsetLevels[onset.level] = (onsetLevels[onset.level] ?? 0) + 1;
@@ -4357,9 +4662,6 @@ function analyzeRhythm(notes, opts = {}) {
4357
4662
  };
4358
4663
  }
4359
4664
 
4360
- // src/analyze/reference/types.ts
4361
- var REFERENCE_PROFILE_VERSION = 1;
4362
-
4363
4665
  // src/analyze/reference/validate.ts
4364
4666
  var MASS_TOLERANCE = 1e-9;
4365
4667
  function assertString(value, name) {
@@ -4427,7 +4729,7 @@ function assertRhythmAnalysis(value, name) {
4427
4729
  `${name}.barOnsets[${index}]`
4428
4730
  );
4429
4731
  }
4430
- assertMassDistribution(read.onsetLevels, 6, `${name}.onsetLevels`, onsets === 0);
4732
+ assertMassDistribution(read.onsetLevels, RHYTHM_LEVEL_COUNT, `${name}.onsetLevels`, onsets === 0);
4431
4733
  const barPositions = assertArray(read.barPositions, `${name}.barPositions`);
4432
4734
  for (let index = 0; index < barPositions.length; index += 1) {
4433
4735
  assertBarPositionProfile(barPositions[index], `${name}.barPositions[${index}]`);
@@ -4468,12 +4770,14 @@ function assertReferencePhraseMelody(value, name) {
4468
4770
  assertOneOf(read.shape, MELODIC_CONTOUR_SHAPES, `${name}.shape`);
4469
4771
  assertMidiPitch(read.low, `${name}.low`);
4470
4772
  assertMidiPitch(read.high, `${name}.high`);
4471
- assertRange(read.mean, 0, 127, `${name}.mean`);
4472
- assertRange(read.peakPitch, 0, 127, `${name}.peakPitch`);
4773
+ assertRange(read.mean, 0, MIDI_MAX, `${name}.mean`);
4774
+ assertRange(read.peakPitch, 0, MIDI_MAX, `${name}.peakPitch`);
4473
4775
  assertRange(read.peakPosition, 0, 1, `${name}.peakPosition`);
4474
4776
  const outline = assertArray(read.outline, `${name}.outline`);
4475
- if (outline.length !== 8) {
4476
- throw new InvalidInputError(`${name}.outline must have length 8; received ${outline.length}`);
4777
+ if (outline.length !== PHRASE_OUTLINE_POINTS) {
4778
+ throw new InvalidInputError(
4779
+ `${name}.outline must have length ${PHRASE_OUTLINE_POINTS}; received ${outline.length}`
4780
+ );
4477
4781
  }
4478
4782
  for (let index = 0; index < outline.length; index += 1) {
4479
4783
  assertFiniteNumber(outline[index], `${name}.outline[${index}]`);
@@ -4482,11 +4786,13 @@ function assertReferencePhraseMelody(value, name) {
4482
4786
  assertRange(read.restRatio, 0, 1, `${name}.restRatio`);
4483
4787
  return read;
4484
4788
  }
4485
- function assertReferencePhrase(value, name, sectionCount, nodeCount) {
4789
+ function assertReferencePhrase(value, name, span, sectionCount, nodeCount) {
4486
4790
  const read = assertRecord(value, name);
4487
4791
  const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
4488
4792
  const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
4489
4793
  assertSpanOrder(startBeat, endBeat, name);
4794
+ assertRange(startBeat, span.startBeat, span.endBeat, `${name}.startBeat`);
4795
+ assertRange(endBeat, span.startBeat, span.endBeat, `${name}.endBeat`);
4490
4796
  assertRange(read.bars, 0, Number.MAX_SAFE_INTEGER, `${name}.bars`);
4491
4797
  assertNullable(
4492
4798
  read.section,
@@ -4507,7 +4813,7 @@ function assertReferencePhrase(value, name, sectionCount, nodeCount) {
4507
4813
  }
4508
4814
  return read;
4509
4815
  }
4510
- function assertReferenceForm(value, name, nodeCount) {
4816
+ function assertReferenceForm(value, name, span, nodeCount) {
4511
4817
  const read = assertRecord(value, name);
4512
4818
  const hypermeter = assertRecord(
4513
4819
  read.hypermeter,
@@ -4521,7 +4827,13 @@ function assertReferenceForm(value, name, nodeCount) {
4521
4827
  }
4522
4828
  const phrases = assertArray(read.phrases, `${name}.phrases`);
4523
4829
  for (let index = 0; index < phrases.length; index += 1) {
4524
- assertReferencePhrase(phrases[index], `${name}.phrases[${index}]`, sections.length, nodeCount);
4830
+ assertReferencePhrase(
4831
+ phrases[index],
4832
+ `${name}.phrases[${index}]`,
4833
+ span,
4834
+ sections.length,
4835
+ nodeCount
4836
+ );
4525
4837
  }
4526
4838
  return read;
4527
4839
  }
@@ -4661,9 +4973,9 @@ function assertReferenceMotif(value, name) {
4661
4973
  low = Math.min(low, pitch);
4662
4974
  high = Math.max(high, pitch);
4663
4975
  }
4664
- if (high - low > 127) {
4976
+ if (high - low > MIDI_MAX) {
4665
4977
  throw new InvalidInputError(
4666
- `${name}.intervals must span at most 127 semitones; received ${high - low}`
4978
+ `${name}.intervals must span at most ${MIDI_MAX} semitones; received ${high - low}`
4667
4979
  );
4668
4980
  }
4669
4981
  const rhythm = assertArray(read.rhythm, `${name}.rhythm`);
@@ -4694,7 +5006,7 @@ function assertReferenceMelody(value, name) {
4694
5006
  const register = assertRecord(v, n);
4695
5007
  assertMidiPitch(register.low, `${n}.low`);
4696
5008
  assertMidiPitch(register.high, `${n}.high`);
4697
- assertRange(register.mean, 0, 127, `${n}.mean`);
5009
+ assertRange(register.mean, 0, MIDI_MAX, `${n}.mean`);
4698
5010
  return register;
4699
5011
  });
4700
5012
  assertNullable(read.shape, `${name}.shape`, (v, n) => assertOneOf(v, MELODIC_CONTOUR_SHAPES, n));
@@ -4725,7 +5037,7 @@ function assertReferenceProfile(value, name = "reference profile") {
4725
5037
  assertRange(span.bars, 0, Number.MAX_SAFE_INTEGER, `${name}.span.bars`);
4726
5038
  assertMeterMap(read.meters, `${name}.meters`);
4727
5039
  const melody = assertReferenceMelody(read.melody, `${name}.melody`);
4728
- assertReferenceForm(read.form, `${name}.form`, melody.graph.nodes.length);
5040
+ assertReferenceForm(read.form, `${name}.form`, { startBeat, endBeat }, melody.graph.nodes.length);
4729
5041
  assertReferenceHarmony(read.harmony, `${name}.harmony`);
4730
5042
  return read;
4731
5043
  }
@@ -4742,11 +5054,12 @@ var PEAK_POSITION_WEIGHT = 0.5;
4742
5054
  var FAMILY_SIZE_BINS = 5;
4743
5055
  var DEPTH_BINS = 4;
4744
5056
  var NAMED_CHORDS = ["Ger", "It", "Fr", "N"];
4745
- var PITCH_PRESERVING = /* @__PURE__ */ new Set([
5057
+ var PITCH_PRESERVING_KINDS = [
4746
5058
  "repetition",
4747
5059
  "augmentation",
4748
5060
  "diminution"
4749
- ]);
5061
+ ];
5062
+ var PITCH_PRESERVING = new Set(PITCH_PRESERVING_KINDS);
4750
5063
  var CELL_START_PITCH = 60;
4751
5064
  function whenBoth(hasA, hasB, measure) {
4752
5065
  if (!hasA && !hasB) {
@@ -4899,7 +5212,7 @@ function cellNotes(motif) {
4899
5212
  low = Math.min(low, offset);
4900
5213
  high = Math.max(high, offset);
4901
5214
  }
4902
- let pitch = Math.min(127 - high, Math.max(-low, CELL_START_PITCH));
5215
+ let pitch = Math.min(MIDI_MAX - high, Math.max(-low, CELL_START_PITCH));
4903
5216
  let beat = 0;
4904
5217
  for (let i = 0; i <= motif.intervals.length; i += 1) {
4905
5218
  const step = motif.rhythm[i] ?? 1;
@@ -5073,8 +5386,8 @@ function phraseMelody(line, startBeat, endBeat, lineMean, meters, budget) {
5073
5386
  }
5074
5387
  const length = endBeat - startBeat;
5075
5388
  const outline = [];
5076
- for (let k = 0; k < 8; k += 1) {
5077
- const at = startBeat + (k + 0.5) * length / 8;
5389
+ for (let k = 0; k < PHRASE_OUTLINE_POINTS; k += 1) {
5390
+ const at = startBeat + (k + 0.5) * length / PHRASE_OUTLINE_POINTS;
5078
5391
  let held = first;
5079
5392
  for (const note of inside) {
5080
5393
  if (note.startBeat > at + BEAT_EPS) {
@@ -5084,7 +5397,7 @@ function phraseMelody(line, startBeat, endBeat, lineMean, meters, budget) {
5084
5397
  }
5085
5398
  outline.push(held.pitch - lineMean);
5086
5399
  }
5087
- const rhythm = analyzeRhythm(inside, { meters, totalBeats: endBeat, budget });
5400
+ const rhythm = rhythmOver(inside, { meters, totalBeats: endBeat, budget }, startBeat);
5088
5401
  return {
5089
5402
  shape: melodicContour(inside).shape,
5090
5403
  low,
@@ -5110,6 +5423,16 @@ export {
5110
5423
  detectKey,
5111
5424
  detectKeyFromNotes,
5112
5425
  detectKeyBest,
5426
+ barSpanOf,
5427
+ clamp01,
5428
+ floorMod,
5429
+ firstSoundingBeat,
5430
+ resolveSpanEnd,
5431
+ lastBarOf,
5432
+ sliceBars,
5433
+ weightSimilarity,
5434
+ harmonicNovelty,
5435
+ combineEvidence,
5113
5436
  augmentedSixthChord,
5114
5437
  augmentedSixthKind,
5115
5438
  augmentedSixthFromPitchClasses,
@@ -5130,16 +5453,8 @@ export {
5130
5453
  secondaryDominant,
5131
5454
  secondaryDominantOf,
5132
5455
  pivotChords,
5133
- barSpanOf,
5134
- clamp01,
5135
- floorMod,
5136
- firstSoundingBeat,
5137
- lastBarOf,
5138
- sliceBars,
5139
- weightSimilarity,
5140
- harmonicNovelty,
5141
- combineEvidence,
5142
5456
  gridForNotes,
5457
+ latestSoundingEnd,
5143
5458
  barGridStart,
5144
5459
  keyTimelineFromNotes,
5145
5460
  detectModulations,
@@ -5149,6 +5464,8 @@ export {
5149
5464
  toVoiceNotes,
5150
5465
  analyzeVoice,
5151
5466
  assertChordTimeline,
5467
+ assertSpansInOrder,
5468
+ statedKeyRegions,
5152
5469
  chordTimelineFromChords,
5153
5470
  chordAtBeat,
5154
5471
  segmentStartingAt,
@@ -5169,6 +5486,7 @@ export {
5169
5486
  motifGraph,
5170
5487
  REDUCTION_LEVELS,
5171
5488
  reduceProgression,
5489
+ REFERENCE_PROFILE_VERSION,
5172
5490
  sectionSequenceSimilarity,
5173
5491
  phraseLengthSimilarity,
5174
5492
  progressionSimilarity,
@@ -5187,9 +5505,9 @@ export {
5187
5505
  ioiBinIndex,
5188
5506
  RHYTHM_IOI_BINS,
5189
5507
  analyzeRhythm,
5190
- REFERENCE_PROFILE_VERSION,
5508
+ rhythmOver,
5191
5509
  assertReferenceProfile,
5192
5510
  weightedMean,
5193
5511
  phraseMelody
5194
5512
  };
5195
- //# sourceMappingURL=chunk-GNVN4PPJ.js.map
5513
+ //# sourceMappingURL=chunk-FJFULJZE.js.map