@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
@@ -33,6 +33,7 @@ import {
33
33
  isNeapolitanChordOf,
34
34
  isScaleTone,
35
35
  keyRelationBetween,
36
+ keySignatureFifths,
36
37
  majorKey,
37
38
  makeChord,
38
39
  minorKey,
@@ -55,25 +56,37 @@ import {
55
56
  allocateChoiceTable,
56
57
  assertArray,
57
58
  assertFiniteNumber,
59
+ assertFlag,
58
60
  assertFunction,
59
61
  assertGenerationBudget,
60
62
  assertInteger,
63
+ assertMeterMap,
61
64
  assertMidiPitch,
62
65
  assertNoteEvents,
63
66
  assertOneOf,
64
67
  assertOptions,
68
+ assertPositiveInt,
65
69
  assertRange,
66
70
  assertRecord,
71
+ assertTimeSignature,
67
72
  barIndexAt,
68
73
  barPositionToBeat,
74
+ barStartBeat,
75
+ beatsPerBar,
69
76
  beatsPerBarAt,
77
+ describeRejected,
70
78
  diatonicLetterOf,
71
79
  formatNote,
80
+ isCompound,
81
+ meterAt,
72
82
  metricGridUnit,
73
83
  metricWeight,
84
+ midiToNote,
74
85
  parseInterval,
75
86
  pitchClassOf,
87
+ pulseBeats,
76
88
  resolveMeters,
89
+ spelledInterval,
77
90
  toNoteData,
78
91
  transposeByInterval,
79
92
  transposeNote
@@ -779,6 +792,14 @@ function outerPitches(voicing) {
779
792
  function bassPcOf2(chord, voicedBass) {
780
793
  return pitchClassOf(voicedBass ?? chord.bassPc ?? chord.rootPc);
781
794
  }
795
+ var CADENCE_TYPES = Object.freeze([
796
+ "authentic",
797
+ "plagal",
798
+ "half",
799
+ "deceptive",
800
+ "phrygian",
801
+ "modal"
802
+ ]);
782
803
  function cadenceType(from, to, key, fromBassPc, toBassPc) {
783
804
  const tonic = pitchClassOf(key.rootPc);
784
805
  const fromOffset = pitchClassOf(from.rootPc - tonic);
@@ -1466,6 +1487,7 @@ function capitalize(text) {
1466
1487
  }
1467
1488
 
1468
1489
  // src/analyze/functional/function.ts
1490
+ var HARMONIC_FUNCTIONS = Object.freeze(["tonic", "subdominant", "dominant"]);
1469
1491
  var FUNCTION_BY_OFFSET = [
1470
1492
  "tonic",
1471
1493
  // 0 I
@@ -1669,6 +1691,16 @@ function stackedTriad(degree, key) {
1669
1691
  }
1670
1692
 
1671
1693
  // src/analyze/form/internal.ts
1694
+ function barSpanOf(startBeat, endBeat, meter) {
1695
+ const firstBar = barIndexAt(startBeat, meter);
1696
+ const lastBar = barIndexAt(endBeat, meter);
1697
+ const partOf = (beat, bar) => {
1698
+ const barStart = barPositionToBeat({ bar, beat: 0 }, meter);
1699
+ const barLength = beatsPerBarAt(barStart, meter);
1700
+ return barLength > 0 ? (beat - barStart) / barLength : 0;
1701
+ };
1702
+ return lastBar - firstBar + partOf(endBeat, lastBar) - partOf(startBeat, firstBar);
1703
+ }
1672
1704
  function clamp01(value) {
1673
1705
  return Math.min(1, Math.max(0, value));
1674
1706
  }
@@ -3267,6 +3299,1805 @@ function detectCadences(timeline, key) {
3267
3299
  return hits;
3268
3300
  }
3269
3301
 
3302
+ // src/analyze/melody/internal.ts
3303
+ var RHYTHM_GRID = 64;
3304
+ function phraseNotes(phrase) {
3305
+ if (Array.isArray(phrase)) {
3306
+ return phrase;
3307
+ }
3308
+ return assertRecord(phrase, "melodic phrase").notes;
3309
+ }
3310
+ function orderedNotes(notes, name, budget) {
3311
+ assertNoteEvents(notes, name, { allowNonPositiveDuration: true, budget });
3312
+ const sounding = notes.filter((note) => note.durationBeat > 0).sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
3313
+ const line = [];
3314
+ for (const note of sounding) {
3315
+ const previous = line[line.length - 1];
3316
+ if (previous !== void 0 && Math.abs(note.startBeat - previous.startBeat) <= BEAT_EPS) {
3317
+ line[line.length - 1] = note;
3318
+ continue;
3319
+ }
3320
+ line.push(note);
3321
+ }
3322
+ return line;
3323
+ }
3324
+ function intervalsOf(notes) {
3325
+ const out = [];
3326
+ for (let i = 1; i < notes.length; i += 1) {
3327
+ out.push((notes[i]?.pitch ?? 0) - (notes[i - 1]?.pitch ?? 0));
3328
+ }
3329
+ return out;
3330
+ }
3331
+ function onsetGaps(notes) {
3332
+ const out = [];
3333
+ for (let i = 1; i < notes.length; i += 1) {
3334
+ out.push((notes[i]?.startBeat ?? 0) - (notes[i - 1]?.startBeat ?? 0));
3335
+ }
3336
+ return out;
3337
+ }
3338
+ function retrogradeGaps(notes) {
3339
+ const out = [];
3340
+ for (let i = notes.length - 1; i >= 1; i -= 1) {
3341
+ const later = notes[i];
3342
+ const earlier = notes[i - 1];
3343
+ out.push(
3344
+ (later?.startBeat ?? 0) + (later?.durationBeat ?? 0) - ((earlier?.startBeat ?? 0) + (earlier?.durationBeat ?? 0))
3345
+ );
3346
+ }
3347
+ return out;
3348
+ }
3349
+ function rhythmProfile(gaps) {
3350
+ const unit = gaps[0] ?? 0;
3351
+ if (!(unit > BEAT_EPS)) {
3352
+ return gaps.length === 0 ? [] : null;
3353
+ }
3354
+ return gaps.map((gap) => Math.round(gap / unit * RHYTHM_GRID) / RHYTHM_GRID);
3355
+ }
3356
+ function sameNumbers(a, b) {
3357
+ if (a.length !== b.length) {
3358
+ return false;
3359
+ }
3360
+ for (let i = 0; i < a.length; i += 1) {
3361
+ if (Math.abs((a[i] ?? 0) - (b[i] ?? 0)) > BEAT_EPS) {
3362
+ return false;
3363
+ }
3364
+ }
3365
+ return true;
3366
+ }
3367
+ function roundTo(value, digits) {
3368
+ const scale = 10 ** digits;
3369
+ return Math.round(value * scale) / scale;
3370
+ }
3371
+ function endBeatOf(notes) {
3372
+ let end = 0;
3373
+ for (const note of notes) {
3374
+ end = Math.max(end, note.startBeat + note.durationBeat);
3375
+ }
3376
+ return end;
3377
+ }
3378
+ function onsetSpan(notes) {
3379
+ if (notes.length < 2) {
3380
+ return 0;
3381
+ }
3382
+ return (notes[notes.length - 1]?.startBeat ?? 0) - (notes[0]?.startBeat ?? 0);
3383
+ }
3384
+
3385
+ // src/analyze/melody/contour.ts
3386
+ var DIRECTIONAL_SHARE = 0.75;
3387
+ var MELODIC_CONTOUR_SHAPES = Object.freeze([
3388
+ "arch",
3389
+ "ascending",
3390
+ "descending",
3391
+ "wave",
3392
+ "static"
3393
+ ]);
3394
+ function extremes(notes) {
3395
+ if (notes.length === 0) {
3396
+ return { peak: -1, trough: -1 };
3397
+ }
3398
+ let peak = 0;
3399
+ let trough = 0;
3400
+ for (let i = 1; i < notes.length; i += 1) {
3401
+ if ((notes[i]?.pitch ?? 0) > (notes[peak]?.pitch ?? 0)) {
3402
+ peak = i;
3403
+ }
3404
+ if ((notes[i]?.pitch ?? 0) < (notes[trough]?.pitch ?? 0)) {
3405
+ trough = i;
3406
+ }
3407
+ }
3408
+ return { peak, trough };
3409
+ }
3410
+ function melodicContour(notes) {
3411
+ const line = orderedNotes(phraseNotes(notes), "contour notes");
3412
+ const intervals = intervalsOf(line);
3413
+ const directions = intervals.map(
3414
+ (step) => step > 0 ? "up" : step < 0 ? "down" : "same"
3415
+ );
3416
+ const { peak, trough } = extremes(line);
3417
+ const range = (line[peak]?.pitch ?? 0) - (line[trough]?.pitch ?? 0);
3418
+ const motion = intervals.reduce((sum, step) => sum + Math.abs(step), 0);
3419
+ if (motion === 0) {
3420
+ return {
3421
+ directions,
3422
+ shape: "static",
3423
+ peakIndex: peak,
3424
+ troughIndex: trough,
3425
+ range: 0,
3426
+ 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"
3427
+ };
3428
+ }
3429
+ const first = line[0]?.pitch ?? 0;
3430
+ const last = line[line.length - 1]?.pitch ?? 0;
3431
+ const net = last - first;
3432
+ const directness = Math.abs(net) / motion;
3433
+ const share = Math.round(directness * 100);
3434
+ if (directness >= DIRECTIONAL_SHARE) {
3435
+ const shape = net > 0 ? "ascending" : "descending";
3436
+ return {
3437
+ directions,
3438
+ shape,
3439
+ peakIndex: peak,
3440
+ troughIndex: trough,
3441
+ range,
3442
+ rationale: `${share}% of the motion runs ${net > 0 ? "up" : "down"}, so the line is ${shape}`
3443
+ };
3444
+ }
3445
+ const peakPitch = line[peak]?.pitch ?? 0;
3446
+ const archMotion = peakPitch - first + (peakPitch - last);
3447
+ if (peak > 0 && peak < line.length - 1 && archMotion >= DIRECTIONAL_SHARE * motion) {
3448
+ return {
3449
+ directions,
3450
+ shape: "arch",
3451
+ peakIndex: peak,
3452
+ troughIndex: trough,
3453
+ range,
3454
+ rationale: `Rises to its peak at note ${peak + 1} of ${line.length} and falls back, so the line is an arch`
3455
+ };
3456
+ }
3457
+ return {
3458
+ directions,
3459
+ shape: "wave",
3460
+ peakIndex: peak,
3461
+ troughIndex: trough,
3462
+ range,
3463
+ rationale: `Changes direction without settling either way (${share}% of the motion nets out), so the line is a wave`
3464
+ };
3465
+ }
3466
+
3467
+ // src/analyze/melody/motifs.ts
3468
+ var MIN_CELL_NOTES = 3;
3469
+ var MAX_CELL_NOTES = 8;
3470
+ var MIN_OCCURRENCES = 2;
3471
+ function windowSignature(notes) {
3472
+ const profile = rhythmProfile(onsetGaps(notes));
3473
+ if (profile === null) {
3474
+ return null;
3475
+ }
3476
+ return `${notes.length}|${intervalsOf(notes).join(",")}|${profile.join(",")}`;
3477
+ }
3478
+ function nonOverlapping(starts, length) {
3479
+ const picked = [];
3480
+ let lastEnd = Number.NEGATIVE_INFINITY;
3481
+ for (const start of starts) {
3482
+ if (start > lastEnd) {
3483
+ picked.push(start);
3484
+ lastEnd = start + length - 1;
3485
+ }
3486
+ }
3487
+ return picked;
3488
+ }
3489
+ function subsumes(outer, inner) {
3490
+ let at = 0;
3491
+ for (const start of inner.starts) {
3492
+ while (at + 1 < outer.starts.length && (outer.starts[at + 1] ?? 0) <= start) {
3493
+ at += 1;
3494
+ }
3495
+ const from = outer.starts[at];
3496
+ if (from === void 0 || start < from || start + inner.length > from + outer.length) {
3497
+ return false;
3498
+ }
3499
+ }
3500
+ return true;
3501
+ }
3502
+ function listBeats(occurrences) {
3503
+ const shown = occurrences.slice(0, 5).map((occurrence) => roundTo(occurrence.startBeat, 3));
3504
+ return occurrences.length > shown.length ? `${shown.join(", ")}, and ${occurrences.length - shown.length} more` : shown.join(", ");
3505
+ }
3506
+ function motifFromGroup(group, notes) {
3507
+ const first = group.starts[0] ?? 0;
3508
+ const prime = notes.slice(first, first + group.length).map((note) => ({ ...note }));
3509
+ const primePitch = prime[0]?.pitch ?? 0;
3510
+ const primeSpan = onsetSpan(prime);
3511
+ const occurrences = group.starts.map((start) => {
3512
+ const statement = notes.slice(start, start + group.length);
3513
+ const span = onsetSpan(statement);
3514
+ return {
3515
+ noteIndex: start,
3516
+ startBeat: statement[0]?.startBeat ?? 0,
3517
+ endBeat: endBeatOf(statement),
3518
+ transpose: (statement[0]?.pitch ?? 0) - primePitch,
3519
+ timeRatio: primeSpan > BEAT_EPS ? span / primeSpan : 1
3520
+ };
3521
+ });
3522
+ const shape = melodicContour(prime).shape;
3523
+ const transposed = occurrences.some((occurrence) => occurrence.transpose !== 0);
3524
+ const stretched = occurrences.some((occurrence) => Math.abs(occurrence.timeRatio - 1) > BEAT_EPS);
3525
+ const detail = [
3526
+ transposed ? "at more than one pitch level" : "at the same pitch level",
3527
+ stretched ? "and in more than one set of note values" : ""
3528
+ ].filter((part) => part !== "").join(" ");
3529
+ return {
3530
+ notes: prime,
3531
+ intervals: intervalsOf(prime),
3532
+ rhythm: rhythmProfile(onsetGaps(prime)) ?? [],
3533
+ occurrences,
3534
+ rationale: `${group.length}-note ${shape} cell stated ${occurrences.length} ${occurrences.length === 1 ? "time" : "times"} (beats ${listBeats(occurrences)}), ${detail}`
3535
+ };
3536
+ }
3537
+ function extractMotifs(notes, opts = {}) {
3538
+ const asked = assertOptions(opts, "opts");
3539
+ const minNotes = assertInteger(asked.minNotes ?? MIN_CELL_NOTES, "motif minNotes", 2, 64);
3540
+ const maxNotes = assertInteger(asked.maxNotes ?? MAX_CELL_NOTES, "motif maxNotes", 2, 64);
3541
+ if (maxNotes < minNotes) {
3542
+ throw new InvalidInputError(
3543
+ `motif maxNotes must be at least minNotes ${minNotes}; received ${maxNotes}`
3544
+ );
3545
+ }
3546
+ const minOccurrences = assertInteger(
3547
+ asked.minOccurrences ?? MIN_OCCURRENCES,
3548
+ "motif minOccurrences",
3549
+ 2
3550
+ );
3551
+ const sounding = orderedNotes(notes, "melody notes", asked.budget);
3552
+ if (sounding.length < minNotes) {
3553
+ return [];
3554
+ }
3555
+ assertGenerationBudget(
3556
+ sounding.length * (maxNotes - minNotes + 1),
3557
+ "motif windows",
3558
+ asked.budget
3559
+ );
3560
+ const groups = /* @__PURE__ */ new Map();
3561
+ for (let length = minNotes; length <= maxNotes; length += 1) {
3562
+ for (let start = 0; start + length <= sounding.length; start += 1) {
3563
+ const signature = windowSignature(sounding.slice(start, start + length));
3564
+ if (signature === null) {
3565
+ continue;
3566
+ }
3567
+ const group = groups.get(signature);
3568
+ if (group === void 0) {
3569
+ groups.set(signature, { signature, length, starts: [start] });
3570
+ } else {
3571
+ group.starts.push(start);
3572
+ }
3573
+ }
3574
+ }
3575
+ const candidates = [...groups.values()].map((group) => ({ ...group, starts: nonOverlapping(group.starts, group.length) })).filter((group) => group.starts.length >= minOccurrences).sort(
3576
+ (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)
3577
+ );
3578
+ const keptByLength = /* @__PURE__ */ new Map();
3579
+ const kept = [];
3580
+ let comparisons = 0;
3581
+ for (const candidate of candidates) {
3582
+ const first = candidate.starts[0] ?? 0;
3583
+ let covered = false;
3584
+ for (let length = candidate.length + 1; length <= maxNotes && !covered; length += 1) {
3585
+ const byStart = keptByLength.get(length);
3586
+ if (byStart === void 0) {
3587
+ continue;
3588
+ }
3589
+ for (let from = Math.max(0, first - (length - candidate.length)); from <= first; from += 1) {
3590
+ comparisons += 1;
3591
+ const outer = byStart.get(from);
3592
+ if (outer === void 0 || outer.starts.length < candidate.starts.length) {
3593
+ continue;
3594
+ }
3595
+ comparisons += outer.starts.length + candidate.starts.length;
3596
+ if (subsumes(outer, candidate)) {
3597
+ covered = true;
3598
+ break;
3599
+ }
3600
+ }
3601
+ }
3602
+ assertGenerationBudget(comparisons, "motif subsumption comparisons", asked.budget);
3603
+ if (!covered) {
3604
+ kept.push(candidate);
3605
+ let byStart = keptByLength.get(candidate.length);
3606
+ if (byStart === void 0) {
3607
+ byStart = /* @__PURE__ */ new Map();
3608
+ keptByLength.set(candidate.length, byStart);
3609
+ }
3610
+ for (const start of candidate.starts) {
3611
+ byStart.set(start, candidate);
3612
+ }
3613
+ }
3614
+ }
3615
+ return kept.map((group) => motifFromGroup(group, sounding));
3616
+ }
3617
+ function motifFromNotes(notes) {
3618
+ const sounding = orderedNotes(notes, "motif notes").map((note) => ({ ...note }));
3619
+ return motifFromGroup(
3620
+ { signature: windowSignature(sounding) ?? "", length: sounding.length, starts: [0] },
3621
+ sounding
3622
+ );
3623
+ }
3624
+
3625
+ // src/analyze/melody/relation.ts
3626
+ var MOTIF_RELATION_KINDS = Object.freeze([
3627
+ "repetition",
3628
+ "transposition",
3629
+ "tonalTransposition",
3630
+ "inversion",
3631
+ "retrograde",
3632
+ "retrogradeInversion",
3633
+ "augmentation",
3634
+ "diminution"
3635
+ ]);
3636
+ function spellingOf(key) {
3637
+ if (key === void 0) {
3638
+ return "sharp";
3639
+ }
3640
+ return keySignatureFifths(key.tonic, key.scale) < 0 ? "flat" : "sharp";
3641
+ }
3642
+ function intervalBetween(from, to, key) {
3643
+ const spelling = spellingOf(key);
3644
+ return spelledInterval(midiToNote(from, spelling), midiToNote(to, spelling));
3645
+ }
3646
+ function intervalName(interval) {
3647
+ return `${interval.descending && interval.semitones !== 0 ? "-" : ""}${interval.quality}${interval.number}`;
3648
+ }
3649
+ function ladderIndices(notes, key) {
3650
+ const root = pitchClassOf(key.rootPc);
3651
+ const offsets = scaleTonesInDegreeOrder(key).map((pc) => pitchClassOf(pc - root));
3652
+ if (offsets.length === 0) {
3653
+ return null;
3654
+ }
3655
+ const out = [];
3656
+ for (const note of notes) {
3657
+ const semitones = note.pitch - root;
3658
+ const octave = Math.floor(semitones / 12);
3659
+ const degree = offsets.indexOf(semitones - octave * 12);
3660
+ if (degree < 0) {
3661
+ return null;
3662
+ }
3663
+ out.push(octave * offsets.length + degree);
3664
+ }
3665
+ return out;
3666
+ }
3667
+ function ladderSteps(indices) {
3668
+ const out = [];
3669
+ for (let i = 1; i < indices.length; i += 1) {
3670
+ out.push((indices[i] ?? 0) - (indices[i - 1] ?? 0));
3671
+ }
3672
+ return out;
3673
+ }
3674
+ function pitchShapes(a, b) {
3675
+ const shapes = [];
3676
+ if (sameNumbers(a, b)) {
3677
+ shapes.push("exact");
3678
+ }
3679
+ if (sameNumbers(
3680
+ b,
3681
+ a.map((step) => -step)
3682
+ )) {
3683
+ shapes.push("inversion");
3684
+ }
3685
+ const reversed = [...a].reverse();
3686
+ if (sameNumbers(
3687
+ b,
3688
+ reversed.map((step) => -step)
3689
+ )) {
3690
+ shapes.push("retrograde");
3691
+ }
3692
+ if (sameNumbers(b, reversed)) {
3693
+ shapes.push("retrogradeInversion");
3694
+ }
3695
+ return shapes;
3696
+ }
3697
+ function totalGap(gaps) {
3698
+ return gaps.reduce((sum, gap) => sum + gap, 0);
3699
+ }
3700
+ function stretchRatio(modelGaps, answerGaps) {
3701
+ const span = totalGap(modelGaps);
3702
+ return span > BEAT_EPS ? totalGap(answerGaps) / span : 1;
3703
+ }
3704
+ function stretchPhrase(ratio2) {
3705
+ return `note values ${roundTo(ratio2, 3)}x the model's`;
3706
+ }
3707
+ function relateMotifs(a, b, keyLike) {
3708
+ const key = keyLike === void 0 ? void 0 : resolveKey(keyLike);
3709
+ const model = assertArray(assertRecord(a, "a").notes, "a.notes");
3710
+ const answer = assertArray(assertRecord(b, "b").notes, "b.notes");
3711
+ if (model.length === 0 || model.length !== answer.length) {
3712
+ return null;
3713
+ }
3714
+ const modelGaps = onsetGaps(model);
3715
+ const answerGaps = onsetGaps(answer);
3716
+ const modelProfile = rhythmProfile(modelGaps);
3717
+ const answerProfile = rhythmProfile(answerGaps);
3718
+ const retroGaps = retrogradeGaps(model);
3719
+ const retroProfile = rhythmProfile(retroGaps);
3720
+ if (modelProfile === null || answerProfile === null) {
3721
+ return null;
3722
+ }
3723
+ const forward = sameNumbers(modelProfile, answerProfile);
3724
+ const backward = retroProfile !== null && sameNumbers(retroProfile, answerProfile);
3725
+ const semitones = (answer[0]?.pitch ?? 0) - (model[0]?.pitch ?? 0);
3726
+ const interval = intervalBetween(model[0]?.pitch ?? 0, answer[0]?.pitch ?? 0, key);
3727
+ const gap = (answer[0]?.startBeat ?? 0) - endBeatOf(model);
3728
+ const sequence = gap >= -HUMANIZE_ADJACENCY && gap <= HUMANIZE_ADJACENCY;
3729
+ const timeRatio = stretchRatio(modelGaps, answerGaps);
3730
+ const stretched = Math.abs(timeRatio - 1) > BEAT_EPS;
3731
+ const shapes = pitchShapes(intervalsOf(model), intervalsOf(answer));
3732
+ const base = { sequence, semitones, interval, timeRatio };
3733
+ const backwardBase = { ...base, timeRatio: stretchRatio(retroGaps, answerGaps) };
3734
+ if (forward && shapes.includes("exact")) {
3735
+ if (semitones === 0) {
3736
+ if (!stretched) {
3737
+ return {
3738
+ ...base,
3739
+ kind: "repetition",
3740
+ rationale: "Exact repetition: the same pitches in the same rhythm"
3741
+ };
3742
+ }
3743
+ const augmented = timeRatio > 1;
3744
+ return {
3745
+ ...base,
3746
+ kind: augmented ? "augmentation" : "diminution",
3747
+ rationale: `${augmented ? "Augmentation" : "Diminution"}: the same pitches in ${stretchPhrase(timeRatio)}`
3748
+ };
3749
+ }
3750
+ const name = intervalName(interval);
3751
+ return {
3752
+ ...base,
3753
+ kind: "transposition",
3754
+ 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)}` : "")
3755
+ };
3756
+ }
3757
+ if (forward && key !== void 0) {
3758
+ const modelLadder = ladderIndices(model, key.scale);
3759
+ const answerLadder = ladderIndices(answer, key.scale);
3760
+ if (modelLadder !== null && answerLadder !== null) {
3761
+ const degrees = (answerLadder[0] ?? 0) - (modelLadder[0] ?? 0);
3762
+ if (degrees !== 0 && sameNumbers(ladderSteps(modelLadder), ladderSteps(answerLadder))) {
3763
+ const direction = degrees > 0 ? "higher" : "lower";
3764
+ const count = Math.abs(degrees);
3765
+ return {
3766
+ ...base,
3767
+ kind: "tonalTransposition",
3768
+ degrees,
3769
+ 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`
3770
+ };
3771
+ }
3772
+ }
3773
+ }
3774
+ if (forward && shapes.includes("inversion")) {
3775
+ return {
3776
+ ...base,
3777
+ kind: "inversion",
3778
+ rationale: "Inversion: every interval turned upside down about the first note" + (stretched ? `, in ${stretchPhrase(timeRatio)}` : "")
3779
+ };
3780
+ }
3781
+ if (backward && shapes.includes("retrograde")) {
3782
+ return {
3783
+ ...backwardBase,
3784
+ kind: "retrograde",
3785
+ rationale: "Retrograde: the cell read back to front, rhythm included"
3786
+ };
3787
+ }
3788
+ if (backward && shapes.includes("retrogradeInversion")) {
3789
+ return {
3790
+ ...backwardBase,
3791
+ kind: "retrogradeInversion",
3792
+ rationale: "Retrograde inversion: the cell read back to front and turned upside down"
3793
+ };
3794
+ }
3795
+ return null;
3796
+ }
3797
+
3798
+ // src/analyze/sequence.ts
3799
+ function gradedEditDistance(a, b, cost, gap = 1) {
3800
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i * gap);
3801
+ for (let i = 1; i <= a.length; i += 1) {
3802
+ const row = new Array(b.length + 1);
3803
+ row[0] = i * gap;
3804
+ for (let j = 1; j <= b.length; j += 1) {
3805
+ const ai = a[i - 1];
3806
+ const bj = b[j - 1];
3807
+ const substitution = (previous[j - 1] ?? 0) + cost(ai, bj);
3808
+ const deletion = (previous[j] ?? 0) + gap;
3809
+ const insertion = (row[j - 1] ?? 0) + gap;
3810
+ row[j] = Math.min(substitution, deletion, insertion);
3811
+ }
3812
+ previous = row;
3813
+ }
3814
+ return previous[b.length] ?? 0;
3815
+ }
3816
+ function distanceToSimilarity(distance, a, b) {
3817
+ const longest = Math.max(a, b);
3818
+ if (longest === 0) {
3819
+ return 1;
3820
+ }
3821
+ return Math.min(1, Math.max(0, 1 - distance / longest));
3822
+ }
3823
+
3824
+ // src/analyze/melody/similarity.ts
3825
+ var INTERVAL_TOLERANCE = 6;
3826
+ var PITCH_WEIGHT = 0.75;
3827
+ var RHYTHM_WEIGHT = 0.25;
3828
+ function intervalCost(x, y) {
3829
+ return Math.min(1, Math.abs(x - y) / INTERVAL_TOLERANCE);
3830
+ }
3831
+ function gapCost(x, y) {
3832
+ if (!(x > BEAT_EPS) || !(y > BEAT_EPS)) {
3833
+ return Math.abs(x - y) > BEAT_EPS ? 1 : 0;
3834
+ }
3835
+ return Math.min(1, Math.abs(Math.log2(x / y)));
3836
+ }
3837
+ function compareMelodies(a, b) {
3838
+ const left = orderedNotes(phraseNotes(a), "melody a");
3839
+ const right = orderedNotes(phraseNotes(b), "melody b");
3840
+ const leftIntervals = intervalsOf(left);
3841
+ const rightIntervals = intervalsOf(right);
3842
+ const pitchSimilarity = distanceToSimilarity(
3843
+ gradedEditDistance(leftIntervals, rightIntervals, intervalCost),
3844
+ leftIntervals.length,
3845
+ rightIntervals.length
3846
+ );
3847
+ const leftGaps = rhythmProfile(onsetGaps(left)) ?? onsetGaps(left);
3848
+ const rightGaps = rhythmProfile(onsetGaps(right)) ?? onsetGaps(right);
3849
+ const rhythmSimilarity = distanceToSimilarity(
3850
+ gradedEditDistance(leftGaps, rightGaps, gapCost),
3851
+ leftGaps.length,
3852
+ rightGaps.length
3853
+ );
3854
+ const similarity = PITCH_WEIGHT * pitchSimilarity + RHYTHM_WEIGHT * rhythmSimilarity;
3855
+ return {
3856
+ similarity,
3857
+ pitchSimilarity,
3858
+ rhythmSimilarity,
3859
+ rationale: `Interval shapes ${Math.round(pitchSimilarity * 100)}% alike, rhythms ${Math.round(rhythmSimilarity * 100)}% alike, weighted ${PITCH_WEIGHT}/${RHYTHM_WEIGHT} into ${roundTo(similarity, 3)}`
3860
+ };
3861
+ }
3862
+ function melodicSimilarity(a, b) {
3863
+ return compareMelodies(a, b).similarity;
3864
+ }
3865
+
3866
+ // src/analyze/melody/graph.ts
3867
+ var DEFAULT_VARIATION_THRESHOLD = 0.75;
3868
+ function summarize(relation) {
3869
+ const { kind, sequence, semitones, degrees, timeRatio } = relation;
3870
+ return { kind, sequence, semitones, degrees, timeRatio };
3871
+ }
3872
+ function betterParent(candidate, current) {
3873
+ const candidateNamed = candidate.relation !== null;
3874
+ const currentNamed = current.relation !== null;
3875
+ if (candidateNamed !== currentNamed) {
3876
+ return candidateNamed;
3877
+ }
3878
+ if (candidate.similarity !== current.similarity) {
3879
+ return candidate.similarity > current.similarity;
3880
+ }
3881
+ if (candidate.startBeat !== current.startBeat) {
3882
+ return candidate.startBeat > current.startBeat;
3883
+ }
3884
+ return candidate.index < current.index;
3885
+ }
3886
+ function motifGraph(line, motifs, opts = {}) {
3887
+ const asked = assertOptions(opts, "opts");
3888
+ const variationThreshold = asked.variationThreshold ?? DEFAULT_VARIATION_THRESHOLD;
3889
+ assertRange(variationThreshold, 0, 1, "motif graph variationThreshold");
3890
+ const ordered = orderedNotes(line, "line", asked.budget);
3891
+ const motifList = assertArray(motifs, "motifs");
3892
+ const infos = [];
3893
+ for (let mi = 0; mi < motifList.length; mi += 1) {
3894
+ const motif = assertRecord(motifList[mi], `motifs[${mi}]`);
3895
+ const notes = assertArray(motif.notes, `motifs[${mi}].notes`);
3896
+ const occurrences = assertArray(
3897
+ motif.occurrences,
3898
+ `motifs[${mi}].occurrences`
3899
+ );
3900
+ for (let oi = 0; oi < occurrences.length; oi += 1) {
3901
+ const occurrence = assertRecord(
3902
+ occurrences[oi],
3903
+ `motifs[${mi}].occurrences[${oi}]`
3904
+ );
3905
+ const { noteIndex } = occurrence;
3906
+ const onLine = Number.isInteger(noteIndex) && noteIndex >= 0 && noteIndex + notes.length <= ordered.length && Math.abs((ordered[noteIndex]?.startBeat ?? Number.NaN) - occurrence.startBeat) <= BEAT_EPS;
3907
+ if (!onLine) {
3908
+ throw new InvalidInputError(
3909
+ `motif occurrence motifs[${mi}].occurrences[${oi}] does not lie on the line`
3910
+ );
3911
+ }
3912
+ infos.push({
3913
+ node: {
3914
+ motif: mi,
3915
+ occurrence: oi,
3916
+ startBeat: occurrence.startBeat,
3917
+ endBeat: occurrence.endBeat
3918
+ },
3919
+ data: motifFromNotes(ordered.slice(noteIndex, noteIndex + notes.length))
3920
+ });
3921
+ }
3922
+ }
3923
+ infos.sort((a, b) => a.node.startBeat - b.node.startBeat || a.node.motif - b.node.motif);
3924
+ const nodeCount = infos.length;
3925
+ assertGenerationBudget(nodeCount * nodeCount, "motif graph comparisons", asked.budget);
3926
+ const edges = [];
3927
+ for (let to = 1; to < nodeCount; to += 1) {
3928
+ const toInfo = infos[to];
3929
+ let best = null;
3930
+ for (let from = 0; from < to; from += 1) {
3931
+ const fromInfo = infos[from];
3932
+ const candidate = {
3933
+ index: from,
3934
+ startBeat: fromInfo.node.startBeat,
3935
+ relation: relateMotifs(fromInfo.data, toInfo.data, asked.key),
3936
+ similarity: melodicSimilarity(fromInfo.data, toInfo.data)
3937
+ };
3938
+ if (best === null || betterParent(candidate, best)) {
3939
+ best = candidate;
3940
+ }
3941
+ }
3942
+ if (best !== null && (best.relation !== null || best.similarity >= variationThreshold)) {
3943
+ edges.push({
3944
+ from: best.index,
3945
+ to,
3946
+ relation: best.relation === null ? null : summarize(best.relation),
3947
+ similarity: best.similarity
3948
+ });
3949
+ }
3950
+ }
3951
+ return { nodes: infos.map((info) => info.node), edges };
3952
+ }
3953
+
3954
+ // src/analyze/reduction/index.ts
3955
+ var REDUCTION_LEVELS = Object.freeze(["structural", "passing", "neighbor"]);
3956
+ var TONIC_ARRIVING = /* @__PURE__ */ new Set(["authentic", "plagal", "modal", "deceptive"]);
3957
+ function signedStep(fromPc, toPc) {
3958
+ return (pitchClassOf(toPc) - pitchClassOf(fromPc) + 18) % 12 - 6;
3959
+ }
3960
+ function isStep2(distance) {
3961
+ const size = Math.abs(distance);
3962
+ return size === 1 || size === 2;
3963
+ }
3964
+ function touches(before, after) {
3965
+ return Math.abs(after.startBeat - before.endBeat) <= BEAT_EPS;
3966
+ }
3967
+ function salienceFloors(durations) {
3968
+ const order = durations.map((_, index) => index).sort((a, b) => {
3969
+ const left = durations[a] ?? 0;
3970
+ const right = durations[b] ?? 0;
3971
+ return left - right || a - b;
3972
+ });
3973
+ const rankOf = new Array(durations.length).fill(0);
3974
+ for (let rank = 0; rank < order.length; rank += 1) {
3975
+ rankOf[order[rank] ?? 0] = rank;
3976
+ }
3977
+ const floors = [];
3978
+ const remaining = durations.length - 1;
3979
+ for (let index = 0; index < durations.length; index += 1) {
3980
+ if (remaining <= 0) {
3981
+ floors.push(0);
3982
+ continue;
3983
+ }
3984
+ const skipped = rankOf[index] ?? 0;
3985
+ const at = (position) => durations[order[position < skipped ? position : position + 1] ?? 0] ?? 0;
3986
+ const middle = remaining >> 1;
3987
+ floors.push(remaining % 2 === 1 ? at(middle) : (at(middle - 1) + at(middle)) / 2);
3988
+ }
3989
+ return floors;
3990
+ }
3991
+ function isTonicChord(chord, key) {
3992
+ return pitchClassOf(chord.rootPc - key.rootPc) === 0 && !isAppliedDominant(chord, key);
3993
+ }
3994
+ function functionalReason(segment, before, after, keyAt) {
3995
+ const key = keyAt(segment.startBeat);
3996
+ if (after !== void 0 && isCadentialSixFour(segment.chord, after.chord, key)) {
3997
+ return { kind: "cadentialSixFour" };
3998
+ }
3999
+ if (isTonicChord(segment.chord, key)) {
4000
+ return { kind: "tonic" };
4001
+ }
4002
+ if (isDominantChordOf(segment.chord, key)) {
4003
+ return { kind: "dominant" };
4004
+ }
4005
+ if (before !== void 0) {
4006
+ const arrival = detectCadence(before.chord, segment.chord, key).type;
4007
+ if (arrival !== null) {
4008
+ return { kind: "cadenceArrival", cadence: arrival };
4009
+ }
4010
+ }
4011
+ if (after !== void 0) {
4012
+ const agent = detectCadence(segment.chord, after.chord, keyAt(after.startBeat)).type;
4013
+ if (agent !== null && TONIC_ARRIVING.has(agent)) {
4014
+ return { kind: "cadenceAgent", cadence: agent };
4015
+ }
4016
+ }
4017
+ return null;
4018
+ }
4019
+ function figureReason(segment, before, after) {
4020
+ if (before === void 0 || after === void 0) {
4021
+ return null;
4022
+ }
4023
+ const incoming = signedStep(before.chord.rootPc, segment.chord.rootPc);
4024
+ const outgoing = signedStep(segment.chord.rootPc, after.chord.rootPc);
4025
+ if (!isStep2(incoming) || !isStep2(outgoing)) {
4026
+ return null;
4027
+ }
4028
+ if (incoming > 0 === outgoing > 0) {
4029
+ return { kind: "passing" };
4030
+ }
4031
+ return pitchClassOf(before.chord.rootPc) === pitchClassOf(after.chord.rootPc) ? { kind: "neighbor" } : null;
4032
+ }
4033
+ function describe2(reason) {
4034
+ switch (reason.kind) {
4035
+ case "tonic":
4036
+ return "Structural: the tonic of the key, which the progression is heard against";
4037
+ case "dominant":
4038
+ return "Structural: the dominant of the key, the chord its tonic is approached from";
4039
+ case "cadentialSixFour":
4040
+ return "Structural: a tonic six-four on the bass of the dominant that follows, which prolongs that dominant rather than framing the tonic";
4041
+ case "cadenceArrival":
4042
+ return `Structural: the arrival of a ${reason.cadence} cadence`;
4043
+ case "cadenceAgent":
4044
+ return `Structural: the chord that drives a ${reason.cadence} cadence`;
4045
+ case "salient":
4046
+ return "Structural: it holds the harmony longer than the chords around it";
4047
+ case "first":
4048
+ return "Structural: the first chord of the progression frames it";
4049
+ case "last":
4050
+ return "Structural: the last chord of the progression frames it";
4051
+ case "unfigured":
4052
+ return "Structural: its root neither steps through to the next chord nor returns to the last, so it embellishes nothing";
4053
+ case "passing":
4054
+ return "Passing: its root steps in from the previous chord and on to the next in the same direction";
4055
+ case "neighbor":
4056
+ return "Neighbor: its root steps away from the surrounding harmony and returns to it";
4057
+ }
4058
+ }
4059
+ var BASIS_READINGS = {
4060
+ function: ({ segment, before, after, keyAt }) => functionalReason(segment, before, after, keyAt),
4061
+ duration: ({ segment, salienceFloor }) => segment.endBeat - segment.startBeat > salienceFloor ? { kind: "salient" } : null
4062
+ };
4063
+ var REDUCTION_BASES = Object.keys(BASIS_READINGS);
4064
+ function levelOf(reason) {
4065
+ if (reason.kind === "passing") {
4066
+ return "passing";
4067
+ }
4068
+ return reason.kind === "neighbor" ? "neighbor" : "structural";
4069
+ }
4070
+ function reduceProgression(timeline, key, opts = {}) {
4071
+ assertChordTimeline(timeline);
4072
+ const asked = assertOptions(opts, "opts");
4073
+ const keyAt = keyScaleAt(key);
4074
+ const basis = assertOneOf(asked.basis ?? "function", REDUCTION_BASES, "reduction basis");
4075
+ const reading = BASIS_READINGS[basis];
4076
+ const segments = timeline.segments;
4077
+ const floors = basis === "duration" ? salienceFloors(segments.map((segment) => segment.endBeat - segment.startBeat)) : [];
4078
+ const result = [];
4079
+ for (let index = 0; index < segments.length; index += 1) {
4080
+ const segment = segments[index];
4081
+ if (segment === void 0) {
4082
+ continue;
4083
+ }
4084
+ const previous = segments[index - 1];
4085
+ const following = segments[index + 1];
4086
+ const before = previous !== void 0 && touches(previous, segment) ? previous : void 0;
4087
+ const after = following !== void 0 && touches(segment, following) ? following : void 0;
4088
+ let reason = reading({ segment, before, after, keyAt, salienceFloor: floors[index] ?? 0 });
4089
+ if (reason === null && index === 0) {
4090
+ reason = { kind: "first" };
4091
+ }
4092
+ if (reason === null && index === segments.length - 1) {
4093
+ reason = { kind: "last" };
4094
+ }
4095
+ reason ??= figureReason(segment, before, after) ?? { kind: "unfigured" };
4096
+ result.push({
4097
+ chord: segment.chord,
4098
+ startBeat: segment.startBeat,
4099
+ endBeat: segment.endBeat,
4100
+ level: levelOf(reason),
4101
+ rationale: describe2(reason)
4102
+ });
4103
+ }
4104
+ return result;
4105
+ }
4106
+
4107
+ // src/analyze/rhythm/internal.ts
4108
+ function isMultipleOf(value, unit) {
4109
+ if (unit <= 0) {
4110
+ return false;
4111
+ }
4112
+ const ratio2 = value / unit;
4113
+ return Math.abs(ratio2 - Math.round(ratio2)) < BEAT_EPS;
4114
+ }
4115
+ function rhythmLevel(beatInQuarters, meters) {
4116
+ const ts = meterAt(beatInQuarters, meters);
4117
+ const offset = beatInQuarters - barStartBeat(beatInQuarters, meters);
4118
+ const pulse = pulseBeats(ts);
4119
+ if (isMultipleOf(offset, pulse)) {
4120
+ return metricWeight(beatInQuarters, meters) + 2;
4121
+ }
4122
+ const subUnit = pulse / (isCompound(ts) ? 3 : 2);
4123
+ return isMultipleOf(offset, subUnit) ? 1 : 0;
4124
+ }
4125
+ function ioiBinIndex(ioi) {
4126
+ const exponent = Math.min(4, Math.max(-4, Math.round(2 * Math.log2(ioi)) / 2));
4127
+ return Math.round((exponent + 4) / 0.5);
4128
+ }
4129
+
4130
+ // src/analyze/rhythm/index.ts
4131
+ var RHYTHM_SLOTS_PER_BEAT = 12;
4132
+ var SLOT_WIDTH = 1 / RHYTHM_SLOTS_PER_BEAT;
4133
+ var RHYTHM_OFF_GRID_TOLERANCE = 1 / 32;
4134
+ var RHYTHM_IOI_BINS = Object.freeze(
4135
+ Array.from({ length: 17 }, (_, i) => 2 ** (-4 + i * 0.5))
4136
+ );
4137
+ function emptyAnalysis() {
4138
+ return {
4139
+ startBeat: 0,
4140
+ endBeat: 0,
4141
+ bars: 0,
4142
+ onsets: 0,
4143
+ onsetDensity: 0,
4144
+ barOnsets: [],
4145
+ onsetLevels: new Array(6).fill(0),
4146
+ barPositions: [],
4147
+ interOnsetShares: new Array(RHYTHM_IOI_BINS.length).fill(0),
4148
+ restRatio: 0,
4149
+ syncopation: 0,
4150
+ offGridRatio: 0,
4151
+ rationale: "Nothing sounds, so there is no rhythm to place on a grid"
4152
+ };
4153
+ }
4154
+ function foldOnsets(sounding) {
4155
+ const sorted = [...sounding].sort((a, b) => a.startBeat - b.startBeat);
4156
+ const onsets = [];
4157
+ for (const note of sorted) {
4158
+ const noteEnd = note.startBeat + note.durationBeat;
4159
+ const current = onsets[onsets.length - 1];
4160
+ if (current !== void 0 && note.startBeat - current.beat <= HUMANIZE_ADJACENCY) {
4161
+ current.end = Math.max(current.end, noteEnd);
4162
+ } else {
4163
+ onsets.push({ beat: note.startBeat, end: noteEnd });
4164
+ }
4165
+ }
4166
+ return onsets;
4167
+ }
4168
+ function snapOnset(onset, meters) {
4169
+ const ts = meterAt(onset.beat, meters);
4170
+ const barStart = barStartBeat(onset.beat, meters);
4171
+ const offset = onset.beat - barStart;
4172
+ const slot = Math.round(offset / SLOT_WIDTH);
4173
+ const snapped = barStart + slot * SLOT_WIDTH;
4174
+ const offGrid = Math.abs(onset.beat - snapped) > RHYTHM_OFF_GRID_TOLERANCE;
4175
+ return { ...onset, ts, barStart, slot, snapped, level: rhythmLevel(snapped, meters), offGrid };
4176
+ }
4177
+ function tsKey(ts) {
4178
+ return ts.grouping === void 0 ? `${ts.numerator}/${ts.denominator}` : `${ts.numerator}/${ts.denominator}:${ts.grouping.join("+")}`;
4179
+ }
4180
+ function barGridBeats(bar, meters) {
4181
+ const barStart = barPositionToBeat({ bar, beat: 0 }, meters);
4182
+ const ts = meterAt(barStart, meters);
4183
+ const barLength = beatsPerBarAt(barStart, meters);
4184
+ const subUnit = pulseBeats(ts) / (isCompound(ts) ? 3 : 2);
4185
+ const steps = Math.max(1, Math.round(barLength / subUnit));
4186
+ return Array.from({ length: steps }, (_, k) => barStart + k * subUnit);
4187
+ }
4188
+ function restRatioOf(sounding, windowStart, windowEnd) {
4189
+ const span = windowEnd - windowStart;
4190
+ if (span <= 0) {
4191
+ return 0;
4192
+ }
4193
+ const intervals = sounding.map((note) => [
4194
+ Math.max(windowStart, note.startBeat),
4195
+ Math.min(windowEnd, note.startBeat + note.durationBeat)
4196
+ ]).filter(([from, to]) => to > from).sort((a, b) => a[0] - b[0]);
4197
+ let covered = 0;
4198
+ let curStart = 0;
4199
+ let curEnd = Number.NEGATIVE_INFINITY;
4200
+ for (const [from, to] of intervals) {
4201
+ if (from - curEnd > HUMANIZE_ADJACENCY) {
4202
+ if (curEnd > Number.NEGATIVE_INFINITY) {
4203
+ covered += curEnd - curStart;
4204
+ }
4205
+ curStart = from;
4206
+ curEnd = to;
4207
+ } else {
4208
+ curEnd = Math.max(curEnd, to);
4209
+ }
4210
+ }
4211
+ if (curEnd > Number.NEGATIVE_INFINITY) {
4212
+ covered += curEnd - curStart;
4213
+ }
4214
+ return 1 - covered / span;
4215
+ }
4216
+ function syncopationOf(snapped, startBeat, endBeat, meters) {
4217
+ if (snapped.length === 0) {
4218
+ return 0;
4219
+ }
4220
+ const firstBar = barIndexAt(startBeat, meters);
4221
+ const lastBar = barIndexAt(endBeat, meters);
4222
+ const points = [];
4223
+ for (let bar = firstBar; bar <= lastBar; bar += 1) {
4224
+ for (const beat of barGridBeats(bar, meters)) {
4225
+ if (beat > startBeat + BEAT_EPS && beat <= endBeat + BEAT_EPS) {
4226
+ points.push({ beat, level: rhythmLevel(beat, meters) });
4227
+ }
4228
+ }
4229
+ }
4230
+ const normalizer = points.reduce((sum, p) => sum + p.level, 0);
4231
+ if (normalizer <= 0) {
4232
+ return 0;
4233
+ }
4234
+ let numerator = 0;
4235
+ let ptr = 0;
4236
+ for (let i = 0; i < snapped.length; i += 1) {
4237
+ const onset = snapped[i];
4238
+ if (onset === void 0) {
4239
+ continue;
4240
+ }
4241
+ const next = snapped[i + 1];
4242
+ const windowEnd = next !== void 0 ? next.snapped : onset.end;
4243
+ while (ptr < points.length && (points[ptr]?.beat ?? Number.POSITIVE_INFINITY) <= onset.snapped + BEAT_EPS) {
4244
+ ptr += 1;
4245
+ }
4246
+ let scan = ptr;
4247
+ let maxLevel = 0;
4248
+ while (scan < points.length && (points[scan]?.beat ?? Number.POSITIVE_INFINITY) < windowEnd - BEAT_EPS) {
4249
+ maxLevel = Math.max(maxLevel, points[scan]?.level ?? 0);
4250
+ scan += 1;
4251
+ }
4252
+ if (maxLevel > onset.level) {
4253
+ numerator += maxLevel - onset.level;
4254
+ }
4255
+ ptr = scan;
4256
+ }
4257
+ return numerator / normalizer;
4258
+ }
4259
+ function analyzeRhythm(notes, opts = {}) {
4260
+ const meters = resolveMeters(opts, "rhythm meters");
4261
+ assertNoteEvents(notes, "rhythm notes", { allowNonPositiveDuration: true, budget: opts.budget });
4262
+ const sounding = notes.filter((note) => note.durationBeat > 0);
4263
+ if (sounding.length === 0) {
4264
+ return emptyAnalysis();
4265
+ }
4266
+ const firstOnset = sounding.reduce(
4267
+ (min, n) => Math.min(min, n.startBeat),
4268
+ Number.POSITIVE_INFINITY
4269
+ );
4270
+ const startBeat = barGridStart(firstOnset, meters);
4271
+ const lastEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
4272
+ const endBeat = Math.max(startBeat, opts.totalBeats ?? lastEnd);
4273
+ assertRange(endBeat, 0, Number.MAX_SAFE_INTEGER, "rhythm totalBeats");
4274
+ const bars = barSpanOf(startBeat, endBeat, meters);
4275
+ assertGenerationBudget(Math.ceil(bars) * 48, "rhythm analysis slots", opts.budget);
4276
+ const onsets = foldOnsets(sounding);
4277
+ const snapped = onsets.map((onset) => snapOnset(onset, meters));
4278
+ const onsetDensity = bars > 0 ? onsets.length / bars : 0;
4279
+ const firstBar = barIndexAt(startBeat, meters);
4280
+ const barOnsets = new Array(lastBarOf(meters, startBeat, endBeat) - firstBar + 1).fill(0);
4281
+ for (const onset of onsets) {
4282
+ const index = barIndexAt(onset.beat, meters) - firstBar;
4283
+ if (index >= 0 && index < barOnsets.length) {
4284
+ barOnsets[index] = (barOnsets[index] ?? 0) + 1;
4285
+ }
4286
+ }
4287
+ const onsetLevels = new Array(6).fill(0);
4288
+ let offGridCount = 0;
4289
+ for (const onset of snapped) {
4290
+ onsetLevels[onset.level] = (onsetLevels[onset.level] ?? 0) + 1;
4291
+ if (onset.offGrid) {
4292
+ offGridCount += 1;
4293
+ }
4294
+ }
4295
+ for (let i = 0; i < onsetLevels.length; i += 1) {
4296
+ onsetLevels[i] = (onsetLevels[i] ?? 0) / onsets.length;
4297
+ }
4298
+ const profiles = [];
4299
+ const profileIndex = /* @__PURE__ */ new Map();
4300
+ for (const onset of snapped) {
4301
+ const key = tsKey(onset.ts);
4302
+ let index = profileIndex.get(key);
4303
+ if (index === void 0) {
4304
+ index = profiles.length;
4305
+ profileIndex.set(key, index);
4306
+ const slotsPerBar = Math.ceil(beatsPerBar(onset.ts) * RHYTHM_SLOTS_PER_BEAT);
4307
+ profiles.push({
4308
+ ts: onset.ts,
4309
+ slotsPerBar,
4310
+ shares: new Array(slotsPerBar).fill(0),
4311
+ onsets: 0
4312
+ });
4313
+ }
4314
+ const profile = profiles[index];
4315
+ if (profile !== void 0) {
4316
+ const slotIndex = (onset.slot % profile.slotsPerBar + profile.slotsPerBar) % profile.slotsPerBar;
4317
+ profile.shares[slotIndex] = (profile.shares[slotIndex] ?? 0) + 1;
4318
+ profile.onsets += 1;
4319
+ }
4320
+ }
4321
+ for (const profile of profiles) {
4322
+ for (let i = 0; i < profile.shares.length; i += 1) {
4323
+ profile.shares[i] = (profile.shares[i] ?? 0) / profile.onsets;
4324
+ }
4325
+ }
4326
+ const interOnsetShares = new Array(RHYTHM_IOI_BINS.length).fill(0);
4327
+ for (let i = 0; i < onsets.length; i += 1) {
4328
+ const onset = onsets[i];
4329
+ if (onset === void 0) {
4330
+ continue;
4331
+ }
4332
+ const next = onsets[i + 1];
4333
+ const ioi = next !== void 0 ? next.beat - onset.beat : onset.end - onset.beat;
4334
+ const bin = ioiBinIndex(ioi);
4335
+ interOnsetShares[bin] = (interOnsetShares[bin] ?? 0) + 1;
4336
+ }
4337
+ for (let i = 0; i < interOnsetShares.length; i += 1) {
4338
+ interOnsetShares[i] = (interOnsetShares[i] ?? 0) / onsets.length;
4339
+ }
4340
+ const restRatio = restRatioOf(sounding, startBeat, endBeat);
4341
+ const syncopation = syncopationOf(snapped, startBeat, endBeat, meters);
4342
+ const offGridRatio = offGridCount / onsets.length;
4343
+ return {
4344
+ startBeat,
4345
+ endBeat,
4346
+ bars,
4347
+ onsets: onsets.length,
4348
+ onsetDensity,
4349
+ barOnsets,
4350
+ onsetLevels,
4351
+ barPositions: profiles,
4352
+ interOnsetShares,
4353
+ restRatio,
4354
+ syncopation,
4355
+ offGridRatio,
4356
+ rationale: `${onsets.length} onsets over ${bars.toFixed(2)} bars (${onsetDensity.toFixed(2)} per bar), ${Math.round(restRatio * 100)}% rest, syncopation ${syncopation.toFixed(2)}`
4357
+ };
4358
+ }
4359
+
4360
+ // src/analyze/reference/types.ts
4361
+ var REFERENCE_PROFILE_VERSION = 1;
4362
+
4363
+ // src/analyze/reference/validate.ts
4364
+ var MASS_TOLERANCE = 1e-9;
4365
+ function assertString(value, name) {
4366
+ if (typeof value !== "string") {
4367
+ throw new InvalidInputError(`${name} must be a string; received ${describeRejected(value)}`);
4368
+ }
4369
+ return value;
4370
+ }
4371
+ function assertSpanOrder(startBeat, endBeat, name) {
4372
+ if (endBeat < startBeat) {
4373
+ throw new InvalidInputError(
4374
+ `${name}.endBeat must be at least ${name}.startBeat; received ${endBeat} < ${startBeat}`
4375
+ );
4376
+ }
4377
+ }
4378
+ function assertNullable(value, name, validate) {
4379
+ if (value === void 0) {
4380
+ throw new InvalidInputError(`${name} must be present; received undefined`);
4381
+ }
4382
+ return value === null ? null : validate(value, name);
4383
+ }
4384
+ function assertMassDistribution(value, length, name, allowAllZero) {
4385
+ const shares = assertArray(value, name);
4386
+ if (shares.length !== length) {
4387
+ throw new InvalidInputError(`${name} must have length ${length}; received ${shares.length}`);
4388
+ }
4389
+ let sum = 0;
4390
+ for (let index = 0; index < shares.length; index += 1) {
4391
+ const share = assertFiniteNumber(shares[index], `${name}[${index}]`);
4392
+ if (share < 0) {
4393
+ throw new InvalidInputError(`${name}[${index}] must be at least 0; received ${share}`);
4394
+ }
4395
+ sum += share;
4396
+ }
4397
+ if (allowAllZero && sum === 0) {
4398
+ return shares;
4399
+ }
4400
+ if (Math.abs(sum - 1) > MASS_TOLERANCE) {
4401
+ throw new InvalidInputError(`${name} must sum to 1; received ${sum}`);
4402
+ }
4403
+ return shares;
4404
+ }
4405
+ function assertBarPositionProfile(value, name) {
4406
+ const read = assertRecord(value, name);
4407
+ assertTimeSignature(read.ts, `${name}.ts`);
4408
+ const slotsPerBar = assertPositiveInt(read.slotsPerBar, `${name}.slotsPerBar`);
4409
+ const onsets = assertInteger(read.onsets, `${name}.onsets`, 0);
4410
+ assertMassDistribution(read.shares, slotsPerBar, `${name}.shares`, onsets === 0);
4411
+ return read;
4412
+ }
4413
+ function assertRhythmAnalysis(value, name) {
4414
+ const read = assertRecord(value, name);
4415
+ const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
4416
+ const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
4417
+ assertSpanOrder(startBeat, endBeat, name);
4418
+ assertRange(read.bars, 0, Number.MAX_SAFE_INTEGER, `${name}.bars`);
4419
+ const onsets = assertInteger(read.onsets, `${name}.onsets`, 0);
4420
+ assertRange(read.onsetDensity, 0, Number.MAX_SAFE_INTEGER, `${name}.onsetDensity`);
4421
+ const barOnsets = assertArray(read.barOnsets, `${name}.barOnsets`);
4422
+ for (let index = 0; index < barOnsets.length; index += 1) {
4423
+ assertRange(
4424
+ barOnsets[index],
4425
+ 0,
4426
+ Number.MAX_SAFE_INTEGER,
4427
+ `${name}.barOnsets[${index}]`
4428
+ );
4429
+ }
4430
+ assertMassDistribution(read.onsetLevels, 6, `${name}.onsetLevels`, onsets === 0);
4431
+ const barPositions = assertArray(read.barPositions, `${name}.barPositions`);
4432
+ for (let index = 0; index < barPositions.length; index += 1) {
4433
+ assertBarPositionProfile(barPositions[index], `${name}.barPositions[${index}]`);
4434
+ }
4435
+ assertMassDistribution(
4436
+ read.interOnsetShares,
4437
+ RHYTHM_IOI_BINS.length,
4438
+ `${name}.interOnsetShares`,
4439
+ onsets === 0
4440
+ );
4441
+ assertRange(read.restRatio, 0, 1, `${name}.restRatio`);
4442
+ assertRange(read.syncopation, 0, 1, `${name}.syncopation`);
4443
+ assertRange(read.offGridRatio, 0, 1, `${name}.offGridRatio`);
4444
+ assertString(read.rationale, `${name}.rationale`);
4445
+ return read;
4446
+ }
4447
+ function assertFormSection(value, name, sectionCount) {
4448
+ const read = assertRecord(value, name);
4449
+ assertString(read.label, `${name}.label`);
4450
+ const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
4451
+ const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
4452
+ assertSpanOrder(startBeat, endBeat, name);
4453
+ assertRange(read.bars, 0, Number.MAX_SAFE_INTEGER, `${name}.bars`);
4454
+ assertInteger(read.firstOccurrence, `${name}.firstOccurrence`, 0, Math.max(sectionCount - 1, 0));
4455
+ assertRange(read.similarity, 0, 1, `${name}.similarity`);
4456
+ assertString(read.rationale, `${name}.rationale`);
4457
+ return read;
4458
+ }
4459
+ function assertReferenceCadence(value, name) {
4460
+ const read = assertRecord(value, name);
4461
+ assertFiniteNumber(read.atBeat, `${name}.atBeat`);
4462
+ assertOneOf(read.type, CADENCE_TYPES, `${name}.type`);
4463
+ assertRange(read.weight, 0, 1, `${name}.weight`);
4464
+ return read;
4465
+ }
4466
+ function assertReferencePhraseMelody(value, name) {
4467
+ const read = assertRecord(value, name);
4468
+ assertOneOf(read.shape, MELODIC_CONTOUR_SHAPES, `${name}.shape`);
4469
+ assertMidiPitch(read.low, `${name}.low`);
4470
+ assertMidiPitch(read.high, `${name}.high`);
4471
+ assertRange(read.mean, 0, 127, `${name}.mean`);
4472
+ assertRange(read.peakPitch, 0, 127, `${name}.peakPitch`);
4473
+ assertRange(read.peakPosition, 0, 1, `${name}.peakPosition`);
4474
+ const outline = assertArray(read.outline, `${name}.outline`);
4475
+ if (outline.length !== 8) {
4476
+ throw new InvalidInputError(`${name}.outline must have length 8; received ${outline.length}`);
4477
+ }
4478
+ for (let index = 0; index < outline.length; index += 1) {
4479
+ assertFiniteNumber(outline[index], `${name}.outline[${index}]`);
4480
+ }
4481
+ assertRange(read.onsetDensity, 0, Number.MAX_SAFE_INTEGER, `${name}.onsetDensity`);
4482
+ assertRange(read.restRatio, 0, 1, `${name}.restRatio`);
4483
+ return read;
4484
+ }
4485
+ function assertReferencePhrase(value, name, sectionCount, nodeCount) {
4486
+ const read = assertRecord(value, name);
4487
+ const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
4488
+ const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
4489
+ assertSpanOrder(startBeat, endBeat, name);
4490
+ assertRange(read.bars, 0, Number.MAX_SAFE_INTEGER, `${name}.bars`);
4491
+ assertNullable(
4492
+ read.section,
4493
+ `${name}.section`,
4494
+ (v, n) => assertInteger(v, n, 0, Math.max(sectionCount - 1, 0))
4495
+ );
4496
+ assertRange(read.confidence, 0, 1, `${name}.confidence`);
4497
+ assertNullable(read.cadence, `${name}.cadence`, assertReferenceCadence);
4498
+ assertNullable(read.melody, `${name}.melody`, assertReferencePhraseMelody);
4499
+ const motifNodes = assertArray(read.motifNodes, `${name}.motifNodes`);
4500
+ for (let index = 0; index < motifNodes.length; index += 1) {
4501
+ assertInteger(
4502
+ motifNodes[index],
4503
+ `${name}.motifNodes[${index}]`,
4504
+ 0,
4505
+ Math.max(nodeCount - 1, 0)
4506
+ );
4507
+ }
4508
+ return read;
4509
+ }
4510
+ function assertReferenceForm(value, name, nodeCount) {
4511
+ const read = assertRecord(value, name);
4512
+ const hypermeter = assertRecord(
4513
+ read.hypermeter,
4514
+ `${name}.hypermeter`
4515
+ );
4516
+ assertRange(hypermeter.groupBars, 0, Number.MAX_SAFE_INTEGER, `${name}.hypermeter.groupBars`);
4517
+ assertRange(hypermeter.confidence, 0, 1, `${name}.hypermeter.confidence`);
4518
+ const sections = assertArray(read.sections, `${name}.sections`);
4519
+ for (let index = 0; index < sections.length; index += 1) {
4520
+ assertFormSection(sections[index], `${name}.sections[${index}]`, sections.length);
4521
+ }
4522
+ const phrases = assertArray(read.phrases, `${name}.phrases`);
4523
+ for (let index = 0; index < phrases.length; index += 1) {
4524
+ assertReferencePhrase(phrases[index], `${name}.phrases[${index}]`, sections.length, nodeCount);
4525
+ }
4526
+ return read;
4527
+ }
4528
+ function assertResolvedKeyField(value, name) {
4529
+ const read = assertRecord(value, name);
4530
+ try {
4531
+ resolveKey(read);
4532
+ } catch (error) {
4533
+ if (error instanceof InvalidInputError) {
4534
+ throw new InvalidInputError(`${name} must be a valid key: ${error.message}`);
4535
+ }
4536
+ throw error;
4537
+ }
4538
+ return read;
4539
+ }
4540
+ function assertReferenceKeyRegion(value, name) {
4541
+ const read = assertRecord(value, name);
4542
+ const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
4543
+ const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
4544
+ assertSpanOrder(startBeat, endBeat, name);
4545
+ assertResolvedKeyField(read.key, `${name}.key`);
4546
+ assertRange(read.confidence, 0, 1, `${name}.confidence`);
4547
+ return read;
4548
+ }
4549
+ function assertReferenceChord(value, name, keys) {
4550
+ const read = assertRecord(value, name);
4551
+ const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
4552
+ const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
4553
+ assertSpanOrder(startBeat, endBeat, name);
4554
+ const keyIndex = assertInteger(read.key, `${name}.key`, 0, Math.max(keys.length - 1, 0));
4555
+ const roman = assertString(read.roman, `${name}.roman`);
4556
+ try {
4557
+ romanToChord(roman, keys[keyIndex]);
4558
+ } catch (error) {
4559
+ if (error instanceof InvalidInputError) {
4560
+ throw new InvalidInputError(
4561
+ `${name}.roman must be readable by romanToChord; received ${describeRejected(roman)}: ${error.message}`
4562
+ );
4563
+ }
4564
+ throw error;
4565
+ }
4566
+ assertOneOf(read.function, HARMONIC_FUNCTIONS, `${name}.function`);
4567
+ assertOneOf(read.level, REDUCTION_LEVELS, `${name}.level`);
4568
+ return read;
4569
+ }
4570
+ function assertReferenceHarmony(value, name) {
4571
+ const read = assertRecord(value, name);
4572
+ const keys = assertArray(read.keys, `${name}.keys`);
4573
+ for (let index = 0; index < keys.length; index += 1) {
4574
+ assertReferenceKeyRegion(keys[index], `${name}.keys[${index}]`);
4575
+ }
4576
+ const resolvedKeys = keys.map((region) => region.key);
4577
+ const chords = assertArray(read.chords, `${name}.chords`);
4578
+ for (let index = 0; index < chords.length; index += 1) {
4579
+ assertReferenceChord(chords[index], `${name}.chords[${index}]`, resolvedKeys);
4580
+ }
4581
+ assertRhythmAnalysis(read.rhythm, `${name}.rhythm`);
4582
+ return read;
4583
+ }
4584
+ function assertMotifRelationSummary(value, name) {
4585
+ const read = assertRecord(value, name);
4586
+ assertOneOf(read.kind, MOTIF_RELATION_KINDS, `${name}.kind`);
4587
+ assertFlag(read.sequence, `${name}.sequence`);
4588
+ assertFiniteNumber(read.semitones, `${name}.semitones`);
4589
+ if (read.degrees !== void 0) {
4590
+ assertFiniteNumber(read.degrees, `${name}.degrees`);
4591
+ }
4592
+ const timeRatio = assertFiniteNumber(read.timeRatio, `${name}.timeRatio`);
4593
+ if (timeRatio <= 0) {
4594
+ throw new InvalidInputError(`${name}.timeRatio must be positive; received ${timeRatio}`);
4595
+ }
4596
+ return read;
4597
+ }
4598
+ function assertMotifGraphNode(value, name, occurrenceCounts) {
4599
+ const read = assertRecord(value, name);
4600
+ const motif = assertInteger(
4601
+ read.motif,
4602
+ `${name}.motif`,
4603
+ 0,
4604
+ Math.max(occurrenceCounts.length - 1, 0)
4605
+ );
4606
+ assertInteger(
4607
+ read.occurrence,
4608
+ `${name}.occurrence`,
4609
+ 0,
4610
+ Math.max((occurrenceCounts[motif] ?? 1) - 1, 0)
4611
+ );
4612
+ const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
4613
+ const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
4614
+ assertSpanOrder(startBeat, endBeat, name);
4615
+ return read;
4616
+ }
4617
+ function assertMotifGraphEdge(value, name, nodeCount) {
4618
+ const read = assertRecord(value, name);
4619
+ const from = assertInteger(read.from, `${name}.from`, 0, Math.max(nodeCount - 1, 0));
4620
+ const to = assertInteger(read.to, `${name}.to`, 0, Math.max(nodeCount - 1, 0));
4621
+ if (from >= to) {
4622
+ throw new InvalidInputError(
4623
+ `${name}.from must be less than ${name}.to; received from=${from}, to=${to}`
4624
+ );
4625
+ }
4626
+ assertNullable(read.relation, `${name}.relation`, assertMotifRelationSummary);
4627
+ assertRange(read.similarity, 0, 1, `${name}.similarity`);
4628
+ return read;
4629
+ }
4630
+ function assertMotifGraph(value, name, occurrenceCounts) {
4631
+ const read = assertRecord(value, name);
4632
+ const nodes = assertArray(read.nodes, `${name}.nodes`);
4633
+ for (let index = 0; index < nodes.length; index += 1) {
4634
+ assertMotifGraphNode(nodes[index], `${name}.nodes[${index}]`, occurrenceCounts);
4635
+ }
4636
+ const edges = assertArray(read.edges, `${name}.edges`);
4637
+ const seenTo = /* @__PURE__ */ new Set();
4638
+ for (let index = 0; index < edges.length; index += 1) {
4639
+ const edge = assertMotifGraphEdge(edges[index], `${name}.edges[${index}]`, nodes.length);
4640
+ if (seenTo.has(edge.to)) {
4641
+ throw new InvalidInputError(`${name}.edges: node ${edge.to} has more than one incoming edge`);
4642
+ }
4643
+ seenTo.add(edge.to);
4644
+ }
4645
+ return read;
4646
+ }
4647
+ function assertReferenceMotif(value, name) {
4648
+ const read = assertRecord(value, name);
4649
+ const intervals = assertArray(read.intervals, `${name}.intervals`);
4650
+ let pitch = 0;
4651
+ let low = 0;
4652
+ let high = 0;
4653
+ for (let index = 0; index < intervals.length; index += 1) {
4654
+ const interval = assertFiniteNumber(intervals[index], `${name}.intervals[${index}]`);
4655
+ if (!Number.isInteger(interval)) {
4656
+ throw new InvalidInputError(
4657
+ `${name}.intervals[${index}] must be a whole number of semitones; received ${interval}`
4658
+ );
4659
+ }
4660
+ pitch += interval;
4661
+ low = Math.min(low, pitch);
4662
+ high = Math.max(high, pitch);
4663
+ }
4664
+ if (high - low > 127) {
4665
+ throw new InvalidInputError(
4666
+ `${name}.intervals must span at most 127 semitones; received ${high - low}`
4667
+ );
4668
+ }
4669
+ const rhythm = assertArray(read.rhythm, `${name}.rhythm`);
4670
+ if (rhythm.length !== intervals.length) {
4671
+ throw new InvalidInputError(
4672
+ `${name}.rhythm must hold one ratio per interval (${intervals.length}); received ${rhythm.length}`
4673
+ );
4674
+ }
4675
+ for (let index = 0; index < rhythm.length; index += 1) {
4676
+ const ratio2 = assertFiniteNumber(rhythm[index], `${name}.rhythm[${index}]`);
4677
+ if (!(ratio2 > 0)) {
4678
+ throw new InvalidInputError(`${name}.rhythm[${index}] must be positive; received ${ratio2}`);
4679
+ }
4680
+ }
4681
+ const unitBeats = assertRange(read.unitBeats, 0, Number.MAX_SAFE_INTEGER, `${name}.unitBeats`);
4682
+ if (unitBeats > 0 !== rhythm.length > 0) {
4683
+ throw new InvalidInputError(
4684
+ `${name}.unitBeats must be positive exactly when rhythm is non-empty; received ${unitBeats} with ${rhythm.length} ratio(s)`
4685
+ );
4686
+ }
4687
+ assertRange(read.spanBeats, 0, Number.MAX_SAFE_INTEGER, `${name}.spanBeats`);
4688
+ assertPositiveInt(read.occurrences, `${name}.occurrences`);
4689
+ return read;
4690
+ }
4691
+ function assertReferenceMelody(value, name) {
4692
+ const read = assertRecord(value, name);
4693
+ assertNullable(read.register, `${name}.register`, (v, n) => {
4694
+ const register = assertRecord(v, n);
4695
+ assertMidiPitch(register.low, `${n}.low`);
4696
+ assertMidiPitch(register.high, `${n}.high`);
4697
+ assertRange(register.mean, 0, 127, `${n}.mean`);
4698
+ return register;
4699
+ });
4700
+ assertNullable(read.shape, `${name}.shape`, (v, n) => assertOneOf(v, MELODIC_CONTOUR_SHAPES, n));
4701
+ const motifs = assertArray(read.motifs, `${name}.motifs`);
4702
+ const occurrenceCounts = [];
4703
+ for (let index = 0; index < motifs.length; index += 1) {
4704
+ assertReferenceMotif(motifs[index], `${name}.motifs[${index}]`);
4705
+ occurrenceCounts.push(motifs[index].occurrences);
4706
+ }
4707
+ assertMotifGraph(read.graph, `${name}.graph`, occurrenceCounts);
4708
+ assertRhythmAnalysis(read.rhythm, `${name}.rhythm`);
4709
+ return read;
4710
+ }
4711
+ function assertReferenceProfile(value, name = "reference profile") {
4712
+ const read = assertRecord(value, name);
4713
+ if (read.profileVersion !== REFERENCE_PROFILE_VERSION) {
4714
+ throw new InvalidInputError(
4715
+ `${name}.profileVersion must be ${REFERENCE_PROFILE_VERSION}; received ${describeRejected(read.profileVersion)}`
4716
+ );
4717
+ }
4718
+ const span = assertRecord(
4719
+ read.span,
4720
+ `${name}.span`
4721
+ );
4722
+ const startBeat = assertFiniteNumber(span.startBeat, `${name}.span.startBeat`);
4723
+ const endBeat = assertFiniteNumber(span.endBeat, `${name}.span.endBeat`);
4724
+ assertSpanOrder(startBeat, endBeat, `${name}.span`);
4725
+ assertRange(span.bars, 0, Number.MAX_SAFE_INTEGER, `${name}.span.bars`);
4726
+ assertMeterMap(read.meters, `${name}.meters`);
4727
+ const melody = assertReferenceMelody(read.melody, `${name}.melody`);
4728
+ assertReferenceForm(read.form, `${name}.form`, melody.graph.nodes.length);
4729
+ assertReferenceHarmony(read.harmony, `${name}.harmony`);
4730
+ return read;
4731
+ }
4732
+
4733
+ // src/analyze/reference/measures.ts
4734
+ var PROGRESSION_GAP = 0.75;
4735
+ var SAME_DEGREE_COST = 0.25;
4736
+ var SAME_FUNCTION_COST = 0.5;
4737
+ var HALF_PHRYGIAN_COST = 0.5;
4738
+ var MODE_CHANGE_COST = 0.5;
4739
+ var SHAPE_MISMATCH_COST = 0.25;
4740
+ var REGISTER_SPAN = 24;
4741
+ var PEAK_POSITION_WEIGHT = 0.5;
4742
+ var FAMILY_SIZE_BINS = 5;
4743
+ var DEPTH_BINS = 4;
4744
+ var NAMED_CHORDS = ["Ger", "It", "Fr", "N"];
4745
+ var PITCH_PRESERVING = /* @__PURE__ */ new Set([
4746
+ "repetition",
4747
+ "augmentation",
4748
+ "diminution"
4749
+ ]);
4750
+ var CELL_START_PITCH = 60;
4751
+ function whenBoth(hasA, hasB, measure) {
4752
+ if (!hasA && !hasB) {
4753
+ return null;
4754
+ }
4755
+ if (!hasA || !hasB) {
4756
+ return 0;
4757
+ }
4758
+ return measure();
4759
+ }
4760
+ function aligned(a, b, cost, gap = 1) {
4761
+ return distanceToSimilarity(gradedEditDistance(a, b, cost, gap), a.length, b.length);
4762
+ }
4763
+ function alignedSimilarity(a, b, cost, gap = 1) {
4764
+ return whenBoth(a.length > 0, b.length > 0, () => aligned(a, b, cost, gap));
4765
+ }
4766
+ function massOf(values) {
4767
+ let total = 0;
4768
+ for (const value of values) {
4769
+ total += value;
4770
+ }
4771
+ return total;
4772
+ }
4773
+ function intersection(a, b) {
4774
+ const massA = massOf(a);
4775
+ const massB = massOf(b);
4776
+ let shared2 = 0;
4777
+ for (let i = 0; i < a.length; i += 1) {
4778
+ shared2 += Math.min((a[i] ?? 0) / massA, (b[i] ?? 0) / massB);
4779
+ }
4780
+ return Math.min(1, shared2);
4781
+ }
4782
+ function distributionSimilarity(a, b) {
4783
+ return whenBoth(massOf(a) > 0, massOf(b) > 0, () => intersection(a, b));
4784
+ }
4785
+ function ratio(a, b) {
4786
+ const larger = Math.max(a, b);
4787
+ return larger === 0 ? 1 : Math.min(a, b) / larger;
4788
+ }
4789
+ function sectionSequenceSimilarity(a, b) {
4790
+ return alignedSimilarity(a, b, (x, y) => x === y ? 0 : 1);
4791
+ }
4792
+ function phraseLengthSimilarity(a, b) {
4793
+ return alignedSimilarity(a, b, (x, y) => {
4794
+ const longer = Math.max(x, y);
4795
+ return longer === 0 ? 0 : Math.min(Math.abs(x - y) / longer, 1);
4796
+ });
4797
+ }
4798
+ function simpleDegreeToken(roman) {
4799
+ for (const name of NAMED_CHORDS) {
4800
+ if (roman.startsWith(name)) {
4801
+ return name;
4802
+ }
4803
+ }
4804
+ const match = roman.match(/^([b#]*)([IV]+)/i);
4805
+ return match ? `${match[1]}${match[2].toUpperCase()}` : roman;
4806
+ }
4807
+ function romanDegreeToken(roman) {
4808
+ const slash = roman.indexOf("/");
4809
+ if (slash < 0) {
4810
+ return simpleDegreeToken(roman);
4811
+ }
4812
+ return `${simpleDegreeToken(roman.slice(0, slash))}/${simpleDegreeToken(roman.slice(slash + 1))}`;
4813
+ }
4814
+ function chordCost(x, y) {
4815
+ if (x.roman === y.roman) {
4816
+ return 0;
4817
+ }
4818
+ if (romanDegreeToken(x.roman) === romanDegreeToken(y.roman)) {
4819
+ return SAME_DEGREE_COST;
4820
+ }
4821
+ return x.function === y.function ? SAME_FUNCTION_COST : 1;
4822
+ }
4823
+ function progressionSimilarity(a, b, budget) {
4824
+ assertGenerationBudget(a.length * b.length, "reference progression alignment", budget);
4825
+ return alignedSimilarity(a, b, chordCost, PROGRESSION_GAP);
4826
+ }
4827
+ function functionMass(chords) {
4828
+ const mass = HARMONIC_FUNCTIONS.map(() => 0);
4829
+ for (const chord of chords) {
4830
+ const bin = HARMONIC_FUNCTIONS.indexOf(chord.function);
4831
+ mass[bin] = (mass[bin] ?? 0) + Math.max(0, chord.endBeat - chord.startBeat);
4832
+ }
4833
+ return mass;
4834
+ }
4835
+ function functionSimilarity(a, b) {
4836
+ return distributionSimilarity(functionMass(a), functionMass(b));
4837
+ }
4838
+ function cadenceSimilarity(a, b) {
4839
+ return alignedSimilarity(a, b, (x, y) => {
4840
+ if (x === y) {
4841
+ return 0;
4842
+ }
4843
+ const halfAndPhrygian = x === "half" && y === "phrygian" || x === "phrygian" && y === "half";
4844
+ return halfAndPhrygian ? HALF_PHRYGIAN_COST : 1;
4845
+ });
4846
+ }
4847
+ function keyTokens(keys) {
4848
+ const home = keys[0]?.scale.rootPc ?? 0;
4849
+ return keys.map((key) => ({
4850
+ distance: ((key.scale.rootPc - home) % 12 + 12) % 12,
4851
+ mask: key.scale.modeMask12
4852
+ }));
4853
+ }
4854
+ function keyPlanSimilarity(a, b) {
4855
+ return alignedSimilarity(keyTokens(a), keyTokens(b), (x, y) => {
4856
+ if (x.distance !== y.distance) {
4857
+ return 1;
4858
+ }
4859
+ return x.mask === y.mask ? 0 : MODE_CHANGE_COST;
4860
+ });
4861
+ }
4862
+ function onsetSimilarity(a, b) {
4863
+ return whenBoth(a.onsets > 0, b.onsets > 0, () => {
4864
+ let weighted = 0;
4865
+ let weight = 0;
4866
+ for (const left of a.barPositions) {
4867
+ for (const right of b.barPositions) {
4868
+ const shared2 = left.ts.numerator === right.ts.numerator && left.ts.denominator === right.ts.denominator && left.onsets > 0 && right.onsets > 0;
4869
+ if (shared2) {
4870
+ const w = left.onsets + right.onsets;
4871
+ weighted += w * intersection(left.shares, right.shares);
4872
+ weight += w;
4873
+ }
4874
+ }
4875
+ }
4876
+ return weight > 0 ? weighted / weight : intersection(a.onsetLevels, b.onsetLevels);
4877
+ });
4878
+ }
4879
+ function densitySimilarity(a, b) {
4880
+ return whenBoth(a.onsets > 0, b.onsets > 0, () => ratio(a.onsetDensity, b.onsetDensity));
4881
+ }
4882
+ function durationSimilarity(a, b) {
4883
+ return whenBoth(
4884
+ a.onsets > 0,
4885
+ b.onsets > 0,
4886
+ () => intersection(a.interOnsetShares, b.interOnsetShares)
4887
+ );
4888
+ }
4889
+ function syncopationSimilarity(a, b) {
4890
+ return whenBoth(a.onsets > 0, b.onsets > 0, () => 1 - Math.abs(a.syncopation - b.syncopation));
4891
+ }
4892
+ function cellNotes(motif) {
4893
+ const notes = [];
4894
+ let low = 0;
4895
+ let high = 0;
4896
+ let offset = 0;
4897
+ for (const interval of motif.intervals) {
4898
+ offset += interval;
4899
+ low = Math.min(low, offset);
4900
+ high = Math.max(high, offset);
4901
+ }
4902
+ let pitch = Math.min(127 - high, Math.max(-low, CELL_START_PITCH));
4903
+ let beat = 0;
4904
+ for (let i = 0; i <= motif.intervals.length; i += 1) {
4905
+ const step = motif.rhythm[i] ?? 1;
4906
+ notes.push({ pitch, startBeat: beat, durationBeat: i < motif.intervals.length ? step : 1 });
4907
+ pitch += motif.intervals[i] ?? 0;
4908
+ beat += step;
4909
+ }
4910
+ return notes;
4911
+ }
4912
+ function bestMatchMean(from, to) {
4913
+ let weighted = 0;
4914
+ let weight = 0;
4915
+ for (const motif of from) {
4916
+ let best = 0;
4917
+ for (const other of to) {
4918
+ best = Math.max(best, compareMelodies(motif.cell, other.cell).pitchSimilarity);
4919
+ }
4920
+ weighted += motif.occurrences * best;
4921
+ weight += motif.occurrences;
4922
+ }
4923
+ return weighted / weight;
4924
+ }
4925
+ function surfaceSimilarity(a, b) {
4926
+ return whenBoth(a.length > 0, b.length > 0, () => {
4927
+ const left = a.map((motif) => ({ cell: cellNotes(motif), occurrences: motif.occurrences }));
4928
+ const right = b.map((motif) => ({ cell: cellNotes(motif), occurrences: motif.occurrences }));
4929
+ return (bestMatchMean(left, right) + bestMatchMean(right, left)) / 2;
4930
+ });
4931
+ }
4932
+ function coverageOf(graph, spanBeats) {
4933
+ if (!(spanBeats > 0) || graph.nodes.length === 0) {
4934
+ return 0;
4935
+ }
4936
+ const spans = graph.nodes.map((node) => [node.startBeat, node.endBeat]).sort((x, y) => x[0] - y[0] || x[1] - y[1]);
4937
+ let covered = 0;
4938
+ let [runStart, runEnd] = spans[0];
4939
+ for (const [start, end] of spans) {
4940
+ if (start > runEnd) {
4941
+ covered += runEnd - runStart;
4942
+ runStart = start;
4943
+ runEnd = end;
4944
+ } else {
4945
+ runEnd = Math.max(runEnd, end);
4946
+ }
4947
+ }
4948
+ covered += runEnd - runStart;
4949
+ return Math.min(1, covered / spanBeats);
4950
+ }
4951
+ function bump(histogram, bin) {
4952
+ histogram[bin] = (histogram[bin] ?? 0) + 1;
4953
+ }
4954
+ function summarise({ graph, spanBeats }) {
4955
+ const kinds = new Array(MOTIF_RELATION_KINDS.length + 1).fill(0);
4956
+ const parentEdge = /* @__PURE__ */ new Map();
4957
+ for (const edge of graph.edges) {
4958
+ const kind = edge.relation?.kind;
4959
+ bump(
4960
+ kinds,
4961
+ kind === void 0 ? MOTIF_RELATION_KINDS.length : MOTIF_RELATION_KINDS.indexOf(kind)
4962
+ );
4963
+ parentEdge.set(edge.to, edge);
4964
+ }
4965
+ const depth = new Array(graph.nodes.length).fill(0);
4966
+ const root = graph.nodes.map((_, i) => i);
4967
+ for (let node = 0; node < graph.nodes.length; node += 1) {
4968
+ const edge = parentEdge.get(node);
4969
+ if (edge !== void 0) {
4970
+ const kind = edge.relation?.kind;
4971
+ const keepsPitch = kind !== void 0 && PITCH_PRESERVING.has(kind);
4972
+ depth[node] = (depth[edge.from] ?? 0) + (keepsPitch ? 0 : 1);
4973
+ root[node] = root[edge.from] ?? edge.from;
4974
+ }
4975
+ }
4976
+ const familySize = /* @__PURE__ */ new Map();
4977
+ for (const r of root) {
4978
+ familySize.set(r, (familySize.get(r) ?? 0) + 1);
4979
+ }
4980
+ const families = new Array(FAMILY_SIZE_BINS).fill(0);
4981
+ for (const size of familySize.values()) {
4982
+ bump(families, Math.min(size, FAMILY_SIZE_BINS) - 1);
4983
+ }
4984
+ const depths = new Array(DEPTH_BINS).fill(0);
4985
+ for (const d of depth) {
4986
+ bump(depths, Math.min(d, DEPTH_BINS - 1));
4987
+ }
4988
+ return { kinds, families, depths, coverage: coverageOf(graph, spanBeats) };
4989
+ }
4990
+ function motifStructureSimilarity(a, b) {
4991
+ const left = summarise(a);
4992
+ const right = summarise(b);
4993
+ const terms = [
4994
+ distributionSimilarity(left.kinds, right.kinds),
4995
+ distributionSimilarity(left.families, right.families),
4996
+ distributionSimilarity(left.depths, right.depths),
4997
+ whenBoth(
4998
+ left.coverage > 0,
4999
+ right.coverage > 0,
5000
+ () => 1 - Math.abs(left.coverage - right.coverage)
5001
+ )
5002
+ ].filter((term) => term !== null);
5003
+ return terms.length === 0 ? null : massOf(terms) / terms.length;
5004
+ }
5005
+ function nullableCost(cost) {
5006
+ return (x, y) => {
5007
+ if (x === null || y === null) {
5008
+ return x === y ? 0 : 1;
5009
+ }
5010
+ return cost(x, y);
5011
+ };
5012
+ }
5013
+ function phraseSimilarity(a, b, cost) {
5014
+ const hasA = a.some((phrase) => phrase !== null);
5015
+ const hasB = b.some((phrase) => phrase !== null);
5016
+ return whenBoth(hasA, hasB, () => aligned(a, b, nullableCost(cost)));
5017
+ }
5018
+ function normalisedOutline(phrase) {
5019
+ const mean = massOf(phrase.outline) / (phrase.outline.length || 1);
5020
+ const range = phrase.high - phrase.low || 1;
5021
+ return phrase.outline.map((point) => (point - mean) / range);
5022
+ }
5023
+ function contourCost(x, y) {
5024
+ const left = normalisedOutline(x);
5025
+ const right = normalisedOutline(y);
5026
+ let difference = 0;
5027
+ for (let i = 0; i < left.length; i += 1) {
5028
+ difference += Math.abs((left[i] ?? 0) - (right[i] ?? 0));
5029
+ }
5030
+ const outlineCost = Math.min(1, difference / (left.length || 1));
5031
+ return Math.min(1, outlineCost + (x.shape === y.shape ? 0 : SHAPE_MISMATCH_COST));
5032
+ }
5033
+ function contourSimilarity(a, b) {
5034
+ return phraseSimilarity(a, b, contourCost);
5035
+ }
5036
+ function registerSimilarity(a, b) {
5037
+ const meanA = a.mean ?? 0;
5038
+ const meanB = b.mean ?? 0;
5039
+ return phraseSimilarity(a.phrases, b.phrases, (x, y) => {
5040
+ const offset = Math.abs(x.mean - meanA - (y.mean - meanB));
5041
+ const range = Math.abs(x.high - x.low - (y.high - y.low));
5042
+ const peak = Math.abs(x.peakPosition - y.peakPosition);
5043
+ return Math.min(1, (offset + range) / REGISTER_SPAN + PEAK_POSITION_WEIGHT * peak);
5044
+ });
5045
+ }
5046
+
5047
+ // src/analyze/reference/internal.ts
5048
+ function weightedMean(notes) {
5049
+ let weighted = 0;
5050
+ let total = 0;
5051
+ for (const note of notes) {
5052
+ weighted += note.pitch * note.durationBeat;
5053
+ total += note.durationBeat;
5054
+ }
5055
+ return total > 0 ? weighted / total : 0;
5056
+ }
5057
+ function phraseMelody(line, startBeat, endBeat, lineMean, meters, budget) {
5058
+ const inside = line.filter((note) => note.startBeat >= startBeat - BEAT_EPS && note.startBeat < endBeat - BEAT_EPS).map((note) => ({
5059
+ ...note,
5060
+ durationBeat: Math.min(note.startBeat + note.durationBeat, endBeat) - note.startBeat
5061
+ }));
5062
+ const first = inside[0];
5063
+ if (first === void 0 || inside.length < 2) {
5064
+ return null;
5065
+ }
5066
+ let low = first.pitch;
5067
+ let peak = first;
5068
+ for (const note of inside) {
5069
+ low = Math.min(low, note.pitch);
5070
+ if (note.pitch > peak.pitch) {
5071
+ peak = note;
5072
+ }
5073
+ }
5074
+ const length = endBeat - startBeat;
5075
+ const outline = [];
5076
+ for (let k = 0; k < 8; k += 1) {
5077
+ const at = startBeat + (k + 0.5) * length / 8;
5078
+ let held = first;
5079
+ for (const note of inside) {
5080
+ if (note.startBeat > at + BEAT_EPS) {
5081
+ break;
5082
+ }
5083
+ held = note;
5084
+ }
5085
+ outline.push(held.pitch - lineMean);
5086
+ }
5087
+ const rhythm = analyzeRhythm(inside, { meters, totalBeats: endBeat, budget });
5088
+ return {
5089
+ shape: melodicContour(inside).shape,
5090
+ low,
5091
+ high: peak.pitch,
5092
+ mean: weightedMean(inside),
5093
+ peakPitch: peak.pitch,
5094
+ peakPosition: Math.min(1, Math.max(0, (peak.startBeat - startBeat) / length)),
5095
+ outline,
5096
+ onsetDensity: rhythm.onsetDensity,
5097
+ restRatio: rhythm.restRatio
5098
+ };
5099
+ }
5100
+
3270
5101
  export {
3271
5102
  HUMANIZE_ADJACENCY,
3272
5103
  sameInstant,
@@ -3283,14 +5114,14 @@ export {
3283
5114
  augmentedSixthKind,
3284
5115
  augmentedSixthFromPitchClasses,
3285
5116
  spellAugmentedSixth,
3286
- isCadentialSixFour,
5117
+ CADENCE_TYPES,
3287
5118
  detectCadence,
3288
5119
  isBorrowedChord,
3289
5120
  borrowedSource,
3290
- isAppliedDominant,
3291
5121
  romanToChord,
3292
5122
  chordToRoman,
3293
5123
  explainRoman,
5124
+ HARMONIC_FUNCTIONS,
3294
5125
  isMinorKey,
3295
5126
  functionOf,
3296
5127
  isDiatonic,
@@ -3299,6 +5130,7 @@ export {
3299
5130
  secondaryDominant,
3300
5131
  secondaryDominantOf,
3301
5132
  pivotChords,
5133
+ barSpanOf,
3302
5134
  clamp01,
3303
5135
  floorMod,
3304
5136
  firstSoundingBeat,
@@ -3314,7 +5146,6 @@ export {
3314
5146
  attachPivots,
3315
5147
  keyLookup,
3316
5148
  prevailingKeyOf,
3317
- keyScaleAt,
3318
5149
  toVoiceNotes,
3319
5150
  analyzeVoice,
3320
5151
  assertChordTimeline,
@@ -3324,6 +5155,41 @@ export {
3324
5155
  chordTimelineFromNotes,
3325
5156
  dirtySlotsFor,
3326
5157
  analyzeTimeline,
3327
- detectCadences
5158
+ detectCadences,
5159
+ orderedNotes,
5160
+ MELODIC_CONTOUR_SHAPES,
5161
+ melodicContour,
5162
+ extractMotifs,
5163
+ motifFromNotes,
5164
+ MOTIF_RELATION_KINDS,
5165
+ relateMotifs,
5166
+ compareMelodies,
5167
+ melodicSimilarity,
5168
+ DEFAULT_VARIATION_THRESHOLD,
5169
+ motifGraph,
5170
+ REDUCTION_LEVELS,
5171
+ reduceProgression,
5172
+ sectionSequenceSimilarity,
5173
+ phraseLengthSimilarity,
5174
+ progressionSimilarity,
5175
+ functionSimilarity,
5176
+ cadenceSimilarity,
5177
+ keyPlanSimilarity,
5178
+ onsetSimilarity,
5179
+ densitySimilarity,
5180
+ durationSimilarity,
5181
+ syncopationSimilarity,
5182
+ surfaceSimilarity,
5183
+ motifStructureSimilarity,
5184
+ contourSimilarity,
5185
+ registerSimilarity,
5186
+ rhythmLevel,
5187
+ ioiBinIndex,
5188
+ RHYTHM_IOI_BINS,
5189
+ analyzeRhythm,
5190
+ REFERENCE_PROFILE_VERSION,
5191
+ assertReferenceProfile,
5192
+ weightedMean,
5193
+ phraseMelody
3328
5194
  };
3329
- //# sourceMappingURL=chunk-H2KJHBW4.js.map
5195
+ //# sourceMappingURL=chunk-GNVN4PPJ.js.map