@libraz/libcantus 1.2.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 (131) hide show
  1. package/NOTICE +63 -0
  2. package/README.md +7 -1
  3. package/README_ja.md +17 -11
  4. package/dist/analyze/index.cjs +31 -7
  5. package/dist/analyze/index.cjs.map +1 -1
  6. package/dist/analyze/index.d.cts +161 -13
  7. package/dist/analyze/index.d.ts +161 -13
  8. package/dist/analyze/index.js +37 -13
  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-HPVSH7SF.cjs → chunk-3MDOEFIQ.cjs} +618 -954
  14. package/dist/chunk-3MDOEFIQ.cjs.map +1 -0
  15. package/dist/{chunk-FBH75DAY.js → chunk-5MWRBYAT.js} +560 -159
  16. package/dist/chunk-5MWRBYAT.js.map +1 -0
  17. package/dist/{chunk-J7FHVZII.cjs → chunk-6TTBEON3.cjs} +3074 -740
  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-HWHGOVO6.cjs → chunk-E3QO3UET.cjs} +31 -23
  22. package/dist/chunk-E3QO3UET.cjs.map +1 -0
  23. package/dist/{chunk-H2KJHBW4.js → chunk-FJFULJZE.js} +2375 -191
  24. package/dist/chunk-FJFULJZE.js.map +1 -0
  25. package/dist/{chunk-Q6LFSAKV.js → chunk-FK3DGAIZ.js} +2657 -323
  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-DSMJRXCZ.cjs → chunk-HYSVSFNI.cjs} +931 -530
  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-LQZQDBCD.cjs +5513 -0
  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-HYJJ2V2I.js → chunk-UTE5A36R.js} +483 -819
  46. package/dist/chunk-UTE5A36R.js.map +1 -0
  47. package/dist/{chunk-NFJ4P6ZZ.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 +20 -8
  67. package/dist/generate/index.cjs.map +1 -1
  68. package/dist/generate/index.d.cts +105 -17
  69. package/dist/generate/index.d.ts +105 -17
  70. package/dist/generate/index.js +19 -7
  71. package/dist/{index-CL4b4gUQ.d.ts → index-5VJgKz_Q.d.cts} +6 -2
  72. package/dist/{index-B9QLziDe.d.cts → index-BLuDesVG.d.ts} +20 -13
  73. package/dist/{index-BHIUyjhZ.d.ts → index-BV6APVa6.d.cts} +20 -13
  74. package/dist/{index-BMweUlm5.d.cts → 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-BNSM-DSk.d.cts → index-DVInzCDS.d.cts} +533 -28
  80. package/dist/{index-9eoQs2C1.d.ts → index-Dg-SAfhz.d.ts} +6 -4
  81. package/dist/{index-BJgP97N1.d.ts → index-jgeug9EA.d.ts} +533 -28
  82. package/dist/{index-cHvbYgJI.d.cts → index-pQihL4z7.d.cts} +6 -4
  83. package/dist/index.cjs +49 -13
  84. package/dist/index.cjs.map +1 -1
  85. package/dist/index.d.cts +13 -13
  86. package/dist/index.d.ts +13 -13
  87. package/dist/index.js +55 -19
  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 +259 -48
  93. package/dist/model/index.d.ts +259 -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/{index-BRMbPkHE.d.ts → profile-B4WqcM4H.d.ts} +85 -610
  98. package/dist/{index-DGCNKAUr.d.cts → profile-BJd1XkPB.d.cts} +85 -610
  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/{index-CJBBiAmU.d.ts → types-7mGFwvMm.d.ts} +1210 -31
  105. package/dist/{index-BBWNZS0f.d.cts → types-_ofpb2be.d.cts} +1210 -31
  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-76ZBTRG2.cjs.map +0 -1
  110. package/dist/chunk-7KG3RTYS.js.map +0 -1
  111. package/dist/chunk-AGOVW7U6.js.map +0 -1
  112. package/dist/chunk-CGBADIO5.cjs.map +0 -1
  113. package/dist/chunk-DSMJRXCZ.cjs.map +0 -1
  114. package/dist/chunk-FBH75DAY.js.map +0 -1
  115. package/dist/chunk-H2KJHBW4.js.map +0 -1
  116. package/dist/chunk-HPVSH7SF.cjs.map +0 -1
  117. package/dist/chunk-HWHGOVO6.cjs.map +0 -1
  118. package/dist/chunk-HYJJ2V2I.js.map +0 -1
  119. package/dist/chunk-J7FHVZII.cjs.map +0 -1
  120. package/dist/chunk-M5RYMAFI.js.map +0 -1
  121. package/dist/chunk-NB25P3EH.js.map +0 -1
  122. package/dist/chunk-NFJ4P6ZZ.js.map +0 -1
  123. package/dist/chunk-NM7UAMC3.cjs.map +0 -1
  124. package/dist/chunk-PM4XJE2Y.cjs.map +0 -1
  125. package/dist/chunk-PUKJP2Z5.js.map +0 -1
  126. package/dist/chunk-Q6LFSAKV.js.map +0 -1
  127. package/dist/chunk-RFIBF2RR.js.map +0 -1
  128. package/dist/chunk-S5BKZZGM.cjs.map +0 -1
  129. package/dist/chunk-SGLEQVJH.cjs +0 -3329
  130. package/dist/chunk-SGLEQVJH.cjs.map +0 -1
  131. package/dist/chunk-UMWZ4ZET.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,
@@ -33,8 +34,11 @@ import {
33
34
  isNeapolitanChordOf,
34
35
  isScaleTone,
35
36
  keyRelationBetween,
37
+ keySignatureFifths,
38
+ keySpelling,
36
39
  majorKey,
37
40
  makeChord,
41
+ makeSpelledChord,
38
42
  minorKey,
39
43
  resolveKey,
40
44
  scaleOf,
@@ -44,10 +48,11 @@ import {
44
48
  spellChord,
45
49
  spellPitchClass,
46
50
  spelledKeyOf,
51
+ spellingAbove,
47
52
  toChordData,
48
53
  toKeyScale,
49
54
  tonicizableDegrees
50
- } from "./chunk-RFIBF2RR.js";
55
+ } from "./chunk-H7FVMBUC.js";
51
56
  import {
52
57
  BEAT_EPS,
53
58
  InvalidInputError,
@@ -55,29 +60,43 @@ import {
55
60
  allocateChoiceTable,
56
61
  assertArray,
57
62
  assertFiniteNumber,
63
+ assertFlag,
58
64
  assertFunction,
59
65
  assertGenerationBudget,
60
66
  assertInteger,
67
+ assertMeterMap,
61
68
  assertMidiPitch,
62
69
  assertNoteEvents,
63
70
  assertOneOf,
64
71
  assertOptions,
72
+ assertPositiveInt,
65
73
  assertRange,
66
74
  assertRecord,
75
+ assertTimeSignature,
67
76
  barIndexAt,
68
77
  barPositionToBeat,
78
+ barStartBeat,
79
+ beatsPerBar,
69
80
  beatsPerBarAt,
81
+ describeRejected,
70
82
  diatonicLetterOf,
71
83
  formatNote,
84
+ isCompound,
85
+ isConsonantInterval,
86
+ meterAt,
72
87
  metricGridUnit,
73
88
  metricWeight,
89
+ midiToNote,
90
+ naturalPitchClassOf,
74
91
  parseInterval,
75
92
  pitchClassOf,
93
+ pulseBeats,
76
94
  resolveMeters,
95
+ spelledInterval,
77
96
  toNoteData,
78
97
  transposeByInterval,
79
98
  transposeNote
80
- } from "./chunk-AGOVW7U6.js";
99
+ } from "./chunk-YYVXRV3V.js";
81
100
 
82
101
  // src/analyze/detect/modes.ts
83
102
  var MODAL_CANDIDATES = Object.freeze([
@@ -191,6 +210,30 @@ function windowWeights(notes, windowStart, windowEnd, meter) {
191
210
  return { weights, totalWeight, maxWeight, lowestPitch };
192
211
  }
193
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
+
194
237
  // src/analyze/detect/profiles.ts
195
238
  var KRUMHANSL_KESSLER_PROFILE = {
196
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],
@@ -289,6 +332,11 @@ function profileScore(dist, vector, tonic) {
289
332
  }
290
333
 
291
334
  // src/analyze/detect/index.ts
335
+ var CHORD_INPUT_KINDS = [
336
+ "auto",
337
+ "midi",
338
+ "pitchClass"
339
+ ];
292
340
  function uniquePitchClasses(pitches) {
293
341
  return [...new Set(pitches.map(pitchClassOf))].sort((a, b) => a - b);
294
342
  }
@@ -302,12 +350,13 @@ function assertPitches(pitches, name, budget = void 0) {
302
350
  function detectChord(pitches, opts = {}) {
303
351
  const asked = assertOptions(opts, "opts");
304
352
  assertPitches(pitches, "chord detection pitches");
353
+ const askedKind = assertOneOf(asked.input ?? "auto", CHORD_INPUT_KINDS, "input");
305
354
  if (asked.bassPc !== void 0) assertFiniteNumber(asked.bassPc, "bassPc");
306
355
  const input = uniquePitchClasses(pitches);
307
356
  if (input.length === 0) {
308
357
  return [];
309
358
  }
310
- 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;
311
360
  const bassPc = asked.bassPc !== void 0 ? pitchClassOf(asked.bassPc) : inputKind === "midi" ? pitchClassOf(pitches.reduce((lowest, pitch) => Math.min(lowest, pitch), Infinity)) : void 0;
312
361
  const inputSet = new Set(input);
313
362
  const matches = [];
@@ -326,6 +375,9 @@ function detectChord(pitches, opts = {}) {
326
375
  if (missingPcs.length > 1 || missingPcs.length === 1 && missingPcs[0] !== fifthPc) {
327
376
  continue;
328
377
  }
378
+ if (tones.length - missingPcs.length < 2) {
379
+ continue;
380
+ }
329
381
  const extraPcs = input.filter((pc) => !toneSet.has(pc));
330
382
  const bassIndex = bassPc === void 0 ? -1 : chord.intervals.findIndex((iv) => pitchClassOf(rootPc + iv) === bassPc);
331
383
  const inversion = bassPc === void 0 || bassIndex < 0 ? null : bassIndex;
@@ -374,31 +426,9 @@ function detectChordBest(pitches, opts = {}) {
374
426
  }
375
427
  return makeChord(best.rootPc, best.quality, best.bassPc);
376
428
  }
377
- var MINOR_VARIANTS = [
378
- { variant: "natural", mask: NATURAL_MINOR_MASK, scaleName: "naturalMinor" },
379
- { variant: "harmonic", mask: HARMONIC_MINOR_MASK, scaleName: "harmonicMinor" },
380
- { variant: "melodic", mask: MELODIC_MINOR_MASK, scaleName: "melodicMinor" }
381
- ];
382
- var MAJOR_VARIANTS = [
383
- { variant: "major", mask: MAJOR_MASK, scaleName: "major" }
384
- ];
385
- var SCALE_NAME_ORDER = [
386
- "major",
387
- "naturalMinor",
388
- "harmonicMinor",
389
- "melodicMinor",
390
- "dorian",
391
- "phrygian",
392
- "lydian",
393
- "mixolydian",
394
- "locrian"
395
- ];
396
429
  function scaleNameRank(name) {
397
430
  return SCALE_NAME_ORDER.indexOf(name);
398
431
  }
399
- var EXPLAINED_RIVALS = 3;
400
- var SCORE_DIGITS = 2;
401
- var MAX_SCORE_DIGITS = 6;
402
432
  function quoteScores(rival, winner) {
403
433
  let digits = SCORE_DIGITS;
404
434
  while (digits < MAX_SCORE_DIGITS && rival.toFixed(digits) === winner.toFixed(digits)) {
@@ -616,13 +646,14 @@ var CHORD_SHAPE = {
616
646
  french: { rootAboveBass: 6, quality: "7b5", intervals: [0, 4, 6, 10] },
617
647
  german: { rootAboveBass: 0, quality: "dom7", intervals: [0, 4, 7, 10] }
618
648
  };
619
- var KIND_BY_SYMBOL = /* @__PURE__ */ new Map([
649
+ var KIND_SYMBOLS = [
620
650
  ["It6", "italian"],
621
651
  ["Fr6", "french"],
622
652
  ["Ger6", "german"],
623
653
  // The German sixth's figures over its bass are 6/5; both spellings are in use.
624
654
  ["Ger65", "german"]
625
- ]);
655
+ ];
656
+ var KIND_BY_SYMBOL = new Map(KIND_SYMBOLS);
626
657
  var SYMBOL_BY_KIND = {
627
658
  italian: "It6",
628
659
  french: "Fr6",
@@ -647,14 +678,16 @@ function augmentedSixthOn(kind, keyLike) {
647
678
  const shape = CHORD_SHAPE[checked];
648
679
  const { scale: key, tonic } = resolveKey(keyLike);
649
680
  const bassPc = bassPcOf(key);
650
- const chord = makeChord(pitchClassOf(bassPc + shape.rootAboveBass), shape.quality, bassPc);
651
- chord.intervals = [...shape.intervals];
652
681
  const bass = transposeByInterval(tonic, BASS_INTERVAL);
653
682
  const tones = spelledInChordOrder(checked, tonic);
654
683
  const [root = bass] = tones;
655
- chord.rootSpelling = bareSpelling(root);
656
- chord.bassSpelling = bareSpelling(bass);
657
- 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];
658
691
  return chord;
659
692
  }
660
693
  function kindOfPitchClasses(pcs, bass) {
@@ -779,6 +812,14 @@ function outerPitches(voicing) {
779
812
  function bassPcOf2(chord, voicedBass) {
780
813
  return pitchClassOf(voicedBass ?? chord.bassPc ?? chord.rootPc);
781
814
  }
815
+ var CADENCE_TYPES = Object.freeze([
816
+ "authentic",
817
+ "plagal",
818
+ "half",
819
+ "deceptive",
820
+ "phrygian",
821
+ "modal"
822
+ ]);
782
823
  function cadenceType(from, to, key, fromBassPc, toBassPc) {
783
824
  const tonic = pitchClassOf(key.rootPc);
784
825
  const fromOffset = pitchClassOf(from.rootPc - tonic);
@@ -988,14 +1029,6 @@ function borrowedSource(chord, key) {
988
1029
  function isTonicizableRoot(rootPc, key) {
989
1030
  return tonicizableDegrees(key).some((degree) => degree.rootPc === pitchClassOf(rootPc));
990
1031
  }
991
- function appliedTarget(chord, key) {
992
- const dominant = hasDominantSonority(chord);
993
- const leadingTone = LEADING_TONE_QUALITIES.has(chord.quality);
994
- if (!dominant && !leadingTone) {
995
- return null;
996
- }
997
- return appliedDominantTarget(chord.rootPc + (dominant ? 5 : 1), key);
998
- }
999
1032
  function isAppliedDominant(chord, key) {
1000
1033
  if (isDiatonicChord(chord, key)) {
1001
1034
  return false;
@@ -1007,7 +1040,7 @@ function isAppliedDominant(chord, key) {
1007
1040
  return pointsAtTonicizableDegree(chord, key);
1008
1041
  }
1009
1042
  function pointsAtTonicizableDegree(chord, key) {
1010
- if (appliedTarget(chord, key) !== null) {
1043
+ if (appliedChordTarget(chord, key) !== null) {
1011
1044
  return true;
1012
1045
  }
1013
1046
  return soundsDominantSeventh(chord) && isTonicizableRoot(pitchClassOf(chord.rootPc - 1), key);
@@ -1050,7 +1083,8 @@ function seventhQuality(base, halfDim) {
1050
1083
  }
1051
1084
  return base === "maj" ? "dom7" : "min7";
1052
1085
  }
1053
- 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);
1054
1088
  var ROMAN_STYLE = {
1055
1089
  maj: { lower: false, suffix: "" },
1056
1090
  min: { lower: true, suffix: "" },
@@ -1122,7 +1156,12 @@ function parseInversion(figures) {
1122
1156
  }
1123
1157
  return { inversion: 0, seventh: false };
1124
1158
  }
1125
- 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;
1126
1165
  const match = /^([b#]?)([iIvV]+)(.*)$/.exec(text.trim());
1127
1166
  if (!match) {
1128
1167
  throw new InvalidInputError(`Invalid Roman numeral: ${text}`);
@@ -1146,8 +1185,17 @@ function parseSimpleRoman(text, key) {
1146
1185
  );
1147
1186
  const rootForQuality = (quality2) => accidental === 0 && degreeNumber === 7 && loweredDegrees(frame).has(7) && LEADING_TONE_QUALITIES.has(quality2) ? pitchClassOf(diatonicRootPc + 1) : diatonicRootPc;
1148
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
+ };
1149
1197
  if (canonical !== void 0) {
1150
- return { rootPc: rootForQuality(canonical), quality: canonical, inversion: 0 };
1198
+ return parsed(canonical, 0);
1151
1199
  }
1152
1200
  const parsedSuffix = /^(o|°|dim|ø|\+|aug)?(maj7|M7)?(65|64|43|42|7|6|2)?$/.exec(suffix);
1153
1201
  if (!parsedSuffix) {
@@ -1186,20 +1234,23 @@ function parseSimpleRoman(text, key) {
1186
1234
  } else {
1187
1235
  quality = base;
1188
1236
  }
1189
- return { rootPc: rootForQuality(quality), quality, inversion };
1237
+ return parsed(quality, inversion);
1190
1238
  }
1191
1239
  var NEAPOLITAN_SIXTH = "N6";
1192
1240
  function chromaticSymbolChord(text, key) {
1193
1241
  if (text === NEAPOLITAN_SIXTH) {
1194
1242
  const rootPc = pitchClassOf(key.scale.rootPc + 1);
1195
- 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
+ });
1196
1247
  }
1197
1248
  return augmentedSixthFromSymbol(text, key);
1198
1249
  }
1199
- function tonicizedKey(targetRoot, key) {
1250
+ function tonicizedKey(targetRoot, key, written) {
1200
1251
  return resolveKey({
1201
1252
  scale: majorKey(targetRoot),
1202
- tonic: spellPitchClass(targetRoot, key.tonic, key.scale)
1253
+ tonic: written ?? spellPitchClass(targetRoot, key.tonic, key.scale)
1203
1254
  });
1204
1255
  }
1205
1256
  function augmentedSixthTarget(chord, key) {
@@ -1217,30 +1268,34 @@ function augmentedSixthTarget(chord, key) {
1217
1268
  return null;
1218
1269
  }
1219
1270
  function chordFromParsed(parsed) {
1220
- const chord = makeChord(parsed.rootPc, parsed.quality);
1221
- if (parsed.inversion > 0 && parsed.inversion < chord.intervals.length) {
1222
- chord.bassPc = pitchClassOf(parsed.rootPc + (chord.intervals[parsed.inversion] ?? 0));
1223
- }
1224
- 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
+ });
1225
1276
  }
1226
1277
  function romanToChord(text, key) {
1227
1278
  const resolved = resolveKey(key);
1228
- 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
+ }
1229
1284
  const trimmed = text.trim().replace(/(\d)\/(?=\d)/g, "$1");
1230
1285
  const slash = trimmed.indexOf("/");
1231
1286
  if (slash >= 0) {
1232
1287
  const applied = trimmed.slice(0, slash);
1233
1288
  const target = trimmed.slice(slash + 1);
1234
- const targetRoot = parseSimpleRoman(target, scale).rootPc;
1235
- const localKey = tonicizedKey(targetRoot, resolved);
1236
- 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));
1237
1292
  }
1238
- return chromaticSymbolChord(trimmed, resolved) ?? chordFromParsed(parseSimpleRoman(trimmed, scale));
1293
+ return chromaticSymbolChord(trimmed, resolved) ?? chordFromParsed(parseSimpleRoman(trimmed, resolved));
1239
1294
  }
1240
1295
  function romanStyle(quality) {
1241
1296
  return ROMAN_STYLE[quality];
1242
1297
  }
1243
- function romanSpelling(rootPc, key) {
1298
+ function romanSpelling(rootPc, key, quality) {
1244
1299
  const tones = scaleTonesInDegreeOrder(romanReference(key));
1245
1300
  const diatonic = tones.indexOf(pitchClassOf(rootPc));
1246
1301
  if (diatonic >= 0) {
@@ -1252,7 +1307,7 @@ function romanSpelling(rootPc, key) {
1252
1307
  return { degreeNumber: i + 1, accidental: "#" };
1253
1308
  }
1254
1309
  }
1255
- for (let i = 0; i < tones.length; i += 1) {
1310
+ for (let i = 0; i < tones.length && !LEADING_TONE_QUALITIES.has(quality); i += 1) {
1256
1311
  const degreeNumber2 = i + 1;
1257
1312
  if (degreeNumber2 === 1 || degreeNumber2 === 5) {
1258
1313
  continue;
@@ -1298,8 +1353,9 @@ function baseMarker(quality) {
1298
1353
  }
1299
1354
  var TRIAD_FIGURES = { 1: "6", 2: "64" };
1300
1355
  var SEVENTH_FIGURES = { 1: "65", 2: "43", 3: "42" };
1301
- var TRIAD_FIGURE_QUALITIES = /* @__PURE__ */ new Set(["maj", "min", "dim", "aug"]);
1302
- 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 = [
1303
1359
  "maj7",
1304
1360
  "min7",
1305
1361
  "dom7",
@@ -1308,7 +1364,8 @@ var SEVENTH_FIGURE_QUALITIES = /* @__PURE__ */ new Set([
1308
1364
  "minMaj7",
1309
1365
  "aug7",
1310
1366
  "augMaj7"
1311
- ]);
1367
+ ];
1368
+ var SEVENTH_FIGURE_QUALITIES = new Set(SEVENTH_FIGURE_QUALITY_NAMES);
1312
1369
  function numeralFor(degreeNumber, lower) {
1313
1370
  const glyph = ROMAN[degreeNumber - 1];
1314
1371
  if (glyph === void 0) {
@@ -1319,6 +1376,47 @@ function numeralFor(degreeNumber, lower) {
1319
1376
  function chordToRoman(chord, key, opts = {}) {
1320
1377
  return renderRoman(toChordData(chord), key, assertOptions(opts, "opts")).roman;
1321
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
+ }
1322
1420
  function renderRoman(chord, keyLike, opts) {
1323
1421
  const resolved = resolveKey(keyLike);
1324
1422
  const key = resolved.scale;
@@ -1326,18 +1424,16 @@ function renderRoman(chord, keyLike, opts) {
1326
1424
  if (augmented !== null) {
1327
1425
  return { roman: augmented, derivation: { kind: "augmentedSixth" } };
1328
1426
  }
1329
- if (opts.applied === true) {
1330
- const target = augmentedSixthTarget(chord, resolved) ?? (isDiatonicChord(chord, key) ? null : appliedTarget(chord, key));
1331
- if (target !== null) {
1332
- const local = renderRoman(chord, tonicizedKey(target.rootPc, resolved), {}).roman;
1333
- const numeral = numeralFor(target.degreeNumber, target.lower);
1334
- return {
1335
- roman: `${local}/${numeral}`,
1336
- derivation: { kind: "applied", target: numeral }
1337
- };
1338
- }
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
+ };
1339
1435
  }
1340
- const { degreeNumber, accidental: spelledAccidental } = romanSpelling(chord.rootPc, key);
1436
+ const { degreeNumber, accidental: spelledAccidental } = reading;
1341
1437
  const { lower, suffix } = romanStyle(chord.quality);
1342
1438
  const cased = numeralFor(degreeNumber, lower);
1343
1439
  const accidental = seventhDegreeAccidental(spelledAccidental, degreeNumber, chord.quality, key);
@@ -1384,7 +1480,15 @@ function renderRoman(chord, keyLike, opts) {
1384
1480
  }
1385
1481
  };
1386
1482
  }
1387
- 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
+ ];
1388
1492
  function describeDegree(degreeNumber, accidental, literal) {
1389
1493
  const name = DEGREE_NAMES[degreeNumber - 1] ?? `${degreeNumber}th`;
1390
1494
  if (accidental !== literal) {
@@ -1466,6 +1570,7 @@ function capitalize(text) {
1466
1570
  }
1467
1571
 
1468
1572
  // src/analyze/functional/function.ts
1573
+ var HARMONIC_FUNCTIONS = Object.freeze(["tonic", "subdominant", "dominant"]);
1469
1574
  var FUNCTION_BY_OFFSET = [
1470
1575
  "tonic",
1471
1576
  // 0 I
@@ -1522,18 +1627,19 @@ function isMinorKey(key) {
1522
1627
  return isMinorScale(toKeyScale(key));
1523
1628
  }
1524
1629
  function functionOf(chord, key) {
1525
- return functionWithReason(toChordData(chord), toKeyScale(key)).function;
1630
+ return functionWithReason(toChordData(chord), resolveKey(key)).function;
1526
1631
  }
1527
1632
  function degreeFunction(chord, key) {
1528
1633
  const functions = isMinorScale(key) ? MINOR_FUNCTION_BY_OFFSET : FUNCTION_BY_OFFSET;
1529
1634
  return functions[pitchClassOf(chord.rootPc - key.rootPc)] ?? "tonic";
1530
1635
  }
1531
- function functionWithReason(chord, key) {
1636
+ function functionWithReason(chord, resolved) {
1637
+ const key = resolved.scale;
1532
1638
  const offset = pitchClassOf(chord.rootPc - key.rootPc);
1533
1639
  if (isNeapolitanChordOf(chord, key)) {
1534
1640
  return { function: "subdominant", reason: "neapolitan" };
1535
1641
  }
1536
- if (augmentedSixthKindOf(chord, key) !== null) {
1642
+ if (augmentedSixthKindOf(chord, resolved) !== null) {
1537
1643
  return { function: "subdominant", reason: "augmentedSixth" };
1538
1644
  }
1539
1645
  if (isAppliedDominant(chord, key)) {
@@ -1559,7 +1665,8 @@ function describeChord(fn, reason, roman, source) {
1559
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]}`;
1560
1666
  return source === null || reason === "neapolitan" ? head : `${head}, ${BORROWED_SOURCE_PHRASE[source]}`;
1561
1667
  }
1562
- function functionAlternatives(chord, key, reason, accepted) {
1668
+ function functionAlternatives(chord, resolved, reason, accepted) {
1669
+ const key = resolved.scale;
1563
1670
  const out = [];
1564
1671
  if (reason === "degree") {
1565
1672
  if (pointsAtTonicizableDegree(chord, key) && isDiatonicChord(chord, key)) {
@@ -1577,7 +1684,7 @@ function functionAlternatives(chord, key, reason, accepted) {
1577
1684
  });
1578
1685
  }
1579
1686
  }
1580
- if (augmentedSixthKindOf(chord, key) === "german") {
1687
+ if (augmentedSixthKindOf(chord, resolved) === "german") {
1581
1688
  out.push({
1582
1689
  label: "tritone substitute",
1583
1690
  reason: "The German sixth sounds a dominant seventh, but the lowered submediant under it fixes the reading as an augmented sixth"
@@ -1594,10 +1701,10 @@ function parallelKey(key) {
1594
1701
  function analyzeChord(chord, key, opts = {}) {
1595
1702
  const asked = assertOptions(opts, "opts");
1596
1703
  const data = toChordData(chord);
1597
- const scale = toKeyScale(key);
1598
- const source = borrowedSourceOf(data, scale);
1599
- const { function: fn, reason } = functionWithReason(data, scale);
1600
- 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;
1601
1708
  return {
1602
1709
  function: fn,
1603
1710
  borrowed: source !== null,
@@ -1605,43 +1712,41 @@ function analyzeChord(chord, key, opts = {}) {
1605
1712
  roman,
1606
1713
  rationale: describeChord(fn, reason, roman, source),
1607
1714
  alternatives: asked.alternatives === true ? [
1608
- ...functionAlternatives(data, scale, reason, fn),
1609
- ...romanAlternatives(data, scale, asked)
1715
+ ...functionAlternatives(data, resolved, reason, fn),
1716
+ ...romanAlternatives(data, resolved, asked)
1610
1717
  ] : []
1611
1718
  };
1612
1719
  }
1613
1720
  function secondaryDominant(targetDegree, key) {
1614
- const scale = toKeyScale(key);
1721
+ const resolved = resolveKey(key);
1722
+ const scale = resolved.scale;
1615
1723
  const degreeCount = scaleTonesInDegreeOrder(scale).length;
1616
1724
  assertInteger(targetDegree, "targetDegree", 1, degreeCount);
1617
1725
  const targetRoot = degreeRootPc(targetDegree, scale);
1618
- return makeChord(pitchClassOf(targetRoot + 7), "dom7");
1726
+ const rootSpelling = transposeByInterval(keySpelling(targetRoot, resolved), "P5");
1727
+ return makeSpelledChord(pitchClassOf(targetRoot + 7), "dom7", { rootSpelling });
1619
1728
  }
1620
1729
  function secondaryDominantOf(target) {
1621
1730
  const data = toChordData(target);
1622
1731
  assertFiniteNumber(data.rootPc, "target chord rootPc");
1623
- const dominant = makeChord(pitchClassOf(data.rootPc + 7), "dom7");
1624
- if (data.rootSpelling !== void 0) {
1625
- const moved = transposeNote(data.rootSpelling, 7);
1626
- dominant.rootSpelling = { letter: moved.letter, alter: moved.alter };
1627
- }
1628
- return dominant;
1732
+ const rootSpelling = data.rootSpelling === void 0 ? void 0 : transposeNote(data.rootSpelling, 7);
1733
+ return makeSpelledChord(pitchClassOf(data.rootPc + 7), "dom7", { rootSpelling });
1629
1734
  }
1630
1735
 
1631
1736
  // src/analyze/functional/pivot.ts
1632
1737
  var HEPTATONIC_DEGREES = 7;
1633
1738
  function pivotChords(from, to) {
1634
- return pivotsBetween(toKeyScale(from), toKeyScale(to));
1739
+ return pivotsBetween(resolveKey(from), resolveKey(to));
1635
1740
  }
1636
1741
  function pivotsBetween(from, to) {
1637
- if (scaleTonesInDegreeOrder(from).length !== HEPTATONIC_DEGREES) {
1742
+ if (scaleTonesInDegreeOrder(from.scale).length !== HEPTATONIC_DEGREES) {
1638
1743
  return [];
1639
1744
  }
1640
1745
  const pivots = [];
1641
1746
  const seen = /* @__PURE__ */ new Set();
1642
1747
  for (let degree = 1; degree <= HEPTATONIC_DEGREES; degree += 1) {
1643
1748
  const chord = stackedTriad(degree, from);
1644
- if (chord === null || !isDiatonicChord(chord, to)) {
1749
+ if (chord === null || !isDiatonicChord(chord, to.scale)) {
1645
1750
  continue;
1646
1751
  }
1647
1752
  const identity = `${chord.rootPc}:${chord.quality}`;
@@ -1669,6 +1774,16 @@ function stackedTriad(degree, key) {
1669
1774
  }
1670
1775
 
1671
1776
  // src/analyze/form/internal.ts
1777
+ function barSpanOf(startBeat, endBeat, meter) {
1778
+ const firstBar = barIndexAt(startBeat, meter);
1779
+ const lastBar = barIndexAt(endBeat, meter);
1780
+ const partOf = (beat, bar) => {
1781
+ const barStart = barPositionToBeat({ bar, beat: 0 }, meter);
1782
+ const barLength = beatsPerBarAt(barStart, meter);
1783
+ return barLength > 0 ? (beat - barStart) / barLength : 0;
1784
+ };
1785
+ return lastBar - firstBar + partOf(endBeat, lastBar) - partOf(startBeat, firstBar);
1786
+ }
1672
1787
  function clamp01(value) {
1673
1788
  return Math.min(1, Math.max(0, value));
1674
1789
  }
@@ -1682,6 +1797,14 @@ function firstSoundingBeat(notes, fallback = 0) {
1682
1797
  }
1683
1798
  return Number.isFinite(first) ? first : fallback;
1684
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
+ }
1685
1808
  function lastBarOf(meter, spanStart, spanEnd) {
1686
1809
  const firstBar = barIndexAt(spanStart, meter);
1687
1810
  const bar = barIndexAt(Math.max(spanStart, spanEnd), meter);
@@ -1763,6 +1886,16 @@ function gridOriginOf(firstOnset, slotBeats) {
1763
1886
  function gridForNotes(notes, slotBeats) {
1764
1887
  return gridOriginOf(firstSoundingBeat(notes), slotBeats);
1765
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
+ }
1766
1899
  function barGridStart(firstOnset, meter) {
1767
1900
  return gridOriginOf(firstOnset, beatsPerBarAt(firstOnset, meter)).startBeat;
1768
1901
  }
@@ -2053,9 +2186,13 @@ function keyTimelineFromNotes(notes, opts = {}) {
2053
2186
  const budget = opts.budget;
2054
2187
  assertNoteEvents(notes, "key timeline notes", { allowNonPositiveDuration: true, budget });
2055
2188
  const sounding = notes.filter((note) => note.durationBeat > 0);
2056
- const lastEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
2057
- const totalBeats = opts.totalBeats ?? lastEnd;
2058
- 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
+ );
2059
2196
  const slotBeats = Math.min(minKeyBeats, expectedKeyBeats);
2060
2197
  const { origin, startBeat: musicStart } = gridForNotes(sounding, slotBeats);
2061
2198
  const slotCount = Math.max(0, Math.ceil((totalBeats - origin) / slotBeats - BEAT_EPS));
@@ -2191,9 +2328,10 @@ function detectModulations(chords, opts = {}) {
2191
2328
  opts.budget
2192
2329
  );
2193
2330
  const sorted = [...chords].filter((segment) => segment.endBeat > segment.startBeat).sort((a, b) => a.startBeat - b.startBeat);
2194
- 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);
2195
2332
  const totalBeats = opts.totalBeats ?? lastChordEnd;
2196
- 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");
2197
2335
  const ordered = sorted.map((segment) => ({ ...segment, endBeat: Math.min(segment.endBeat, totalBeats) })).filter((segment) => segment.endBeat > segment.startBeat);
2198
2336
  if (ordered.length === 0) {
2199
2337
  return [];
@@ -2390,7 +2528,7 @@ function prevailingKeyOf(regions) {
2390
2528
  // src/analyze/voice/index.ts
2391
2529
  function keyScaleAt(key) {
2392
2530
  if (typeof key === "function") {
2393
- return key;
2531
+ return (beat) => toKeyScale(key(beat));
2394
2532
  }
2395
2533
  const scale = toKeyScale(key);
2396
2534
  return () => scale;
@@ -2419,7 +2557,8 @@ var TENSION_DEGREE_BY_INTERVAL = {
2419
2557
  8: 13,
2420
2558
  9: 13
2421
2559
  };
2422
- 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);
2423
2562
  function tensionDegree(ic) {
2424
2563
  return TENSION_DEGREE_BY_INTERVAL[ic];
2425
2564
  }
@@ -2434,11 +2573,22 @@ function isStep(a, b) {
2434
2573
  return d === 1 || d === 2;
2435
2574
  }
2436
2575
  var LEAP_SEMITONES = 3;
2437
- 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) {
2438
2588
  if (delta > 0) {
2439
2589
  return ic === 1 || ic === 2 ? "sus2-3" : null;
2440
2590
  }
2441
- if (ic === 2) {
2591
+ if (ic === 1 || ic === 2) {
2442
2592
  return "sus9-8";
2443
2593
  }
2444
2594
  if (ic === 10 || ic === 11) {
@@ -2452,6 +2602,18 @@ function suspensionType(ic, delta) {
2452
2602
  }
2453
2603
  return null;
2454
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
+ }
2455
2617
  var STEP_RESOLUTION = 2;
2456
2618
  var NEAREST_CHORD_TONE_REACH = 12;
2457
2619
  function nearestChordTone(pitch, chord, maxDistance) {
@@ -2471,17 +2633,20 @@ function analyzeVoice(voice, chordAtBeat2, key, otherVoicesAtBeat = () => []) {
2471
2633
  assertFunction(otherVoicesAtBeat, "otherVoicesAtBeat");
2472
2634
  const keyAt = keyScaleAt(key);
2473
2635
  const result = [];
2636
+ const sounding = soundingIndices(voice);
2637
+ const line = new Map(sounding.map((index, position) => [index, position]));
2474
2638
  for (let i = 0; i < voice.length; i += 1) {
2475
2639
  const note = voice[i];
2476
2640
  if (!note) {
2477
2641
  continue;
2478
2642
  }
2479
- const prevNote = i > 0 ? voice[i - 1] : void 0;
2480
- const nextNote = i + 1 < voice.length ? voice[i + 1] : void 0;
2481
- const prevEnd = prevNote === void 0 ? Number.NEGATIVE_INFINITY : prevNote.startBeat + prevNote.durationBeat;
2482
- const noteEnd = note.startBeat + note.durationBeat;
2483
- const prev = prevNote !== void 0 && adjacent(prevEnd, note.startBeat) ? prevNote : void 0;
2484
- 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;
2485
2650
  const chord = chordAtBeat2(note.startBeat);
2486
2651
  const labels = [];
2487
2652
  const member = isChordMember(note.pitch, chord);
@@ -2508,12 +2673,11 @@ function analyzeVoice(voice, chordAtBeat2, key, otherVoicesAtBeat = () => []) {
2508
2673
  const prepared = prev.pitch === note.pitch && isChordMember(prev.pitch, prevChord);
2509
2674
  const resolves = isStep(next.pitch, note.pitch);
2510
2675
  const others = otherVoicesAtBeat(note.startBeat);
2511
- const verticallyDissonant = others.some(
2512
- (ov) => createsVerticalDissonance(note.pitch, ov.pitch, true)
2513
- );
2676
+ const verticallyDissonant = others.length > 0 ? others.some((ov) => createsVerticalDissonance(note.pitch, ov.pitch, true)) : clashesWithChord(note.pitch, chord);
2514
2677
  if (prepared && verticallyDissonant && resolves) {
2515
2678
  const ic = intervalAboveBass(note.pitch, others, chord);
2516
- 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);
2517
2681
  if (type !== null) {
2518
2682
  labels.push({ kind: "suspension", type, resolveTo: next.pitch });
2519
2683
  handled = true;
@@ -2643,8 +2807,34 @@ function assertChordTimeline(timeline, name = "timeline") {
2643
2807
  assertFiniteNumber(segment.startBeat, `${name}.segments[${index}].startBeat`);
2644
2808
  assertFiniteNumber(segment.endBeat, `${name}.segments[${index}].endBeat`);
2645
2809
  }
2810
+ assertSpansInOrder(segments, `${name}.segments`);
2646
2811
  return read;
2647
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
+ }
2648
2838
  function chordTimelineFromChords(chords, totalBeats) {
2649
2839
  assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, "timeline totalBeats");
2650
2840
  assertArray(chords, "chords");
@@ -2709,6 +2899,7 @@ var EXTRA_TONE_PENALTY = 0.6;
2709
2899
  var OMITTED_TONE_PENALTY = 0.15;
2710
2900
  var BASS_ROOT_STANDING = 0.95;
2711
2901
  var OCTAVE = 12;
2902
+ var CHORD_SEGMENTATIONS = ["dynamic", "grid"];
2712
2903
  function scoreMatch(match, weights, maxWeight, key, pedalPc) {
2713
2904
  const rootWeight = maxWeight > 0 ? (weights[match.rootPc] ?? 0) / maxWeight : 0;
2714
2905
  const rootStanding = match.inversion === 0 ? Math.max(rootWeight, BASS_ROOT_STANDING) : rootWeight;
@@ -2725,7 +2916,10 @@ function scoreMatch(match, weights, maxWeight, key, pedalPc) {
2725
2916
  score -= OMITTED_TONE_PENALTY * account.missing;
2726
2917
  return score;
2727
2918
  }
2728
- 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
+ );
2729
2923
  function isSlashReading(match, pedalPc) {
2730
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);
2731
2925
  }
@@ -2757,6 +2951,30 @@ function bassSoundsOnlyBelow(notes, windowStart, windowEnd, bassPc) {
2757
2951
  }
2758
2952
  return highestBass <= upperFloor;
2759
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
+ }
2760
2978
  function chordConfidence(chord, weights, totalWeight, exact) {
2761
2979
  const tones = new Set(chordPitchClasses(chord));
2762
2980
  let chordWeight = 0;
@@ -2769,23 +2987,20 @@ function chordConfidence(chord, weights, totalWeight, exact) {
2769
2987
  const confidence = Math.min(1, Math.max(0, raw));
2770
2988
  return exact ? confidence : confidence * INEXACT_CONFIDENCE_FACTOR;
2771
2989
  }
2772
- function analyzeWindow(notes, windowStart, windowEnd, meters, key) {
2773
- const { weights, totalWeight, maxWeight, lowestPitch } = windowWeights(
2774
- notes,
2775
- windowStart,
2776
- windowEnd,
2777
- meters
2778
- );
2990
+ function analyzeWindow(notes, windowStart, windowEnd, meters, key, harmonicRhythm) {
2991
+ const { weights, totalWeight, maxWeight } = windowWeights(notes, windowStart, windowEnd, meters);
2779
2992
  if (totalWeight <= BEAT_EPS) {
2780
2993
  return null;
2781
2994
  }
2782
2995
  const ranked = weights.map((weight, pc) => ({ pc, weight })).filter(({ weight }) => weight > 0).sort((a, b) => b.weight - a.weight || a.pc - b.pc);
2783
- const selected = ranked.filter(({ weight }) => weight >= maxWeight * NOISE_THRESHOLD_RATIO).slice(0, MAX_DETECTION_PCS).map(({ pc }) => pc);
2784
- const bassPc = pitchClassOf(lowestPitch);
2785
- const hasBass = selected.includes(bassPc);
2786
- const detectionPitches = hasBass ? selected.map((pc) => pc === bassPc ? pc : pc + 12) : selected;
2787
- const pedalPc = hasBass && bassSoundsOnlyBelow(notes, windowStart, windowEnd, bassPc) ? bassPc : null;
2788
- 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;
2789
3004
  let bestMatch = null;
2790
3005
  let bestScore = Number.NEGATIVE_INFINITY;
2791
3006
  for (const match of matches) {
@@ -2802,10 +3017,26 @@ function analyzeWindow(notes, windowStart, windowEnd, meters, key) {
2802
3017
  makeChord(bestMatch.rootPc, bestMatch.quality),
2803
3018
  pedalPc
2804
3019
  );
3020
+ const explained = account.exact || isSlashReading(bestMatch, pedalPc);
2805
3021
  const reading = {
2806
3022
  chord: shared(chord),
2807
- confidence: chordConfidence(chord, weights, totalWeight, account.exact)
3023
+ confidence: chordConfidence(chord, weights, totalWeight, explained)
2808
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
+ }
2809
3040
  const augmented = hasBass ? augmentedSixthFromPitchClasses(selected, bassPc, key) : null;
2810
3041
  if (augmented !== null) {
2811
3042
  reading.augmented = {
@@ -2817,6 +3048,13 @@ function analyzeWindow(notes, windowStart, windowEnd, meters, key) {
2817
3048
  }
2818
3049
  return null;
2819
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
+ }
2820
3058
  function shared(chord) {
2821
3059
  Object.freeze(chord.intervals);
2822
3060
  return Object.freeze(chord);
@@ -2843,6 +3081,29 @@ function resolvesOntoDominant(readings, index, key) {
2843
3081
  }
2844
3082
  return true;
2845
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
+ }
2846
3107
  function sameChord(a, b) {
2847
3108
  return a.rootPc === b.rootPc && a.quality === b.quality && a.bassPc === b.bassPc;
2848
3109
  }
@@ -3091,7 +3352,11 @@ function analyzeTimeline(notes, opts = {}, previous, dirty) {
3091
3352
  const meters = resolveMeters(opts, "timeline meters");
3092
3353
  const harmonicRhythm = opts.harmonicRhythm ?? beatsPerBarAt(0, meters);
3093
3354
  assertRange(harmonicRhythm, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "harmonic rhythm");
3094
- const segmentation = opts.segmentation ?? "dynamic";
3355
+ const segmentation = assertOneOf(
3356
+ opts.segmentation ?? "dynamic",
3357
+ CHORD_SEGMENTATIONS,
3358
+ "timeline segmentation"
3359
+ );
3095
3360
  const gridUnit = metricGridUnit(meters);
3096
3361
  const minChordBeats = opts.minChordBeats ?? gridUnit;
3097
3362
  assertRange(minChordBeats, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "minChordBeats");
@@ -3103,9 +3368,13 @@ function analyzeTimeline(notes, opts = {}, previous, dirty) {
3103
3368
  }
3104
3369
  assertNoteEvents(notes, "timeline notes", noteOptions);
3105
3370
  const sounding = sortedNoteEvents(notes.filter((note) => note.durationBeat > 0));
3106
- const lastNoteEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
3107
- const totalBeats = opts.totalBeats ?? lastNoteEnd;
3108
- 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
+ );
3109
3378
  const slotBeats = segmentation === "grid" ? harmonicRhythm : slotBeatsWithin(gridUnit, minChordBeats, harmonicRhythm);
3110
3379
  const { origin, startBeat: musicStart } = gridForNotes(sounding, slotBeats);
3111
3380
  const grid = { origin, slotBeats };
@@ -3116,14 +3385,7 @@ function analyzeTimeline(notes, opts = {}, previous, dirty) {
3116
3385
  "timeline windows",
3117
3386
  budget
3118
3387
  );
3119
- const keys = opts.key !== void 0 ? [
3120
- {
3121
- startBeat: musicStart,
3122
- endBeat: totalBeats,
3123
- key: resolveKey(opts.key),
3124
- confidence: 1
3125
- }
3126
- ] : keyTimelineFromNotes(sounding, { meters, totalBeats, budget });
3388
+ const keys = opts.key !== void 0 ? statedKeyRegions(resolveKey(opts.key), musicStart, totalBeats) : keyTimelineFromNotes(sounding, { meters, totalBeats, budget });
3127
3389
  const prevailingKey = prevailingKeyOf(keys) ?? resolveKey(majorKey(0));
3128
3390
  const keyAt = keyLookup(keys, prevailingKey);
3129
3391
  const scaleAt = (beat) => scaleOf(keyAt(beat));
@@ -3179,12 +3441,27 @@ function analyzeTimeline(notes, opts = {}, previous, dirty) {
3179
3441
  const spanKeyWhole = keyAt(start);
3180
3442
  const id = windowId(start, end, spanKey);
3181
3443
  const cached = carried !== void 0 && spanIsClean(grid, span, dirtyFrom, dirtyTo) ? carried.windows.get(id) : void 0;
3182
- 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
+ );
3183
3452
  windows.set(id, inferred);
3184
3453
  readings.push(inferred);
3185
3454
  readingKeys.push(spanKey);
3186
3455
  readingBounds.push({ start, end });
3187
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
+ }
3188
3465
  for (let i = 0; i < readings.length; i += 1) {
3189
3466
  const offered = readings[i]?.augmented;
3190
3467
  const spanKey = readingKeys[i];
@@ -3267,63 +3544,1970 @@ function detectCadences(timeline, key) {
3267
3544
  return hits;
3268
3545
  }
3269
3546
 
3270
- export {
3271
- HUMANIZE_ADJACENCY,
3272
- sameInstant,
3273
- adjacent,
3274
- hasEnded,
3275
- windowWeights,
3276
- MODAL_SCALE_NAMES,
3277
- detectChord,
3278
- detectChordBest,
3279
- detectKey,
3280
- detectKeyFromNotes,
3281
- detectKeyBest,
3282
- augmentedSixthChord,
3283
- augmentedSixthKind,
3284
- augmentedSixthFromPitchClasses,
3285
- spellAugmentedSixth,
3286
- isCadentialSixFour,
3287
- detectCadence,
3288
- isBorrowedChord,
3289
- borrowedSource,
3290
- isAppliedDominant,
3291
- romanToChord,
3292
- chordToRoman,
3293
- explainRoman,
3294
- isMinorKey,
3295
- functionOf,
3296
- isDiatonic,
3297
- parallelKey,
3298
- analyzeChord,
3299
- secondaryDominant,
3300
- secondaryDominantOf,
3301
- pivotChords,
3302
- clamp01,
3303
- floorMod,
3304
- firstSoundingBeat,
3305
- lastBarOf,
3306
- sliceBars,
3307
- weightSimilarity,
3308
- harmonicNovelty,
3309
- combineEvidence,
3547
+ // src/analyze/melody/internal.ts
3548
+ var RHYTHM_GRID = 64;
3549
+ function phraseNotes(phrase) {
3550
+ if (Array.isArray(phrase)) {
3551
+ return phrase;
3552
+ }
3553
+ return assertRecord(phrase, "melodic phrase").notes;
3554
+ }
3555
+ function orderedNotes(notes, name, budget) {
3556
+ assertNoteEvents(notes, name, { allowNonPositiveDuration: true, budget });
3557
+ const sounding = notes.filter((note) => note.durationBeat > 0).sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
3558
+ const line = [];
3559
+ for (const note of sounding) {
3560
+ const previous = line[line.length - 1];
3561
+ if (previous !== void 0 && Math.abs(note.startBeat - previous.startBeat) <= BEAT_EPS) {
3562
+ line[line.length - 1] = note;
3563
+ continue;
3564
+ }
3565
+ line.push(note);
3566
+ }
3567
+ return line;
3568
+ }
3569
+ function intervalsOf(notes) {
3570
+ const out = [];
3571
+ for (let i = 1; i < notes.length; i += 1) {
3572
+ out.push((notes[i]?.pitch ?? 0) - (notes[i - 1]?.pitch ?? 0));
3573
+ }
3574
+ return out;
3575
+ }
3576
+ function onsetGaps(notes) {
3577
+ const out = [];
3578
+ for (let i = 1; i < notes.length; i += 1) {
3579
+ out.push((notes[i]?.startBeat ?? 0) - (notes[i - 1]?.startBeat ?? 0));
3580
+ }
3581
+ return out;
3582
+ }
3583
+ function retrogradeGaps(notes) {
3584
+ const out = [];
3585
+ for (let i = notes.length - 1; i >= 1; i -= 1) {
3586
+ const later = notes[i];
3587
+ const earlier = notes[i - 1];
3588
+ out.push(
3589
+ (later?.startBeat ?? 0) + (later?.durationBeat ?? 0) - ((earlier?.startBeat ?? 0) + (earlier?.durationBeat ?? 0))
3590
+ );
3591
+ }
3592
+ return out;
3593
+ }
3594
+ function rhythmProfile(gaps) {
3595
+ const unit = gaps[0] ?? 0;
3596
+ if (!(unit > BEAT_EPS)) {
3597
+ return gaps.length === 0 ? [] : null;
3598
+ }
3599
+ return gaps.map((gap) => Math.round(gap / unit * RHYTHM_GRID) / RHYTHM_GRID);
3600
+ }
3601
+ function sameNumbers(a, b) {
3602
+ if (a.length !== b.length) {
3603
+ return false;
3604
+ }
3605
+ for (let i = 0; i < a.length; i += 1) {
3606
+ if (Math.abs((a[i] ?? 0) - (b[i] ?? 0)) > BEAT_EPS) {
3607
+ return false;
3608
+ }
3609
+ }
3610
+ return true;
3611
+ }
3612
+ function roundTo(value, digits) {
3613
+ const scale = 10 ** digits;
3614
+ return Math.round(value * scale) / scale;
3615
+ }
3616
+ function endBeatOf(notes) {
3617
+ return latestSoundingEnd(notes);
3618
+ }
3619
+ function onsetSpan(notes) {
3620
+ if (notes.length < 2) {
3621
+ return 0;
3622
+ }
3623
+ return (notes[notes.length - 1]?.startBeat ?? 0) - (notes[0]?.startBeat ?? 0);
3624
+ }
3625
+
3626
+ // src/analyze/melody/contour.ts
3627
+ var DIRECTIONAL_SHARE = 0.75;
3628
+ var MELODIC_CONTOUR_SHAPES = Object.freeze([
3629
+ "arch",
3630
+ "ascending",
3631
+ "descending",
3632
+ "wave",
3633
+ "static"
3634
+ ]);
3635
+ function extremes(notes) {
3636
+ if (notes.length === 0) {
3637
+ return { peak: -1, trough: -1 };
3638
+ }
3639
+ let peak = 0;
3640
+ let trough = 0;
3641
+ for (let i = 1; i < notes.length; i += 1) {
3642
+ if ((notes[i]?.pitch ?? 0) > (notes[peak]?.pitch ?? 0)) {
3643
+ peak = i;
3644
+ }
3645
+ if ((notes[i]?.pitch ?? 0) < (notes[trough]?.pitch ?? 0)) {
3646
+ trough = i;
3647
+ }
3648
+ }
3649
+ return { peak, trough };
3650
+ }
3651
+ function melodicContour(notes) {
3652
+ const line = orderedNotes(phraseNotes(notes), "contour notes");
3653
+ const intervals = intervalsOf(line);
3654
+ const directions = intervals.map(
3655
+ (step) => step > 0 ? "up" : step < 0 ? "down" : "same"
3656
+ );
3657
+ const { peak, trough } = extremes(line);
3658
+ const range = (line[peak]?.pitch ?? 0) - (line[trough]?.pitch ?? 0);
3659
+ const motion = intervals.reduce((sum, step) => sum + Math.abs(step), 0);
3660
+ if (motion === 0) {
3661
+ return {
3662
+ directions,
3663
+ shape: "static",
3664
+ peakIndex: peak,
3665
+ troughIndex: trough,
3666
+ range: 0,
3667
+ rationale: line.length < 2 ? "Too short to have a direction, so the line is static" : "Every note at the same pitch, so the line is static"
3668
+ };
3669
+ }
3670
+ const first = line[0]?.pitch ?? 0;
3671
+ const last = line[line.length - 1]?.pitch ?? 0;
3672
+ const net = last - first;
3673
+ const directness = Math.abs(net) / motion;
3674
+ const share = Math.round(directness * 100);
3675
+ if (directness >= DIRECTIONAL_SHARE) {
3676
+ const shape = net > 0 ? "ascending" : "descending";
3677
+ return {
3678
+ directions,
3679
+ shape,
3680
+ peakIndex: peak,
3681
+ troughIndex: trough,
3682
+ range,
3683
+ rationale: `The net ${net > 0 ? "rise" : "fall"} is ${share}% of the motion, so the line is ${shape}`
3684
+ };
3685
+ }
3686
+ const peakPitch = line[peak]?.pitch ?? 0;
3687
+ const archMotion = peakPitch - first + (peakPitch - last);
3688
+ if (peak > 0 && peak < line.length - 1 && archMotion >= DIRECTIONAL_SHARE * motion) {
3689
+ return {
3690
+ directions,
3691
+ shape: "arch",
3692
+ peakIndex: peak,
3693
+ troughIndex: trough,
3694
+ range,
3695
+ rationale: `Rises to its peak at note ${peak + 1} of ${line.length} and falls back, so the line is an arch`
3696
+ };
3697
+ }
3698
+ return {
3699
+ directions,
3700
+ shape: "wave",
3701
+ peakIndex: peak,
3702
+ troughIndex: trough,
3703
+ range,
3704
+ rationale: `Changes direction without settling either way (the net movement is ${share}% of the motion), so the line is a wave`
3705
+ };
3706
+ }
3707
+
3708
+ // src/analyze/melody/motifs.ts
3709
+ var MIN_CELL_NOTES = 3;
3710
+ var MAX_CELL_NOTES = 8;
3711
+ var MIN_OCCURRENCES = 2;
3712
+ function windowSignature(notes) {
3713
+ const profile = rhythmProfile(onsetGaps(notes));
3714
+ if (profile === null) {
3715
+ return null;
3716
+ }
3717
+ return `${notes.length}|${intervalsOf(notes).join(",")}|${profile.join(",")}`;
3718
+ }
3719
+ function nonOverlapping(starts, length) {
3720
+ const picked = [];
3721
+ let lastEnd = Number.NEGATIVE_INFINITY;
3722
+ for (const start of starts) {
3723
+ if (start > lastEnd) {
3724
+ picked.push(start);
3725
+ lastEnd = start + length - 1;
3726
+ }
3727
+ }
3728
+ return picked;
3729
+ }
3730
+ function subsumes(outer, inner) {
3731
+ let at = 0;
3732
+ for (const start of inner.starts) {
3733
+ while (at + 1 < outer.starts.length && (outer.starts[at + 1] ?? 0) <= start) {
3734
+ at += 1;
3735
+ }
3736
+ const from = outer.starts[at];
3737
+ if (from === void 0 || start < from || start + inner.length > from + outer.length) {
3738
+ return false;
3739
+ }
3740
+ }
3741
+ return true;
3742
+ }
3743
+ function listBeats(occurrences) {
3744
+ const shown = occurrences.slice(0, 5).map((occurrence) => roundTo(occurrence.startBeat, 3));
3745
+ return occurrences.length > shown.length ? `${shown.join(", ")}, and ${occurrences.length - shown.length} more` : shown.join(", ");
3746
+ }
3747
+ function motifFromGroup(group, notes) {
3748
+ const first = group.starts[0] ?? 0;
3749
+ const prime = notes.slice(first, first + group.length).map((note) => ({ ...note }));
3750
+ const primePitch = prime[0]?.pitch ?? 0;
3751
+ const primeSpan = onsetSpan(prime);
3752
+ const occurrences = group.starts.map((start) => {
3753
+ const statement = notes.slice(start, start + group.length);
3754
+ const span = onsetSpan(statement);
3755
+ return {
3756
+ noteIndex: start,
3757
+ startBeat: statement[0]?.startBeat ?? 0,
3758
+ endBeat: endBeatOf(statement),
3759
+ transpose: (statement[0]?.pitch ?? 0) - primePitch,
3760
+ timeRatio: primeSpan > BEAT_EPS ? span / primeSpan : 1
3761
+ };
3762
+ });
3763
+ const shape = melodicContour(prime).shape;
3764
+ const transposed = occurrences.some((occurrence) => occurrence.transpose !== 0);
3765
+ const stretched = occurrences.some((occurrence) => Math.abs(occurrence.timeRatio - 1) > BEAT_EPS);
3766
+ const detail = [
3767
+ transposed ? "at more than one pitch level" : "at the same pitch level",
3768
+ stretched ? "and in more than one set of note values" : ""
3769
+ ].filter((part) => part !== "").join(" ");
3770
+ return {
3771
+ notes: prime,
3772
+ intervals: intervalsOf(prime),
3773
+ rhythm: rhythmProfile(onsetGaps(prime)) ?? [],
3774
+ occurrences,
3775
+ rationale: `${group.length}-note ${shape} cell stated ${occurrences.length} ${occurrences.length === 1 ? "time" : "times"} (beats ${listBeats(occurrences)}), ${detail}`
3776
+ };
3777
+ }
3778
+ function extractMotifs(notes, opts = {}) {
3779
+ const asked = assertOptions(opts, "opts");
3780
+ const minNotes = assertInteger(asked.minNotes ?? MIN_CELL_NOTES, "motif minNotes", 2, 64);
3781
+ const maxNotes = assertInteger(asked.maxNotes ?? MAX_CELL_NOTES, "motif maxNotes", 2, 64);
3782
+ if (maxNotes < minNotes) {
3783
+ throw new InvalidInputError(
3784
+ `motif maxNotes must be at least minNotes ${minNotes}; received ${maxNotes}`
3785
+ );
3786
+ }
3787
+ const minOccurrences = assertInteger(
3788
+ asked.minOccurrences ?? MIN_OCCURRENCES,
3789
+ "motif minOccurrences",
3790
+ 2
3791
+ );
3792
+ const sounding = orderedNotes(notes, "melody notes", asked.budget);
3793
+ if (sounding.length < minNotes) {
3794
+ return [];
3795
+ }
3796
+ assertGenerationBudget(
3797
+ sounding.length * (maxNotes - minNotes + 1),
3798
+ "motif windows",
3799
+ asked.budget
3800
+ );
3801
+ const groups = /* @__PURE__ */ new Map();
3802
+ for (let length = minNotes; length <= maxNotes; length += 1) {
3803
+ for (let start = 0; start + length <= sounding.length; start += 1) {
3804
+ const signature = windowSignature(sounding.slice(start, start + length));
3805
+ if (signature === null) {
3806
+ continue;
3807
+ }
3808
+ const group = groups.get(signature);
3809
+ if (group === void 0) {
3810
+ groups.set(signature, { signature, length, starts: [start] });
3811
+ } else {
3812
+ group.starts.push(start);
3813
+ }
3814
+ }
3815
+ }
3816
+ const candidates = [...groups.values()].map((group) => ({ ...group, starts: nonOverlapping(group.starts, group.length) })).filter((group) => group.starts.length >= minOccurrences).sort(
3817
+ (a, b) => b.length - a.length || b.starts.length - a.starts.length || (a.starts[0] ?? 0) - (b.starts[0] ?? 0) || (a.signature < b.signature ? -1 : a.signature > b.signature ? 1 : 0)
3818
+ );
3819
+ const keptByLength = /* @__PURE__ */ new Map();
3820
+ const kept = [];
3821
+ let comparisons = 0;
3822
+ for (const candidate of candidates) {
3823
+ const first = candidate.starts[0] ?? 0;
3824
+ let covered = false;
3825
+ for (let length = candidate.length + 1; length <= maxNotes && !covered; length += 1) {
3826
+ const byStart = keptByLength.get(length);
3827
+ if (byStart === void 0) {
3828
+ continue;
3829
+ }
3830
+ for (let from = Math.max(0, first - (length - candidate.length)); from <= first; from += 1) {
3831
+ comparisons += 1;
3832
+ const outer = byStart.get(from);
3833
+ if (outer === void 0 || outer.starts.length < candidate.starts.length) {
3834
+ continue;
3835
+ }
3836
+ comparisons += outer.starts.length + candidate.starts.length;
3837
+ if (subsumes(outer, candidate)) {
3838
+ covered = true;
3839
+ break;
3840
+ }
3841
+ }
3842
+ }
3843
+ assertGenerationBudget(comparisons, "motif subsumption comparisons", asked.budget);
3844
+ if (!covered) {
3845
+ kept.push(candidate);
3846
+ let byStart = keptByLength.get(candidate.length);
3847
+ if (byStart === void 0) {
3848
+ byStart = /* @__PURE__ */ new Map();
3849
+ keptByLength.set(candidate.length, byStart);
3850
+ }
3851
+ for (const start of candidate.starts) {
3852
+ byStart.set(start, candidate);
3853
+ }
3854
+ }
3855
+ }
3856
+ return kept.map((group) => motifFromGroup(group, sounding));
3857
+ }
3858
+ function motifFromNotes(notes) {
3859
+ const sounding = orderedNotes(notes, "motif notes").map((note) => ({ ...note }));
3860
+ return motifFromGroup(
3861
+ { signature: windowSignature(sounding) ?? "", length: sounding.length, starts: [0] },
3862
+ sounding
3863
+ );
3864
+ }
3865
+
3866
+ // src/analyze/melody/relation.ts
3867
+ var MOTIF_RELATION_KINDS = Object.freeze([
3868
+ "repetition",
3869
+ "transposition",
3870
+ "tonalTransposition",
3871
+ "inversion",
3872
+ "retrograde",
3873
+ "retrogradeInversion",
3874
+ "augmentation",
3875
+ "diminution"
3876
+ ]);
3877
+ function spellingOf(key) {
3878
+ if (key === void 0) {
3879
+ return "sharp";
3880
+ }
3881
+ return keySignatureFifths(key.tonic, key.scale) < 0 ? "flat" : "sharp";
3882
+ }
3883
+ function intervalBetween(from, to, key) {
3884
+ const start = midiToNote(from, spellingOf(key));
3885
+ return spelledInterval(start, transposeNote(start, to - from));
3886
+ }
3887
+ function intervalName(interval) {
3888
+ return `${interval.descending && interval.semitones !== 0 ? "-" : ""}${interval.quality}${interval.number}`;
3889
+ }
3890
+ function ladderIndices(notes, key) {
3891
+ const root = pitchClassOf(key.rootPc);
3892
+ const offsets = scaleTonesInDegreeOrder(key).map((pc) => pitchClassOf(pc - root));
3893
+ if (offsets.length === 0) {
3894
+ return null;
3895
+ }
3896
+ const out = [];
3897
+ for (const note of notes) {
3898
+ const semitones = note.pitch - root;
3899
+ const octave = Math.floor(semitones / 12);
3900
+ const degree = offsets.indexOf(semitones - octave * 12);
3901
+ if (degree < 0) {
3902
+ return null;
3903
+ }
3904
+ out.push(octave * offsets.length + degree);
3905
+ }
3906
+ return out;
3907
+ }
3908
+ function ladderSteps(indices) {
3909
+ const out = [];
3910
+ for (let i = 1; i < indices.length; i += 1) {
3911
+ out.push((indices[i] ?? 0) - (indices[i - 1] ?? 0));
3912
+ }
3913
+ return out;
3914
+ }
3915
+ function pitchShapes(a, b) {
3916
+ const shapes = [];
3917
+ if (sameNumbers(a, b)) {
3918
+ shapes.push("exact");
3919
+ }
3920
+ if (sameNumbers(
3921
+ b,
3922
+ a.map((step) => -step)
3923
+ )) {
3924
+ shapes.push("inversion");
3925
+ }
3926
+ const reversed = [...a].reverse();
3927
+ if (sameNumbers(
3928
+ b,
3929
+ reversed.map((step) => -step)
3930
+ )) {
3931
+ shapes.push("retrograde");
3932
+ }
3933
+ if (sameNumbers(b, reversed)) {
3934
+ shapes.push("retrogradeInversion");
3935
+ }
3936
+ return shapes;
3937
+ }
3938
+ function totalGap(gaps) {
3939
+ return gaps.reduce((sum, gap) => sum + gap, 0);
3940
+ }
3941
+ function stretchRatio(modelGaps, answerGaps) {
3942
+ const span = totalGap(modelGaps);
3943
+ return span > BEAT_EPS ? totalGap(answerGaps) / span : 1;
3944
+ }
3945
+ function stretchPhrase(ratio2) {
3946
+ return `note values ${roundTo(ratio2, 3)}x the model's`;
3947
+ }
3948
+ function relateMotifs(a, b, keyLike) {
3949
+ const key = keyLike === void 0 ? void 0 : resolveKey(keyLike);
3950
+ const model = assertArray(assertRecord(a, "a").notes, "a.notes");
3951
+ const answer = assertArray(assertRecord(b, "b").notes, "b.notes");
3952
+ if (model.length === 0 || model.length !== answer.length) {
3953
+ return null;
3954
+ }
3955
+ const modelGaps = onsetGaps(model);
3956
+ const answerGaps = onsetGaps(answer);
3957
+ const modelProfile = rhythmProfile(modelGaps);
3958
+ const answerProfile = rhythmProfile(answerGaps);
3959
+ const retroGaps = retrogradeGaps(model);
3960
+ const retroProfile = rhythmProfile(retroGaps);
3961
+ if (modelProfile === null || answerProfile === null) {
3962
+ return null;
3963
+ }
3964
+ const forward = sameNumbers(modelProfile, answerProfile);
3965
+ const backward = retroProfile !== null && sameNumbers(retroProfile, answerProfile);
3966
+ const semitones = (answer[0]?.pitch ?? 0) - (model[0]?.pitch ?? 0);
3967
+ const interval = intervalBetween(model[0]?.pitch ?? 0, answer[0]?.pitch ?? 0, key);
3968
+ const gap = (answer[0]?.startBeat ?? 0) - endBeatOf(model);
3969
+ const sequence = gap >= -HUMANIZE_ADJACENCY && gap <= HUMANIZE_ADJACENCY;
3970
+ const timeRatio = stretchRatio(modelGaps, answerGaps);
3971
+ const stretched = Math.abs(timeRatio - 1) > BEAT_EPS;
3972
+ const shapes = pitchShapes(intervalsOf(model), intervalsOf(answer));
3973
+ const base = { sequence, semitones, interval, timeRatio };
3974
+ const backwardBase = { ...base, timeRatio: stretchRatio(retroGaps, answerGaps) };
3975
+ if (forward && shapes.includes("exact")) {
3976
+ if (semitones === 0) {
3977
+ if (!stretched) {
3978
+ return {
3979
+ ...base,
3980
+ kind: "repetition",
3981
+ rationale: "Exact repetition: the same pitches in the same rhythm"
3982
+ };
3983
+ }
3984
+ const augmented = timeRatio > 1;
3985
+ return {
3986
+ ...base,
3987
+ kind: augmented ? "augmentation" : "diminution",
3988
+ rationale: `${augmented ? "Augmentation" : "Diminution"}: the same pitches in ${stretchPhrase(timeRatio)}`
3989
+ };
3990
+ }
3991
+ const name = intervalName(interval);
3992
+ return {
3993
+ ...base,
3994
+ kind: "transposition",
3995
+ rationale: sequence ? `Real sequence: the cell restated ${name} away with every interval kept literally` + (stretched ? `, in ${stretchPhrase(timeRatio)}` : "") : `Transposition by ${name}: every interval kept literally` + (stretched ? `, in ${stretchPhrase(timeRatio)}` : "")
3996
+ };
3997
+ }
3998
+ if (forward && key !== void 0) {
3999
+ const modelLadder = ladderIndices(model, key.scale);
4000
+ const answerLadder = ladderIndices(answer, key.scale);
4001
+ if (modelLadder !== null && answerLadder !== null) {
4002
+ const degrees = (answerLadder[0] ?? 0) - (modelLadder[0] ?? 0);
4003
+ if (degrees !== 0 && sameNumbers(ladderSteps(modelLadder), ladderSteps(answerLadder))) {
4004
+ const direction = degrees > 0 ? "higher" : "lower";
4005
+ const count = Math.abs(degrees);
4006
+ return {
4007
+ ...base,
4008
+ kind: "tonalTransposition",
4009
+ degrees,
4010
+ rationale: `${sequence ? "Tonal sequence" : "Tonal transposition"}: the cell restated ${count} scale ${count === 1 ? "degree" : "degrees"} ${direction} inside the key, so the key resizes its intervals \u2014 a real one would have kept them literal`
4011
+ };
4012
+ }
4013
+ }
4014
+ }
4015
+ const literalRetrograde = backward && shapes.includes("retrograde") && answer[0]?.pitch === model[model.length - 1]?.pitch;
4016
+ if (forward && shapes.includes("inversion") && !(literalRetrograde && semitones !== 0)) {
4017
+ return {
4018
+ ...base,
4019
+ kind: "inversion",
4020
+ rationale: `Inversion: every interval turned upside down${semitones === 0 ? " about the first note" : ""}` + (stretched ? `, in ${stretchPhrase(timeRatio)}` : "")
4021
+ };
4022
+ }
4023
+ if (backward && shapes.includes("retrograde")) {
4024
+ return {
4025
+ ...backwardBase,
4026
+ kind: "retrograde",
4027
+ rationale: "Retrograde: the cell read back to front, rhythm included"
4028
+ };
4029
+ }
4030
+ if (backward && shapes.includes("retrogradeInversion")) {
4031
+ return {
4032
+ ...backwardBase,
4033
+ kind: "retrogradeInversion",
4034
+ rationale: "Retrograde inversion: the cell read back to front and turned upside down"
4035
+ };
4036
+ }
4037
+ return null;
4038
+ }
4039
+
4040
+ // src/analyze/sequence.ts
4041
+ function gradedEditDistance(a, b, cost, gap = 1) {
4042
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i * gap);
4043
+ for (let i = 1; i <= a.length; i += 1) {
4044
+ const row = new Array(b.length + 1);
4045
+ row[0] = i * gap;
4046
+ for (let j = 1; j <= b.length; j += 1) {
4047
+ const ai = a[i - 1];
4048
+ const bj = b[j - 1];
4049
+ const substitution = (previous[j - 1] ?? 0) + cost(ai, bj);
4050
+ const deletion = (previous[j] ?? 0) + gap;
4051
+ const insertion = (row[j - 1] ?? 0) + gap;
4052
+ row[j] = Math.min(substitution, deletion, insertion);
4053
+ }
4054
+ previous = row;
4055
+ }
4056
+ return previous[b.length] ?? 0;
4057
+ }
4058
+ function distanceToSimilarity(distance, a, b) {
4059
+ const longest = Math.max(a, b);
4060
+ if (longest === 0) {
4061
+ return 1;
4062
+ }
4063
+ return Math.min(1, Math.max(0, 1 - distance / longest));
4064
+ }
4065
+
4066
+ // src/analyze/melody/similarity.ts
4067
+ var INTERVAL_TOLERANCE = 6;
4068
+ var PITCH_WEIGHT = 0.75;
4069
+ var RHYTHM_WEIGHT = 0.25;
4070
+ function intervalCost(x, y) {
4071
+ return Math.min(1, Math.abs(x - y) / INTERVAL_TOLERANCE);
4072
+ }
4073
+ function gapCost(x, y) {
4074
+ if (!(x > BEAT_EPS) || !(y > BEAT_EPS)) {
4075
+ return Math.abs(x - y) > BEAT_EPS ? 1 : 0;
4076
+ }
4077
+ return Math.min(1, Math.abs(Math.log2(x / y)));
4078
+ }
4079
+ function compareMelodies(a, b) {
4080
+ const left = orderedNotes(phraseNotes(a), "melody a");
4081
+ const right = orderedNotes(phraseNotes(b), "melody b");
4082
+ const leftIntervals = intervalsOf(left);
4083
+ const rightIntervals = intervalsOf(right);
4084
+ assertGenerationBudget(leftIntervals.length * rightIntervals.length, "melody alignment");
4085
+ const pitchSimilarity = distanceToSimilarity(
4086
+ gradedEditDistance(leftIntervals, rightIntervals, intervalCost),
4087
+ leftIntervals.length,
4088
+ rightIntervals.length
4089
+ );
4090
+ const leftGaps = rhythmProfile(onsetGaps(left)) ?? onsetGaps(left);
4091
+ const rightGaps = rhythmProfile(onsetGaps(right)) ?? onsetGaps(right);
4092
+ const rhythmSimilarity = distanceToSimilarity(
4093
+ gradedEditDistance(leftGaps, rightGaps, gapCost),
4094
+ leftGaps.length,
4095
+ rightGaps.length
4096
+ );
4097
+ const similarity = PITCH_WEIGHT * pitchSimilarity + RHYTHM_WEIGHT * rhythmSimilarity;
4098
+ return {
4099
+ similarity,
4100
+ pitchSimilarity,
4101
+ rhythmSimilarity,
4102
+ rationale: `Interval shapes ${Math.round(pitchSimilarity * 100)}% alike, rhythms ${Math.round(rhythmSimilarity * 100)}% alike, weighted ${PITCH_WEIGHT}/${RHYTHM_WEIGHT} into ${roundTo(similarity, 3)}`
4103
+ };
4104
+ }
4105
+ function melodicSimilarity(a, b) {
4106
+ return compareMelodies(a, b).similarity;
4107
+ }
4108
+
4109
+ // src/analyze/melody/graph.ts
4110
+ var DEFAULT_VARIATION_THRESHOLD = 0.75;
4111
+ function summarize(relation) {
4112
+ const { kind, sequence, semitones, degrees, timeRatio } = relation;
4113
+ return { kind, sequence, semitones, degrees, timeRatio };
4114
+ }
4115
+ function betterParent(candidate, current) {
4116
+ const candidateNamed = candidate.relation !== null;
4117
+ const currentNamed = current.relation !== null;
4118
+ if (candidateNamed !== currentNamed) {
4119
+ return candidateNamed;
4120
+ }
4121
+ if (candidate.similarity !== current.similarity) {
4122
+ return candidate.similarity > current.similarity;
4123
+ }
4124
+ if (candidate.startBeat !== current.startBeat) {
4125
+ return candidate.startBeat > current.startBeat;
4126
+ }
4127
+ return candidate.index < current.index;
4128
+ }
4129
+ function motifGraph(line, motifs, opts = {}) {
4130
+ const asked = assertOptions(opts, "opts");
4131
+ const variationThreshold = asked.variationThreshold ?? DEFAULT_VARIATION_THRESHOLD;
4132
+ assertRange(variationThreshold, 0, 1, "motif graph variationThreshold");
4133
+ const ordered = orderedNotes(line, "line", asked.budget);
4134
+ const motifList = assertArray(motifs, "motifs");
4135
+ const infos = [];
4136
+ for (let mi = 0; mi < motifList.length; mi += 1) {
4137
+ const motif = assertRecord(motifList[mi], `motifs[${mi}]`);
4138
+ const notes = assertArray(motif.notes, `motifs[${mi}].notes`);
4139
+ const occurrences = assertArray(
4140
+ motif.occurrences,
4141
+ `motifs[${mi}].occurrences`
4142
+ );
4143
+ for (let oi = 0; oi < occurrences.length; oi += 1) {
4144
+ const occurrence = assertRecord(
4145
+ occurrences[oi],
4146
+ `motifs[${mi}].occurrences[${oi}]`
4147
+ );
4148
+ const { noteIndex } = occurrence;
4149
+ const onLine = Number.isInteger(noteIndex) && noteIndex >= 0 && noteIndex + notes.length <= ordered.length && Math.abs((ordered[noteIndex]?.startBeat ?? Number.NaN) - occurrence.startBeat) <= BEAT_EPS;
4150
+ if (!onLine) {
4151
+ throw new InvalidInputError(
4152
+ `motif occurrence motifs[${mi}].occurrences[${oi}] does not lie on the line`
4153
+ );
4154
+ }
4155
+ infos.push({
4156
+ node: {
4157
+ motif: mi,
4158
+ occurrence: oi,
4159
+ startBeat: occurrence.startBeat,
4160
+ endBeat: occurrence.endBeat
4161
+ },
4162
+ data: motifFromNotes(ordered.slice(noteIndex, noteIndex + notes.length))
4163
+ });
4164
+ }
4165
+ }
4166
+ infos.sort((a, b) => a.node.startBeat - b.node.startBeat || a.node.motif - b.node.motif);
4167
+ const nodeCount = infos.length;
4168
+ assertGenerationBudget(nodeCount * nodeCount, "motif graph comparisons", asked.budget);
4169
+ const edges = [];
4170
+ for (let to = 1; to < nodeCount; to += 1) {
4171
+ const toInfo = infos[to];
4172
+ let best = null;
4173
+ for (let from = 0; from < to; from += 1) {
4174
+ const fromInfo = infos[from];
4175
+ const candidate = {
4176
+ index: from,
4177
+ startBeat: fromInfo.node.startBeat,
4178
+ relation: relateMotifs(fromInfo.data, toInfo.data, asked.key),
4179
+ similarity: melodicSimilarity(fromInfo.data, toInfo.data)
4180
+ };
4181
+ if (best === null || betterParent(candidate, best)) {
4182
+ best = candidate;
4183
+ }
4184
+ }
4185
+ if (best !== null && (best.relation !== null || best.similarity >= variationThreshold)) {
4186
+ edges.push({
4187
+ from: best.index,
4188
+ to,
4189
+ relation: best.relation === null ? null : summarize(best.relation),
4190
+ similarity: best.similarity
4191
+ });
4192
+ }
4193
+ }
4194
+ return { nodes: infos.map((info) => info.node), edges };
4195
+ }
4196
+
4197
+ // src/analyze/reduction/index.ts
4198
+ var REDUCTION_LEVELS = Object.freeze(["structural", "passing", "neighbor"]);
4199
+ var TONIC_ARRIVING_CADENCES = [
4200
+ "authentic",
4201
+ "plagal",
4202
+ "modal",
4203
+ "deceptive"
4204
+ ];
4205
+ var TONIC_ARRIVING = new Set(TONIC_ARRIVING_CADENCES);
4206
+ function signedStep(fromPc, toPc) {
4207
+ return (pitchClassOf(toPc) - pitchClassOf(fromPc) + 18) % 12 - 6;
4208
+ }
4209
+ function isStep2(distance) {
4210
+ const size = Math.abs(distance);
4211
+ return size === 1 || size === 2;
4212
+ }
4213
+ function touches(before, after) {
4214
+ return Math.abs(after.startBeat - before.endBeat) <= BEAT_EPS;
4215
+ }
4216
+ function salienceFloors(durations) {
4217
+ const order = durations.map((_, index) => index).sort((a, b) => {
4218
+ const left = durations[a] ?? 0;
4219
+ const right = durations[b] ?? 0;
4220
+ return left - right || a - b;
4221
+ });
4222
+ const rankOf = new Array(durations.length).fill(0);
4223
+ for (let rank = 0; rank < order.length; rank += 1) {
4224
+ rankOf[order[rank] ?? 0] = rank;
4225
+ }
4226
+ const floors = [];
4227
+ const remaining = durations.length - 1;
4228
+ for (let index = 0; index < durations.length; index += 1) {
4229
+ if (remaining <= 0) {
4230
+ floors.push(0);
4231
+ continue;
4232
+ }
4233
+ const skipped = rankOf[index] ?? 0;
4234
+ const at = (position) => durations[order[position < skipped ? position : position + 1] ?? 0] ?? 0;
4235
+ const middle = remaining >> 1;
4236
+ floors.push(remaining % 2 === 1 ? at(middle) : (at(middle - 1) + at(middle)) / 2);
4237
+ }
4238
+ return floors;
4239
+ }
4240
+ function isTonicChord(chord, key) {
4241
+ return pitchClassOf(chord.rootPc - key.rootPc) === 0 && !isAppliedDominant(chord, key);
4242
+ }
4243
+ function functionalReason(segment, before, after, keyAt) {
4244
+ const key = keyAt(segment.startBeat);
4245
+ if (after !== void 0 && isCadentialSixFour(segment.chord, after.chord, key)) {
4246
+ return { kind: "cadentialSixFour" };
4247
+ }
4248
+ if (isTonicChord(segment.chord, key)) {
4249
+ return { kind: "tonic" };
4250
+ }
4251
+ if (isDominantChordOf(segment.chord, key)) {
4252
+ return { kind: "dominant" };
4253
+ }
4254
+ if (before !== void 0) {
4255
+ const arrival = detectCadence(before.chord, segment.chord, key).type;
4256
+ if (arrival !== null) {
4257
+ return { kind: "cadenceArrival", cadence: arrival };
4258
+ }
4259
+ }
4260
+ if (after !== void 0) {
4261
+ const agent = detectCadence(segment.chord, after.chord, keyAt(after.startBeat)).type;
4262
+ if (agent !== null && TONIC_ARRIVING.has(agent)) {
4263
+ return { kind: "cadenceAgent", cadence: agent };
4264
+ }
4265
+ }
4266
+ return null;
4267
+ }
4268
+ function figureReason(segment, before, after) {
4269
+ if (before === void 0 || after === void 0) {
4270
+ return null;
4271
+ }
4272
+ const incoming = signedStep(before.chord.rootPc, segment.chord.rootPc);
4273
+ const outgoing = signedStep(segment.chord.rootPc, after.chord.rootPc);
4274
+ if (!isStep2(incoming) || !isStep2(outgoing)) {
4275
+ return null;
4276
+ }
4277
+ if (incoming > 0 === outgoing > 0) {
4278
+ return { kind: "passing" };
4279
+ }
4280
+ return pitchClassOf(before.chord.rootPc) === pitchClassOf(after.chord.rootPc) ? { kind: "neighbor" } : null;
4281
+ }
4282
+ function describe2(reason) {
4283
+ switch (reason.kind) {
4284
+ case "tonic":
4285
+ return "Structural: the tonic of the key, which the progression is heard against";
4286
+ case "dominant":
4287
+ return "Structural: the dominant of the key, the chord its tonic is approached from";
4288
+ case "cadentialSixFour":
4289
+ return "Structural: a tonic six-four on the bass of the dominant that follows, which prolongs that dominant rather than framing the tonic";
4290
+ case "cadenceArrival":
4291
+ return `Structural: the arrival of a ${reason.cadence} cadence`;
4292
+ case "cadenceAgent":
4293
+ return `Structural: the chord that drives a ${reason.cadence} cadence`;
4294
+ case "salient":
4295
+ return "Structural: it holds the harmony longer than the chords around it";
4296
+ case "first":
4297
+ return "Structural: the first chord of the progression frames it";
4298
+ case "last":
4299
+ return "Structural: the last chord of the progression frames it";
4300
+ case "unfigured":
4301
+ return "Structural: its root neither steps through to the next chord nor returns to the last, so it embellishes nothing";
4302
+ case "passing":
4303
+ return "Passing: its root steps in from the previous chord and on to the next in the same direction";
4304
+ case "neighbor":
4305
+ return "Neighbor: its root steps away from the surrounding harmony and returns to it";
4306
+ }
4307
+ }
4308
+ var BASIS_READINGS = {
4309
+ function: ({ segment, before, after, keyAt }) => functionalReason(segment, before, after, keyAt),
4310
+ duration: ({ segment, salienceFloor }) => segment.endBeat - segment.startBeat > salienceFloor ? { kind: "salient" } : null
4311
+ };
4312
+ var REDUCTION_BASES = Object.keys(BASIS_READINGS);
4313
+ function levelOf(reason) {
4314
+ if (reason.kind === "passing") {
4315
+ return "passing";
4316
+ }
4317
+ return reason.kind === "neighbor" ? "neighbor" : "structural";
4318
+ }
4319
+ function reduceProgression(timeline, key, opts = {}) {
4320
+ assertChordTimeline(timeline);
4321
+ const asked = assertOptions(opts, "opts");
4322
+ const keyAt = keyScaleAt(key);
4323
+ const basis = assertOneOf(asked.basis ?? "function", REDUCTION_BASES, "reduction basis");
4324
+ const reading = BASIS_READINGS[basis];
4325
+ const segments = timeline.segments;
4326
+ const floors = basis === "duration" ? salienceFloors(segments.map((segment) => segment.endBeat - segment.startBeat)) : [];
4327
+ const result = [];
4328
+ for (let index = 0; index < segments.length; index += 1) {
4329
+ const segment = segments[index];
4330
+ if (segment === void 0) {
4331
+ continue;
4332
+ }
4333
+ const previous = segments[index - 1];
4334
+ const following = segments[index + 1];
4335
+ const before = previous !== void 0 && touches(previous, segment) ? previous : void 0;
4336
+ const after = following !== void 0 && touches(segment, following) ? following : void 0;
4337
+ let reason = reading({ segment, before, after, keyAt, salienceFloor: floors[index] ?? 0 });
4338
+ if (reason === null && index === 0) {
4339
+ reason = { kind: "first" };
4340
+ }
4341
+ if (reason === null && index === segments.length - 1) {
4342
+ reason = { kind: "last" };
4343
+ }
4344
+ reason ??= figureReason(segment, before, after) ?? { kind: "unfigured" };
4345
+ result.push({
4346
+ chord: segment.chord,
4347
+ startBeat: segment.startBeat,
4348
+ endBeat: segment.endBeat,
4349
+ level: levelOf(reason),
4350
+ rationale: describe2(reason)
4351
+ });
4352
+ }
4353
+ return result;
4354
+ }
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
+
4361
+ // src/analyze/rhythm/internal.ts
4362
+ function isMultipleOf(value, unit) {
4363
+ if (unit <= 0) {
4364
+ return false;
4365
+ }
4366
+ const ratio2 = value / unit;
4367
+ return Math.abs(ratio2 - Math.round(ratio2)) < BEAT_EPS;
4368
+ }
4369
+ function rhythmLevel(beatInQuarters, meters) {
4370
+ const ts = meterAt(beatInQuarters, meters);
4371
+ const offset = beatInQuarters - barStartBeat(beatInQuarters, meters);
4372
+ const pulse = pulseBeats(ts);
4373
+ if (isMultipleOf(offset, pulse)) {
4374
+ return metricWeight(beatInQuarters, meters) + 2;
4375
+ }
4376
+ const subUnit = pulse / (isCompound(ts) ? 3 : 2);
4377
+ return isMultipleOf(offset, subUnit) ? 1 : 0;
4378
+ }
4379
+ var IOI_MIN_EXPONENT = -4;
4380
+ var IOI_MAX_EXPONENT = 4;
4381
+ var IOI_EXPONENT_STEP = 0.5;
4382
+ function ioiBinIndex(ioi) {
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);
4388
+ }
4389
+
4390
+ // src/analyze/rhythm/index.ts
4391
+ var RHYTHM_SLOTS_PER_BEAT = 12;
4392
+ var SLOT_WIDTH = 1 / RHYTHM_SLOTS_PER_BEAT;
4393
+ var RHYTHM_OFF_GRID_TOLERANCE = 1 / 32;
4394
+ var RHYTHM_IOI_BINS = Object.freeze(
4395
+ Array.from({ length: 17 }, (_, i) => 2 ** (-4 + i * 0.5))
4396
+ );
4397
+ var BUDGET_SLOTS_PER_BAR = 48;
4398
+ var RHYTHM_LEVEL_COUNT = 6;
4399
+ function emptyAnalysis() {
4400
+ return {
4401
+ startBeat: 0,
4402
+ endBeat: 0,
4403
+ bars: 0,
4404
+ onsets: 0,
4405
+ onsetDensity: 0,
4406
+ barOnsets: [],
4407
+ onsetLevels: new Array(RHYTHM_LEVEL_COUNT).fill(0),
4408
+ barPositions: [],
4409
+ interOnsetShares: new Array(RHYTHM_IOI_BINS.length).fill(0),
4410
+ restRatio: 0,
4411
+ syncopation: 0,
4412
+ offGridRatio: 0,
4413
+ rationale: "Nothing sounds, so there is no rhythm to place on a grid"
4414
+ };
4415
+ }
4416
+ function foldOnsets(sounding) {
4417
+ const sorted = [...sounding].sort((a, b) => a.startBeat - b.startBeat);
4418
+ const onsets = [];
4419
+ for (const note of sorted) {
4420
+ const noteEnd = note.startBeat + note.durationBeat;
4421
+ const current = onsets[onsets.length - 1];
4422
+ if (current !== void 0 && note.startBeat - current.beat <= HUMANIZE_ADJACENCY) {
4423
+ current.end = Math.max(current.end, noteEnd);
4424
+ } else {
4425
+ onsets.push({ beat: note.startBeat, end: noteEnd });
4426
+ }
4427
+ }
4428
+ return onsets;
4429
+ }
4430
+ function snapOnset(onset, meters) {
4431
+ const ts = meterAt(onset.beat, meters);
4432
+ const barStart = barStartBeat(onset.beat, meters);
4433
+ const offset = onset.beat - barStart;
4434
+ const slot = Math.round(offset / SLOT_WIDTH);
4435
+ const snapped = barStart + slot * SLOT_WIDTH;
4436
+ const offGrid = Math.abs(onset.beat - snapped) > RHYTHM_OFF_GRID_TOLERANCE;
4437
+ return { ...onset, ts, barStart, slot, snapped, level: rhythmLevel(snapped, meters), offGrid };
4438
+ }
4439
+ function tsKey(ts) {
4440
+ return ts.grouping === void 0 ? `${ts.numerator}/${ts.denominator}` : `${ts.numerator}/${ts.denominator}:${ts.grouping.join("+")}`;
4441
+ }
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) {
4460
+ const barStart = barPositionToBeat({ bar, beat: 0 }, meters);
4461
+ const ts = meterAt(barStart, meters);
4462
+ const barLength = beatsPerBarAt(barStart, meters);
4463
+ const pulse = pulseBeats(ts);
4464
+ const [used, other] = isCompound(ts) ? [3, 2] : [2, 3];
4465
+ const subUnit = pulse / used;
4466
+ const steps = Math.max(1, Math.round(barLength / 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);
4483
+ }
4484
+ function restRatioOf(sounding, windowStart, windowEnd) {
4485
+ const span = windowEnd - windowStart;
4486
+ if (span <= 0) {
4487
+ return 0;
4488
+ }
4489
+ const intervals = sounding.map((note) => [
4490
+ Math.max(windowStart, note.startBeat),
4491
+ Math.min(windowEnd, note.startBeat + note.durationBeat)
4492
+ ]).filter(([from, to]) => to > from).sort((a, b) => a[0] - b[0]);
4493
+ let covered = 0;
4494
+ let curStart = 0;
4495
+ let curEnd = Number.NEGATIVE_INFINITY;
4496
+ for (const [from, to] of intervals) {
4497
+ if (from - curEnd > HUMANIZE_ADJACENCY) {
4498
+ if (curEnd > Number.NEGATIVE_INFINITY) {
4499
+ covered += curEnd - curStart;
4500
+ }
4501
+ curStart = from;
4502
+ curEnd = to;
4503
+ } else {
4504
+ curEnd = Math.max(curEnd, to);
4505
+ }
4506
+ }
4507
+ if (curEnd > Number.NEGATIVE_INFINITY) {
4508
+ covered += curEnd - curStart;
4509
+ }
4510
+ return 1 - covered / span;
4511
+ }
4512
+ function syncopationOf(snapped, startBeat, endBeat, meters) {
4513
+ if (snapped.length === 0) {
4514
+ return 0;
4515
+ }
4516
+ const firstBar = barIndexAt(startBeat, meters);
4517
+ const lastBar = barIndexAt(endBeat, meters);
4518
+ const otherPulses = otherDivisionPulses(snapped, meters);
4519
+ const points = [];
4520
+ for (let bar = firstBar; bar <= lastBar; bar += 1) {
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);
4524
+ }
4525
+ }
4526
+ }
4527
+ const normalizer = points.reduce((sum, p) => sum + p.level, 0);
4528
+ if (normalizer <= 0) {
4529
+ return 0;
4530
+ }
4531
+ let numerator = 0;
4532
+ let ptr = 0;
4533
+ for (let i = 0; i < snapped.length; i += 1) {
4534
+ const onset = snapped[i];
4535
+ if (onset === void 0) {
4536
+ continue;
4537
+ }
4538
+ const next = snapped[i + 1];
4539
+ const windowEnd = next !== void 0 ? next.snapped : onset.end;
4540
+ while (ptr < points.length && (points[ptr]?.beat ?? Number.POSITIVE_INFINITY) <= onset.snapped + BEAT_EPS) {
4541
+ ptr += 1;
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;
4545
+ let scan = ptr;
4546
+ let maxLevel = 0;
4547
+ while (scan < points.length && (points[scan]?.beat ?? Number.POSITIVE_INFINITY) < windowEnd - BEAT_EPS) {
4548
+ maxLevel = Math.max(maxLevel, points[scan]?.level ?? 0);
4549
+ scan += 1;
4550
+ }
4551
+ if (maxLevel > onsetLevel) {
4552
+ numerator += maxLevel - onsetLevel;
4553
+ }
4554
+ ptr = scan;
4555
+ }
4556
+ return numerator / normalizer;
4557
+ }
4558
+ function analyzeRhythm(notes, opts = {}) {
4559
+ return rhythmOver(notes, opts, void 0);
4560
+ }
4561
+ function rhythmOver(notes, opts, windowStart) {
4562
+ const meters = resolveMeters(opts, "rhythm meters");
4563
+ assertNoteEvents(notes, "rhythm notes", { allowNonPositiveDuration: true, budget: opts.budget });
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
+ );
4572
+ if (sounding.length === 0) {
4573
+ return emptyAnalysis();
4574
+ }
4575
+ const bars = barSpanOf(startBeat, endBeat, meters);
4576
+ assertGenerationBudget(
4577
+ Math.ceil(bars) * BUDGET_SLOTS_PER_BAR,
4578
+ "rhythm analysis slots",
4579
+ opts.budget
4580
+ );
4581
+ const onsets = foldOnsets(sounding);
4582
+ const snapped = onsets.map((onset) => snapOnset(onset, meters));
4583
+ const onsetDensity = bars > 0 ? onsets.length / bars : 0;
4584
+ const firstBar = barIndexAt(startBeat, meters);
4585
+ const barOnsets = new Array(lastBarOf(meters, startBeat, endBeat) - firstBar + 1).fill(0);
4586
+ for (const onset of onsets) {
4587
+ const index = barIndexAt(onset.beat, meters) - firstBar;
4588
+ if (index >= 0 && index < barOnsets.length) {
4589
+ barOnsets[index] = (barOnsets[index] ?? 0) + 1;
4590
+ }
4591
+ }
4592
+ const onsetLevels = new Array(RHYTHM_LEVEL_COUNT).fill(0);
4593
+ let offGridCount = 0;
4594
+ for (const onset of snapped) {
4595
+ onsetLevels[onset.level] = (onsetLevels[onset.level] ?? 0) + 1;
4596
+ if (onset.offGrid) {
4597
+ offGridCount += 1;
4598
+ }
4599
+ }
4600
+ for (let i = 0; i < onsetLevels.length; i += 1) {
4601
+ onsetLevels[i] = (onsetLevels[i] ?? 0) / onsets.length;
4602
+ }
4603
+ const profiles = [];
4604
+ const profileIndex = /* @__PURE__ */ new Map();
4605
+ for (const onset of snapped) {
4606
+ const key = tsKey(onset.ts);
4607
+ let index = profileIndex.get(key);
4608
+ if (index === void 0) {
4609
+ index = profiles.length;
4610
+ profileIndex.set(key, index);
4611
+ const slotsPerBar = Math.ceil(beatsPerBar(onset.ts) * RHYTHM_SLOTS_PER_BEAT);
4612
+ profiles.push({
4613
+ ts: onset.ts,
4614
+ slotsPerBar,
4615
+ shares: new Array(slotsPerBar).fill(0),
4616
+ onsets: 0
4617
+ });
4618
+ }
4619
+ const profile = profiles[index];
4620
+ if (profile !== void 0) {
4621
+ const slotIndex = (onset.slot % profile.slotsPerBar + profile.slotsPerBar) % profile.slotsPerBar;
4622
+ profile.shares[slotIndex] = (profile.shares[slotIndex] ?? 0) + 1;
4623
+ profile.onsets += 1;
4624
+ }
4625
+ }
4626
+ for (const profile of profiles) {
4627
+ for (let i = 0; i < profile.shares.length; i += 1) {
4628
+ profile.shares[i] = (profile.shares[i] ?? 0) / profile.onsets;
4629
+ }
4630
+ }
4631
+ const interOnsetShares = new Array(RHYTHM_IOI_BINS.length).fill(0);
4632
+ for (let i = 0; i < onsets.length; i += 1) {
4633
+ const onset = onsets[i];
4634
+ if (onset === void 0) {
4635
+ continue;
4636
+ }
4637
+ const next = onsets[i + 1];
4638
+ const ioi = next !== void 0 ? next.beat - onset.beat : onset.end - onset.beat;
4639
+ const bin = ioiBinIndex(ioi);
4640
+ interOnsetShares[bin] = (interOnsetShares[bin] ?? 0) + 1;
4641
+ }
4642
+ for (let i = 0; i < interOnsetShares.length; i += 1) {
4643
+ interOnsetShares[i] = (interOnsetShares[i] ?? 0) / onsets.length;
4644
+ }
4645
+ const restRatio = restRatioOf(sounding, startBeat, endBeat);
4646
+ const syncopation = syncopationOf(snapped, startBeat, endBeat, meters);
4647
+ const offGridRatio = offGridCount / onsets.length;
4648
+ return {
4649
+ startBeat,
4650
+ endBeat,
4651
+ bars,
4652
+ onsets: onsets.length,
4653
+ onsetDensity,
4654
+ barOnsets,
4655
+ onsetLevels,
4656
+ barPositions: profiles,
4657
+ interOnsetShares,
4658
+ restRatio,
4659
+ syncopation,
4660
+ offGridRatio,
4661
+ rationale: `${onsets.length} onsets over ${bars.toFixed(2)} bars (${onsetDensity.toFixed(2)} per bar), ${Math.round(restRatio * 100)}% rest, syncopation ${syncopation.toFixed(2)}`
4662
+ };
4663
+ }
4664
+
4665
+ // src/analyze/reference/validate.ts
4666
+ var MASS_TOLERANCE = 1e-9;
4667
+ function assertString(value, name) {
4668
+ if (typeof value !== "string") {
4669
+ throw new InvalidInputError(`${name} must be a string; received ${describeRejected(value)}`);
4670
+ }
4671
+ return value;
4672
+ }
4673
+ function assertSpanOrder(startBeat, endBeat, name) {
4674
+ if (endBeat < startBeat) {
4675
+ throw new InvalidInputError(
4676
+ `${name}.endBeat must be at least ${name}.startBeat; received ${endBeat} < ${startBeat}`
4677
+ );
4678
+ }
4679
+ }
4680
+ function assertNullable(value, name, validate) {
4681
+ if (value === void 0) {
4682
+ throw new InvalidInputError(`${name} must be present; received undefined`);
4683
+ }
4684
+ return value === null ? null : validate(value, name);
4685
+ }
4686
+ function assertMassDistribution(value, length, name, allowAllZero) {
4687
+ const shares = assertArray(value, name);
4688
+ if (shares.length !== length) {
4689
+ throw new InvalidInputError(`${name} must have length ${length}; received ${shares.length}`);
4690
+ }
4691
+ let sum = 0;
4692
+ for (let index = 0; index < shares.length; index += 1) {
4693
+ const share = assertFiniteNumber(shares[index], `${name}[${index}]`);
4694
+ if (share < 0) {
4695
+ throw new InvalidInputError(`${name}[${index}] must be at least 0; received ${share}`);
4696
+ }
4697
+ sum += share;
4698
+ }
4699
+ if (allowAllZero && sum === 0) {
4700
+ return shares;
4701
+ }
4702
+ if (Math.abs(sum - 1) > MASS_TOLERANCE) {
4703
+ throw new InvalidInputError(`${name} must sum to 1; received ${sum}`);
4704
+ }
4705
+ return shares;
4706
+ }
4707
+ function assertBarPositionProfile(value, name) {
4708
+ const read = assertRecord(value, name);
4709
+ assertTimeSignature(read.ts, `${name}.ts`);
4710
+ const slotsPerBar = assertPositiveInt(read.slotsPerBar, `${name}.slotsPerBar`);
4711
+ const onsets = assertInteger(read.onsets, `${name}.onsets`, 0);
4712
+ assertMassDistribution(read.shares, slotsPerBar, `${name}.shares`, onsets === 0);
4713
+ return read;
4714
+ }
4715
+ function assertRhythmAnalysis(value, name) {
4716
+ const read = assertRecord(value, name);
4717
+ const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
4718
+ const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
4719
+ assertSpanOrder(startBeat, endBeat, name);
4720
+ assertRange(read.bars, 0, Number.MAX_SAFE_INTEGER, `${name}.bars`);
4721
+ const onsets = assertInteger(read.onsets, `${name}.onsets`, 0);
4722
+ assertRange(read.onsetDensity, 0, Number.MAX_SAFE_INTEGER, `${name}.onsetDensity`);
4723
+ const barOnsets = assertArray(read.barOnsets, `${name}.barOnsets`);
4724
+ for (let index = 0; index < barOnsets.length; index += 1) {
4725
+ assertRange(
4726
+ barOnsets[index],
4727
+ 0,
4728
+ Number.MAX_SAFE_INTEGER,
4729
+ `${name}.barOnsets[${index}]`
4730
+ );
4731
+ }
4732
+ assertMassDistribution(read.onsetLevels, RHYTHM_LEVEL_COUNT, `${name}.onsetLevels`, onsets === 0);
4733
+ const barPositions = assertArray(read.barPositions, `${name}.barPositions`);
4734
+ for (let index = 0; index < barPositions.length; index += 1) {
4735
+ assertBarPositionProfile(barPositions[index], `${name}.barPositions[${index}]`);
4736
+ }
4737
+ assertMassDistribution(
4738
+ read.interOnsetShares,
4739
+ RHYTHM_IOI_BINS.length,
4740
+ `${name}.interOnsetShares`,
4741
+ onsets === 0
4742
+ );
4743
+ assertRange(read.restRatio, 0, 1, `${name}.restRatio`);
4744
+ assertRange(read.syncopation, 0, 1, `${name}.syncopation`);
4745
+ assertRange(read.offGridRatio, 0, 1, `${name}.offGridRatio`);
4746
+ assertString(read.rationale, `${name}.rationale`);
4747
+ return read;
4748
+ }
4749
+ function assertFormSection(value, name, sectionCount) {
4750
+ const read = assertRecord(value, name);
4751
+ assertString(read.label, `${name}.label`);
4752
+ const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
4753
+ const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
4754
+ assertSpanOrder(startBeat, endBeat, name);
4755
+ assertRange(read.bars, 0, Number.MAX_SAFE_INTEGER, `${name}.bars`);
4756
+ assertInteger(read.firstOccurrence, `${name}.firstOccurrence`, 0, Math.max(sectionCount - 1, 0));
4757
+ assertRange(read.similarity, 0, 1, `${name}.similarity`);
4758
+ assertString(read.rationale, `${name}.rationale`);
4759
+ return read;
4760
+ }
4761
+ function assertReferenceCadence(value, name) {
4762
+ const read = assertRecord(value, name);
4763
+ assertFiniteNumber(read.atBeat, `${name}.atBeat`);
4764
+ assertOneOf(read.type, CADENCE_TYPES, `${name}.type`);
4765
+ assertRange(read.weight, 0, 1, `${name}.weight`);
4766
+ return read;
4767
+ }
4768
+ function assertReferencePhraseMelody(value, name) {
4769
+ const read = assertRecord(value, name);
4770
+ assertOneOf(read.shape, MELODIC_CONTOUR_SHAPES, `${name}.shape`);
4771
+ assertMidiPitch(read.low, `${name}.low`);
4772
+ assertMidiPitch(read.high, `${name}.high`);
4773
+ assertRange(read.mean, 0, MIDI_MAX, `${name}.mean`);
4774
+ assertRange(read.peakPitch, 0, MIDI_MAX, `${name}.peakPitch`);
4775
+ assertRange(read.peakPosition, 0, 1, `${name}.peakPosition`);
4776
+ const outline = assertArray(read.outline, `${name}.outline`);
4777
+ if (outline.length !== PHRASE_OUTLINE_POINTS) {
4778
+ throw new InvalidInputError(
4779
+ `${name}.outline must have length ${PHRASE_OUTLINE_POINTS}; received ${outline.length}`
4780
+ );
4781
+ }
4782
+ for (let index = 0; index < outline.length; index += 1) {
4783
+ assertFiniteNumber(outline[index], `${name}.outline[${index}]`);
4784
+ }
4785
+ assertRange(read.onsetDensity, 0, Number.MAX_SAFE_INTEGER, `${name}.onsetDensity`);
4786
+ assertRange(read.restRatio, 0, 1, `${name}.restRatio`);
4787
+ return read;
4788
+ }
4789
+ function assertReferencePhrase(value, name, span, sectionCount, nodeCount) {
4790
+ const read = assertRecord(value, name);
4791
+ const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
4792
+ const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
4793
+ assertSpanOrder(startBeat, endBeat, name);
4794
+ assertRange(startBeat, span.startBeat, span.endBeat, `${name}.startBeat`);
4795
+ assertRange(endBeat, span.startBeat, span.endBeat, `${name}.endBeat`);
4796
+ assertRange(read.bars, 0, Number.MAX_SAFE_INTEGER, `${name}.bars`);
4797
+ assertNullable(
4798
+ read.section,
4799
+ `${name}.section`,
4800
+ (v, n) => assertInteger(v, n, 0, Math.max(sectionCount - 1, 0))
4801
+ );
4802
+ assertRange(read.confidence, 0, 1, `${name}.confidence`);
4803
+ assertNullable(read.cadence, `${name}.cadence`, assertReferenceCadence);
4804
+ assertNullable(read.melody, `${name}.melody`, assertReferencePhraseMelody);
4805
+ const motifNodes = assertArray(read.motifNodes, `${name}.motifNodes`);
4806
+ for (let index = 0; index < motifNodes.length; index += 1) {
4807
+ assertInteger(
4808
+ motifNodes[index],
4809
+ `${name}.motifNodes[${index}]`,
4810
+ 0,
4811
+ Math.max(nodeCount - 1, 0)
4812
+ );
4813
+ }
4814
+ return read;
4815
+ }
4816
+ function assertReferenceForm(value, name, span, nodeCount) {
4817
+ const read = assertRecord(value, name);
4818
+ const hypermeter = assertRecord(
4819
+ read.hypermeter,
4820
+ `${name}.hypermeter`
4821
+ );
4822
+ assertRange(hypermeter.groupBars, 0, Number.MAX_SAFE_INTEGER, `${name}.hypermeter.groupBars`);
4823
+ assertRange(hypermeter.confidence, 0, 1, `${name}.hypermeter.confidence`);
4824
+ const sections = assertArray(read.sections, `${name}.sections`);
4825
+ for (let index = 0; index < sections.length; index += 1) {
4826
+ assertFormSection(sections[index], `${name}.sections[${index}]`, sections.length);
4827
+ }
4828
+ const phrases = assertArray(read.phrases, `${name}.phrases`);
4829
+ for (let index = 0; index < phrases.length; index += 1) {
4830
+ assertReferencePhrase(
4831
+ phrases[index],
4832
+ `${name}.phrases[${index}]`,
4833
+ span,
4834
+ sections.length,
4835
+ nodeCount
4836
+ );
4837
+ }
4838
+ return read;
4839
+ }
4840
+ function assertResolvedKeyField(value, name) {
4841
+ const read = assertRecord(value, name);
4842
+ try {
4843
+ resolveKey(read);
4844
+ } catch (error) {
4845
+ if (error instanceof InvalidInputError) {
4846
+ throw new InvalidInputError(`${name} must be a valid key: ${error.message}`);
4847
+ }
4848
+ throw error;
4849
+ }
4850
+ return read;
4851
+ }
4852
+ function assertReferenceKeyRegion(value, name) {
4853
+ const read = assertRecord(value, name);
4854
+ const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
4855
+ const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
4856
+ assertSpanOrder(startBeat, endBeat, name);
4857
+ assertResolvedKeyField(read.key, `${name}.key`);
4858
+ assertRange(read.confidence, 0, 1, `${name}.confidence`);
4859
+ return read;
4860
+ }
4861
+ function assertReferenceChord(value, name, keys) {
4862
+ const read = assertRecord(value, name);
4863
+ const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
4864
+ const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
4865
+ assertSpanOrder(startBeat, endBeat, name);
4866
+ const keyIndex = assertInteger(read.key, `${name}.key`, 0, Math.max(keys.length - 1, 0));
4867
+ const roman = assertString(read.roman, `${name}.roman`);
4868
+ try {
4869
+ romanToChord(roman, keys[keyIndex]);
4870
+ } catch (error) {
4871
+ if (error instanceof InvalidInputError) {
4872
+ throw new InvalidInputError(
4873
+ `${name}.roman must be readable by romanToChord; received ${describeRejected(roman)}: ${error.message}`
4874
+ );
4875
+ }
4876
+ throw error;
4877
+ }
4878
+ assertOneOf(read.function, HARMONIC_FUNCTIONS, `${name}.function`);
4879
+ assertOneOf(read.level, REDUCTION_LEVELS, `${name}.level`);
4880
+ return read;
4881
+ }
4882
+ function assertReferenceHarmony(value, name) {
4883
+ const read = assertRecord(value, name);
4884
+ const keys = assertArray(read.keys, `${name}.keys`);
4885
+ for (let index = 0; index < keys.length; index += 1) {
4886
+ assertReferenceKeyRegion(keys[index], `${name}.keys[${index}]`);
4887
+ }
4888
+ const resolvedKeys = keys.map((region) => region.key);
4889
+ const chords = assertArray(read.chords, `${name}.chords`);
4890
+ for (let index = 0; index < chords.length; index += 1) {
4891
+ assertReferenceChord(chords[index], `${name}.chords[${index}]`, resolvedKeys);
4892
+ }
4893
+ assertRhythmAnalysis(read.rhythm, `${name}.rhythm`);
4894
+ return read;
4895
+ }
4896
+ function assertMotifRelationSummary(value, name) {
4897
+ const read = assertRecord(value, name);
4898
+ assertOneOf(read.kind, MOTIF_RELATION_KINDS, `${name}.kind`);
4899
+ assertFlag(read.sequence, `${name}.sequence`);
4900
+ assertFiniteNumber(read.semitones, `${name}.semitones`);
4901
+ if (read.degrees !== void 0) {
4902
+ assertFiniteNumber(read.degrees, `${name}.degrees`);
4903
+ }
4904
+ const timeRatio = assertFiniteNumber(read.timeRatio, `${name}.timeRatio`);
4905
+ if (timeRatio <= 0) {
4906
+ throw new InvalidInputError(`${name}.timeRatio must be positive; received ${timeRatio}`);
4907
+ }
4908
+ return read;
4909
+ }
4910
+ function assertMotifGraphNode(value, name, occurrenceCounts) {
4911
+ const read = assertRecord(value, name);
4912
+ const motif = assertInteger(
4913
+ read.motif,
4914
+ `${name}.motif`,
4915
+ 0,
4916
+ Math.max(occurrenceCounts.length - 1, 0)
4917
+ );
4918
+ assertInteger(
4919
+ read.occurrence,
4920
+ `${name}.occurrence`,
4921
+ 0,
4922
+ Math.max((occurrenceCounts[motif] ?? 1) - 1, 0)
4923
+ );
4924
+ const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
4925
+ const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
4926
+ assertSpanOrder(startBeat, endBeat, name);
4927
+ return read;
4928
+ }
4929
+ function assertMotifGraphEdge(value, name, nodeCount) {
4930
+ const read = assertRecord(value, name);
4931
+ const from = assertInteger(read.from, `${name}.from`, 0, Math.max(nodeCount - 1, 0));
4932
+ const to = assertInteger(read.to, `${name}.to`, 0, Math.max(nodeCount - 1, 0));
4933
+ if (from >= to) {
4934
+ throw new InvalidInputError(
4935
+ `${name}.from must be less than ${name}.to; received from=${from}, to=${to}`
4936
+ );
4937
+ }
4938
+ assertNullable(read.relation, `${name}.relation`, assertMotifRelationSummary);
4939
+ assertRange(read.similarity, 0, 1, `${name}.similarity`);
4940
+ return read;
4941
+ }
4942
+ function assertMotifGraph(value, name, occurrenceCounts) {
4943
+ const read = assertRecord(value, name);
4944
+ const nodes = assertArray(read.nodes, `${name}.nodes`);
4945
+ for (let index = 0; index < nodes.length; index += 1) {
4946
+ assertMotifGraphNode(nodes[index], `${name}.nodes[${index}]`, occurrenceCounts);
4947
+ }
4948
+ const edges = assertArray(read.edges, `${name}.edges`);
4949
+ const seenTo = /* @__PURE__ */ new Set();
4950
+ for (let index = 0; index < edges.length; index += 1) {
4951
+ const edge = assertMotifGraphEdge(edges[index], `${name}.edges[${index}]`, nodes.length);
4952
+ if (seenTo.has(edge.to)) {
4953
+ throw new InvalidInputError(`${name}.edges: node ${edge.to} has more than one incoming edge`);
4954
+ }
4955
+ seenTo.add(edge.to);
4956
+ }
4957
+ return read;
4958
+ }
4959
+ function assertReferenceMotif(value, name) {
4960
+ const read = assertRecord(value, name);
4961
+ const intervals = assertArray(read.intervals, `${name}.intervals`);
4962
+ let pitch = 0;
4963
+ let low = 0;
4964
+ let high = 0;
4965
+ for (let index = 0; index < intervals.length; index += 1) {
4966
+ const interval = assertFiniteNumber(intervals[index], `${name}.intervals[${index}]`);
4967
+ if (!Number.isInteger(interval)) {
4968
+ throw new InvalidInputError(
4969
+ `${name}.intervals[${index}] must be a whole number of semitones; received ${interval}`
4970
+ );
4971
+ }
4972
+ pitch += interval;
4973
+ low = Math.min(low, pitch);
4974
+ high = Math.max(high, pitch);
4975
+ }
4976
+ if (high - low > MIDI_MAX) {
4977
+ throw new InvalidInputError(
4978
+ `${name}.intervals must span at most ${MIDI_MAX} semitones; received ${high - low}`
4979
+ );
4980
+ }
4981
+ const rhythm = assertArray(read.rhythm, `${name}.rhythm`);
4982
+ if (rhythm.length !== intervals.length) {
4983
+ throw new InvalidInputError(
4984
+ `${name}.rhythm must hold one ratio per interval (${intervals.length}); received ${rhythm.length}`
4985
+ );
4986
+ }
4987
+ for (let index = 0; index < rhythm.length; index += 1) {
4988
+ const ratio2 = assertFiniteNumber(rhythm[index], `${name}.rhythm[${index}]`);
4989
+ if (!(ratio2 > 0)) {
4990
+ throw new InvalidInputError(`${name}.rhythm[${index}] must be positive; received ${ratio2}`);
4991
+ }
4992
+ }
4993
+ const unitBeats = assertRange(read.unitBeats, 0, Number.MAX_SAFE_INTEGER, `${name}.unitBeats`);
4994
+ if (unitBeats > 0 !== rhythm.length > 0) {
4995
+ throw new InvalidInputError(
4996
+ `${name}.unitBeats must be positive exactly when rhythm is non-empty; received ${unitBeats} with ${rhythm.length} ratio(s)`
4997
+ );
4998
+ }
4999
+ assertRange(read.spanBeats, 0, Number.MAX_SAFE_INTEGER, `${name}.spanBeats`);
5000
+ assertPositiveInt(read.occurrences, `${name}.occurrences`);
5001
+ return read;
5002
+ }
5003
+ function assertReferenceMelody(value, name) {
5004
+ const read = assertRecord(value, name);
5005
+ assertNullable(read.register, `${name}.register`, (v, n) => {
5006
+ const register = assertRecord(v, n);
5007
+ assertMidiPitch(register.low, `${n}.low`);
5008
+ assertMidiPitch(register.high, `${n}.high`);
5009
+ assertRange(register.mean, 0, MIDI_MAX, `${n}.mean`);
5010
+ return register;
5011
+ });
5012
+ assertNullable(read.shape, `${name}.shape`, (v, n) => assertOneOf(v, MELODIC_CONTOUR_SHAPES, n));
5013
+ const motifs = assertArray(read.motifs, `${name}.motifs`);
5014
+ const occurrenceCounts = [];
5015
+ for (let index = 0; index < motifs.length; index += 1) {
5016
+ assertReferenceMotif(motifs[index], `${name}.motifs[${index}]`);
5017
+ occurrenceCounts.push(motifs[index].occurrences);
5018
+ }
5019
+ assertMotifGraph(read.graph, `${name}.graph`, occurrenceCounts);
5020
+ assertRhythmAnalysis(read.rhythm, `${name}.rhythm`);
5021
+ return read;
5022
+ }
5023
+ function assertReferenceProfile(value, name = "reference profile") {
5024
+ const read = assertRecord(value, name);
5025
+ if (read.profileVersion !== REFERENCE_PROFILE_VERSION) {
5026
+ throw new InvalidInputError(
5027
+ `${name}.profileVersion must be ${REFERENCE_PROFILE_VERSION}; received ${describeRejected(read.profileVersion)}`
5028
+ );
5029
+ }
5030
+ const span = assertRecord(
5031
+ read.span,
5032
+ `${name}.span`
5033
+ );
5034
+ const startBeat = assertFiniteNumber(span.startBeat, `${name}.span.startBeat`);
5035
+ const endBeat = assertFiniteNumber(span.endBeat, `${name}.span.endBeat`);
5036
+ assertSpanOrder(startBeat, endBeat, `${name}.span`);
5037
+ assertRange(span.bars, 0, Number.MAX_SAFE_INTEGER, `${name}.span.bars`);
5038
+ assertMeterMap(read.meters, `${name}.meters`);
5039
+ const melody = assertReferenceMelody(read.melody, `${name}.melody`);
5040
+ assertReferenceForm(read.form, `${name}.form`, { startBeat, endBeat }, melody.graph.nodes.length);
5041
+ assertReferenceHarmony(read.harmony, `${name}.harmony`);
5042
+ return read;
5043
+ }
5044
+
5045
+ // src/analyze/reference/measures.ts
5046
+ var PROGRESSION_GAP = 0.75;
5047
+ var SAME_DEGREE_COST = 0.25;
5048
+ var SAME_FUNCTION_COST = 0.5;
5049
+ var HALF_PHRYGIAN_COST = 0.5;
5050
+ var MODE_CHANGE_COST = 0.5;
5051
+ var SHAPE_MISMATCH_COST = 0.25;
5052
+ var REGISTER_SPAN = 24;
5053
+ var PEAK_POSITION_WEIGHT = 0.5;
5054
+ var FAMILY_SIZE_BINS = 5;
5055
+ var DEPTH_BINS = 4;
5056
+ var NAMED_CHORDS = ["Ger", "It", "Fr", "N"];
5057
+ var PITCH_PRESERVING_KINDS = [
5058
+ "repetition",
5059
+ "augmentation",
5060
+ "diminution"
5061
+ ];
5062
+ var PITCH_PRESERVING = new Set(PITCH_PRESERVING_KINDS);
5063
+ var CELL_START_PITCH = 60;
5064
+ function whenBoth(hasA, hasB, measure) {
5065
+ if (!hasA && !hasB) {
5066
+ return null;
5067
+ }
5068
+ if (!hasA || !hasB) {
5069
+ return 0;
5070
+ }
5071
+ return measure();
5072
+ }
5073
+ function aligned(a, b, cost, gap = 1) {
5074
+ return distanceToSimilarity(gradedEditDistance(a, b, cost, gap), a.length, b.length);
5075
+ }
5076
+ function alignedSimilarity(a, b, cost, gap = 1) {
5077
+ return whenBoth(a.length > 0, b.length > 0, () => aligned(a, b, cost, gap));
5078
+ }
5079
+ function massOf(values) {
5080
+ let total = 0;
5081
+ for (const value of values) {
5082
+ total += value;
5083
+ }
5084
+ return total;
5085
+ }
5086
+ function intersection(a, b) {
5087
+ const massA = massOf(a);
5088
+ const massB = massOf(b);
5089
+ let shared2 = 0;
5090
+ for (let i = 0; i < a.length; i += 1) {
5091
+ shared2 += Math.min((a[i] ?? 0) / massA, (b[i] ?? 0) / massB);
5092
+ }
5093
+ return Math.min(1, shared2);
5094
+ }
5095
+ function distributionSimilarity(a, b) {
5096
+ return whenBoth(massOf(a) > 0, massOf(b) > 0, () => intersection(a, b));
5097
+ }
5098
+ function ratio(a, b) {
5099
+ const larger = Math.max(a, b);
5100
+ return larger === 0 ? 1 : Math.min(a, b) / larger;
5101
+ }
5102
+ function sectionSequenceSimilarity(a, b) {
5103
+ return alignedSimilarity(a, b, (x, y) => x === y ? 0 : 1);
5104
+ }
5105
+ function phraseLengthSimilarity(a, b) {
5106
+ return alignedSimilarity(a, b, (x, y) => {
5107
+ const longer = Math.max(x, y);
5108
+ return longer === 0 ? 0 : Math.min(Math.abs(x - y) / longer, 1);
5109
+ });
5110
+ }
5111
+ function simpleDegreeToken(roman) {
5112
+ for (const name of NAMED_CHORDS) {
5113
+ if (roman.startsWith(name)) {
5114
+ return name;
5115
+ }
5116
+ }
5117
+ const match = roman.match(/^([b#]*)([IV]+)/i);
5118
+ return match ? `${match[1]}${match[2].toUpperCase()}` : roman;
5119
+ }
5120
+ function romanDegreeToken(roman) {
5121
+ const slash = roman.indexOf("/");
5122
+ if (slash < 0) {
5123
+ return simpleDegreeToken(roman);
5124
+ }
5125
+ return `${simpleDegreeToken(roman.slice(0, slash))}/${simpleDegreeToken(roman.slice(slash + 1))}`;
5126
+ }
5127
+ function chordCost(x, y) {
5128
+ if (x.roman === y.roman) {
5129
+ return 0;
5130
+ }
5131
+ if (romanDegreeToken(x.roman) === romanDegreeToken(y.roman)) {
5132
+ return SAME_DEGREE_COST;
5133
+ }
5134
+ return x.function === y.function ? SAME_FUNCTION_COST : 1;
5135
+ }
5136
+ function progressionSimilarity(a, b, budget) {
5137
+ assertGenerationBudget(a.length * b.length, "reference progression alignment", budget);
5138
+ return alignedSimilarity(a, b, chordCost, PROGRESSION_GAP);
5139
+ }
5140
+ function functionMass(chords) {
5141
+ const mass = HARMONIC_FUNCTIONS.map(() => 0);
5142
+ for (const chord of chords) {
5143
+ const bin = HARMONIC_FUNCTIONS.indexOf(chord.function);
5144
+ mass[bin] = (mass[bin] ?? 0) + Math.max(0, chord.endBeat - chord.startBeat);
5145
+ }
5146
+ return mass;
5147
+ }
5148
+ function functionSimilarity(a, b) {
5149
+ return distributionSimilarity(functionMass(a), functionMass(b));
5150
+ }
5151
+ function cadenceSimilarity(a, b) {
5152
+ return alignedSimilarity(a, b, (x, y) => {
5153
+ if (x === y) {
5154
+ return 0;
5155
+ }
5156
+ const halfAndPhrygian = x === "half" && y === "phrygian" || x === "phrygian" && y === "half";
5157
+ return halfAndPhrygian ? HALF_PHRYGIAN_COST : 1;
5158
+ });
5159
+ }
5160
+ function keyTokens(keys) {
5161
+ const home = keys[0]?.scale.rootPc ?? 0;
5162
+ return keys.map((key) => ({
5163
+ distance: ((key.scale.rootPc - home) % 12 + 12) % 12,
5164
+ mask: key.scale.modeMask12
5165
+ }));
5166
+ }
5167
+ function keyPlanSimilarity(a, b) {
5168
+ return alignedSimilarity(keyTokens(a), keyTokens(b), (x, y) => {
5169
+ if (x.distance !== y.distance) {
5170
+ return 1;
5171
+ }
5172
+ return x.mask === y.mask ? 0 : MODE_CHANGE_COST;
5173
+ });
5174
+ }
5175
+ function onsetSimilarity(a, b) {
5176
+ return whenBoth(a.onsets > 0, b.onsets > 0, () => {
5177
+ let weighted = 0;
5178
+ let weight = 0;
5179
+ for (const left of a.barPositions) {
5180
+ for (const right of b.barPositions) {
5181
+ const shared2 = left.ts.numerator === right.ts.numerator && left.ts.denominator === right.ts.denominator && left.onsets > 0 && right.onsets > 0;
5182
+ if (shared2) {
5183
+ const w = left.onsets + right.onsets;
5184
+ weighted += w * intersection(left.shares, right.shares);
5185
+ weight += w;
5186
+ }
5187
+ }
5188
+ }
5189
+ return weight > 0 ? weighted / weight : intersection(a.onsetLevels, b.onsetLevels);
5190
+ });
5191
+ }
5192
+ function densitySimilarity(a, b) {
5193
+ return whenBoth(a.onsets > 0, b.onsets > 0, () => ratio(a.onsetDensity, b.onsetDensity));
5194
+ }
5195
+ function durationSimilarity(a, b) {
5196
+ return whenBoth(
5197
+ a.onsets > 0,
5198
+ b.onsets > 0,
5199
+ () => intersection(a.interOnsetShares, b.interOnsetShares)
5200
+ );
5201
+ }
5202
+ function syncopationSimilarity(a, b) {
5203
+ return whenBoth(a.onsets > 0, b.onsets > 0, () => 1 - Math.abs(a.syncopation - b.syncopation));
5204
+ }
5205
+ function cellNotes(motif) {
5206
+ const notes = [];
5207
+ let low = 0;
5208
+ let high = 0;
5209
+ let offset = 0;
5210
+ for (const interval of motif.intervals) {
5211
+ offset += interval;
5212
+ low = Math.min(low, offset);
5213
+ high = Math.max(high, offset);
5214
+ }
5215
+ let pitch = Math.min(MIDI_MAX - high, Math.max(-low, CELL_START_PITCH));
5216
+ let beat = 0;
5217
+ for (let i = 0; i <= motif.intervals.length; i += 1) {
5218
+ const step = motif.rhythm[i] ?? 1;
5219
+ notes.push({ pitch, startBeat: beat, durationBeat: i < motif.intervals.length ? step : 1 });
5220
+ pitch += motif.intervals[i] ?? 0;
5221
+ beat += step;
5222
+ }
5223
+ return notes;
5224
+ }
5225
+ function bestMatchMean(from, to) {
5226
+ let weighted = 0;
5227
+ let weight = 0;
5228
+ for (const motif of from) {
5229
+ let best = 0;
5230
+ for (const other of to) {
5231
+ best = Math.max(best, compareMelodies(motif.cell, other.cell).pitchSimilarity);
5232
+ }
5233
+ weighted += motif.occurrences * best;
5234
+ weight += motif.occurrences;
5235
+ }
5236
+ return weighted / weight;
5237
+ }
5238
+ function surfaceSimilarity(a, b) {
5239
+ return whenBoth(a.length > 0, b.length > 0, () => {
5240
+ const left = a.map((motif) => ({ cell: cellNotes(motif), occurrences: motif.occurrences }));
5241
+ const right = b.map((motif) => ({ cell: cellNotes(motif), occurrences: motif.occurrences }));
5242
+ return (bestMatchMean(left, right) + bestMatchMean(right, left)) / 2;
5243
+ });
5244
+ }
5245
+ function coverageOf(graph, spanBeats) {
5246
+ if (!(spanBeats > 0) || graph.nodes.length === 0) {
5247
+ return 0;
5248
+ }
5249
+ const spans = graph.nodes.map((node) => [node.startBeat, node.endBeat]).sort((x, y) => x[0] - y[0] || x[1] - y[1]);
5250
+ let covered = 0;
5251
+ let [runStart, runEnd] = spans[0];
5252
+ for (const [start, end] of spans) {
5253
+ if (start > runEnd) {
5254
+ covered += runEnd - runStart;
5255
+ runStart = start;
5256
+ runEnd = end;
5257
+ } else {
5258
+ runEnd = Math.max(runEnd, end);
5259
+ }
5260
+ }
5261
+ covered += runEnd - runStart;
5262
+ return Math.min(1, covered / spanBeats);
5263
+ }
5264
+ function bump(histogram, bin) {
5265
+ histogram[bin] = (histogram[bin] ?? 0) + 1;
5266
+ }
5267
+ function summarise({ graph, spanBeats }) {
5268
+ const kinds = new Array(MOTIF_RELATION_KINDS.length + 1).fill(0);
5269
+ const parentEdge = /* @__PURE__ */ new Map();
5270
+ for (const edge of graph.edges) {
5271
+ const kind = edge.relation?.kind;
5272
+ bump(
5273
+ kinds,
5274
+ kind === void 0 ? MOTIF_RELATION_KINDS.length : MOTIF_RELATION_KINDS.indexOf(kind)
5275
+ );
5276
+ parentEdge.set(edge.to, edge);
5277
+ }
5278
+ const depth = new Array(graph.nodes.length).fill(0);
5279
+ const root = graph.nodes.map((_, i) => i);
5280
+ for (let node = 0; node < graph.nodes.length; node += 1) {
5281
+ const edge = parentEdge.get(node);
5282
+ if (edge !== void 0) {
5283
+ const kind = edge.relation?.kind;
5284
+ const keepsPitch = kind !== void 0 && PITCH_PRESERVING.has(kind);
5285
+ depth[node] = (depth[edge.from] ?? 0) + (keepsPitch ? 0 : 1);
5286
+ root[node] = root[edge.from] ?? edge.from;
5287
+ }
5288
+ }
5289
+ const familySize = /* @__PURE__ */ new Map();
5290
+ for (const r of root) {
5291
+ familySize.set(r, (familySize.get(r) ?? 0) + 1);
5292
+ }
5293
+ const families = new Array(FAMILY_SIZE_BINS).fill(0);
5294
+ for (const size of familySize.values()) {
5295
+ bump(families, Math.min(size, FAMILY_SIZE_BINS) - 1);
5296
+ }
5297
+ const depths = new Array(DEPTH_BINS).fill(0);
5298
+ for (const d of depth) {
5299
+ bump(depths, Math.min(d, DEPTH_BINS - 1));
5300
+ }
5301
+ return { kinds, families, depths, coverage: coverageOf(graph, spanBeats) };
5302
+ }
5303
+ function motifStructureSimilarity(a, b) {
5304
+ const left = summarise(a);
5305
+ const right = summarise(b);
5306
+ const terms = [
5307
+ distributionSimilarity(left.kinds, right.kinds),
5308
+ distributionSimilarity(left.families, right.families),
5309
+ distributionSimilarity(left.depths, right.depths),
5310
+ whenBoth(
5311
+ left.coverage > 0,
5312
+ right.coverage > 0,
5313
+ () => 1 - Math.abs(left.coverage - right.coverage)
5314
+ )
5315
+ ].filter((term) => term !== null);
5316
+ return terms.length === 0 ? null : massOf(terms) / terms.length;
5317
+ }
5318
+ function nullableCost(cost) {
5319
+ return (x, y) => {
5320
+ if (x === null || y === null) {
5321
+ return x === y ? 0 : 1;
5322
+ }
5323
+ return cost(x, y);
5324
+ };
5325
+ }
5326
+ function phraseSimilarity(a, b, cost) {
5327
+ const hasA = a.some((phrase) => phrase !== null);
5328
+ const hasB = b.some((phrase) => phrase !== null);
5329
+ return whenBoth(hasA, hasB, () => aligned(a, b, nullableCost(cost)));
5330
+ }
5331
+ function normalisedOutline(phrase) {
5332
+ const mean = massOf(phrase.outline) / (phrase.outline.length || 1);
5333
+ const range = phrase.high - phrase.low || 1;
5334
+ return phrase.outline.map((point) => (point - mean) / range);
5335
+ }
5336
+ function contourCost(x, y) {
5337
+ const left = normalisedOutline(x);
5338
+ const right = normalisedOutline(y);
5339
+ let difference = 0;
5340
+ for (let i = 0; i < left.length; i += 1) {
5341
+ difference += Math.abs((left[i] ?? 0) - (right[i] ?? 0));
5342
+ }
5343
+ const outlineCost = Math.min(1, difference / (left.length || 1));
5344
+ return Math.min(1, outlineCost + (x.shape === y.shape ? 0 : SHAPE_MISMATCH_COST));
5345
+ }
5346
+ function contourSimilarity(a, b) {
5347
+ return phraseSimilarity(a, b, contourCost);
5348
+ }
5349
+ function registerSimilarity(a, b) {
5350
+ const meanA = a.mean ?? 0;
5351
+ const meanB = b.mean ?? 0;
5352
+ return phraseSimilarity(a.phrases, b.phrases, (x, y) => {
5353
+ const offset = Math.abs(x.mean - meanA - (y.mean - meanB));
5354
+ const range = Math.abs(x.high - x.low - (y.high - y.low));
5355
+ const peak = Math.abs(x.peakPosition - y.peakPosition);
5356
+ return Math.min(1, (offset + range) / REGISTER_SPAN + PEAK_POSITION_WEIGHT * peak);
5357
+ });
5358
+ }
5359
+
5360
+ // src/analyze/reference/internal.ts
5361
+ function weightedMean(notes) {
5362
+ let weighted = 0;
5363
+ let total = 0;
5364
+ for (const note of notes) {
5365
+ weighted += note.pitch * note.durationBeat;
5366
+ total += note.durationBeat;
5367
+ }
5368
+ return total > 0 ? weighted / total : 0;
5369
+ }
5370
+ function phraseMelody(line, startBeat, endBeat, lineMean, meters, budget) {
5371
+ const inside = line.filter((note) => note.startBeat >= startBeat - BEAT_EPS && note.startBeat < endBeat - BEAT_EPS).map((note) => ({
5372
+ ...note,
5373
+ durationBeat: Math.min(note.startBeat + note.durationBeat, endBeat) - note.startBeat
5374
+ }));
5375
+ const first = inside[0];
5376
+ if (first === void 0 || inside.length < 2) {
5377
+ return null;
5378
+ }
5379
+ let low = first.pitch;
5380
+ let peak = first;
5381
+ for (const note of inside) {
5382
+ low = Math.min(low, note.pitch);
5383
+ if (note.pitch > peak.pitch) {
5384
+ peak = note;
5385
+ }
5386
+ }
5387
+ const length = endBeat - startBeat;
5388
+ const outline = [];
5389
+ for (let k = 0; k < PHRASE_OUTLINE_POINTS; k += 1) {
5390
+ const at = startBeat + (k + 0.5) * length / PHRASE_OUTLINE_POINTS;
5391
+ let held = first;
5392
+ for (const note of inside) {
5393
+ if (note.startBeat > at + BEAT_EPS) {
5394
+ break;
5395
+ }
5396
+ held = note;
5397
+ }
5398
+ outline.push(held.pitch - lineMean);
5399
+ }
5400
+ const rhythm = rhythmOver(inside, { meters, totalBeats: endBeat, budget }, startBeat);
5401
+ return {
5402
+ shape: melodicContour(inside).shape,
5403
+ low,
5404
+ high: peak.pitch,
5405
+ mean: weightedMean(inside),
5406
+ peakPitch: peak.pitch,
5407
+ peakPosition: Math.min(1, Math.max(0, (peak.startBeat - startBeat) / length)),
5408
+ outline,
5409
+ onsetDensity: rhythm.onsetDensity,
5410
+ restRatio: rhythm.restRatio
5411
+ };
5412
+ }
5413
+
5414
+ export {
5415
+ HUMANIZE_ADJACENCY,
5416
+ sameInstant,
5417
+ adjacent,
5418
+ hasEnded,
5419
+ windowWeights,
5420
+ MODAL_SCALE_NAMES,
5421
+ detectChord,
5422
+ detectChordBest,
5423
+ detectKey,
5424
+ detectKeyFromNotes,
5425
+ detectKeyBest,
5426
+ barSpanOf,
5427
+ clamp01,
5428
+ floorMod,
5429
+ firstSoundingBeat,
5430
+ resolveSpanEnd,
5431
+ lastBarOf,
5432
+ sliceBars,
5433
+ weightSimilarity,
5434
+ harmonicNovelty,
5435
+ combineEvidence,
5436
+ augmentedSixthChord,
5437
+ augmentedSixthKind,
5438
+ augmentedSixthFromPitchClasses,
5439
+ spellAugmentedSixth,
5440
+ CADENCE_TYPES,
5441
+ detectCadence,
5442
+ isBorrowedChord,
5443
+ borrowedSource,
5444
+ romanToChord,
5445
+ chordToRoman,
5446
+ explainRoman,
5447
+ HARMONIC_FUNCTIONS,
5448
+ isMinorKey,
5449
+ functionOf,
5450
+ isDiatonic,
5451
+ parallelKey,
5452
+ analyzeChord,
5453
+ secondaryDominant,
5454
+ secondaryDominantOf,
5455
+ pivotChords,
3310
5456
  gridForNotes,
5457
+ latestSoundingEnd,
3311
5458
  barGridStart,
3312
5459
  keyTimelineFromNotes,
3313
5460
  detectModulations,
3314
5461
  attachPivots,
3315
5462
  keyLookup,
3316
5463
  prevailingKeyOf,
3317
- keyScaleAt,
3318
5464
  toVoiceNotes,
3319
5465
  analyzeVoice,
3320
5466
  assertChordTimeline,
5467
+ assertSpansInOrder,
5468
+ statedKeyRegions,
3321
5469
  chordTimelineFromChords,
3322
5470
  chordAtBeat,
3323
5471
  segmentStartingAt,
3324
5472
  chordTimelineFromNotes,
3325
5473
  dirtySlotsFor,
3326
5474
  analyzeTimeline,
3327
- detectCadences
5475
+ detectCadences,
5476
+ orderedNotes,
5477
+ MELODIC_CONTOUR_SHAPES,
5478
+ melodicContour,
5479
+ extractMotifs,
5480
+ motifFromNotes,
5481
+ MOTIF_RELATION_KINDS,
5482
+ relateMotifs,
5483
+ compareMelodies,
5484
+ melodicSimilarity,
5485
+ DEFAULT_VARIATION_THRESHOLD,
5486
+ motifGraph,
5487
+ REDUCTION_LEVELS,
5488
+ reduceProgression,
5489
+ REFERENCE_PROFILE_VERSION,
5490
+ sectionSequenceSimilarity,
5491
+ phraseLengthSimilarity,
5492
+ progressionSimilarity,
5493
+ functionSimilarity,
5494
+ cadenceSimilarity,
5495
+ keyPlanSimilarity,
5496
+ onsetSimilarity,
5497
+ densitySimilarity,
5498
+ durationSimilarity,
5499
+ syncopationSimilarity,
5500
+ surfaceSimilarity,
5501
+ motifStructureSimilarity,
5502
+ contourSimilarity,
5503
+ registerSimilarity,
5504
+ rhythmLevel,
5505
+ ioiBinIndex,
5506
+ RHYTHM_IOI_BINS,
5507
+ analyzeRhythm,
5508
+ rhythmOver,
5509
+ assertReferenceProfile,
5510
+ weightedMean,
5511
+ phraseMelody
3328
5512
  };
3329
- //# sourceMappingURL=chunk-H2KJHBW4.js.map
5513
+ //# sourceMappingURL=chunk-FJFULJZE.js.map