@libraz/libcantus 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/NOTICE +63 -0
  2. package/README.md +7 -1
  3. package/README_ja.md +17 -11
  4. package/dist/analyze/index.cjs +31 -7
  5. package/dist/analyze/index.cjs.map +1 -1
  6. package/dist/analyze/index.d.cts +161 -13
  7. package/dist/analyze/index.d.ts +161 -13
  8. package/dist/analyze/index.js +37 -13
  9. package/dist/{chunk-NB25P3EH.js → chunk-2G5MCGYU.js} +33 -11
  10. package/dist/chunk-2G5MCGYU.js.map +1 -0
  11. package/dist/{chunk-NM7UAMC3.cjs → chunk-3FDBMIPR.cjs} +64 -64
  12. package/dist/chunk-3FDBMIPR.cjs.map +1 -0
  13. package/dist/{chunk-HPVSH7SF.cjs → chunk-3MDOEFIQ.cjs} +618 -954
  14. package/dist/chunk-3MDOEFIQ.cjs.map +1 -0
  15. package/dist/{chunk-FBH75DAY.js → chunk-5MWRBYAT.js} +560 -159
  16. package/dist/chunk-5MWRBYAT.js.map +1 -0
  17. package/dist/{chunk-J7FHVZII.cjs → chunk-6TTBEON3.cjs} +3074 -740
  18. package/dist/chunk-6TTBEON3.cjs.map +1 -0
  19. package/dist/{chunk-UMWZ4ZET.js → chunk-7NQ6AL3H.js} +4 -4
  20. package/dist/chunk-7NQ6AL3H.js.map +1 -0
  21. package/dist/{chunk-HWHGOVO6.cjs → chunk-E3QO3UET.cjs} +31 -23
  22. package/dist/chunk-E3QO3UET.cjs.map +1 -0
  23. package/dist/{chunk-H2KJHBW4.js → chunk-FJFULJZE.js} +2375 -191
  24. package/dist/chunk-FJFULJZE.js.map +1 -0
  25. package/dist/{chunk-Q6LFSAKV.js → chunk-FK3DGAIZ.js} +2657 -323
  26. package/dist/chunk-FK3DGAIZ.js.map +1 -0
  27. package/dist/chunk-GEDTJHAC.cjs.map +1 -1
  28. package/dist/{chunk-RFIBF2RR.js → chunk-H7FVMBUC.js} +138 -65
  29. package/dist/chunk-H7FVMBUC.js.map +1 -0
  30. package/dist/{chunk-DSMJRXCZ.cjs → chunk-HYSVSFNI.cjs} +931 -530
  31. package/dist/chunk-HYSVSFNI.cjs.map +1 -0
  32. package/dist/{chunk-35NBV2FT.cjs → chunk-KMYGRFZ6.cjs} +47 -24
  33. package/dist/chunk-KMYGRFZ6.cjs.map +1 -0
  34. package/dist/chunk-LQZQDBCD.cjs +5513 -0
  35. package/dist/chunk-LQZQDBCD.cjs.map +1 -0
  36. package/dist/{chunk-PM4XJE2Y.cjs → chunk-P3CPXAJ5.cjs} +76 -54
  37. package/dist/chunk-P3CPXAJ5.cjs.map +1 -0
  38. package/dist/{chunk-S5BKZZGM.cjs → chunk-S3QELJUE.cjs} +72 -56
  39. package/dist/chunk-S3QELJUE.cjs.map +1 -0
  40. package/dist/{chunk-PUKJP2Z5.js → chunk-TXFWSU5D.js} +31 -8
  41. package/dist/chunk-TXFWSU5D.js.map +1 -0
  42. package/dist/{chunk-76ZBTRG2.cjs → chunk-U4EQTDGN.cjs} +258 -218
  43. package/dist/chunk-U4EQTDGN.cjs.map +1 -0
  44. package/dist/chunk-UGLFDG7L.cjs.map +1 -1
  45. package/dist/{chunk-HYJJ2V2I.js → chunk-UTE5A36R.js} +483 -819
  46. package/dist/chunk-UTE5A36R.js.map +1 -0
  47. package/dist/{chunk-NFJ4P6ZZ.js → chunk-UZKQISRF.js} +17 -9
  48. package/dist/chunk-UZKQISRF.js.map +1 -0
  49. package/dist/{chunk-7KG3RTYS.js → chunk-VL5NXVCN.js} +281 -155
  50. package/dist/chunk-VL5NXVCN.js.map +1 -0
  51. package/dist/{chunk-CGBADIO5.cjs → chunk-WOMCXN2W.cjs} +328 -202
  52. package/dist/chunk-WOMCXN2W.cjs.map +1 -0
  53. package/dist/{chunk-3R3QVKY5.cjs → chunk-YDGMWH43.cjs} +397 -324
  54. package/dist/chunk-YDGMWH43.cjs.map +1 -0
  55. package/dist/{chunk-AGOVW7U6.js → chunk-YYVXRV3V.js} +72 -56
  56. package/dist/chunk-YYVXRV3V.js.map +1 -0
  57. package/dist/{chunk-M5RYMAFI.js → chunk-ZLNSH4UL.js} +71 -31
  58. package/dist/chunk-ZLNSH4UL.js.map +1 -0
  59. package/dist/{coerce-DRERzdve.d.cts → coerce-CUhHc3Tu.d.ts} +16 -8
  60. package/dist/{coerce-DP0vYu-x.d.ts → coerce-Ytt2Eiq9.d.cts} +16 -8
  61. package/dist/core/index.cjs +5 -5
  62. package/dist/core/index.cjs.map +1 -1
  63. package/dist/core/index.d.cts +9 -8
  64. package/dist/core/index.d.ts +9 -8
  65. package/dist/core/index.js +4 -4
  66. package/dist/generate/index.cjs +20 -8
  67. package/dist/generate/index.cjs.map +1 -1
  68. package/dist/generate/index.d.cts +105 -17
  69. package/dist/generate/index.d.ts +105 -17
  70. package/dist/generate/index.js +19 -7
  71. package/dist/{index-CL4b4gUQ.d.ts → index-5VJgKz_Q.d.cts} +6 -2
  72. package/dist/{index-B9QLziDe.d.cts → index-BLuDesVG.d.ts} +20 -13
  73. package/dist/{index-BHIUyjhZ.d.ts → index-BV6APVa6.d.cts} +20 -13
  74. package/dist/{index-BMweUlm5.d.cts → index-Bf0Sv8a9.d.ts} +6 -2
  75. package/dist/{index-Dw2DP7vm.d.ts → index-BkT21EMv.d.cts} +3 -2
  76. package/dist/{index-BL5N2Oyz.d.cts → index-C7KCaTRz.d.ts} +3 -2
  77. package/dist/{index-Cy15oTvh.d.ts → index-C9MYbuKS.d.cts} +7 -3
  78. package/dist/{index-Cy15oTvh.d.cts → index-C9MYbuKS.d.ts} +7 -3
  79. package/dist/{index-BNSM-DSk.d.cts → index-DVInzCDS.d.cts} +533 -28
  80. package/dist/{index-9eoQs2C1.d.ts → index-Dg-SAfhz.d.ts} +6 -4
  81. package/dist/{index-BJgP97N1.d.ts → index-jgeug9EA.d.ts} +533 -28
  82. package/dist/{index-cHvbYgJI.d.cts → index-pQihL4z7.d.cts} +6 -4
  83. package/dist/index.cjs +49 -13
  84. package/dist/index.cjs.map +1 -1
  85. package/dist/index.d.cts +13 -13
  86. package/dist/index.d.ts +13 -13
  87. package/dist/index.js +55 -19
  88. package/dist/{masks-Cc7-MzzY.d.ts → masks-BnzXnBj0.d.cts} +3 -2
  89. package/dist/{masks-pADfTOiD.d.cts → masks-tycmkrgs.d.ts} +3 -2
  90. package/dist/model/index.cjs +12 -12
  91. package/dist/model/index.cjs.map +1 -1
  92. package/dist/model/index.d.cts +259 -48
  93. package/dist/model/index.d.ts +259 -48
  94. package/dist/model/index.js +11 -11
  95. package/dist/{positional-DJgs6ltD.d.ts → positional-CscpxtlE.d.cts} +3 -2
  96. package/dist/{positional-DMZer-FM.d.cts → positional-DLnfUgqG.d.ts} +3 -2
  97. package/dist/{index-BRMbPkHE.d.ts → profile-B4WqcM4H.d.ts} +85 -610
  98. package/dist/{index-DGCNKAUr.d.cts → profile-BJd1XkPB.d.cts} +85 -610
  99. package/dist/theory/index.cjs +5 -5
  100. package/dist/theory/index.cjs.map +1 -1
  101. package/dist/theory/index.d.cts +18 -14
  102. package/dist/theory/index.d.ts +18 -14
  103. package/dist/theory/index.js +4 -4
  104. package/dist/{index-CJBBiAmU.d.ts → types-7mGFwvMm.d.ts} +1210 -31
  105. package/dist/{index-BBWNZS0f.d.cts → types-_ofpb2be.d.cts} +1210 -31
  106. package/package.json +13 -13
  107. package/dist/chunk-35NBV2FT.cjs.map +0 -1
  108. package/dist/chunk-3R3QVKY5.cjs.map +0 -1
  109. package/dist/chunk-76ZBTRG2.cjs.map +0 -1
  110. package/dist/chunk-7KG3RTYS.js.map +0 -1
  111. package/dist/chunk-AGOVW7U6.js.map +0 -1
  112. package/dist/chunk-CGBADIO5.cjs.map +0 -1
  113. package/dist/chunk-DSMJRXCZ.cjs.map +0 -1
  114. package/dist/chunk-FBH75DAY.js.map +0 -1
  115. package/dist/chunk-H2KJHBW4.js.map +0 -1
  116. package/dist/chunk-HPVSH7SF.cjs.map +0 -1
  117. package/dist/chunk-HWHGOVO6.cjs.map +0 -1
  118. package/dist/chunk-HYJJ2V2I.js.map +0 -1
  119. package/dist/chunk-J7FHVZII.cjs.map +0 -1
  120. package/dist/chunk-M5RYMAFI.js.map +0 -1
  121. package/dist/chunk-NB25P3EH.js.map +0 -1
  122. package/dist/chunk-NFJ4P6ZZ.js.map +0 -1
  123. package/dist/chunk-NM7UAMC3.cjs.map +0 -1
  124. package/dist/chunk-PM4XJE2Y.cjs.map +0 -1
  125. package/dist/chunk-PUKJP2Z5.js.map +0 -1
  126. package/dist/chunk-Q6LFSAKV.js.map +0 -1
  127. package/dist/chunk-RFIBF2RR.js.map +0 -1
  128. package/dist/chunk-S5BKZZGM.cjs.map +0 -1
  129. package/dist/chunk-SGLEQVJH.cjs +0 -3329
  130. package/dist/chunk-SGLEQVJH.cjs.map +0 -1
  131. package/dist/chunk-UMWZ4ZET.js.map +0 -1
@@ -1,16 +1,39 @@
1
1
  import {
2
+ CADENCE_TYPES,
3
+ DEFAULT_VARIATION_THRESHOLD,
2
4
  HUMANIZE_ADJACENCY,
5
+ MELODIC_CONTOUR_SHAPES,
6
+ MOTIF_RELATION_KINDS,
7
+ RHYTHM_IOI_BINS,
8
+ analyzeRhythm,
9
+ analyzeVoice,
3
10
  assertChordTimeline,
11
+ assertReferenceProfile,
12
+ barSpanOf,
4
13
  borrowedSource,
14
+ chordTimelineFromChords,
5
15
  chordToRoman,
16
+ contourSimilarity,
17
+ densitySimilarity,
6
18
  detectChordBest,
19
+ durationSimilarity,
7
20
  functionOf,
21
+ ioiBinIndex,
8
22
  isDiatonic,
9
- isMinorKey,
10
- parallelKey
11
- } from "./chunk-H2KJHBW4.js";
23
+ keyLookup,
24
+ melodicSimilarity,
25
+ onsetSimilarity,
26
+ parallelKey,
27
+ phraseMelody,
28
+ registerSimilarity,
29
+ rhythmLevel,
30
+ romanToChord,
31
+ syncopationSimilarity
32
+ } from "./chunk-FJFULJZE.js";
12
33
  import {
34
+ ALGORITHM_VERSION,
13
35
  LIMBS,
36
+ MIN_ALGORITHM_VERSION,
14
37
  canSound,
15
38
  createPositionalRng,
16
39
  deriveSeed,
@@ -18,16 +41,20 @@ import {
18
41
  includeAt,
19
42
  instrumentRange,
20
43
  resolveAlgorithmVersion,
44
+ scaleUnitDraw,
21
45
  toStringedProfile
22
- } from "./chunk-7KG3RTYS.js";
46
+ } from "./chunk-VL5NXVCN.js";
23
47
  import {
24
- createNoteEventIndex
25
- } from "./chunk-PUKJP2Z5.js";
48
+ canonicalNoteOrder,
49
+ createNoteEventIndex,
50
+ soundingIndices
51
+ } from "./chunk-TXFWSU5D.js";
26
52
  import {
27
53
  chordScales,
28
54
  roleOf
29
- } from "./chunk-NB25P3EH.js";
55
+ } from "./chunk-2G5MCGYU.js";
30
56
  import {
57
+ cadencesThroughRaisedSeventh,
31
58
  chordPitchClasses,
32
59
  chordQualities,
33
60
  chordToneRole,
@@ -39,6 +66,7 @@ import {
39
66
  fifthPcOf,
40
67
  heptatonicFrameOf,
41
68
  isForbiddenMelodicLeap,
69
+ isNativeDominantSeventh,
42
70
  isScaleTone,
43
71
  leadingTonePcOf,
44
72
  majorKey,
@@ -50,26 +78,32 @@ import {
50
78
  resolveKey,
51
79
  scaleLadderPitch,
52
80
  scaleLadderPosition,
53
- scaleSystemOf,
81
+ scaleOf,
54
82
  scaleTonesInDegreeOrder,
55
83
  shiftByScaleDegrees,
56
84
  soundsDominantSeventh,
85
+ spanFromChord,
57
86
  spellPitchClass,
58
87
  toChordData,
59
88
  toKeyScale,
60
89
  tonicizableDegrees
61
- } from "./chunk-RFIBF2RR.js";
90
+ } from "./chunk-H7FVMBUC.js";
62
91
  import {
63
92
  ARTICULATIONS,
64
93
  BEAT_EPS,
65
94
  InvalidInputError,
95
+ NoSolutionError,
96
+ allocateCandidateTable,
97
+ allocateChoiceTable,
66
98
  assertArray,
67
99
  assertDegree,
68
100
  assertFiniteNumber,
69
101
  assertFiniteSemitones,
102
+ assertFlag,
70
103
  assertFunction,
71
104
  assertGenerationBudget,
72
105
  assertInteger,
106
+ assertMeterMap,
73
107
  assertMidiPitch,
74
108
  assertNoteEvents,
75
109
  assertOneOf,
@@ -78,7 +112,10 @@ import {
78
112
  assertRange,
79
113
  assertRecord,
80
114
  assertTimeSignature,
115
+ barIndexAt,
116
+ barStartBeat,
81
117
  beatsPerBar,
118
+ beatsPerBarAt,
82
119
  clampToMidi,
83
120
  describeRejected,
84
121
  dropSilentNotes,
@@ -92,10 +129,12 @@ import {
92
129
  pulseBeats,
93
130
  resolveMeters,
94
131
  soundingNotesOnly,
132
+ spelledInterval,
95
133
  toMeterData,
96
134
  toSpelledInterval,
135
+ transposeByInterval,
97
136
  transposeNote
98
- } from "./chunk-AGOVW7U6.js";
137
+ } from "./chunk-YYVXRV3V.js";
99
138
 
100
139
  // src/generate/context/draw.ts
101
140
  var DRAW_METHODS = ["at", "prob", "range", "float"];
@@ -150,7 +189,7 @@ function drawsFrom(rng, ...prefix) {
150
189
  );
151
190
  }
152
191
  assertFiniteNumber(hi - lo, "float span");
153
- return lo + at2(...path) * (hi - lo);
192
+ return scaleUnitDraw(at2(...path), lo, hi);
154
193
  }
155
194
  };
156
195
  }
@@ -375,7 +414,9 @@ var GENRES = Object.freeze([
375
414
  ]);
376
415
  var PROVENANCE_BASES = Object.freeze(["idiom", "construction", "traditional"]);
377
416
  function sameTs(a, b) {
378
- return a.numerator === b.numerator && a.denominator === b.denominator;
417
+ const grouping = a.grouping;
418
+ const theirs = b.grouping;
419
+ return a.numerator === b.numerator && a.denominator === b.denominator && (grouping === void 0 || theirs === void 0 ? grouping === theirs : Array.isArray(grouping) && grouping.length === theirs.length && grouping.every((entry, index) => entry === theirs[index]));
379
420
  }
380
421
  function fitsQuery(entry, query) {
381
422
  const candidate = assertRecord(entry, "vocabulary entry");
@@ -536,6 +577,14 @@ function sustainsShift(semitones, stepBeats, bpm, difficulty) {
536
577
 
537
578
  // src/generate/context/index.ts
538
579
  var DEFAULT_SEED = 0;
580
+ function nullish(value) {
581
+ return value === void 0 || value === null;
582
+ }
583
+ function assertPositionalSource(rng) {
584
+ const source = assertRecord(rng, "ctx.rng");
585
+ assertFunction(source.at, "ctx.rng.at");
586
+ return source;
587
+ }
539
588
  var VERSION_SEGMENT = "v";
540
589
  function resolveContext(input) {
541
590
  if (input !== null && typeof input !== "number" && input !== void 0) {
@@ -546,18 +595,16 @@ function resolveContext(input) {
546
595
  }
547
596
  const ctx = typeof input === "number" ? { seed: input } : input ?? { seed: DEFAULT_SEED };
548
597
  const seed = assertInteger(ctx.seed ?? DEFAULT_SEED, "seed", 0, 4294967295);
549
- const complexity = ctx.complexity ?? {};
598
+ const complexity = nullish(ctx.complexity) ? {} : assertRecord(ctx.complexity, "ctx.complexity");
550
599
  const rhythmic = complexity.rhythmic === void 0 ? void 0 : assertRange(complexity.rhythmic, 0, 1, "complexity rhythmic");
551
600
  const harmonic = complexity.harmonic === void 0 ? void 0 : assertRange(complexity.harmonic, 0, 1, "complexity harmonic");
552
601
  const ornament2 = complexity.ornament === void 0 ? void 0 : assertRange(complexity.ornament, 0, 1, "complexity ornament");
553
602
  const difficulty = complexity.difficulty === void 0 ? void 0 : assertDifficulty(complexity.difficulty);
554
603
  const bpm = ctx.bpm === void 0 ? void 0 : assertRange(ctx.bpm, Number.MIN_VALUE, 1e3, "bpm");
555
604
  const algorithmVersion = resolveAlgorithmVersion(ctx.algorithmVersion);
556
- const supplied = ctx.rng;
557
- const instruments = ctx.instruments;
558
- const vocabulary = (ctx.vocabulary ?? []).map(
559
- (entry, index) => assertVocabulary(entry, `vocabulary[${index}]`)
560
- );
605
+ const supplied = ctx.rng ? assertPositionalSource(ctx.rng) : void 0;
606
+ const instruments = nullish(ctx.instruments) ? void 0 : assertRecord(ctx.instruments, "ctx.instruments");
607
+ const vocabulary = (nullish(ctx.vocabulary) ? [] : assertArray(ctx.vocabulary, "ctx.vocabulary")).map((entry, index) => assertVocabulary(entry, `vocabulary[${index}]`));
561
608
  return {
562
609
  seed,
563
610
  rhythmic,
@@ -566,7 +613,7 @@ function resolveContext(input) {
566
613
  difficulty,
567
614
  bpm,
568
615
  algorithmVersion,
569
- instrument: (part) => instruments?.[part],
616
+ instrument: (part) => instruments !== void 0 && Object.hasOwn(instruments, part) ? instruments[part] : void 0,
570
617
  vocabulary,
571
618
  part: (name) => supplied ? drawsFrom(supplied, name, VERSION_SEGMENT, algorithmVersion) : drawsFrom(createPositionalRng(deriveSeed(seed, name, VERSION_SEGMENT, algorithmVersion)))
572
619
  };
@@ -718,6 +765,7 @@ function bassRegister(named, given, octave) {
718
765
  var BAR_STEPS = 16;
719
766
  var BEAT_STEPS = 4;
720
767
  var STEP_BEATS = 1 / BEAT_STEPS;
768
+ var FIGURE_RATES = ["straight", "half", "double"];
721
769
  function assertGridEvents(events) {
722
770
  assertArray(events, "figure events");
723
771
  events.forEach((event, index) => {
@@ -732,6 +780,16 @@ function assertSpanSteps(spanSteps) {
732
780
  function streamOf(event) {
733
781
  return event.limb ?? event.voice ?? "";
734
782
  }
783
+ function positionKey(event, step) {
784
+ return `${streamOf(event)}@${step}`;
785
+ }
786
+ function drawKey(event) {
787
+ const stream = `${streamOf(event)}`;
788
+ return stream === "" ? [event.step] : [stream, event.step];
789
+ }
790
+ function halvedStep(step) {
791
+ return Number.isInteger(step) ? Math.round(step / 2) : step / 2;
792
+ }
735
793
  var FIGURE_TS = { numerator: 4, denominator: 4 };
736
794
  function subdivisionBeats(ts) {
737
795
  const pulse2 = pulseBeats(ts);
@@ -754,46 +812,57 @@ function stepWeight(step, ts) {
754
812
  function thin(events, amount, ts) {
755
813
  const figure = assertGridEvents(events);
756
814
  assertRange(amount, 0, 1, "thin amount");
757
- const signature = meterAt(0, toMeterData(ts, "ts"));
815
+ return thinFigure(figure, amount, meterAt(0, toMeterData(ts, "ts")));
816
+ }
817
+ function thinFigure(figure, amount, ts) {
758
818
  const floor = Math.min(4, Math.floor(amount * 5));
759
- return figure.filter((event) => stepWeight(event.step, signature) >= floor);
819
+ return figure.filter((event) => stepWeight(event.step, ts) >= floor);
760
820
  }
761
821
  function double(events, spanSteps = BAR_STEPS) {
762
822
  assertSpanSteps(spanSteps);
763
823
  const sorted = [...assertGridEvents(events)].sort((a, b) => a.step - b.step);
764
- const out = [];
765
- for (let i = 0; i < sorted.length; i += 1) {
824
+ const nextStep = [];
825
+ const following = /* @__PURE__ */ new Map();
826
+ for (let i = sorted.length - 1; i >= 0; i -= 1) {
766
827
  const event = sorted[i];
767
- if (!event) {
768
- continue;
769
- }
828
+ const stream = streamOf(event);
829
+ nextStep[i] = following.get(stream) ?? spanSteps;
830
+ following.set(stream, event.step);
831
+ }
832
+ const out = [];
833
+ sorted.forEach((event, i) => {
770
834
  out.push(event);
771
- const next = sorted[i + 1]?.step ?? spanSteps;
835
+ const next = nextStep[i] ?? spanSteps;
772
836
  const mid = event.step + (next - event.step) / 2;
773
837
  if (Number.isInteger(mid) && mid > event.step && mid < spanSteps) {
774
838
  out.push({ ...event, step: mid, velocity: event.velocity * ADDED_NOTE_VELOCITY });
775
839
  }
776
- }
777
- return out;
840
+ });
841
+ return out.sort((a, b) => a.step - b.step);
778
842
  }
779
843
  var ADDED_NOTE_VELOCITY = 0.75;
780
844
  function halfTime(events, spanSteps = BAR_STEPS) {
781
845
  assertSpanSteps(spanSteps);
782
- return assertGridEvents(events).map((event) => ({ ...event, step: event.step * 2 })).filter((event) => event.step < spanSteps);
846
+ return halfTimeFigure(assertGridEvents(events), spanSteps);
847
+ }
848
+ function halfTimeFigure(figure, spanSteps) {
849
+ return figure.map((event) => ({ ...event, step: event.step * 2 })).filter((event) => event.step < spanSteps);
783
850
  }
784
851
  function doubleTime(events, spanSteps = BAR_STEPS) {
785
852
  assertSpanSteps(spanSteps);
786
- assertGridEvents(events);
853
+ return doubleTimeFigure(assertGridEvents(events), spanSteps);
854
+ }
855
+ function doubleTimeFigure(figure, spanSteps) {
787
856
  const merged = /* @__PURE__ */ new Map();
788
857
  const place = (event, step, velocity) => {
789
- const key = `${streamOf(event)}@${step}`;
858
+ const key = positionKey(event, step);
790
859
  const held = merged.get(key);
791
860
  if (held === void 0 || velocity > held.velocity) {
792
861
  merged.set(key, { ...event, step, velocity });
793
862
  }
794
863
  };
795
- for (const event of events) {
796
- const step = Math.round(event.step / 2);
864
+ for (const event of figure) {
865
+ const step = halvedStep(event.step);
797
866
  place(event, step, event.velocity);
798
867
  const repeated = step + spanSteps / 2;
799
868
  if (repeated < spanSteps) {
@@ -810,14 +879,17 @@ function syncopate(events, amount, draw, ...path) {
810
879
  const dial = asked === void 0 ? amount : asked.amount;
811
880
  const signature = meterAt(0, toMeterData(asked?.ts ?? FIGURE_TS, "ts"));
812
881
  assertRange(dial, 0, 1, "syncopate amount");
813
- const occupied = new Set(figure.map((event) => `${streamOf(event)}@${event.step}`));
882
+ return syncopateFigure(figure, dial, signature, sampler, path);
883
+ }
884
+ function syncopateFigure(figure, dial, ts, sampler, path) {
885
+ const occupied = new Set(figure.map((event) => positionKey(event, event.step)));
814
886
  const out = [...figure];
815
887
  for (const event of figure) {
816
888
  const anticipated = event.step - 1;
817
- if (stepWeight(event.step, signature) < 2 || anticipated < 1 || occupied.has(`${streamOf(event)}@${anticipated}`)) {
889
+ if (stepWeight(event.step, ts) < 2 || anticipated < 1 || occupied.has(positionKey(event, anticipated))) {
818
890
  continue;
819
891
  }
820
- if (!sampler.prob(dial, ...path, "syncopate", event.step)) {
892
+ if (!sampler.prob(dial, ...path, "syncopate", ...drawKey(event))) {
821
893
  continue;
822
894
  }
823
895
  out.push({ ...event, step: anticipated, velocity: event.velocity * ADDED_NOTE_VELOCITY });
@@ -830,35 +902,47 @@ function ornamentBy(events, isOrnament, amount, draw, ...path) {
830
902
  const sampler = assertDraw(draw);
831
903
  assertSeedPath(path);
832
904
  assertRange(amount, 0, 1, "ornament amount");
905
+ return ornamentFigure(figure, marks, amount, sampler, path);
906
+ }
907
+ function ornamentFigure(figure, marks, amount, sampler, path) {
833
908
  return figure.filter(
834
- (event) => !marks(event) || sampler.prob(amount, ...path, "ornament", event.step)
909
+ (event) => !marks(event) || sampler.prob(amount, ...path, "ornament", ...drawKey(event))
835
910
  );
836
911
  }
837
912
  var NEUTRAL_RHYTHMIC = 0.5;
838
913
  function deform(events, opts, draw, ...path) {
839
914
  const figure = assertGridEvents(events);
840
915
  const asked = assertRecord(opts, "opts");
841
- assertDraw(draw);
916
+ const sampler = assertDraw(draw);
842
917
  assertSeedPath(path);
843
918
  const spanSteps = assertSpanSteps(asked.spanSteps === void 0 ? BAR_STEPS : asked.spanSteps);
844
- const ts = asked.ts ?? FIGURE_TS;
845
- let out = asked.rate === "half" ? halfTime(figure, spanSteps) : asked.rate === "double" ? doubleTime(figure, spanSteps) : [...figure];
846
- if (asked.rhythmic !== void 0) {
847
- const rhythmic = assertRange(asked.rhythmic, 0, 1, "complexity rhythmic");
848
- if (rhythmic < NEUTRAL_RHYTHMIC) {
849
- out = thin(out, (NEUTRAL_RHYTHMIC - rhythmic) / NEUTRAL_RHYTHMIC, ts);
850
- } else if (rhythmic > NEUTRAL_RHYTHMIC) {
851
- out = syncopate(
852
- out,
853
- { amount: (rhythmic - NEUTRAL_RHYTHMIC) / NEUTRAL_RHYTHMIC, ts },
854
- draw,
855
- ...path
856
- );
857
- }
919
+ return deformFigure(figure, spanSteps, resolveDials(asked), sampler, path);
920
+ }
921
+ function resolveDials(asked) {
922
+ const rate = assertOneOf(asked.rate ?? "straight", FIGURE_RATES, "rate");
923
+ const rhythmic = asked.rhythmic === void 0 ? void 0 : assertRange(asked.rhythmic, 0, 1, "complexity rhythmic");
924
+ const ts = meterAt(0, toMeterData(asked.ts ?? FIGURE_TS, "ts"));
925
+ const ornament2 = asked.ornament === void 0 ? void 0 : assertRange(asked.ornament, 0, 1, "ornament amount");
926
+ const isOrnament = asked.isOrnament ? assertFunction(asked.isOrnament, "opts.isOrnament") : void 0;
927
+ return { rate, rhythmic, ts, ornament: ornament2, isOrnament };
928
+ }
929
+ function deformFigure(figure, spanSteps, dials, sampler, path) {
930
+ let out = dials.rate === "half" ? halfTimeFigure(figure, spanSteps) : dials.rate === "double" ? doubleTimeFigure(figure, spanSteps) : [...figure];
931
+ const { rhythmic } = dials;
932
+ if (rhythmic !== void 0 && rhythmic < NEUTRAL_RHYTHMIC) {
933
+ out = thinFigure(out, (NEUTRAL_RHYTHMIC - rhythmic) / NEUTRAL_RHYTHMIC, dials.ts);
934
+ } else if (rhythmic !== void 0 && rhythmic > NEUTRAL_RHYTHMIC) {
935
+ out = syncopateFigure(
936
+ out,
937
+ (rhythmic - NEUTRAL_RHYTHMIC) / NEUTRAL_RHYTHMIC,
938
+ dials.ts,
939
+ sampler,
940
+ path
941
+ );
858
942
  }
859
- if (asked.ornament !== void 0 && asked.isOrnament) {
860
- const isOrnament = assertFunction(asked.isOrnament, "opts.isOrnament");
861
- out = ornamentBy(out, (event) => isOrnament(event), asked.ornament, draw, ...path);
943
+ const { ornament: ornament2, isOrnament } = dials;
944
+ if (ornament2 !== void 0 && isOrnament !== void 0) {
945
+ out = ornamentFigure(out, (event) => isOrnament(event), ornament2, sampler, path);
862
946
  }
863
947
  return out.sort((a, b) => a.step - b.step);
864
948
  }
@@ -975,6 +1059,32 @@ function isLickMaterial(material) {
975
1059
  const candidate = material;
976
1060
  return typeof candidate === "object" && candidate !== null && typeof candidate.lengthSteps === "number" && Array.isArray(candidate.notes);
977
1061
  }
1062
+ function assertLickMaterial(material, label) {
1063
+ material.notes.forEach((given, index) => {
1064
+ const name = `${label} notes[${index}]`;
1065
+ const lickNote = assertRecord(given, name);
1066
+ assertDegree(lickNote.degree, `${name}.degree`);
1067
+ assertFiniteNumber(lickNote.step, `${name}.step`);
1068
+ assertFiniteNumber(lickNote.velocity, `${name}.velocity`);
1069
+ if (lickNote.alter !== void 0) {
1070
+ assertInteger(lickNote.alter, `${name}.alter`);
1071
+ }
1072
+ if (lickNote.lengthSteps !== void 0) {
1073
+ assertFiniteNumber(lickNote.lengthSteps, `${name}.lengthSteps`);
1074
+ }
1075
+ if (lickNote.articulation !== void 0) {
1076
+ assertOneOf(lickNote.articulation, ARTICULATIONS, `${name}.articulation`);
1077
+ }
1078
+ for (const field of ["voice", "limb"]) {
1079
+ const value = lickNote[field];
1080
+ if (value !== void 0 && typeof value !== "string") {
1081
+ throw new InvalidInputError(
1082
+ `${name}.${field} must be a string; received ${describeRejected(value)}`
1083
+ );
1084
+ }
1085
+ }
1086
+ });
1087
+ }
978
1088
  function note(degree, step, velocity, extra) {
979
1089
  return { degree, step, velocity, ...extra };
980
1090
  }
@@ -1202,10 +1312,11 @@ function placeLicks(timeline, key, opts) {
1202
1312
  const supplied = asked.difficulty === void 0 ? void 0 : assertDifficulty(asked.difficulty, "lick difficulty");
1203
1313
  const difficulty = resolved.difficulty ?? supplied;
1204
1314
  const { instrument, low } = bassRegister(resolved.instrument("bass"), asked.instrument, octave);
1205
- const dictionary = mergeVocabulary(
1206
- BASS_LICKS,
1207
- vocabularyOfKind(resolved.vocabulary, isLickMaterial)
1208
- );
1315
+ const callerLicks = vocabularyOfKind(resolved.vocabulary, isLickMaterial);
1316
+ callerLicks.forEach((entry, index) => {
1317
+ assertLickMaterial(entry.material, `lick vocabulary[${index}]`);
1318
+ });
1319
+ const dictionary = mergeVocabulary(BASS_LICKS, callerLicks);
1209
1320
  const playable = instrument === void 0 ? {} : { articulations: instrument.articulations };
1210
1321
  const raw = [];
1211
1322
  for (let index = 0; index < segments.length; index += 1) {
@@ -1637,9 +1748,10 @@ function imitate(lead, opts) {
1637
1748
  let offset = 0;
1638
1749
  if (invert) {
1639
1750
  if (tonal) {
1640
- const here = scaleLadderPosition(pitch, key);
1641
- pitch = scaleLadderPitch(2 * scaleLadderPosition(pivot, key).rung - here.rung, key);
1642
- offset = -here.offset;
1751
+ const axis = scaleLadderPosition(pivot, key);
1752
+ const here = scaleLadderPosition(pitch - axis.offset, key);
1753
+ pitch = scaleLadderPitch(2 * axis.rung - here.rung, key);
1754
+ offset = axis.offset - here.offset;
1643
1755
  } else {
1644
1756
  pitch = 2 * pivot - pitch;
1645
1757
  }
@@ -1845,7 +1957,7 @@ function generateCounterMelody(opts) {
1845
1957
  throw new InvalidInputError("countermelody needs a timeline or a chordAt callback");
1846
1958
  }
1847
1959
  const chordChangeBeats = timeline === void 0 ? opts.chordChangeBeats ?? [] : timeline.segments.flatMap((segment) => [segment.startBeat, segment.endBeat]);
1848
- const register = opts.register ?? "below";
1960
+ const register = assertOneOf(opts.register ?? "below", ["above", "below"], "register");
1849
1961
  assertNoteEvents(opts.melody, "countermelody melody", { allowNonPositiveDuration: true });
1850
1962
  const melody = createNoteEventIndex(soundingNotesOnly(opts.melody), {
1851
1963
  tieBreak: register === "below" ? "highest" : "lowest"
@@ -2078,13 +2190,14 @@ function denser(level) {
2078
2190
  }
2079
2191
  return "sixteenth";
2080
2192
  }
2193
+ var LEVEL_ORDER = ["quarter", "eighth", "sixteenth"];
2081
2194
  function hiHatStep(level) {
2082
2195
  if (level === "sixteenth") {
2083
2196
  return 0.25;
2084
2197
  }
2085
2198
  return level === "eighth" ? 0.5 : 1;
2086
2199
  }
2087
- function getHiHatLevel(section, style, rhythmic, bpm, draw, difficulty, bar2) {
2200
+ function getHiHatBar(section, style, rhythmic, bpm, draw, difficulty, bar2) {
2088
2201
  const allow16th = bpm < HH_16TH_BPM_THRESHOLD;
2089
2202
  let base = "eighth";
2090
2203
  let dialed = true;
@@ -2124,22 +2237,41 @@ function getHiHatLevel(section, style, rhythmic, bpm, draw, difficulty, bar2) {
2124
2237
  break;
2125
2238
  }
2126
2239
  }
2240
+ const playable = (proposed) => {
2241
+ let level2 = !allow16th && proposed === "sixteenth" ? "eighth" : proposed;
2242
+ while (level2 !== "quarter" && !sustainsStrokes(hiHatStep(level2), bpm, difficulty)) {
2243
+ level2 = sparser(level2);
2244
+ }
2245
+ return level2;
2246
+ };
2127
2247
  let level = base;
2248
+ let thinnedUpTo = Number.NEGATIVE_INFINITY;
2249
+ let filledAbove = Number.POSITIVE_INFINITY;
2128
2250
  if (dialed) {
2129
- if (draw.at("hhThin", bar2) >= 2 * rhythmic) {
2251
+ const thin2 = draw.at("hhThin", bar2);
2252
+ if (thin2 >= 2 * rhythmic) {
2130
2253
  level = sparser(level);
2131
2254
  }
2132
- if (draw.at("hhFill", bar2) < 2 * rhythmic - 1) {
2255
+ const fill2 = draw.at("hhFill", bar2);
2256
+ if (fill2 < 2 * rhythmic - 1) {
2133
2257
  level = denser(level);
2134
2258
  }
2259
+ thinnedUpTo = thin2 / 2;
2260
+ filledAbove = (fill2 + 1) / 2;
2135
2261
  }
2136
- if (!allow16th && level === "sixteenth") {
2137
- level = "eighth";
2138
- }
2139
- while (level !== "quarter" && !sustainsStrokes(hiHatStep(level), bpm, difficulty)) {
2140
- level = sparser(level);
2141
- }
2142
- return level;
2262
+ const rank = (candidate) => LEVEL_ORDER.indexOf(candidate);
2263
+ const sparsest = rank(playable(dialed ? sparser(base) : base));
2264
+ const middle = rank(playable(base));
2265
+ const densest = rank(playable(dialed ? denser(base) : base));
2266
+ const entryOf = (grid) => sparsest >= rank(grid) ? Number.NEGATIVE_INFINITY : middle >= rank(grid) ? thinnedUpTo : densest >= rank(grid) ? filledAbove : Number.POSITIVE_INFINITY;
2267
+ return {
2268
+ level: playable(level),
2269
+ entries: {
2270
+ quarter: entryOf("quarter"),
2271
+ eighth: entryOf("eighth"),
2272
+ sixteenth: entryOf("sixteenth")
2273
+ }
2274
+ };
2143
2275
  }
2144
2276
  function hiHatVelocityMultiplier(sixteenth, jitter) {
2145
2277
  switch (sixteenth) {
@@ -2350,11 +2482,12 @@ function generateKickForBeat(ctx, sec, kick) {
2350
2482
  const sixteenths = kick.length / 4;
2351
2483
  for (let step = 0; step < sixteenths; step += 1) {
2352
2484
  const index = ctx.beat * sixteenths + step;
2353
- if (!kick[index]) {
2485
+ const entry = kick[index];
2486
+ if (entry === void 0) {
2354
2487
  continue;
2355
2488
  }
2356
2489
  if (step === 0) {
2357
- ctx.track.add(GM.BD, ctx.beatTick, EIGHTH, ctx.velocity);
2490
+ ctx.track.add(GM.BD, ctx.beatTick, EIGHTH, ctx.velocity, void 0, entry);
2358
2491
  continue;
2359
2492
  }
2360
2493
  const offBeat = step % 2 === 1;
@@ -2362,7 +2495,7 @@ function generateKickForBeat(ctx, sec, kick) {
2362
2495
  continue;
2363
2496
  }
2364
2497
  const tick = swing16(ctx.beatTick + step * SIXTEENTH, sec, ctx.swingAmount);
2365
- ctx.track.add(GM.BD, tick, offBeat ? SIXTEENTH : EIGHTH, ctx.velocity * 0.85);
2498
+ ctx.track.add(GM.BD, tick, offBeat ? SIXTEENTH : EIGHTH, ctx.velocity * 0.85, void 0, entry);
2366
2499
  }
2367
2500
  }
2368
2501
  function roleBackbeatWeight(role) {
@@ -2415,16 +2548,19 @@ function generateGhostNotesForBeat(ctx, sec) {
2415
2548
  return;
2416
2549
  }
2417
2550
  const positions = selectGhostPositions(sec.ghostMood);
2418
- let ghostProb = leanedBy(getGhostDensity(sec.ghostMood, ctx.section, ctx.bpm), sec.ornament);
2551
+ const ghostDensity = getGhostDensity(sec.ghostMood, ctx.section, ctx.bpm);
2552
+ let ghostProb = leanedBy(ghostDensity, sec.ornament);
2419
2553
  if (sec.ghostBoost) {
2420
2554
  ghostProb = Math.min(1, ghostProb * 1.4);
2421
2555
  }
2422
2556
  for (const pos of positions) {
2423
2557
  const sixteenthInBeat = pos === "e" ? 1 : 3;
2424
2558
  const posProb = getGhostProbabilityAtPosition(ctx.beat, sixteenthInBeat, leadIns);
2425
- if (!ctx.draw.prob(ghostProb * posProb, "ghost", ctx.bar, ctx.beat, pos)) {
2559
+ const at2 = ctx.draw.at("ghost", ctx.bar, ctx.beat, pos);
2560
+ if (!(at2 < ghostProb * posProb)) {
2426
2561
  continue;
2427
2562
  }
2563
+ const entry = sec.ornamentFollowsRhythmic ? at2 / (posProb * (sec.ghostBoost ? 1.4 : 1) * ghostDensity) - 0.5 : Number.NEGATIVE_INFINITY;
2428
2564
  const variation = ctx.draw.float(0.85, 1.15, "ghostVelocity", ctx.bar, ctx.beat, pos);
2429
2565
  const ghostBase = getGhostVelocity(ctx.section, ctx.beat / 2);
2430
2566
  let ghostVel = ctx.velocity * ghostBase * variation;
@@ -2433,7 +2569,7 @@ function generateGhostNotesForBeat(ctx, sec) {
2433
2569
  }
2434
2570
  const offset = pos === "e" ? SIXTEENTH : 3 * SIXTEENTH;
2435
2571
  const tick = swing16(ctx.beatTick + offset, sec, ctx.swingAmount);
2436
- ctx.track.add(GM.SD, tick, SIXTEENTH, ghostVel);
2572
+ ctx.track.add(GM.SD, tick, SIXTEENTH, ghostVel, void 0, entry);
2437
2573
  }
2438
2574
  }
2439
2575
  function generatePreChorusBuildup(ctx, sec, isSectionLastBar) {
@@ -2491,17 +2627,25 @@ function generateHiHatForBeat(ctx, sec) {
2491
2627
  const allowsOpenHiHat = sec.role !== "ambient" && sec.role !== "minimal";
2492
2628
  const isDynamicOpen = ctx.barHasOpenHh && ctx.beat === ctx.openHhBeat && !usesOffbeatOpenHiHat(sec.style);
2493
2629
  const dm = sec.densityMult;
2630
+ const beatEntry = ctx.section === "intro" && ctx.beat !== 0 ? ctx.hhEntries.eighth : ctx.hhEntries.quarter;
2494
2631
  if (ctx.hhLevel === "quarter") {
2495
2632
  if (ctx.section === "intro" && ctx.beat !== 0) {
2496
2633
  return;
2497
2634
  }
2498
2635
  const hhVel = Math.max(20, ctx.velocity * dm * 0.75 * typeMult);
2499
2636
  if (allowsOpenHiHat && isDynamicOpen) {
2500
- ctx.track.add(GM.OHH, ctx.beatTick, EIGHTH, hhVel + OHH_VEL_BOOST);
2637
+ ctx.track.add(GM.OHH, ctx.beatTick, EIGHTH, hhVel + OHH_VEL_BOOST, void 0, beatEntry);
2501
2638
  } else if (allowsOpenHiHat && opensOnDownbeat(ctx, sec)) {
2502
- ctx.track.add(hiHatNote("open"), ctx.beatTick, EIGHTH, Math.max(20, hhVel * 1.1));
2639
+ ctx.track.add(
2640
+ hiHatNote("open"),
2641
+ ctx.beatTick,
2642
+ EIGHTH,
2643
+ Math.max(20, hhVel * 1.1),
2644
+ void 0,
2645
+ beatEntry
2646
+ );
2503
2647
  } else {
2504
- ctx.track.add(hhInstrument, ctx.beatTick, EIGHTH, hhVel);
2648
+ ctx.track.add(hhInstrument, ctx.beatTick, EIGHTH, hhVel, void 0, beatEntry);
2505
2649
  }
2506
2650
  return;
2507
2651
  }
@@ -2511,22 +2655,37 @@ function generateHiHatForBeat(ctx, sec) {
2511
2655
  if (eighth === 1) {
2512
2656
  hhTick = swing16(hhTick, sec, ctx.swingAmount);
2513
2657
  }
2658
+ const entry = eighth === 0 ? beatEntry : ctx.hhEntries.eighth;
2514
2659
  if (ctx.section === "intro" && eighth === 1) {
2515
2660
  if (sec.useFootHh && ctx.beat % 2 === 0) {
2516
- ctx.track.add(GM.FHH, hhTick, EIGHTH, footHiHatVelocity(ctx.draw, ctx.bar, ctx.beat));
2661
+ ctx.track.add(
2662
+ GM.FHH,
2663
+ hhTick,
2664
+ EIGHTH,
2665
+ footHiHatVelocity(ctx.draw, ctx.bar, ctx.beat),
2666
+ void 0,
2667
+ entry
2668
+ );
2517
2669
  }
2518
2670
  continue;
2519
2671
  }
2520
2672
  const hhVel = Math.max(20, ctx.velocity * dm * typeMult * (eighth === 0 ? 0.9 : 0.65));
2521
2673
  if (isDynamicOpen && allowsOpenHiHat && eighth === 0) {
2522
- ctx.track.add(GM.OHH, hhTick, EIGHTH, hhVel + OHH_VEL_BOOST);
2674
+ ctx.track.add(GM.OHH, hhTick, EIGHTH, hhVel + OHH_VEL_BOOST, void 0, entry);
2523
2675
  continue;
2524
2676
  }
2525
2677
  const useOpen = eighth === 0 ? opensOnDownbeat(ctx, sec) : opensOffbeat(ctx, sec);
2526
2678
  if (useOpen && allowsOpenHiHat) {
2527
- ctx.track.add(hiHatNote("open"), hhTick, EIGHTH, Math.max(20, hhVel * 1.1));
2679
+ ctx.track.add(
2680
+ hiHatNote("open"),
2681
+ hhTick,
2682
+ EIGHTH,
2683
+ Math.max(20, hhVel * 1.1),
2684
+ void 0,
2685
+ entry
2686
+ );
2528
2687
  } else {
2529
- ctx.track.add(hhInstrument, hhTick, EIGHTH / 2, hhVel);
2688
+ ctx.track.add(hhInstrument, hhTick, EIGHTH / 2, hhVel, void 0, entry);
2530
2689
  }
2531
2690
  }
2532
2691
  return;
@@ -2541,26 +2700,41 @@ function generateHiHatForBeat(ctx, sec) {
2541
2700
  ctx.draw.float(0.95, 1.05, "hatVelocity", ctx.bar, ctx.beat, sixteenth)
2542
2701
  );
2543
2702
  const hhVel = Math.max(20, ctx.velocity * dm * typeMult * metricVel);
2703
+ const entry = sixteenth === 0 ? beatEntry : sixteenth === 2 ? ctx.hhEntries.eighth : ctx.hhEntries.sixteenth;
2544
2704
  if (isDynamicOpen && allowsOpenHiHat && sixteenth === 0) {
2545
- ctx.track.add(GM.OHH, hhTick, SIXTEENTH, hhVel + OHH_VEL_BOOST);
2705
+ ctx.track.add(GM.OHH, hhTick, SIXTEENTH, hhVel + OHH_VEL_BOOST, void 0, entry);
2546
2706
  continue;
2547
2707
  }
2548
2708
  if (allowsOpenHiHat && sixteenth === 0 && opensOnDownbeat(ctx, sec)) {
2549
- ctx.track.add(hiHatNote("open"), hhTick, SIXTEENTH, Math.max(20, hhVel * 1.1));
2709
+ ctx.track.add(
2710
+ hiHatNote("open"),
2711
+ hhTick,
2712
+ SIXTEENTH,
2713
+ Math.max(20, hhVel * 1.1),
2714
+ void 0,
2715
+ entry
2716
+ );
2550
2717
  continue;
2551
2718
  }
2552
2719
  if (allowsOpenHiHat && sixteenth === 2 && opensOffbeat(ctx, sec)) {
2553
- ctx.track.add(hiHatNote("open"), hhTick, SIXTEENTH, Math.max(20, hhVel * 1.1));
2720
+ ctx.track.add(
2721
+ hiHatNote("open"),
2722
+ hhTick,
2723
+ SIXTEENTH,
2724
+ Math.max(20, hhVel * 1.1),
2725
+ void 0,
2726
+ entry
2727
+ );
2554
2728
  continue;
2555
2729
  }
2556
2730
  if (allowsOpenHiHat && ctx.beat === 3 && sixteenth === 3) {
2557
2731
  const openProb = Math.max(0.1, Math.min(0.4, 30 / ctx.bpm));
2558
2732
  if (ctx.draw.prob(openProb, "openHat", ctx.bar, ctx.beat, sixteenth)) {
2559
- ctx.track.add(GM.OHH, hhTick, SIXTEENTH, Math.max(20, hhVel * 1.2));
2733
+ ctx.track.add(GM.OHH, hhTick, SIXTEENTH, Math.max(20, hhVel * 1.2), void 0, entry);
2560
2734
  continue;
2561
2735
  }
2562
2736
  }
2563
- ctx.track.add(hhInstrument, hhTick, SIXTEENTH / 2, hhVel);
2737
+ ctx.track.add(hhInstrument, hhTick, SIXTEENTH / 2, hhVel, void 0, entry);
2564
2738
  }
2565
2739
  }
2566
2740
 
@@ -2587,7 +2761,7 @@ function bjorklund(pulses, steps) {
2587
2761
  }
2588
2762
  let head = Array.from({ length: pulses }, () => [true]);
2589
2763
  let remainder = Array.from({ length: steps - pulses }, () => [false]);
2590
- while (remainder.length > 1) {
2764
+ do {
2591
2765
  const count = Math.min(head.length, remainder.length);
2592
2766
  const next = [];
2593
2767
  for (let i = 0; i < count; i += 1) {
@@ -2595,7 +2769,7 @@ function bjorklund(pulses, steps) {
2595
2769
  }
2596
2770
  remainder = head.length > count ? head.slice(count) : remainder.slice(count);
2597
2771
  head = next;
2598
- }
2772
+ } while (remainder.length > 1);
2599
2773
  return [...head.flat(), ...remainder.flat()];
2600
2774
  }
2601
2775
 
@@ -2615,6 +2789,7 @@ var FILL_TYPES = Object.freeze([
2615
2789
  "flamsAndDrags",
2616
2790
  "halfTimeFill"
2617
2791
  ]);
2792
+ var FILL_VELOCITY_BASES = Object.freeze(["fill", "accent"]);
2618
2793
  var S = SIXTEENTH;
2619
2794
  var E = EIGHTH;
2620
2795
  function fill(scale, offset) {
@@ -2738,6 +2913,29 @@ var FILL_ARCHETYPES = deepFreeze({
2738
2913
  function isFillArchetype(material) {
2739
2914
  return typeof material === "object" && material !== null && Array.isArray(material.atBeat);
2740
2915
  }
2916
+ function assertFillArchetype(material, label) {
2917
+ material.atBeat.forEach((beat, beatIndex) => {
2918
+ const strokes = assertArray(beat, `${label} atBeat[${beatIndex}]`);
2919
+ strokes.forEach((given, strokeIndex) => {
2920
+ const name = `${label} atBeat[${beatIndex}][${strokeIndex}]`;
2921
+ const stroke = assertRecord(given, name);
2922
+ assertMidiPitch(stroke.voice, `${name}.voice`);
2923
+ assertFiniteNumber(stroke.offset, `${name}.offset`);
2924
+ assertFiniteNumber(stroke.duration, `${name}.duration`);
2925
+ const velocity = assertRecord(stroke.velocity, `${name}.velocity`);
2926
+ assertOneOf(velocity.base, FILL_VELOCITY_BASES, `${name}.velocity.base`);
2927
+ if (velocity.scale !== void 0) {
2928
+ assertFiniteNumber(velocity.scale, `${name}.velocity.scale`);
2929
+ }
2930
+ if (velocity.offset !== void 0) {
2931
+ assertRange(velocity.offset, -127, 127, `${name}.velocity.offset`);
2932
+ }
2933
+ if (stroke.articulation !== void 0) {
2934
+ assertOneOf(stroke.articulation, ARTICULATIONS, `${name}.articulation`);
2935
+ }
2936
+ });
2937
+ });
2938
+ }
2741
2939
  function fillArchetypeFor(id, supplied) {
2742
2940
  return supplied?.get(id) ?? FILL_ARCHETYPES[id];
2743
2941
  }
@@ -2912,6 +3110,8 @@ var HitList = class {
2912
3110
  hits = [];
2913
3111
  /** Voices already struck at each quantized beat. */
2914
3112
  #onsets = /* @__PURE__ */ new Map();
3113
+ /** The rhythmic-dial setting each onset enters at, by onset key; absent is always. */
3114
+ #entries = /* @__PURE__ */ new Map();
2915
3115
  /**
2916
3116
  * Append a hit, clamping velocity and dropping non-positive positions.
2917
3117
  *
@@ -2919,12 +3119,23 @@ var HitList = class {
2919
3119
  * sounds once however many voices ask for it, and a second entry at the same
2920
3120
  * pitch and position is a note-on a reader answers with no note-off, heard as
2921
3121
  * a hung or cut stroke rather than as a louder one.
3122
+ *
3123
+ * `entry` is the rhythmic-dial setting above which the stroke sounds, for a
3124
+ * stroke the dial adds; a stroke asked for twice sounds from the lower one.
2922
3125
  */
2923
- add(pitch, startBeat, durationBeat, velocity, articulation) {
3126
+ add(pitch, startBeat, durationBeat, velocity, articulation, entry = Number.NEGATIVE_INFINITY) {
2924
3127
  const beat = Math.max(0, startBeat);
3128
+ const key = onsetKey(pitch, beat);
3129
+ const held = this.#entries.get(key);
2925
3130
  if (this.hasOnset(pitch, beat)) {
3131
+ if (held !== void 0 && entry < held) {
3132
+ this.#entries.set(key, entry);
3133
+ }
2926
3134
  return;
2927
3135
  }
3136
+ if (entry !== Number.NEGATIVE_INFINITY) {
3137
+ this.#entries.set(key, entry);
3138
+ }
2928
3139
  const hit2 = {
2929
3140
  pitch,
2930
3141
  startBeat: beat,
@@ -2942,6 +3153,13 @@ var HitList = class {
2942
3153
  this.#onsets.set(beatSlot(beat), /* @__PURE__ */ new Set([pitch]));
2943
3154
  }
2944
3155
  }
3156
+ /**
3157
+ * The rhythmic-dial setting above which a written stroke sounds: minus
3158
+ * infinity for one that sounds at every setting.
3159
+ */
3160
+ entryOf(pitch, startBeat) {
3161
+ return this.#entries.get(onsetKey(pitch, startBeat)) ?? Number.NEGATIVE_INFINITY;
3162
+ }
2945
3163
  /** Whether a hit of this pitch already starts exactly at `startBeat`. */
2946
3164
  hasOnset(pitch, startBeat) {
2947
3165
  return this.#onsets.get(beatSlot(startBeat))?.has(pitch) ?? false;
@@ -3038,17 +3256,14 @@ var KICK_FIGURES = deepFreeze({
3038
3256
  ],
3039
3257
  standard: DEFAULT_FIGURE
3040
3258
  });
3041
- function emptyPattern() {
3042
- return Array.from({ length: KICK_STEPS }, () => false);
3043
- }
3044
3259
  function isInPreChorusLift(section, bar2, sectionBars, nextIsChorus) {
3045
3260
  if (section !== "b" || !nextIsChorus || sectionBars < 3) {
3046
3261
  return false;
3047
3262
  }
3048
3263
  return bar2 >= sectionBars - 2;
3049
3264
  }
3050
- function realiseKickFigure(figure, section, bar2, draw, rhythmic) {
3051
- const pattern = emptyPattern();
3265
+ function realiseKickEntries(figure, section, bar2, draw, rhythmic) {
3266
+ const entries = Array.from({ length: KICK_STEPS }, () => void 0);
3052
3267
  for (const slot of figure) {
3053
3268
  if (slot.step < 0 || slot.step >= KICK_STEPS) {
3054
3269
  continue;
@@ -3060,19 +3275,23 @@ function realiseKickFigure(figure, section, bar2, draw, rhythmic) {
3060
3275
  continue;
3061
3276
  }
3062
3277
  if (slot.probability === void 0) {
3063
- pattern[slot.step] = true;
3278
+ entries[slot.step] = Number.NEGATIVE_INFINITY;
3064
3279
  continue;
3065
3280
  }
3066
- if (draw.prob(leanedBy(slot.probability, rhythmic), "kick", bar2, slot.slot ?? slot.step)) {
3067
- pattern[slot.step] = true;
3281
+ const at2 = draw.at("kick", bar2, slot.slot ?? slot.step);
3282
+ if (at2 < leanedBy(slot.probability, rhythmic)) {
3283
+ entries[slot.step] = Math.min(
3284
+ entries[slot.step] ?? Number.POSITIVE_INFINITY,
3285
+ at2 / slot.probability - 0.5
3286
+ );
3068
3287
  }
3069
3288
  }
3070
- return pattern;
3289
+ return entries;
3071
3290
  }
3072
- function getKickPattern(section, style, bar2, draw, rhythmic) {
3291
+ function getKickEntries(section, style, bar2, draw, rhythmic) {
3073
3292
  const own = KICK_FIGURES[style] ?? DEFAULT_FIGURE;
3074
3293
  const figure = section === "outro" ? [...OUTRO_FIGURE, ...statedSlots(own)] : own;
3075
- return realiseKickFigure(figure, section, bar2, draw, rhythmic);
3294
+ return realiseKickEntries(figure, section, bar2, draw, rhythmic);
3076
3295
  }
3077
3296
 
3078
3297
  // src/generate/drums/percussion.ts
@@ -3109,6 +3328,8 @@ var PERC_TABLE = [
3109
3328
  /* rock */
3110
3329
  [F, F, F, [false, false, true], F, F, F, F, [false, false, true]]
3111
3330
  ];
3331
+ var SHAKER_SIXTEENTH_CURVE = [0.75, 0.45, 0.6, 0.45];
3332
+ var SHAKER_EIGHTH_CURVE = [0.75, 0.55];
3112
3333
  function getPercussionConfig(mood, section) {
3113
3334
  const row = PERC_TABLE[mood] ?? PERC_TABLE[1 /* Standard */];
3114
3335
  const act = row?.[sectionIndex(section)] ?? F;
@@ -3133,10 +3354,11 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
3133
3354
  if (config.shaker) {
3134
3355
  const use16th = config.shaker16th && bpm < HH_16TH_BPM_THRESHOLD && sustainsStrokes(SIXTEENTH, bpm, sec.difficulty);
3135
3356
  if (use16th) {
3136
- const velCurve = [0.75, 0.45, 0.6, 0.45];
3357
+ const velCurve = SHAKER_SIXTEENTH_CURVE;
3137
3358
  for (let beat = 0; beat < barBeats; beat += 1) {
3138
3359
  for (let sub = 0; sub < 4; sub += 1) {
3139
- if (sub % 2 === 1 && !draw.prob(sec.rhythmic, "shaker16", barStart, beat, sub)) {
3360
+ const entry = sub % 2 === 1 ? draw.at("shaker16", barStart, beat, sub) : Number.NEGATIVE_INFINITY;
3361
+ if (!(entry < sec.rhythmic)) {
3140
3362
  continue;
3141
3363
  }
3142
3364
  const raw = 80 * (velCurve[sub] ?? 0.5) * densityMult * draw.float(0.9, 1.1, "shaker", barStart, beat, sub);
@@ -3144,12 +3366,14 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
3144
3366
  GM.SHAKER,
3145
3367
  swing16(barStart + beat + sub * SIXTEENTH, sec, swingAmount),
3146
3368
  SIXTEENTH,
3147
- Math.max(25, Math.min(85, raw))
3369
+ Math.max(25, Math.min(85, raw)),
3370
+ void 0,
3371
+ entry
3148
3372
  );
3149
3373
  }
3150
3374
  }
3151
3375
  } else {
3152
- const velCurve = [0.75, 0.55];
3376
+ const velCurve = SHAKER_EIGHTH_CURVE;
3153
3377
  for (let beat = 0; beat < barBeats; beat += 1) {
3154
3378
  for (let sub = 0; sub < 2; sub += 1) {
3155
3379
  const raw = 80 * (velCurve[sub] ?? 0.6) * densityMult * draw.float(0.9, 1.1, "shaker", barStart, beat, sub);
@@ -3435,6 +3659,7 @@ function placeDrumPattern(opts) {
3435
3659
  assertRange(opts.velocity, 1, 127, "drum pattern velocity");
3436
3660
  }
3437
3661
  const feel = opts.feel === void 0 ? "straight" : assertOneOf(opts.feel, DRUM_FEELS, "feel");
3662
+ const rate = assertOneOf(opts.rate ?? "straight", FIGURE_RATES, "rate");
3438
3663
  const resolved = resolveContext(opts.ctx);
3439
3664
  const bpm = resolved.bpm ?? DEFAULT_BPM2;
3440
3665
  const draw = resolved.part("drums");
@@ -3480,7 +3705,7 @@ function placeDrumPattern(opts) {
3480
3705
  ...resolved.rhythmic === void 0 ? {} : { rhythmic: resolved.rhythmic },
3481
3706
  ...resolved.ornament === void 0 ? {} : { ornament: resolved.ornament },
3482
3707
  isOrnament: (stroke) => stroke.articulation === "ghost",
3483
- rate: opts.rate ?? "straight",
3708
+ rate,
3484
3709
  spanSteps: entry.material.steps,
3485
3710
  // The dials rank the figure's positions against the bar it is written
3486
3711
  // in, which is the bar this call is placing it in.
@@ -3567,6 +3792,7 @@ function generateDrums(opts) {
3567
3792
  densityMult,
3568
3793
  rhythmic,
3569
3794
  ornament: ornamentDial,
3795
+ ornamentFollowsRhythmic: resolved.ornament === void 0,
3570
3796
  difficulty,
3571
3797
  ghostBoost: mapping.ghostBoost,
3572
3798
  useRide: shouldUseRideForSection(section, style),
@@ -3582,10 +3808,15 @@ function generateDrums(opts) {
3582
3808
  const percMood = percMoodCategory(style);
3583
3809
  const nextSection = opts.nextSection ? mapSection(opts.nextSection) : section;
3584
3810
  const nextEnergy = sectionEnergy(nextSection);
3585
- const callerFillEntries = selectVocabulary(
3586
- vocabularyOfKind(resolved.vocabulary, isFillArchetype),
3587
- { section: publicSection, bpm, difficulty }
3588
- );
3811
+ const suppliedFills = vocabularyOfKind(resolved.vocabulary, isFillArchetype);
3812
+ suppliedFills.forEach((entry, index) => {
3813
+ assertFillArchetype(entry.material, `drum fill vocabulary[${index}]`);
3814
+ });
3815
+ const callerFillEntries = selectVocabulary(suppliedFills, {
3816
+ section: publicSection,
3817
+ bpm,
3818
+ difficulty
3819
+ });
3589
3820
  const callerFills = new Map(callerFillEntries.map((entry) => [entry.id, entry.material]));
3590
3821
  const callerFillIds = callerFillEntries.map((entry) => entry.id);
3591
3822
  const euclidSteps = opts.euclideanKick?.steps ?? 16;
@@ -3596,7 +3827,7 @@ function generateDrums(opts) {
3596
3827
  for (let bar2 = 0; bar2 < opts.bars; bar2 += 1) {
3597
3828
  const barStart = bar2 * barBeats;
3598
3829
  const isLastBar = bar2 === opts.bars - 1;
3599
- const hhLevel = getHiHatLevel(section, style, rhythmic, bpm, draw, difficulty, bar2);
3830
+ const hiHat = getHiHatBar(section, style, rhythmic, bpm, draw, difficulty, bar2);
3600
3831
  if (bar2 === 0 && section === "chorus") {
3601
3832
  track.add(GM.CRASH, barStart, 0.5, 100 * densityMult);
3602
3833
  }
@@ -3608,10 +3839,10 @@ function generateDrums(opts) {
3608
3839
  }
3609
3840
  let kick;
3610
3841
  if (!euclidKick && reuseSectionKick) {
3611
- sectionKick ??= getKickPattern(section, style, 0, draw, rhythmic);
3842
+ sectionKick ??= getKickEntries(section, style, 0, draw, rhythmic);
3612
3843
  kick = sectionKick;
3613
3844
  } else if (!euclidKick) {
3614
- kick = getKickPattern(section, style, bar2, draw, rhythmic);
3845
+ kick = getKickEntries(section, style, bar2, draw, rhythmic);
3615
3846
  }
3616
3847
  const inLift = isInPreChorusLift(section, bar2, opts.bars, nextSection === "chorus");
3617
3848
  let currentFill = FILL_ARCHETYPES.snareRoll;
@@ -3667,7 +3898,8 @@ function generateDrums(opts) {
3667
3898
  swingAmount,
3668
3899
  barHasOpenHh,
3669
3900
  openHhBeat: openHhBeatIndex,
3670
- hhLevel,
3901
+ hhLevel: hiHat.level,
3902
+ hhEntries: hiHat.entries,
3671
3903
  draw
3672
3904
  };
3673
3905
  if (playMainVoices) {
@@ -3728,24 +3960,44 @@ function generateDrums(opts) {
3728
3960
  const endBeat = opts.bars * barBeats;
3729
3961
  const kit = resolved.instrument("drums");
3730
3962
  const written = track.hits.filter((hit2) => hit2.startBeat < endBeat && playableOn(kit, hit2.pitch)).sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
3731
- return underCeiling(written, bpm, difficulty, exemptOnsets);
3963
+ return underCeiling(written, bpm, difficulty, exemptOnsets, track);
3732
3964
  }
3733
- function underCeiling(hits, bpm, difficulty, exempt) {
3965
+ function underCeiling(hits, bpm, difficulty, exempt, track) {
3734
3966
  if (difficulty === void 0) {
3735
3967
  return [...hits];
3736
3968
  }
3737
- const lastOnset = /* @__PURE__ */ new Map();
3738
- const kept = [];
3739
- for (const hit2 of hits) {
3740
- const previous = lastOnset.get(hit2.pitch);
3741
- const reachable = previous === void 0 || exempt.has(onsetKey(hit2.pitch, hit2.startBeat)) || sustainsStrokes(hit2.startBeat - previous, bpm, difficulty);
3969
+ const entries = hits.map((hit2) => track.entryOf(hit2.pitch, hit2.startBeat));
3970
+ const order = hits.map((_, index) => index).sort((a, b) => {
3971
+ const left = entries[a];
3972
+ const right = entries[b];
3973
+ return left < right ? -1 : left > right ? 1 : a - b;
3974
+ });
3975
+ const keptBeats = /* @__PURE__ */ new Map();
3976
+ const kept = hits.map(() => false);
3977
+ for (const index of order) {
3978
+ const hit2 = hits[index];
3979
+ const beats = keptBeats.get(hit2.pitch) ?? [];
3980
+ let at2 = 0;
3981
+ let end = beats.length;
3982
+ while (at2 < end) {
3983
+ const mid = at2 + end >>> 1;
3984
+ if (beats[mid] > hit2.startBeat) {
3985
+ end = mid;
3986
+ } else {
3987
+ at2 = mid + 1;
3988
+ }
3989
+ }
3990
+ const previous = beats[at2 - 1];
3991
+ const next = beats[at2];
3992
+ const reachable = exempt.has(onsetKey(hit2.pitch, hit2.startBeat)) || (previous === void 0 || sustainsStrokes(hit2.startBeat - previous, bpm, difficulty)) && (next === void 0 || sustainsStrokes(next - hit2.startBeat, bpm, difficulty));
3742
3993
  if (!reachable) {
3743
3994
  continue;
3744
3995
  }
3745
- kept.push(hit2);
3746
- lastOnset.set(hit2.pitch, hit2.startBeat);
3996
+ kept[index] = true;
3997
+ beats.splice(at2, 0, hit2.startBeat);
3998
+ keptBeats.set(hit2.pitch, beats);
3747
3999
  }
3748
- return kept;
4000
+ return hits.filter((_, index) => kept[index]);
3749
4001
  }
3750
4002
  function playableOn(kit, pitch) {
3751
4003
  return kit === void 0 || canSound(kit, pitch);
@@ -3764,7 +4016,7 @@ var MIN_VELOCITY = 1;
3764
4016
  var MAX_VELOCITY = 127;
3765
4017
  function humanize(events, opts = {}) {
3766
4018
  const asked = assertOptions(opts, "opts");
3767
- const ts = meterAt(0, toMeterData(asked.ts ?? DEFAULT_TS3, "ts"));
4019
+ const meter = toMeterData(asked.ts ?? DEFAULT_TS3, "ts");
3768
4020
  assertNoteEvents(events, "humanize events", { allowNonPositiveDuration: true });
3769
4021
  const sounding2 = dropSilentNotes(events);
3770
4022
  const timing = asked.timing ?? DEFAULT_TIMING;
@@ -3780,7 +4032,7 @@ function humanize(events, opts = {}) {
3780
4032
  return sounding2.map((event) => {
3781
4033
  const timingOffset = draw.float(-timing, timing, "timing", event.startBeat, event.pitch);
3782
4034
  const startBeat = event.startBeat + timingOffset;
3783
- const weight = metricWeight(event.startBeat, ts);
4035
+ const weight = metricWeight(event.startBeat, meter);
3784
4036
  const accentBoost = weight / MAX_METRIC_WEIGHT * accent2;
3785
4037
  const velocityOffset = draw.float(
3786
4038
  -velocityJitter,
@@ -3794,6 +4046,21 @@ function humanize(events, opts = {}) {
3794
4046
  return { ...event, startBeat, velocity };
3795
4047
  });
3796
4048
  }
4049
+ function grooveSignature(ts) {
4050
+ const meter = toMeterData(ts, "ts");
4051
+ if (!Array.isArray(meter)) {
4052
+ return meter;
4053
+ }
4054
+ const opening = meterAt(0, meter);
4055
+ for (const change of meter) {
4056
+ if (change.ts.numerator !== opening.numerator || change.ts.denominator !== opening.denominator) {
4057
+ throw new InvalidInputError(
4058
+ `groove time signature must be one signature throughout; received ${change.ts.numerator}/${change.ts.denominator} at beat ${change.startBeat} in a meter that is ${opening.numerator}/${opening.denominator} at beat 0`
4059
+ );
4060
+ }
4061
+ }
4062
+ return opening;
4063
+ }
3797
4064
  function quantizeToGrid(beat, barBeats, subdivision, slotsPerBar) {
3798
4065
  const barIndex = Math.floor(beat / barBeats);
3799
4066
  const modBeat = beat - barIndex * barBeats;
@@ -3809,7 +4076,7 @@ function quantizeToGrid(beat, barBeats, subdivision, slotsPerBar) {
3809
4076
  };
3810
4077
  }
3811
4078
  function extractGrooveTemplate(events, ts, subdivision = DEFAULT_GROOVE_SUBDIVISION) {
3812
- const meter = meterAt(0, toMeterData(ts, "ts"));
4079
+ const meter = grooveSignature(ts);
3813
4080
  assertNoteEvents(events, "groove source events", { allowNonPositiveDuration: true });
3814
4081
  assertPositiveInt(subdivision, "groove subdivision");
3815
4082
  const barBeats = beatsPerBar(meter);
@@ -3850,7 +4117,7 @@ function extractGrooveTemplate(events, ts, subdivision = DEFAULT_GROOVE_SUBDIVIS
3850
4117
  return { subdivision, slotsPerBar, slots, ts: meter };
3851
4118
  }
3852
4119
  function applyGrooveTemplate(events, template, ts) {
3853
- const meter = meterAt(0, toMeterData(ts, "ts"));
4120
+ const meter = grooveSignature(ts);
3854
4121
  assertNoteEvents(events, "groove target events", { allowNonPositiveDuration: true });
3855
4122
  assertPositiveInt(template.subdivision, "template subdivision");
3856
4123
  assertPositiveInt(template.slotsPerBar, "template slotsPerBar");
@@ -3914,15 +4181,27 @@ function classifyMelodyTones(melody, ts) {
3914
4181
  const meter = meterAt(0, toMeterData(ts, "ts"));
3915
4182
  assertNoteEvents(melody, "melody notes", { allowNonPositiveDuration: true });
3916
4183
  const pulse2 = pulseBeats(meter);
3917
- return melody.map((_, index) => {
3918
- const role = roleOfNote(melody, index, meter, pulse2);
3919
- return { noteIndex: index, role, ornamental: role !== "structural" };
4184
+ const sounding2 = new Set(soundingIndices(melody));
4185
+ const line = canonicalNoteOrder(melody).filter((index) => sounding2.has(index));
4186
+ const roles = melody.map(() => "structural");
4187
+ line.forEach((index, position) => {
4188
+ const before = line[position - 1];
4189
+ const after = line[position + 1];
4190
+ roles[index] = roleOfNote(
4191
+ before === void 0 ? void 0 : melody[before],
4192
+ melody[index],
4193
+ after === void 0 ? void 0 : melody[after],
4194
+ meter,
4195
+ pulse2
4196
+ );
3920
4197
  });
4198
+ return roles.map((role, index) => ({
4199
+ noteIndex: index,
4200
+ role,
4201
+ ornamental: role !== "structural"
4202
+ }));
3921
4203
  }
3922
- function roleOfNote(melody, index, ts, pulse2) {
3923
- const note2 = melody[index];
3924
- const prev = melody[index - 1];
3925
- const next = melody[index + 1];
4204
+ function roleOfNote(prev, note2, next, ts, pulse2) {
3926
4205
  if (!note2 || !prev || !next) {
3927
4206
  return "structural";
3928
4207
  }
@@ -3978,13 +4257,11 @@ var DEFAULT_PLACEMENT = {
3978
4257
  };
3979
4258
  var DEFAULT_METER = { numerator: 4, denominator: 4 };
3980
4259
  var TIE_BREAK_JITTER2 = 1e-6;
3981
- function cadencesThroughRaisedSeventh(key) {
3982
- return isMinorKey(key) && scaleSystemOf(key) === "common-practice";
3983
- }
3984
4260
  function isKeyTone(pitch, key) {
3985
4261
  return isScaleTone(pitch, key) || cadencesThroughRaisedSeventh(key) && pitchClassOf(pitch) === leadingTonePcOf(key);
3986
4262
  }
3987
4263
  function inferKey(melody) {
4264
+ const line = inOnsetOrder(melody);
3988
4265
  let best = majorKey(0);
3989
4266
  let bestScore = Number.NEGATIVE_INFINITY;
3990
4267
  const candidates = [];
@@ -3994,7 +4271,7 @@ function inferKey(melody) {
3994
4271
  for (const key of candidates) {
3995
4272
  const tonic = pitchClassOf(key.rootPc);
3996
4273
  let score = 0;
3997
- for (const n of melody) {
4274
+ for (const n of line) {
3998
4275
  const w = Math.max(0.25, n.durationBeat);
3999
4276
  const pc = pitchClassOf(n.pitch);
4000
4277
  if (isKeyTone(n.pitch, key)) {
@@ -4011,6 +4288,12 @@ function inferKey(melody) {
4011
4288
  }
4012
4289
  return best;
4013
4290
  }
4291
+ function inOnsetOrder(melody) {
4292
+ return canonicalNoteOrder(melody).flatMap((index) => {
4293
+ const note2 = melody[index];
4294
+ return note2 === void 0 ? [] : [note2];
4295
+ });
4296
+ }
4014
4297
 
4015
4298
  // src/generate/harmonize/cost.ts
4016
4299
  var NON_CHORD_TONE_STRONG = 4;
@@ -4307,15 +4590,23 @@ function buildSegments(spans, melody, ts, grid, phraseEnds, budget) {
4307
4590
  }
4308
4591
 
4309
4592
  // src/generate/harmonize/candidates.ts
4310
- var ROOT_WEIGHT_BY_SEMITONE = [0, 0.9, 0.3, 0.45, 0.45, 0.15, 0.9, 0.1, 0.3, 0.3, 0.3, 0.9];
4593
+ var ROOT_WEIGHT_BY_SEMITONE = [
4594
+ 0,
4595
+ 0.9,
4596
+ 0.3,
4597
+ 0.45,
4598
+ 0.45,
4599
+ 0.15,
4600
+ 0.9,
4601
+ 0.1,
4602
+ 0.3,
4603
+ 0.3,
4604
+ 0.3,
4605
+ 0.9
4606
+ ];
4311
4607
  var UNSTABLE_TRIAD = 0.9;
4312
- var UNSTABLE_QUALITIES = /* @__PURE__ */ new Set([
4313
- "dim",
4314
- "aug",
4315
- "dim7",
4316
- "m7b5",
4317
- "aug7"
4318
- ]);
4608
+ var UNSTABLE_QUALITY_NAMES = ["dim", "aug", "dim7", "m7b5", "aug7"];
4609
+ var UNSTABLE_QUALITIES = new Set(UNSTABLE_QUALITY_NAMES);
4319
4610
  function vocabularyBase(rootPc, quality, tonicPc) {
4320
4611
  const above = (pitchClassOf(rootPc) - tonicPc + 12) % 12;
4321
4612
  const root = ROOT_WEIGHT_BY_SEMITONE[above] ?? UNSTABLE_TRIAD;
@@ -4329,7 +4620,7 @@ function secondaryDominantTargets(key) {
4329
4620
  tonicizableDegrees(key).map((degree) => [degree.degreeNumber, degree])
4330
4621
  );
4331
4622
  return SECONDARY_DOMINANT_TARGETS.map((degree) => tonicizable.get(degree)).filter(
4332
- (degree) => degree !== void 0
4623
+ (degree) => degree !== void 0 && !isNativeDominantSeventh(degree.rootPc + 7, key)
4333
4624
  );
4334
4625
  }
4335
4626
  var SECONDARY_DOMINANT_ENTRY_ORDER = [5, 6, 2, 4];
@@ -4344,13 +4635,14 @@ var REHARMONIZE_DIAL = {
4344
4635
  function admittedCount(fraction, size) {
4345
4636
  return Math.max(0, Math.min(size, Math.ceil(fraction * size)));
4346
4637
  }
4347
- function buildCandidates(key, harmonic) {
4638
+ function buildCandidates(keyLike, harmonic) {
4639
+ const { scale: key, tonic } = resolveKey(keyLike);
4348
4640
  const tonicPc = pitchClassOf(key.rootPc);
4349
4641
  const frame = heptatonicFrameOf(key);
4350
4642
  const tones = scaleTonesInDegreeOrder(frame);
4351
4643
  const candidates = tones.map((rootPc, index) => {
4352
4644
  const degree = index + 1;
4353
- const quality = diatonicTriad(degree, frame).quality;
4645
+ const quality = diatonicTriad(degree, { ...frame, tonic }).quality;
4354
4646
  return {
4355
4647
  rootPc,
4356
4648
  quality,
@@ -4394,7 +4686,7 @@ function buildCandidates(key, harmonic) {
4394
4686
  const parallelTones = scaleTonesInDegreeOrder(parallel);
4395
4687
  const borrowed = [];
4396
4688
  for (let degree = 1; degree <= parallelTones.length; degree += 1) {
4397
- const chord = diatonicTriad(degree, parallel);
4689
+ const chord = diatonicTriad(degree, { ...parallel, tonic });
4398
4690
  if (!isDiatonic(chord, key)) {
4399
4691
  borrowed.push(chord);
4400
4692
  }
@@ -4440,11 +4732,11 @@ function harmonizeMelody(opts) {
4440
4732
  allowNonPositiveDuration: true,
4441
4733
  budget: opts.budget
4442
4734
  });
4443
- const phraseEnds = opts.phraseEnds ?? [];
4735
+ const phraseEnds = assertArray(opts.phraseEnds ?? [], "harmonize phrase ends");
4444
4736
  for (const end of phraseEnds) {
4445
4737
  assertFiniteNumber(end, "harmonize phrase end");
4446
4738
  }
4447
- const soundingMelody = soundingNotesOnly(opts.melody);
4739
+ const soundingMelody = inOnsetOrder(soundingNotesOnly(opts.melody));
4448
4740
  const ts = meterAt(0, toMeterData(opts.ts ?? DEFAULT_METER, "ts"));
4449
4741
  assertTimeSignature(ts);
4450
4742
  const requestedKey = opts.key ?? "infer";
@@ -4452,7 +4744,10 @@ function harmonizeMelody(opts) {
4452
4744
  requestedKey === "infer" ? inferKey(soundingMelody) : requestedKey
4453
4745
  );
4454
4746
  const key = resolvedKey.scale;
4455
- const placement = opts.placement ?? DEFAULT_PLACEMENT;
4747
+ const placement = assertRecord(
4748
+ opts.placement ?? DEFAULT_PLACEMENT,
4749
+ "harmonize placement"
4750
+ );
4456
4751
  const ctx = resolveContext(opts.ctx);
4457
4752
  const reharmonize = assertOneOf(
4458
4753
  opts.reharmonize ?? "diatonic",
@@ -4463,7 +4758,7 @@ function harmonizeMelody(opts) {
4463
4758
  if (soundingMelody.length === 0) {
4464
4759
  return { transposeSemitones: 0, key: resolvedKey, chords: [], melodyRoles: [] };
4465
4760
  }
4466
- const candidates = buildCandidates(key, harmonic);
4761
+ const candidates = buildCandidates(resolvedKey, harmonic);
4467
4762
  const candAt = (i) => candidates[i] ?? FALLBACK;
4468
4763
  const draw = ctx.part("harmony");
4469
4764
  const jitter = candidates.map((_, index) => draw.at("jitter", index) * TIE_BREAK_JITTER2);
@@ -4514,6 +4809,7 @@ function harmonizeMelody(opts) {
4514
4809
  "harmonization placement search",
4515
4810
  opts.budget
4516
4811
  );
4812
+ let bestFit = Number.POSITIVE_INFINITY;
4517
4813
  let bestCost = Number.POSITIVE_INFINITY;
4518
4814
  let bestTs = 0;
4519
4815
  let bestPath = [];
@@ -4528,8 +4824,10 @@ function harmonizeMelody(opts) {
4528
4824
  closingSegments,
4529
4825
  articulatedSegments
4530
4826
  );
4531
- const total = cost + keyFitCost(shifted, key) + tessituraCost(shifted);
4532
- if (total < bestCost) {
4827
+ const fit = keyFitCost(shifted, key);
4828
+ const total = cost + tessituraCost(shifted);
4829
+ if (fit < bestFit || fit === bestFit && total < bestCost) {
4830
+ bestFit = fit;
4533
4831
  bestCost = total;
4534
4832
  bestTs = shift;
4535
4833
  bestPath = path;
@@ -4664,7 +4962,11 @@ function contourOffsets(contour, count) {
4664
4962
  }
4665
4963
  function generateMotif(opts) {
4666
4964
  assertRecord(opts, "motif options");
4667
- const contour = opts.contour ?? "arch";
4965
+ const contour = assertOneOf(
4966
+ opts.contour ?? "arch",
4967
+ ["arch", "ascending", "descending", "wave"],
4968
+ "motif contour"
4969
+ );
4668
4970
  const bars = assertPositiveInt(opts.bars, "motif bars");
4669
4971
  const barBeats = beatsPerBar(meterAt(0, toMeterData(opts.ts ?? DEFAULT_TS4, "ts")));
4670
4972
  const totalBeats = bars * barBeats;
@@ -4811,6 +5113,10 @@ function developMotif(cell, timeline, key, bars, ts) {
4811
5113
  if (span <= 0) {
4812
5114
  return inTimeOrder(cell);
4813
5115
  }
5116
+ const line = canonicalNoteOrder(cellNotes).flatMap((index) => {
5117
+ const note2 = cellNotes[index];
5118
+ return note2 === void 0 ? [] : [note2];
5119
+ });
4814
5120
  const tileSpan = Math.max(span, MIN_TILE_SPAN);
4815
5121
  const tileCount = Math.ceil(totalBeats / tileSpan);
4816
5122
  assertGenerationBudget(tileCount * cellNotes.length, "developed motif notes");
@@ -4818,7 +5124,7 @@ function developMotif(cell, timeline, key, bars, ts) {
4818
5124
  const offset = k * tileSpan;
4819
5125
  const taken = /* @__PURE__ */ new Map();
4820
5126
  let previous;
4821
- for (const n of cellNotes) {
5127
+ for (const n of line) {
4822
5128
  const startBeat = offset + (n.startBeat - origin);
4823
5129
  if (startBeat >= totalBeats) {
4824
5130
  continue;
@@ -4831,153 +5137,2143 @@ function developMotif(cell, timeline, key, bars, ts) {
4831
5137
  const pitch = developedPitch(n.pitch, allows, direction, taken);
4832
5138
  taken.set(pitch, n.pitch);
4833
5139
  previous = n.pitch;
4834
- out.push({ pitch, startBeat, durationBeat: n.durationBeat });
5140
+ out.push({
5141
+ pitch,
5142
+ startBeat,
5143
+ durationBeat: Math.min(n.durationBeat, totalBeats - startBeat)
5144
+ });
4835
5145
  }
4836
5146
  }
4837
5147
  return inTimeOrder({ notes: out });
4838
5148
  }
4839
5149
 
4840
- // src/generate/ornament/index.ts
4841
- var ORNAMENT_STYLES = Object.freeze(["ghost", "flam", "drag", "slide", "accent"]);
4842
- var DEFAULT_TS5 = { numerator: 4, denominator: 4 };
4843
- var DEFAULT_STYLE2 = "ghost";
4844
- var DEFAULT_AMOUNT = 0.5;
4845
- var GHOST_VELOCITY_SCALE = 0.45;
4846
- var ACCENT_VELOCITY_BOOST = 12;
4847
- var DEFAULT_VELOCITY2 = 80;
4848
- var MIN_VELOCITY2 = 1;
4849
- var MAX_VELOCITY2 = 127;
4850
- var SLIDE_MIN_SEMITONES = 1;
4851
- function eligible(style, note2, previous, next, ts) {
4852
- const strong = isStrongBeat(note2.startBeat, ts);
4853
- switch (style) {
4854
- case "ghost":
4855
- return !strong;
4856
- case "flam":
4857
- case "accent":
4858
- return strong;
4859
- case "drag":
4860
- return !strong && next !== void 0 && isStrongBeat(next.startBeat, ts);
4861
- case "slide":
4862
- return previous !== void 0 && Math.abs(note2.pitch - previous.pitch) >= SLIDE_MIN_SEMITONES;
5150
+ // src/generate/plan/internal.ts
5151
+ var STRONG_WEIGHT = 2;
5152
+ function positionClass(beat, meters) {
5153
+ const weight = metricWeight(beat, meters);
5154
+ return weight >= STRONG_WEIGHT ? "strong" : weight > 0 ? "weak" : "off";
5155
+ }
5156
+ function finalDownbeat(phrase, meters) {
5157
+ let downbeat = barStartBeat(phrase.startBeat, meters);
5158
+ while (downbeat + beatsPerBarAt(downbeat, meters) < phrase.endBeat - BEAT_EPS) {
5159
+ downbeat += beatsPerBarAt(downbeat, meters);
4863
5160
  }
5161
+ return Math.max(downbeat, phrase.startBeat);
4864
5162
  }
4865
- function articulationOf(style) {
4866
- return style;
5163
+
5164
+ // src/generate/plan/types.ts
5165
+ var COMPOSITION_PLAN_VERSION = 1;
5166
+ function tent(t, apex) {
5167
+ if (t <= apex) {
5168
+ return apex === 0 ? 1 : -1 + 2 * t / apex;
5169
+ }
5170
+ return apex === 1 ? 1 : 1 - 2 * (t - apex) / (1 - apex);
5171
+ }
5172
+ function plannedContourAt(shape, peakPosition, t) {
5173
+ switch (shape) {
5174
+ case "arch":
5175
+ return tent(t, peakPosition);
5176
+ case "ascending":
5177
+ return -1 + 2 * t;
5178
+ case "descending":
5179
+ return 1 - 2 * t;
5180
+ case "wave":
5181
+ return t < 0.5 ? tent(2 * t, 0.5) : tent(2 * t - 1, 0.5);
5182
+ case "static":
5183
+ return 0;
5184
+ }
4867
5185
  }
4868
- function ornamentedVelocity(style, note2) {
4869
- if (note2.velocity === void 0 && style !== "ghost" && style !== "accent") {
4870
- return void 0;
5186
+
5187
+ // src/generate/plan/validate.ts
5188
+ var MASS_TOLERANCE = 1e-9;
5189
+ function assertString(value, name) {
5190
+ if (typeof value !== "string") {
5191
+ throw new InvalidInputError(`${name} must be a string; received ${describeRejected(value)}`);
4871
5192
  }
4872
- const velocity = note2.velocity ?? DEFAULT_VELOCITY2;
4873
- if (style === "ghost") {
4874
- return clampVelocity(velocity * GHOST_VELOCITY_SCALE);
5193
+ return value;
5194
+ }
5195
+ function assertSpanOrder(startBeat, endBeat, name) {
5196
+ if (endBeat < startBeat) {
5197
+ throw new InvalidInputError(
5198
+ `${name}.endBeat must be at least ${name}.startBeat; received ${endBeat} < ${startBeat}`
5199
+ );
4875
5200
  }
4876
- if (style === "accent") {
4877
- return clampVelocity(velocity + ACCENT_VELOCITY_BOOST);
5201
+ }
5202
+ function assertNullable(value, name, validate) {
5203
+ if (value === void 0) {
5204
+ throw new InvalidInputError(`${name} must be present; received undefined`);
4878
5205
  }
4879
- return velocity;
5206
+ return value === null ? null : validate(value, name);
4880
5207
  }
4881
- function clampVelocity(velocity) {
4882
- return Math.min(MAX_VELOCITY2, Math.max(MIN_VELOCITY2, Math.round(velocity)));
5208
+ function assertMassDistribution(value, length, name) {
5209
+ const shares = assertArray(value, name);
5210
+ if (shares.length !== length) {
5211
+ throw new InvalidInputError(`${name} must have length ${length}; received ${shares.length}`);
5212
+ }
5213
+ let sum = 0;
5214
+ for (let index = 0; index < shares.length; index += 1) {
5215
+ const share = assertFiniteNumber(shares[index], `${name}[${index}]`);
5216
+ if (share < 0) {
5217
+ throw new InvalidInputError(`${name}[${index}] must be at least 0; received ${share}`);
5218
+ }
5219
+ sum += share;
5220
+ }
5221
+ if (Math.abs(sum - 1) > MASS_TOLERANCE) {
5222
+ throw new InvalidInputError(`${name} must sum to 1; received ${sum}`);
5223
+ }
5224
+ return shares;
4883
5225
  }
4884
- function ornament(notes, opts = {}) {
4885
- const asked = assertOptions(opts, "opts");
4886
- assertNoteEvents(notes, "ornament notes", { allowNonPositiveDuration: true });
4887
- const style = assertOneOf(asked.style ?? DEFAULT_STYLE2, ORNAMENT_STYLES, "ornament style");
4888
- const ts = meterAt(0, toMeterData(asked.ts ?? DEFAULT_TS5, "ts"));
4889
- assertTimeSignature(ts);
4890
- if (asked.amount !== void 0) {
4891
- assertRange(asked.amount, 0, 1, "ornament amount");
5226
+ function assertResolvedKeyField(value, name) {
5227
+ const read = assertRecord(value, name);
5228
+ try {
5229
+ resolveKey(read);
5230
+ } catch (error) {
5231
+ if (error instanceof InvalidInputError) {
5232
+ throw new InvalidInputError(`${name} must be a valid key: ${error.message}`);
5233
+ }
5234
+ throw error;
4892
5235
  }
4893
- const ctx = resolveContextWith(asked.ctx, { ornament: asked.amount });
4894
- const amount = ctx.ornament ?? DEFAULT_AMOUNT;
4895
- const draw = ctx.part("ornament");
4896
- const sounding2 = dropSilentNotes(notes);
4897
- const ordered = [...sounding2].sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
4898
- const previousOf = previousOnsets(ordered);
4899
- const nextOf = nextOnsets(ordered);
4900
- const gaps = strokeGaps(ordered);
4901
- return sounding2.map((note2) => {
4902
- const decorated = { ...note2 };
4903
- if (note2.articulation !== void 0 || !eligible(style, note2, previousOf.get(note2), nextOf.get(note2), ts)) {
4904
- return decorated;
5236
+ return read;
5237
+ }
5238
+ function assertPlannedSection(value, name) {
5239
+ const read = assertRecord(value, name);
5240
+ assertString(read.label, `${name}.label`);
5241
+ const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
5242
+ const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
5243
+ assertSpanOrder(startBeat, endBeat, name);
5244
+ return read;
5245
+ }
5246
+ function assertPlannedPhrase(value, name, sectionCount, motifCount) {
5247
+ const read = assertRecord(value, name);
5248
+ const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
5249
+ const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
5250
+ assertSpanOrder(startBeat, endBeat, name);
5251
+ assertNullable(
5252
+ read.section,
5253
+ `${name}.section`,
5254
+ (v, n) => assertInteger(v, n, 0, Math.max(sectionCount - 1, 0))
5255
+ );
5256
+ assertNullable(read.cadence, `${name}.cadence`, (v, n) => assertOneOf(v, CADENCE_TYPES, n));
5257
+ assertOneOf(read.shape, MELODIC_CONTOUR_SHAPES, `${name}.shape`);
5258
+ assertRange(read.peakPosition, 0, 1, `${name}.peakPosition`);
5259
+ const register = assertRecord(
5260
+ read.register,
5261
+ `${name}.register`
5262
+ );
5263
+ assertMidiPitch(register.low, `${name}.register.low`);
5264
+ assertMidiPitch(register.high, `${name}.register.high`);
5265
+ assertRange(register.mean, 0, 127, `${name}.register.mean`);
5266
+ assertRange(read.onsetDensity, 0, Number.MAX_SAFE_INTEGER, `${name}.onsetDensity`);
5267
+ const motifs = assertArray(read.motifs, `${name}.motifs`);
5268
+ for (let index = 0; index < motifs.length; index += 1) {
5269
+ assertInteger(
5270
+ motifs[index],
5271
+ `${name}.motifs[${index}]`,
5272
+ 0,
5273
+ Math.max(motifCount - 1, 0)
5274
+ );
5275
+ }
5276
+ return read;
5277
+ }
5278
+ function assertPlannedChord(value, name, keys) {
5279
+ const read = assertRecord(value, name);
5280
+ const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
5281
+ const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
5282
+ assertSpanOrder(startBeat, endBeat, name);
5283
+ const keyIndex = assertInteger(read.key, `${name}.key`, 0, Math.max(keys.length - 1, 0));
5284
+ const roman = assertString(read.roman, `${name}.roman`);
5285
+ try {
5286
+ romanToChord(roman, keys[keyIndex]);
5287
+ } catch (error) {
5288
+ if (error instanceof InvalidInputError) {
5289
+ throw new InvalidInputError(
5290
+ `${name}.roman must be readable by romanToChord; received ${describeRejected(roman)}: ${error.message}`
5291
+ );
4905
5292
  }
4906
- if (!sustainsStrokes(gaps.get(note2) ?? Number.POSITIVE_INFINITY, ctx.bpm, ctx.difficulty)) {
4907
- return decorated;
5293
+ throw error;
5294
+ }
5295
+ return read;
5296
+ }
5297
+ function assertPlannedMotifRelation(value, name) {
5298
+ const read = assertRecord(value, name);
5299
+ assertOneOf(read.kind, MOTIF_RELATION_KINDS, `${name}.kind`);
5300
+ assertFlag(read.sequence, `${name}.sequence`);
5301
+ assertFiniteNumber(read.semitones, `${name}.semitones`);
5302
+ if (read.kind === "tonalTransposition") {
5303
+ assertFiniteNumber(read.degrees, `${name}.degrees`);
5304
+ } else if (read.degrees !== void 0) {
5305
+ assertFiniteNumber(read.degrees, `${name}.degrees`);
5306
+ }
5307
+ const timeRatio = assertFiniteNumber(read.timeRatio, `${name}.timeRatio`);
5308
+ if (timeRatio <= 0) {
5309
+ throw new InvalidInputError(`${name}.timeRatio must be positive; received ${timeRatio}`);
5310
+ }
5311
+ return read;
5312
+ }
5313
+ function assertPlannedMotif(value, name, ownIndex, motifCount, phraseCount) {
5314
+ const read = assertRecord(value, name);
5315
+ assertInteger(read.phrase, `${name}.phrase`, 0, Math.max(phraseCount - 1, 0));
5316
+ const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
5317
+ const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
5318
+ assertSpanOrder(startBeat, endBeat, name);
5319
+ assertPositiveInt(read.notes, `${name}.notes`);
5320
+ const from = assertNullable(
5321
+ read.from,
5322
+ `${name}.from`,
5323
+ (v, n) => assertInteger(v, n, 0, Math.max(motifCount - 1, 0))
5324
+ );
5325
+ if (from !== null && from >= ownIndex) {
5326
+ throw new InvalidInputError(
5327
+ `${name}.from must be less than this motif's own index (${ownIndex}); received ${from}`
5328
+ );
5329
+ }
5330
+ const relation = assertNullable(read.relation, `${name}.relation`, assertPlannedMotifRelation);
5331
+ if (from === null && relation !== null) {
5332
+ throw new InvalidInputError(
5333
+ `${name}.relation must be null for a root motif (from = null); received a relation`
5334
+ );
5335
+ }
5336
+ const rhythm = assertNullable(read.rhythm, `${name}.rhythm`, (v, n) => assertArray(v, n));
5337
+ if (rhythm !== null) {
5338
+ if (from !== null) {
5339
+ throw new InvalidInputError(`${name}.rhythm must be null for a derived statement`);
4908
5340
  }
4909
- if (!draw.prob(amount, style, note2.startBeat, note2.pitch)) {
4910
- return decorated;
5341
+ if (rhythm.length !== read.notes - 1) {
5342
+ throw new InvalidInputError(
5343
+ `${name}.rhythm must hold notes - 1 (${read.notes - 1}) gaps; received ${rhythm.length}`
5344
+ );
4911
5345
  }
4912
- decorated.articulation = articulationOf(style);
4913
- const velocity = ornamentedVelocity(style, note2);
4914
- if (velocity !== void 0) {
4915
- decorated.velocity = velocity;
5346
+ let total = 0;
5347
+ for (let index = 0; index < rhythm.length; index += 1) {
5348
+ const gap = assertFiniteNumber(rhythm[index], `${name}.rhythm[${index}]`);
5349
+ if (gap <= 0) {
5350
+ throw new InvalidInputError(`${name}.rhythm[${index}] must be positive; received ${gap}`);
5351
+ }
5352
+ total += gap;
4916
5353
  }
4917
- return decorated;
4918
- });
4919
- }
4920
- function previousOnsets(ordered) {
4921
- const previousOf = /* @__PURE__ */ new Map();
4922
- let earlier;
4923
- for (const note2 of ordered) {
4924
- if (earlier !== void 0 && earlier.startBeat === note2.startBeat) {
4925
- previousOf.set(note2, previousOf.get(earlier));
4926
- } else {
4927
- previousOf.set(note2, earlier);
5354
+ if (total >= endBeat - startBeat - BEAT_EPS) {
5355
+ throw new InvalidInputError(
5356
+ `${name}.rhythm must sum to less than the statement's length (${endBeat - startBeat}); received ${total}`
5357
+ );
4928
5358
  }
4929
- earlier = note2;
4930
5359
  }
4931
- return previousOf;
5360
+ return read;
4932
5361
  }
4933
- function nextOnsets(ordered) {
4934
- const nextOf = /* @__PURE__ */ new Map();
4935
- let later;
4936
- for (let index = ordered.length - 1; index >= 0; index -= 1) {
4937
- const note2 = ordered[index];
4938
- if (!note2) continue;
4939
- if (later !== void 0 && later.startBeat === note2.startBeat) {
4940
- nextOf.set(note2, nextOf.get(later));
4941
- } else {
4942
- nextOf.set(note2, later);
4943
- }
4944
- later = note2;
4945
- }
4946
- return nextOf;
5362
+ function assertPlannedRhythm(value, name) {
5363
+ const read = assertRecord(value, name);
5364
+ assertMassDistribution(read.onsetLevels, 6, `${name}.onsetLevels`);
5365
+ assertMassDistribution(read.interOnsetShares, RHYTHM_IOI_BINS.length, `${name}.interOnsetShares`);
5366
+ assertRange(read.syncopation, 0, 1, `${name}.syncopation`);
5367
+ return read;
4947
5368
  }
4948
- function strokeGaps(ordered) {
4949
- const distinct = [...new Set(ordered.map((note2) => note2.startBeat))].sort((a, b) => a - b);
4950
- const gapAt = /* @__PURE__ */ new Map();
4951
- for (let index = 0; index < distinct.length; index += 1) {
4952
- const beat = distinct[index] ?? 0;
4953
- const before = index === 0 ? Number.POSITIVE_INFINITY : beat - (distinct[index - 1] ?? 0);
4954
- const after = index === distinct.length - 1 ? Number.POSITIVE_INFINITY : (distinct[index + 1] ?? 0) - beat;
4955
- gapAt.set(beat, Math.min(before, after));
5369
+ function assertCompositionPlan(value, name = "composition plan") {
5370
+ const read = assertRecord(value, name);
5371
+ if (read.planVersion !== COMPOSITION_PLAN_VERSION) {
5372
+ throw new InvalidInputError(
5373
+ `${name}.planVersion must be ${COMPOSITION_PLAN_VERSION}; received ${describeRejected(read.planVersion)}`
5374
+ );
4956
5375
  }
4957
- const gaps = /* @__PURE__ */ new Map();
4958
- for (const note2 of ordered) {
4959
- gaps.set(note2, gapAt.get(note2.startBeat) ?? Number.POSITIVE_INFINITY);
5376
+ assertInteger(read.seed, `${name}.seed`, 0, 4294967295);
5377
+ assertInteger(
5378
+ read.algorithmVersion,
5379
+ `${name}.algorithmVersion`,
5380
+ MIN_ALGORITHM_VERSION,
5381
+ ALGORITHM_VERSION
5382
+ );
5383
+ const keys = assertArray(read.keys, `${name}.keys`);
5384
+ if (keys.length === 0) {
5385
+ throw new InvalidInputError(`${name}.keys must hold at least one key; received an empty array`);
4960
5386
  }
4961
- return gaps;
5387
+ for (let index = 0; index < keys.length; index += 1) {
5388
+ assertResolvedKeyField(keys[index], `${name}.keys[${index}]`);
5389
+ }
5390
+ assertMeterMap(read.meters, `${name}.meters`);
5391
+ const span = assertRecord(
5392
+ read.span,
5393
+ `${name}.span`
5394
+ );
5395
+ const spanStart = assertFiniteNumber(span.startBeat, `${name}.span.startBeat`);
5396
+ const spanEnd = assertFiniteNumber(span.endBeat, `${name}.span.endBeat`);
5397
+ assertSpanOrder(spanStart, spanEnd, `${name}.span`);
5398
+ assertRange(span.bars, 0, Number.MAX_SAFE_INTEGER, `${name}.span.bars`);
5399
+ const sections = assertArray(read.sections, `${name}.sections`);
5400
+ for (let index = 0; index < sections.length; index += 1) {
5401
+ assertPlannedSection(sections[index], `${name}.sections[${index}]`);
5402
+ }
5403
+ const phrases = assertArray(read.phrases, `${name}.phrases`);
5404
+ const motifs = assertArray(read.motifs, `${name}.motifs`);
5405
+ for (let index = 0; index < phrases.length; index += 1) {
5406
+ assertPlannedPhrase(
5407
+ phrases[index],
5408
+ `${name}.phrases[${index}]`,
5409
+ sections.length,
5410
+ motifs.length
5411
+ );
5412
+ }
5413
+ const harmony = assertArray(read.harmony, `${name}.harmony`);
5414
+ for (let index = 0; index < harmony.length; index += 1) {
5415
+ assertPlannedChord(harmony[index], `${name}.harmony[${index}]`, keys);
5416
+ }
5417
+ for (let index = 0; index < motifs.length; index += 1) {
5418
+ assertPlannedMotif(
5419
+ motifs[index],
5420
+ `${name}.motifs[${index}]`,
5421
+ index,
5422
+ motifs.length,
5423
+ phrases.length
5424
+ );
5425
+ }
5426
+ assertPlannedRhythm(read.rhythm, `${name}.rhythm`);
5427
+ return read;
4962
5428
  }
4963
5429
 
4964
- // src/generate/progression/index.ts
4965
- var DEFAULT_TS6 = { numerator: 4, denominator: 4 };
4966
- var PROG_STYLES = Object.freeze(["minimal", "dance", "idol", "rock"]);
4967
- var BORROWED_DEGREES = Object.freeze({
4968
- /** Flat submediant: bVI. */
4969
- bVI: 8,
4970
- /** Flat subtonic: bVII. */
4971
- bVII: 10,
4972
- /** Flat mediant: bIII. */
4973
- bIII: 11,
4974
- /** Minor subdominant borrowed from the parallel minor: iv. */
4975
- iv: 12,
4976
- /** Neapolitan: bII. */
4977
- bII: 13,
4978
- /** Sharp subdominant, the diminished #IV. */
4979
- sharpIV: 14
4980
- });
5430
+ // src/generate/plan/timeline.ts
5431
+ function planTimeline(plan) {
5432
+ return planChordTimeline(assertCompositionPlan(plan));
5433
+ }
5434
+ function planChordTimeline(plan) {
5435
+ const spans = plan.harmony.map((chord) => {
5436
+ const key = plan.keys[chord.key];
5437
+ return spanFromChord(romanToChord(chord.roman, key), chord.startBeat);
5438
+ });
5439
+ return chordTimelineFromChords(spans, plan.span.endBeat);
5440
+ }
5441
+
5442
+ // src/generate/plan/harmony.ts
5443
+ var HARMONY_OK_KIND_NAMES = [
5444
+ "chordTone",
5445
+ "tension",
5446
+ "suspension",
5447
+ "appoggiatura",
5448
+ "anticipation",
5449
+ "passing",
5450
+ "neighbor"
5451
+ ];
5452
+ var HARMONY_OK_KINDS = new Set(HARMONY_OK_KIND_NAMES);
5453
+ var CADENCE_DEGREES = {
5454
+ authentic: [1, 3],
5455
+ plagal: [1, 3],
5456
+ half: [2, 5, 7],
5457
+ phrygian: [2, 5, 7],
5458
+ modal: [1]
5459
+ };
5460
+ function planKeyAt(plan) {
5461
+ const regions = [...plan.harmony].sort((a, b) => a.startBeat - b.startBeat).map((chord) => ({
5462
+ startBeat: chord.startBeat,
5463
+ endBeat: chord.endBeat,
5464
+ key: plan.keys[chord.key],
5465
+ confidence: 1
5466
+ }));
5467
+ return keyLookup(regions, plan.keys[0]);
5468
+ }
5469
+ function cadencePitchClasses(plan, timeline, cadence, beat) {
5470
+ if (cadence === "deceptive") {
5471
+ const chord = timeline.at(beat);
5472
+ return chord === null ? null : chordPitchClasses(chord);
5473
+ }
5474
+ const entry = plan.harmony.find((chord) => beat >= chord.startBeat && beat < chord.endBeat);
5475
+ const scale = scaleOf(plan.keys[entry?.key ?? 0]);
5476
+ const tones = scaleTonesInDegreeOrder(scale);
5477
+ const raised = cadencesThroughRaisedSeventh(scale);
5478
+ return CADENCE_DEGREES[cadence].map((degree) => {
5479
+ const tone = tones[degree - 1];
5480
+ return raised && degree === 7 && tone !== void 0 ? leadingTonePcOf(scale) : tone;
5481
+ }).filter((pc) => pc !== void 0);
5482
+ }
5483
+ function planHarmonyMisfits(plan, timeline = planChordTimeline(plan)) {
5484
+ const keyAt = planKeyAt(plan);
5485
+ const scaleAt2 = (beat) => scaleOf(keyAt(beat));
5486
+ return (line) => {
5487
+ const analyzed = analyzeVoice(line, timeline.at, scaleAt2);
5488
+ return line.map((note2, index) => {
5489
+ const position = positionClass(note2.startBeat, plan.meters);
5490
+ if (position === "off") {
5491
+ return null;
5492
+ }
5493
+ const kinds = (analyzed[index]?.labels ?? []).map((label) => label.kind);
5494
+ if (kinds.some((kind) => HARMONY_OK_KINDS.has(kind))) {
5495
+ return null;
5496
+ }
5497
+ const lenient = position === "weak" || timeline.at(note2.startBeat) === null;
5498
+ return lenient && isScaleTone(note2.pitch, scaleAt2(note2.startBeat)) ? null : kinds;
5499
+ });
5500
+ };
5501
+ }
5502
+
5503
+ // src/generate/melody/pitch-dp.ts
5504
+ var NO_LEAP = 0;
5505
+ var LEAP_UP = 1;
5506
+ var LEAP_DOWN = 2;
5507
+ var LEAP_STATES = 3;
5508
+ var SEARCH_LABEL = "melody pitch search";
5509
+ var WIDEST_THIRD = 4;
5510
+ var LEAP_CONTINUATION_COST = 0.5;
5511
+ function intervalCost(size) {
5512
+ if (size === 0) {
5513
+ return 0.2;
5514
+ }
5515
+ if (size <= 2) {
5516
+ return 0;
5517
+ }
5518
+ if (size <= WIDEST_THIRD) {
5519
+ return 0.3;
5520
+ }
5521
+ if (size <= 7) {
5522
+ return 0.6;
5523
+ }
5524
+ return 1;
5525
+ }
5526
+ function leapOf(delta) {
5527
+ if (Math.abs(delta) <= WIDEST_THIRD) {
5528
+ return NO_LEAP;
5529
+ }
5530
+ return delta > 0 ? LEAP_UP : LEAP_DOWN;
5531
+ }
5532
+ function stepCost(from, to, leap, bothFixed) {
5533
+ const delta = to - from;
5534
+ const size = Math.abs(delta);
5535
+ if (!bothFixed && isForbiddenMelodicLeap(from, to)) {
5536
+ return Number.POSITIVE_INFINITY;
5537
+ }
5538
+ let cost = intervalCost(size);
5539
+ if (leap === LEAP_UP && delta > 0 || leap === LEAP_DOWN && delta < 0) {
5540
+ cost += LEAP_CONTINUATION_COST;
5541
+ }
5542
+ return cost;
5543
+ }
5544
+ function searchPitches(slots, budget, extra) {
5545
+ const first = slots[0];
5546
+ if (first === void 0) {
5547
+ return [];
5548
+ }
5549
+ let previous = allocateCandidateTable(
5550
+ first.candidates.length,
5551
+ LEAP_STATES,
5552
+ SEARCH_LABEL,
5553
+ budget
5554
+ ).fill(Number.POSITIVE_INFINITY);
5555
+ for (let j = 0; j < first.candidates.length; j += 1) {
5556
+ previous[j * LEAP_STATES + NO_LEAP] = first.cost(first.candidates[j]);
5557
+ }
5558
+ const backs = [];
5559
+ for (let k = 1; k < slots.length; k += 1) {
5560
+ const before = slots[k - 1];
5561
+ const slot = slots[k];
5562
+ const unary = slot.candidates.map((pitch) => slot.cost(pitch));
5563
+ const current = allocateCandidateTable(
5564
+ slot.candidates.length,
5565
+ LEAP_STATES,
5566
+ SEARCH_LABEL,
5567
+ budget
5568
+ ).fill(Number.POSITIVE_INFINITY);
5569
+ const back = allocateChoiceTable(
5570
+ slot.candidates.length,
5571
+ LEAP_STATES,
5572
+ SEARCH_LABEL,
5573
+ budget
5574
+ ).fill(-1);
5575
+ const bothFixed = before.fixed && slot.fixed;
5576
+ for (let state2 = 0; state2 < previous.length; state2 += 1) {
5577
+ const reached = previous[state2];
5578
+ if (reached === Number.POSITIVE_INFINITY) {
5579
+ continue;
5580
+ }
5581
+ const from = before.candidates[Math.floor(state2 / LEAP_STATES)];
5582
+ const leap = state2 % LEAP_STATES;
5583
+ for (let j = 0; j < slot.candidates.length; j += 1) {
5584
+ const to = slot.candidates[j];
5585
+ const step = stepCost(from, to, leap, bothFixed);
5586
+ if (step === Number.POSITIVE_INFINITY) {
5587
+ continue;
5588
+ }
5589
+ const total = reached + step + unary[j] + (extra ? extra(k, from, to) : 0);
5590
+ const index = j * LEAP_STATES + leapOf(to - from);
5591
+ if (total < current[index]) {
5592
+ current[index] = total;
5593
+ back[index] = state2;
5594
+ }
5595
+ }
5596
+ }
5597
+ backs.push(back);
5598
+ previous = current;
5599
+ }
5600
+ let best = -1;
5601
+ for (let state2 = 0; state2 < previous.length; state2 += 1) {
5602
+ if (previous[state2] < Number.POSITIVE_INFINITY && (best < 0 || previous[state2] < previous[best])) {
5603
+ best = state2;
5604
+ }
5605
+ }
5606
+ if (best < 0) {
5607
+ return null;
5608
+ }
5609
+ const pitches = new Array(slots.length);
5610
+ let state = best;
5611
+ for (let k = slots.length - 1; k >= 0; k -= 1) {
5612
+ pitches[k] = slots[k].candidates[Math.floor(state / LEAP_STATES)];
5613
+ if (k > 0) {
5614
+ state = backs[k - 1][state];
5615
+ }
5616
+ }
5617
+ return pitches;
5618
+ }
5619
+
5620
+ // src/generate/melody/derive.ts
5621
+ var VARIATION_STEP_COST = 0.2;
5622
+ var OCTAVE = 12;
5623
+ var LEVEL_TARGET_COST = 0.1;
5624
+ var LEVEL_PLAN_COST = 0.02;
5625
+ var LEVEL_REACH = 24;
5626
+ var TONAL_LEVEL_REACH = 14;
5627
+ function stretched(cell, timeRatio) {
5628
+ return timeRatio === 1 ? cell : transformMotif(cell, "augment", timeRatio);
5629
+ }
5630
+ function shaped(cell, relation) {
5631
+ switch (relation.kind) {
5632
+ case "repetition":
5633
+ return cell;
5634
+ case "transposition":
5635
+ case "tonalTransposition":
5636
+ return stretched(cell, relation.timeRatio);
5637
+ case "inversion":
5638
+ return stretched(transformMotif(cell, "invert"), relation.timeRatio);
5639
+ case "retrograde":
5640
+ return stretched(transformMotif(cell, "retrograde"), relation.timeRatio);
5641
+ case "retrogradeInversion":
5642
+ return stretched(
5643
+ transformMotif(transformMotif(cell, "invert"), "retrograde"),
5644
+ relation.timeRatio
5645
+ );
5646
+ case "augmentation":
5647
+ return transformMotif(cell, "augment", relation.timeRatio);
5648
+ case "diminution":
5649
+ return transformMotif(cell, "diminish", 1 / relation.timeRatio);
5650
+ }
5651
+ }
5652
+ function relationLevels(kind, low = -LEVEL_REACH, high = LEVEL_REACH) {
5653
+ const between = (from, to, skipZero) => {
5654
+ const levels = [];
5655
+ for (let level = Math.ceil(from); level <= to; level += 1) {
5656
+ if (!(skipZero && level === 0)) levels.push(level);
5657
+ }
5658
+ return levels;
5659
+ };
5660
+ switch (kind) {
5661
+ case "repetition":
5662
+ case "augmentation":
5663
+ case "diminution":
5664
+ return [-OCTAVE, 0, OCTAVE];
5665
+ case "transposition":
5666
+ return between(low, high, true);
5667
+ case "tonalTransposition":
5668
+ return between(-TONAL_LEVEL_REACH, TONAL_LEVEL_REACH, true);
5669
+ case "inversion":
5670
+ case "retrograde":
5671
+ case "retrogradeInversion":
5672
+ return between(low, high, false);
5673
+ }
5674
+ }
5675
+ function replayAt(source, relation, level, startBeat, key) {
5676
+ const head = source[0];
5677
+ const cell = shaped({ notes: source.map((n) => ({ ...n })) }, relation);
5678
+ const first = cell.notes[0];
5679
+ const offset = startBeat - first.startBeat;
5680
+ const shift = head.pitch + level - first.pitch;
5681
+ return cell.notes.map((n) => ({
5682
+ ...n,
5683
+ pitch: relation.kind === "tonalTransposition" ? shiftByScaleDegrees(n.pitch, level, key) : n.pitch + shift,
5684
+ startBeat: n.startBeat + offset
5685
+ }));
5686
+ }
5687
+ function clipStatement(notes, endBeat) {
5688
+ return notes.filter((n) => n.startBeat < endBeat - BEAT_EPS).map((n) => ({ ...n, durationBeat: Math.min(n.durationBeat, endBeat - n.startBeat) }));
5689
+ }
5690
+ function outsideCount(notes, frame) {
5691
+ return notes.filter((n) => n.pitch < frame.low || n.pitch > frame.high).length;
5692
+ }
5693
+ function nearerLevel(level, best, planned) {
5694
+ const distance = Math.abs(level - planned);
5695
+ const bestDistance = Math.abs(best - planned);
5696
+ return distance < bestDistance || distance === bestDistance && level < best;
5697
+ }
5698
+ function repairStatement(notes, forced, frame) {
5699
+ const slots = notes.map(
5700
+ (n, index) => forced[index] ? {
5701
+ candidates: frame.candidatesAt(n.startBeat),
5702
+ cost: frame.costAt(n.startBeat),
5703
+ fixed: false
5704
+ } : { candidates: [n.pitch], cost: () => 0, fixed: true }
5705
+ );
5706
+ const pitches = searchPitches(slots, frame.budget);
5707
+ if (pitches === null) {
5708
+ throw new NoSolutionError(
5709
+ `no repair fits the statement into the register ${frame.low}..${frame.high}`,
5710
+ { at: frame.at }
5711
+ );
5712
+ }
5713
+ return notes.map((n, index) => ({ ...n, pitch: pitches[index] }));
5714
+ }
5715
+ function placeDerived(source, relation, startBeat, endBeat, key, frame, before) {
5716
+ const planned = relation.kind === "tonalTransposition" ? relation.degrees ?? 0 : relation.semitones;
5717
+ const replay = (level) => clipStatement(replayAt(source, relation, level, startBeat, key), endBeat);
5718
+ const base = replay(0).map((n) => n.pitch);
5719
+ const levels = relationLevels(
5720
+ relation.kind,
5721
+ Math.max(-LEVEL_REACH, frame.low - Math.min(...base)),
5722
+ Math.min(LEVEL_REACH, frame.high - Math.max(...base))
5723
+ );
5724
+ let best = null;
5725
+ for (const level of levels) {
5726
+ const notes2 = replay(level);
5727
+ if (outsideCount(notes2, frame) > 0) {
5728
+ continue;
5729
+ }
5730
+ const distance = notes2.reduce((sum, n) => sum + Math.abs(n.pitch - frame.targetAt(n.startBeat)), 0) / notes2.length;
5731
+ const cost = frame.misfits(notes2, before) + LEVEL_TARGET_COST * distance + LEVEL_PLAN_COST * Math.abs(level - planned);
5732
+ if (best === null || cost < best.cost || cost === best.cost && nearerLevel(level, best.level, planned)) {
5733
+ best = { level, notes: notes2, cost };
5734
+ }
5735
+ }
5736
+ if (best !== null) {
5737
+ return best.notes;
5738
+ }
5739
+ let fallback = null;
5740
+ for (const level of relationLevels(relation.kind)) {
5741
+ const notes2 = replay(level);
5742
+ const outside = outsideCount(notes2, frame);
5743
+ if (fallback === null || outside < fallback.outside || outside === fallback.outside && nearerLevel(level, fallback.level, planned)) {
5744
+ fallback = { level, notes: notes2, outside };
5745
+ }
5746
+ }
5747
+ const notes = fallback.notes;
5748
+ return repairStatement(
5749
+ notes,
5750
+ notes.map((n) => n.pitch < frame.low || n.pitch > frame.high),
5751
+ frame
5752
+ );
5753
+ }
5754
+ function varyStatement(source, startBeat, frame) {
5755
+ const offset = startBeat - source[0].startBeat;
5756
+ const last = source.length - 1;
5757
+ const inside = (pitch) => pitch >= frame.low && pitch <= frame.high;
5758
+ const outer = [source[0].pitch, source[last].pitch];
5759
+ const shift = [0, OCTAVE, -OCTAVE].find((s) => outer.every((pitch) => inside(pitch + s)));
5760
+ const notes = source.map((n) => ({ ...n, startBeat: n.startBeat + offset }));
5761
+ const slots = notes.map(
5762
+ (n, index) => (index === 0 || index === last) && shift !== void 0 ? { candidates: [n.pitch + shift], cost: () => 0, fixed: true } : {
5763
+ candidates: frame.candidatesAt(n.startBeat),
5764
+ cost: frame.costAt(n.startBeat),
5765
+ fixed: false
5766
+ }
5767
+ );
5768
+ const pitches = searchPitches(
5769
+ slots,
5770
+ frame.budget,
5771
+ (index, from, to) => VARIATION_STEP_COST * Math.abs(
5772
+ to - from - (source[index].pitch - source[index - 1].pitch)
5773
+ )
5774
+ );
5775
+ if (pitches === null) {
5776
+ throw new NoSolutionError(`no variation line fits the register ${frame.low}..${frame.high}`, {
5777
+ at: frame.at
5778
+ });
5779
+ }
5780
+ return notes.map((n, index) => ({ ...n, pitch: pitches[index] }));
5781
+ }
5782
+
5783
+ // src/generate/melody/index.ts
5784
+ var RHYTHM_SUBDIVISION = 4;
5785
+ var OFF_SCALE_COST = 1;
5786
+ var TARGET_COST = 0.1;
5787
+ var TIE_BREAK = 1e-6;
5788
+ var SEARCH_STATES_SQUARED = 9;
5789
+ function melodyContext(plan, input) {
5790
+ const fromPlan = { seed: plan.seed, algorithmVersion: plan.algorithmVersion };
5791
+ if (input === void 0) {
5792
+ return resolveContext(fromPlan);
5793
+ }
5794
+ if (typeof input === "number") {
5795
+ return resolveContext({ ...fromPlan, seed: input });
5796
+ }
5797
+ const read = assertRecord(input, "ctx");
5798
+ if (read.rng !== void 0) {
5799
+ throw new InvalidInputError(
5800
+ "ctx.rng cannot drive a melody: every onset and tie-break is drawn at an address of phrase and beat under a seed; pass a seed instead"
5801
+ );
5802
+ }
5803
+ return resolveContext({
5804
+ ...read,
5805
+ seed: read.seed ?? plan.seed,
5806
+ algorithmVersion: read.algorithmVersion ?? plan.algorithmVersion
5807
+ });
5808
+ }
5809
+ function chordEntryAt(plan, beat) {
5810
+ return plan.harmony.find((chord) => beat >= chord.startBeat && beat < chord.endBeat);
5811
+ }
5812
+ function scaleAt(scene, beat) {
5813
+ return scene.scales[chordEntryAt(scene.plan, beat)?.key ?? 0];
5814
+ }
5815
+ function targetPitch(phrase, beat) {
5816
+ const length = phrase.endBeat - phrase.startBeat;
5817
+ const t = length > 0 ? Math.min(Math.max((beat - phrase.startBeat) / length, 0), 1) : 0;
5818
+ const { low, high, mean } = phrase.register;
5819
+ return mean + (high - low) / 2 * plannedContourAt(phrase.shape, phrase.peakPosition, t);
5820
+ }
5821
+ function registerPitches(phrase) {
5822
+ const pitches = [];
5823
+ for (let pitch = phrase.register.low; pitch <= phrase.register.high; pitch += 1) {
5824
+ pitches.push(pitch);
5825
+ }
5826
+ return pitches;
5827
+ }
5828
+ function frameOf(scene, index) {
5829
+ const phrase = scene.plan.phrases[index];
5830
+ const { low, high } = phrase.register;
5831
+ const register = registerPitches(phrase);
5832
+ const draw = scene.ctx.part("melody");
5833
+ return {
5834
+ low,
5835
+ high,
5836
+ at: phrase.startBeat,
5837
+ budget: scene.budget,
5838
+ candidatesAt: (beat) => {
5839
+ const position = positionClass(beat, scene.plan.meters);
5840
+ const chord = scene.timeline.at(beat);
5841
+ const tones = position === "strong" && chord !== null ? chordPitchClasses(chord) : null;
5842
+ const scale = scaleAt(scene, beat);
5843
+ const candidates = position === "off" ? register : tones !== null ? register.filter((pitch) => tones.includes(pitchClassOf(pitch))) : register.filter((pitch) => isScaleTone(pitch, scale));
5844
+ if (candidates.length === 0) {
5845
+ throw new NoSolutionError(
5846
+ `no ${position} pitch at beat ${beat} fits the register ${low}..${high}`,
5847
+ { at: phrase.startBeat }
5848
+ );
5849
+ }
5850
+ return candidates;
5851
+ },
5852
+ costAt: (beat) => {
5853
+ const off = positionClass(beat, scene.plan.meters) === "off";
5854
+ const scale = scaleAt(scene, beat);
5855
+ const target = targetPitch(phrase, beat);
5856
+ return (pitch) => {
5857
+ let cost = TARGET_COST * Math.abs(pitch - target);
5858
+ if (off && !isScaleTone(pitch, scale)) {
5859
+ cost += OFF_SCALE_COST;
5860
+ }
5861
+ return cost + draw.float(0, TIE_BREAK, index, "tie", beat, pitch);
5862
+ };
5863
+ },
5864
+ targetAt: (beat) => targetPitch(phrase, beat),
5865
+ misfits: (notes, before) => scene.harmonyMisfits([...before, ...notes]).slice(before.length).filter((kinds) => kinds !== null).length
5866
+ };
5867
+ }
5868
+ function membersOf(plan) {
5869
+ return plan.phrases.map((phrase, index) => {
5870
+ const found = new Set(phrase.motifs);
5871
+ plan.motifs.forEach((motif, m) => {
5872
+ if (motif.phrase === index) {
5873
+ found.add(m);
5874
+ }
5875
+ });
5876
+ return [...found].sort(
5877
+ (a, b) => (plan.motifs[a]?.startBeat ?? 0) - (plan.motifs[b]?.startBeat ?? 0) || a - b
5878
+ );
5879
+ });
5880
+ }
5881
+ function segmentsOf(scene, index) {
5882
+ const phrase = scene.plan.phrases[index];
5883
+ const segments = [];
5884
+ let cursor = phrase.startBeat;
5885
+ for (const m of scene.members[index]) {
5886
+ const motif = scene.plan.motifs[m];
5887
+ if (motif === void 0) {
5888
+ continue;
5889
+ }
5890
+ if (motif.startBeat > cursor + BEAT_EPS) {
5891
+ segments.push({ startBeat: cursor, endBeat: motif.startBeat, motif: null });
5892
+ }
5893
+ segments.push({ startBeat: motif.startBeat, endBeat: motif.endBeat, motif: m });
5894
+ cursor = Math.max(cursor, motif.endBeat);
5895
+ }
5896
+ if (phrase.endBeat > cursor + BEAT_EPS) {
5897
+ segments.push({ startBeat: cursor, endBeat: phrase.endBeat, motif: null });
5898
+ }
5899
+ return segments;
5900
+ }
5901
+ function notesAt(onsets, endBeat) {
5902
+ return onsets.map((startBeat, i) => ({
5903
+ pitch: 0,
5904
+ startBeat,
5905
+ durationBeat: (onsets[i + 1] ?? endBeat) - startBeat
5906
+ }));
5907
+ }
5908
+ function onsetSlots(scene, startBeat, endBeat) {
5909
+ const { meters } = scene.plan;
5910
+ const slots = [startBeat];
5911
+ let bar2 = barStartBeat(startBeat, meters);
5912
+ while (bar2 < endBeat - BEAT_EPS) {
5913
+ const barEnd = bar2 + beatsPerBarAt(bar2, meters);
5914
+ for (let k = 0; bar2 + k / RHYTHM_SUBDIVISION < barEnd - BEAT_EPS; k += 1) {
5915
+ const beat = bar2 + k / RHYTHM_SUBDIVISION;
5916
+ if (beat > startBeat + BEAT_EPS && beat < endBeat - BEAT_EPS) {
5917
+ slots.push(beat);
5918
+ }
5919
+ }
5920
+ assertGenerationBudget(slots.length, "melody onset slots", scene.budget);
5921
+ bar2 = barEnd;
5922
+ }
5923
+ return slots;
5924
+ }
5925
+ function drawOnsets(scene, phraseIndex, startBeat, endBeat, count) {
5926
+ const slots = onsetSlots(scene, startBeat, endBeat);
5927
+ assertGenerationBudget(slots.length * Math.max(1, count - 1), "melody onset slots", scene.budget);
5928
+ if (count - 1 > slots.length - 1) {
5929
+ throw new NoSolutionError(
5930
+ `${count} onsets do not fit the sixteenth grid of beats ${startBeat}..${endBeat}`,
5931
+ { at: scene.plan.phrases[phraseIndex].startBeat }
5932
+ );
5933
+ }
5934
+ const levels = slots.map((beat) => rhythmLevel(beat, scene.plan.meters));
5935
+ const perLevel = /* @__PURE__ */ new Map();
5936
+ for (const level of levels) {
5937
+ perLevel.set(level, (perLevel.get(level) ?? 0) + 1);
5938
+ }
5939
+ const levelWeight = levels.map(
5940
+ (level) => (scene.plan.rhythm.onsetLevels[level] ?? 0) / perLevel.get(level)
5941
+ );
5942
+ const draw = scene.ctx.part("melody");
5943
+ const onsets = [startBeat];
5944
+ let current = 0;
5945
+ for (let k = 1; k < count; k += 1) {
5946
+ const last = slots.length - 1 - (count - 1 - k);
5947
+ const candidates = [];
5948
+ for (let j = current + 1; j <= last; j += 1) candidates.push(j);
5949
+ const gapShare = (j) => scene.plan.rhythm.interOnsetShares[ioiBinIndex(slots[j] - slots[current])] ?? 0;
5950
+ let weights = candidates.map((j) => levelWeight[j] * gapShare(j));
5951
+ if (!weights.some((w) => w > 0)) weights = candidates.map((j) => levelWeight[j]);
5952
+ if (!weights.some((w) => w > 0)) weights = candidates.map(() => 1);
5953
+ const total = weights.reduce((sum, w) => sum + w, 0);
5954
+ let target = draw.float(0, 1, phraseIndex, "onset", startBeat, k) * total;
5955
+ let pick = candidates.length - 1;
5956
+ for (let c = 0; c < candidates.length; c += 1) {
5957
+ target -= weights[c];
5958
+ if (target < 0) {
5959
+ pick = c;
5960
+ break;
5961
+ }
5962
+ }
5963
+ current = candidates[pick];
5964
+ onsets.push(slots[current]);
5965
+ }
5966
+ return notesAt(onsets, endBeat);
5967
+ }
5968
+ function placeRhythm(scene, motif) {
5969
+ const { rhythm, startBeat, endBeat, notes } = motif;
5970
+ if (rhythm === null) {
5971
+ return drawOnsets(scene, motif.phrase, startBeat, endBeat, notes);
5972
+ }
5973
+ const onsets = [startBeat];
5974
+ for (const gap of rhythm) {
5975
+ onsets.push(onsets.at(-1) + gap);
5976
+ }
5977
+ return notesAt(onsets, endBeat);
5978
+ }
5979
+ function searchedLine(rhythm, frame) {
5980
+ const slots = rhythm.map((note2) => ({
5981
+ candidates: frame.candidatesAt(note2.startBeat),
5982
+ cost: frame.costAt(note2.startBeat),
5983
+ fixed: false
5984
+ }));
5985
+ const pitches = searchPitches(slots, frame.budget);
5986
+ if (pitches === null) {
5987
+ throw new NoSolutionError(`no pitch line fits the register ${frame.low}..${frame.high}`, {
5988
+ at: frame.at
5989
+ });
5990
+ }
5991
+ return rhythm.map((note2, i) => ({ ...note2, pitch: pitches[i] }));
5992
+ }
5993
+ function statementOf(scene, m) {
5994
+ const known = scene.statements.get(m);
5995
+ if (known !== void 0) {
5996
+ return known;
5997
+ }
5998
+ scene.inProgress.add(m);
5999
+ const motif = scene.plan.motifs[m];
6000
+ const frame = scene.frames[motif.phrase];
6001
+ let notes;
6002
+ if (motif.from === null) {
6003
+ if (motif.endBeat - motif.startBeat <= BEAT_EPS) {
6004
+ throw new NoSolutionError(`root motif ${m} spans no time to place its notes in`, {
6005
+ at: frame.at
6006
+ });
6007
+ }
6008
+ notes = searchedLine(placeRhythm(scene, motif), frame);
6009
+ } else {
6010
+ const source = statementOf(scene, motif.from);
6011
+ notes = motif.relation === null ? varyStatement(source, motif.startBeat, frame) : placeDerived(
6012
+ source,
6013
+ motif.relation,
6014
+ motif.startBeat,
6015
+ motif.endBeat,
6016
+ scaleAt(scene, motif.startBeat),
6017
+ frame,
6018
+ lineBefore(scene, motif.phrase, motif.startBeat)
6019
+ );
6020
+ }
6021
+ scene.inProgress.delete(m);
6022
+ scene.statements.set(m, notes);
6023
+ return notes;
6024
+ }
6025
+ function freeNoteCount(scene, index, segment) {
6026
+ const phrase = scene.plan.phrases[index];
6027
+ const bars = barSpanOf(segment.startBeat, segment.endBeat, scene.plan.meters);
6028
+ return Math.max(1, Math.round(phrase.onsetDensity * bars));
6029
+ }
6030
+ function freeSpan(scene, index, segment) {
6031
+ const key = `${index}:${segment.startBeat}`;
6032
+ const known = scene.free.get(key);
6033
+ if (known !== void 0) {
6034
+ return known;
6035
+ }
6036
+ const rhythm = drawOnsets(
6037
+ scene,
6038
+ index,
6039
+ segment.startBeat,
6040
+ segment.endBeat,
6041
+ freeNoteCount(scene, index, segment)
6042
+ );
6043
+ const notes = searchedLine(rhythm, scene.frames[index]);
6044
+ scene.free.set(key, notes);
6045
+ return notes;
6046
+ }
6047
+ function lineBefore(scene, index, beat) {
6048
+ const line = [];
6049
+ for (const segment of segmentsOf(scene, index)) {
6050
+ if (segment.endBeat > beat + BEAT_EPS) {
6051
+ break;
6052
+ }
6053
+ if (segment.motif === null) {
6054
+ line.push(...freeSpan(scene, index, segment));
6055
+ } else if (!scene.inProgress.has(segment.motif)) {
6056
+ line.push(...statementOf(scene, segment.motif));
6057
+ }
6058
+ }
6059
+ return line.sort((a, b) => a.startBeat - b.startBeat);
6060
+ }
6061
+ function heldNoteIndex(scene, index, line, downbeat) {
6062
+ const phrase = scene.plan.phrases[index];
6063
+ const sounding2 = line.findIndex(
6064
+ (note2) => note2.startBeat <= downbeat + BEAT_EPS && note2.startBeat + note2.durationBeat > downbeat + BEAT_EPS
6065
+ );
6066
+ return sounding2 >= 0 ? sounding2 : line.findIndex(
6067
+ (note2) => note2.startBeat >= downbeat - BEAT_EPS && note2.startBeat < phrase.endBeat - BEAT_EPS
6068
+ );
6069
+ }
6070
+ function repairCadence(scene, index, line, held) {
6071
+ const phrase = scene.plan.phrases[index];
6072
+ if (phrase.cadence === null) {
6073
+ return;
6074
+ }
6075
+ let target = held;
6076
+ for (let i = line.length - 1; target < 0 && i >= 0; i -= 1) {
6077
+ if (positionClass(line[i].startBeat, scene.plan.meters) !== "off") {
6078
+ target = i;
6079
+ }
6080
+ }
6081
+ if (target < 0) {
6082
+ return;
6083
+ }
6084
+ const note2 = line[target];
6085
+ const allowed = cadencePitchClasses(scene.plan, scene.timeline, phrase.cadence, note2.startBeat);
6086
+ if (allowed === null) {
6087
+ return;
6088
+ }
6089
+ const frame = scene.frames[index];
6090
+ const candidates = registerPitches(phrase).filter(
6091
+ (pitch) => allowed.includes(pitchClassOf(pitch))
6092
+ );
6093
+ if (candidates.length === 0) {
6094
+ throw new NoSolutionError(
6095
+ `no pitch in the register ${frame.low}..${frame.high} can end the ${phrase.cadence} cadence`,
6096
+ { at: frame.at }
6097
+ );
6098
+ }
6099
+ if (allowed.includes(pitchClassOf(note2.pitch))) {
6100
+ return;
6101
+ }
6102
+ const slots = line.map(
6103
+ (n, i) => i === target ? { candidates, cost: frame.costAt(n.startBeat), fixed: false } : { candidates: [n.pitch], cost: () => 0, fixed: true }
6104
+ );
6105
+ const pitches = searchPitches(slots, frame.budget);
6106
+ if (pitches === null) {
6107
+ throw new NoSolutionError(`no cadence pitch joins its neighbours without an augmented step`, {
6108
+ at: frame.at
6109
+ });
6110
+ }
6111
+ note2.pitch = pitches[target];
6112
+ }
6113
+ function repickCutOnset(scene, index, line, held) {
6114
+ const note2 = line[held];
6115
+ const derived = scene.plan.motifs.some(
6116
+ (motif) => motif.from !== null && note2.startBeat >= motif.startBeat - BEAT_EPS && note2.startBeat < motif.endBeat - BEAT_EPS
6117
+ );
6118
+ if (scene.plan.phrases[index].cadence !== null || derived) {
6119
+ return;
6120
+ }
6121
+ const frame = scene.frames[index];
6122
+ const candidates = frame.candidatesAt(note2.startBeat);
6123
+ if (candidates.includes(note2.pitch)) {
6124
+ return;
6125
+ }
6126
+ const slots = line.map(
6127
+ (n, i) => i === held ? { candidates, cost: frame.costAt(n.startBeat), fixed: false } : { candidates: [n.pitch], cost: () => 0, fixed: true }
6128
+ );
6129
+ const pitches = searchPitches(slots, frame.budget);
6130
+ if (pitches === null) {
6131
+ throw new NoSolutionError(`no pitch at the final downbeat joins its neighbours`, {
6132
+ at: frame.at
6133
+ });
6134
+ }
6135
+ line[held] = { ...note2, pitch: pitches[held] };
6136
+ }
6137
+ function phraseLine(scene, index) {
6138
+ const line = [];
6139
+ for (const segment of segmentsOf(scene, index)) {
6140
+ line.push(
6141
+ ...segment.motif === null ? freeSpan(scene, index, segment) : statementOf(scene, segment.motif)
6142
+ );
6143
+ }
6144
+ return line.sort((a, b) => a.startBeat - b.startBeat);
6145
+ }
6146
+ function chargePitchSearch(scene) {
6147
+ const { plan } = scene;
6148
+ let estimate = 0;
6149
+ plan.phrases.forEach((phrase, index) => {
6150
+ let searched = 0;
6151
+ for (const segment of segmentsOf(scene, index)) {
6152
+ const motif = segment.motif === null ? void 0 : plan.motifs[segment.motif];
6153
+ if (motif === void 0) {
6154
+ searched += freeNoteCount(scene, index, segment);
6155
+ } else if (motif.relation === null) {
6156
+ searched += motif.notes;
6157
+ } else {
6158
+ estimate += motif.notes * relationLevels(motif.relation.kind).length;
6159
+ }
6160
+ }
6161
+ const register = Math.max(0, phrase.register.high - phrase.register.low + 1);
6162
+ estimate += searched * SEARCH_STATES_SQUARED * register * register;
6163
+ });
6164
+ assertGenerationBudget(estimate, "melody pitch search", scene.budget);
6165
+ }
6166
+ function generateMelody(plan, opts) {
6167
+ assertCompositionPlan(plan);
6168
+ const asked = assertOptions(opts, "opts");
6169
+ if (asked.budget !== void 0) {
6170
+ assertPositiveInt(asked.budget, "budget", Number.MAX_SAFE_INTEGER);
6171
+ }
6172
+ const timeline = planChordTimeline(plan);
6173
+ const scene = {
6174
+ plan,
6175
+ ctx: melodyContext(plan, asked.ctx),
6176
+ budget: asked.budget,
6177
+ timeline,
6178
+ harmonyMisfits: planHarmonyMisfits(plan, timeline),
6179
+ scales: plan.keys.map((key) => scaleOf(key)),
6180
+ members: membersOf(plan),
6181
+ frames: [],
6182
+ statements: /* @__PURE__ */ new Map(),
6183
+ free: /* @__PURE__ */ new Map(),
6184
+ inProgress: /* @__PURE__ */ new Set()
6185
+ };
6186
+ scene.frames = plan.phrases.map((_, index) => frameOf(scene, index));
6187
+ chargePitchSearch(scene);
6188
+ const melody = [];
6189
+ plan.phrases.forEach((_, index) => {
6190
+ const line = phraseLine(scene, index);
6191
+ const downbeat = finalDownbeat(plan.phrases[index], plan.meters);
6192
+ let held = heldNoteIndex(scene, index, line, downbeat);
6193
+ if (held >= 0) {
6194
+ line.length = held + 1;
6195
+ const note2 = line[held];
6196
+ if (note2.startBeat < downbeat - BEAT_EPS) {
6197
+ const cut = downbeat - note2.startBeat;
6198
+ line[held] = { ...note2, durationBeat: cut };
6199
+ line.push({ ...note2, startBeat: downbeat, durationBeat: note2.durationBeat - cut });
6200
+ held += 1;
6201
+ repickCutOnset(scene, index, line, held);
6202
+ }
6203
+ }
6204
+ repairCadence(scene, index, line, held);
6205
+ if (held >= 0) {
6206
+ const note2 = line[held];
6207
+ const phraseEnd = plan.phrases[index].endBeat;
6208
+ line[held] = { ...note2, durationBeat: phraseEnd - note2.startBeat };
6209
+ }
6210
+ melody.push(...line);
6211
+ });
6212
+ melody.sort((a, b) => a.startBeat - b.startBeat);
6213
+ return motifToNoteEvents({ notes: melody });
6214
+ }
6215
+
6216
+ // src/generate/ornament/index.ts
6217
+ var ORNAMENT_STYLES = Object.freeze(["ghost", "flam", "drag", "slide", "accent"]);
6218
+ var DEFAULT_TS5 = { numerator: 4, denominator: 4 };
6219
+ var DEFAULT_STYLE2 = "ghost";
6220
+ var DEFAULT_AMOUNT = 0.5;
6221
+ var GHOST_VELOCITY_SCALE = 0.45;
6222
+ var ACCENT_VELOCITY_BOOST = 12;
6223
+ var DEFAULT_VELOCITY2 = 80;
6224
+ var MIN_VELOCITY2 = 1;
6225
+ var MAX_VELOCITY2 = 127;
6226
+ var SLIDE_MIN_SEMITONES = 1;
6227
+ function eligible(style, note2, previous, next, meter) {
6228
+ const strong = isStrongBeat(note2.startBeat, meter);
6229
+ switch (style) {
6230
+ case "ghost":
6231
+ return !strong;
6232
+ case "flam":
6233
+ case "accent":
6234
+ return strong;
6235
+ case "drag":
6236
+ return !strong && next !== void 0 && isStrongBeat(next.startBeat, meter);
6237
+ case "slide":
6238
+ return previous !== void 0 && Math.abs(note2.pitch - previous.pitch) >= SLIDE_MIN_SEMITONES;
6239
+ }
6240
+ }
6241
+ function articulationOf(style) {
6242
+ return style;
6243
+ }
6244
+ function ornamentedVelocity(style, note2) {
6245
+ if (note2.velocity === void 0 && style !== "ghost" && style !== "accent") {
6246
+ return void 0;
6247
+ }
6248
+ const velocity = note2.velocity ?? DEFAULT_VELOCITY2;
6249
+ if (style === "ghost") {
6250
+ return clampVelocity(velocity * GHOST_VELOCITY_SCALE);
6251
+ }
6252
+ if (style === "accent") {
6253
+ return clampVelocity(velocity + ACCENT_VELOCITY_BOOST);
6254
+ }
6255
+ return velocity;
6256
+ }
6257
+ function clampVelocity(velocity) {
6258
+ return Math.min(MAX_VELOCITY2, Math.max(MIN_VELOCITY2, Math.round(velocity)));
6259
+ }
6260
+ function ornament(notes, opts = {}) {
6261
+ const asked = assertOptions(opts, "opts");
6262
+ assertNoteEvents(notes, "ornament notes", { allowNonPositiveDuration: true });
6263
+ const style = assertOneOf(asked.style ?? DEFAULT_STYLE2, ORNAMENT_STYLES, "ornament style");
6264
+ const meter = toMeterData(asked.ts ?? DEFAULT_TS5, "ts");
6265
+ if (asked.amount !== void 0) {
6266
+ assertRange(asked.amount, 0, 1, "ornament amount");
6267
+ }
6268
+ const ctx = resolveContextWith(asked.ctx, { ornament: asked.amount });
6269
+ const amount = ctx.ornament ?? DEFAULT_AMOUNT;
6270
+ const draw = ctx.part("ornament");
6271
+ const sounding2 = dropSilentNotes(notes);
6272
+ const ordered = [...sounding2].sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
6273
+ const previousOf = previousOnsets(ordered);
6274
+ const nextOf = nextOnsets(ordered);
6275
+ const gaps = strokeGaps(ordered);
6276
+ return sounding2.map((note2) => {
6277
+ const decorated = { ...note2 };
6278
+ if (note2.articulation !== void 0 || !eligible(style, note2, previousOf.get(note2), nextOf.get(note2), meter)) {
6279
+ return decorated;
6280
+ }
6281
+ if (!sustainsStrokes(gaps.get(note2) ?? Number.POSITIVE_INFINITY, ctx.bpm, ctx.difficulty)) {
6282
+ return decorated;
6283
+ }
6284
+ if (!draw.prob(amount, style, note2.startBeat, note2.pitch)) {
6285
+ return decorated;
6286
+ }
6287
+ decorated.articulation = articulationOf(style);
6288
+ const velocity = ornamentedVelocity(style, note2);
6289
+ if (velocity !== void 0) {
6290
+ decorated.velocity = velocity;
6291
+ }
6292
+ return decorated;
6293
+ });
6294
+ }
6295
+ function previousOnsets(ordered) {
6296
+ const previousOf = /* @__PURE__ */ new Map();
6297
+ let earlier;
6298
+ for (const note2 of ordered) {
6299
+ if (earlier !== void 0 && earlier.startBeat === note2.startBeat) {
6300
+ previousOf.set(note2, previousOf.get(earlier));
6301
+ } else {
6302
+ previousOf.set(note2, earlier);
6303
+ }
6304
+ earlier = note2;
6305
+ }
6306
+ return previousOf;
6307
+ }
6308
+ function nextOnsets(ordered) {
6309
+ const nextOf = /* @__PURE__ */ new Map();
6310
+ let later;
6311
+ for (let index = ordered.length - 1; index >= 0; index -= 1) {
6312
+ const note2 = ordered[index];
6313
+ if (!note2) continue;
6314
+ if (later !== void 0 && later.startBeat === note2.startBeat) {
6315
+ nextOf.set(note2, nextOf.get(later));
6316
+ } else {
6317
+ nextOf.set(note2, later);
6318
+ }
6319
+ later = note2;
6320
+ }
6321
+ return nextOf;
6322
+ }
6323
+ function strokeGaps(ordered) {
6324
+ const distinct = [...new Set(ordered.map((note2) => note2.startBeat))].sort((a, b) => a - b);
6325
+ const gapAt = /* @__PURE__ */ new Map();
6326
+ for (let index = 0; index < distinct.length; index += 1) {
6327
+ const beat = distinct[index] ?? 0;
6328
+ const before = index === 0 ? Number.POSITIVE_INFINITY : beat - (distinct[index - 1] ?? 0);
6329
+ const after = index === distinct.length - 1 ? Number.POSITIVE_INFINITY : (distinct[index + 1] ?? 0) - beat;
6330
+ gapAt.set(beat, Math.min(before, after));
6331
+ }
6332
+ const gaps = /* @__PURE__ */ new Map();
6333
+ for (const note2 of ordered) {
6334
+ gaps.set(note2, gapAt.get(note2.startBeat) ?? Number.POSITIVE_INFINITY);
6335
+ }
6336
+ return gaps;
6337
+ }
6338
+
6339
+ // src/generate/plan/derive.ts
6340
+ var PRESERVE_ASPECTS = [
6341
+ "form",
6342
+ "phraseLengths",
6343
+ "harmonicFunction",
6344
+ "harmonicRhythm",
6345
+ "motifRelations",
6346
+ "registerShape",
6347
+ "rhythm"
6348
+ ];
6349
+ var NEUTRAL_SHAPE = "arch";
6350
+ var NEUTRAL_PEAK_POSITION = 0.6;
6351
+ var SHAPE_KEEP_THRESHOLD = 0.5;
6352
+ var NEUTRAL_ONSET_LEVELS = Object.freeze([0, 1, 2, 3, 4, 5].map((level) => level / 15));
6353
+ var FUNCTION_DEGREES = {
6354
+ tonic: [1, 6, 3],
6355
+ subdominant: [4, 2],
6356
+ dominant: [5, 7]
6357
+ };
6358
+ var STRONG_WEIGHT2 = 2;
6359
+ var HEPTATONIC_DEGREES = 7;
6360
+ function mod12(value) {
6361
+ return (value % 12 + 12) % 12;
6362
+ }
6363
+ function lerp(weight, reference, neutral) {
6364
+ return weight * reference + (1 - weight) * neutral;
6365
+ }
6366
+ function readPreserve(value) {
6367
+ const read = value === void 0 ? {} : assertRecord(value, "opts.preserve");
6368
+ const weights = {};
6369
+ for (const aspect of PRESERVE_ASPECTS) {
6370
+ const weight = read[aspect];
6371
+ weights[aspect] = weight === void 0 ? 1 : assertRange(weight, 0, 1, `opts.preserve.${aspect}`);
6372
+ }
6373
+ return weights;
6374
+ }
6375
+ function readRegister(value) {
6376
+ const read = assertRecord(value, "opts.register");
6377
+ const low = assertMidiPitch(read.low, "opts.register.low");
6378
+ const high = assertMidiPitch(read.high, "opts.register.high");
6379
+ if (low > high) {
6380
+ throw new InvalidInputError(
6381
+ `opts.register.low must not exceed opts.register.high; received ${low} > ${high}`
6382
+ );
6383
+ }
6384
+ return { low, high };
6385
+ }
6386
+ function rejectSuppliedRng(ctx) {
6387
+ if (typeof ctx === "object" && ctx !== null && ctx.rng !== void 0) {
6388
+ throw new InvalidInputError(
6389
+ "opts.ctx.rng is not accepted: a composition plan records its seed, not a source"
6390
+ );
6391
+ }
6392
+ }
6393
+ function plannedKeys(profile, requested) {
6394
+ const regions = profile.harmony.keys.map(({ key }) => {
6395
+ const resolved = resolveKey(key);
6396
+ return {
6397
+ scale: { ...resolved.scale },
6398
+ tonic: { ...resolved.tonic },
6399
+ variant: resolved.variant
6400
+ };
6401
+ });
6402
+ const home = regions[0];
6403
+ if (requested === void 0) {
6404
+ return regions;
6405
+ }
6406
+ const target = resolveKey(requested);
6407
+ if (target.variant !== home.variant || target.scale.modeMask12 !== home.scale.modeMask12) {
6408
+ throw new InvalidInputError(
6409
+ "opts.key must be in the same mode as the reference's home key; a key change moves the tonic only"
6410
+ );
6411
+ }
6412
+ const step = spelledInterval(
6413
+ { letter: home.tonic.letter, alter: home.tonic.alter },
6414
+ { letter: target.tonic.letter, alter: target.tonic.alter }
6415
+ );
6416
+ const moved = regions.map(
6417
+ (key) => ({
6418
+ scale: {
6419
+ rootPc: mod12(key.scale.rootPc + step.semitones),
6420
+ modeMask12: key.scale.modeMask12
6421
+ },
6422
+ tonic: transposeByInterval({ letter: key.tonic.letter, alter: key.tonic.alter }, step),
6423
+ variant: key.variant
6424
+ })
6425
+ );
6426
+ moved[0] = target;
6427
+ return moved;
6428
+ }
6429
+ function nearestBoundary(beat, boundaries) {
6430
+ let best = beat;
6431
+ let bestDistance = Number.POSITIVE_INFINITY;
6432
+ for (const boundary of boundaries) {
6433
+ const distance = Math.abs(boundary - beat);
6434
+ if (distance < bestDistance - BEAT_EPS) {
6435
+ best = boundary;
6436
+ bestDistance = distance;
6437
+ }
6438
+ }
6439
+ return best;
6440
+ }
6441
+ function plannedSections(profile, weight, draw) {
6442
+ const sections = profile.form.sections;
6443
+ const boundaries = [profile.span.startBeat, profile.span.endBeat];
6444
+ for (const phrase of profile.form.phrases) {
6445
+ boundaries.push(phrase.startBeat, phrase.endBeat);
6446
+ }
6447
+ boundaries.sort((a, b) => a - b);
6448
+ const planned = [];
6449
+ sections.forEach((section, index) => {
6450
+ const keep = sections.length < 2 || draw.prob(weight, "form", index);
6451
+ const neighbour = index + 1 < sections.length ? index + 1 : index - 1;
6452
+ const startBeat = nearestBoundary(section.startBeat, boundaries);
6453
+ const endBeat = nearestBoundary(section.endBeat, boundaries);
6454
+ if (endBeat - startBeat > BEAT_EPS) {
6455
+ planned.push({
6456
+ label: keep ? section.label : sections[neighbour]?.label ?? section.label,
6457
+ startBeat,
6458
+ endBeat
6459
+ });
6460
+ }
6461
+ });
6462
+ return planned;
6463
+ }
6464
+ function beatAfterBars(startBeat, bars, meters) {
6465
+ let beat = startBeat;
6466
+ let left = bars;
6467
+ while (left > BEAT_EPS) {
6468
+ const step = Math.min(1, left);
6469
+ beat += step * beatsPerBarAt(beat, meters);
6470
+ left -= step;
6471
+ }
6472
+ return beat;
6473
+ }
6474
+ function barsBetween(startBeat, endBeat, meters) {
6475
+ let beat = startBeat;
6476
+ let bars = 0;
6477
+ while (beat < endBeat - BEAT_EPS) {
6478
+ const barBeats = beatsPerBarAt(beat, meters);
6479
+ const step = Math.min(barStartBeat(beat, meters) + barBeats, endBeat) - beat;
6480
+ bars += step / barBeats;
6481
+ beat += step;
6482
+ }
6483
+ return bars;
6484
+ }
6485
+ function spanSegments(profile, sections) {
6486
+ const { startBeat, endBeat } = profile.span;
6487
+ const cuts = [startBeat, endBeat];
6488
+ for (const section of sections) {
6489
+ for (const beat of [section.startBeat, section.endBeat]) {
6490
+ if (beat > startBeat + BEAT_EPS && beat < endBeat - BEAT_EPS) cuts.push(beat);
6491
+ }
6492
+ }
6493
+ cuts.sort((a, b) => a - b);
6494
+ const segments = [];
6495
+ for (let index = 1; index < cuts.length; index += 1) {
6496
+ const from = cuts[index - 1];
6497
+ const to = cuts[index];
6498
+ if (to - from > BEAT_EPS) segments.push([from, to]);
6499
+ }
6500
+ return segments;
6501
+ }
6502
+ function plannedPhraseSpans(profile, sections, weight, draw) {
6503
+ const segments = spanSegments(profile, sections);
6504
+ const pieces = profile.form.phrases.map((phrase, source) => ({
6505
+ startBeat: phrase.startBeat,
6506
+ endBeat: phrase.endBeat,
6507
+ source,
6508
+ cadence: phrase.cadence?.type ?? null
6509
+ }));
6510
+ const replaced = pieces.map((_, index) => !draw.prob(weight, "phraseLengths", index));
6511
+ const spans = [];
6512
+ for (const [segmentStart, segmentEnd] of segments) {
6513
+ const members = pieces.map((piece, index) => ({ piece, index })).filter(
6514
+ ({ piece }) => piece.startBeat >= segmentStart - BEAT_EPS && piece.startBeat < segmentEnd - BEAT_EPS
6515
+ );
6516
+ const laid = [];
6517
+ let cursor = segmentStart;
6518
+ for (const { piece, index } of members) {
6519
+ const lengths = [];
6520
+ if (replaced[index]) {
6521
+ const bars = barsBetween(piece.startBeat, piece.endBeat, profile.meters);
6522
+ const rounded = 2 ** Math.round(Math.log2(bars));
6523
+ const overflows = beatAfterBars(cursor, rounded, profile.meters) > segmentEnd + BEAT_EPS;
6524
+ let partStart = cursor;
6525
+ for (const part of overflows ? [rounded / 2, rounded / 2] : [rounded]) {
6526
+ const partEnd = beatAfterBars(partStart, part, profile.meters);
6527
+ lengths.push(partEnd - partStart);
6528
+ partStart = partEnd;
6529
+ }
6530
+ } else {
6531
+ lengths.push(piece.endBeat - piece.startBeat);
6532
+ }
6533
+ lengths.forEach((length, part) => {
6534
+ if (cursor >= segmentEnd - BEAT_EPS) return;
6535
+ const end = cursor + length;
6536
+ laid.push({
6537
+ startBeat: cursor,
6538
+ endBeat: Math.min(end, segmentEnd),
6539
+ source: piece.source,
6540
+ cadence: part === lengths.length - 1 ? piece.cadence : null
6541
+ });
6542
+ cursor = end;
6543
+ });
6544
+ }
6545
+ const last = laid.at(-1);
6546
+ if (last) {
6547
+ last.endBeat = segmentEnd;
6548
+ } else {
6549
+ laid.push({
6550
+ startBeat: segmentStart,
6551
+ endBeat: segmentEnd,
6552
+ source: spans.at(-1)?.source ?? (profile.form.phrases.length > 0 ? 0 : -1),
6553
+ cadence: null
6554
+ });
6555
+ }
6556
+ spans.push(...laid);
6557
+ }
6558
+ return spans;
6559
+ }
6560
+ function sectionAt(sections, beat) {
6561
+ const index = sections.findIndex(
6562
+ (section) => beat >= section.startBeat - BEAT_EPS && beat < section.endBeat - BEAT_EPS
6563
+ );
6564
+ return index < 0 ? null : index;
6565
+ }
6566
+ function phraseHolding(phrases, startBeat, endBeat) {
6567
+ return phrases.findIndex(
6568
+ (phrase) => startBeat >= phrase.startBeat - BEAT_EPS && endBeat <= phrase.endBeat + BEAT_EPS
6569
+ );
6570
+ }
6571
+ function better(a, b) {
6572
+ for (let field = 0; field < a.length; field += 1) {
6573
+ if (a[field] !== b[field]) return a[field] > b[field];
6574
+ }
6575
+ return false;
6576
+ }
6577
+ function scheduleMotifs(candidates, meters) {
6578
+ const order = [...candidates].sort((a, b) => a.startBeat - b.startBeat || a.node - b.node);
6579
+ const count = order.length;
6580
+ const firstFrom = (from, beat) => {
6581
+ let low = from;
6582
+ let high = count;
6583
+ while (low < high) {
6584
+ const mid = low + high >> 1;
6585
+ if (order[mid].startBeat < beat - BEAT_EPS) low = mid + 1;
6586
+ else high = mid;
6587
+ }
6588
+ return low;
6589
+ };
6590
+ const next = order.map((candidate, index2) => firstFrom(index2 + 1, candidate.endBeat));
6591
+ const own = order.map(
6592
+ (candidate) => [
6593
+ candidate.notes,
6594
+ metricWeight(candidate.startBeat, meters) < STRONG_WEIGHT2 ? -1 : 0,
6595
+ 1
6596
+ ]
6597
+ );
6598
+ const taking = (index2) => {
6599
+ const rest = best[next[index2]];
6600
+ const mine = own[index2];
6601
+ return [mine[0] + rest[0], mine[1] + rest[1], mine[2] + rest[2]];
6602
+ };
6603
+ const best = new Array(count + 1).fill([0, 0, 0]);
6604
+ for (let index2 = count - 1; index2 >= 0; index2 -= 1) {
6605
+ const taken = taking(index2);
6606
+ const skipped = best[index2 + 1];
6607
+ best[index2] = better(taken, skipped) ? taken : skipped;
6608
+ }
6609
+ const chosen = [];
6610
+ let index = 0;
6611
+ while (index < count) {
6612
+ if (!better(best[index], taking(index))) {
6613
+ chosen.push(order[index]);
6614
+ index = next[index];
6615
+ } else {
6616
+ index += 1;
6617
+ }
6618
+ }
6619
+ return chosen.sort((a, b) => a.node - b.node);
6620
+ }
6621
+ function selectedDerivation(graph, parentEdge, selectedAt, node) {
6622
+ let plain = true;
6623
+ let semitones = 0;
6624
+ let steps = 0;
6625
+ for (let edge = parentEdge.get(node); edge !== void 0; edge = parentEdge.get(edge.from)) {
6626
+ const relation = edge.relation;
6627
+ steps += 1;
6628
+ plain &&= relation !== null && (relation.kind === "repetition" || relation.kind === "transposition" && Math.abs(relation.timeRatio - 1) <= BEAT_EPS);
6629
+ semitones += relation?.semitones ?? 0;
6630
+ const from = selectedAt.get(edge.from);
6631
+ if (from === void 0) continue;
6632
+ if (steps === 1) {
6633
+ return { from, relation: relation === null ? null : { ...relation } };
6634
+ }
6635
+ if (!plain) {
6636
+ return { from: null, relation: null };
6637
+ }
6638
+ const gap = (graph.nodes[node]?.startBeat ?? 0) - (graph.nodes[edge.from]?.endBeat ?? 0);
6639
+ return {
6640
+ from,
6641
+ relation: {
6642
+ kind: semitones === 0 ? "repetition" : "transposition",
6643
+ sequence: gap >= -HUMANIZE_ADJACENCY && gap <= HUMANIZE_ADJACENCY,
6644
+ semitones,
6645
+ timeRatio: 1
6646
+ }
6647
+ };
6648
+ }
6649
+ return { from: null, relation: null };
6650
+ }
6651
+ function functionTriad(fn, degree, key) {
6652
+ if (fn === "dominant" && cadencesThroughRaisedSeventh(key.scale)) {
6653
+ if (degree === 5) {
6654
+ return makeChord(pitchClassOf(key.scale.rootPc + 7), "maj");
6655
+ }
6656
+ if (degree === 7) {
6657
+ return makeChord(leadingTonePcOf(key.scale), "dim");
6658
+ }
6659
+ }
6660
+ return diatonicTriad(degree, key);
6661
+ }
6662
+ function functionCandidates(fn, key) {
6663
+ if (scaleTonesInDegreeOrder(key).length !== HEPTATONIC_DEGREES) {
6664
+ return null;
6665
+ }
6666
+ const numerals = FUNCTION_DEGREES[fn].map((degree) => functionTriad(fn, degree, key)).filter((chord) => functionOf(chord, key) === fn).map((chord) => chordToRoman(chord, key));
6667
+ return numerals.length === 0 ? null : numerals;
6668
+ }
6669
+ function plannedHarmony(profile, keys, preserve, draw) {
6670
+ const chords = profile.harmony.chords;
6671
+ const romans = chords.map((chord, index) => {
6672
+ if (chord.level !== "structural" || draw.prob(preserve.harmonicFunction, "harmonicFunction", index)) {
6673
+ return chord.roman;
6674
+ }
6675
+ const candidates = functionCandidates(chord.function, keys[chord.key]);
6676
+ if (candidates === null) {
6677
+ return chord.roman;
6678
+ }
6679
+ const pick = draw.range(0, candidates.length - 1, "harmonicFunction", index, "pick");
6680
+ return candidates[pick];
6681
+ });
6682
+ const starts = [];
6683
+ chords.forEach((chord, index) => {
6684
+ const keep = index === 0 || draw.prob(preserve.harmonicRhythm, "harmonicRhythm", index);
6685
+ const start = keep ? chord.startBeat : barStartBeat(chord.startBeat, profile.meters);
6686
+ starts.push(index === 0 ? start : Math.max(start, starts[index - 1]));
6687
+ });
6688
+ const planned = [];
6689
+ chords.forEach((chord, index) => {
6690
+ const next = chords[index + 1];
6691
+ const nextStart = starts[index + 1];
6692
+ let endBeat = chord.endBeat;
6693
+ if (next !== void 0 && nextStart !== void 0) {
6694
+ endBeat = Math.abs(chord.endBeat - next.startBeat) <= BEAT_EPS ? nextStart : Math.min(chord.endBeat, nextStart);
6695
+ }
6696
+ const startBeat = starts[index];
6697
+ if (endBeat - startBeat > BEAT_EPS) {
6698
+ planned.push({ startBeat, endBeat, key: chord.key, roman: romans[index] });
6699
+ }
6700
+ });
6701
+ return planned;
6702
+ }
6703
+ function plannedRhythm(profile, weight) {
6704
+ const reference = profile.melody.rhythm;
6705
+ return {
6706
+ onsetLevels: reference.onsetLevels.map(
6707
+ (share, level) => lerp(weight, share, NEUTRAL_ONSET_LEVELS[level])
6708
+ ),
6709
+ interOnsetShares: [...reference.interOnsetShares],
6710
+ syncopation: lerp(weight, reference.syncopation, 0)
6711
+ };
6712
+ }
6713
+ function deriveCompositionPlan(reference, opts) {
6714
+ const profile = assertReferenceProfile(reference, "reference");
6715
+ const asked = assertOptions(opts, "opts");
6716
+ const preserve = readPreserve(asked.preserve);
6717
+ const requestedRegister = asked.register === void 0 ? void 0 : readRegister(asked.register);
6718
+ rejectSuppliedRng(asked.ctx);
6719
+ const ctx = resolveContext(asked.ctx);
6720
+ const draw = ctx.part("plan");
6721
+ const melodyRegister = profile.melody.register;
6722
+ if (profile.melody.rhythm.onsets === 0 || melodyRegister === null) {
6723
+ throw new InvalidInputError("reference must carry melody onsets to derive a plan from");
6724
+ }
6725
+ const keys = plannedKeys(profile, asked.key);
6726
+ const homeShift = mod12(
6727
+ keys[0].scale.rootPc - (profile.harmony.keys[0]?.key.scale.rootPc ?? 0)
6728
+ );
6729
+ const shift = homeShift >= 6 ? homeShift - 12 : homeShift;
6730
+ const source = { low: melodyRegister.low + shift, high: melodyRegister.high + shift };
6731
+ if (source.low < 0 || source.high > 127) {
6732
+ throw new InvalidInputError(
6733
+ `the reference melody range moved to the new key leaves the MIDI range; received ${source.low}..${source.high}`
6734
+ );
6735
+ }
6736
+ const target = requestedRegister ?? source;
6737
+ const scale = source.high === source.low ? 0 : (target.high - target.low) / (source.high - source.low);
6738
+ const offset = source.high === source.low ? (target.low + target.high) / 2 : target.low - source.low * scale;
6739
+ const mapPitch = (pitch) => (pitch + shift) * scale + offset;
6740
+ const neutralRegister = {
6741
+ low: target.low,
6742
+ high: target.high,
6743
+ mean: (target.low + target.high) / 2
6744
+ };
6745
+ const sections = plannedSections(profile, preserve.form, draw);
6746
+ const shapeWeight = preserve.registerShape;
6747
+ const phraseBars = profile.form.phrases.reduce(
6748
+ (bars, phrase) => bars + barIndexAt(phrase.endBeat, profile.meters) - barIndexAt(phrase.startBeat, profile.meters),
6749
+ 0
6750
+ );
6751
+ assertGenerationBudget(phraseBars, "plan phrase bars");
6752
+ const spans = plannedPhraseSpans(profile, sections, preserve.phraseLengths, draw);
6753
+ const phrases = spans.map((span) => {
6754
+ const melody = profile.form.phrases[span.source]?.melody ?? null;
6755
+ const register = melody ? {
6756
+ low: Math.round(lerp(shapeWeight, mapPitch(melody.low), neutralRegister.low)),
6757
+ high: Math.round(lerp(shapeWeight, mapPitch(melody.high), neutralRegister.high)),
6758
+ mean: lerp(shapeWeight, mapPitch(melody.mean), neutralRegister.mean)
6759
+ } : { ...neutralRegister };
6760
+ return {
6761
+ startBeat: span.startBeat,
6762
+ endBeat: span.endBeat,
6763
+ section: sectionAt(sections, span.startBeat),
6764
+ cadence: span.cadence,
6765
+ shape: melody && shapeWeight >= SHAPE_KEEP_THRESHOLD ? melody.shape : NEUTRAL_SHAPE,
6766
+ peakPosition: melody ? lerp(shapeWeight, melody.peakPosition, NEUTRAL_PEAK_POSITION) : NEUTRAL_PEAK_POSITION,
6767
+ register,
6768
+ onsetDensity: melody ? melody.onsetDensity : profile.melody.rhythm.onsetDensity,
6769
+ motifs: []
6770
+ };
6771
+ });
6772
+ const graph = profile.melody.graph;
6773
+ const noteCount = (node) => (profile.melody.motifs[graph.nodes[node]?.motif ?? -1]?.intervals.length ?? 0) + 1;
6774
+ assertGenerationBudget(graph.nodes.length * phrases.length, "plan motif selection", asked.budget);
6775
+ const candidates = [];
6776
+ graph.nodes.forEach((node, index) => {
6777
+ const phrase = phraseHolding(phrases, node.startBeat, node.endBeat);
6778
+ if (phrase >= 0) {
6779
+ candidates.push({
6780
+ node: index,
6781
+ phrase,
6782
+ startBeat: node.startBeat,
6783
+ endBeat: node.endBeat,
6784
+ notes: noteCount(index)
6785
+ });
6786
+ }
6787
+ });
6788
+ const selected = scheduleMotifs(candidates, profile.meters);
6789
+ const selectedAt = new Map(selected.map((candidate, index) => [candidate.node, index]));
6790
+ const parentEdge = new Map(graph.edges.map((edge) => [edge.to, edge]));
6791
+ const motifs = selected.map((candidate, index) => {
6792
+ const { from, relation } = selectedDerivation(graph, parentEdge, selectedAt, candidate.node);
6793
+ const keepRelation = draw.prob(preserve.motifRelations, "motifRelations", index);
6794
+ const keepRhythm = from === null && draw.prob(preserve.rhythm, "rhythm", index);
6795
+ const cell = profile.melody.motifs[graph.nodes[candidate.node]?.motif ?? -1];
6796
+ phrases[candidate.phrase]?.motifs.push(index);
6797
+ return {
6798
+ phrase: candidate.phrase,
6799
+ startBeat: candidate.startBeat,
6800
+ endBeat: candidate.endBeat,
6801
+ notes: candidate.notes,
6802
+ rhythm: keepRhythm && cell !== void 0 ? cell.rhythm.map((ratio) => ratio * cell.unitBeats) : null,
6803
+ from,
6804
+ relation: keepRelation ? relation : null
6805
+ };
6806
+ });
6807
+ return {
6808
+ planVersion: COMPOSITION_PLAN_VERSION,
6809
+ seed: ctx.seed,
6810
+ algorithmVersion: ctx.algorithmVersion,
6811
+ keys,
6812
+ meters: profile.meters.map((entry) => ({
6813
+ startBeat: entry.startBeat,
6814
+ ts: entry.ts.grouping === void 0 ? { ...entry.ts } : { ...entry.ts, grouping: [...entry.ts.grouping] }
6815
+ })),
6816
+ span: { ...profile.span },
6817
+ sections,
6818
+ phrases,
6819
+ harmony: plannedHarmony(profile, keys, preserve, draw),
6820
+ motifs,
6821
+ rhythm: plannedRhythm(profile, preserve.rhythm)
6822
+ };
6823
+ }
6824
+
6825
+ // src/generate/plan/evaluate.ts
6826
+ var PEAK_POSITION_TOLERANCE = 0.2;
6827
+ var OUTLINE_POINTS = 8;
6828
+ var HELD_NOTE_BEATS = 1;
6829
+ function interiorBoundaries(plan) {
6830
+ const beats = /* @__PURE__ */ new Set();
6831
+ for (const phrase of plan.phrases) {
6832
+ for (const beat of [phrase.startBeat, phrase.endBeat]) {
6833
+ if (beat > plan.span.startBeat + BEAT_EPS && beat < plan.span.endBeat - BEAT_EPS) {
6834
+ beats.add(beat);
6835
+ }
6836
+ }
6837
+ }
6838
+ return [...beats].sort((a, b) => a - b);
6839
+ }
6840
+ function restsOf(melody, plan) {
6841
+ const rests = [];
6842
+ let reach = plan.span.startBeat;
6843
+ for (const note2 of [...melody].sort((a, b) => a.startBeat - b.startBeat)) {
6844
+ if (note2.startBeat > reach + BEAT_EPS) {
6845
+ rests.push({ startBeat: reach, endBeat: note2.startBeat });
6846
+ }
6847
+ reach = Math.max(reach, note2.startBeat + note2.durationBeat);
6848
+ }
6849
+ if (plan.span.endBeat > reach + BEAT_EPS) {
6850
+ rests.push({ startBeat: reach, endBeat: plan.span.endBeat });
6851
+ }
6852
+ return rests;
6853
+ }
6854
+ function phraseBoundaryViolations(melody, plan) {
6855
+ const rests = restsOf(melody, plan);
6856
+ const violations = [];
6857
+ for (const beat of interiorBoundaries(plan)) {
6858
+ const pulse2 = pulseBeats(meterAt(beat, plan.meters));
6859
+ const held = melody.some(
6860
+ (note2) => note2.durationBeat >= HELD_NOTE_BEATS - BEAT_EPS && Math.abs(note2.startBeat + note2.durationBeat - beat) <= pulse2 + BEAT_EPS
6861
+ );
6862
+ const rested = rests.some(
6863
+ (rest) => rest.endBeat - rest.startBeat >= pulse2 - BEAT_EPS && rest.startBeat <= beat + pulse2 + BEAT_EPS && rest.endBeat >= beat - pulse2 - BEAT_EPS
6864
+ );
6865
+ if (!held && !rested) {
6866
+ violations.push({
6867
+ kind: "phraseBoundary",
6868
+ severity: "error",
6869
+ atBeat: beat,
6870
+ expected: `a note of at least ${HELD_NOTE_BEATS} beat(s) ending, or a rest of at least ${pulse2} beat(s), within ${pulse2} beat(s)`,
6871
+ actual: "neither",
6872
+ rationale: `the plan places a phrase boundary at beat ${beat}, which the candidate does not mark`
6873
+ });
6874
+ }
6875
+ }
6876
+ return violations;
6877
+ }
6878
+ function spanViolation(melody, plan) {
6879
+ const end = melody.reduce((max, note2) => Math.max(max, note2.startBeat + note2.durationBeat), 0);
6880
+ if (end <= plan.span.endBeat + BEAT_EPS) {
6881
+ return [];
6882
+ }
6883
+ return [
6884
+ {
6885
+ kind: "span",
6886
+ severity: "error",
6887
+ atBeat: plan.span.endBeat,
6888
+ expected: `ending by beat ${plan.span.endBeat}`,
6889
+ actual: `ending at beat ${end}`,
6890
+ rationale: "the candidate's last note runs past the plan's span"
6891
+ }
6892
+ ];
6893
+ }
6894
+ function phraseLines(melody, plan) {
6895
+ return plan.phrases.map(
6896
+ (phrase) => melody.flatMap(
6897
+ (note2, index) => note2.startBeat >= phrase.startBeat - BEAT_EPS && note2.startBeat < phrase.endBeat - BEAT_EPS ? [index] : []
6898
+ )
6899
+ );
6900
+ }
6901
+ function cadenceViolations(melody, plan, timeline, lines) {
6902
+ const violations = [];
6903
+ plan.phrases.forEach((phrase, index) => {
6904
+ if (phrase.cadence === null) {
6905
+ return;
6906
+ }
6907
+ const last = lines[index]?.at(-1);
6908
+ const note2 = last === void 0 ? void 0 : melody[last];
6909
+ const allowed = note2 === void 0 ? [] : cadencePitchClasses(plan, timeline, phrase.cadence, note2.startBeat);
6910
+ if (note2 !== void 0 && (allowed === null || allowed.includes(pitchClassOf(note2.pitch)))) {
6911
+ return;
6912
+ }
6913
+ violations.push({
6914
+ kind: "cadence",
6915
+ severity: "error",
6916
+ atBeat: phrase.endBeat,
6917
+ expected: `a closing note on pitch class ${(allowed ?? []).join(" or ")} for a ${phrase.cadence} cadence`,
6918
+ actual: note2 === void 0 ? "no note" : `pitch ${note2.pitch}`,
6919
+ rationale: `the phrase ending at beat ${phrase.endBeat} is planned to close on a ${phrase.cadence} cadence`
6920
+ });
6921
+ });
6922
+ return violations;
6923
+ }
6924
+ function harmonyViolations(melody, plan, timeline, lines) {
6925
+ const misfits = planHarmonyMisfits(plan, timeline);
6926
+ const violations = [];
6927
+ for (const line of lines) {
6928
+ const read = misfits(line.map((index) => melody[index]));
6929
+ read.forEach((kinds, at2) => {
6930
+ if (kinds === null) {
6931
+ return;
6932
+ }
6933
+ const note2 = melody[line[at2]];
6934
+ const derived = plan.motifs.some(
6935
+ (motif) => motif.from !== null && note2.startBeat >= motif.startBeat - BEAT_EPS && note2.startBeat < motif.endBeat - BEAT_EPS
6936
+ );
6937
+ const strong = positionClass(note2.startBeat, plan.meters) === "strong" && timeline.at(note2.startBeat) !== null;
6938
+ violations.push({
6939
+ kind: "harmony",
6940
+ severity: derived ? "warning" : "error",
6941
+ atBeat: note2.startBeat,
6942
+ expected: strong ? "a chord tone or an ornamental tone" : "a chord tone, an ornamental tone, or a scale tone",
6943
+ actual: kinds.length === 0 ? "no harmonic label" : kinds.join(", "),
6944
+ rationale: derived ? `the note at beat ${note2.startBeat} sits in a derived statement, whose transformation takes precedence over the harmony` : `the note at beat ${note2.startBeat} does not fit the planned harmony`
6945
+ });
6946
+ });
6947
+ }
6948
+ return violations;
6949
+ }
6950
+ function notesInRange(melody, startBeat, endBeat) {
6951
+ return melody.filter(
6952
+ (note2) => note2.startBeat >= startBeat - BEAT_EPS && note2.startBeat < endBeat - BEAT_EPS
6953
+ );
6954
+ }
6955
+ function keptEnd(statement, melody, plan) {
6956
+ const phrase = phraseAt(plan, statement.startBeat);
6957
+ if (phrase === void 0) {
6958
+ return statement.endBeat;
6959
+ }
6960
+ const downbeat = finalDownbeat(phrase, plan.meters);
6961
+ const held = notesInRange(melody, downbeat, phrase.endBeat).length === 1;
6962
+ return held ? Math.min(statement.endBeat, downbeat) : statement.endBeat;
6963
+ }
6964
+ function replayLevel(source, derived, statement, sourceEnd, derivedEnd, sourceCut, plan, key, closing) {
6965
+ const relation = statement.relation;
6966
+ const planned = relation.kind === "tonalTransposition" ? relation.degrees ?? 0 : relation.semitones;
6967
+ const { low, high } = plan.phrases[statement.phrase].register;
6968
+ const outside = (pitch) => pitch < low || pitch > high;
6969
+ const backward = relation.kind === "retrograde" || relation.kind === "retrogradeInversion";
6970
+ const cell = clipStatement(
6971
+ source.map((note2) => ({
6972
+ pitch: note2.pitch,
6973
+ startBeat: note2.startBeat,
6974
+ durationBeat: note2.durationBeat
6975
+ })),
6976
+ sourceEnd
6977
+ );
6978
+ const replays = relationLevels(relation.kind).map((level) => {
6979
+ const notes = clipStatement(
6980
+ replayAt(cell, relation, level, statement.startBeat, key),
6981
+ derivedEnd
6982
+ );
6983
+ return { level, notes, outside: notes.filter((note2) => outside(note2.pitch)).length };
6984
+ });
6985
+ const fewestOutside = Math.min(...replays.map((replay) => replay.outside));
6986
+ let best = null;
6987
+ for (const { level, notes: expected, outside: away } of replays) {
6988
+ const compared = sourceCut ? derived.slice(0, expected.length) : derived;
6989
+ if (expected.length !== compared.length) {
6990
+ continue;
6991
+ }
6992
+ let forced = 0;
6993
+ let holds = true;
6994
+ for (let i = 0; holds && i < expected.length; i += 1) {
6995
+ const want = expected[i];
6996
+ const got = compared[i];
6997
+ if (i > 0) {
6998
+ const gap = got.startBeat - compared[i - 1].startBeat;
6999
+ holds = Math.abs(gap - (want.startBeat - expected[i - 1].startBeat)) <= BEAT_EPS;
7000
+ }
7001
+ if (!holds || got.pitch === want.pitch) {
7002
+ continue;
7003
+ }
7004
+ const origin = source[backward ? cell.length - 1 - i : i];
7005
+ forced += 1;
7006
+ holds = closing.has(got) || origin !== void 0 && closing.has(origin) || away === fewestOutside && outside(want.pitch);
7007
+ }
7008
+ const distance = Math.abs(level - planned);
7009
+ const nearer = best === null || distance < Math.abs(best.level - planned) || distance === Math.abs(best.level - planned) && level < best.level;
7010
+ if (holds && nearer) {
7011
+ best = { level, forced };
7012
+ }
7013
+ }
7014
+ return best;
7015
+ }
7016
+ function motifDerivationViolations(melody, plan, closing) {
7017
+ const keyAt = planKeyAt(plan);
7018
+ const errors = [];
7019
+ const warnings = [];
7020
+ for (const statement of plan.motifs) {
7021
+ if (statement.from === null) {
7022
+ continue;
7023
+ }
7024
+ const source = plan.motifs[statement.from];
7025
+ const sourceNotes = notesInRange(melody, source.startBeat, source.endBeat);
7026
+ const derivedNotes = notesInRange(melody, statement.startBeat, statement.endBeat);
7027
+ const relation = statement.relation;
7028
+ const rationale = `the statement at beat ${statement.startBeat} does not derive from its source the way the plan names`;
7029
+ const sourceKept = keptEnd(source, melody, plan);
7030
+ const derivedKept = keptEnd(statement, melody, plan);
7031
+ if (sourceKept <= source.startBeat + BEAT_EPS || derivedKept <= statement.startBeat + BEAT_EPS) {
7032
+ continue;
7033
+ }
7034
+ if (sourceNotes.length === 0 || derivedNotes.length === 0) {
7035
+ errors.push({
7036
+ kind: "motifDerivation",
7037
+ severity: "error",
7038
+ atBeat: statement.startBeat,
7039
+ expected: relation === null ? "a variation" : relation.kind,
7040
+ actual: "no notes to compare",
7041
+ rationale
7042
+ });
7043
+ continue;
7044
+ }
7045
+ const sourceCut = sourceKept < source.endBeat - BEAT_EPS;
7046
+ const derivedCut = derivedKept < statement.endBeat - BEAT_EPS;
7047
+ if (relation === null) {
7048
+ const sourceEnd = derivedCut ? Math.min(sourceKept, source.startBeat + derivedKept - statement.startBeat) : sourceKept;
7049
+ const derivedEnd = sourceCut ? Math.min(derivedKept, statement.startBeat + sourceKept - source.startBeat) : derivedKept;
7050
+ const sourceLine = notesInRange(melody, source.startBeat, sourceEnd);
7051
+ const derivedLine = notesInRange(melody, statement.startBeat, derivedEnd);
7052
+ if (sourceLine.length === 0 || derivedLine.length === 0) {
7053
+ continue;
7054
+ }
7055
+ const similarity = melodicSimilarity(sourceLine, derivedLine);
7056
+ if (similarity < DEFAULT_VARIATION_THRESHOLD) {
7057
+ errors.push({
7058
+ kind: "motifDerivation",
7059
+ severity: "error",
7060
+ atBeat: statement.startBeat,
7061
+ expected: `a variation (melodicSimilarity >= ${DEFAULT_VARIATION_THRESHOLD})`,
7062
+ actual: `melodicSimilarity ${similarity.toFixed(3)}`,
7063
+ rationale
7064
+ });
7065
+ }
7066
+ continue;
7067
+ }
7068
+ const kept = notesInRange(melody, source.startBeat, sourceKept);
7069
+ const backward = relation.kind === "retrograde" || relation.kind === "retrogradeInversion";
7070
+ if (kept.length === 0 || sourceCut && backward) {
7071
+ continue;
7072
+ }
7073
+ const replay = replayLevel(
7074
+ kept,
7075
+ notesInRange(melody, statement.startBeat, derivedKept),
7076
+ statement,
7077
+ sourceKept,
7078
+ derivedKept,
7079
+ sourceCut,
7080
+ plan,
7081
+ scaleOf(keyAt(statement.startBeat)),
7082
+ closing
7083
+ );
7084
+ const planned = relation.kind === "tonalTransposition" ? relation.degrees ?? 0 : relation.semitones;
7085
+ if (replay === null) {
7086
+ errors.push({
7087
+ kind: "motifDerivation",
7088
+ severity: "error",
7089
+ atBeat: statement.startBeat,
7090
+ expected: relation.kind,
7091
+ actual: "no pitch level replays the source",
7092
+ rationale
7093
+ });
7094
+ } else if (replay.level !== planned || replay.forced > 0) {
7095
+ warnings.push({
7096
+ kind: "motifDisplaced",
7097
+ severity: "warning",
7098
+ atBeat: statement.startBeat,
7099
+ expected: `${relation.kind} at level ${planned} with every pitch replayed`,
7100
+ actual: `level ${replay.level}, ${replay.forced} excused note(s)`,
7101
+ rationale: `the statement at beat ${statement.startBeat} carries out its planned derivation at another pitch level or with notes excused`
7102
+ });
7103
+ }
7104
+ }
7105
+ return { errors, warnings };
7106
+ }
7107
+ function phraseAt(plan, beat) {
7108
+ return plan.phrases.find(
7109
+ (phrase) => beat >= phrase.startBeat - BEAT_EPS && beat < phrase.endBeat - BEAT_EPS
7110
+ );
7111
+ }
7112
+ function registerViolations(melody, plan) {
7113
+ const violations = [];
7114
+ for (const note2 of melody) {
7115
+ const phrase = phraseAt(plan, note2.startBeat);
7116
+ if (phrase === void 0) {
7117
+ continue;
7118
+ }
7119
+ if (note2.pitch < phrase.register.low || note2.pitch > phrase.register.high) {
7120
+ violations.push({
7121
+ kind: "register",
7122
+ severity: "error",
7123
+ atBeat: note2.startBeat,
7124
+ expected: `a pitch within [${phrase.register.low}, ${phrase.register.high}]`,
7125
+ actual: `pitch ${note2.pitch}`,
7126
+ rationale: `the note at beat ${note2.startBeat} falls outside its phrase's planned register`
7127
+ });
7128
+ }
7129
+ }
7130
+ return violations;
7131
+ }
7132
+ function planOutline(phrase) {
7133
+ const amplitude = (phrase.register.high - phrase.register.low) / 2;
7134
+ const outline = [];
7135
+ for (let k = 0; k < OUTLINE_POINTS; k += 1) {
7136
+ const t = (k + 0.5) / OUTLINE_POINTS;
7137
+ outline.push(
7138
+ phrase.register.mean + plannedContourAt(phrase.shape, phrase.peakPosition, t) * amplitude
7139
+ );
7140
+ }
7141
+ return outline;
7142
+ }
7143
+ function planContourPhrase(phrase) {
7144
+ return {
7145
+ shape: phrase.shape,
7146
+ low: phrase.register.low,
7147
+ high: phrase.register.high,
7148
+ outline: planOutline(phrase)
7149
+ };
7150
+ }
7151
+ function planRegisterPlan(plan) {
7152
+ const phrases = plan.phrases.map((phrase) => ({
7153
+ low: phrase.register.low,
7154
+ high: phrase.register.high,
7155
+ mean: phrase.register.mean,
7156
+ peakPosition: phrase.peakPosition
7157
+ }));
7158
+ let weighted = 0;
7159
+ let weight = 0;
7160
+ for (const phrase of plan.phrases) {
7161
+ const length = phrase.endBeat - phrase.startBeat;
7162
+ weighted += phrase.register.mean * length;
7163
+ weight += length;
7164
+ }
7165
+ return { phrases, mean: weight > 0 ? weighted / weight : null };
7166
+ }
7167
+ function planRhythmReading(plan) {
7168
+ const hasOnsets = plan.rhythm.onsetLevels.some((share) => share > 0);
7169
+ return {
7170
+ onsets: hasOnsets ? 1 : 0,
7171
+ onsetLevels: plan.rhythm.onsetLevels,
7172
+ barPositions: [],
7173
+ interOnsetShares: plan.rhythm.interOnsetShares,
7174
+ syncopation: plan.rhythm.syncopation
7175
+ };
7176
+ }
7177
+ function peakPositionWarnings(plan, readings) {
7178
+ const violations = [];
7179
+ plan.phrases.forEach((phrase, index) => {
7180
+ const reading = readings[index];
7181
+ if (reading == null) {
7182
+ return;
7183
+ }
7184
+ const drift = Math.abs(reading.peakPosition - phrase.peakPosition);
7185
+ if (drift > PEAK_POSITION_TOLERANCE) {
7186
+ violations.push({
7187
+ kind: "peakPosition",
7188
+ severity: "warning",
7189
+ atBeat: phrase.startBeat,
7190
+ expected: `a peak position within ${PEAK_POSITION_TOLERANCE} of ${phrase.peakPosition}`,
7191
+ actual: `${reading.peakPosition}`,
7192
+ rationale: `the phrase starting at beat ${phrase.startBeat} places its melodic peak further from the planned position than the tolerance allows`
7193
+ });
7194
+ }
7195
+ });
7196
+ return violations;
7197
+ }
7198
+ function lineMeanOf(melody) {
7199
+ let weighted = 0;
7200
+ let total = 0;
7201
+ for (const note2 of melody) {
7202
+ weighted += note2.pitch * note2.durationBeat;
7203
+ total += note2.durationBeat;
7204
+ }
7205
+ return total > 0 ? weighted / total : null;
7206
+ }
7207
+ function planDensity(plan) {
7208
+ let weighted = 0;
7209
+ let bars = 0;
7210
+ for (const phrase of plan.phrases) {
7211
+ const span = barSpanOf(phrase.startBeat, phrase.endBeat, plan.meters);
7212
+ weighted += phrase.onsetDensity * span;
7213
+ bars += span;
7214
+ }
7215
+ return { onsets: 1, onsetDensity: bars > 0 ? weighted / bars : 0 };
7216
+ }
7217
+ function evaluateComposition(melody, plan, opts) {
7218
+ assertCompositionPlan(plan, "evaluated plan");
7219
+ const { budget } = assertOptions(opts, "evaluate options");
7220
+ const notes = [
7221
+ ...assertNoteEvents(melody, "evaluated melody", { budget, allowNonPositiveDuration: true })
7222
+ ].sort((a, b) => a.startBeat - b.startBeat);
7223
+ const timeline = planChordTimeline(plan);
7224
+ const lines = phraseLines(notes, plan);
7225
+ const closing = new Set(
7226
+ lines.flatMap((line) => line.length === 0 ? [] : [notes[line.at(-1)]])
7227
+ );
7228
+ const derivations = motifDerivationViolations(notes, plan, closing);
7229
+ const lineMean = lineMeanOf(notes);
7230
+ const readings = plan.phrases.map(
7231
+ (phrase) => phraseMelody(notes, phrase.startBeat, phrase.endBeat, lineMean ?? 0, plan.meters, budget)
7232
+ );
7233
+ const violations = [
7234
+ ...spanViolation(notes, plan),
7235
+ ...phraseBoundaryViolations(notes, plan),
7236
+ ...cadenceViolations(notes, plan, timeline, lines),
7237
+ ...harmonyViolations(notes, plan, timeline, lines),
7238
+ ...registerViolations(notes, plan),
7239
+ ...derivations.errors,
7240
+ ...derivations.warnings,
7241
+ ...peakPositionWarnings(plan, readings)
7242
+ ];
7243
+ const rhythm = analyzeRhythm(notes, {
7244
+ meters: plan.meters,
7245
+ totalBeats: plan.span.endBeat,
7246
+ budget
7247
+ });
7248
+ const target = planRhythmReading(plan);
7249
+ const fit = {
7250
+ contour: contourSimilarity(plan.phrases.map(planContourPhrase), readings),
7251
+ register: registerSimilarity(planRegisterPlan(plan), { phrases: readings, mean: lineMean }),
7252
+ onset: onsetSimilarity(target, rhythm),
7253
+ duration: durationSimilarity(target, rhythm),
7254
+ syncopation: syncopationSimilarity(target, rhythm),
7255
+ density: densitySimilarity(planDensity(plan), rhythm)
7256
+ };
7257
+ return { violations, fit };
7258
+ }
7259
+
7260
+ // src/generate/progression/index.ts
7261
+ var DEFAULT_TS6 = { numerator: 4, denominator: 4 };
7262
+ var PROG_STYLES = Object.freeze(["minimal", "dance", "idol", "rock"]);
7263
+ var BORROWED_DEGREES = Object.freeze({
7264
+ /** Flat submediant: bVI. */
7265
+ bVI: 8,
7266
+ /** Flat subtonic: bVII. */
7267
+ bVII: 10,
7268
+ /** Flat mediant: bIII. */
7269
+ bIII: 11,
7270
+ /** Minor subdominant borrowed from the parallel minor: iv. */
7271
+ iv: 12,
7272
+ /** Neapolitan: bII. */
7273
+ bII: 13,
7274
+ /** Sharp subdominant, the diminished #IV. */
7275
+ sharpIV: 14
7276
+ });
4981
7277
  var PRESETS = [
4982
7278
  {
4983
7279
  id: "fourChordPop",
@@ -5158,12 +7454,12 @@ function degreeToRootPc(degree, key) {
5158
7454
  const offset = BORROWED_OFFSET[degree] ?? 0;
5159
7455
  return ((key.rootPc % 12 + offset) % 12 + 12) % 12;
5160
7456
  }
5161
- function autoQuality(degree, key, harmonicDominant) {
7457
+ function autoQuality(degree, key, tonic, harmonicDominant) {
5162
7458
  if (degree >= 1 && degree <= 7) {
5163
- if (harmonicDominant && isMinorKey(key) && degree === 5) {
7459
+ if (harmonicDominant && cadencesThroughRaisedSeventh(key) && degree === 5) {
5164
7460
  return "maj";
5165
7461
  }
5166
- return diatonicTriad(degree, heptatonicFrameOf(key)).quality;
7462
+ return diatonicTriad(degree, { ...heptatonicFrameOf(key), tonic }).quality;
5167
7463
  }
5168
7464
  if (degree === 14) {
5169
7465
  return "dim";
@@ -5173,11 +7469,11 @@ function autoQuality(degree, key, harmonicDominant) {
5173
7469
  }
5174
7470
  return "maj";
5175
7471
  }
5176
- function resolveCycle(degrees, key, ext, harmonicDominant) {
7472
+ function resolveCycle(degrees, key, tonic, ext, harmonicDominant) {
5177
7473
  const steps = degrees.map((degree) => {
5178
7474
  const step = {
5179
7475
  rootPc: degreeToRootPc(degree, key),
5180
- quality: ext !== void 0 && ext !== "auto" ? ext : autoQuality(degree, key, harmonicDominant)
7476
+ quality: ext !== void 0 && ext !== "auto" ? ext : autoQuality(degree, key, tonic, harmonicDominant)
5181
7477
  };
5182
7478
  if (degree >= 1 && degree <= 7) {
5183
7479
  step.degree = degree;
@@ -5210,17 +7506,22 @@ function generateProgression(opts) {
5210
7506
  assertRecord(opts, "progression options");
5211
7507
  assertPositiveInt(opts.bars, "progression bars");
5212
7508
  assertGenerationBudget(opts.bars, "progression chords");
5213
- const key = toKeyScale(opts.key);
7509
+ const { scale: key, tonic } = resolveKey(opts.key);
5214
7510
  const ctx = resolveContext(opts.ctx);
5215
7511
  let preset;
5216
7512
  if (opts.preset !== void 0) {
5217
- if (opts.preset.degrees.length === 0) {
7513
+ const custom = assertRecord(
7514
+ opts.preset,
7515
+ "progression preset"
7516
+ );
7517
+ const degrees = assertArray(custom.degrees, "progression preset degrees");
7518
+ if (degrees.length === 0) {
5218
7519
  throw new InvalidInputError("progression preset must name at least one degree");
5219
7520
  }
5220
7521
  preset = {
5221
- id: opts.preset.id ?? "custom",
5222
- name: opts.preset.name ?? "Custom",
5223
- degrees: opts.preset.degrees.map((degree, index) => {
7522
+ id: custom.id ?? "custom",
7523
+ name: custom.name ?? "Custom",
7524
+ degrees: degrees.map((degree, index) => {
5224
7525
  assertDegree(degree, `progression preset degrees[${index}]`);
5225
7526
  const supported = degree >= 1 && degree <= 7 || Object.values(BORROWED_DEGREES).includes(degree);
5226
7527
  if (!supported) {
@@ -5230,8 +7531,12 @@ function generateProgression(opts) {
5230
7531
  }
5231
7532
  return degree;
5232
7533
  }),
5233
- functional: opts.preset.functional ?? "loop",
5234
- styles: opts.preset.styles ?? [opts.style]
7534
+ functional: assertOneOf(
7535
+ custom.functional ?? "loop",
7536
+ ["loop", "tensionBuild", "cadenceStrong", "stable"],
7537
+ "progression preset functional"
7538
+ ),
7539
+ styles: custom.styles ?? [opts.style]
5235
7540
  };
5236
7541
  }
5237
7542
  if (preset === void 0 && opts.presetId !== void 0) {
@@ -5245,6 +7550,7 @@ function generateProgression(opts) {
5245
7550
  const cycle = resolveCycle(
5246
7551
  preset?.degrees ?? [0],
5247
7552
  key,
7553
+ tonic,
5248
7554
  ext,
5249
7555
  preset?.functional === "cadenceStrong"
5250
7556
  );
@@ -5277,10 +7583,11 @@ function generateProgression(opts) {
5277
7583
  const domRoot = (next.rootPc + 7) % 12;
5278
7584
  const targetsTonic = (next.rootPc - tonicPc + 12) % 12 === 0;
5279
7585
  const isLastTonicStatement = cur.rootPc === tonicPc && tonicStatements <= 1;
5280
- const alreadySecondary = cur.rootPc === domRoot && cur.quality === "dom7";
7586
+ const alreadySecondary = cur.rootPc === domRoot && soundsDominantSeventh(makeChord(cur.rootPc, cur.quality));
7587
+ const isNativeDominant = isNativeDominantSeventh(domRoot, key);
5281
7588
  const isResolutionTarget = chords[i - 1]?.secondaryDominant === true;
5282
7589
  const takesNoDominant = !canBeTonicized(next, tonicizable);
5283
- if (targetsTonic || isLastTonicStatement || alreadySecondary || isResolutionTarget || takesNoDominant) {
7590
+ if (targetsTonic || isLastTonicStatement || alreadySecondary || isResolutionTarget || takesNoDominant || isNativeDominant) {
5284
7591
  continue;
5285
7592
  }
5286
7593
  if (draw.prob(harmonic, "reharmonize", i)) {
@@ -5318,11 +7625,11 @@ function triadQualityOf(chord) {
5318
7625
  }
5319
7626
  return hasThird;
5320
7627
  }
5321
- function diatonicTriadsOf(key) {
7628
+ function diatonicTriadsOf(key, tonic) {
5322
7629
  const degrees = scaleTonesInDegreeOrder(key).length;
5323
7630
  const triads = [];
5324
7631
  for (let degree = 1; degree <= degrees; degree += 1) {
5325
- triads.push(diatonicTriad(degree, key));
7632
+ triads.push(diatonicTriad(degree, { ...key, tonic }));
5326
7633
  }
5327
7634
  return triads;
5328
7635
  }
@@ -5356,10 +7663,22 @@ function tritoneSubstituteRoot(dominant, tonic, key) {
5356
7663
  const resolution = spellPitchClass(pitchClassOf(dominant.rootPc + 5), tonic, key);
5357
7664
  return transposeNote(resolution, 1);
5358
7665
  }
7666
+ var MEDIANT_THIRDS = {
7667
+ 3: "m3",
7668
+ 4: "M3",
7669
+ 8: "-M3",
7670
+ 9: "-m3"
7671
+ };
7672
+ function mediantRoot(source, offset, tonic, key) {
7673
+ const hint = source.rootSpelling;
7674
+ const root = hint !== void 0 && noteToPitchClass(hint) === pitchClassOf(source.rootPc) ? hint : spellPitchClass(source.rootPc, tonic, key);
7675
+ return transposeByInterval(bareSpelling(root), MEDIANT_THIRDS[offset]);
7676
+ }
5359
7677
  function substituteChord(chord, key, opts) {
5360
7678
  assertOptions(opts, "opts");
5361
7679
  const source = toChordData(chord);
5362
- const { scale, tonic } = resolveKey(key);
7680
+ const resolved = resolveKey(key);
7681
+ const { scale, tonic } = resolved;
5363
7682
  const originalTriad = chordPitchClasses(makeChord(source.rootPc, triadQualityOf(source)));
5364
7683
  const candidates = [];
5365
7684
  if (soundsDominantSeventh(source)) {
@@ -5369,24 +7688,27 @@ function substituteChord(chord, key, opts) {
5369
7688
  type: "tritone"
5370
7689
  });
5371
7690
  }
5372
- for (const triad of diatonicTriadsOf(scale)) {
7691
+ for (const triad of diatonicTriadsOf(scale, tonic)) {
5373
7692
  const offset = pitchClassOf(triad.rootPc - source.rootPc);
5374
7693
  if (THIRD_OFFSETS.includes(offset) && commonToneCount(originalTriad, chordPitchClasses(triad)) === 2) {
5375
7694
  candidates.push({ chord: spelledInKey(triad, tonic, scale), type: "relative" });
5376
7695
  }
5377
7696
  }
5378
- const targetFunction = functionOf(source, scale);
7697
+ const targetFunction = functionOf(source, resolved);
5379
7698
  const parallel = parallelKey(scale);
5380
- for (const triad of diatonicTriadsOf(parallel)) {
5381
- if (functionOf(triad, scale) === targetFunction && !isDiatonic(triad, scale)) {
7699
+ for (const triad of diatonicTriadsOf(parallel, tonic)) {
7700
+ if (functionOf(triad, resolved) === targetFunction && !isDiatonic(triad, resolved)) {
5382
7701
  candidates.push({ chord: spelledInKey(triad, tonic, parallel), type: "borrowed" });
5383
7702
  }
5384
7703
  }
5385
7704
  for (const offset of THIRD_OFFSETS) {
5386
7705
  for (const quality of ["maj", "min"]) {
5387
7706
  const mediant = makeChord(pitchClassOf(source.rootPc + offset), quality);
5388
- if (commonToneCount(originalTriad, chordPitchClasses(mediant)) === 1 && !isDiatonic(mediant, scale)) {
5389
- candidates.push({ chord: spelledInKey(mediant, tonic, scale), type: "chromaticMediant" });
7707
+ if (commonToneCount(originalTriad, chordPitchClasses(mediant)) === 1 && !isDiatonic(mediant, resolved)) {
7708
+ candidates.push({
7709
+ chord: spelledOnRoot(mediant, mediantRoot(source, offset, tonic, scale), tonic, scale),
7710
+ type: "chromaticMediant"
7711
+ });
5390
7712
  }
5391
7713
  }
5392
7714
  }
@@ -5411,14 +7733,15 @@ function substituteChord(chord, key, opts) {
5411
7733
  results.push({
5412
7734
  chord: candidate.chord,
5413
7735
  type: candidate.type,
5414
- roman: chordToRoman(candidate.chord, scale),
5415
- function: functionOf(candidate.chord, scale)
7736
+ roman: chordToRoman(candidate.chord, resolved),
7737
+ function: functionOf(candidate.chord, resolved)
5416
7738
  });
5417
7739
  }
5418
7740
  return results;
5419
7741
  }
5420
7742
  function modalInterchangePalette(key) {
5421
- const { scale, tonic } = resolveKey(key);
7743
+ const resolved = resolveKey(key);
7744
+ const { scale, tonic } = resolved;
5422
7745
  const palette = [];
5423
7746
  const seen = /* @__PURE__ */ new Set();
5424
7747
  const add = (chord) => {
@@ -5429,18 +7752,18 @@ function modalInterchangePalette(key) {
5429
7752
  seen.add(dedupeKey);
5430
7753
  palette.push({
5431
7754
  chord,
5432
- roman: chordToRoman(chord, scale),
5433
- source: borrowedSource(chord, scale)
7755
+ roman: chordToRoman(chord, resolved),
7756
+ source: borrowedSource(chord, resolved)
5434
7757
  });
5435
7758
  };
5436
7759
  const parallel = parallelKey(scale);
5437
- for (const triad of diatonicTriadsOf(parallel)) {
5438
- if (!isDiatonic(triad, scale) && borrowedSource(triad, scale) !== null) {
7760
+ for (const triad of diatonicTriadsOf(parallel, tonic)) {
7761
+ if (!isDiatonic(triad, resolved) && borrowedSource(triad, resolved) !== null) {
5439
7762
  add(spelledInKey(triad, tonic, parallel));
5440
7763
  }
5441
7764
  }
5442
7765
  const neapolitan = makeChord(pitchClassOf(scale.rootPc + 1), "maj");
5443
- if (!isDiatonic(neapolitan, scale)) {
7766
+ if (!isDiatonic(neapolitan, resolved)) {
5444
7767
  add(spelledOnRoot(neapolitan, transposeNote(tonic, 1), tonic, scale));
5445
7768
  }
5446
7769
  return palette;
@@ -5469,12 +7792,13 @@ var DEFAULT_RHYTHM_VELOCITY = 96;
5469
7792
  function rhythmToNoteEvents(events, pitch, velocity = DEFAULT_RHYTHM_VELOCITY) {
5470
7793
  assertMidiPitch(pitch, "pitch");
5471
7794
  assertInteger(velocity, "velocity", 0, 127);
5472
- return assertArray(events, "events").map((event) => ({
5473
- pitch,
5474
- startBeat: event.position,
5475
- durationBeat: event.duration,
5476
- velocity
5477
- }));
7795
+ return assertArray(events, "events").map((given, index) => {
7796
+ const event = assertRecord(given, `events[${index}]`);
7797
+ const bound = Number.MAX_SAFE_INTEGER;
7798
+ assertRange(event.position, -bound, bound, `events[${index}].position`);
7799
+ assertRange(event.duration, -bound, bound, `events[${index}].duration`);
7800
+ return { pitch, startBeat: event.position, durationBeat: event.duration, velocity };
7801
+ });
5478
7802
  }
5479
7803
  var DEFAULT_SUBDIVISION = 2;
5480
7804
  var DEFAULT_DENSITY = 0.5;
@@ -5569,10 +7893,14 @@ export {
5569
7893
  thin,
5570
7894
  double,
5571
7895
  halfTime,
7896
+ halfTimeFigure,
5572
7897
  doubleTime,
7898
+ doubleTimeFigure,
5573
7899
  syncopate,
5574
7900
  ornamentBy,
5575
7901
  deform,
7902
+ resolveDials,
7903
+ deformFigure,
5576
7904
  withinCeiling,
5577
7905
  isLickMaterial,
5578
7906
  BASS_LICKS,
@@ -5603,8 +7931,14 @@ export {
5603
7931
  motifToNoteEvents,
5604
7932
  transformMotif,
5605
7933
  developMotif,
7934
+ COMPOSITION_PLAN_VERSION,
7935
+ assertCompositionPlan,
7936
+ planTimeline,
7937
+ generateMelody,
5606
7938
  ORNAMENT_STYLES,
5607
7939
  ornament,
7940
+ deriveCompositionPlan,
7941
+ evaluateComposition,
5608
7942
  BORROWED_DEGREES,
5609
7943
  progressions,
5610
7944
  progressionsByStyle,
@@ -5618,4 +7952,4 @@ export {
5618
7952
  generateRhythm,
5619
7953
  rhythmDensity
5620
7954
  };
5621
- //# sourceMappingURL=chunk-Q6LFSAKV.js.map
7955
+ //# sourceMappingURL=chunk-FK3DGAIZ.js.map