@libraz/libcantus 1.3.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 (130) hide show
  1. package/NOTICE +63 -0
  2. package/README.md +3 -1
  3. package/README_ja.md +13 -11
  4. package/dist/analyze/index.cjs +7 -7
  5. package/dist/analyze/index.cjs.map +1 -1
  6. package/dist/analyze/index.d.cts +14 -12
  7. package/dist/analyze/index.d.ts +14 -12
  8. package/dist/analyze/index.js +6 -6
  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-YFA7X2F6.cjs → chunk-3MDOEFIQ.cjs} +411 -327
  14. package/dist/chunk-3MDOEFIQ.cjs.map +1 -0
  15. package/dist/{chunk-IPEF6NWB.js → chunk-5MWRBYAT.js} +278 -154
  16. package/dist/chunk-5MWRBYAT.js.map +1 -0
  17. package/dist/{chunk-B2PW6EHN.cjs → chunk-6TTBEON3.cjs} +1253 -889
  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-PWC4CRWT.cjs → chunk-E3QO3UET.cjs} +31 -23
  22. package/dist/chunk-E3QO3UET.cjs.map +1 -0
  23. package/dist/{chunk-GNVN4PPJ.js → chunk-FJFULJZE.js} +531 -213
  24. package/dist/chunk-FJFULJZE.js.map +1 -0
  25. package/dist/{chunk-CPMB7P75.js → chunk-FK3DGAIZ.js} +608 -244
  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-4R2LWV7E.cjs → chunk-HYSVSFNI.cjs} +664 -540
  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-IFZDWIKM.cjs → chunk-LQZQDBCD.cjs} +1048 -730
  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-MLBPXA4Z.js → chunk-UTE5A36R.js} +198 -114
  46. package/dist/chunk-UTE5A36R.js.map +1 -0
  47. package/dist/{chunk-ZBUROTJQ.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 +8 -8
  67. package/dist/generate/index.cjs.map +1 -1
  68. package/dist/generate/index.d.cts +28 -18
  69. package/dist/generate/index.d.ts +28 -18
  70. package/dist/generate/index.js +7 -7
  71. package/dist/{index-BMweUlm5.d.cts → index-5VJgKz_Q.d.cts} +6 -2
  72. package/dist/{index-BHIUyjhZ.d.ts → index-BLuDesVG.d.ts} +20 -13
  73. package/dist/{index-B9QLziDe.d.cts → index-BV6APVa6.d.cts} +20 -13
  74. package/dist/{index-CL4b4gUQ.d.ts → 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-CY-3NVfy.d.cts → index-DVInzCDS.d.cts} +53 -29
  80. package/dist/{index-9eoQs2C1.d.ts → index-Dg-SAfhz.d.ts} +6 -4
  81. package/dist/{index-zaZdmnU4.d.ts → index-jgeug9EA.d.ts} +53 -29
  82. package/dist/{index-cHvbYgJI.d.cts → index-pQihL4z7.d.cts} +6 -4
  83. package/dist/index.cjs +13 -13
  84. package/dist/index.cjs.map +1 -1
  85. package/dist/index.d.cts +11 -11
  86. package/dist/index.d.ts +11 -11
  87. package/dist/index.js +12 -12
  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 +63 -48
  93. package/dist/model/index.d.ts +63 -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/{profile-DLdlIpS9.d.ts → profile-B4WqcM4H.d.ts} +25 -17
  98. package/dist/{profile-CQMEqdH9.d.cts → profile-BJd1XkPB.d.cts} +25 -17
  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/{types-D-XgxGvp.d.cts → types-7mGFwvMm.d.ts} +45 -22
  105. package/dist/{types-Dh7C064C.d.ts → types-_ofpb2be.d.cts} +45 -22
  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-4R2LWV7E.cjs.map +0 -1
  110. package/dist/chunk-76ZBTRG2.cjs.map +0 -1
  111. package/dist/chunk-7KG3RTYS.js.map +0 -1
  112. package/dist/chunk-AGOVW7U6.js.map +0 -1
  113. package/dist/chunk-B2PW6EHN.cjs.map +0 -1
  114. package/dist/chunk-CGBADIO5.cjs.map +0 -1
  115. package/dist/chunk-CPMB7P75.js.map +0 -1
  116. package/dist/chunk-GNVN4PPJ.js.map +0 -1
  117. package/dist/chunk-IFZDWIKM.cjs.map +0 -1
  118. package/dist/chunk-IPEF6NWB.js.map +0 -1
  119. package/dist/chunk-M5RYMAFI.js.map +0 -1
  120. package/dist/chunk-MLBPXA4Z.js.map +0 -1
  121. package/dist/chunk-NB25P3EH.js.map +0 -1
  122. package/dist/chunk-NM7UAMC3.cjs.map +0 -1
  123. package/dist/chunk-PM4XJE2Y.cjs.map +0 -1
  124. package/dist/chunk-PUKJP2Z5.js.map +0 -1
  125. package/dist/chunk-PWC4CRWT.cjs.map +0 -1
  126. package/dist/chunk-RFIBF2RR.js.map +0 -1
  127. package/dist/chunk-S5BKZZGM.cjs.map +0 -1
  128. package/dist/chunk-UMWZ4ZET.js.map +0 -1
  129. package/dist/chunk-YFA7X2F6.cjs.map +0 -1
  130. package/dist/chunk-ZBUROTJQ.js.map +0 -1
@@ -20,7 +20,6 @@ import {
20
20
  functionOf,
21
21
  ioiBinIndex,
22
22
  isDiatonic,
23
- isMinorKey,
24
23
  keyLookup,
25
24
  melodicSimilarity,
26
25
  onsetSimilarity,
@@ -30,7 +29,7 @@ import {
30
29
  rhythmLevel,
31
30
  romanToChord,
32
31
  syncopationSimilarity
33
- } from "./chunk-GNVN4PPJ.js";
32
+ } from "./chunk-FJFULJZE.js";
34
33
  import {
35
34
  ALGORITHM_VERSION,
36
35
  LIMBS,
@@ -42,16 +41,20 @@ import {
42
41
  includeAt,
43
42
  instrumentRange,
44
43
  resolveAlgorithmVersion,
44
+ scaleUnitDraw,
45
45
  toStringedProfile
46
- } from "./chunk-7KG3RTYS.js";
46
+ } from "./chunk-VL5NXVCN.js";
47
47
  import {
48
- createNoteEventIndex
49
- } from "./chunk-PUKJP2Z5.js";
48
+ canonicalNoteOrder,
49
+ createNoteEventIndex,
50
+ soundingIndices
51
+ } from "./chunk-TXFWSU5D.js";
50
52
  import {
51
53
  chordScales,
52
54
  roleOf
53
- } from "./chunk-NB25P3EH.js";
55
+ } from "./chunk-2G5MCGYU.js";
54
56
  import {
57
+ cadencesThroughRaisedSeventh,
55
58
  chordPitchClasses,
56
59
  chordQualities,
57
60
  chordToneRole,
@@ -63,6 +66,7 @@ import {
63
66
  fifthPcOf,
64
67
  heptatonicFrameOf,
65
68
  isForbiddenMelodicLeap,
69
+ isNativeDominantSeventh,
66
70
  isScaleTone,
67
71
  leadingTonePcOf,
68
72
  majorKey,
@@ -75,7 +79,6 @@ import {
75
79
  scaleLadderPitch,
76
80
  scaleLadderPosition,
77
81
  scaleOf,
78
- scaleSystemOf,
79
82
  scaleTonesInDegreeOrder,
80
83
  shiftByScaleDegrees,
81
84
  soundsDominantSeventh,
@@ -84,7 +87,7 @@ import {
84
87
  toChordData,
85
88
  toKeyScale,
86
89
  tonicizableDegrees
87
- } from "./chunk-RFIBF2RR.js";
90
+ } from "./chunk-H7FVMBUC.js";
88
91
  import {
89
92
  ARTICULATIONS,
90
93
  BEAT_EPS,
@@ -109,6 +112,7 @@ import {
109
112
  assertRange,
110
113
  assertRecord,
111
114
  assertTimeSignature,
115
+ barIndexAt,
112
116
  barStartBeat,
113
117
  beatsPerBar,
114
118
  beatsPerBarAt,
@@ -130,7 +134,7 @@ import {
130
134
  toSpelledInterval,
131
135
  transposeByInterval,
132
136
  transposeNote
133
- } from "./chunk-AGOVW7U6.js";
137
+ } from "./chunk-YYVXRV3V.js";
134
138
 
135
139
  // src/generate/context/draw.ts
136
140
  var DRAW_METHODS = ["at", "prob", "range", "float"];
@@ -185,7 +189,7 @@ function drawsFrom(rng, ...prefix) {
185
189
  );
186
190
  }
187
191
  assertFiniteNumber(hi - lo, "float span");
188
- return lo + at2(...path) * (hi - lo);
192
+ return scaleUnitDraw(at2(...path), lo, hi);
189
193
  }
190
194
  };
191
195
  }
@@ -410,7 +414,9 @@ var GENRES = Object.freeze([
410
414
  ]);
411
415
  var PROVENANCE_BASES = Object.freeze(["idiom", "construction", "traditional"]);
412
416
  function sameTs(a, b) {
413
- 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]));
414
420
  }
415
421
  function fitsQuery(entry, query) {
416
422
  const candidate = assertRecord(entry, "vocabulary entry");
@@ -571,6 +577,14 @@ function sustainsShift(semitones, stepBeats, bpm, difficulty) {
571
577
 
572
578
  // src/generate/context/index.ts
573
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
+ }
574
588
  var VERSION_SEGMENT = "v";
575
589
  function resolveContext(input) {
576
590
  if (input !== null && typeof input !== "number" && input !== void 0) {
@@ -581,18 +595,16 @@ function resolveContext(input) {
581
595
  }
582
596
  const ctx = typeof input === "number" ? { seed: input } : input ?? { seed: DEFAULT_SEED };
583
597
  const seed = assertInteger(ctx.seed ?? DEFAULT_SEED, "seed", 0, 4294967295);
584
- const complexity = ctx.complexity ?? {};
598
+ const complexity = nullish(ctx.complexity) ? {} : assertRecord(ctx.complexity, "ctx.complexity");
585
599
  const rhythmic = complexity.rhythmic === void 0 ? void 0 : assertRange(complexity.rhythmic, 0, 1, "complexity rhythmic");
586
600
  const harmonic = complexity.harmonic === void 0 ? void 0 : assertRange(complexity.harmonic, 0, 1, "complexity harmonic");
587
601
  const ornament2 = complexity.ornament === void 0 ? void 0 : assertRange(complexity.ornament, 0, 1, "complexity ornament");
588
602
  const difficulty = complexity.difficulty === void 0 ? void 0 : assertDifficulty(complexity.difficulty);
589
603
  const bpm = ctx.bpm === void 0 ? void 0 : assertRange(ctx.bpm, Number.MIN_VALUE, 1e3, "bpm");
590
604
  const algorithmVersion = resolveAlgorithmVersion(ctx.algorithmVersion);
591
- const supplied = ctx.rng;
592
- const instruments = ctx.instruments;
593
- const vocabulary = (ctx.vocabulary ?? []).map(
594
- (entry, index) => assertVocabulary(entry, `vocabulary[${index}]`)
595
- );
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}]`));
596
608
  return {
597
609
  seed,
598
610
  rhythmic,
@@ -601,7 +613,7 @@ function resolveContext(input) {
601
613
  difficulty,
602
614
  bpm,
603
615
  algorithmVersion,
604
- instrument: (part) => instruments?.[part],
616
+ instrument: (part) => instruments !== void 0 && Object.hasOwn(instruments, part) ? instruments[part] : void 0,
605
617
  vocabulary,
606
618
  part: (name) => supplied ? drawsFrom(supplied, name, VERSION_SEGMENT, algorithmVersion) : drawsFrom(createPositionalRng(deriveSeed(seed, name, VERSION_SEGMENT, algorithmVersion)))
607
619
  };
@@ -753,6 +765,7 @@ function bassRegister(named, given, octave) {
753
765
  var BAR_STEPS = 16;
754
766
  var BEAT_STEPS = 4;
755
767
  var STEP_BEATS = 1 / BEAT_STEPS;
768
+ var FIGURE_RATES = ["straight", "half", "double"];
756
769
  function assertGridEvents(events) {
757
770
  assertArray(events, "figure events");
758
771
  events.forEach((event, index) => {
@@ -767,6 +780,16 @@ function assertSpanSteps(spanSteps) {
767
780
  function streamOf(event) {
768
781
  return event.limb ?? event.voice ?? "";
769
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
+ }
770
793
  var FIGURE_TS = { numerator: 4, denominator: 4 };
771
794
  function subdivisionBeats(ts) {
772
795
  const pulse2 = pulseBeats(ts);
@@ -789,46 +812,57 @@ function stepWeight(step, ts) {
789
812
  function thin(events, amount, ts) {
790
813
  const figure = assertGridEvents(events);
791
814
  assertRange(amount, 0, 1, "thin amount");
792
- const signature = meterAt(0, toMeterData(ts, "ts"));
815
+ return thinFigure(figure, amount, meterAt(0, toMeterData(ts, "ts")));
816
+ }
817
+ function thinFigure(figure, amount, ts) {
793
818
  const floor = Math.min(4, Math.floor(amount * 5));
794
- return figure.filter((event) => stepWeight(event.step, signature) >= floor);
819
+ return figure.filter((event) => stepWeight(event.step, ts) >= floor);
795
820
  }
796
821
  function double(events, spanSteps = BAR_STEPS) {
797
822
  assertSpanSteps(spanSteps);
798
823
  const sorted = [...assertGridEvents(events)].sort((a, b) => a.step - b.step);
799
- const out = [];
800
- 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) {
801
827
  const event = sorted[i];
802
- if (!event) {
803
- continue;
804
- }
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) => {
805
834
  out.push(event);
806
- const next = sorted[i + 1]?.step ?? spanSteps;
835
+ const next = nextStep[i] ?? spanSteps;
807
836
  const mid = event.step + (next - event.step) / 2;
808
837
  if (Number.isInteger(mid) && mid > event.step && mid < spanSteps) {
809
838
  out.push({ ...event, step: mid, velocity: event.velocity * ADDED_NOTE_VELOCITY });
810
839
  }
811
- }
812
- return out;
840
+ });
841
+ return out.sort((a, b) => a.step - b.step);
813
842
  }
814
843
  var ADDED_NOTE_VELOCITY = 0.75;
815
844
  function halfTime(events, spanSteps = BAR_STEPS) {
816
845
  assertSpanSteps(spanSteps);
817
- 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);
818
850
  }
819
851
  function doubleTime(events, spanSteps = BAR_STEPS) {
820
852
  assertSpanSteps(spanSteps);
821
- assertGridEvents(events);
853
+ return doubleTimeFigure(assertGridEvents(events), spanSteps);
854
+ }
855
+ function doubleTimeFigure(figure, spanSteps) {
822
856
  const merged = /* @__PURE__ */ new Map();
823
857
  const place = (event, step, velocity) => {
824
- const key = `${streamOf(event)}@${step}`;
858
+ const key = positionKey(event, step);
825
859
  const held = merged.get(key);
826
860
  if (held === void 0 || velocity > held.velocity) {
827
861
  merged.set(key, { ...event, step, velocity });
828
862
  }
829
863
  };
830
- for (const event of events) {
831
- const step = Math.round(event.step / 2);
864
+ for (const event of figure) {
865
+ const step = halvedStep(event.step);
832
866
  place(event, step, event.velocity);
833
867
  const repeated = step + spanSteps / 2;
834
868
  if (repeated < spanSteps) {
@@ -845,14 +879,17 @@ function syncopate(events, amount, draw, ...path) {
845
879
  const dial = asked === void 0 ? amount : asked.amount;
846
880
  const signature = meterAt(0, toMeterData(asked?.ts ?? FIGURE_TS, "ts"));
847
881
  assertRange(dial, 0, 1, "syncopate amount");
848
- 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)));
849
886
  const out = [...figure];
850
887
  for (const event of figure) {
851
888
  const anticipated = event.step - 1;
852
- 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))) {
853
890
  continue;
854
891
  }
855
- if (!sampler.prob(dial, ...path, "syncopate", event.step)) {
892
+ if (!sampler.prob(dial, ...path, "syncopate", ...drawKey(event))) {
856
893
  continue;
857
894
  }
858
895
  out.push({ ...event, step: anticipated, velocity: event.velocity * ADDED_NOTE_VELOCITY });
@@ -865,35 +902,47 @@ function ornamentBy(events, isOrnament, amount, draw, ...path) {
865
902
  const sampler = assertDraw(draw);
866
903
  assertSeedPath(path);
867
904
  assertRange(amount, 0, 1, "ornament amount");
905
+ return ornamentFigure(figure, marks, amount, sampler, path);
906
+ }
907
+ function ornamentFigure(figure, marks, amount, sampler, path) {
868
908
  return figure.filter(
869
- (event) => !marks(event) || sampler.prob(amount, ...path, "ornament", event.step)
909
+ (event) => !marks(event) || sampler.prob(amount, ...path, "ornament", ...drawKey(event))
870
910
  );
871
911
  }
872
912
  var NEUTRAL_RHYTHMIC = 0.5;
873
913
  function deform(events, opts, draw, ...path) {
874
914
  const figure = assertGridEvents(events);
875
915
  const asked = assertRecord(opts, "opts");
876
- assertDraw(draw);
916
+ const sampler = assertDraw(draw);
877
917
  assertSeedPath(path);
878
918
  const spanSteps = assertSpanSteps(asked.spanSteps === void 0 ? BAR_STEPS : asked.spanSteps);
879
- const ts = asked.ts ?? FIGURE_TS;
880
- let out = asked.rate === "half" ? halfTime(figure, spanSteps) : asked.rate === "double" ? doubleTime(figure, spanSteps) : [...figure];
881
- if (asked.rhythmic !== void 0) {
882
- const rhythmic = assertRange(asked.rhythmic, 0, 1, "complexity rhythmic");
883
- if (rhythmic < NEUTRAL_RHYTHMIC) {
884
- out = thin(out, (NEUTRAL_RHYTHMIC - rhythmic) / NEUTRAL_RHYTHMIC, ts);
885
- } else if (rhythmic > NEUTRAL_RHYTHMIC) {
886
- out = syncopate(
887
- out,
888
- { amount: (rhythmic - NEUTRAL_RHYTHMIC) / NEUTRAL_RHYTHMIC, ts },
889
- draw,
890
- ...path
891
- );
892
- }
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
+ );
893
942
  }
894
- if (asked.ornament !== void 0 && asked.isOrnament) {
895
- const isOrnament = assertFunction(asked.isOrnament, "opts.isOrnament");
896
- 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);
897
946
  }
898
947
  return out.sort((a, b) => a.step - b.step);
899
948
  }
@@ -1010,6 +1059,32 @@ function isLickMaterial(material) {
1010
1059
  const candidate = material;
1011
1060
  return typeof candidate === "object" && candidate !== null && typeof candidate.lengthSteps === "number" && Array.isArray(candidate.notes);
1012
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
+ }
1013
1088
  function note(degree, step, velocity, extra) {
1014
1089
  return { degree, step, velocity, ...extra };
1015
1090
  }
@@ -1237,10 +1312,11 @@ function placeLicks(timeline, key, opts) {
1237
1312
  const supplied = asked.difficulty === void 0 ? void 0 : assertDifficulty(asked.difficulty, "lick difficulty");
1238
1313
  const difficulty = resolved.difficulty ?? supplied;
1239
1314
  const { instrument, low } = bassRegister(resolved.instrument("bass"), asked.instrument, octave);
1240
- const dictionary = mergeVocabulary(
1241
- BASS_LICKS,
1242
- vocabularyOfKind(resolved.vocabulary, isLickMaterial)
1243
- );
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);
1244
1320
  const playable = instrument === void 0 ? {} : { articulations: instrument.articulations };
1245
1321
  const raw = [];
1246
1322
  for (let index = 0; index < segments.length; index += 1) {
@@ -1672,9 +1748,10 @@ function imitate(lead, opts) {
1672
1748
  let offset = 0;
1673
1749
  if (invert) {
1674
1750
  if (tonal) {
1675
- const here = scaleLadderPosition(pitch, key);
1676
- pitch = scaleLadderPitch(2 * scaleLadderPosition(pivot, key).rung - here.rung, key);
1677
- 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;
1678
1755
  } else {
1679
1756
  pitch = 2 * pivot - pitch;
1680
1757
  }
@@ -1880,7 +1957,7 @@ function generateCounterMelody(opts) {
1880
1957
  throw new InvalidInputError("countermelody needs a timeline or a chordAt callback");
1881
1958
  }
1882
1959
  const chordChangeBeats = timeline === void 0 ? opts.chordChangeBeats ?? [] : timeline.segments.flatMap((segment) => [segment.startBeat, segment.endBeat]);
1883
- const register = opts.register ?? "below";
1960
+ const register = assertOneOf(opts.register ?? "below", ["above", "below"], "register");
1884
1961
  assertNoteEvents(opts.melody, "countermelody melody", { allowNonPositiveDuration: true });
1885
1962
  const melody = createNoteEventIndex(soundingNotesOnly(opts.melody), {
1886
1963
  tieBreak: register === "below" ? "highest" : "lowest"
@@ -2113,13 +2190,14 @@ function denser(level) {
2113
2190
  }
2114
2191
  return "sixteenth";
2115
2192
  }
2193
+ var LEVEL_ORDER = ["quarter", "eighth", "sixteenth"];
2116
2194
  function hiHatStep(level) {
2117
2195
  if (level === "sixteenth") {
2118
2196
  return 0.25;
2119
2197
  }
2120
2198
  return level === "eighth" ? 0.5 : 1;
2121
2199
  }
2122
- function getHiHatLevel(section, style, rhythmic, bpm, draw, difficulty, bar2) {
2200
+ function getHiHatBar(section, style, rhythmic, bpm, draw, difficulty, bar2) {
2123
2201
  const allow16th = bpm < HH_16TH_BPM_THRESHOLD;
2124
2202
  let base = "eighth";
2125
2203
  let dialed = true;
@@ -2159,22 +2237,41 @@ function getHiHatLevel(section, style, rhythmic, bpm, draw, difficulty, bar2) {
2159
2237
  break;
2160
2238
  }
2161
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
+ };
2162
2247
  let level = base;
2248
+ let thinnedUpTo = Number.NEGATIVE_INFINITY;
2249
+ let filledAbove = Number.POSITIVE_INFINITY;
2163
2250
  if (dialed) {
2164
- if (draw.at("hhThin", bar2) >= 2 * rhythmic) {
2251
+ const thin2 = draw.at("hhThin", bar2);
2252
+ if (thin2 >= 2 * rhythmic) {
2165
2253
  level = sparser(level);
2166
2254
  }
2167
- if (draw.at("hhFill", bar2) < 2 * rhythmic - 1) {
2255
+ const fill2 = draw.at("hhFill", bar2);
2256
+ if (fill2 < 2 * rhythmic - 1) {
2168
2257
  level = denser(level);
2169
2258
  }
2259
+ thinnedUpTo = thin2 / 2;
2260
+ filledAbove = (fill2 + 1) / 2;
2170
2261
  }
2171
- if (!allow16th && level === "sixteenth") {
2172
- level = "eighth";
2173
- }
2174
- while (level !== "quarter" && !sustainsStrokes(hiHatStep(level), bpm, difficulty)) {
2175
- level = sparser(level);
2176
- }
2177
- 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
+ };
2178
2275
  }
2179
2276
  function hiHatVelocityMultiplier(sixteenth, jitter) {
2180
2277
  switch (sixteenth) {
@@ -2385,11 +2482,12 @@ function generateKickForBeat(ctx, sec, kick) {
2385
2482
  const sixteenths = kick.length / 4;
2386
2483
  for (let step = 0; step < sixteenths; step += 1) {
2387
2484
  const index = ctx.beat * sixteenths + step;
2388
- if (!kick[index]) {
2485
+ const entry = kick[index];
2486
+ if (entry === void 0) {
2389
2487
  continue;
2390
2488
  }
2391
2489
  if (step === 0) {
2392
- ctx.track.add(GM.BD, ctx.beatTick, EIGHTH, ctx.velocity);
2490
+ ctx.track.add(GM.BD, ctx.beatTick, EIGHTH, ctx.velocity, void 0, entry);
2393
2491
  continue;
2394
2492
  }
2395
2493
  const offBeat = step % 2 === 1;
@@ -2397,7 +2495,7 @@ function generateKickForBeat(ctx, sec, kick) {
2397
2495
  continue;
2398
2496
  }
2399
2497
  const tick = swing16(ctx.beatTick + step * SIXTEENTH, sec, ctx.swingAmount);
2400
- 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);
2401
2499
  }
2402
2500
  }
2403
2501
  function roleBackbeatWeight(role) {
@@ -2450,16 +2548,19 @@ function generateGhostNotesForBeat(ctx, sec) {
2450
2548
  return;
2451
2549
  }
2452
2550
  const positions = selectGhostPositions(sec.ghostMood);
2453
- 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);
2454
2553
  if (sec.ghostBoost) {
2455
2554
  ghostProb = Math.min(1, ghostProb * 1.4);
2456
2555
  }
2457
2556
  for (const pos of positions) {
2458
2557
  const sixteenthInBeat = pos === "e" ? 1 : 3;
2459
2558
  const posProb = getGhostProbabilityAtPosition(ctx.beat, sixteenthInBeat, leadIns);
2460
- 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)) {
2461
2561
  continue;
2462
2562
  }
2563
+ const entry = sec.ornamentFollowsRhythmic ? at2 / (posProb * (sec.ghostBoost ? 1.4 : 1) * ghostDensity) - 0.5 : Number.NEGATIVE_INFINITY;
2463
2564
  const variation = ctx.draw.float(0.85, 1.15, "ghostVelocity", ctx.bar, ctx.beat, pos);
2464
2565
  const ghostBase = getGhostVelocity(ctx.section, ctx.beat / 2);
2465
2566
  let ghostVel = ctx.velocity * ghostBase * variation;
@@ -2468,7 +2569,7 @@ function generateGhostNotesForBeat(ctx, sec) {
2468
2569
  }
2469
2570
  const offset = pos === "e" ? SIXTEENTH : 3 * SIXTEENTH;
2470
2571
  const tick = swing16(ctx.beatTick + offset, sec, ctx.swingAmount);
2471
- ctx.track.add(GM.SD, tick, SIXTEENTH, ghostVel);
2572
+ ctx.track.add(GM.SD, tick, SIXTEENTH, ghostVel, void 0, entry);
2472
2573
  }
2473
2574
  }
2474
2575
  function generatePreChorusBuildup(ctx, sec, isSectionLastBar) {
@@ -2526,17 +2627,25 @@ function generateHiHatForBeat(ctx, sec) {
2526
2627
  const allowsOpenHiHat = sec.role !== "ambient" && sec.role !== "minimal";
2527
2628
  const isDynamicOpen = ctx.barHasOpenHh && ctx.beat === ctx.openHhBeat && !usesOffbeatOpenHiHat(sec.style);
2528
2629
  const dm = sec.densityMult;
2630
+ const beatEntry = ctx.section === "intro" && ctx.beat !== 0 ? ctx.hhEntries.eighth : ctx.hhEntries.quarter;
2529
2631
  if (ctx.hhLevel === "quarter") {
2530
2632
  if (ctx.section === "intro" && ctx.beat !== 0) {
2531
2633
  return;
2532
2634
  }
2533
2635
  const hhVel = Math.max(20, ctx.velocity * dm * 0.75 * typeMult);
2534
2636
  if (allowsOpenHiHat && isDynamicOpen) {
2535
- 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);
2536
2638
  } else if (allowsOpenHiHat && opensOnDownbeat(ctx, sec)) {
2537
- 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
+ );
2538
2647
  } else {
2539
- ctx.track.add(hhInstrument, ctx.beatTick, EIGHTH, hhVel);
2648
+ ctx.track.add(hhInstrument, ctx.beatTick, EIGHTH, hhVel, void 0, beatEntry);
2540
2649
  }
2541
2650
  return;
2542
2651
  }
@@ -2546,22 +2655,37 @@ function generateHiHatForBeat(ctx, sec) {
2546
2655
  if (eighth === 1) {
2547
2656
  hhTick = swing16(hhTick, sec, ctx.swingAmount);
2548
2657
  }
2658
+ const entry = eighth === 0 ? beatEntry : ctx.hhEntries.eighth;
2549
2659
  if (ctx.section === "intro" && eighth === 1) {
2550
2660
  if (sec.useFootHh && ctx.beat % 2 === 0) {
2551
- 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
+ );
2552
2669
  }
2553
2670
  continue;
2554
2671
  }
2555
2672
  const hhVel = Math.max(20, ctx.velocity * dm * typeMult * (eighth === 0 ? 0.9 : 0.65));
2556
2673
  if (isDynamicOpen && allowsOpenHiHat && eighth === 0) {
2557
- 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);
2558
2675
  continue;
2559
2676
  }
2560
2677
  const useOpen = eighth === 0 ? opensOnDownbeat(ctx, sec) : opensOffbeat(ctx, sec);
2561
2678
  if (useOpen && allowsOpenHiHat) {
2562
- 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
+ );
2563
2687
  } else {
2564
- ctx.track.add(hhInstrument, hhTick, EIGHTH / 2, hhVel);
2688
+ ctx.track.add(hhInstrument, hhTick, EIGHTH / 2, hhVel, void 0, entry);
2565
2689
  }
2566
2690
  }
2567
2691
  return;
@@ -2576,26 +2700,41 @@ function generateHiHatForBeat(ctx, sec) {
2576
2700
  ctx.draw.float(0.95, 1.05, "hatVelocity", ctx.bar, ctx.beat, sixteenth)
2577
2701
  );
2578
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;
2579
2704
  if (isDynamicOpen && allowsOpenHiHat && sixteenth === 0) {
2580
- 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);
2581
2706
  continue;
2582
2707
  }
2583
2708
  if (allowsOpenHiHat && sixteenth === 0 && opensOnDownbeat(ctx, sec)) {
2584
- 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
+ );
2585
2717
  continue;
2586
2718
  }
2587
2719
  if (allowsOpenHiHat && sixteenth === 2 && opensOffbeat(ctx, sec)) {
2588
- 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
+ );
2589
2728
  continue;
2590
2729
  }
2591
2730
  if (allowsOpenHiHat && ctx.beat === 3 && sixteenth === 3) {
2592
2731
  const openProb = Math.max(0.1, Math.min(0.4, 30 / ctx.bpm));
2593
2732
  if (ctx.draw.prob(openProb, "openHat", ctx.bar, ctx.beat, sixteenth)) {
2594
- 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);
2595
2734
  continue;
2596
2735
  }
2597
2736
  }
2598
- ctx.track.add(hhInstrument, hhTick, SIXTEENTH / 2, hhVel);
2737
+ ctx.track.add(hhInstrument, hhTick, SIXTEENTH / 2, hhVel, void 0, entry);
2599
2738
  }
2600
2739
  }
2601
2740
 
@@ -2622,7 +2761,7 @@ function bjorklund(pulses, steps) {
2622
2761
  }
2623
2762
  let head = Array.from({ length: pulses }, () => [true]);
2624
2763
  let remainder = Array.from({ length: steps - pulses }, () => [false]);
2625
- while (remainder.length > 1) {
2764
+ do {
2626
2765
  const count = Math.min(head.length, remainder.length);
2627
2766
  const next = [];
2628
2767
  for (let i = 0; i < count; i += 1) {
@@ -2630,7 +2769,7 @@ function bjorklund(pulses, steps) {
2630
2769
  }
2631
2770
  remainder = head.length > count ? head.slice(count) : remainder.slice(count);
2632
2771
  head = next;
2633
- }
2772
+ } while (remainder.length > 1);
2634
2773
  return [...head.flat(), ...remainder.flat()];
2635
2774
  }
2636
2775
 
@@ -2650,6 +2789,7 @@ var FILL_TYPES = Object.freeze([
2650
2789
  "flamsAndDrags",
2651
2790
  "halfTimeFill"
2652
2791
  ]);
2792
+ var FILL_VELOCITY_BASES = Object.freeze(["fill", "accent"]);
2653
2793
  var S = SIXTEENTH;
2654
2794
  var E = EIGHTH;
2655
2795
  function fill(scale, offset) {
@@ -2773,6 +2913,29 @@ var FILL_ARCHETYPES = deepFreeze({
2773
2913
  function isFillArchetype(material) {
2774
2914
  return typeof material === "object" && material !== null && Array.isArray(material.atBeat);
2775
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
+ }
2776
2939
  function fillArchetypeFor(id, supplied) {
2777
2940
  return supplied?.get(id) ?? FILL_ARCHETYPES[id];
2778
2941
  }
@@ -2947,6 +3110,8 @@ var HitList = class {
2947
3110
  hits = [];
2948
3111
  /** Voices already struck at each quantized beat. */
2949
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();
2950
3115
  /**
2951
3116
  * Append a hit, clamping velocity and dropping non-positive positions.
2952
3117
  *
@@ -2954,12 +3119,23 @@ var HitList = class {
2954
3119
  * sounds once however many voices ask for it, and a second entry at the same
2955
3120
  * pitch and position is a note-on a reader answers with no note-off, heard as
2956
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.
2957
3125
  */
2958
- add(pitch, startBeat, durationBeat, velocity, articulation) {
3126
+ add(pitch, startBeat, durationBeat, velocity, articulation, entry = Number.NEGATIVE_INFINITY) {
2959
3127
  const beat = Math.max(0, startBeat);
3128
+ const key = onsetKey(pitch, beat);
3129
+ const held = this.#entries.get(key);
2960
3130
  if (this.hasOnset(pitch, beat)) {
3131
+ if (held !== void 0 && entry < held) {
3132
+ this.#entries.set(key, entry);
3133
+ }
2961
3134
  return;
2962
3135
  }
3136
+ if (entry !== Number.NEGATIVE_INFINITY) {
3137
+ this.#entries.set(key, entry);
3138
+ }
2963
3139
  const hit2 = {
2964
3140
  pitch,
2965
3141
  startBeat: beat,
@@ -2977,6 +3153,13 @@ var HitList = class {
2977
3153
  this.#onsets.set(beatSlot(beat), /* @__PURE__ */ new Set([pitch]));
2978
3154
  }
2979
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
+ }
2980
3163
  /** Whether a hit of this pitch already starts exactly at `startBeat`. */
2981
3164
  hasOnset(pitch, startBeat) {
2982
3165
  return this.#onsets.get(beatSlot(startBeat))?.has(pitch) ?? false;
@@ -3073,17 +3256,14 @@ var KICK_FIGURES = deepFreeze({
3073
3256
  ],
3074
3257
  standard: DEFAULT_FIGURE
3075
3258
  });
3076
- function emptyPattern() {
3077
- return Array.from({ length: KICK_STEPS }, () => false);
3078
- }
3079
3259
  function isInPreChorusLift(section, bar2, sectionBars, nextIsChorus) {
3080
3260
  if (section !== "b" || !nextIsChorus || sectionBars < 3) {
3081
3261
  return false;
3082
3262
  }
3083
3263
  return bar2 >= sectionBars - 2;
3084
3264
  }
3085
- function realiseKickFigure(figure, section, bar2, draw, rhythmic) {
3086
- const pattern = emptyPattern();
3265
+ function realiseKickEntries(figure, section, bar2, draw, rhythmic) {
3266
+ const entries = Array.from({ length: KICK_STEPS }, () => void 0);
3087
3267
  for (const slot of figure) {
3088
3268
  if (slot.step < 0 || slot.step >= KICK_STEPS) {
3089
3269
  continue;
@@ -3095,19 +3275,23 @@ function realiseKickFigure(figure, section, bar2, draw, rhythmic) {
3095
3275
  continue;
3096
3276
  }
3097
3277
  if (slot.probability === void 0) {
3098
- pattern[slot.step] = true;
3278
+ entries[slot.step] = Number.NEGATIVE_INFINITY;
3099
3279
  continue;
3100
3280
  }
3101
- if (draw.prob(leanedBy(slot.probability, rhythmic), "kick", bar2, slot.slot ?? slot.step)) {
3102
- 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
+ );
3103
3287
  }
3104
3288
  }
3105
- return pattern;
3289
+ return entries;
3106
3290
  }
3107
- function getKickPattern(section, style, bar2, draw, rhythmic) {
3291
+ function getKickEntries(section, style, bar2, draw, rhythmic) {
3108
3292
  const own = KICK_FIGURES[style] ?? DEFAULT_FIGURE;
3109
3293
  const figure = section === "outro" ? [...OUTRO_FIGURE, ...statedSlots(own)] : own;
3110
- return realiseKickFigure(figure, section, bar2, draw, rhythmic);
3294
+ return realiseKickEntries(figure, section, bar2, draw, rhythmic);
3111
3295
  }
3112
3296
 
3113
3297
  // src/generate/drums/percussion.ts
@@ -3144,6 +3328,8 @@ var PERC_TABLE = [
3144
3328
  /* rock */
3145
3329
  [F, F, F, [false, false, true], F, F, F, F, [false, false, true]]
3146
3330
  ];
3331
+ var SHAKER_SIXTEENTH_CURVE = [0.75, 0.45, 0.6, 0.45];
3332
+ var SHAKER_EIGHTH_CURVE = [0.75, 0.55];
3147
3333
  function getPercussionConfig(mood, section) {
3148
3334
  const row = PERC_TABLE[mood] ?? PERC_TABLE[1 /* Standard */];
3149
3335
  const act = row?.[sectionIndex(section)] ?? F;
@@ -3168,10 +3354,11 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
3168
3354
  if (config.shaker) {
3169
3355
  const use16th = config.shaker16th && bpm < HH_16TH_BPM_THRESHOLD && sustainsStrokes(SIXTEENTH, bpm, sec.difficulty);
3170
3356
  if (use16th) {
3171
- const velCurve = [0.75, 0.45, 0.6, 0.45];
3357
+ const velCurve = SHAKER_SIXTEENTH_CURVE;
3172
3358
  for (let beat = 0; beat < barBeats; beat += 1) {
3173
3359
  for (let sub = 0; sub < 4; sub += 1) {
3174
- 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)) {
3175
3362
  continue;
3176
3363
  }
3177
3364
  const raw = 80 * (velCurve[sub] ?? 0.5) * densityMult * draw.float(0.9, 1.1, "shaker", barStart, beat, sub);
@@ -3179,12 +3366,14 @@ function generateAuxPercussionForBar(track, barStart, config, role, densityMult,
3179
3366
  GM.SHAKER,
3180
3367
  swing16(barStart + beat + sub * SIXTEENTH, sec, swingAmount),
3181
3368
  SIXTEENTH,
3182
- Math.max(25, Math.min(85, raw))
3369
+ Math.max(25, Math.min(85, raw)),
3370
+ void 0,
3371
+ entry
3183
3372
  );
3184
3373
  }
3185
3374
  }
3186
3375
  } else {
3187
- const velCurve = [0.75, 0.55];
3376
+ const velCurve = SHAKER_EIGHTH_CURVE;
3188
3377
  for (let beat = 0; beat < barBeats; beat += 1) {
3189
3378
  for (let sub = 0; sub < 2; sub += 1) {
3190
3379
  const raw = 80 * (velCurve[sub] ?? 0.6) * densityMult * draw.float(0.9, 1.1, "shaker", barStart, beat, sub);
@@ -3470,6 +3659,7 @@ function placeDrumPattern(opts) {
3470
3659
  assertRange(opts.velocity, 1, 127, "drum pattern velocity");
3471
3660
  }
3472
3661
  const feel = opts.feel === void 0 ? "straight" : assertOneOf(opts.feel, DRUM_FEELS, "feel");
3662
+ const rate = assertOneOf(opts.rate ?? "straight", FIGURE_RATES, "rate");
3473
3663
  const resolved = resolveContext(opts.ctx);
3474
3664
  const bpm = resolved.bpm ?? DEFAULT_BPM2;
3475
3665
  const draw = resolved.part("drums");
@@ -3515,7 +3705,7 @@ function placeDrumPattern(opts) {
3515
3705
  ...resolved.rhythmic === void 0 ? {} : { rhythmic: resolved.rhythmic },
3516
3706
  ...resolved.ornament === void 0 ? {} : { ornament: resolved.ornament },
3517
3707
  isOrnament: (stroke) => stroke.articulation === "ghost",
3518
- rate: opts.rate ?? "straight",
3708
+ rate,
3519
3709
  spanSteps: entry.material.steps,
3520
3710
  // The dials rank the figure's positions against the bar it is written
3521
3711
  // in, which is the bar this call is placing it in.
@@ -3602,6 +3792,7 @@ function generateDrums(opts) {
3602
3792
  densityMult,
3603
3793
  rhythmic,
3604
3794
  ornament: ornamentDial,
3795
+ ornamentFollowsRhythmic: resolved.ornament === void 0,
3605
3796
  difficulty,
3606
3797
  ghostBoost: mapping.ghostBoost,
3607
3798
  useRide: shouldUseRideForSection(section, style),
@@ -3617,10 +3808,15 @@ function generateDrums(opts) {
3617
3808
  const percMood = percMoodCategory(style);
3618
3809
  const nextSection = opts.nextSection ? mapSection(opts.nextSection) : section;
3619
3810
  const nextEnergy = sectionEnergy(nextSection);
3620
- const callerFillEntries = selectVocabulary(
3621
- vocabularyOfKind(resolved.vocabulary, isFillArchetype),
3622
- { section: publicSection, bpm, difficulty }
3623
- );
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
+ });
3624
3820
  const callerFills = new Map(callerFillEntries.map((entry) => [entry.id, entry.material]));
3625
3821
  const callerFillIds = callerFillEntries.map((entry) => entry.id);
3626
3822
  const euclidSteps = opts.euclideanKick?.steps ?? 16;
@@ -3631,7 +3827,7 @@ function generateDrums(opts) {
3631
3827
  for (let bar2 = 0; bar2 < opts.bars; bar2 += 1) {
3632
3828
  const barStart = bar2 * barBeats;
3633
3829
  const isLastBar = bar2 === opts.bars - 1;
3634
- const hhLevel = getHiHatLevel(section, style, rhythmic, bpm, draw, difficulty, bar2);
3830
+ const hiHat = getHiHatBar(section, style, rhythmic, bpm, draw, difficulty, bar2);
3635
3831
  if (bar2 === 0 && section === "chorus") {
3636
3832
  track.add(GM.CRASH, barStart, 0.5, 100 * densityMult);
3637
3833
  }
@@ -3643,10 +3839,10 @@ function generateDrums(opts) {
3643
3839
  }
3644
3840
  let kick;
3645
3841
  if (!euclidKick && reuseSectionKick) {
3646
- sectionKick ??= getKickPattern(section, style, 0, draw, rhythmic);
3842
+ sectionKick ??= getKickEntries(section, style, 0, draw, rhythmic);
3647
3843
  kick = sectionKick;
3648
3844
  } else if (!euclidKick) {
3649
- kick = getKickPattern(section, style, bar2, draw, rhythmic);
3845
+ kick = getKickEntries(section, style, bar2, draw, rhythmic);
3650
3846
  }
3651
3847
  const inLift = isInPreChorusLift(section, bar2, opts.bars, nextSection === "chorus");
3652
3848
  let currentFill = FILL_ARCHETYPES.snareRoll;
@@ -3702,7 +3898,8 @@ function generateDrums(opts) {
3702
3898
  swingAmount,
3703
3899
  barHasOpenHh,
3704
3900
  openHhBeat: openHhBeatIndex,
3705
- hhLevel,
3901
+ hhLevel: hiHat.level,
3902
+ hhEntries: hiHat.entries,
3706
3903
  draw
3707
3904
  };
3708
3905
  if (playMainVoices) {
@@ -3763,24 +3960,44 @@ function generateDrums(opts) {
3763
3960
  const endBeat = opts.bars * barBeats;
3764
3961
  const kit = resolved.instrument("drums");
3765
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);
3766
- return underCeiling(written, bpm, difficulty, exemptOnsets);
3963
+ return underCeiling(written, bpm, difficulty, exemptOnsets, track);
3767
3964
  }
3768
- function underCeiling(hits, bpm, difficulty, exempt) {
3965
+ function underCeiling(hits, bpm, difficulty, exempt, track) {
3769
3966
  if (difficulty === void 0) {
3770
3967
  return [...hits];
3771
3968
  }
3772
- const lastOnset = /* @__PURE__ */ new Map();
3773
- const kept = [];
3774
- for (const hit2 of hits) {
3775
- const previous = lastOnset.get(hit2.pitch);
3776
- 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));
3777
3993
  if (!reachable) {
3778
3994
  continue;
3779
3995
  }
3780
- kept.push(hit2);
3781
- lastOnset.set(hit2.pitch, hit2.startBeat);
3996
+ kept[index] = true;
3997
+ beats.splice(at2, 0, hit2.startBeat);
3998
+ keptBeats.set(hit2.pitch, beats);
3782
3999
  }
3783
- return kept;
4000
+ return hits.filter((_, index) => kept[index]);
3784
4001
  }
3785
4002
  function playableOn(kit, pitch) {
3786
4003
  return kit === void 0 || canSound(kit, pitch);
@@ -3799,7 +4016,7 @@ var MIN_VELOCITY = 1;
3799
4016
  var MAX_VELOCITY = 127;
3800
4017
  function humanize(events, opts = {}) {
3801
4018
  const asked = assertOptions(opts, "opts");
3802
- const ts = meterAt(0, toMeterData(asked.ts ?? DEFAULT_TS3, "ts"));
4019
+ const meter = toMeterData(asked.ts ?? DEFAULT_TS3, "ts");
3803
4020
  assertNoteEvents(events, "humanize events", { allowNonPositiveDuration: true });
3804
4021
  const sounding2 = dropSilentNotes(events);
3805
4022
  const timing = asked.timing ?? DEFAULT_TIMING;
@@ -3815,7 +4032,7 @@ function humanize(events, opts = {}) {
3815
4032
  return sounding2.map((event) => {
3816
4033
  const timingOffset = draw.float(-timing, timing, "timing", event.startBeat, event.pitch);
3817
4034
  const startBeat = event.startBeat + timingOffset;
3818
- const weight = metricWeight(event.startBeat, ts);
4035
+ const weight = metricWeight(event.startBeat, meter);
3819
4036
  const accentBoost = weight / MAX_METRIC_WEIGHT * accent2;
3820
4037
  const velocityOffset = draw.float(
3821
4038
  -velocityJitter,
@@ -3829,6 +4046,21 @@ function humanize(events, opts = {}) {
3829
4046
  return { ...event, startBeat, velocity };
3830
4047
  });
3831
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
+ }
3832
4064
  function quantizeToGrid(beat, barBeats, subdivision, slotsPerBar) {
3833
4065
  const barIndex = Math.floor(beat / barBeats);
3834
4066
  const modBeat = beat - barIndex * barBeats;
@@ -3844,7 +4076,7 @@ function quantizeToGrid(beat, barBeats, subdivision, slotsPerBar) {
3844
4076
  };
3845
4077
  }
3846
4078
  function extractGrooveTemplate(events, ts, subdivision = DEFAULT_GROOVE_SUBDIVISION) {
3847
- const meter = meterAt(0, toMeterData(ts, "ts"));
4079
+ const meter = grooveSignature(ts);
3848
4080
  assertNoteEvents(events, "groove source events", { allowNonPositiveDuration: true });
3849
4081
  assertPositiveInt(subdivision, "groove subdivision");
3850
4082
  const barBeats = beatsPerBar(meter);
@@ -3885,7 +4117,7 @@ function extractGrooveTemplate(events, ts, subdivision = DEFAULT_GROOVE_SUBDIVIS
3885
4117
  return { subdivision, slotsPerBar, slots, ts: meter };
3886
4118
  }
3887
4119
  function applyGrooveTemplate(events, template, ts) {
3888
- const meter = meterAt(0, toMeterData(ts, "ts"));
4120
+ const meter = grooveSignature(ts);
3889
4121
  assertNoteEvents(events, "groove target events", { allowNonPositiveDuration: true });
3890
4122
  assertPositiveInt(template.subdivision, "template subdivision");
3891
4123
  assertPositiveInt(template.slotsPerBar, "template slotsPerBar");
@@ -3949,15 +4181,27 @@ function classifyMelodyTones(melody, ts) {
3949
4181
  const meter = meterAt(0, toMeterData(ts, "ts"));
3950
4182
  assertNoteEvents(melody, "melody notes", { allowNonPositiveDuration: true });
3951
4183
  const pulse2 = pulseBeats(meter);
3952
- return melody.map((_, index) => {
3953
- const role = roleOfNote(melody, index, meter, pulse2);
3954
- 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
+ );
3955
4197
  });
4198
+ return roles.map((role, index) => ({
4199
+ noteIndex: index,
4200
+ role,
4201
+ ornamental: role !== "structural"
4202
+ }));
3956
4203
  }
3957
- function roleOfNote(melody, index, ts, pulse2) {
3958
- const note2 = melody[index];
3959
- const prev = melody[index - 1];
3960
- const next = melody[index + 1];
4204
+ function roleOfNote(prev, note2, next, ts, pulse2) {
3961
4205
  if (!note2 || !prev || !next) {
3962
4206
  return "structural";
3963
4207
  }
@@ -4013,13 +4257,11 @@ var DEFAULT_PLACEMENT = {
4013
4257
  };
4014
4258
  var DEFAULT_METER = { numerator: 4, denominator: 4 };
4015
4259
  var TIE_BREAK_JITTER2 = 1e-6;
4016
- function cadencesThroughRaisedSeventh(key) {
4017
- return isMinorKey(key) && scaleSystemOf(key) === "common-practice";
4018
- }
4019
4260
  function isKeyTone(pitch, key) {
4020
4261
  return isScaleTone(pitch, key) || cadencesThroughRaisedSeventh(key) && pitchClassOf(pitch) === leadingTonePcOf(key);
4021
4262
  }
4022
4263
  function inferKey(melody) {
4264
+ const line = inOnsetOrder(melody);
4023
4265
  let best = majorKey(0);
4024
4266
  let bestScore = Number.NEGATIVE_INFINITY;
4025
4267
  const candidates = [];
@@ -4029,7 +4271,7 @@ function inferKey(melody) {
4029
4271
  for (const key of candidates) {
4030
4272
  const tonic = pitchClassOf(key.rootPc);
4031
4273
  let score = 0;
4032
- for (const n of melody) {
4274
+ for (const n of line) {
4033
4275
  const w = Math.max(0.25, n.durationBeat);
4034
4276
  const pc = pitchClassOf(n.pitch);
4035
4277
  if (isKeyTone(n.pitch, key)) {
@@ -4046,6 +4288,12 @@ function inferKey(melody) {
4046
4288
  }
4047
4289
  return best;
4048
4290
  }
4291
+ function inOnsetOrder(melody) {
4292
+ return canonicalNoteOrder(melody).flatMap((index) => {
4293
+ const note2 = melody[index];
4294
+ return note2 === void 0 ? [] : [note2];
4295
+ });
4296
+ }
4049
4297
 
4050
4298
  // src/generate/harmonize/cost.ts
4051
4299
  var NON_CHORD_TONE_STRONG = 4;
@@ -4342,15 +4590,23 @@ function buildSegments(spans, melody, ts, grid, phraseEnds, budget) {
4342
4590
  }
4343
4591
 
4344
4592
  // src/generate/harmonize/candidates.ts
4345
- 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
+ ];
4346
4607
  var UNSTABLE_TRIAD = 0.9;
4347
- var UNSTABLE_QUALITIES = /* @__PURE__ */ new Set([
4348
- "dim",
4349
- "aug",
4350
- "dim7",
4351
- "m7b5",
4352
- "aug7"
4353
- ]);
4608
+ var UNSTABLE_QUALITY_NAMES = ["dim", "aug", "dim7", "m7b5", "aug7"];
4609
+ var UNSTABLE_QUALITIES = new Set(UNSTABLE_QUALITY_NAMES);
4354
4610
  function vocabularyBase(rootPc, quality, tonicPc) {
4355
4611
  const above = (pitchClassOf(rootPc) - tonicPc + 12) % 12;
4356
4612
  const root = ROOT_WEIGHT_BY_SEMITONE[above] ?? UNSTABLE_TRIAD;
@@ -4364,7 +4620,7 @@ function secondaryDominantTargets(key) {
4364
4620
  tonicizableDegrees(key).map((degree) => [degree.degreeNumber, degree])
4365
4621
  );
4366
4622
  return SECONDARY_DOMINANT_TARGETS.map((degree) => tonicizable.get(degree)).filter(
4367
- (degree) => degree !== void 0
4623
+ (degree) => degree !== void 0 && !isNativeDominantSeventh(degree.rootPc + 7, key)
4368
4624
  );
4369
4625
  }
4370
4626
  var SECONDARY_DOMINANT_ENTRY_ORDER = [5, 6, 2, 4];
@@ -4379,13 +4635,14 @@ var REHARMONIZE_DIAL = {
4379
4635
  function admittedCount(fraction, size) {
4380
4636
  return Math.max(0, Math.min(size, Math.ceil(fraction * size)));
4381
4637
  }
4382
- function buildCandidates(key, harmonic) {
4638
+ function buildCandidates(keyLike, harmonic) {
4639
+ const { scale: key, tonic } = resolveKey(keyLike);
4383
4640
  const tonicPc = pitchClassOf(key.rootPc);
4384
4641
  const frame = heptatonicFrameOf(key);
4385
4642
  const tones = scaleTonesInDegreeOrder(frame);
4386
4643
  const candidates = tones.map((rootPc, index) => {
4387
4644
  const degree = index + 1;
4388
- const quality = diatonicTriad(degree, frame).quality;
4645
+ const quality = diatonicTriad(degree, { ...frame, tonic }).quality;
4389
4646
  return {
4390
4647
  rootPc,
4391
4648
  quality,
@@ -4429,7 +4686,7 @@ function buildCandidates(key, harmonic) {
4429
4686
  const parallelTones = scaleTonesInDegreeOrder(parallel);
4430
4687
  const borrowed = [];
4431
4688
  for (let degree = 1; degree <= parallelTones.length; degree += 1) {
4432
- const chord = diatonicTriad(degree, parallel);
4689
+ const chord = diatonicTriad(degree, { ...parallel, tonic });
4433
4690
  if (!isDiatonic(chord, key)) {
4434
4691
  borrowed.push(chord);
4435
4692
  }
@@ -4475,11 +4732,11 @@ function harmonizeMelody(opts) {
4475
4732
  allowNonPositiveDuration: true,
4476
4733
  budget: opts.budget
4477
4734
  });
4478
- const phraseEnds = opts.phraseEnds ?? [];
4735
+ const phraseEnds = assertArray(opts.phraseEnds ?? [], "harmonize phrase ends");
4479
4736
  for (const end of phraseEnds) {
4480
4737
  assertFiniteNumber(end, "harmonize phrase end");
4481
4738
  }
4482
- const soundingMelody = soundingNotesOnly(opts.melody);
4739
+ const soundingMelody = inOnsetOrder(soundingNotesOnly(opts.melody));
4483
4740
  const ts = meterAt(0, toMeterData(opts.ts ?? DEFAULT_METER, "ts"));
4484
4741
  assertTimeSignature(ts);
4485
4742
  const requestedKey = opts.key ?? "infer";
@@ -4487,7 +4744,10 @@ function harmonizeMelody(opts) {
4487
4744
  requestedKey === "infer" ? inferKey(soundingMelody) : requestedKey
4488
4745
  );
4489
4746
  const key = resolvedKey.scale;
4490
- const placement = opts.placement ?? DEFAULT_PLACEMENT;
4747
+ const placement = assertRecord(
4748
+ opts.placement ?? DEFAULT_PLACEMENT,
4749
+ "harmonize placement"
4750
+ );
4491
4751
  const ctx = resolveContext(opts.ctx);
4492
4752
  const reharmonize = assertOneOf(
4493
4753
  opts.reharmonize ?? "diatonic",
@@ -4498,7 +4758,7 @@ function harmonizeMelody(opts) {
4498
4758
  if (soundingMelody.length === 0) {
4499
4759
  return { transposeSemitones: 0, key: resolvedKey, chords: [], melodyRoles: [] };
4500
4760
  }
4501
- const candidates = buildCandidates(key, harmonic);
4761
+ const candidates = buildCandidates(resolvedKey, harmonic);
4502
4762
  const candAt = (i) => candidates[i] ?? FALLBACK;
4503
4763
  const draw = ctx.part("harmony");
4504
4764
  const jitter = candidates.map((_, index) => draw.at("jitter", index) * TIE_BREAK_JITTER2);
@@ -4549,6 +4809,7 @@ function harmonizeMelody(opts) {
4549
4809
  "harmonization placement search",
4550
4810
  opts.budget
4551
4811
  );
4812
+ let bestFit = Number.POSITIVE_INFINITY;
4552
4813
  let bestCost = Number.POSITIVE_INFINITY;
4553
4814
  let bestTs = 0;
4554
4815
  let bestPath = [];
@@ -4563,8 +4824,10 @@ function harmonizeMelody(opts) {
4563
4824
  closingSegments,
4564
4825
  articulatedSegments
4565
4826
  );
4566
- const total = cost + keyFitCost(shifted, key) + tessituraCost(shifted);
4567
- 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;
4568
4831
  bestCost = total;
4569
4832
  bestTs = shift;
4570
4833
  bestPath = path;
@@ -4699,7 +4962,11 @@ function contourOffsets(contour, count) {
4699
4962
  }
4700
4963
  function generateMotif(opts) {
4701
4964
  assertRecord(opts, "motif options");
4702
- const contour = opts.contour ?? "arch";
4965
+ const contour = assertOneOf(
4966
+ opts.contour ?? "arch",
4967
+ ["arch", "ascending", "descending", "wave"],
4968
+ "motif contour"
4969
+ );
4703
4970
  const bars = assertPositiveInt(opts.bars, "motif bars");
4704
4971
  const barBeats = beatsPerBar(meterAt(0, toMeterData(opts.ts ?? DEFAULT_TS4, "ts")));
4705
4972
  const totalBeats = bars * barBeats;
@@ -4846,6 +5113,10 @@ function developMotif(cell, timeline, key, bars, ts) {
4846
5113
  if (span <= 0) {
4847
5114
  return inTimeOrder(cell);
4848
5115
  }
5116
+ const line = canonicalNoteOrder(cellNotes).flatMap((index) => {
5117
+ const note2 = cellNotes[index];
5118
+ return note2 === void 0 ? [] : [note2];
5119
+ });
4849
5120
  const tileSpan = Math.max(span, MIN_TILE_SPAN);
4850
5121
  const tileCount = Math.ceil(totalBeats / tileSpan);
4851
5122
  assertGenerationBudget(tileCount * cellNotes.length, "developed motif notes");
@@ -4853,7 +5124,7 @@ function developMotif(cell, timeline, key, bars, ts) {
4853
5124
  const offset = k * tileSpan;
4854
5125
  const taken = /* @__PURE__ */ new Map();
4855
5126
  let previous;
4856
- for (const n of cellNotes) {
5127
+ for (const n of line) {
4857
5128
  const startBeat = offset + (n.startBeat - origin);
4858
5129
  if (startBeat >= totalBeats) {
4859
5130
  continue;
@@ -4866,7 +5137,11 @@ function developMotif(cell, timeline, key, bars, ts) {
4866
5137
  const pitch = developedPitch(n.pitch, allows, direction, taken);
4867
5138
  taken.set(pitch, n.pitch);
4868
5139
  previous = n.pitch;
4869
- out.push({ pitch, startBeat, durationBeat: n.durationBeat });
5140
+ out.push({
5141
+ pitch,
5142
+ startBeat,
5143
+ durationBeat: Math.min(n.durationBeat, totalBeats - startBeat)
5144
+ });
4870
5145
  }
4871
5146
  }
4872
5147
  return inTimeOrder({ notes: out });
@@ -4878,6 +5153,13 @@ function positionClass(beat, meters) {
4878
5153
  const weight = metricWeight(beat, meters);
4879
5154
  return weight >= STRONG_WEIGHT ? "strong" : weight > 0 ? "weak" : "off";
4880
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);
5160
+ }
5161
+ return Math.max(downbeat, phrase.startBeat);
5162
+ }
4881
5163
 
4882
5164
  // src/generate/plan/types.ts
4883
5165
  var COMPOSITION_PLAN_VERSION = 1;
@@ -5158,7 +5440,7 @@ function planChordTimeline(plan) {
5158
5440
  }
5159
5441
 
5160
5442
  // src/generate/plan/harmony.ts
5161
- var HARMONY_OK_KINDS = /* @__PURE__ */ new Set([
5443
+ var HARMONY_OK_KIND_NAMES = [
5162
5444
  "chordTone",
5163
5445
  "tension",
5164
5446
  "suspension",
@@ -5166,7 +5448,8 @@ var HARMONY_OK_KINDS = /* @__PURE__ */ new Set([
5166
5448
  "anticipation",
5167
5449
  "passing",
5168
5450
  "neighbor"
5169
- ]);
5451
+ ];
5452
+ var HARMONY_OK_KINDS = new Set(HARMONY_OK_KIND_NAMES);
5170
5453
  var CADENCE_DEGREES = {
5171
5454
  authentic: [1, 3],
5172
5455
  plagal: [1, 3],
@@ -5189,8 +5472,13 @@ function cadencePitchClasses(plan, timeline, cadence, beat) {
5189
5472
  return chord === null ? null : chordPitchClasses(chord);
5190
5473
  }
5191
5474
  const entry = plan.harmony.find((chord) => beat >= chord.startBeat && beat < chord.endBeat);
5192
- const tones = scaleTonesInDegreeOrder(scaleOf(plan.keys[entry?.key ?? 0]));
5193
- return CADENCE_DEGREES[cadence].map((degree) => tones[degree - 1]).filter((pc) => pc !== void 0);
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);
5194
5482
  }
5195
5483
  function planHarmonyMisfits(plan, timeline = planChordTimeline(plan)) {
5196
5484
  const keyAt = planKeyAt(plan);
@@ -5206,7 +5494,8 @@ function planHarmonyMisfits(plan, timeline = planChordTimeline(plan)) {
5206
5494
  if (kinds.some((kind) => HARMONY_OK_KINDS.has(kind))) {
5207
5495
  return null;
5208
5496
  }
5209
- return position === "weak" && isScaleTone(note2.pitch, scaleAt2(note2.startBeat)) ? null : kinds;
5497
+ const lenient = position === "weak" || timeline.at(note2.startBeat) === null;
5498
+ return lenient && isScaleTone(note2.pitch, scaleAt2(note2.startBeat)) ? null : kinds;
5210
5499
  });
5211
5500
  };
5212
5501
  }
@@ -5341,16 +5630,19 @@ function stretched(cell, timeRatio) {
5341
5630
  function shaped(cell, relation) {
5342
5631
  switch (relation.kind) {
5343
5632
  case "repetition":
5344
- case "tonalTransposition":
5345
5633
  return cell;
5346
5634
  case "transposition":
5635
+ case "tonalTransposition":
5347
5636
  return stretched(cell, relation.timeRatio);
5348
5637
  case "inversion":
5349
5638
  return stretched(transformMotif(cell, "invert"), relation.timeRatio);
5350
5639
  case "retrograde":
5351
- return transformMotif(cell, "retrograde");
5640
+ return stretched(transformMotif(cell, "retrograde"), relation.timeRatio);
5352
5641
  case "retrogradeInversion":
5353
- return transformMotif(transformMotif(cell, "invert"), "retrograde");
5642
+ return stretched(
5643
+ transformMotif(transformMotif(cell, "invert"), "retrograde"),
5644
+ relation.timeRatio
5645
+ );
5354
5646
  case "augmentation":
5355
5647
  return transformMotif(cell, "augment", relation.timeRatio);
5356
5648
  case "diminution":
@@ -5625,6 +5917,7 @@ function onsetSlots(scene, startBeat, endBeat) {
5625
5917
  slots.push(beat);
5626
5918
  }
5627
5919
  }
5920
+ assertGenerationBudget(slots.length, "melody onset slots", scene.budget);
5628
5921
  bar2 = barEnd;
5629
5922
  }
5630
5923
  return slots;
@@ -5765,15 +6058,6 @@ function lineBefore(scene, index, beat) {
5765
6058
  }
5766
6059
  return line.sort((a, b) => a.startBeat - b.startBeat);
5767
6060
  }
5768
- function finalDownbeat(scene, index) {
5769
- const phrase = scene.plan.phrases[index];
5770
- const { meters } = scene.plan;
5771
- let downbeat = barStartBeat(phrase.startBeat, meters);
5772
- while (downbeat + beatsPerBarAt(downbeat, meters) < phrase.endBeat - BEAT_EPS) {
5773
- downbeat += beatsPerBarAt(downbeat, meters);
5774
- }
5775
- return Math.max(downbeat, phrase.startBeat);
5776
- }
5777
6061
  function heldNoteIndex(scene, index, line, downbeat) {
5778
6062
  const phrase = scene.plan.phrases[index];
5779
6063
  const sounding2 = line.findIndex(
@@ -5904,7 +6188,7 @@ function generateMelody(plan, opts) {
5904
6188
  const melody = [];
5905
6189
  plan.phrases.forEach((_, index) => {
5906
6190
  const line = phraseLine(scene, index);
5907
- const downbeat = finalDownbeat(scene, index);
6191
+ const downbeat = finalDownbeat(plan.phrases[index], plan.meters);
5908
6192
  let held = heldNoteIndex(scene, index, line, downbeat);
5909
6193
  if (held >= 0) {
5910
6194
  line.length = held + 1;
@@ -5940,8 +6224,8 @@ var DEFAULT_VELOCITY2 = 80;
5940
6224
  var MIN_VELOCITY2 = 1;
5941
6225
  var MAX_VELOCITY2 = 127;
5942
6226
  var SLIDE_MIN_SEMITONES = 1;
5943
- function eligible(style, note2, previous, next, ts) {
5944
- const strong = isStrongBeat(note2.startBeat, ts);
6227
+ function eligible(style, note2, previous, next, meter) {
6228
+ const strong = isStrongBeat(note2.startBeat, meter);
5945
6229
  switch (style) {
5946
6230
  case "ghost":
5947
6231
  return !strong;
@@ -5949,7 +6233,7 @@ function eligible(style, note2, previous, next, ts) {
5949
6233
  case "accent":
5950
6234
  return strong;
5951
6235
  case "drag":
5952
- return !strong && next !== void 0 && isStrongBeat(next.startBeat, ts);
6236
+ return !strong && next !== void 0 && isStrongBeat(next.startBeat, meter);
5953
6237
  case "slide":
5954
6238
  return previous !== void 0 && Math.abs(note2.pitch - previous.pitch) >= SLIDE_MIN_SEMITONES;
5955
6239
  }
@@ -5977,8 +6261,7 @@ function ornament(notes, opts = {}) {
5977
6261
  const asked = assertOptions(opts, "opts");
5978
6262
  assertNoteEvents(notes, "ornament notes", { allowNonPositiveDuration: true });
5979
6263
  const style = assertOneOf(asked.style ?? DEFAULT_STYLE2, ORNAMENT_STYLES, "ornament style");
5980
- const ts = meterAt(0, toMeterData(asked.ts ?? DEFAULT_TS5, "ts"));
5981
- assertTimeSignature(ts);
6264
+ const meter = toMeterData(asked.ts ?? DEFAULT_TS5, "ts");
5982
6265
  if (asked.amount !== void 0) {
5983
6266
  assertRange(asked.amount, 0, 1, "ornament amount");
5984
6267
  }
@@ -5992,7 +6275,7 @@ function ornament(notes, opts = {}) {
5992
6275
  const gaps = strokeGaps(ordered);
5993
6276
  return sounding2.map((note2) => {
5994
6277
  const decorated = { ...note2 };
5995
- if (note2.articulation !== void 0 || !eligible(style, note2, previousOf.get(note2), nextOf.get(note2), ts)) {
6278
+ if (note2.articulation !== void 0 || !eligible(style, note2, previousOf.get(note2), nextOf.get(note2), meter)) {
5996
6279
  return decorated;
5997
6280
  }
5998
6281
  if (!sustainsStrokes(gaps.get(note2) ?? Number.POSITIVE_INFINITY, ctx.bpm, ctx.difficulty)) {
@@ -6108,13 +6391,14 @@ function rejectSuppliedRng(ctx) {
6108
6391
  }
6109
6392
  }
6110
6393
  function plannedKeys(profile, requested) {
6111
- const regions = profile.harmony.keys.map(
6112
- ({ key }) => ({
6113
- scale: { ...key.scale },
6114
- tonic: { ...key.tonic },
6115
- variant: key.variant
6116
- })
6117
- );
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
+ });
6118
6402
  const home = regions[0];
6119
6403
  if (requested === void 0) {
6120
6404
  return regions;
@@ -6364,11 +6648,23 @@ function selectedDerivation(graph, parentEdge, selectedAt, node) {
6364
6648
  }
6365
6649
  return { from: null, relation: null };
6366
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
+ }
6367
6662
  function functionCandidates(fn, key) {
6368
6663
  if (scaleTonesInDegreeOrder(key).length !== HEPTATONIC_DEGREES) {
6369
6664
  return null;
6370
6665
  }
6371
- return FUNCTION_DEGREES[fn].map((degree) => chordToRoman(diatonicTriad(degree, key), key));
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;
6372
6668
  }
6373
6669
  function plannedHarmony(profile, keys, preserve, draw) {
6374
6670
  const chords = profile.harmony.chords;
@@ -6448,6 +6744,11 @@ function deriveCompositionPlan(reference, opts) {
6448
6744
  };
6449
6745
  const sections = plannedSections(profile, preserve.form, draw);
6450
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");
6451
6752
  const spans = plannedPhraseSpans(profile, sections, preserve.phraseLengths, draw);
6452
6753
  const phrases = spans.map((span) => {
6453
6754
  const melody = profile.form.phrases[span.source]?.melody ?? null;
@@ -6633,7 +6934,7 @@ function harmonyViolations(melody, plan, timeline, lines) {
6633
6934
  const derived = plan.motifs.some(
6634
6935
  (motif) => motif.from !== null && note2.startBeat >= motif.startBeat - BEAT_EPS && note2.startBeat < motif.endBeat - BEAT_EPS
6635
6936
  );
6636
- const strong = positionClass(note2.startBeat, plan.meters) === "strong";
6937
+ const strong = positionClass(note2.startBeat, plan.meters) === "strong" && timeline.at(note2.startBeat) !== null;
6637
6938
  violations.push({
6638
6939
  kind: "harmony",
6639
6940
  severity: derived ? "warning" : "error",
@@ -6651,7 +6952,16 @@ function notesInRange(melody, startBeat, endBeat) {
6651
6952
  (note2) => note2.startBeat >= startBeat - BEAT_EPS && note2.startBeat < endBeat - BEAT_EPS
6652
6953
  );
6653
6954
  }
6654
- function replayLevel(source, derived, statement, sourceEnd, plan, key, closing) {
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) {
6655
6965
  const relation = statement.relation;
6656
6966
  const planned = relation.kind === "tonalTransposition" ? relation.degrees ?? 0 : relation.semitones;
6657
6967
  const { low, high } = plan.phrases[statement.phrase].register;
@@ -6668,23 +6978,24 @@ function replayLevel(source, derived, statement, sourceEnd, plan, key, closing)
6668
6978
  const replays = relationLevels(relation.kind).map((level) => {
6669
6979
  const notes = clipStatement(
6670
6980
  replayAt(cell, relation, level, statement.startBeat, key),
6671
- statement.endBeat
6981
+ derivedEnd
6672
6982
  );
6673
6983
  return { level, notes, outside: notes.filter((note2) => outside(note2.pitch)).length };
6674
6984
  });
6675
6985
  const fewestOutside = Math.min(...replays.map((replay) => replay.outside));
6676
6986
  let best = null;
6677
6987
  for (const { level, notes: expected, outside: away } of replays) {
6678
- if (expected.length !== derived.length) {
6988
+ const compared = sourceCut ? derived.slice(0, expected.length) : derived;
6989
+ if (expected.length !== compared.length) {
6679
6990
  continue;
6680
6991
  }
6681
6992
  let forced = 0;
6682
6993
  let holds = true;
6683
6994
  for (let i = 0; holds && i < expected.length; i += 1) {
6684
6995
  const want = expected[i];
6685
- const got = derived[i];
6996
+ const got = compared[i];
6686
6997
  if (i > 0) {
6687
- const gap = got.startBeat - derived[i - 1].startBeat;
6998
+ const gap = got.startBeat - compared[i - 1].startBeat;
6688
6999
  holds = Math.abs(gap - (want.startBeat - expected[i - 1].startBeat)) <= BEAT_EPS;
6689
7000
  }
6690
7001
  if (!holds || got.pitch === want.pitch) {
@@ -6715,6 +7026,11 @@ function motifDerivationViolations(melody, plan, closing) {
6715
7026
  const derivedNotes = notesInRange(melody, statement.startBeat, statement.endBeat);
6716
7027
  const relation = statement.relation;
6717
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
+ }
6718
7034
  if (sourceNotes.length === 0 || derivedNotes.length === 0) {
6719
7035
  errors.push({
6720
7036
  kind: "motifDerivation",
@@ -6726,8 +7042,17 @@ function motifDerivationViolations(melody, plan, closing) {
6726
7042
  });
6727
7043
  continue;
6728
7044
  }
7045
+ const sourceCut = sourceKept < source.endBeat - BEAT_EPS;
7046
+ const derivedCut = derivedKept < statement.endBeat - BEAT_EPS;
6729
7047
  if (relation === null) {
6730
- const similarity = melodicSimilarity(sourceNotes, derivedNotes);
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);
6731
7056
  if (similarity < DEFAULT_VARIATION_THRESHOLD) {
6732
7057
  errors.push({
6733
7058
  kind: "motifDerivation",
@@ -6740,11 +7065,18 @@ function motifDerivationViolations(melody, plan, closing) {
6740
7065
  }
6741
7066
  continue;
6742
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
+ }
6743
7073
  const replay = replayLevel(
6744
- sourceNotes,
6745
- derivedNotes,
7074
+ kept,
7075
+ notesInRange(melody, statement.startBeat, derivedKept),
6746
7076
  statement,
6747
- source.endBeat,
7077
+ sourceKept,
7078
+ derivedKept,
7079
+ sourceCut,
6748
7080
  plan,
6749
7081
  scaleOf(keyAt(statement.startBeat)),
6750
7082
  closing
@@ -7122,12 +7454,12 @@ function degreeToRootPc(degree, key) {
7122
7454
  const offset = BORROWED_OFFSET[degree] ?? 0;
7123
7455
  return ((key.rootPc % 12 + offset) % 12 + 12) % 12;
7124
7456
  }
7125
- function autoQuality(degree, key, harmonicDominant) {
7457
+ function autoQuality(degree, key, tonic, harmonicDominant) {
7126
7458
  if (degree >= 1 && degree <= 7) {
7127
- if (harmonicDominant && isMinorKey(key) && degree === 5) {
7459
+ if (harmonicDominant && cadencesThroughRaisedSeventh(key) && degree === 5) {
7128
7460
  return "maj";
7129
7461
  }
7130
- return diatonicTriad(degree, heptatonicFrameOf(key)).quality;
7462
+ return diatonicTriad(degree, { ...heptatonicFrameOf(key), tonic }).quality;
7131
7463
  }
7132
7464
  if (degree === 14) {
7133
7465
  return "dim";
@@ -7137,11 +7469,11 @@ function autoQuality(degree, key, harmonicDominant) {
7137
7469
  }
7138
7470
  return "maj";
7139
7471
  }
7140
- function resolveCycle(degrees, key, ext, harmonicDominant) {
7472
+ function resolveCycle(degrees, key, tonic, ext, harmonicDominant) {
7141
7473
  const steps = degrees.map((degree) => {
7142
7474
  const step = {
7143
7475
  rootPc: degreeToRootPc(degree, key),
7144
- quality: ext !== void 0 && ext !== "auto" ? ext : autoQuality(degree, key, harmonicDominant)
7476
+ quality: ext !== void 0 && ext !== "auto" ? ext : autoQuality(degree, key, tonic, harmonicDominant)
7145
7477
  };
7146
7478
  if (degree >= 1 && degree <= 7) {
7147
7479
  step.degree = degree;
@@ -7174,17 +7506,22 @@ function generateProgression(opts) {
7174
7506
  assertRecord(opts, "progression options");
7175
7507
  assertPositiveInt(opts.bars, "progression bars");
7176
7508
  assertGenerationBudget(opts.bars, "progression chords");
7177
- const key = toKeyScale(opts.key);
7509
+ const { scale: key, tonic } = resolveKey(opts.key);
7178
7510
  const ctx = resolveContext(opts.ctx);
7179
7511
  let preset;
7180
7512
  if (opts.preset !== void 0) {
7181
- 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) {
7182
7519
  throw new InvalidInputError("progression preset must name at least one degree");
7183
7520
  }
7184
7521
  preset = {
7185
- id: opts.preset.id ?? "custom",
7186
- name: opts.preset.name ?? "Custom",
7187
- degrees: opts.preset.degrees.map((degree, index) => {
7522
+ id: custom.id ?? "custom",
7523
+ name: custom.name ?? "Custom",
7524
+ degrees: degrees.map((degree, index) => {
7188
7525
  assertDegree(degree, `progression preset degrees[${index}]`);
7189
7526
  const supported = degree >= 1 && degree <= 7 || Object.values(BORROWED_DEGREES).includes(degree);
7190
7527
  if (!supported) {
@@ -7194,8 +7531,12 @@ function generateProgression(opts) {
7194
7531
  }
7195
7532
  return degree;
7196
7533
  }),
7197
- functional: opts.preset.functional ?? "loop",
7198
- 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]
7199
7540
  };
7200
7541
  }
7201
7542
  if (preset === void 0 && opts.presetId !== void 0) {
@@ -7209,6 +7550,7 @@ function generateProgression(opts) {
7209
7550
  const cycle = resolveCycle(
7210
7551
  preset?.degrees ?? [0],
7211
7552
  key,
7553
+ tonic,
7212
7554
  ext,
7213
7555
  preset?.functional === "cadenceStrong"
7214
7556
  );
@@ -7241,10 +7583,11 @@ function generateProgression(opts) {
7241
7583
  const domRoot = (next.rootPc + 7) % 12;
7242
7584
  const targetsTonic = (next.rootPc - tonicPc + 12) % 12 === 0;
7243
7585
  const isLastTonicStatement = cur.rootPc === tonicPc && tonicStatements <= 1;
7244
- 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);
7245
7588
  const isResolutionTarget = chords[i - 1]?.secondaryDominant === true;
7246
7589
  const takesNoDominant = !canBeTonicized(next, tonicizable);
7247
- if (targetsTonic || isLastTonicStatement || alreadySecondary || isResolutionTarget || takesNoDominant) {
7590
+ if (targetsTonic || isLastTonicStatement || alreadySecondary || isResolutionTarget || takesNoDominant || isNativeDominant) {
7248
7591
  continue;
7249
7592
  }
7250
7593
  if (draw.prob(harmonic, "reharmonize", i)) {
@@ -7282,11 +7625,11 @@ function triadQualityOf(chord) {
7282
7625
  }
7283
7626
  return hasThird;
7284
7627
  }
7285
- function diatonicTriadsOf(key) {
7628
+ function diatonicTriadsOf(key, tonic) {
7286
7629
  const degrees = scaleTonesInDegreeOrder(key).length;
7287
7630
  const triads = [];
7288
7631
  for (let degree = 1; degree <= degrees; degree += 1) {
7289
- triads.push(diatonicTriad(degree, key));
7632
+ triads.push(diatonicTriad(degree, { ...key, tonic }));
7290
7633
  }
7291
7634
  return triads;
7292
7635
  }
@@ -7320,10 +7663,22 @@ function tritoneSubstituteRoot(dominant, tonic, key) {
7320
7663
  const resolution = spellPitchClass(pitchClassOf(dominant.rootPc + 5), tonic, key);
7321
7664
  return transposeNote(resolution, 1);
7322
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
+ }
7323
7677
  function substituteChord(chord, key, opts) {
7324
7678
  assertOptions(opts, "opts");
7325
7679
  const source = toChordData(chord);
7326
- const { scale, tonic } = resolveKey(key);
7680
+ const resolved = resolveKey(key);
7681
+ const { scale, tonic } = resolved;
7327
7682
  const originalTriad = chordPitchClasses(makeChord(source.rootPc, triadQualityOf(source)));
7328
7683
  const candidates = [];
7329
7684
  if (soundsDominantSeventh(source)) {
@@ -7333,24 +7688,27 @@ function substituteChord(chord, key, opts) {
7333
7688
  type: "tritone"
7334
7689
  });
7335
7690
  }
7336
- for (const triad of diatonicTriadsOf(scale)) {
7691
+ for (const triad of diatonicTriadsOf(scale, tonic)) {
7337
7692
  const offset = pitchClassOf(triad.rootPc - source.rootPc);
7338
7693
  if (THIRD_OFFSETS.includes(offset) && commonToneCount(originalTriad, chordPitchClasses(triad)) === 2) {
7339
7694
  candidates.push({ chord: spelledInKey(triad, tonic, scale), type: "relative" });
7340
7695
  }
7341
7696
  }
7342
- const targetFunction = functionOf(source, scale);
7697
+ const targetFunction = functionOf(source, resolved);
7343
7698
  const parallel = parallelKey(scale);
7344
- for (const triad of diatonicTriadsOf(parallel)) {
7345
- 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)) {
7346
7701
  candidates.push({ chord: spelledInKey(triad, tonic, parallel), type: "borrowed" });
7347
7702
  }
7348
7703
  }
7349
7704
  for (const offset of THIRD_OFFSETS) {
7350
7705
  for (const quality of ["maj", "min"]) {
7351
7706
  const mediant = makeChord(pitchClassOf(source.rootPc + offset), quality);
7352
- if (commonToneCount(originalTriad, chordPitchClasses(mediant)) === 1 && !isDiatonic(mediant, scale)) {
7353
- 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
+ });
7354
7712
  }
7355
7713
  }
7356
7714
  }
@@ -7375,14 +7733,15 @@ function substituteChord(chord, key, opts) {
7375
7733
  results.push({
7376
7734
  chord: candidate.chord,
7377
7735
  type: candidate.type,
7378
- roman: chordToRoman(candidate.chord, scale),
7379
- function: functionOf(candidate.chord, scale)
7736
+ roman: chordToRoman(candidate.chord, resolved),
7737
+ function: functionOf(candidate.chord, resolved)
7380
7738
  });
7381
7739
  }
7382
7740
  return results;
7383
7741
  }
7384
7742
  function modalInterchangePalette(key) {
7385
- const { scale, tonic } = resolveKey(key);
7743
+ const resolved = resolveKey(key);
7744
+ const { scale, tonic } = resolved;
7386
7745
  const palette = [];
7387
7746
  const seen = /* @__PURE__ */ new Set();
7388
7747
  const add = (chord) => {
@@ -7393,18 +7752,18 @@ function modalInterchangePalette(key) {
7393
7752
  seen.add(dedupeKey);
7394
7753
  palette.push({
7395
7754
  chord,
7396
- roman: chordToRoman(chord, scale),
7397
- source: borrowedSource(chord, scale)
7755
+ roman: chordToRoman(chord, resolved),
7756
+ source: borrowedSource(chord, resolved)
7398
7757
  });
7399
7758
  };
7400
7759
  const parallel = parallelKey(scale);
7401
- for (const triad of diatonicTriadsOf(parallel)) {
7402
- 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) {
7403
7762
  add(spelledInKey(triad, tonic, parallel));
7404
7763
  }
7405
7764
  }
7406
7765
  const neapolitan = makeChord(pitchClassOf(scale.rootPc + 1), "maj");
7407
- if (!isDiatonic(neapolitan, scale)) {
7766
+ if (!isDiatonic(neapolitan, resolved)) {
7408
7767
  add(spelledOnRoot(neapolitan, transposeNote(tonic, 1), tonic, scale));
7409
7768
  }
7410
7769
  return palette;
@@ -7433,12 +7792,13 @@ var DEFAULT_RHYTHM_VELOCITY = 96;
7433
7792
  function rhythmToNoteEvents(events, pitch, velocity = DEFAULT_RHYTHM_VELOCITY) {
7434
7793
  assertMidiPitch(pitch, "pitch");
7435
7794
  assertInteger(velocity, "velocity", 0, 127);
7436
- return assertArray(events, "events").map((event) => ({
7437
- pitch,
7438
- startBeat: event.position,
7439
- durationBeat: event.duration,
7440
- velocity
7441
- }));
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
+ });
7442
7802
  }
7443
7803
  var DEFAULT_SUBDIVISION = 2;
7444
7804
  var DEFAULT_DENSITY = 0.5;
@@ -7533,10 +7893,14 @@ export {
7533
7893
  thin,
7534
7894
  double,
7535
7895
  halfTime,
7896
+ halfTimeFigure,
7536
7897
  doubleTime,
7898
+ doubleTimeFigure,
7537
7899
  syncopate,
7538
7900
  ornamentBy,
7539
7901
  deform,
7902
+ resolveDials,
7903
+ deformFigure,
7540
7904
  withinCeiling,
7541
7905
  isLickMaterial,
7542
7906
  BASS_LICKS,
@@ -7588,4 +7952,4 @@ export {
7588
7952
  generateRhythm,
7589
7953
  rhythmDensity
7590
7954
  };
7591
- //# sourceMappingURL=chunk-CPMB7P75.js.map
7955
+ //# sourceMappingURL=chunk-FK3DGAIZ.js.map