@libraz/libcantus 1.2.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 (56) hide show
  1. package/README.md +4 -0
  2. package/README_ja.md +4 -0
  3. package/dist/analyze/index.cjs +28 -4
  4. package/dist/analyze/index.cjs.map +1 -1
  5. package/dist/analyze/index.d.cts +151 -5
  6. package/dist/analyze/index.d.ts +151 -5
  7. package/dist/analyze/index.js +34 -10
  8. package/dist/{chunk-DSMJRXCZ.cjs → chunk-4R2LWV7E.cjs} +384 -107
  9. package/dist/chunk-4R2LWV7E.cjs.map +1 -0
  10. package/dist/{chunk-J7FHVZII.cjs → chunk-B2PW6EHN.cjs} +2000 -30
  11. package/dist/chunk-B2PW6EHN.cjs.map +1 -0
  12. package/dist/{chunk-Q6LFSAKV.js → chunk-CPMB7P75.js} +1973 -3
  13. package/dist/chunk-CPMB7P75.js.map +1 -0
  14. package/dist/{chunk-H2KJHBW4.js → chunk-GNVN4PPJ.js} +1871 -5
  15. package/dist/chunk-GNVN4PPJ.js.map +1 -0
  16. package/dist/{chunk-SGLEQVJH.cjs → chunk-IFZDWIKM.cjs} +1868 -2
  17. package/dist/chunk-IFZDWIKM.cjs.map +1 -0
  18. package/dist/{chunk-FBH75DAY.js → chunk-IPEF6NWB.js} +287 -10
  19. package/dist/chunk-IPEF6NWB.js.map +1 -0
  20. package/dist/{chunk-HYJJ2V2I.js → chunk-MLBPXA4Z.js} +300 -720
  21. package/dist/chunk-MLBPXA4Z.js.map +1 -0
  22. package/dist/{chunk-HWHGOVO6.cjs → chunk-PWC4CRWT.cjs} +1 -1
  23. package/dist/{chunk-HWHGOVO6.cjs.map → chunk-PWC4CRWT.cjs.map} +1 -1
  24. package/dist/{chunk-HPVSH7SF.cjs → chunk-YFA7X2F6.cjs} +350 -770
  25. package/dist/chunk-YFA7X2F6.cjs.map +1 -0
  26. package/dist/{chunk-NFJ4P6ZZ.js → chunk-ZBUROTJQ.js} +1 -1
  27. package/dist/generate/index.cjs +15 -3
  28. package/dist/generate/index.cjs.map +1 -1
  29. package/dist/generate/index.d.cts +84 -6
  30. package/dist/generate/index.d.ts +84 -6
  31. package/dist/generate/index.js +14 -2
  32. package/dist/{index-BNSM-DSk.d.cts → index-CY-3NVfy.d.cts} +484 -3
  33. package/dist/{index-BJgP97N1.d.ts → index-zaZdmnU4.d.ts} +484 -3
  34. package/dist/index.cjs +43 -7
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +5 -5
  37. package/dist/index.d.ts +5 -5
  38. package/dist/index.js +48 -12
  39. package/dist/model/index.cjs +5 -5
  40. package/dist/model/index.d.cts +203 -7
  41. package/dist/model/index.d.ts +203 -7
  42. package/dist/model/index.js +4 -4
  43. package/dist/{index-DGCNKAUr.d.cts → profile-CQMEqdH9.d.cts} +65 -598
  44. package/dist/{index-BRMbPkHE.d.ts → profile-DLdlIpS9.d.ts} +65 -598
  45. package/dist/{index-BBWNZS0f.d.cts → types-D-XgxGvp.d.cts} +1169 -13
  46. package/dist/{index-CJBBiAmU.d.ts → types-Dh7C064C.d.ts} +1169 -13
  47. package/package.json +5 -5
  48. package/dist/chunk-DSMJRXCZ.cjs.map +0 -1
  49. package/dist/chunk-FBH75DAY.js.map +0 -1
  50. package/dist/chunk-H2KJHBW4.js.map +0 -1
  51. package/dist/chunk-HPVSH7SF.cjs.map +0 -1
  52. package/dist/chunk-HYJJ2V2I.js.map +0 -1
  53. package/dist/chunk-J7FHVZII.cjs.map +0 -1
  54. package/dist/chunk-Q6LFSAKV.js.map +0 -1
  55. package/dist/chunk-SGLEQVJH.cjs.map +0 -1
  56. /package/dist/{chunk-NFJ4P6ZZ.js.map → chunk-ZBUROTJQ.js.map} +0 -0
@@ -1,46 +1,65 @@
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-H2KJHBW4.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
62
+ scaleOf
44
63
  } from "./chunk-RFIBF2RR.js";
45
64
  import {
46
65
  BEAT_EPS,
@@ -61,10 +80,8 @@ import {
61
80
  beatsPerBarAt,
62
81
  describeRejected,
63
82
  isStrongBeat,
64
- midiToNote,
65
83
  pitchClassOf,
66
- resolveMeters,
67
- spelledInterval
84
+ resolveMeters
68
85
  } from "./chunk-AGOVW7U6.js";
69
86
 
70
87
  // src/analyze/arrange/internal.ts
@@ -1074,549 +1091,6 @@ function hypermeter(notes, meter, opts = {}) {
1074
1091
  };
1075
1092
  }
1076
1093
 
1077
- // src/analyze/melody/internal.ts
1078
- var RHYTHM_GRID = 64;
1079
- function phraseNotes(phrase) {
1080
- if (Array.isArray(phrase)) {
1081
- return phrase;
1082
- }
1083
- return assertRecord(phrase, "melodic phrase").notes;
1084
- }
1085
- function orderedNotes(notes, name, budget) {
1086
- assertNoteEvents(notes, name, { allowNonPositiveDuration: true, budget });
1087
- const sounding = notes.filter((note) => note.durationBeat > 0).sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
1088
- const line = [];
1089
- for (const note of sounding) {
1090
- const previous = line[line.length - 1];
1091
- if (previous !== void 0 && Math.abs(note.startBeat - previous.startBeat) <= BEAT_EPS) {
1092
- line[line.length - 1] = note;
1093
- continue;
1094
- }
1095
- line.push(note);
1096
- }
1097
- return line;
1098
- }
1099
- function intervalsOf(notes) {
1100
- const out = [];
1101
- for (let i = 1; i < notes.length; i += 1) {
1102
- out.push((notes[i]?.pitch ?? 0) - (notes[i - 1]?.pitch ?? 0));
1103
- }
1104
- return out;
1105
- }
1106
- function onsetGaps(notes) {
1107
- const out = [];
1108
- for (let i = 1; i < notes.length; i += 1) {
1109
- out.push((notes[i]?.startBeat ?? 0) - (notes[i - 1]?.startBeat ?? 0));
1110
- }
1111
- return out;
1112
- }
1113
- function retrogradeGaps(notes) {
1114
- const out = [];
1115
- for (let i = notes.length - 1; i >= 1; i -= 1) {
1116
- const later = notes[i];
1117
- const earlier = notes[i - 1];
1118
- out.push(
1119
- (later?.startBeat ?? 0) + (later?.durationBeat ?? 0) - ((earlier?.startBeat ?? 0) + (earlier?.durationBeat ?? 0))
1120
- );
1121
- }
1122
- return out;
1123
- }
1124
- function rhythmProfile(gaps) {
1125
- const unit = gaps[0] ?? 0;
1126
- if (!(unit > BEAT_EPS)) {
1127
- return gaps.length === 0 ? [] : null;
1128
- }
1129
- return gaps.map((gap) => Math.round(gap / unit * RHYTHM_GRID) / RHYTHM_GRID);
1130
- }
1131
- function sameNumbers(a, b) {
1132
- if (a.length !== b.length) {
1133
- return false;
1134
- }
1135
- for (let i = 0; i < a.length; i += 1) {
1136
- if (Math.abs((a[i] ?? 0) - (b[i] ?? 0)) > BEAT_EPS) {
1137
- return false;
1138
- }
1139
- }
1140
- return true;
1141
- }
1142
- function roundTo(value, digits) {
1143
- const scale = 10 ** digits;
1144
- return Math.round(value * scale) / scale;
1145
- }
1146
- function endBeatOf(notes) {
1147
- let end = 0;
1148
- for (const note of notes) {
1149
- end = Math.max(end, note.startBeat + note.durationBeat);
1150
- }
1151
- return end;
1152
- }
1153
- function onsetSpan(notes) {
1154
- if (notes.length < 2) {
1155
- return 0;
1156
- }
1157
- return (notes[notes.length - 1]?.startBeat ?? 0) - (notes[0]?.startBeat ?? 0);
1158
- }
1159
-
1160
- // src/analyze/melody/contour.ts
1161
- var DIRECTIONAL_SHARE = 0.75;
1162
- function extremes(notes) {
1163
- if (notes.length === 0) {
1164
- return { peak: -1, trough: -1 };
1165
- }
1166
- let peak = 0;
1167
- let trough = 0;
1168
- for (let i = 1; i < notes.length; i += 1) {
1169
- if ((notes[i]?.pitch ?? 0) > (notes[peak]?.pitch ?? 0)) {
1170
- peak = i;
1171
- }
1172
- if ((notes[i]?.pitch ?? 0) < (notes[trough]?.pitch ?? 0)) {
1173
- trough = i;
1174
- }
1175
- }
1176
- return { peak, trough };
1177
- }
1178
- function melodicContour(notes) {
1179
- const line = orderedNotes(phraseNotes(notes), "contour notes");
1180
- const intervals = intervalsOf(line);
1181
- const directions = intervals.map(
1182
- (step) => step > 0 ? "up" : step < 0 ? "down" : "same"
1183
- );
1184
- const { peak, trough } = extremes(line);
1185
- const range = (line[peak]?.pitch ?? 0) - (line[trough]?.pitch ?? 0);
1186
- const motion = intervals.reduce((sum, step) => sum + Math.abs(step), 0);
1187
- if (motion === 0) {
1188
- return {
1189
- directions,
1190
- shape: "static",
1191
- peakIndex: peak,
1192
- troughIndex: trough,
1193
- range: 0,
1194
- 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"
1195
- };
1196
- }
1197
- const first = line[0]?.pitch ?? 0;
1198
- const last = line[line.length - 1]?.pitch ?? 0;
1199
- const net = last - first;
1200
- const directness = Math.abs(net) / motion;
1201
- const share = Math.round(directness * 100);
1202
- if (directness >= DIRECTIONAL_SHARE) {
1203
- const shape = net > 0 ? "ascending" : "descending";
1204
- return {
1205
- directions,
1206
- shape,
1207
- peakIndex: peak,
1208
- troughIndex: trough,
1209
- range,
1210
- rationale: `${share}% of the motion runs ${net > 0 ? "up" : "down"}, so the line is ${shape}`
1211
- };
1212
- }
1213
- const peakPitch = line[peak]?.pitch ?? 0;
1214
- const archMotion = peakPitch - first + (peakPitch - last);
1215
- if (peak > 0 && peak < line.length - 1 && archMotion >= DIRECTIONAL_SHARE * motion) {
1216
- return {
1217
- directions,
1218
- shape: "arch",
1219
- peakIndex: peak,
1220
- troughIndex: trough,
1221
- range,
1222
- rationale: `Rises to its peak at note ${peak + 1} of ${line.length} and falls back, so the line is an arch`
1223
- };
1224
- }
1225
- return {
1226
- directions,
1227
- shape: "wave",
1228
- peakIndex: peak,
1229
- troughIndex: trough,
1230
- range,
1231
- rationale: `Changes direction without settling either way (${share}% of the motion nets out), so the line is a wave`
1232
- };
1233
- }
1234
-
1235
- // src/analyze/melody/motifs.ts
1236
- var MIN_CELL_NOTES = 3;
1237
- var MAX_CELL_NOTES = 8;
1238
- var MIN_OCCURRENCES = 2;
1239
- function windowSignature(notes) {
1240
- const profile = rhythmProfile(onsetGaps(notes));
1241
- if (profile === null) {
1242
- return null;
1243
- }
1244
- return `${notes.length}|${intervalsOf(notes).join(",")}|${profile.join(",")}`;
1245
- }
1246
- function nonOverlapping(starts, length) {
1247
- const picked = [];
1248
- let lastEnd = Number.NEGATIVE_INFINITY;
1249
- for (const start of starts) {
1250
- if (start > lastEnd) {
1251
- picked.push(start);
1252
- lastEnd = start + length - 1;
1253
- }
1254
- }
1255
- return picked;
1256
- }
1257
- function subsumes(outer, inner) {
1258
- let at = 0;
1259
- for (const start of inner.starts) {
1260
- while (at + 1 < outer.starts.length && (outer.starts[at + 1] ?? 0) <= start) {
1261
- at += 1;
1262
- }
1263
- const from = outer.starts[at];
1264
- if (from === void 0 || start < from || start + inner.length > from + outer.length) {
1265
- return false;
1266
- }
1267
- }
1268
- return true;
1269
- }
1270
- function listBeats(occurrences) {
1271
- const shown = occurrences.slice(0, 5).map((occurrence) => roundTo(occurrence.startBeat, 3));
1272
- return occurrences.length > shown.length ? `${shown.join(", ")}, and ${occurrences.length - shown.length} more` : shown.join(", ");
1273
- }
1274
- function motifFromGroup(group, notes) {
1275
- const first = group.starts[0] ?? 0;
1276
- const prime = notes.slice(first, first + group.length).map((note) => ({ ...note }));
1277
- const primePitch = prime[0]?.pitch ?? 0;
1278
- const primeSpan = onsetSpan(prime);
1279
- const occurrences = group.starts.map((start) => {
1280
- const statement = notes.slice(start, start + group.length);
1281
- const span = onsetSpan(statement);
1282
- return {
1283
- noteIndex: start,
1284
- startBeat: statement[0]?.startBeat ?? 0,
1285
- endBeat: endBeatOf(statement),
1286
- transpose: (statement[0]?.pitch ?? 0) - primePitch,
1287
- timeRatio: primeSpan > BEAT_EPS ? span / primeSpan : 1
1288
- };
1289
- });
1290
- const shape = melodicContour(prime).shape;
1291
- const transposed = occurrences.some((occurrence) => occurrence.transpose !== 0);
1292
- const stretched = occurrences.some((occurrence) => Math.abs(occurrence.timeRatio - 1) > BEAT_EPS);
1293
- const detail = [
1294
- transposed ? "at more than one pitch level" : "at the same pitch level",
1295
- stretched ? "and in more than one set of note values" : ""
1296
- ].filter((part) => part !== "").join(" ");
1297
- return {
1298
- notes: prime,
1299
- intervals: intervalsOf(prime),
1300
- rhythm: rhythmProfile(onsetGaps(prime)) ?? [],
1301
- occurrences,
1302
- rationale: `${group.length}-note ${shape} cell stated ${occurrences.length} ${occurrences.length === 1 ? "time" : "times"} (beats ${listBeats(occurrences)}), ${detail}`
1303
- };
1304
- }
1305
- function extractMotifs(notes, opts = {}) {
1306
- const asked = assertOptions(opts, "opts");
1307
- const minNotes = assertInteger(asked.minNotes ?? MIN_CELL_NOTES, "motif minNotes", 2, 64);
1308
- const maxNotes = assertInteger(asked.maxNotes ?? MAX_CELL_NOTES, "motif maxNotes", 2, 64);
1309
- if (maxNotes < minNotes) {
1310
- throw new InvalidInputError(
1311
- `motif maxNotes must be at least minNotes ${minNotes}; received ${maxNotes}`
1312
- );
1313
- }
1314
- const minOccurrences = assertInteger(
1315
- asked.minOccurrences ?? MIN_OCCURRENCES,
1316
- "motif minOccurrences",
1317
- 2
1318
- );
1319
- const sounding = orderedNotes(notes, "melody notes", asked.budget);
1320
- if (sounding.length < minNotes) {
1321
- return [];
1322
- }
1323
- assertGenerationBudget(
1324
- sounding.length * (maxNotes - minNotes + 1),
1325
- "motif windows",
1326
- asked.budget
1327
- );
1328
- const groups = /* @__PURE__ */ new Map();
1329
- for (let length = minNotes; length <= maxNotes; length += 1) {
1330
- for (let start = 0; start + length <= sounding.length; start += 1) {
1331
- const signature = windowSignature(sounding.slice(start, start + length));
1332
- if (signature === null) {
1333
- continue;
1334
- }
1335
- const group = groups.get(signature);
1336
- if (group === void 0) {
1337
- groups.set(signature, { signature, length, starts: [start] });
1338
- } else {
1339
- group.starts.push(start);
1340
- }
1341
- }
1342
- }
1343
- const candidates = [...groups.values()].map((group) => ({ ...group, starts: nonOverlapping(group.starts, group.length) })).filter((group) => group.starts.length >= minOccurrences).sort(
1344
- (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)
1345
- );
1346
- const keptByLength = /* @__PURE__ */ new Map();
1347
- const kept = [];
1348
- let comparisons = 0;
1349
- for (const candidate of candidates) {
1350
- const first = candidate.starts[0] ?? 0;
1351
- let covered = false;
1352
- for (let length = candidate.length + 1; length <= maxNotes && !covered; length += 1) {
1353
- const byStart = keptByLength.get(length);
1354
- if (byStart === void 0) {
1355
- continue;
1356
- }
1357
- for (let from = Math.max(0, first - (length - candidate.length)); from <= first; from += 1) {
1358
- comparisons += 1;
1359
- const outer = byStart.get(from);
1360
- if (outer === void 0 || outer.starts.length < candidate.starts.length) {
1361
- continue;
1362
- }
1363
- comparisons += outer.starts.length + candidate.starts.length;
1364
- if (subsumes(outer, candidate)) {
1365
- covered = true;
1366
- break;
1367
- }
1368
- }
1369
- }
1370
- assertGenerationBudget(comparisons, "motif subsumption comparisons", asked.budget);
1371
- if (!covered) {
1372
- kept.push(candidate);
1373
- let byStart = keptByLength.get(candidate.length);
1374
- if (byStart === void 0) {
1375
- byStart = /* @__PURE__ */ new Map();
1376
- keptByLength.set(candidate.length, byStart);
1377
- }
1378
- for (const start of candidate.starts) {
1379
- byStart.set(start, candidate);
1380
- }
1381
- }
1382
- }
1383
- return kept.map((group) => motifFromGroup(group, sounding));
1384
- }
1385
- function motifFromNotes(notes) {
1386
- const sounding = orderedNotes(notes, "motif notes").map((note) => ({ ...note }));
1387
- return motifFromGroup(
1388
- { signature: windowSignature(sounding) ?? "", length: sounding.length, starts: [0] },
1389
- sounding
1390
- );
1391
- }
1392
-
1393
- // src/analyze/melody/relation.ts
1394
- function spellingOf(key) {
1395
- if (key === void 0) {
1396
- return "sharp";
1397
- }
1398
- return keySignatureFifths(key.tonic, key.scale) < 0 ? "flat" : "sharp";
1399
- }
1400
- function intervalBetween(from, to, key) {
1401
- const spelling = spellingOf(key);
1402
- return spelledInterval(midiToNote(from, spelling), midiToNote(to, spelling));
1403
- }
1404
- function intervalName(interval) {
1405
- return `${interval.descending && interval.semitones !== 0 ? "-" : ""}${interval.quality}${interval.number}`;
1406
- }
1407
- function ladderIndices(notes, key) {
1408
- const root = pitchClassOf(key.rootPc);
1409
- const offsets = scaleTonesInDegreeOrder(key).map((pc) => pitchClassOf(pc - root));
1410
- if (offsets.length === 0) {
1411
- return null;
1412
- }
1413
- const out = [];
1414
- for (const note of notes) {
1415
- const semitones = note.pitch - root;
1416
- const octave = Math.floor(semitones / 12);
1417
- const degree = offsets.indexOf(semitones - octave * 12);
1418
- if (degree < 0) {
1419
- return null;
1420
- }
1421
- out.push(octave * offsets.length + degree);
1422
- }
1423
- return out;
1424
- }
1425
- function ladderSteps(indices) {
1426
- const out = [];
1427
- for (let i = 1; i < indices.length; i += 1) {
1428
- out.push((indices[i] ?? 0) - (indices[i - 1] ?? 0));
1429
- }
1430
- return out;
1431
- }
1432
- function pitchShapes(a, b) {
1433
- const shapes = [];
1434
- if (sameNumbers(a, b)) {
1435
- shapes.push("exact");
1436
- }
1437
- if (sameNumbers(
1438
- b,
1439
- a.map((step) => -step)
1440
- )) {
1441
- shapes.push("inversion");
1442
- }
1443
- const reversed = [...a].reverse();
1444
- if (sameNumbers(
1445
- b,
1446
- reversed.map((step) => -step)
1447
- )) {
1448
- shapes.push("retrograde");
1449
- }
1450
- if (sameNumbers(b, reversed)) {
1451
- shapes.push("retrogradeInversion");
1452
- }
1453
- return shapes;
1454
- }
1455
- function totalGap(gaps) {
1456
- return gaps.reduce((sum, gap) => sum + gap, 0);
1457
- }
1458
- function stretchRatio(modelGaps, answerGaps) {
1459
- const span = totalGap(modelGaps);
1460
- return span > BEAT_EPS ? totalGap(answerGaps) / span : 1;
1461
- }
1462
- function stretchPhrase(ratio) {
1463
- return `note values ${roundTo(ratio, 3)}x the model's`;
1464
- }
1465
- function relateMotifs(a, b, keyLike) {
1466
- const key = keyLike === void 0 ? void 0 : resolveKey(keyLike);
1467
- const model = assertArray(assertRecord(a, "a").notes, "a.notes");
1468
- const answer = assertArray(assertRecord(b, "b").notes, "b.notes");
1469
- if (model.length === 0 || model.length !== answer.length) {
1470
- return null;
1471
- }
1472
- const modelGaps = onsetGaps(model);
1473
- const answerGaps = onsetGaps(answer);
1474
- const modelProfile = rhythmProfile(modelGaps);
1475
- const answerProfile = rhythmProfile(answerGaps);
1476
- const retroGaps = retrogradeGaps(model);
1477
- const retroProfile = rhythmProfile(retroGaps);
1478
- if (modelProfile === null || answerProfile === null) {
1479
- return null;
1480
- }
1481
- const forward = sameNumbers(modelProfile, answerProfile);
1482
- const backward = retroProfile !== null && sameNumbers(retroProfile, answerProfile);
1483
- const semitones = (answer[0]?.pitch ?? 0) - (model[0]?.pitch ?? 0);
1484
- const interval = intervalBetween(model[0]?.pitch ?? 0, answer[0]?.pitch ?? 0, key);
1485
- const gap = (answer[0]?.startBeat ?? 0) - endBeatOf(model);
1486
- const sequence = gap >= -HUMANIZE_ADJACENCY && gap <= HUMANIZE_ADJACENCY;
1487
- const timeRatio = stretchRatio(modelGaps, answerGaps);
1488
- const stretched = Math.abs(timeRatio - 1) > BEAT_EPS;
1489
- const shapes = pitchShapes(intervalsOf(model), intervalsOf(answer));
1490
- const base = { sequence, semitones, interval, timeRatio };
1491
- const backwardBase = { ...base, timeRatio: stretchRatio(retroGaps, answerGaps) };
1492
- if (forward && shapes.includes("exact")) {
1493
- if (semitones === 0) {
1494
- if (!stretched) {
1495
- return {
1496
- ...base,
1497
- kind: "repetition",
1498
- rationale: "Exact repetition: the same pitches in the same rhythm"
1499
- };
1500
- }
1501
- const augmented = timeRatio > 1;
1502
- return {
1503
- ...base,
1504
- kind: augmented ? "augmentation" : "diminution",
1505
- rationale: `${augmented ? "Augmentation" : "Diminution"}: the same pitches in ${stretchPhrase(timeRatio)}`
1506
- };
1507
- }
1508
- const name = intervalName(interval);
1509
- return {
1510
- ...base,
1511
- kind: "transposition",
1512
- 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)}` : "")
1513
- };
1514
- }
1515
- if (forward && key !== void 0) {
1516
- const modelLadder = ladderIndices(model, key.scale);
1517
- const answerLadder = ladderIndices(answer, key.scale);
1518
- if (modelLadder !== null && answerLadder !== null) {
1519
- const degrees = (answerLadder[0] ?? 0) - (modelLadder[0] ?? 0);
1520
- if (degrees !== 0 && sameNumbers(ladderSteps(modelLadder), ladderSteps(answerLadder))) {
1521
- const direction = degrees > 0 ? "higher" : "lower";
1522
- const count = Math.abs(degrees);
1523
- return {
1524
- ...base,
1525
- kind: "tonalTransposition",
1526
- degrees,
1527
- 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`
1528
- };
1529
- }
1530
- }
1531
- }
1532
- if (forward && shapes.includes("inversion")) {
1533
- return {
1534
- ...base,
1535
- kind: "inversion",
1536
- rationale: "Inversion: every interval turned upside down about the first note" + (stretched ? `, in ${stretchPhrase(timeRatio)}` : "")
1537
- };
1538
- }
1539
- if (backward && shapes.includes("retrograde")) {
1540
- return {
1541
- ...backwardBase,
1542
- kind: "retrograde",
1543
- rationale: "Retrograde: the cell read back to front, rhythm included"
1544
- };
1545
- }
1546
- if (backward && shapes.includes("retrogradeInversion")) {
1547
- return {
1548
- ...backwardBase,
1549
- kind: "retrogradeInversion",
1550
- rationale: "Retrograde inversion: the cell read back to front and turned upside down"
1551
- };
1552
- }
1553
- return null;
1554
- }
1555
-
1556
- // src/analyze/melody/similarity.ts
1557
- var INTERVAL_TOLERANCE = 6;
1558
- var PITCH_WEIGHT = 0.75;
1559
- var RHYTHM_WEIGHT = 0.25;
1560
- function gradedEditDistance(a, b, cost) {
1561
- let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
1562
- for (let i = 1; i <= a.length; i += 1) {
1563
- const row = new Array(b.length + 1);
1564
- row[0] = i;
1565
- for (let j = 1; j <= b.length; j += 1) {
1566
- const substitution = (previous[j - 1] ?? 0) + cost(a[i - 1] ?? 0, b[j - 1] ?? 0);
1567
- const deletion = (previous[j] ?? 0) + 1;
1568
- const insertion = (row[j - 1] ?? 0) + 1;
1569
- row[j] = Math.min(substitution, deletion, insertion);
1570
- }
1571
- previous = row;
1572
- }
1573
- return previous[b.length] ?? 0;
1574
- }
1575
- function distanceToSimilarity(distance, a, b) {
1576
- const longest = Math.max(a, b);
1577
- if (longest === 0) {
1578
- return 1;
1579
- }
1580
- return Math.min(1, Math.max(0, 1 - distance / longest));
1581
- }
1582
- function intervalCost(x, y) {
1583
- return Math.min(1, Math.abs(x - y) / INTERVAL_TOLERANCE);
1584
- }
1585
- function gapCost(x, y) {
1586
- if (!(x > BEAT_EPS) || !(y > BEAT_EPS)) {
1587
- return Math.abs(x - y) > BEAT_EPS ? 1 : 0;
1588
- }
1589
- return Math.min(1, Math.abs(Math.log2(x / y)));
1590
- }
1591
- function compareMelodies(a, b) {
1592
- const left = orderedNotes(phraseNotes(a), "melody a");
1593
- const right = orderedNotes(phraseNotes(b), "melody b");
1594
- const leftIntervals = intervalsOf(left);
1595
- const rightIntervals = intervalsOf(right);
1596
- const pitchSimilarity = distanceToSimilarity(
1597
- gradedEditDistance(leftIntervals, rightIntervals, intervalCost),
1598
- leftIntervals.length,
1599
- rightIntervals.length
1600
- );
1601
- const leftGaps = rhythmProfile(onsetGaps(left)) ?? onsetGaps(left);
1602
- const rightGaps = rhythmProfile(onsetGaps(right)) ?? onsetGaps(right);
1603
- const rhythmSimilarity = distanceToSimilarity(
1604
- gradedEditDistance(leftGaps, rightGaps, gapCost),
1605
- leftGaps.length,
1606
- rightGaps.length
1607
- );
1608
- const similarity = PITCH_WEIGHT * pitchSimilarity + RHYTHM_WEIGHT * rhythmSimilarity;
1609
- return {
1610
- similarity,
1611
- pitchSimilarity,
1612
- rhythmSimilarity,
1613
- rationale: `Interval shapes ${Math.round(pitchSimilarity * 100)}% alike, rhythms ${Math.round(rhythmSimilarity * 100)}% alike, weighted ${PITCH_WEIGHT}/${RHYTHM_WEIGHT} into ${roundTo(similarity, 3)}`
1614
- };
1615
- }
1616
- function melodicSimilarity(a, b) {
1617
- return compareMelodies(a, b).similarity;
1618
- }
1619
-
1620
1094
  // src/analyze/form/phrase.ts
1621
1095
  var CADENCE_BOUNDARY_STRENGTH = {
1622
1096
  authentic: 0.9,
@@ -1714,17 +1188,6 @@ function choosePhrasePath(beats, strengths, minPhraseBeats, expected) {
1714
1188
  function arrivalEnd(timeline, atBeat) {
1715
1189
  return segmentStartingAt(timeline.segments, atBeat)?.endBeat ?? atBeat;
1716
1190
  }
1717
- function barSpanOf(startBeat, endBeat, meters) {
1718
- const firstBar = barIndexAt(startBeat, meters);
1719
- const lastBar = barIndexAt(endBeat, meters);
1720
- const partOf = (beat, bar) => {
1721
- const barStart = barPositionToBeat({ bar, beat: 0 }, meters);
1722
- const barLength = beatsPerBarAt(barStart, meters);
1723
- return barLength > 0 ? (beat - barStart) / barLength : 0;
1724
- };
1725
- const bars = lastBar - firstBar + partOf(endBeat, lastBar) - partOf(startBeat, firstBar);
1726
- return Math.round(bars * 100) / 100;
1727
- }
1728
1191
  function keyContextFor(opts, notes, meters, totalBeats) {
1729
1192
  if (opts.key !== void 0) {
1730
1193
  return opts.key;
@@ -1934,7 +1397,7 @@ function phrasesFromTimeline(timeline, notes, opts = {}) {
1934
1397
  isLast
1935
1398
  );
1936
1399
  const signals = cadence === null ? reading.signals.filter((signal) => signal !== "cadence") : reading.signals;
1937
- const bars = barSpanOf(startBeat, endBeat, meters);
1400
+ const bars = Math.round(barSpanOf(startBeat, endBeat, meters) * 100) / 100;
1938
1401
  phrases.push({
1939
1402
  startBeat,
1940
1403
  endBeat,
@@ -2120,156 +1583,277 @@ function sectionsFromNotes(notes, opts = {}) {
2120
1583
  return sections;
2121
1584
  }
2122
1585
 
2123
- // src/analyze/reduction/index.ts
2124
- var TONIC_ARRIVING = /* @__PURE__ */ new Set(["authentic", "plagal", "modal", "deceptive"]);
2125
- function signedStep(fromPc, toPc) {
2126
- return (pitchClassOf(toPc) - pitchClassOf(fromPc) + 18) % 12 - 6;
2127
- }
2128
- function isStep(distance) {
2129
- const size = Math.abs(distance);
2130
- return size === 1 || size === 2;
2131
- }
2132
- function touches(before, after) {
2133
- return Math.abs(after.startBeat - before.endBeat) <= BEAT_EPS;
2134
- }
2135
- function salienceFloors(durations) {
2136
- const order = durations.map((_, index) => index).sort((a, b) => {
2137
- const left = durations[a] ?? 0;
2138
- const right = durations[b] ?? 0;
2139
- return left - right || a - b;
2140
- });
2141
- const rankOf = new Array(durations.length).fill(0);
2142
- for (let rank = 0; rank < order.length; rank += 1) {
2143
- rankOf[order[rank] ?? 0] = rank;
2144
- }
2145
- const floors = [];
2146
- const remaining = durations.length - 1;
2147
- for (let index = 0; index < durations.length; index += 1) {
2148
- if (remaining <= 0) {
2149
- floors.push(0);
2150
- 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
+ }
2151
1595
  }
2152
- const skipped = rankOf[index] ?? 0;
2153
- const at = (position) => durations[order[position < skipped ? position : position + 1] ?? 0] ?? 0;
2154
- const middle = remaining >> 1;
2155
- floors.push(remaining % 2 === 1 ? at(middle) : (at(middle - 1) + at(middle)) / 2);
2156
- }
2157
- return floors;
2158
- }
2159
- function isTonicChord(chord, key) {
2160
- return pitchClassOf(chord.rootPc - key.rootPc) === 0 && !isAppliedDominant(chord, key);
2161
- }
2162
- function functionalReason(segment, before, after, keyAt) {
2163
- const key = keyAt(segment.startBeat);
2164
- if (after !== void 0 && isCadentialSixFour(segment.chord, after.chord, key)) {
2165
- return { kind: "cadentialSixFour" };
2166
- }
2167
- if (isTonicChord(segment.chord, key)) {
2168
- return { kind: "tonic" };
2169
- }
2170
- if (isDominantChordOf(segment.chord, key)) {
2171
- return { kind: "dominant" };
2172
- }
2173
- if (before !== void 0) {
2174
- const arrival = detectCadence(before.chord, segment.chord, key).type;
2175
- if (arrival !== null) {
2176
- return { kind: "cadenceArrival", cadence: arrival };
2177
- }
2178
- }
2179
- if (after !== void 0) {
2180
- const agent = detectCadence(segment.chord, after.chord, keyAt(after.startBeat)).type;
2181
- if (agent !== null && TONIC_ARRIVING.has(agent)) {
2182
- return { kind: "cadenceAgent", cadence: agent };
2183
- }
2184
- }
2185
- return null;
2186
- }
2187
- function figureReason(segment, before, after) {
2188
- if (before === void 0 || after === void 0) {
2189
- return null;
2190
- }
2191
- const incoming = signedStep(before.chord.rootPc, segment.chord.rootPc);
2192
- const outgoing = signedStep(segment.chord.rootPc, after.chord.rootPc);
2193
- if (!isStep(incoming) || !isStep(outgoing)) {
2194
- return null;
2195
- }
2196
- if (incoming > 0 === outgoing > 0) {
2197
- return { kind: "passing" };
2198
- }
2199
- return pitchClassOf(before.chord.rootPc) === pitchClassOf(after.chord.rootPc) ? { kind: "neighbor" } : null;
2200
- }
2201
- function describe(reason) {
2202
- switch (reason.kind) {
2203
- case "tonic":
2204
- return "Structural: the tonic of the key, which the progression is heard against";
2205
- case "dominant":
2206
- return "Structural: the dominant of the key, the chord its tonic is approached from";
2207
- case "cadentialSixFour":
2208
- return "Structural: a tonic six-four on the bass of the dominant that follows, which prolongs that dominant rather than framing the tonic";
2209
- case "cadenceArrival":
2210
- return `Structural: the arrival of a ${reason.cadence} cadence`;
2211
- case "cadenceAgent":
2212
- return `Structural: the chord that drives a ${reason.cadence} cadence`;
2213
- case "salient":
2214
- return "Structural: it holds the harmony longer than the chords around it";
2215
- case "first":
2216
- return "Structural: the first chord of the progression frames it";
2217
- case "last":
2218
- return "Structural: the last chord of the progression frames it";
2219
- case "unfigured":
2220
- return "Structural: its root neither steps through to the next chord nor returns to the last, so it embellishes nothing";
2221
- case "passing":
2222
- return "Passing: its root steps in from the previous chord and on to the next in the same direction";
2223
- case "neighbor":
2224
- return "Neighbor: its root steps away from the surrounding harmony and returns to it";
2225
- }
2226
- }
2227
- var BASIS_READINGS = {
2228
- function: ({ segment, before, after, keyAt }) => functionalReason(segment, before, after, keyAt),
2229
- duration: ({ segment, salienceFloor }) => segment.endBeat - segment.startBeat > salienceFloor ? { kind: "salient" } : null
2230
- };
2231
- var REDUCTION_BASES = Object.keys(BASIS_READINGS);
2232
- function levelOf(reason) {
2233
- if (reason.kind === "passing") {
2234
- return "passing";
2235
1596
  }
2236
- 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) };
2237
1654
  }
2238
- function reduceProgression(timeline, key, opts = {}) {
2239
- assertChordTimeline(timeline);
2240
- const asked = assertOptions(opts, "opts");
2241
- const keyAt = keyScaleAt(key);
2242
- const basis = assertOneOf(asked.basis ?? "function", REDUCTION_BASES, "reduction basis");
2243
- const reading = BASIS_READINGS[basis];
1655
+
1656
+ // src/analyze/reference/profile.ts
1657
+ function spanOf(sounding, timeline, meters, totalBeats) {
2244
1658
  const segments = timeline.segments;
2245
- const floors = basis === "duration" ? salienceFloors(segments.map((segment) => segment.endBeat - segment.startBeat)) : [];
2246
- const result = [];
2247
- for (let index = 0; index < segments.length; index += 1) {
2248
- const segment = segments[index];
2249
- if (segment === void 0) {
2250
- continue;
2251
- }
2252
- const previous = segments[index - 1];
2253
- const following = segments[index + 1];
2254
- const before = previous !== void 0 && touches(previous, segment) ? previous : void 0;
2255
- const after = following !== void 0 && touches(segment, following) ? following : void 0;
2256
- let reason = reading({ segment, before, after, keyAt, salienceFloor: floors[index] ?? 0 });
2257
- if (reason === null && index === 0) {
2258
- reason = { kind: "first" };
2259
- }
2260
- if (reason === null && index === segments.length - 1) {
2261
- reason = { kind: "last" };
2262
- }
2263
- reason ??= figureReason(segment, before, after) ?? { kind: "unfigured" };
2264
- result.push({
2265
- chord: segment.chord,
2266
- startBeat: segment.startBeat,
2267
- endBeat: segment.endBeat,
2268
- level: levelOf(reason),
2269
- 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
2270
1701
  });
2271
1702
  }
2272
- 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);
2273
1857
  }
2274
1858
 
2275
1859
  export {
@@ -2280,15 +1864,11 @@ export {
2280
1864
  tensionCurve,
2281
1865
  tensionCurveFrom,
2282
1866
  hypermeter,
2283
- melodicContour,
2284
- extractMotifs,
2285
- motifFromNotes,
2286
- relateMotifs,
2287
- compareMelodies,
2288
- melodicSimilarity,
2289
1867
  phrasesFromTimeline,
2290
1868
  structuralCadences,
2291
1869
  sectionsFromNotes,
2292
- reduceProgression
1870
+ compareReferences,
1871
+ referenceFromReadings,
1872
+ analyzeReference
2293
1873
  };
2294
- //# sourceMappingURL=chunk-HYJJ2V2I.js.map
1874
+ //# sourceMappingURL=chunk-MLBPXA4Z.js.map