@libraz/libcantus 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +4 -0
  2. package/README_ja.md +4 -0
  3. package/dist/analyze/index.cjs +31 -7
  4. package/dist/analyze/index.cjs.map +1 -1
  5. package/dist/analyze/index.d.cts +155 -9
  6. package/dist/analyze/index.d.ts +155 -9
  7. package/dist/analyze/index.js +37 -13
  8. package/dist/{chunk-TT6PKMPR.cjs → chunk-35NBV2FT.cjs} +16 -16
  9. package/dist/{chunk-TT6PKMPR.cjs.map → chunk-35NBV2FT.cjs.map} +1 -1
  10. package/dist/{chunk-YPB3A3EY.cjs → chunk-3R3QVKY5.cjs} +314 -285
  11. package/dist/chunk-3R3QVKY5.cjs.map +1 -0
  12. package/dist/{chunk-GDKE6RQW.cjs → chunk-4R2LWV7E.cjs} +691 -413
  13. package/dist/chunk-4R2LWV7E.cjs.map +1 -0
  14. package/dist/{chunk-RVS4G4MR.cjs → chunk-76ZBTRG2.cjs} +198 -198
  15. package/dist/{chunk-RVS4G4MR.cjs.map → chunk-76ZBTRG2.cjs.map} +1 -1
  16. package/dist/{chunk-X6RJAHII.js → chunk-7KG3RTYS.js} +2 -2
  17. package/dist/{chunk-PQL7E5F6.js → chunk-AGOVW7U6.js} +1 -1
  18. package/dist/{chunk-PQL7E5F6.js.map → chunk-AGOVW7U6.js.map} +1 -1
  19. package/dist/{chunk-5KW7DNCJ.cjs → chunk-B2PW6EHN.cjs} +2423 -453
  20. package/dist/chunk-B2PW6EHN.cjs.map +1 -0
  21. package/dist/{chunk-J772DL3B.cjs → chunk-CGBADIO5.cjs} +53 -53
  22. package/dist/{chunk-J772DL3B.cjs.map → chunk-CGBADIO5.cjs.map} +1 -1
  23. package/dist/{chunk-R5VUXQMP.js → chunk-CPMB7P75.js} +1978 -8
  24. package/dist/chunk-CPMB7P75.js.map +1 -0
  25. package/dist/{chunk-VLE5R7PF.js → chunk-GNVN4PPJ.js} +1874 -8
  26. package/dist/chunk-GNVN4PPJ.js.map +1 -0
  27. package/dist/{chunk-Q6R3QAZD.cjs → chunk-IFZDWIKM.cjs} +2199 -333
  28. package/dist/chunk-IFZDWIKM.cjs.map +1 -0
  29. package/dist/{chunk-DKICR7AN.js → chunk-IPEF6NWB.js} +296 -18
  30. package/dist/chunk-IPEF6NWB.js.map +1 -0
  31. package/dist/{chunk-AWEMD747.js → chunk-M5RYMAFI.js} +3 -3
  32. package/dist/{chunk-HSY2VQXZ.js → chunk-MLBPXA4Z.js} +330 -728
  33. package/dist/chunk-MLBPXA4Z.js.map +1 -0
  34. package/dist/{chunk-H66ARFU5.js → chunk-NB25P3EH.js} +3 -3
  35. package/dist/{chunk-5JJ6EHEZ.cjs → chunk-NM7UAMC3.cjs} +62 -62
  36. package/dist/{chunk-5JJ6EHEZ.cjs.map → chunk-NM7UAMC3.cjs.map} +1 -1
  37. package/dist/{chunk-R7B5N2JK.cjs → chunk-PM4XJE2Y.cjs} +47 -47
  38. package/dist/{chunk-R7B5N2JK.cjs.map → chunk-PM4XJE2Y.cjs.map} +1 -1
  39. package/dist/{chunk-7GOKQJTQ.js → chunk-PUKJP2Z5.js} +2 -2
  40. package/dist/{chunk-QCXYKSZS.cjs → chunk-PWC4CRWT.cjs} +19 -19
  41. package/dist/{chunk-QCXYKSZS.cjs.map → chunk-PWC4CRWT.cjs.map} +1 -1
  42. package/dist/{chunk-FPBZFNCD.js → chunk-RFIBF2RR.js} +47 -18
  43. package/dist/{chunk-FPBZFNCD.js.map → chunk-RFIBF2RR.js.map} +1 -1
  44. package/dist/{chunk-XKSO2V5I.cjs → chunk-S5BKZZGM.cjs} +1 -1
  45. package/dist/{chunk-XKSO2V5I.cjs.map → chunk-S5BKZZGM.cjs.map} +1 -1
  46. package/dist/{chunk-HOHFYGKW.js → chunk-UMWZ4ZET.js} +2 -2
  47. package/dist/{chunk-XVMJO3QF.cjs → chunk-YFA7X2F6.cjs} +489 -887
  48. package/dist/chunk-YFA7X2F6.cjs.map +1 -0
  49. package/dist/{chunk-57WB2EDF.js → chunk-ZBUROTJQ.js} +3 -3
  50. package/dist/{coerce-wwdELNYS.d.ts → coerce-DP0vYu-x.d.ts} +77 -1
  51. package/dist/{coerce-CJlgULrQ.d.cts → coerce-DRERzdve.d.cts} +77 -1
  52. package/dist/core/index.cjs +5 -5
  53. package/dist/core/index.d.cts +2 -2
  54. package/dist/core/index.d.ts +2 -2
  55. package/dist/core/index.js +4 -4
  56. package/dist/generate/index.cjs +20 -8
  57. package/dist/generate/index.cjs.map +1 -1
  58. package/dist/generate/index.d.cts +87 -9
  59. package/dist/generate/index.d.ts +87 -9
  60. package/dist/generate/index.js +19 -7
  61. package/dist/{index-BaoKVVhE.d.cts → index-B9QLziDe.d.cts} +2 -2
  62. package/dist/{index-CnHQgqNh.d.ts → index-BHIUyjhZ.d.ts} +2 -2
  63. package/dist/{index-CBCP84rv.d.cts → index-BL5N2Oyz.d.cts} +1 -1
  64. package/dist/{index-D_hzxD8w.d.cts → index-BMweUlm5.d.cts} +3 -0
  65. package/dist/{index-Ci3xyows.d.ts → index-CL4b4gUQ.d.ts} +3 -0
  66. package/dist/{index-BzLNkmA7.d.cts → index-CY-3NVfy.d.cts} +486 -5
  67. package/dist/{index-CnIdP4EZ.d.ts → index-Dw2DP7vm.d.ts} +1 -1
  68. package/dist/{index-DpC-mDlV.d.ts → index-zaZdmnU4.d.ts} +486 -5
  69. package/dist/index.cjs +49 -13
  70. package/dist/index.cjs.map +1 -1
  71. package/dist/index.d.cts +10 -10
  72. package/dist/index.d.ts +10 -10
  73. package/dist/index.js +55 -19
  74. package/dist/{masks-O73Rhj5d.d.ts → masks-Cc7-MzzY.d.ts} +1 -1
  75. package/dist/{masks-C8TWLFIi.d.cts → masks-pADfTOiD.d.cts} +1 -1
  76. package/dist/model/index.cjs +12 -12
  77. package/dist/model/index.d.cts +213 -16
  78. package/dist/model/index.d.ts +213 -16
  79. package/dist/model/index.js +11 -11
  80. package/dist/{index-DTsKaPaC.d.cts → profile-CQMEqdH9.d.cts} +88 -601
  81. package/dist/{index-BUtIf2go.d.ts → profile-DLdlIpS9.d.ts} +88 -601
  82. package/dist/theory/index.cjs +5 -5
  83. package/dist/theory/index.d.cts +7 -7
  84. package/dist/theory/index.d.ts +7 -7
  85. package/dist/theory/index.js +4 -4
  86. package/dist/{index-BpWOhkkE.d.ts → types-D-XgxGvp.d.cts} +1170 -14
  87. package/dist/{index-B3WCj_Hm.d.cts → types-Dh7C064C.d.ts} +1170 -14
  88. package/package.json +6 -5
  89. package/dist/chunk-5KW7DNCJ.cjs.map +0 -1
  90. package/dist/chunk-DKICR7AN.js.map +0 -1
  91. package/dist/chunk-GDKE6RQW.cjs.map +0 -1
  92. package/dist/chunk-HSY2VQXZ.js.map +0 -1
  93. package/dist/chunk-Q6R3QAZD.cjs.map +0 -1
  94. package/dist/chunk-R5VUXQMP.js.map +0 -1
  95. package/dist/chunk-VLE5R7PF.js.map +0 -1
  96. package/dist/chunk-XVMJO3QF.cjs.map +0 -1
  97. package/dist/chunk-YPB3A3EY.cjs.map +0 -1
  98. /package/dist/{chunk-X6RJAHII.js.map → chunk-7KG3RTYS.js.map} +0 -0
  99. /package/dist/{chunk-AWEMD747.js.map → chunk-M5RYMAFI.js.map} +0 -0
  100. /package/dist/{chunk-H66ARFU5.js.map → chunk-NB25P3EH.js.map} +0 -0
  101. /package/dist/{chunk-7GOKQJTQ.js.map → chunk-PUKJP2Z5.js.map} +0 -0
  102. /package/dist/{chunk-HOHFYGKW.js.map → chunk-UMWZ4ZET.js.map} +0 -0
  103. /package/dist/{chunk-57WB2EDF.js.map → chunk-ZBUROTJQ.js.map} +0 -0
@@ -1,47 +1,66 @@
1
1
  import {
2
- HUMANIZE_ADJACENCY,
2
+ REFERENCE_PROFILE_VERSION,
3
3
  adjacent,
4
+ analyzeRhythm,
4
5
  analyzeTimeline,
5
6
  analyzeVoice,
6
7
  assertChordTimeline,
8
+ assertReferenceProfile,
7
9
  attachPivots,
8
10
  barGridStart,
11
+ barSpanOf,
12
+ cadenceSimilarity,
9
13
  chordTimelineFromNotes,
14
+ chordToRoman,
10
15
  clamp01,
11
16
  combineEvidence,
12
- detectCadence,
17
+ contourSimilarity,
18
+ densitySimilarity,
13
19
  detectCadences,
14
20
  dirtySlotsFor,
21
+ durationSimilarity,
22
+ extractMotifs,
15
23
  firstSoundingBeat,
16
24
  floorMod,
17
25
  functionOf,
26
+ functionSimilarity,
18
27
  gridForNotes,
19
28
  harmonicNovelty,
20
29
  hasEnded,
21
- isAppliedDominant,
22
- isCadentialSixFour,
23
30
  keyLookup,
24
- keyScaleAt,
31
+ keyPlanSimilarity,
25
32
  keyTimelineFromNotes,
26
33
  lastBarOf,
34
+ melodicContour,
35
+ melodicSimilarity,
36
+ motifGraph,
37
+ motifStructureSimilarity,
38
+ onsetSimilarity,
39
+ orderedNotes,
40
+ phraseLengthSimilarity,
41
+ phraseMelody,
27
42
  prevailingKeyOf,
43
+ progressionSimilarity,
44
+ reduceProgression,
45
+ registerSimilarity,
28
46
  sameInstant,
47
+ sectionSequenceSimilarity,
29
48
  segmentStartingAt,
30
49
  sliceBars,
50
+ surfaceSimilarity,
51
+ syncopationSimilarity,
31
52
  weightSimilarity,
53
+ weightedMean,
32
54
  windowWeights
33
- } from "./chunk-VLE5R7PF.js";
55
+ } from "./chunk-GNVN4PPJ.js";
34
56
  import {
35
57
  PROFILE_WEIGHTS,
36
58
  chordPitchClasses,
37
59
  evaluateSafety,
38
- isDominantChordOf,
39
- keySignatureFifths,
40
60
  majorKey,
41
61
  resolveKey,
42
- scaleOf,
43
- scaleTonesInDegreeOrder
44
- } from "./chunk-FPBZFNCD.js";
62
+ scaleOf
63
+ } from "./chunk-RFIBF2RR.js";
45
64
  import {
46
65
  BEAT_EPS,
47
66
  InvalidInputError,
@@ -61,11 +80,9 @@ import {
61
80
  beatsPerBarAt,
62
81
  describeRejected,
63
82
  isStrongBeat,
64
- midiToNote,
65
83
  pitchClassOf,
66
- resolveMeters,
67
- spelledInterval
68
- } from "./chunk-PQL7E5F6.js";
84
+ resolveMeters
85
+ } from "./chunk-AGOVW7U6.js";
69
86
 
70
87
  // src/analyze/arrange/internal.ts
71
88
  var MAX_LANE_LEAP = 24;
@@ -328,6 +345,8 @@ function prepareTracks(tracks) {
328
345
  const prevEnd = prev === void 0 ? void 0 : prev.startBeat + prev.durationBeat;
329
346
  const contiguous = prevEnd !== void 0 && adjacent(prevEnd, note.startBeat);
330
347
  return {
348
+ id: note.id,
349
+ originalIndex: note.originalIndex,
331
350
  pitch: note.pitch,
332
351
  prevPitch: contiguous ? prev?.pitch : void 0,
333
352
  startBeat: note.startBeat,
@@ -395,7 +414,7 @@ function soundingVoicesAt(prepared, beat) {
395
414
  if (previous !== void 0) {
396
415
  snap.prevPitch = previous;
397
416
  }
398
- out.push({ track: t, voice: v, snapshot: snap });
417
+ out.push({ track: t, voice: v, trackIndex: track.trackIndex, note, snapshot: snap });
399
418
  }
400
419
  }
401
420
  return out;
@@ -403,7 +422,21 @@ function soundingVoicesAt(prepared, beat) {
403
422
  function otherVoicesSounding(prepared, excludeTrack, excludeVoice, beat, cache) {
404
423
  const sounding = cache.get(beat) ?? soundingVoicesAt(prepared, beat);
405
424
  cache.set(beat, sounding);
406
- return sounding.filter(({ track, voice }) => track !== excludeTrack || voice !== excludeVoice).map(({ snapshot }) => snapshot);
425
+ return sounding.filter(({ track, voice }) => track !== excludeTrack || voice !== excludeVoice);
426
+ }
427
+ function conflictPartner(sounding, reasons) {
428
+ const partner = {
429
+ trackIndex: sounding.trackIndex,
430
+ noteId: sounding.note.id,
431
+ reasons
432
+ };
433
+ if (sounding.note.originalIndex !== void 0) {
434
+ partner.originalIndex = sounding.note.originalIndex;
435
+ }
436
+ return partner;
437
+ }
438
+ function snapshotsOf(voices) {
439
+ return voices.map(({ snapshot }) => snapshot);
407
440
  }
408
441
  function analyzePolyphony(notes, chordAtBeat, key) {
409
442
  const passage = assertNoteEvents(notes, "notes", { allowNonPositiveDuration: true });
@@ -425,7 +458,7 @@ function analyzePolyphony(notes, chordAtBeat, key) {
425
458
  subVoice.voice,
426
459
  chordAtBeat,
427
460
  key,
428
- (beat) => otherVoicesSounding(prepared, 0, v, beat, soundingCache)
461
+ (beat) => snapshotsOf(otherVoicesSounding(prepared, 0, v, beat, soundingCache))
429
462
  );
430
463
  for (const note of analyzed) {
431
464
  if (note.originalIndex !== void 0) {
@@ -549,7 +582,7 @@ function analyzeArrangementWith(tracks, opts = {}, previous, dirty) {
549
582
  subVoice.voice,
550
583
  timeline.at,
551
584
  scaleAt,
552
- (beat) => otherVoicesSounding(prepared, t, v, beat, soundingCache)
585
+ (beat) => snapshotsOf(otherVoicesSounding(prepared, t, v, beat, soundingCache))
553
586
  );
554
587
  for (const note of analyzed) {
555
588
  notes.push({ ...note, trackIndex: track.trackIndex });
@@ -563,18 +596,19 @@ function analyzeArrangementWith(tracks, opts = {}, previous, dirty) {
563
596
  }
564
597
  for (const beat of evaluationBeats(note, timeline)) {
565
598
  const atOnset = sameInstant(beat, note.startBeat);
599
+ const others = otherVoicesSounding(prepared, t, v, beat, soundingCache);
566
600
  const safetyQuery = {
567
601
  profile,
568
602
  candidatePitch: note.pitch,
569
603
  prevPitch: atOnset ? preparedNote.prevPitch : note.pitch,
570
604
  chord: timeline.at(beat),
571
605
  key: scaleAt(beat),
572
- otherVoices: otherVoicesSounding(prepared, t, v, beat, soundingCache),
606
+ otherVoices: snapshotsOf(others),
573
607
  strongBeat: isStrongBeat(beat, meters)
574
608
  };
575
609
  let result = evaluateSafety(safetyQuery, { suggestions: false });
576
610
  if (result.safety !== 0 /* Safe */ && result.safety >= minSeverity) {
577
- result = evaluateSafety(safetyQuery);
611
+ result = evaluateSafety(safetyQuery, { partners: true });
578
612
  const conflict = {
579
613
  beat,
580
614
  trackName: track.name,
@@ -597,6 +631,11 @@ function analyzeArrangementWith(tracks, opts = {}, previous, dirty) {
597
631
  if (result.suggestions !== void 0) {
598
632
  conflict.suggestions = [...result.suggestions];
599
633
  }
634
+ if (result.partners !== void 0) {
635
+ conflict.partners = result.partners.map(
636
+ ({ index, reasons }) => conflictPartner(others[index], reasons)
637
+ );
638
+ }
600
639
  conflicts.push(conflict);
601
640
  }
602
641
  }
@@ -1052,549 +1091,6 @@ function hypermeter(notes, meter, opts = {}) {
1052
1091
  };
1053
1092
  }
1054
1093
 
1055
- // src/analyze/melody/internal.ts
1056
- var RHYTHM_GRID = 64;
1057
- function phraseNotes(phrase) {
1058
- if (Array.isArray(phrase)) {
1059
- return phrase;
1060
- }
1061
- return assertRecord(phrase, "melodic phrase").notes;
1062
- }
1063
- function orderedNotes(notes, name, budget) {
1064
- assertNoteEvents(notes, name, { allowNonPositiveDuration: true, budget });
1065
- const sounding = notes.filter((note) => note.durationBeat > 0).sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
1066
- const line = [];
1067
- for (const note of sounding) {
1068
- const previous = line[line.length - 1];
1069
- if (previous !== void 0 && Math.abs(note.startBeat - previous.startBeat) <= BEAT_EPS) {
1070
- line[line.length - 1] = note;
1071
- continue;
1072
- }
1073
- line.push(note);
1074
- }
1075
- return line;
1076
- }
1077
- function intervalsOf(notes) {
1078
- const out = [];
1079
- for (let i = 1; i < notes.length; i += 1) {
1080
- out.push((notes[i]?.pitch ?? 0) - (notes[i - 1]?.pitch ?? 0));
1081
- }
1082
- return out;
1083
- }
1084
- function onsetGaps(notes) {
1085
- const out = [];
1086
- for (let i = 1; i < notes.length; i += 1) {
1087
- out.push((notes[i]?.startBeat ?? 0) - (notes[i - 1]?.startBeat ?? 0));
1088
- }
1089
- return out;
1090
- }
1091
- function retrogradeGaps(notes) {
1092
- const out = [];
1093
- for (let i = notes.length - 1; i >= 1; i -= 1) {
1094
- const later = notes[i];
1095
- const earlier = notes[i - 1];
1096
- out.push(
1097
- (later?.startBeat ?? 0) + (later?.durationBeat ?? 0) - ((earlier?.startBeat ?? 0) + (earlier?.durationBeat ?? 0))
1098
- );
1099
- }
1100
- return out;
1101
- }
1102
- function rhythmProfile(gaps) {
1103
- const unit = gaps[0] ?? 0;
1104
- if (!(unit > BEAT_EPS)) {
1105
- return gaps.length === 0 ? [] : null;
1106
- }
1107
- return gaps.map((gap) => Math.round(gap / unit * RHYTHM_GRID) / RHYTHM_GRID);
1108
- }
1109
- function sameNumbers(a, b) {
1110
- if (a.length !== b.length) {
1111
- return false;
1112
- }
1113
- for (let i = 0; i < a.length; i += 1) {
1114
- if (Math.abs((a[i] ?? 0) - (b[i] ?? 0)) > BEAT_EPS) {
1115
- return false;
1116
- }
1117
- }
1118
- return true;
1119
- }
1120
- function roundTo(value, digits) {
1121
- const scale = 10 ** digits;
1122
- return Math.round(value * scale) / scale;
1123
- }
1124
- function endBeatOf(notes) {
1125
- let end = 0;
1126
- for (const note of notes) {
1127
- end = Math.max(end, note.startBeat + note.durationBeat);
1128
- }
1129
- return end;
1130
- }
1131
- function onsetSpan(notes) {
1132
- if (notes.length < 2) {
1133
- return 0;
1134
- }
1135
- return (notes[notes.length - 1]?.startBeat ?? 0) - (notes[0]?.startBeat ?? 0);
1136
- }
1137
-
1138
- // src/analyze/melody/contour.ts
1139
- var DIRECTIONAL_SHARE = 0.75;
1140
- function extremes(notes) {
1141
- if (notes.length === 0) {
1142
- return { peak: -1, trough: -1 };
1143
- }
1144
- let peak = 0;
1145
- let trough = 0;
1146
- for (let i = 1; i < notes.length; i += 1) {
1147
- if ((notes[i]?.pitch ?? 0) > (notes[peak]?.pitch ?? 0)) {
1148
- peak = i;
1149
- }
1150
- if ((notes[i]?.pitch ?? 0) < (notes[trough]?.pitch ?? 0)) {
1151
- trough = i;
1152
- }
1153
- }
1154
- return { peak, trough };
1155
- }
1156
- function melodicContour(notes) {
1157
- const line = orderedNotes(phraseNotes(notes), "contour notes");
1158
- const intervals = intervalsOf(line);
1159
- const directions = intervals.map(
1160
- (step) => step > 0 ? "up" : step < 0 ? "down" : "same"
1161
- );
1162
- const { peak, trough } = extremes(line);
1163
- const range = (line[peak]?.pitch ?? 0) - (line[trough]?.pitch ?? 0);
1164
- const motion = intervals.reduce((sum, step) => sum + Math.abs(step), 0);
1165
- if (motion === 0) {
1166
- return {
1167
- directions,
1168
- shape: "static",
1169
- peakIndex: peak,
1170
- troughIndex: trough,
1171
- range: 0,
1172
- 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"
1173
- };
1174
- }
1175
- const first = line[0]?.pitch ?? 0;
1176
- const last = line[line.length - 1]?.pitch ?? 0;
1177
- const net = last - first;
1178
- const directness = Math.abs(net) / motion;
1179
- const share = Math.round(directness * 100);
1180
- if (directness >= DIRECTIONAL_SHARE) {
1181
- const shape = net > 0 ? "ascending" : "descending";
1182
- return {
1183
- directions,
1184
- shape,
1185
- peakIndex: peak,
1186
- troughIndex: trough,
1187
- range,
1188
- rationale: `${share}% of the motion runs ${net > 0 ? "up" : "down"}, so the line is ${shape}`
1189
- };
1190
- }
1191
- const peakPitch = line[peak]?.pitch ?? 0;
1192
- const archMotion = peakPitch - first + (peakPitch - last);
1193
- if (peak > 0 && peak < line.length - 1 && archMotion >= DIRECTIONAL_SHARE * motion) {
1194
- return {
1195
- directions,
1196
- shape: "arch",
1197
- peakIndex: peak,
1198
- troughIndex: trough,
1199
- range,
1200
- rationale: `Rises to its peak at note ${peak + 1} of ${line.length} and falls back, so the line is an arch`
1201
- };
1202
- }
1203
- return {
1204
- directions,
1205
- shape: "wave",
1206
- peakIndex: peak,
1207
- troughIndex: trough,
1208
- range,
1209
- rationale: `Changes direction without settling either way (${share}% of the motion nets out), so the line is a wave`
1210
- };
1211
- }
1212
-
1213
- // src/analyze/melody/motifs.ts
1214
- var MIN_CELL_NOTES = 3;
1215
- var MAX_CELL_NOTES = 8;
1216
- var MIN_OCCURRENCES = 2;
1217
- function windowSignature(notes) {
1218
- const profile = rhythmProfile(onsetGaps(notes));
1219
- if (profile === null) {
1220
- return null;
1221
- }
1222
- return `${notes.length}|${intervalsOf(notes).join(",")}|${profile.join(",")}`;
1223
- }
1224
- function nonOverlapping(starts, length) {
1225
- const picked = [];
1226
- let lastEnd = Number.NEGATIVE_INFINITY;
1227
- for (const start of starts) {
1228
- if (start > lastEnd) {
1229
- picked.push(start);
1230
- lastEnd = start + length - 1;
1231
- }
1232
- }
1233
- return picked;
1234
- }
1235
- function subsumes(outer, inner) {
1236
- let at = 0;
1237
- for (const start of inner.starts) {
1238
- while (at + 1 < outer.starts.length && (outer.starts[at + 1] ?? 0) <= start) {
1239
- at += 1;
1240
- }
1241
- const from = outer.starts[at];
1242
- if (from === void 0 || start < from || start + inner.length > from + outer.length) {
1243
- return false;
1244
- }
1245
- }
1246
- return true;
1247
- }
1248
- function listBeats(occurrences) {
1249
- const shown = occurrences.slice(0, 5).map((occurrence) => roundTo(occurrence.startBeat, 3));
1250
- return occurrences.length > shown.length ? `${shown.join(", ")}, and ${occurrences.length - shown.length} more` : shown.join(", ");
1251
- }
1252
- function motifFromGroup(group, notes) {
1253
- const first = group.starts[0] ?? 0;
1254
- const prime = notes.slice(first, first + group.length).map((note) => ({ ...note }));
1255
- const primePitch = prime[0]?.pitch ?? 0;
1256
- const primeSpan = onsetSpan(prime);
1257
- const occurrences = group.starts.map((start) => {
1258
- const statement = notes.slice(start, start + group.length);
1259
- const span = onsetSpan(statement);
1260
- return {
1261
- noteIndex: start,
1262
- startBeat: statement[0]?.startBeat ?? 0,
1263
- endBeat: endBeatOf(statement),
1264
- transpose: (statement[0]?.pitch ?? 0) - primePitch,
1265
- timeRatio: primeSpan > BEAT_EPS ? span / primeSpan : 1
1266
- };
1267
- });
1268
- const shape = melodicContour(prime).shape;
1269
- const transposed = occurrences.some((occurrence) => occurrence.transpose !== 0);
1270
- const stretched = occurrences.some((occurrence) => Math.abs(occurrence.timeRatio - 1) > BEAT_EPS);
1271
- const detail = [
1272
- transposed ? "at more than one pitch level" : "at the same pitch level",
1273
- stretched ? "and in more than one set of note values" : ""
1274
- ].filter((part) => part !== "").join(" ");
1275
- return {
1276
- notes: prime,
1277
- intervals: intervalsOf(prime),
1278
- rhythm: rhythmProfile(onsetGaps(prime)) ?? [],
1279
- occurrences,
1280
- rationale: `${group.length}-note ${shape} cell stated ${occurrences.length} ${occurrences.length === 1 ? "time" : "times"} (beats ${listBeats(occurrences)}), ${detail}`
1281
- };
1282
- }
1283
- function extractMotifs(notes, opts = {}) {
1284
- const asked = assertOptions(opts, "opts");
1285
- const minNotes = assertInteger(asked.minNotes ?? MIN_CELL_NOTES, "motif minNotes", 2, 64);
1286
- const maxNotes = assertInteger(asked.maxNotes ?? MAX_CELL_NOTES, "motif maxNotes", 2, 64);
1287
- if (maxNotes < minNotes) {
1288
- throw new InvalidInputError(
1289
- `motif maxNotes must be at least minNotes ${minNotes}; received ${maxNotes}`
1290
- );
1291
- }
1292
- const minOccurrences = assertInteger(
1293
- asked.minOccurrences ?? MIN_OCCURRENCES,
1294
- "motif minOccurrences",
1295
- 2
1296
- );
1297
- const sounding = orderedNotes(notes, "melody notes", asked.budget);
1298
- if (sounding.length < minNotes) {
1299
- return [];
1300
- }
1301
- assertGenerationBudget(
1302
- sounding.length * (maxNotes - minNotes + 1),
1303
- "motif windows",
1304
- asked.budget
1305
- );
1306
- const groups = /* @__PURE__ */ new Map();
1307
- for (let length = minNotes; length <= maxNotes; length += 1) {
1308
- for (let start = 0; start + length <= sounding.length; start += 1) {
1309
- const signature = windowSignature(sounding.slice(start, start + length));
1310
- if (signature === null) {
1311
- continue;
1312
- }
1313
- const group = groups.get(signature);
1314
- if (group === void 0) {
1315
- groups.set(signature, { signature, length, starts: [start] });
1316
- } else {
1317
- group.starts.push(start);
1318
- }
1319
- }
1320
- }
1321
- const candidates = [...groups.values()].map((group) => ({ ...group, starts: nonOverlapping(group.starts, group.length) })).filter((group) => group.starts.length >= minOccurrences).sort(
1322
- (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)
1323
- );
1324
- const keptByLength = /* @__PURE__ */ new Map();
1325
- const kept = [];
1326
- let comparisons = 0;
1327
- for (const candidate of candidates) {
1328
- const first = candidate.starts[0] ?? 0;
1329
- let covered = false;
1330
- for (let length = candidate.length + 1; length <= maxNotes && !covered; length += 1) {
1331
- const byStart = keptByLength.get(length);
1332
- if (byStart === void 0) {
1333
- continue;
1334
- }
1335
- for (let from = Math.max(0, first - (length - candidate.length)); from <= first; from += 1) {
1336
- comparisons += 1;
1337
- const outer = byStart.get(from);
1338
- if (outer === void 0 || outer.starts.length < candidate.starts.length) {
1339
- continue;
1340
- }
1341
- comparisons += outer.starts.length + candidate.starts.length;
1342
- if (subsumes(outer, candidate)) {
1343
- covered = true;
1344
- break;
1345
- }
1346
- }
1347
- }
1348
- assertGenerationBudget(comparisons, "motif subsumption comparisons", asked.budget);
1349
- if (!covered) {
1350
- kept.push(candidate);
1351
- let byStart = keptByLength.get(candidate.length);
1352
- if (byStart === void 0) {
1353
- byStart = /* @__PURE__ */ new Map();
1354
- keptByLength.set(candidate.length, byStart);
1355
- }
1356
- for (const start of candidate.starts) {
1357
- byStart.set(start, candidate);
1358
- }
1359
- }
1360
- }
1361
- return kept.map((group) => motifFromGroup(group, sounding));
1362
- }
1363
- function motifFromNotes(notes) {
1364
- const sounding = orderedNotes(notes, "motif notes").map((note) => ({ ...note }));
1365
- return motifFromGroup(
1366
- { signature: windowSignature(sounding) ?? "", length: sounding.length, starts: [0] },
1367
- sounding
1368
- );
1369
- }
1370
-
1371
- // src/analyze/melody/relation.ts
1372
- function spellingOf(key) {
1373
- if (key === void 0) {
1374
- return "sharp";
1375
- }
1376
- return keySignatureFifths(key.tonic, key.scale) < 0 ? "flat" : "sharp";
1377
- }
1378
- function intervalBetween(from, to, key) {
1379
- const spelling = spellingOf(key);
1380
- return spelledInterval(midiToNote(from, spelling), midiToNote(to, spelling));
1381
- }
1382
- function intervalName(interval) {
1383
- return `${interval.descending && interval.semitones !== 0 ? "-" : ""}${interval.quality}${interval.number}`;
1384
- }
1385
- function ladderIndices(notes, key) {
1386
- const root = pitchClassOf(key.rootPc);
1387
- const offsets = scaleTonesInDegreeOrder(key).map((pc) => pitchClassOf(pc - root));
1388
- if (offsets.length === 0) {
1389
- return null;
1390
- }
1391
- const out = [];
1392
- for (const note of notes) {
1393
- const semitones = note.pitch - root;
1394
- const octave = Math.floor(semitones / 12);
1395
- const degree = offsets.indexOf(semitones - octave * 12);
1396
- if (degree < 0) {
1397
- return null;
1398
- }
1399
- out.push(octave * offsets.length + degree);
1400
- }
1401
- return out;
1402
- }
1403
- function ladderSteps(indices) {
1404
- const out = [];
1405
- for (let i = 1; i < indices.length; i += 1) {
1406
- out.push((indices[i] ?? 0) - (indices[i - 1] ?? 0));
1407
- }
1408
- return out;
1409
- }
1410
- function pitchShapes(a, b) {
1411
- const shapes = [];
1412
- if (sameNumbers(a, b)) {
1413
- shapes.push("exact");
1414
- }
1415
- if (sameNumbers(
1416
- b,
1417
- a.map((step) => -step)
1418
- )) {
1419
- shapes.push("inversion");
1420
- }
1421
- const reversed = [...a].reverse();
1422
- if (sameNumbers(
1423
- b,
1424
- reversed.map((step) => -step)
1425
- )) {
1426
- shapes.push("retrograde");
1427
- }
1428
- if (sameNumbers(b, reversed)) {
1429
- shapes.push("retrogradeInversion");
1430
- }
1431
- return shapes;
1432
- }
1433
- function totalGap(gaps) {
1434
- return gaps.reduce((sum, gap) => sum + gap, 0);
1435
- }
1436
- function stretchRatio(modelGaps, answerGaps) {
1437
- const span = totalGap(modelGaps);
1438
- return span > BEAT_EPS ? totalGap(answerGaps) / span : 1;
1439
- }
1440
- function stretchPhrase(ratio) {
1441
- return `note values ${roundTo(ratio, 3)}x the model's`;
1442
- }
1443
- function relateMotifs(a, b, keyLike) {
1444
- const key = keyLike === void 0 ? void 0 : resolveKey(keyLike);
1445
- const model = assertArray(assertRecord(a, "a").notes, "a.notes");
1446
- const answer = assertArray(assertRecord(b, "b").notes, "b.notes");
1447
- if (model.length === 0 || model.length !== answer.length) {
1448
- return null;
1449
- }
1450
- const modelGaps = onsetGaps(model);
1451
- const answerGaps = onsetGaps(answer);
1452
- const modelProfile = rhythmProfile(modelGaps);
1453
- const answerProfile = rhythmProfile(answerGaps);
1454
- const retroGaps = retrogradeGaps(model);
1455
- const retroProfile = rhythmProfile(retroGaps);
1456
- if (modelProfile === null || answerProfile === null) {
1457
- return null;
1458
- }
1459
- const forward = sameNumbers(modelProfile, answerProfile);
1460
- const backward = retroProfile !== null && sameNumbers(retroProfile, answerProfile);
1461
- const semitones = (answer[0]?.pitch ?? 0) - (model[0]?.pitch ?? 0);
1462
- const interval = intervalBetween(model[0]?.pitch ?? 0, answer[0]?.pitch ?? 0, key);
1463
- const gap = (answer[0]?.startBeat ?? 0) - endBeatOf(model);
1464
- const sequence = gap >= -HUMANIZE_ADJACENCY && gap <= HUMANIZE_ADJACENCY;
1465
- const timeRatio = stretchRatio(modelGaps, answerGaps);
1466
- const stretched = Math.abs(timeRatio - 1) > BEAT_EPS;
1467
- const shapes = pitchShapes(intervalsOf(model), intervalsOf(answer));
1468
- const base = { sequence, semitones, interval, timeRatio };
1469
- const backwardBase = { ...base, timeRatio: stretchRatio(retroGaps, answerGaps) };
1470
- if (forward && shapes.includes("exact")) {
1471
- if (semitones === 0) {
1472
- if (!stretched) {
1473
- return {
1474
- ...base,
1475
- kind: "repetition",
1476
- rationale: "Exact repetition: the same pitches in the same rhythm"
1477
- };
1478
- }
1479
- const augmented = timeRatio > 1;
1480
- return {
1481
- ...base,
1482
- kind: augmented ? "augmentation" : "diminution",
1483
- rationale: `${augmented ? "Augmentation" : "Diminution"}: the same pitches in ${stretchPhrase(timeRatio)}`
1484
- };
1485
- }
1486
- const name = intervalName(interval);
1487
- return {
1488
- ...base,
1489
- kind: "transposition",
1490
- 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)}` : "")
1491
- };
1492
- }
1493
- if (forward && key !== void 0) {
1494
- const modelLadder = ladderIndices(model, key.scale);
1495
- const answerLadder = ladderIndices(answer, key.scale);
1496
- if (modelLadder !== null && answerLadder !== null) {
1497
- const degrees = (answerLadder[0] ?? 0) - (modelLadder[0] ?? 0);
1498
- if (degrees !== 0 && sameNumbers(ladderSteps(modelLadder), ladderSteps(answerLadder))) {
1499
- const direction = degrees > 0 ? "higher" : "lower";
1500
- const count = Math.abs(degrees);
1501
- return {
1502
- ...base,
1503
- kind: "tonalTransposition",
1504
- degrees,
1505
- 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`
1506
- };
1507
- }
1508
- }
1509
- }
1510
- if (forward && shapes.includes("inversion")) {
1511
- return {
1512
- ...base,
1513
- kind: "inversion",
1514
- rationale: "Inversion: every interval turned upside down about the first note" + (stretched ? `, in ${stretchPhrase(timeRatio)}` : "")
1515
- };
1516
- }
1517
- if (backward && shapes.includes("retrograde")) {
1518
- return {
1519
- ...backwardBase,
1520
- kind: "retrograde",
1521
- rationale: "Retrograde: the cell read back to front, rhythm included"
1522
- };
1523
- }
1524
- if (backward && shapes.includes("retrogradeInversion")) {
1525
- return {
1526
- ...backwardBase,
1527
- kind: "retrogradeInversion",
1528
- rationale: "Retrograde inversion: the cell read back to front and turned upside down"
1529
- };
1530
- }
1531
- return null;
1532
- }
1533
-
1534
- // src/analyze/melody/similarity.ts
1535
- var INTERVAL_TOLERANCE = 6;
1536
- var PITCH_WEIGHT = 0.75;
1537
- var RHYTHM_WEIGHT = 0.25;
1538
- function gradedEditDistance(a, b, cost) {
1539
- let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
1540
- for (let i = 1; i <= a.length; i += 1) {
1541
- const row = new Array(b.length + 1);
1542
- row[0] = i;
1543
- for (let j = 1; j <= b.length; j += 1) {
1544
- const substitution = (previous[j - 1] ?? 0) + cost(a[i - 1] ?? 0, b[j - 1] ?? 0);
1545
- const deletion = (previous[j] ?? 0) + 1;
1546
- const insertion = (row[j - 1] ?? 0) + 1;
1547
- row[j] = Math.min(substitution, deletion, insertion);
1548
- }
1549
- previous = row;
1550
- }
1551
- return previous[b.length] ?? 0;
1552
- }
1553
- function distanceToSimilarity(distance, a, b) {
1554
- const longest = Math.max(a, b);
1555
- if (longest === 0) {
1556
- return 1;
1557
- }
1558
- return Math.min(1, Math.max(0, 1 - distance / longest));
1559
- }
1560
- function intervalCost(x, y) {
1561
- return Math.min(1, Math.abs(x - y) / INTERVAL_TOLERANCE);
1562
- }
1563
- function gapCost(x, y) {
1564
- if (!(x > BEAT_EPS) || !(y > BEAT_EPS)) {
1565
- return Math.abs(x - y) > BEAT_EPS ? 1 : 0;
1566
- }
1567
- return Math.min(1, Math.abs(Math.log2(x / y)));
1568
- }
1569
- function compareMelodies(a, b) {
1570
- const left = orderedNotes(phraseNotes(a), "melody a");
1571
- const right = orderedNotes(phraseNotes(b), "melody b");
1572
- const leftIntervals = intervalsOf(left);
1573
- const rightIntervals = intervalsOf(right);
1574
- const pitchSimilarity = distanceToSimilarity(
1575
- gradedEditDistance(leftIntervals, rightIntervals, intervalCost),
1576
- leftIntervals.length,
1577
- rightIntervals.length
1578
- );
1579
- const leftGaps = rhythmProfile(onsetGaps(left)) ?? onsetGaps(left);
1580
- const rightGaps = rhythmProfile(onsetGaps(right)) ?? onsetGaps(right);
1581
- const rhythmSimilarity = distanceToSimilarity(
1582
- gradedEditDistance(leftGaps, rightGaps, gapCost),
1583
- leftGaps.length,
1584
- rightGaps.length
1585
- );
1586
- const similarity = PITCH_WEIGHT * pitchSimilarity + RHYTHM_WEIGHT * rhythmSimilarity;
1587
- return {
1588
- similarity,
1589
- pitchSimilarity,
1590
- rhythmSimilarity,
1591
- rationale: `Interval shapes ${Math.round(pitchSimilarity * 100)}% alike, rhythms ${Math.round(rhythmSimilarity * 100)}% alike, weighted ${PITCH_WEIGHT}/${RHYTHM_WEIGHT} into ${roundTo(similarity, 3)}`
1592
- };
1593
- }
1594
- function melodicSimilarity(a, b) {
1595
- return compareMelodies(a, b).similarity;
1596
- }
1597
-
1598
1094
  // src/analyze/form/phrase.ts
1599
1095
  var CADENCE_BOUNDARY_STRENGTH = {
1600
1096
  authentic: 0.9,
@@ -1692,17 +1188,6 @@ function choosePhrasePath(beats, strengths, minPhraseBeats, expected) {
1692
1188
  function arrivalEnd(timeline, atBeat) {
1693
1189
  return segmentStartingAt(timeline.segments, atBeat)?.endBeat ?? atBeat;
1694
1190
  }
1695
- function barSpanOf(startBeat, endBeat, meters) {
1696
- const firstBar = barIndexAt(startBeat, meters);
1697
- const lastBar = barIndexAt(endBeat, meters);
1698
- const partOf = (beat, bar) => {
1699
- const barStart = barPositionToBeat({ bar, beat: 0 }, meters);
1700
- const barLength = beatsPerBarAt(barStart, meters);
1701
- return barLength > 0 ? (beat - barStart) / barLength : 0;
1702
- };
1703
- const bars = lastBar - firstBar + partOf(endBeat, lastBar) - partOf(startBeat, firstBar);
1704
- return Math.round(bars * 100) / 100;
1705
- }
1706
1191
  function keyContextFor(opts, notes, meters, totalBeats) {
1707
1192
  if (opts.key !== void 0) {
1708
1193
  return opts.key;
@@ -1912,7 +1397,7 @@ function phrasesFromTimeline(timeline, notes, opts = {}) {
1912
1397
  isLast
1913
1398
  );
1914
1399
  const signals = cadence === null ? reading.signals.filter((signal) => signal !== "cadence") : reading.signals;
1915
- const bars = barSpanOf(startBeat, endBeat, meters);
1400
+ const bars = Math.round(barSpanOf(startBeat, endBeat, meters) * 100) / 100;
1916
1401
  phrases.push({
1917
1402
  startBeat,
1918
1403
  endBeat,
@@ -2098,156 +1583,277 @@ function sectionsFromNotes(notes, opts = {}) {
2098
1583
  return sections;
2099
1584
  }
2100
1585
 
2101
- // src/analyze/reduction/index.ts
2102
- var TONIC_ARRIVING = /* @__PURE__ */ new Set(["authentic", "plagal", "modal", "deceptive"]);
2103
- function signedStep(fromPc, toPc) {
2104
- return (pitchClassOf(toPc) - pitchClassOf(fromPc) + 18) % 12 - 6;
2105
- }
2106
- function isStep(distance) {
2107
- const size = Math.abs(distance);
2108
- return size === 1 || size === 2;
2109
- }
2110
- function touches(before, after) {
2111
- return Math.abs(after.startBeat - before.endBeat) <= BEAT_EPS;
2112
- }
2113
- function salienceFloors(durations) {
2114
- const order = durations.map((_, index) => index).sort((a, b) => {
2115
- const left = durations[a] ?? 0;
2116
- const right = durations[b] ?? 0;
2117
- return left - right || a - b;
2118
- });
2119
- const rankOf = new Array(durations.length).fill(0);
2120
- for (let rank = 0; rank < order.length; rank += 1) {
2121
- rankOf[order[rank] ?? 0] = rank;
2122
- }
2123
- const floors = [];
2124
- const remaining = durations.length - 1;
2125
- for (let index = 0; index < durations.length; index += 1) {
2126
- if (remaining <= 0) {
2127
- floors.push(0);
2128
- continue;
1586
+ // src/analyze/reference/compare.ts
1587
+ var RATIONALE_FIELDS = 2;
1588
+ function rationaleOf(comparison) {
1589
+ const measured = [];
1590
+ for (const group of [comparison.form, comparison.harmony, comparison.melody, comparison.rhythm]) {
1591
+ for (const [name, value] of Object.entries(group)) {
1592
+ if (value !== null) {
1593
+ measured.push({ name, value, order: measured.length });
1594
+ }
2129
1595
  }
2130
- const skipped = rankOf[index] ?? 0;
2131
- const at = (position) => durations[order[position < skipped ? position : position + 1] ?? 0] ?? 0;
2132
- const middle = remaining >> 1;
2133
- floors.push(remaining % 2 === 1 ? at(middle) : (at(middle - 1) + at(middle)) / 2);
2134
- }
2135
- return floors;
2136
- }
2137
- function isTonicChord(chord, key) {
2138
- return pitchClassOf(chord.rootPc - key.rootPc) === 0 && !isAppliedDominant(chord, key);
2139
- }
2140
- function functionalReason(segment, before, after, keyAt) {
2141
- const key = keyAt(segment.startBeat);
2142
- if (after !== void 0 && isCadentialSixFour(segment.chord, after.chord, key)) {
2143
- return { kind: "cadentialSixFour" };
2144
- }
2145
- if (isTonicChord(segment.chord, key)) {
2146
- return { kind: "tonic" };
2147
- }
2148
- if (isDominantChordOf(segment.chord, key)) {
2149
- return { kind: "dominant" };
2150
- }
2151
- if (before !== void 0) {
2152
- const arrival = detectCadence(before.chord, segment.chord, key).type;
2153
- if (arrival !== null) {
2154
- return { kind: "cadenceArrival", cadence: arrival };
2155
- }
2156
- }
2157
- if (after !== void 0) {
2158
- const agent = detectCadence(segment.chord, after.chord, keyAt(after.startBeat)).type;
2159
- if (agent !== null && TONIC_ARRIVING.has(agent)) {
2160
- return { kind: "cadenceAgent", cadence: agent };
2161
- }
2162
- }
2163
- return null;
2164
- }
2165
- function figureReason(segment, before, after) {
2166
- if (before === void 0 || after === void 0) {
2167
- return null;
2168
- }
2169
- const incoming = signedStep(before.chord.rootPc, segment.chord.rootPc);
2170
- const outgoing = signedStep(segment.chord.rootPc, after.chord.rootPc);
2171
- if (!isStep(incoming) || !isStep(outgoing)) {
2172
- return null;
2173
- }
2174
- if (incoming > 0 === outgoing > 0) {
2175
- return { kind: "passing" };
2176
- }
2177
- return pitchClassOf(before.chord.rootPc) === pitchClassOf(after.chord.rootPc) ? { kind: "neighbor" } : null;
2178
- }
2179
- function describe(reason) {
2180
- switch (reason.kind) {
2181
- case "tonic":
2182
- return "Structural: the tonic of the key, which the progression is heard against";
2183
- case "dominant":
2184
- return "Structural: the dominant of the key, the chord its tonic is approached from";
2185
- case "cadentialSixFour":
2186
- return "Structural: a tonic six-four on the bass of the dominant that follows, which prolongs that dominant rather than framing the tonic";
2187
- case "cadenceArrival":
2188
- return `Structural: the arrival of a ${reason.cadence} cadence`;
2189
- case "cadenceAgent":
2190
- return `Structural: the chord that drives a ${reason.cadence} cadence`;
2191
- case "salient":
2192
- return "Structural: it holds the harmony longer than the chords around it";
2193
- case "first":
2194
- return "Structural: the first chord of the progression frames it";
2195
- case "last":
2196
- return "Structural: the last chord of the progression frames it";
2197
- case "unfigured":
2198
- return "Structural: its root neither steps through to the next chord nor returns to the last, so it embellishes nothing";
2199
- case "passing":
2200
- return "Passing: its root steps in from the previous chord and on to the next in the same direction";
2201
- case "neighbor":
2202
- return "Neighbor: its root steps away from the surrounding harmony and returns to it";
2203
- }
2204
- }
2205
- var BASIS_READINGS = {
2206
- function: ({ segment, before, after, keyAt }) => functionalReason(segment, before, after, keyAt),
2207
- duration: ({ segment, salienceFloor }) => segment.endBeat - segment.startBeat > salienceFloor ? { kind: "salient" } : null
2208
- };
2209
- var REDUCTION_BASES = Object.keys(BASIS_READINGS);
2210
- function levelOf(reason) {
2211
- if (reason.kind === "passing") {
2212
- return "passing";
2213
1596
  }
2214
- return reason.kind === "neighbor" ? "neighbor" : "structural";
1597
+ if (measured.length === 0) {
1598
+ return "Nothing to compare: neither profile has material for any field";
1599
+ }
1600
+ const describe = (entries) => entries.map((entry) => `${entry.name} ${Math.round(entry.value * 100)}%`).join(", ");
1601
+ const highest = [...measured].sort((x, y) => y.value - x.value || x.order - y.order);
1602
+ const lowest = [...measured].sort((x, y) => x.value - y.value || x.order - y.order);
1603
+ return `Most alike: ${describe(highest.slice(0, RATIONALE_FIELDS))}; least alike: ${describe(lowest.slice(0, RATIONALE_FIELDS))}`;
1604
+ }
1605
+ function compareReferences(a, b, opts) {
1606
+ assertReferenceProfile(a, "reference profile a");
1607
+ assertReferenceProfile(b, "reference profile b");
1608
+ const { budget } = assertOptions(opts, "compare options");
1609
+ const structural = (profile) => profile.harmony.chords.filter((chord) => chord.level === "structural");
1610
+ const cadences = (profile) => profile.form.phrases.map((phrase) => phrase.cadence?.type ?? null);
1611
+ const phraseMelodies = (profile) => profile.form.phrases.map((phrase) => phrase.melody);
1612
+ const comparison = {
1613
+ form: {
1614
+ sectionSequenceSimilarity: sectionSequenceSimilarity(
1615
+ a.form.sections.map((section) => section.label),
1616
+ b.form.sections.map((section) => section.label)
1617
+ ),
1618
+ phraseLengthSimilarity: phraseLengthSimilarity(
1619
+ a.form.phrases.map((phrase) => phrase.bars),
1620
+ b.form.phrases.map((phrase) => phrase.bars)
1621
+ )
1622
+ },
1623
+ harmony: {
1624
+ progressionSimilarity: progressionSimilarity(structural(a), structural(b), budget),
1625
+ functionSimilarity: functionSimilarity(a.harmony.chords, b.harmony.chords),
1626
+ cadenceSimilarity: cadenceSimilarity(cadences(a), cadences(b)),
1627
+ keyPlanSimilarity: keyPlanSimilarity(
1628
+ a.harmony.keys.map((region) => region.key),
1629
+ b.harmony.keys.map((region) => region.key)
1630
+ ),
1631
+ harmonicRhythmOnsetSimilarity: onsetSimilarity(a.harmony.rhythm, b.harmony.rhythm),
1632
+ harmonicRhythmDensitySimilarity: densitySimilarity(a.harmony.rhythm, b.harmony.rhythm)
1633
+ },
1634
+ melody: {
1635
+ surfaceSimilarity: surfaceSimilarity(a.melody.motifs, b.melody.motifs),
1636
+ motifStructureSimilarity: motifStructureSimilarity(
1637
+ { graph: a.melody.graph, spanBeats: a.span.endBeat - a.span.startBeat },
1638
+ { graph: b.melody.graph, spanBeats: b.span.endBeat - b.span.startBeat }
1639
+ ),
1640
+ contourSimilarity: contourSimilarity(phraseMelodies(a), phraseMelodies(b)),
1641
+ registerSimilarity: registerSimilarity(
1642
+ { phrases: phraseMelodies(a), mean: a.melody.register?.mean ?? null },
1643
+ { phrases: phraseMelodies(b), mean: b.melody.register?.mean ?? null }
1644
+ )
1645
+ },
1646
+ rhythm: {
1647
+ onsetSimilarity: onsetSimilarity(a.melody.rhythm, b.melody.rhythm),
1648
+ densitySimilarity: densitySimilarity(a.melody.rhythm, b.melody.rhythm),
1649
+ durationSimilarity: durationSimilarity(a.melody.rhythm, b.melody.rhythm),
1650
+ syncopationSimilarity: syncopationSimilarity(a.melody.rhythm, b.melody.rhythm)
1651
+ }
1652
+ };
1653
+ return { ...comparison, rationale: rationaleOf(comparison) };
2215
1654
  }
2216
- function reduceProgression(timeline, key, opts = {}) {
2217
- assertChordTimeline(timeline);
2218
- const asked = assertOptions(opts, "opts");
2219
- const keyAt = keyScaleAt(key);
2220
- const basis = assertOneOf(asked.basis ?? "function", REDUCTION_BASES, "reduction basis");
2221
- const reading = BASIS_READINGS[basis];
1655
+
1656
+ // src/analyze/reference/profile.ts
1657
+ function spanOf(sounding, timeline, meters, totalBeats) {
2222
1658
  const segments = timeline.segments;
2223
- const floors = basis === "duration" ? salienceFloors(segments.map((segment) => segment.endBeat - segment.startBeat)) : [];
2224
- const result = [];
2225
- for (let index = 0; index < segments.length; index += 1) {
2226
- const segment = segments[index];
2227
- if (segment === void 0) {
2228
- continue;
2229
- }
2230
- const previous = segments[index - 1];
2231
- const following = segments[index + 1];
2232
- const before = previous !== void 0 && touches(previous, segment) ? previous : void 0;
2233
- const after = following !== void 0 && touches(segment, following) ? following : void 0;
2234
- let reason = reading({ segment, before, after, keyAt, salienceFloor: floors[index] ?? 0 });
2235
- if (reason === null && index === 0) {
2236
- reason = { kind: "first" };
2237
- }
2238
- if (reason === null && index === segments.length - 1) {
2239
- reason = { kind: "last" };
2240
- }
2241
- reason ??= figureReason(segment, before, after) ?? { kind: "unfigured" };
2242
- result.push({
2243
- chord: segment.chord,
2244
- startBeat: segment.startBeat,
2245
- endBeat: segment.endBeat,
2246
- level: levelOf(reason),
2247
- rationale: describe(reason)
1659
+ const firstSegment = segments[0];
1660
+ const lastSegment = segments[segments.length - 1];
1661
+ const firstOnset = firstSoundingBeat(sounding, firstSegment?.startBeat ?? 0);
1662
+ const startBeat = barGridStart(
1663
+ Math.min(firstOnset, firstSegment?.startBeat ?? firstOnset),
1664
+ meters
1665
+ );
1666
+ const notesEnd = sounding.reduce(
1667
+ (end, note) => Math.max(end, note.startBeat + note.durationBeat),
1668
+ lastSegment?.endBeat ?? 0
1669
+ );
1670
+ return { startBeat, endBeat: Math.max(startBeat, totalBeats ?? notesEnd) };
1671
+ }
1672
+ function sameKey(a, b) {
1673
+ return pitchClassOf(a.scale.rootPc) === pitchClassOf(b.scale.rootPc) && a.scale.modeMask12 === b.scale.modeMask12;
1674
+ }
1675
+ function keyIndexAt(regions, beat, prevailing) {
1676
+ const holding = regions.findIndex((region) => region.startBeat <= beat && beat < region.endBeat);
1677
+ if (holding >= 0) {
1678
+ return holding;
1679
+ }
1680
+ return Math.max(
1681
+ 0,
1682
+ regions.findIndex((region) => sameKey(region.key, prevailing))
1683
+ );
1684
+ }
1685
+ function sectionAt(sections, beat) {
1686
+ const index = sections.findIndex(
1687
+ (section) => beat >= section.startBeat - BEAT_EPS && beat < section.endBeat - BEAT_EPS
1688
+ );
1689
+ return index < 0 ? null : index;
1690
+ }
1691
+ function readingsFor(notes, opts = {}) {
1692
+ const asked = assertOptions(opts, "reference options");
1693
+ const meters = resolveMeters(asked, "reference meters");
1694
+ const budget = asked.budget;
1695
+ if (asked.timeline === void 0) {
1696
+ return chordTimelineFromNotes(notes, {
1697
+ meters,
1698
+ key: asked.key,
1699
+ totalBeats: asked.totalBeats,
1700
+ budget
2248
1701
  });
2249
1702
  }
2250
- return result;
1703
+ const timeline = assertChordTimeline(asked.timeline, "reference timeline");
1704
+ let keys;
1705
+ if (asked.key === void 0) {
1706
+ keys = keyTimelineFromNotes(notes, { meters, totalBeats: asked.totalBeats, budget });
1707
+ } else {
1708
+ const sounding = assertNoteEvents(notes, "reference notes", {
1709
+ allowNonPositiveDuration: true,
1710
+ budget
1711
+ }).filter((note) => note.durationBeat > 0);
1712
+ const span = spanOf(sounding, timeline, meters, asked.totalBeats);
1713
+ keys = [
1714
+ {
1715
+ startBeat: span.startBeat,
1716
+ endBeat: span.endBeat,
1717
+ key: resolveKey(asked.key),
1718
+ confidence: 1
1719
+ }
1720
+ ];
1721
+ }
1722
+ return { timeline, keys, prevailingKey: prevailingKeyOf(keys) ?? resolveKey(majorKey(0)) };
1723
+ }
1724
+ function referenceFromReadings(notes, readings, opts = {}) {
1725
+ const asked = assertOptions(opts, "reference options");
1726
+ const meters = resolveMeters(asked, "reference meters");
1727
+ const budget = asked.budget;
1728
+ const sounding = assertNoteEvents(notes, "reference notes", {
1729
+ allowNonPositiveDuration: true,
1730
+ budget
1731
+ }).filter((note) => note.durationBeat > 0);
1732
+ if (asked.totalBeats !== void 0) {
1733
+ assertRange(asked.totalBeats, 0, Number.MAX_SAFE_INTEGER, "reference totalBeats");
1734
+ }
1735
+ const line = orderedNotes(asked.melody ?? notes, "reference melody", budget);
1736
+ const { timeline, prevailingKey } = readings;
1737
+ const span = spanOf(sounding, timeline, meters, asked.totalBeats);
1738
+ const spanEnd = span.endBeat;
1739
+ const keyAt = keyLookup(readings.keys, prevailingKey);
1740
+ const scaleAt = (beat) => scaleOf(keyAt(beat));
1741
+ const cadenceBeats = detectCadences(timeline, scaleAt).map((hit) => hit.atBeat);
1742
+ const grouping = hypermeter(sounding, meters, { cadenceBeats, totalBeats: spanEnd, budget });
1743
+ const phrases = phrasesFromTimeline(timeline, notes, {
1744
+ meters,
1745
+ key: scaleAt,
1746
+ hypermeter: grouping,
1747
+ totalBeats: spanEnd,
1748
+ budget
1749
+ });
1750
+ const cadences = /* @__PURE__ */ new Map();
1751
+ for (const closing of structuralCadences(phrases)) {
1752
+ const type = closing.cadence.type;
1753
+ if (type !== null) {
1754
+ cadences.set(closing.phraseIndex, {
1755
+ atBeat: closing.atBeat,
1756
+ type,
1757
+ weight: closing.weight
1758
+ });
1759
+ }
1760
+ }
1761
+ const sections = sectionsFromNotes(notes, {
1762
+ meters,
1763
+ unitBars: grouping.groupBars,
1764
+ totalBeats: spanEnd,
1765
+ budget
1766
+ });
1767
+ const keys = readings.keys.length === 0 && timeline.segments.length > 0 ? [
1768
+ {
1769
+ startBeat: span.startBeat,
1770
+ endBeat: spanEnd,
1771
+ key: resolveKey(prevailingKey),
1772
+ confidence: 0
1773
+ }
1774
+ ] : readings.keys.map((region) => ({
1775
+ startBeat: region.startBeat,
1776
+ endBeat: region.endBeat,
1777
+ key: resolveKey(region.key),
1778
+ confidence: region.confidence
1779
+ }));
1780
+ const chords = reduceProgression(timeline, scaleAt).map((reduced) => {
1781
+ const index = keyIndexAt(keys, (reduced.startBeat + reduced.endBeat) / 2, prevailingKey);
1782
+ const key = keys[index].key;
1783
+ return {
1784
+ startBeat: reduced.startBeat,
1785
+ endBeat: reduced.endBeat,
1786
+ key: index,
1787
+ roman: chordToRoman(reduced.chord, key),
1788
+ function: functionOf(reduced.chord, key),
1789
+ level: reduced.level
1790
+ };
1791
+ });
1792
+ const chordOnsets = timeline.segments.map((segment) => ({
1793
+ pitch: 60,
1794
+ startBeat: segment.startBeat,
1795
+ durationBeat: segment.endBeat - segment.startBeat
1796
+ }));
1797
+ const harmonyRhythm = analyzeRhythm(chordOnsets, { meters, totalBeats: spanEnd, budget });
1798
+ const motifs = extractMotifs(line, { budget });
1799
+ const graph = motifGraph(line, motifs, { key: prevailingKey, budget });
1800
+ let register = null;
1801
+ if (line.length > 0) {
1802
+ const pitches = line.map((note) => note.pitch);
1803
+ register = {
1804
+ low: Math.min(...pitches),
1805
+ high: Math.max(...pitches),
1806
+ mean: weightedMean(line)
1807
+ };
1808
+ }
1809
+ const lineMean = register?.mean ?? 0;
1810
+ const referenceMotifs = motifs.map((motif) => {
1811
+ const first = motif.occurrences[0];
1812
+ return {
1813
+ intervals: motif.intervals,
1814
+ rhythm: motif.rhythm,
1815
+ unitBeats: motif.notes.length < 2 ? 0 : motif.notes[1].startBeat - motif.notes[0].startBeat,
1816
+ spanBeats: first === void 0 ? 0 : first.endBeat - first.startBeat,
1817
+ occurrences: motif.occurrences.length
1818
+ };
1819
+ });
1820
+ const referencePhrases = phrases.map((phrase, index) => ({
1821
+ startBeat: phrase.startBeat,
1822
+ endBeat: phrase.endBeat,
1823
+ bars: barSpanOf(phrase.startBeat, phrase.endBeat, meters),
1824
+ section: sectionAt(sections, phrase.startBeat),
1825
+ confidence: phrase.confidence,
1826
+ cadence: cadences.get(index) ?? null,
1827
+ melody: phraseMelody(line, phrase.startBeat, phrase.endBeat, lineMean, meters, budget),
1828
+ motifNodes: graph.nodes.flatMap(
1829
+ (node, nodeIndex) => node.startBeat >= phrase.startBeat - BEAT_EPS && node.startBeat < phrase.endBeat - BEAT_EPS ? [nodeIndex] : []
1830
+ )
1831
+ }));
1832
+ return {
1833
+ profileVersion: REFERENCE_PROFILE_VERSION,
1834
+ span: {
1835
+ startBeat: span.startBeat,
1836
+ endBeat: spanEnd,
1837
+ bars: barSpanOf(span.startBeat, spanEnd, meters)
1838
+ },
1839
+ meters,
1840
+ form: {
1841
+ hypermeter: { groupBars: grouping.groupBars, confidence: grouping.confidence },
1842
+ sections,
1843
+ phrases: referencePhrases
1844
+ },
1845
+ harmony: { keys, chords, rhythm: harmonyRhythm },
1846
+ melody: {
1847
+ register,
1848
+ shape: line.length < 2 ? null : melodicContour(line).shape,
1849
+ motifs: referenceMotifs,
1850
+ graph,
1851
+ rhythm: analyzeRhythm(line, { meters, totalBeats: spanEnd, budget })
1852
+ }
1853
+ };
1854
+ }
1855
+ function analyzeReference(notes, opts) {
1856
+ return referenceFromReadings(notes, readingsFor(notes, opts), opts);
2251
1857
  }
2252
1858
 
2253
1859
  export {
@@ -2258,15 +1864,11 @@ export {
2258
1864
  tensionCurve,
2259
1865
  tensionCurveFrom,
2260
1866
  hypermeter,
2261
- melodicContour,
2262
- extractMotifs,
2263
- motifFromNotes,
2264
- relateMotifs,
2265
- compareMelodies,
2266
- melodicSimilarity,
2267
1867
  phrasesFromTimeline,
2268
1868
  structuralCadences,
2269
1869
  sectionsFromNotes,
2270
- reduceProgression
1870
+ compareReferences,
1871
+ referenceFromReadings,
1872
+ analyzeReference
2271
1873
  };
2272
- //# sourceMappingURL=chunk-HSY2VQXZ.js.map
1874
+ //# sourceMappingURL=chunk-MLBPXA4Z.js.map