@libraz/libcantus 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +4 -0
  2. package/README_ja.md +4 -0
  3. package/dist/analyze/index.cjs +31 -7
  4. package/dist/analyze/index.cjs.map +1 -1
  5. package/dist/analyze/index.d.cts +155 -9
  6. package/dist/analyze/index.d.ts +155 -9
  7. package/dist/analyze/index.js +37 -13
  8. package/dist/{chunk-TT6PKMPR.cjs → chunk-35NBV2FT.cjs} +16 -16
  9. package/dist/{chunk-TT6PKMPR.cjs.map → chunk-35NBV2FT.cjs.map} +1 -1
  10. package/dist/{chunk-YPB3A3EY.cjs → chunk-3R3QVKY5.cjs} +314 -285
  11. package/dist/chunk-3R3QVKY5.cjs.map +1 -0
  12. package/dist/{chunk-GDKE6RQW.cjs → chunk-4R2LWV7E.cjs} +691 -413
  13. package/dist/chunk-4R2LWV7E.cjs.map +1 -0
  14. package/dist/{chunk-RVS4G4MR.cjs → chunk-76ZBTRG2.cjs} +198 -198
  15. package/dist/{chunk-RVS4G4MR.cjs.map → chunk-76ZBTRG2.cjs.map} +1 -1
  16. package/dist/{chunk-X6RJAHII.js → chunk-7KG3RTYS.js} +2 -2
  17. package/dist/{chunk-PQL7E5F6.js → chunk-AGOVW7U6.js} +1 -1
  18. package/dist/{chunk-PQL7E5F6.js.map → chunk-AGOVW7U6.js.map} +1 -1
  19. package/dist/{chunk-5KW7DNCJ.cjs → chunk-B2PW6EHN.cjs} +2423 -453
  20. package/dist/chunk-B2PW6EHN.cjs.map +1 -0
  21. package/dist/{chunk-J772DL3B.cjs → chunk-CGBADIO5.cjs} +53 -53
  22. package/dist/{chunk-J772DL3B.cjs.map → chunk-CGBADIO5.cjs.map} +1 -1
  23. package/dist/{chunk-R5VUXQMP.js → chunk-CPMB7P75.js} +1978 -8
  24. package/dist/chunk-CPMB7P75.js.map +1 -0
  25. package/dist/{chunk-VLE5R7PF.js → chunk-GNVN4PPJ.js} +1874 -8
  26. package/dist/chunk-GNVN4PPJ.js.map +1 -0
  27. package/dist/{chunk-Q6R3QAZD.cjs → chunk-IFZDWIKM.cjs} +2199 -333
  28. package/dist/chunk-IFZDWIKM.cjs.map +1 -0
  29. package/dist/{chunk-DKICR7AN.js → chunk-IPEF6NWB.js} +296 -18
  30. package/dist/chunk-IPEF6NWB.js.map +1 -0
  31. package/dist/{chunk-AWEMD747.js → chunk-M5RYMAFI.js} +3 -3
  32. package/dist/{chunk-HSY2VQXZ.js → chunk-MLBPXA4Z.js} +330 -728
  33. package/dist/chunk-MLBPXA4Z.js.map +1 -0
  34. package/dist/{chunk-H66ARFU5.js → chunk-NB25P3EH.js} +3 -3
  35. package/dist/{chunk-5JJ6EHEZ.cjs → chunk-NM7UAMC3.cjs} +62 -62
  36. package/dist/{chunk-5JJ6EHEZ.cjs.map → chunk-NM7UAMC3.cjs.map} +1 -1
  37. package/dist/{chunk-R7B5N2JK.cjs → chunk-PM4XJE2Y.cjs} +47 -47
  38. package/dist/{chunk-R7B5N2JK.cjs.map → chunk-PM4XJE2Y.cjs.map} +1 -1
  39. package/dist/{chunk-7GOKQJTQ.js → chunk-PUKJP2Z5.js} +2 -2
  40. package/dist/{chunk-QCXYKSZS.cjs → chunk-PWC4CRWT.cjs} +19 -19
  41. package/dist/{chunk-QCXYKSZS.cjs.map → chunk-PWC4CRWT.cjs.map} +1 -1
  42. package/dist/{chunk-FPBZFNCD.js → chunk-RFIBF2RR.js} +47 -18
  43. package/dist/{chunk-FPBZFNCD.js.map → chunk-RFIBF2RR.js.map} +1 -1
  44. package/dist/{chunk-XKSO2V5I.cjs → chunk-S5BKZZGM.cjs} +1 -1
  45. package/dist/{chunk-XKSO2V5I.cjs.map → chunk-S5BKZZGM.cjs.map} +1 -1
  46. package/dist/{chunk-HOHFYGKW.js → chunk-UMWZ4ZET.js} +2 -2
  47. package/dist/{chunk-XVMJO3QF.cjs → chunk-YFA7X2F6.cjs} +489 -887
  48. package/dist/chunk-YFA7X2F6.cjs.map +1 -0
  49. package/dist/{chunk-57WB2EDF.js → chunk-ZBUROTJQ.js} +3 -3
  50. package/dist/{coerce-wwdELNYS.d.ts → coerce-DP0vYu-x.d.ts} +77 -1
  51. package/dist/{coerce-CJlgULrQ.d.cts → coerce-DRERzdve.d.cts} +77 -1
  52. package/dist/core/index.cjs +5 -5
  53. package/dist/core/index.d.cts +2 -2
  54. package/dist/core/index.d.ts +2 -2
  55. package/dist/core/index.js +4 -4
  56. package/dist/generate/index.cjs +20 -8
  57. package/dist/generate/index.cjs.map +1 -1
  58. package/dist/generate/index.d.cts +87 -9
  59. package/dist/generate/index.d.ts +87 -9
  60. package/dist/generate/index.js +19 -7
  61. package/dist/{index-BaoKVVhE.d.cts → index-B9QLziDe.d.cts} +2 -2
  62. package/dist/{index-CnHQgqNh.d.ts → index-BHIUyjhZ.d.ts} +2 -2
  63. package/dist/{index-CBCP84rv.d.cts → index-BL5N2Oyz.d.cts} +1 -1
  64. package/dist/{index-D_hzxD8w.d.cts → index-BMweUlm5.d.cts} +3 -0
  65. package/dist/{index-Ci3xyows.d.ts → index-CL4b4gUQ.d.ts} +3 -0
  66. package/dist/{index-BzLNkmA7.d.cts → index-CY-3NVfy.d.cts} +486 -5
  67. package/dist/{index-CnIdP4EZ.d.ts → index-Dw2DP7vm.d.ts} +1 -1
  68. package/dist/{index-DpC-mDlV.d.ts → index-zaZdmnU4.d.ts} +486 -5
  69. package/dist/index.cjs +49 -13
  70. package/dist/index.cjs.map +1 -1
  71. package/dist/index.d.cts +10 -10
  72. package/dist/index.d.ts +10 -10
  73. package/dist/index.js +55 -19
  74. package/dist/{masks-O73Rhj5d.d.ts → masks-Cc7-MzzY.d.ts} +1 -1
  75. package/dist/{masks-C8TWLFIi.d.cts → masks-pADfTOiD.d.cts} +1 -1
  76. package/dist/model/index.cjs +12 -12
  77. package/dist/model/index.d.cts +213 -16
  78. package/dist/model/index.d.ts +213 -16
  79. package/dist/model/index.js +11 -11
  80. package/dist/{index-DTsKaPaC.d.cts → profile-CQMEqdH9.d.cts} +88 -601
  81. package/dist/{index-BUtIf2go.d.ts → profile-DLdlIpS9.d.ts} +88 -601
  82. package/dist/theory/index.cjs +5 -5
  83. package/dist/theory/index.d.cts +7 -7
  84. package/dist/theory/index.d.ts +7 -7
  85. package/dist/theory/index.js +4 -4
  86. package/dist/{index-BpWOhkkE.d.ts → types-D-XgxGvp.d.cts} +1170 -14
  87. package/dist/{index-B3WCj_Hm.d.cts → types-Dh7C064C.d.ts} +1170 -14
  88. package/package.json +6 -5
  89. package/dist/chunk-5KW7DNCJ.cjs.map +0 -1
  90. package/dist/chunk-DKICR7AN.js.map +0 -1
  91. package/dist/chunk-GDKE6RQW.cjs.map +0 -1
  92. package/dist/chunk-HSY2VQXZ.js.map +0 -1
  93. package/dist/chunk-Q6R3QAZD.cjs.map +0 -1
  94. package/dist/chunk-R5VUXQMP.js.map +0 -1
  95. package/dist/chunk-VLE5R7PF.js.map +0 -1
  96. package/dist/chunk-XVMJO3QF.cjs.map +0 -1
  97. package/dist/chunk-YPB3A3EY.cjs.map +0 -1
  98. /package/dist/{chunk-X6RJAHII.js.map → chunk-7KG3RTYS.js.map} +0 -0
  99. /package/dist/{chunk-AWEMD747.js.map → chunk-M5RYMAFI.js.map} +0 -0
  100. /package/dist/{chunk-H66ARFU5.js.map → chunk-NB25P3EH.js.map} +0 -0
  101. /package/dist/{chunk-7GOKQJTQ.js.map → chunk-PUKJP2Z5.js.map} +0 -0
  102. /package/dist/{chunk-HOHFYGKW.js.map → chunk-UMWZ4ZET.js.map} +0 -0
  103. /package/dist/{chunk-57WB2EDF.js.map → chunk-ZBUROTJQ.js.map} +0 -0
@@ -1,16 +1,40 @@
1
1
  import {
2
+ CADENCE_TYPES,
3
+ DEFAULT_VARIATION_THRESHOLD,
2
4
  HUMANIZE_ADJACENCY,
5
+ MELODIC_CONTOUR_SHAPES,
6
+ MOTIF_RELATION_KINDS,
7
+ RHYTHM_IOI_BINS,
8
+ analyzeRhythm,
9
+ analyzeVoice,
3
10
  assertChordTimeline,
11
+ assertReferenceProfile,
12
+ barSpanOf,
4
13
  borrowedSource,
14
+ chordTimelineFromChords,
5
15
  chordToRoman,
16
+ contourSimilarity,
17
+ densitySimilarity,
6
18
  detectChordBest,
19
+ durationSimilarity,
7
20
  functionOf,
21
+ ioiBinIndex,
8
22
  isDiatonic,
9
23
  isMinorKey,
10
- parallelKey
11
- } from "./chunk-VLE5R7PF.js";
24
+ keyLookup,
25
+ melodicSimilarity,
26
+ onsetSimilarity,
27
+ parallelKey,
28
+ phraseMelody,
29
+ registerSimilarity,
30
+ rhythmLevel,
31
+ romanToChord,
32
+ syncopationSimilarity
33
+ } from "./chunk-GNVN4PPJ.js";
12
34
  import {
35
+ ALGORITHM_VERSION,
13
36
  LIMBS,
37
+ MIN_ALGORITHM_VERSION,
14
38
  canSound,
15
39
  createPositionalRng,
16
40
  deriveSeed,
@@ -19,14 +43,14 @@ import {
19
43
  instrumentRange,
20
44
  resolveAlgorithmVersion,
21
45
  toStringedProfile
22
- } from "./chunk-X6RJAHII.js";
46
+ } from "./chunk-7KG3RTYS.js";
23
47
  import {
24
48
  createNoteEventIndex
25
- } from "./chunk-7GOKQJTQ.js";
49
+ } from "./chunk-PUKJP2Z5.js";
26
50
  import {
27
51
  chordScales,
28
52
  roleOf
29
- } from "./chunk-H66ARFU5.js";
53
+ } from "./chunk-NB25P3EH.js";
30
54
  import {
31
55
  chordPitchClasses,
32
56
  chordQualities,
@@ -50,26 +74,33 @@ import {
50
74
  resolveKey,
51
75
  scaleLadderPitch,
52
76
  scaleLadderPosition,
77
+ scaleOf,
53
78
  scaleSystemOf,
54
79
  scaleTonesInDegreeOrder,
55
80
  shiftByScaleDegrees,
56
81
  soundsDominantSeventh,
82
+ spanFromChord,
57
83
  spellPitchClass,
58
84
  toChordData,
59
85
  toKeyScale,
60
86
  tonicizableDegrees
61
- } from "./chunk-FPBZFNCD.js";
87
+ } from "./chunk-RFIBF2RR.js";
62
88
  import {
63
89
  ARTICULATIONS,
64
90
  BEAT_EPS,
65
91
  InvalidInputError,
92
+ NoSolutionError,
93
+ allocateCandidateTable,
94
+ allocateChoiceTable,
66
95
  assertArray,
67
96
  assertDegree,
68
97
  assertFiniteNumber,
69
98
  assertFiniteSemitones,
99
+ assertFlag,
70
100
  assertFunction,
71
101
  assertGenerationBudget,
72
102
  assertInteger,
103
+ assertMeterMap,
73
104
  assertMidiPitch,
74
105
  assertNoteEvents,
75
106
  assertOneOf,
@@ -78,7 +109,9 @@ import {
78
109
  assertRange,
79
110
  assertRecord,
80
111
  assertTimeSignature,
112
+ barStartBeat,
81
113
  beatsPerBar,
114
+ beatsPerBarAt,
82
115
  clampToMidi,
83
116
  describeRejected,
84
117
  dropSilentNotes,
@@ -92,10 +125,12 @@ import {
92
125
  pulseBeats,
93
126
  resolveMeters,
94
127
  soundingNotesOnly,
128
+ spelledInterval,
95
129
  toMeterData,
96
130
  toSpelledInterval,
131
+ transposeByInterval,
97
132
  transposeNote
98
- } from "./chunk-PQL7E5F6.js";
133
+ } from "./chunk-AGOVW7U6.js";
99
134
 
100
135
  // src/generate/context/draw.ts
101
136
  var DRAW_METHODS = ["at", "prob", "range", "float"];
@@ -4837,6 +4872,1063 @@ function developMotif(cell, timeline, key, bars, ts) {
4837
4872
  return inTimeOrder({ notes: out });
4838
4873
  }
4839
4874
 
4875
+ // src/generate/plan/internal.ts
4876
+ var STRONG_WEIGHT = 2;
4877
+ function positionClass(beat, meters) {
4878
+ const weight = metricWeight(beat, meters);
4879
+ return weight >= STRONG_WEIGHT ? "strong" : weight > 0 ? "weak" : "off";
4880
+ }
4881
+
4882
+ // src/generate/plan/types.ts
4883
+ var COMPOSITION_PLAN_VERSION = 1;
4884
+ function tent(t, apex) {
4885
+ if (t <= apex) {
4886
+ return apex === 0 ? 1 : -1 + 2 * t / apex;
4887
+ }
4888
+ return apex === 1 ? 1 : 1 - 2 * (t - apex) / (1 - apex);
4889
+ }
4890
+ function plannedContourAt(shape, peakPosition, t) {
4891
+ switch (shape) {
4892
+ case "arch":
4893
+ return tent(t, peakPosition);
4894
+ case "ascending":
4895
+ return -1 + 2 * t;
4896
+ case "descending":
4897
+ return 1 - 2 * t;
4898
+ case "wave":
4899
+ return t < 0.5 ? tent(2 * t, 0.5) : tent(2 * t - 1, 0.5);
4900
+ case "static":
4901
+ return 0;
4902
+ }
4903
+ }
4904
+
4905
+ // src/generate/plan/validate.ts
4906
+ var MASS_TOLERANCE = 1e-9;
4907
+ function assertString(value, name) {
4908
+ if (typeof value !== "string") {
4909
+ throw new InvalidInputError(`${name} must be a string; received ${describeRejected(value)}`);
4910
+ }
4911
+ return value;
4912
+ }
4913
+ function assertSpanOrder(startBeat, endBeat, name) {
4914
+ if (endBeat < startBeat) {
4915
+ throw new InvalidInputError(
4916
+ `${name}.endBeat must be at least ${name}.startBeat; received ${endBeat} < ${startBeat}`
4917
+ );
4918
+ }
4919
+ }
4920
+ function assertNullable(value, name, validate) {
4921
+ if (value === void 0) {
4922
+ throw new InvalidInputError(`${name} must be present; received undefined`);
4923
+ }
4924
+ return value === null ? null : validate(value, name);
4925
+ }
4926
+ function assertMassDistribution(value, length, name) {
4927
+ const shares = assertArray(value, name);
4928
+ if (shares.length !== length) {
4929
+ throw new InvalidInputError(`${name} must have length ${length}; received ${shares.length}`);
4930
+ }
4931
+ let sum = 0;
4932
+ for (let index = 0; index < shares.length; index += 1) {
4933
+ const share = assertFiniteNumber(shares[index], `${name}[${index}]`);
4934
+ if (share < 0) {
4935
+ throw new InvalidInputError(`${name}[${index}] must be at least 0; received ${share}`);
4936
+ }
4937
+ sum += share;
4938
+ }
4939
+ if (Math.abs(sum - 1) > MASS_TOLERANCE) {
4940
+ throw new InvalidInputError(`${name} must sum to 1; received ${sum}`);
4941
+ }
4942
+ return shares;
4943
+ }
4944
+ function assertResolvedKeyField(value, name) {
4945
+ const read = assertRecord(value, name);
4946
+ try {
4947
+ resolveKey(read);
4948
+ } catch (error) {
4949
+ if (error instanceof InvalidInputError) {
4950
+ throw new InvalidInputError(`${name} must be a valid key: ${error.message}`);
4951
+ }
4952
+ throw error;
4953
+ }
4954
+ return read;
4955
+ }
4956
+ function assertPlannedSection(value, name) {
4957
+ const read = assertRecord(value, name);
4958
+ assertString(read.label, `${name}.label`);
4959
+ const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
4960
+ const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
4961
+ assertSpanOrder(startBeat, endBeat, name);
4962
+ return read;
4963
+ }
4964
+ function assertPlannedPhrase(value, name, sectionCount, motifCount) {
4965
+ const read = assertRecord(value, name);
4966
+ const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
4967
+ const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
4968
+ assertSpanOrder(startBeat, endBeat, name);
4969
+ assertNullable(
4970
+ read.section,
4971
+ `${name}.section`,
4972
+ (v, n) => assertInteger(v, n, 0, Math.max(sectionCount - 1, 0))
4973
+ );
4974
+ assertNullable(read.cadence, `${name}.cadence`, (v, n) => assertOneOf(v, CADENCE_TYPES, n));
4975
+ assertOneOf(read.shape, MELODIC_CONTOUR_SHAPES, `${name}.shape`);
4976
+ assertRange(read.peakPosition, 0, 1, `${name}.peakPosition`);
4977
+ const register = assertRecord(
4978
+ read.register,
4979
+ `${name}.register`
4980
+ );
4981
+ assertMidiPitch(register.low, `${name}.register.low`);
4982
+ assertMidiPitch(register.high, `${name}.register.high`);
4983
+ assertRange(register.mean, 0, 127, `${name}.register.mean`);
4984
+ assertRange(read.onsetDensity, 0, Number.MAX_SAFE_INTEGER, `${name}.onsetDensity`);
4985
+ const motifs = assertArray(read.motifs, `${name}.motifs`);
4986
+ for (let index = 0; index < motifs.length; index += 1) {
4987
+ assertInteger(
4988
+ motifs[index],
4989
+ `${name}.motifs[${index}]`,
4990
+ 0,
4991
+ Math.max(motifCount - 1, 0)
4992
+ );
4993
+ }
4994
+ return read;
4995
+ }
4996
+ function assertPlannedChord(value, name, keys) {
4997
+ const read = assertRecord(value, name);
4998
+ const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
4999
+ const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
5000
+ assertSpanOrder(startBeat, endBeat, name);
5001
+ const keyIndex = assertInteger(read.key, `${name}.key`, 0, Math.max(keys.length - 1, 0));
5002
+ const roman = assertString(read.roman, `${name}.roman`);
5003
+ try {
5004
+ romanToChord(roman, keys[keyIndex]);
5005
+ } catch (error) {
5006
+ if (error instanceof InvalidInputError) {
5007
+ throw new InvalidInputError(
5008
+ `${name}.roman must be readable by romanToChord; received ${describeRejected(roman)}: ${error.message}`
5009
+ );
5010
+ }
5011
+ throw error;
5012
+ }
5013
+ return read;
5014
+ }
5015
+ function assertPlannedMotifRelation(value, name) {
5016
+ const read = assertRecord(value, name);
5017
+ assertOneOf(read.kind, MOTIF_RELATION_KINDS, `${name}.kind`);
5018
+ assertFlag(read.sequence, `${name}.sequence`);
5019
+ assertFiniteNumber(read.semitones, `${name}.semitones`);
5020
+ if (read.kind === "tonalTransposition") {
5021
+ assertFiniteNumber(read.degrees, `${name}.degrees`);
5022
+ } else if (read.degrees !== void 0) {
5023
+ assertFiniteNumber(read.degrees, `${name}.degrees`);
5024
+ }
5025
+ const timeRatio = assertFiniteNumber(read.timeRatio, `${name}.timeRatio`);
5026
+ if (timeRatio <= 0) {
5027
+ throw new InvalidInputError(`${name}.timeRatio must be positive; received ${timeRatio}`);
5028
+ }
5029
+ return read;
5030
+ }
5031
+ function assertPlannedMotif(value, name, ownIndex, motifCount, phraseCount) {
5032
+ const read = assertRecord(value, name);
5033
+ assertInteger(read.phrase, `${name}.phrase`, 0, Math.max(phraseCount - 1, 0));
5034
+ const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
5035
+ const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
5036
+ assertSpanOrder(startBeat, endBeat, name);
5037
+ assertPositiveInt(read.notes, `${name}.notes`);
5038
+ const from = assertNullable(
5039
+ read.from,
5040
+ `${name}.from`,
5041
+ (v, n) => assertInteger(v, n, 0, Math.max(motifCount - 1, 0))
5042
+ );
5043
+ if (from !== null && from >= ownIndex) {
5044
+ throw new InvalidInputError(
5045
+ `${name}.from must be less than this motif's own index (${ownIndex}); received ${from}`
5046
+ );
5047
+ }
5048
+ const relation = assertNullable(read.relation, `${name}.relation`, assertPlannedMotifRelation);
5049
+ if (from === null && relation !== null) {
5050
+ throw new InvalidInputError(
5051
+ `${name}.relation must be null for a root motif (from = null); received a relation`
5052
+ );
5053
+ }
5054
+ const rhythm = assertNullable(read.rhythm, `${name}.rhythm`, (v, n) => assertArray(v, n));
5055
+ if (rhythm !== null) {
5056
+ if (from !== null) {
5057
+ throw new InvalidInputError(`${name}.rhythm must be null for a derived statement`);
5058
+ }
5059
+ if (rhythm.length !== read.notes - 1) {
5060
+ throw new InvalidInputError(
5061
+ `${name}.rhythm must hold notes - 1 (${read.notes - 1}) gaps; received ${rhythm.length}`
5062
+ );
5063
+ }
5064
+ let total = 0;
5065
+ for (let index = 0; index < rhythm.length; index += 1) {
5066
+ const gap = assertFiniteNumber(rhythm[index], `${name}.rhythm[${index}]`);
5067
+ if (gap <= 0) {
5068
+ throw new InvalidInputError(`${name}.rhythm[${index}] must be positive; received ${gap}`);
5069
+ }
5070
+ total += gap;
5071
+ }
5072
+ if (total >= endBeat - startBeat - BEAT_EPS) {
5073
+ throw new InvalidInputError(
5074
+ `${name}.rhythm must sum to less than the statement's length (${endBeat - startBeat}); received ${total}`
5075
+ );
5076
+ }
5077
+ }
5078
+ return read;
5079
+ }
5080
+ function assertPlannedRhythm(value, name) {
5081
+ const read = assertRecord(value, name);
5082
+ assertMassDistribution(read.onsetLevels, 6, `${name}.onsetLevels`);
5083
+ assertMassDistribution(read.interOnsetShares, RHYTHM_IOI_BINS.length, `${name}.interOnsetShares`);
5084
+ assertRange(read.syncopation, 0, 1, `${name}.syncopation`);
5085
+ return read;
5086
+ }
5087
+ function assertCompositionPlan(value, name = "composition plan") {
5088
+ const read = assertRecord(value, name);
5089
+ if (read.planVersion !== COMPOSITION_PLAN_VERSION) {
5090
+ throw new InvalidInputError(
5091
+ `${name}.planVersion must be ${COMPOSITION_PLAN_VERSION}; received ${describeRejected(read.planVersion)}`
5092
+ );
5093
+ }
5094
+ assertInteger(read.seed, `${name}.seed`, 0, 4294967295);
5095
+ assertInteger(
5096
+ read.algorithmVersion,
5097
+ `${name}.algorithmVersion`,
5098
+ MIN_ALGORITHM_VERSION,
5099
+ ALGORITHM_VERSION
5100
+ );
5101
+ const keys = assertArray(read.keys, `${name}.keys`);
5102
+ if (keys.length === 0) {
5103
+ throw new InvalidInputError(`${name}.keys must hold at least one key; received an empty array`);
5104
+ }
5105
+ for (let index = 0; index < keys.length; index += 1) {
5106
+ assertResolvedKeyField(keys[index], `${name}.keys[${index}]`);
5107
+ }
5108
+ assertMeterMap(read.meters, `${name}.meters`);
5109
+ const span = assertRecord(
5110
+ read.span,
5111
+ `${name}.span`
5112
+ );
5113
+ const spanStart = assertFiniteNumber(span.startBeat, `${name}.span.startBeat`);
5114
+ const spanEnd = assertFiniteNumber(span.endBeat, `${name}.span.endBeat`);
5115
+ assertSpanOrder(spanStart, spanEnd, `${name}.span`);
5116
+ assertRange(span.bars, 0, Number.MAX_SAFE_INTEGER, `${name}.span.bars`);
5117
+ const sections = assertArray(read.sections, `${name}.sections`);
5118
+ for (let index = 0; index < sections.length; index += 1) {
5119
+ assertPlannedSection(sections[index], `${name}.sections[${index}]`);
5120
+ }
5121
+ const phrases = assertArray(read.phrases, `${name}.phrases`);
5122
+ const motifs = assertArray(read.motifs, `${name}.motifs`);
5123
+ for (let index = 0; index < phrases.length; index += 1) {
5124
+ assertPlannedPhrase(
5125
+ phrases[index],
5126
+ `${name}.phrases[${index}]`,
5127
+ sections.length,
5128
+ motifs.length
5129
+ );
5130
+ }
5131
+ const harmony = assertArray(read.harmony, `${name}.harmony`);
5132
+ for (let index = 0; index < harmony.length; index += 1) {
5133
+ assertPlannedChord(harmony[index], `${name}.harmony[${index}]`, keys);
5134
+ }
5135
+ for (let index = 0; index < motifs.length; index += 1) {
5136
+ assertPlannedMotif(
5137
+ motifs[index],
5138
+ `${name}.motifs[${index}]`,
5139
+ index,
5140
+ motifs.length,
5141
+ phrases.length
5142
+ );
5143
+ }
5144
+ assertPlannedRhythm(read.rhythm, `${name}.rhythm`);
5145
+ return read;
5146
+ }
5147
+
5148
+ // src/generate/plan/timeline.ts
5149
+ function planTimeline(plan) {
5150
+ return planChordTimeline(assertCompositionPlan(plan));
5151
+ }
5152
+ function planChordTimeline(plan) {
5153
+ const spans = plan.harmony.map((chord) => {
5154
+ const key = plan.keys[chord.key];
5155
+ return spanFromChord(romanToChord(chord.roman, key), chord.startBeat);
5156
+ });
5157
+ return chordTimelineFromChords(spans, plan.span.endBeat);
5158
+ }
5159
+
5160
+ // src/generate/plan/harmony.ts
5161
+ var HARMONY_OK_KINDS = /* @__PURE__ */ new Set([
5162
+ "chordTone",
5163
+ "tension",
5164
+ "suspension",
5165
+ "appoggiatura",
5166
+ "anticipation",
5167
+ "passing",
5168
+ "neighbor"
5169
+ ]);
5170
+ var CADENCE_DEGREES = {
5171
+ authentic: [1, 3],
5172
+ plagal: [1, 3],
5173
+ half: [2, 5, 7],
5174
+ phrygian: [2, 5, 7],
5175
+ modal: [1]
5176
+ };
5177
+ function planKeyAt(plan) {
5178
+ const regions = [...plan.harmony].sort((a, b) => a.startBeat - b.startBeat).map((chord) => ({
5179
+ startBeat: chord.startBeat,
5180
+ endBeat: chord.endBeat,
5181
+ key: plan.keys[chord.key],
5182
+ confidence: 1
5183
+ }));
5184
+ return keyLookup(regions, plan.keys[0]);
5185
+ }
5186
+ function cadencePitchClasses(plan, timeline, cadence, beat) {
5187
+ if (cadence === "deceptive") {
5188
+ const chord = timeline.at(beat);
5189
+ return chord === null ? null : chordPitchClasses(chord);
5190
+ }
5191
+ 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);
5194
+ }
5195
+ function planHarmonyMisfits(plan, timeline = planChordTimeline(plan)) {
5196
+ const keyAt = planKeyAt(plan);
5197
+ const scaleAt2 = (beat) => scaleOf(keyAt(beat));
5198
+ return (line) => {
5199
+ const analyzed = analyzeVoice(line, timeline.at, scaleAt2);
5200
+ return line.map((note2, index) => {
5201
+ const position = positionClass(note2.startBeat, plan.meters);
5202
+ if (position === "off") {
5203
+ return null;
5204
+ }
5205
+ const kinds = (analyzed[index]?.labels ?? []).map((label) => label.kind);
5206
+ if (kinds.some((kind) => HARMONY_OK_KINDS.has(kind))) {
5207
+ return null;
5208
+ }
5209
+ return position === "weak" && isScaleTone(note2.pitch, scaleAt2(note2.startBeat)) ? null : kinds;
5210
+ });
5211
+ };
5212
+ }
5213
+
5214
+ // src/generate/melody/pitch-dp.ts
5215
+ var NO_LEAP = 0;
5216
+ var LEAP_UP = 1;
5217
+ var LEAP_DOWN = 2;
5218
+ var LEAP_STATES = 3;
5219
+ var SEARCH_LABEL = "melody pitch search";
5220
+ var WIDEST_THIRD = 4;
5221
+ var LEAP_CONTINUATION_COST = 0.5;
5222
+ function intervalCost(size) {
5223
+ if (size === 0) {
5224
+ return 0.2;
5225
+ }
5226
+ if (size <= 2) {
5227
+ return 0;
5228
+ }
5229
+ if (size <= WIDEST_THIRD) {
5230
+ return 0.3;
5231
+ }
5232
+ if (size <= 7) {
5233
+ return 0.6;
5234
+ }
5235
+ return 1;
5236
+ }
5237
+ function leapOf(delta) {
5238
+ if (Math.abs(delta) <= WIDEST_THIRD) {
5239
+ return NO_LEAP;
5240
+ }
5241
+ return delta > 0 ? LEAP_UP : LEAP_DOWN;
5242
+ }
5243
+ function stepCost(from, to, leap, bothFixed) {
5244
+ const delta = to - from;
5245
+ const size = Math.abs(delta);
5246
+ if (!bothFixed && isForbiddenMelodicLeap(from, to)) {
5247
+ return Number.POSITIVE_INFINITY;
5248
+ }
5249
+ let cost = intervalCost(size);
5250
+ if (leap === LEAP_UP && delta > 0 || leap === LEAP_DOWN && delta < 0) {
5251
+ cost += LEAP_CONTINUATION_COST;
5252
+ }
5253
+ return cost;
5254
+ }
5255
+ function searchPitches(slots, budget, extra) {
5256
+ const first = slots[0];
5257
+ if (first === void 0) {
5258
+ return [];
5259
+ }
5260
+ let previous = allocateCandidateTable(
5261
+ first.candidates.length,
5262
+ LEAP_STATES,
5263
+ SEARCH_LABEL,
5264
+ budget
5265
+ ).fill(Number.POSITIVE_INFINITY);
5266
+ for (let j = 0; j < first.candidates.length; j += 1) {
5267
+ previous[j * LEAP_STATES + NO_LEAP] = first.cost(first.candidates[j]);
5268
+ }
5269
+ const backs = [];
5270
+ for (let k = 1; k < slots.length; k += 1) {
5271
+ const before = slots[k - 1];
5272
+ const slot = slots[k];
5273
+ const unary = slot.candidates.map((pitch) => slot.cost(pitch));
5274
+ const current = allocateCandidateTable(
5275
+ slot.candidates.length,
5276
+ LEAP_STATES,
5277
+ SEARCH_LABEL,
5278
+ budget
5279
+ ).fill(Number.POSITIVE_INFINITY);
5280
+ const back = allocateChoiceTable(
5281
+ slot.candidates.length,
5282
+ LEAP_STATES,
5283
+ SEARCH_LABEL,
5284
+ budget
5285
+ ).fill(-1);
5286
+ const bothFixed = before.fixed && slot.fixed;
5287
+ for (let state2 = 0; state2 < previous.length; state2 += 1) {
5288
+ const reached = previous[state2];
5289
+ if (reached === Number.POSITIVE_INFINITY) {
5290
+ continue;
5291
+ }
5292
+ const from = before.candidates[Math.floor(state2 / LEAP_STATES)];
5293
+ const leap = state2 % LEAP_STATES;
5294
+ for (let j = 0; j < slot.candidates.length; j += 1) {
5295
+ const to = slot.candidates[j];
5296
+ const step = stepCost(from, to, leap, bothFixed);
5297
+ if (step === Number.POSITIVE_INFINITY) {
5298
+ continue;
5299
+ }
5300
+ const total = reached + step + unary[j] + (extra ? extra(k, from, to) : 0);
5301
+ const index = j * LEAP_STATES + leapOf(to - from);
5302
+ if (total < current[index]) {
5303
+ current[index] = total;
5304
+ back[index] = state2;
5305
+ }
5306
+ }
5307
+ }
5308
+ backs.push(back);
5309
+ previous = current;
5310
+ }
5311
+ let best = -1;
5312
+ for (let state2 = 0; state2 < previous.length; state2 += 1) {
5313
+ if (previous[state2] < Number.POSITIVE_INFINITY && (best < 0 || previous[state2] < previous[best])) {
5314
+ best = state2;
5315
+ }
5316
+ }
5317
+ if (best < 0) {
5318
+ return null;
5319
+ }
5320
+ const pitches = new Array(slots.length);
5321
+ let state = best;
5322
+ for (let k = slots.length - 1; k >= 0; k -= 1) {
5323
+ pitches[k] = slots[k].candidates[Math.floor(state / LEAP_STATES)];
5324
+ if (k > 0) {
5325
+ state = backs[k - 1][state];
5326
+ }
5327
+ }
5328
+ return pitches;
5329
+ }
5330
+
5331
+ // src/generate/melody/derive.ts
5332
+ var VARIATION_STEP_COST = 0.2;
5333
+ var OCTAVE = 12;
5334
+ var LEVEL_TARGET_COST = 0.1;
5335
+ var LEVEL_PLAN_COST = 0.02;
5336
+ var LEVEL_REACH = 24;
5337
+ var TONAL_LEVEL_REACH = 14;
5338
+ function stretched(cell, timeRatio) {
5339
+ return timeRatio === 1 ? cell : transformMotif(cell, "augment", timeRatio);
5340
+ }
5341
+ function shaped(cell, relation) {
5342
+ switch (relation.kind) {
5343
+ case "repetition":
5344
+ case "tonalTransposition":
5345
+ return cell;
5346
+ case "transposition":
5347
+ return stretched(cell, relation.timeRatio);
5348
+ case "inversion":
5349
+ return stretched(transformMotif(cell, "invert"), relation.timeRatio);
5350
+ case "retrograde":
5351
+ return transformMotif(cell, "retrograde");
5352
+ case "retrogradeInversion":
5353
+ return transformMotif(transformMotif(cell, "invert"), "retrograde");
5354
+ case "augmentation":
5355
+ return transformMotif(cell, "augment", relation.timeRatio);
5356
+ case "diminution":
5357
+ return transformMotif(cell, "diminish", 1 / relation.timeRatio);
5358
+ }
5359
+ }
5360
+ function relationLevels(kind, low = -LEVEL_REACH, high = LEVEL_REACH) {
5361
+ const between = (from, to, skipZero) => {
5362
+ const levels = [];
5363
+ for (let level = Math.ceil(from); level <= to; level += 1) {
5364
+ if (!(skipZero && level === 0)) levels.push(level);
5365
+ }
5366
+ return levels;
5367
+ };
5368
+ switch (kind) {
5369
+ case "repetition":
5370
+ case "augmentation":
5371
+ case "diminution":
5372
+ return [-OCTAVE, 0, OCTAVE];
5373
+ case "transposition":
5374
+ return between(low, high, true);
5375
+ case "tonalTransposition":
5376
+ return between(-TONAL_LEVEL_REACH, TONAL_LEVEL_REACH, true);
5377
+ case "inversion":
5378
+ case "retrograde":
5379
+ case "retrogradeInversion":
5380
+ return between(low, high, false);
5381
+ }
5382
+ }
5383
+ function replayAt(source, relation, level, startBeat, key) {
5384
+ const head = source[0];
5385
+ const cell = shaped({ notes: source.map((n) => ({ ...n })) }, relation);
5386
+ const first = cell.notes[0];
5387
+ const offset = startBeat - first.startBeat;
5388
+ const shift = head.pitch + level - first.pitch;
5389
+ return cell.notes.map((n) => ({
5390
+ ...n,
5391
+ pitch: relation.kind === "tonalTransposition" ? shiftByScaleDegrees(n.pitch, level, key) : n.pitch + shift,
5392
+ startBeat: n.startBeat + offset
5393
+ }));
5394
+ }
5395
+ function clipStatement(notes, endBeat) {
5396
+ return notes.filter((n) => n.startBeat < endBeat - BEAT_EPS).map((n) => ({ ...n, durationBeat: Math.min(n.durationBeat, endBeat - n.startBeat) }));
5397
+ }
5398
+ function outsideCount(notes, frame) {
5399
+ return notes.filter((n) => n.pitch < frame.low || n.pitch > frame.high).length;
5400
+ }
5401
+ function nearerLevel(level, best, planned) {
5402
+ const distance = Math.abs(level - planned);
5403
+ const bestDistance = Math.abs(best - planned);
5404
+ return distance < bestDistance || distance === bestDistance && level < best;
5405
+ }
5406
+ function repairStatement(notes, forced, frame) {
5407
+ const slots = notes.map(
5408
+ (n, index) => forced[index] ? {
5409
+ candidates: frame.candidatesAt(n.startBeat),
5410
+ cost: frame.costAt(n.startBeat),
5411
+ fixed: false
5412
+ } : { candidates: [n.pitch], cost: () => 0, fixed: true }
5413
+ );
5414
+ const pitches = searchPitches(slots, frame.budget);
5415
+ if (pitches === null) {
5416
+ throw new NoSolutionError(
5417
+ `no repair fits the statement into the register ${frame.low}..${frame.high}`,
5418
+ { at: frame.at }
5419
+ );
5420
+ }
5421
+ return notes.map((n, index) => ({ ...n, pitch: pitches[index] }));
5422
+ }
5423
+ function placeDerived(source, relation, startBeat, endBeat, key, frame, before) {
5424
+ const planned = relation.kind === "tonalTransposition" ? relation.degrees ?? 0 : relation.semitones;
5425
+ const replay = (level) => clipStatement(replayAt(source, relation, level, startBeat, key), endBeat);
5426
+ const base = replay(0).map((n) => n.pitch);
5427
+ const levels = relationLevels(
5428
+ relation.kind,
5429
+ Math.max(-LEVEL_REACH, frame.low - Math.min(...base)),
5430
+ Math.min(LEVEL_REACH, frame.high - Math.max(...base))
5431
+ );
5432
+ let best = null;
5433
+ for (const level of levels) {
5434
+ const notes2 = replay(level);
5435
+ if (outsideCount(notes2, frame) > 0) {
5436
+ continue;
5437
+ }
5438
+ const distance = notes2.reduce((sum, n) => sum + Math.abs(n.pitch - frame.targetAt(n.startBeat)), 0) / notes2.length;
5439
+ const cost = frame.misfits(notes2, before) + LEVEL_TARGET_COST * distance + LEVEL_PLAN_COST * Math.abs(level - planned);
5440
+ if (best === null || cost < best.cost || cost === best.cost && nearerLevel(level, best.level, planned)) {
5441
+ best = { level, notes: notes2, cost };
5442
+ }
5443
+ }
5444
+ if (best !== null) {
5445
+ return best.notes;
5446
+ }
5447
+ let fallback = null;
5448
+ for (const level of relationLevels(relation.kind)) {
5449
+ const notes2 = replay(level);
5450
+ const outside = outsideCount(notes2, frame);
5451
+ if (fallback === null || outside < fallback.outside || outside === fallback.outside && nearerLevel(level, fallback.level, planned)) {
5452
+ fallback = { level, notes: notes2, outside };
5453
+ }
5454
+ }
5455
+ const notes = fallback.notes;
5456
+ return repairStatement(
5457
+ notes,
5458
+ notes.map((n) => n.pitch < frame.low || n.pitch > frame.high),
5459
+ frame
5460
+ );
5461
+ }
5462
+ function varyStatement(source, startBeat, frame) {
5463
+ const offset = startBeat - source[0].startBeat;
5464
+ const last = source.length - 1;
5465
+ const inside = (pitch) => pitch >= frame.low && pitch <= frame.high;
5466
+ const outer = [source[0].pitch, source[last].pitch];
5467
+ const shift = [0, OCTAVE, -OCTAVE].find((s) => outer.every((pitch) => inside(pitch + s)));
5468
+ const notes = source.map((n) => ({ ...n, startBeat: n.startBeat + offset }));
5469
+ const slots = notes.map(
5470
+ (n, index) => (index === 0 || index === last) && shift !== void 0 ? { candidates: [n.pitch + shift], cost: () => 0, fixed: true } : {
5471
+ candidates: frame.candidatesAt(n.startBeat),
5472
+ cost: frame.costAt(n.startBeat),
5473
+ fixed: false
5474
+ }
5475
+ );
5476
+ const pitches = searchPitches(
5477
+ slots,
5478
+ frame.budget,
5479
+ (index, from, to) => VARIATION_STEP_COST * Math.abs(
5480
+ to - from - (source[index].pitch - source[index - 1].pitch)
5481
+ )
5482
+ );
5483
+ if (pitches === null) {
5484
+ throw new NoSolutionError(`no variation line fits the register ${frame.low}..${frame.high}`, {
5485
+ at: frame.at
5486
+ });
5487
+ }
5488
+ return notes.map((n, index) => ({ ...n, pitch: pitches[index] }));
5489
+ }
5490
+
5491
+ // src/generate/melody/index.ts
5492
+ var RHYTHM_SUBDIVISION = 4;
5493
+ var OFF_SCALE_COST = 1;
5494
+ var TARGET_COST = 0.1;
5495
+ var TIE_BREAK = 1e-6;
5496
+ var SEARCH_STATES_SQUARED = 9;
5497
+ function melodyContext(plan, input) {
5498
+ const fromPlan = { seed: plan.seed, algorithmVersion: plan.algorithmVersion };
5499
+ if (input === void 0) {
5500
+ return resolveContext(fromPlan);
5501
+ }
5502
+ if (typeof input === "number") {
5503
+ return resolveContext({ ...fromPlan, seed: input });
5504
+ }
5505
+ const read = assertRecord(input, "ctx");
5506
+ if (read.rng !== void 0) {
5507
+ throw new InvalidInputError(
5508
+ "ctx.rng cannot drive a melody: every onset and tie-break is drawn at an address of phrase and beat under a seed; pass a seed instead"
5509
+ );
5510
+ }
5511
+ return resolveContext({
5512
+ ...read,
5513
+ seed: read.seed ?? plan.seed,
5514
+ algorithmVersion: read.algorithmVersion ?? plan.algorithmVersion
5515
+ });
5516
+ }
5517
+ function chordEntryAt(plan, beat) {
5518
+ return plan.harmony.find((chord) => beat >= chord.startBeat && beat < chord.endBeat);
5519
+ }
5520
+ function scaleAt(scene, beat) {
5521
+ return scene.scales[chordEntryAt(scene.plan, beat)?.key ?? 0];
5522
+ }
5523
+ function targetPitch(phrase, beat) {
5524
+ const length = phrase.endBeat - phrase.startBeat;
5525
+ const t = length > 0 ? Math.min(Math.max((beat - phrase.startBeat) / length, 0), 1) : 0;
5526
+ const { low, high, mean } = phrase.register;
5527
+ return mean + (high - low) / 2 * plannedContourAt(phrase.shape, phrase.peakPosition, t);
5528
+ }
5529
+ function registerPitches(phrase) {
5530
+ const pitches = [];
5531
+ for (let pitch = phrase.register.low; pitch <= phrase.register.high; pitch += 1) {
5532
+ pitches.push(pitch);
5533
+ }
5534
+ return pitches;
5535
+ }
5536
+ function frameOf(scene, index) {
5537
+ const phrase = scene.plan.phrases[index];
5538
+ const { low, high } = phrase.register;
5539
+ const register = registerPitches(phrase);
5540
+ const draw = scene.ctx.part("melody");
5541
+ return {
5542
+ low,
5543
+ high,
5544
+ at: phrase.startBeat,
5545
+ budget: scene.budget,
5546
+ candidatesAt: (beat) => {
5547
+ const position = positionClass(beat, scene.plan.meters);
5548
+ const chord = scene.timeline.at(beat);
5549
+ const tones = position === "strong" && chord !== null ? chordPitchClasses(chord) : null;
5550
+ const scale = scaleAt(scene, beat);
5551
+ const candidates = position === "off" ? register : tones !== null ? register.filter((pitch) => tones.includes(pitchClassOf(pitch))) : register.filter((pitch) => isScaleTone(pitch, scale));
5552
+ if (candidates.length === 0) {
5553
+ throw new NoSolutionError(
5554
+ `no ${position} pitch at beat ${beat} fits the register ${low}..${high}`,
5555
+ { at: phrase.startBeat }
5556
+ );
5557
+ }
5558
+ return candidates;
5559
+ },
5560
+ costAt: (beat) => {
5561
+ const off = positionClass(beat, scene.plan.meters) === "off";
5562
+ const scale = scaleAt(scene, beat);
5563
+ const target = targetPitch(phrase, beat);
5564
+ return (pitch) => {
5565
+ let cost = TARGET_COST * Math.abs(pitch - target);
5566
+ if (off && !isScaleTone(pitch, scale)) {
5567
+ cost += OFF_SCALE_COST;
5568
+ }
5569
+ return cost + draw.float(0, TIE_BREAK, index, "tie", beat, pitch);
5570
+ };
5571
+ },
5572
+ targetAt: (beat) => targetPitch(phrase, beat),
5573
+ misfits: (notes, before) => scene.harmonyMisfits([...before, ...notes]).slice(before.length).filter((kinds) => kinds !== null).length
5574
+ };
5575
+ }
5576
+ function membersOf(plan) {
5577
+ return plan.phrases.map((phrase, index) => {
5578
+ const found = new Set(phrase.motifs);
5579
+ plan.motifs.forEach((motif, m) => {
5580
+ if (motif.phrase === index) {
5581
+ found.add(m);
5582
+ }
5583
+ });
5584
+ return [...found].sort(
5585
+ (a, b) => (plan.motifs[a]?.startBeat ?? 0) - (plan.motifs[b]?.startBeat ?? 0) || a - b
5586
+ );
5587
+ });
5588
+ }
5589
+ function segmentsOf(scene, index) {
5590
+ const phrase = scene.plan.phrases[index];
5591
+ const segments = [];
5592
+ let cursor = phrase.startBeat;
5593
+ for (const m of scene.members[index]) {
5594
+ const motif = scene.plan.motifs[m];
5595
+ if (motif === void 0) {
5596
+ continue;
5597
+ }
5598
+ if (motif.startBeat > cursor + BEAT_EPS) {
5599
+ segments.push({ startBeat: cursor, endBeat: motif.startBeat, motif: null });
5600
+ }
5601
+ segments.push({ startBeat: motif.startBeat, endBeat: motif.endBeat, motif: m });
5602
+ cursor = Math.max(cursor, motif.endBeat);
5603
+ }
5604
+ if (phrase.endBeat > cursor + BEAT_EPS) {
5605
+ segments.push({ startBeat: cursor, endBeat: phrase.endBeat, motif: null });
5606
+ }
5607
+ return segments;
5608
+ }
5609
+ function notesAt(onsets, endBeat) {
5610
+ return onsets.map((startBeat, i) => ({
5611
+ pitch: 0,
5612
+ startBeat,
5613
+ durationBeat: (onsets[i + 1] ?? endBeat) - startBeat
5614
+ }));
5615
+ }
5616
+ function onsetSlots(scene, startBeat, endBeat) {
5617
+ const { meters } = scene.plan;
5618
+ const slots = [startBeat];
5619
+ let bar2 = barStartBeat(startBeat, meters);
5620
+ while (bar2 < endBeat - BEAT_EPS) {
5621
+ const barEnd = bar2 + beatsPerBarAt(bar2, meters);
5622
+ for (let k = 0; bar2 + k / RHYTHM_SUBDIVISION < barEnd - BEAT_EPS; k += 1) {
5623
+ const beat = bar2 + k / RHYTHM_SUBDIVISION;
5624
+ if (beat > startBeat + BEAT_EPS && beat < endBeat - BEAT_EPS) {
5625
+ slots.push(beat);
5626
+ }
5627
+ }
5628
+ bar2 = barEnd;
5629
+ }
5630
+ return slots;
5631
+ }
5632
+ function drawOnsets(scene, phraseIndex, startBeat, endBeat, count) {
5633
+ const slots = onsetSlots(scene, startBeat, endBeat);
5634
+ assertGenerationBudget(slots.length * Math.max(1, count - 1), "melody onset slots", scene.budget);
5635
+ if (count - 1 > slots.length - 1) {
5636
+ throw new NoSolutionError(
5637
+ `${count} onsets do not fit the sixteenth grid of beats ${startBeat}..${endBeat}`,
5638
+ { at: scene.plan.phrases[phraseIndex].startBeat }
5639
+ );
5640
+ }
5641
+ const levels = slots.map((beat) => rhythmLevel(beat, scene.plan.meters));
5642
+ const perLevel = /* @__PURE__ */ new Map();
5643
+ for (const level of levels) {
5644
+ perLevel.set(level, (perLevel.get(level) ?? 0) + 1);
5645
+ }
5646
+ const levelWeight = levels.map(
5647
+ (level) => (scene.plan.rhythm.onsetLevels[level] ?? 0) / perLevel.get(level)
5648
+ );
5649
+ const draw = scene.ctx.part("melody");
5650
+ const onsets = [startBeat];
5651
+ let current = 0;
5652
+ for (let k = 1; k < count; k += 1) {
5653
+ const last = slots.length - 1 - (count - 1 - k);
5654
+ const candidates = [];
5655
+ for (let j = current + 1; j <= last; j += 1) candidates.push(j);
5656
+ const gapShare = (j) => scene.plan.rhythm.interOnsetShares[ioiBinIndex(slots[j] - slots[current])] ?? 0;
5657
+ let weights = candidates.map((j) => levelWeight[j] * gapShare(j));
5658
+ if (!weights.some((w) => w > 0)) weights = candidates.map((j) => levelWeight[j]);
5659
+ if (!weights.some((w) => w > 0)) weights = candidates.map(() => 1);
5660
+ const total = weights.reduce((sum, w) => sum + w, 0);
5661
+ let target = draw.float(0, 1, phraseIndex, "onset", startBeat, k) * total;
5662
+ let pick = candidates.length - 1;
5663
+ for (let c = 0; c < candidates.length; c += 1) {
5664
+ target -= weights[c];
5665
+ if (target < 0) {
5666
+ pick = c;
5667
+ break;
5668
+ }
5669
+ }
5670
+ current = candidates[pick];
5671
+ onsets.push(slots[current]);
5672
+ }
5673
+ return notesAt(onsets, endBeat);
5674
+ }
5675
+ function placeRhythm(scene, motif) {
5676
+ const { rhythm, startBeat, endBeat, notes } = motif;
5677
+ if (rhythm === null) {
5678
+ return drawOnsets(scene, motif.phrase, startBeat, endBeat, notes);
5679
+ }
5680
+ const onsets = [startBeat];
5681
+ for (const gap of rhythm) {
5682
+ onsets.push(onsets.at(-1) + gap);
5683
+ }
5684
+ return notesAt(onsets, endBeat);
5685
+ }
5686
+ function searchedLine(rhythm, frame) {
5687
+ const slots = rhythm.map((note2) => ({
5688
+ candidates: frame.candidatesAt(note2.startBeat),
5689
+ cost: frame.costAt(note2.startBeat),
5690
+ fixed: false
5691
+ }));
5692
+ const pitches = searchPitches(slots, frame.budget);
5693
+ if (pitches === null) {
5694
+ throw new NoSolutionError(`no pitch line fits the register ${frame.low}..${frame.high}`, {
5695
+ at: frame.at
5696
+ });
5697
+ }
5698
+ return rhythm.map((note2, i) => ({ ...note2, pitch: pitches[i] }));
5699
+ }
5700
+ function statementOf(scene, m) {
5701
+ const known = scene.statements.get(m);
5702
+ if (known !== void 0) {
5703
+ return known;
5704
+ }
5705
+ scene.inProgress.add(m);
5706
+ const motif = scene.plan.motifs[m];
5707
+ const frame = scene.frames[motif.phrase];
5708
+ let notes;
5709
+ if (motif.from === null) {
5710
+ if (motif.endBeat - motif.startBeat <= BEAT_EPS) {
5711
+ throw new NoSolutionError(`root motif ${m} spans no time to place its notes in`, {
5712
+ at: frame.at
5713
+ });
5714
+ }
5715
+ notes = searchedLine(placeRhythm(scene, motif), frame);
5716
+ } else {
5717
+ const source = statementOf(scene, motif.from);
5718
+ notes = motif.relation === null ? varyStatement(source, motif.startBeat, frame) : placeDerived(
5719
+ source,
5720
+ motif.relation,
5721
+ motif.startBeat,
5722
+ motif.endBeat,
5723
+ scaleAt(scene, motif.startBeat),
5724
+ frame,
5725
+ lineBefore(scene, motif.phrase, motif.startBeat)
5726
+ );
5727
+ }
5728
+ scene.inProgress.delete(m);
5729
+ scene.statements.set(m, notes);
5730
+ return notes;
5731
+ }
5732
+ function freeNoteCount(scene, index, segment) {
5733
+ const phrase = scene.plan.phrases[index];
5734
+ const bars = barSpanOf(segment.startBeat, segment.endBeat, scene.plan.meters);
5735
+ return Math.max(1, Math.round(phrase.onsetDensity * bars));
5736
+ }
5737
+ function freeSpan(scene, index, segment) {
5738
+ const key = `${index}:${segment.startBeat}`;
5739
+ const known = scene.free.get(key);
5740
+ if (known !== void 0) {
5741
+ return known;
5742
+ }
5743
+ const rhythm = drawOnsets(
5744
+ scene,
5745
+ index,
5746
+ segment.startBeat,
5747
+ segment.endBeat,
5748
+ freeNoteCount(scene, index, segment)
5749
+ );
5750
+ const notes = searchedLine(rhythm, scene.frames[index]);
5751
+ scene.free.set(key, notes);
5752
+ return notes;
5753
+ }
5754
+ function lineBefore(scene, index, beat) {
5755
+ const line = [];
5756
+ for (const segment of segmentsOf(scene, index)) {
5757
+ if (segment.endBeat > beat + BEAT_EPS) {
5758
+ break;
5759
+ }
5760
+ if (segment.motif === null) {
5761
+ line.push(...freeSpan(scene, index, segment));
5762
+ } else if (!scene.inProgress.has(segment.motif)) {
5763
+ line.push(...statementOf(scene, segment.motif));
5764
+ }
5765
+ }
5766
+ return line.sort((a, b) => a.startBeat - b.startBeat);
5767
+ }
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
+ function heldNoteIndex(scene, index, line, downbeat) {
5778
+ const phrase = scene.plan.phrases[index];
5779
+ const sounding2 = line.findIndex(
5780
+ (note2) => note2.startBeat <= downbeat + BEAT_EPS && note2.startBeat + note2.durationBeat > downbeat + BEAT_EPS
5781
+ );
5782
+ return sounding2 >= 0 ? sounding2 : line.findIndex(
5783
+ (note2) => note2.startBeat >= downbeat - BEAT_EPS && note2.startBeat < phrase.endBeat - BEAT_EPS
5784
+ );
5785
+ }
5786
+ function repairCadence(scene, index, line, held) {
5787
+ const phrase = scene.plan.phrases[index];
5788
+ if (phrase.cadence === null) {
5789
+ return;
5790
+ }
5791
+ let target = held;
5792
+ for (let i = line.length - 1; target < 0 && i >= 0; i -= 1) {
5793
+ if (positionClass(line[i].startBeat, scene.plan.meters) !== "off") {
5794
+ target = i;
5795
+ }
5796
+ }
5797
+ if (target < 0) {
5798
+ return;
5799
+ }
5800
+ const note2 = line[target];
5801
+ const allowed = cadencePitchClasses(scene.plan, scene.timeline, phrase.cadence, note2.startBeat);
5802
+ if (allowed === null) {
5803
+ return;
5804
+ }
5805
+ const frame = scene.frames[index];
5806
+ const candidates = registerPitches(phrase).filter(
5807
+ (pitch) => allowed.includes(pitchClassOf(pitch))
5808
+ );
5809
+ if (candidates.length === 0) {
5810
+ throw new NoSolutionError(
5811
+ `no pitch in the register ${frame.low}..${frame.high} can end the ${phrase.cadence} cadence`,
5812
+ { at: frame.at }
5813
+ );
5814
+ }
5815
+ if (allowed.includes(pitchClassOf(note2.pitch))) {
5816
+ return;
5817
+ }
5818
+ const slots = line.map(
5819
+ (n, i) => i === target ? { candidates, cost: frame.costAt(n.startBeat), fixed: false } : { candidates: [n.pitch], cost: () => 0, fixed: true }
5820
+ );
5821
+ const pitches = searchPitches(slots, frame.budget);
5822
+ if (pitches === null) {
5823
+ throw new NoSolutionError(`no cadence pitch joins its neighbours without an augmented step`, {
5824
+ at: frame.at
5825
+ });
5826
+ }
5827
+ note2.pitch = pitches[target];
5828
+ }
5829
+ function repickCutOnset(scene, index, line, held) {
5830
+ const note2 = line[held];
5831
+ const derived = scene.plan.motifs.some(
5832
+ (motif) => motif.from !== null && note2.startBeat >= motif.startBeat - BEAT_EPS && note2.startBeat < motif.endBeat - BEAT_EPS
5833
+ );
5834
+ if (scene.plan.phrases[index].cadence !== null || derived) {
5835
+ return;
5836
+ }
5837
+ const frame = scene.frames[index];
5838
+ const candidates = frame.candidatesAt(note2.startBeat);
5839
+ if (candidates.includes(note2.pitch)) {
5840
+ return;
5841
+ }
5842
+ const slots = line.map(
5843
+ (n, i) => i === held ? { candidates, cost: frame.costAt(n.startBeat), fixed: false } : { candidates: [n.pitch], cost: () => 0, fixed: true }
5844
+ );
5845
+ const pitches = searchPitches(slots, frame.budget);
5846
+ if (pitches === null) {
5847
+ throw new NoSolutionError(`no pitch at the final downbeat joins its neighbours`, {
5848
+ at: frame.at
5849
+ });
5850
+ }
5851
+ line[held] = { ...note2, pitch: pitches[held] };
5852
+ }
5853
+ function phraseLine(scene, index) {
5854
+ const line = [];
5855
+ for (const segment of segmentsOf(scene, index)) {
5856
+ line.push(
5857
+ ...segment.motif === null ? freeSpan(scene, index, segment) : statementOf(scene, segment.motif)
5858
+ );
5859
+ }
5860
+ return line.sort((a, b) => a.startBeat - b.startBeat);
5861
+ }
5862
+ function chargePitchSearch(scene) {
5863
+ const { plan } = scene;
5864
+ let estimate = 0;
5865
+ plan.phrases.forEach((phrase, index) => {
5866
+ let searched = 0;
5867
+ for (const segment of segmentsOf(scene, index)) {
5868
+ const motif = segment.motif === null ? void 0 : plan.motifs[segment.motif];
5869
+ if (motif === void 0) {
5870
+ searched += freeNoteCount(scene, index, segment);
5871
+ } else if (motif.relation === null) {
5872
+ searched += motif.notes;
5873
+ } else {
5874
+ estimate += motif.notes * relationLevels(motif.relation.kind).length;
5875
+ }
5876
+ }
5877
+ const register = Math.max(0, phrase.register.high - phrase.register.low + 1);
5878
+ estimate += searched * SEARCH_STATES_SQUARED * register * register;
5879
+ });
5880
+ assertGenerationBudget(estimate, "melody pitch search", scene.budget);
5881
+ }
5882
+ function generateMelody(plan, opts) {
5883
+ assertCompositionPlan(plan);
5884
+ const asked = assertOptions(opts, "opts");
5885
+ if (asked.budget !== void 0) {
5886
+ assertPositiveInt(asked.budget, "budget", Number.MAX_SAFE_INTEGER);
5887
+ }
5888
+ const timeline = planChordTimeline(plan);
5889
+ const scene = {
5890
+ plan,
5891
+ ctx: melodyContext(plan, asked.ctx),
5892
+ budget: asked.budget,
5893
+ timeline,
5894
+ harmonyMisfits: planHarmonyMisfits(plan, timeline),
5895
+ scales: plan.keys.map((key) => scaleOf(key)),
5896
+ members: membersOf(plan),
5897
+ frames: [],
5898
+ statements: /* @__PURE__ */ new Map(),
5899
+ free: /* @__PURE__ */ new Map(),
5900
+ inProgress: /* @__PURE__ */ new Set()
5901
+ };
5902
+ scene.frames = plan.phrases.map((_, index) => frameOf(scene, index));
5903
+ chargePitchSearch(scene);
5904
+ const melody = [];
5905
+ plan.phrases.forEach((_, index) => {
5906
+ const line = phraseLine(scene, index);
5907
+ const downbeat = finalDownbeat(scene, index);
5908
+ let held = heldNoteIndex(scene, index, line, downbeat);
5909
+ if (held >= 0) {
5910
+ line.length = held + 1;
5911
+ const note2 = line[held];
5912
+ if (note2.startBeat < downbeat - BEAT_EPS) {
5913
+ const cut = downbeat - note2.startBeat;
5914
+ line[held] = { ...note2, durationBeat: cut };
5915
+ line.push({ ...note2, startBeat: downbeat, durationBeat: note2.durationBeat - cut });
5916
+ held += 1;
5917
+ repickCutOnset(scene, index, line, held);
5918
+ }
5919
+ }
5920
+ repairCadence(scene, index, line, held);
5921
+ if (held >= 0) {
5922
+ const note2 = line[held];
5923
+ const phraseEnd = plan.phrases[index].endBeat;
5924
+ line[held] = { ...note2, durationBeat: phraseEnd - note2.startBeat };
5925
+ }
5926
+ melody.push(...line);
5927
+ });
5928
+ melody.sort((a, b) => a.startBeat - b.startBeat);
5929
+ return motifToNoteEvents({ notes: melody });
5930
+ }
5931
+
4840
5932
  // src/generate/ornament/index.ts
4841
5933
  var ORNAMENT_STYLES = Object.freeze(["ghost", "flam", "drag", "slide", "accent"]);
4842
5934
  var DEFAULT_TS5 = { numerator: 4, denominator: 4 };
@@ -4961,6 +6053,878 @@ function strokeGaps(ordered) {
4961
6053
  return gaps;
4962
6054
  }
4963
6055
 
6056
+ // src/generate/plan/derive.ts
6057
+ var PRESERVE_ASPECTS = [
6058
+ "form",
6059
+ "phraseLengths",
6060
+ "harmonicFunction",
6061
+ "harmonicRhythm",
6062
+ "motifRelations",
6063
+ "registerShape",
6064
+ "rhythm"
6065
+ ];
6066
+ var NEUTRAL_SHAPE = "arch";
6067
+ var NEUTRAL_PEAK_POSITION = 0.6;
6068
+ var SHAPE_KEEP_THRESHOLD = 0.5;
6069
+ var NEUTRAL_ONSET_LEVELS = Object.freeze([0, 1, 2, 3, 4, 5].map((level) => level / 15));
6070
+ var FUNCTION_DEGREES = {
6071
+ tonic: [1, 6, 3],
6072
+ subdominant: [4, 2],
6073
+ dominant: [5, 7]
6074
+ };
6075
+ var STRONG_WEIGHT2 = 2;
6076
+ var HEPTATONIC_DEGREES = 7;
6077
+ function mod12(value) {
6078
+ return (value % 12 + 12) % 12;
6079
+ }
6080
+ function lerp(weight, reference, neutral) {
6081
+ return weight * reference + (1 - weight) * neutral;
6082
+ }
6083
+ function readPreserve(value) {
6084
+ const read = value === void 0 ? {} : assertRecord(value, "opts.preserve");
6085
+ const weights = {};
6086
+ for (const aspect of PRESERVE_ASPECTS) {
6087
+ const weight = read[aspect];
6088
+ weights[aspect] = weight === void 0 ? 1 : assertRange(weight, 0, 1, `opts.preserve.${aspect}`);
6089
+ }
6090
+ return weights;
6091
+ }
6092
+ function readRegister(value) {
6093
+ const read = assertRecord(value, "opts.register");
6094
+ const low = assertMidiPitch(read.low, "opts.register.low");
6095
+ const high = assertMidiPitch(read.high, "opts.register.high");
6096
+ if (low > high) {
6097
+ throw new InvalidInputError(
6098
+ `opts.register.low must not exceed opts.register.high; received ${low} > ${high}`
6099
+ );
6100
+ }
6101
+ return { low, high };
6102
+ }
6103
+ function rejectSuppliedRng(ctx) {
6104
+ if (typeof ctx === "object" && ctx !== null && ctx.rng !== void 0) {
6105
+ throw new InvalidInputError(
6106
+ "opts.ctx.rng is not accepted: a composition plan records its seed, not a source"
6107
+ );
6108
+ }
6109
+ }
6110
+ 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
+ );
6118
+ const home = regions[0];
6119
+ if (requested === void 0) {
6120
+ return regions;
6121
+ }
6122
+ const target = resolveKey(requested);
6123
+ if (target.variant !== home.variant || target.scale.modeMask12 !== home.scale.modeMask12) {
6124
+ throw new InvalidInputError(
6125
+ "opts.key must be in the same mode as the reference's home key; a key change moves the tonic only"
6126
+ );
6127
+ }
6128
+ const step = spelledInterval(
6129
+ { letter: home.tonic.letter, alter: home.tonic.alter },
6130
+ { letter: target.tonic.letter, alter: target.tonic.alter }
6131
+ );
6132
+ const moved = regions.map(
6133
+ (key) => ({
6134
+ scale: {
6135
+ rootPc: mod12(key.scale.rootPc + step.semitones),
6136
+ modeMask12: key.scale.modeMask12
6137
+ },
6138
+ tonic: transposeByInterval({ letter: key.tonic.letter, alter: key.tonic.alter }, step),
6139
+ variant: key.variant
6140
+ })
6141
+ );
6142
+ moved[0] = target;
6143
+ return moved;
6144
+ }
6145
+ function nearestBoundary(beat, boundaries) {
6146
+ let best = beat;
6147
+ let bestDistance = Number.POSITIVE_INFINITY;
6148
+ for (const boundary of boundaries) {
6149
+ const distance = Math.abs(boundary - beat);
6150
+ if (distance < bestDistance - BEAT_EPS) {
6151
+ best = boundary;
6152
+ bestDistance = distance;
6153
+ }
6154
+ }
6155
+ return best;
6156
+ }
6157
+ function plannedSections(profile, weight, draw) {
6158
+ const sections = profile.form.sections;
6159
+ const boundaries = [profile.span.startBeat, profile.span.endBeat];
6160
+ for (const phrase of profile.form.phrases) {
6161
+ boundaries.push(phrase.startBeat, phrase.endBeat);
6162
+ }
6163
+ boundaries.sort((a, b) => a - b);
6164
+ const planned = [];
6165
+ sections.forEach((section, index) => {
6166
+ const keep = sections.length < 2 || draw.prob(weight, "form", index);
6167
+ const neighbour = index + 1 < sections.length ? index + 1 : index - 1;
6168
+ const startBeat = nearestBoundary(section.startBeat, boundaries);
6169
+ const endBeat = nearestBoundary(section.endBeat, boundaries);
6170
+ if (endBeat - startBeat > BEAT_EPS) {
6171
+ planned.push({
6172
+ label: keep ? section.label : sections[neighbour]?.label ?? section.label,
6173
+ startBeat,
6174
+ endBeat
6175
+ });
6176
+ }
6177
+ });
6178
+ return planned;
6179
+ }
6180
+ function beatAfterBars(startBeat, bars, meters) {
6181
+ let beat = startBeat;
6182
+ let left = bars;
6183
+ while (left > BEAT_EPS) {
6184
+ const step = Math.min(1, left);
6185
+ beat += step * beatsPerBarAt(beat, meters);
6186
+ left -= step;
6187
+ }
6188
+ return beat;
6189
+ }
6190
+ function barsBetween(startBeat, endBeat, meters) {
6191
+ let beat = startBeat;
6192
+ let bars = 0;
6193
+ while (beat < endBeat - BEAT_EPS) {
6194
+ const barBeats = beatsPerBarAt(beat, meters);
6195
+ const step = Math.min(barStartBeat(beat, meters) + barBeats, endBeat) - beat;
6196
+ bars += step / barBeats;
6197
+ beat += step;
6198
+ }
6199
+ return bars;
6200
+ }
6201
+ function spanSegments(profile, sections) {
6202
+ const { startBeat, endBeat } = profile.span;
6203
+ const cuts = [startBeat, endBeat];
6204
+ for (const section of sections) {
6205
+ for (const beat of [section.startBeat, section.endBeat]) {
6206
+ if (beat > startBeat + BEAT_EPS && beat < endBeat - BEAT_EPS) cuts.push(beat);
6207
+ }
6208
+ }
6209
+ cuts.sort((a, b) => a - b);
6210
+ const segments = [];
6211
+ for (let index = 1; index < cuts.length; index += 1) {
6212
+ const from = cuts[index - 1];
6213
+ const to = cuts[index];
6214
+ if (to - from > BEAT_EPS) segments.push([from, to]);
6215
+ }
6216
+ return segments;
6217
+ }
6218
+ function plannedPhraseSpans(profile, sections, weight, draw) {
6219
+ const segments = spanSegments(profile, sections);
6220
+ const pieces = profile.form.phrases.map((phrase, source) => ({
6221
+ startBeat: phrase.startBeat,
6222
+ endBeat: phrase.endBeat,
6223
+ source,
6224
+ cadence: phrase.cadence?.type ?? null
6225
+ }));
6226
+ const replaced = pieces.map((_, index) => !draw.prob(weight, "phraseLengths", index));
6227
+ const spans = [];
6228
+ for (const [segmentStart, segmentEnd] of segments) {
6229
+ const members = pieces.map((piece, index) => ({ piece, index })).filter(
6230
+ ({ piece }) => piece.startBeat >= segmentStart - BEAT_EPS && piece.startBeat < segmentEnd - BEAT_EPS
6231
+ );
6232
+ const laid = [];
6233
+ let cursor = segmentStart;
6234
+ for (const { piece, index } of members) {
6235
+ const lengths = [];
6236
+ if (replaced[index]) {
6237
+ const bars = barsBetween(piece.startBeat, piece.endBeat, profile.meters);
6238
+ const rounded = 2 ** Math.round(Math.log2(bars));
6239
+ const overflows = beatAfterBars(cursor, rounded, profile.meters) > segmentEnd + BEAT_EPS;
6240
+ let partStart = cursor;
6241
+ for (const part of overflows ? [rounded / 2, rounded / 2] : [rounded]) {
6242
+ const partEnd = beatAfterBars(partStart, part, profile.meters);
6243
+ lengths.push(partEnd - partStart);
6244
+ partStart = partEnd;
6245
+ }
6246
+ } else {
6247
+ lengths.push(piece.endBeat - piece.startBeat);
6248
+ }
6249
+ lengths.forEach((length, part) => {
6250
+ if (cursor >= segmentEnd - BEAT_EPS) return;
6251
+ const end = cursor + length;
6252
+ laid.push({
6253
+ startBeat: cursor,
6254
+ endBeat: Math.min(end, segmentEnd),
6255
+ source: piece.source,
6256
+ cadence: part === lengths.length - 1 ? piece.cadence : null
6257
+ });
6258
+ cursor = end;
6259
+ });
6260
+ }
6261
+ const last = laid.at(-1);
6262
+ if (last) {
6263
+ last.endBeat = segmentEnd;
6264
+ } else {
6265
+ laid.push({
6266
+ startBeat: segmentStart,
6267
+ endBeat: segmentEnd,
6268
+ source: spans.at(-1)?.source ?? (profile.form.phrases.length > 0 ? 0 : -1),
6269
+ cadence: null
6270
+ });
6271
+ }
6272
+ spans.push(...laid);
6273
+ }
6274
+ return spans;
6275
+ }
6276
+ function sectionAt(sections, beat) {
6277
+ const index = sections.findIndex(
6278
+ (section) => beat >= section.startBeat - BEAT_EPS && beat < section.endBeat - BEAT_EPS
6279
+ );
6280
+ return index < 0 ? null : index;
6281
+ }
6282
+ function phraseHolding(phrases, startBeat, endBeat) {
6283
+ return phrases.findIndex(
6284
+ (phrase) => startBeat >= phrase.startBeat - BEAT_EPS && endBeat <= phrase.endBeat + BEAT_EPS
6285
+ );
6286
+ }
6287
+ function better(a, b) {
6288
+ for (let field = 0; field < a.length; field += 1) {
6289
+ if (a[field] !== b[field]) return a[field] > b[field];
6290
+ }
6291
+ return false;
6292
+ }
6293
+ function scheduleMotifs(candidates, meters) {
6294
+ const order = [...candidates].sort((a, b) => a.startBeat - b.startBeat || a.node - b.node);
6295
+ const count = order.length;
6296
+ const firstFrom = (from, beat) => {
6297
+ let low = from;
6298
+ let high = count;
6299
+ while (low < high) {
6300
+ const mid = low + high >> 1;
6301
+ if (order[mid].startBeat < beat - BEAT_EPS) low = mid + 1;
6302
+ else high = mid;
6303
+ }
6304
+ return low;
6305
+ };
6306
+ const next = order.map((candidate, index2) => firstFrom(index2 + 1, candidate.endBeat));
6307
+ const own = order.map(
6308
+ (candidate) => [
6309
+ candidate.notes,
6310
+ metricWeight(candidate.startBeat, meters) < STRONG_WEIGHT2 ? -1 : 0,
6311
+ 1
6312
+ ]
6313
+ );
6314
+ const taking = (index2) => {
6315
+ const rest = best[next[index2]];
6316
+ const mine = own[index2];
6317
+ return [mine[0] + rest[0], mine[1] + rest[1], mine[2] + rest[2]];
6318
+ };
6319
+ const best = new Array(count + 1).fill([0, 0, 0]);
6320
+ for (let index2 = count - 1; index2 >= 0; index2 -= 1) {
6321
+ const taken = taking(index2);
6322
+ const skipped = best[index2 + 1];
6323
+ best[index2] = better(taken, skipped) ? taken : skipped;
6324
+ }
6325
+ const chosen = [];
6326
+ let index = 0;
6327
+ while (index < count) {
6328
+ if (!better(best[index], taking(index))) {
6329
+ chosen.push(order[index]);
6330
+ index = next[index];
6331
+ } else {
6332
+ index += 1;
6333
+ }
6334
+ }
6335
+ return chosen.sort((a, b) => a.node - b.node);
6336
+ }
6337
+ function selectedDerivation(graph, parentEdge, selectedAt, node) {
6338
+ let plain = true;
6339
+ let semitones = 0;
6340
+ let steps = 0;
6341
+ for (let edge = parentEdge.get(node); edge !== void 0; edge = parentEdge.get(edge.from)) {
6342
+ const relation = edge.relation;
6343
+ steps += 1;
6344
+ plain &&= relation !== null && (relation.kind === "repetition" || relation.kind === "transposition" && Math.abs(relation.timeRatio - 1) <= BEAT_EPS);
6345
+ semitones += relation?.semitones ?? 0;
6346
+ const from = selectedAt.get(edge.from);
6347
+ if (from === void 0) continue;
6348
+ if (steps === 1) {
6349
+ return { from, relation: relation === null ? null : { ...relation } };
6350
+ }
6351
+ if (!plain) {
6352
+ return { from: null, relation: null };
6353
+ }
6354
+ const gap = (graph.nodes[node]?.startBeat ?? 0) - (graph.nodes[edge.from]?.endBeat ?? 0);
6355
+ return {
6356
+ from,
6357
+ relation: {
6358
+ kind: semitones === 0 ? "repetition" : "transposition",
6359
+ sequence: gap >= -HUMANIZE_ADJACENCY && gap <= HUMANIZE_ADJACENCY,
6360
+ semitones,
6361
+ timeRatio: 1
6362
+ }
6363
+ };
6364
+ }
6365
+ return { from: null, relation: null };
6366
+ }
6367
+ function functionCandidates(fn, key) {
6368
+ if (scaleTonesInDegreeOrder(key).length !== HEPTATONIC_DEGREES) {
6369
+ return null;
6370
+ }
6371
+ return FUNCTION_DEGREES[fn].map((degree) => chordToRoman(diatonicTriad(degree, key), key));
6372
+ }
6373
+ function plannedHarmony(profile, keys, preserve, draw) {
6374
+ const chords = profile.harmony.chords;
6375
+ const romans = chords.map((chord, index) => {
6376
+ if (chord.level !== "structural" || draw.prob(preserve.harmonicFunction, "harmonicFunction", index)) {
6377
+ return chord.roman;
6378
+ }
6379
+ const candidates = functionCandidates(chord.function, keys[chord.key]);
6380
+ if (candidates === null) {
6381
+ return chord.roman;
6382
+ }
6383
+ const pick = draw.range(0, candidates.length - 1, "harmonicFunction", index, "pick");
6384
+ return candidates[pick];
6385
+ });
6386
+ const starts = [];
6387
+ chords.forEach((chord, index) => {
6388
+ const keep = index === 0 || draw.prob(preserve.harmonicRhythm, "harmonicRhythm", index);
6389
+ const start = keep ? chord.startBeat : barStartBeat(chord.startBeat, profile.meters);
6390
+ starts.push(index === 0 ? start : Math.max(start, starts[index - 1]));
6391
+ });
6392
+ const planned = [];
6393
+ chords.forEach((chord, index) => {
6394
+ const next = chords[index + 1];
6395
+ const nextStart = starts[index + 1];
6396
+ let endBeat = chord.endBeat;
6397
+ if (next !== void 0 && nextStart !== void 0) {
6398
+ endBeat = Math.abs(chord.endBeat - next.startBeat) <= BEAT_EPS ? nextStart : Math.min(chord.endBeat, nextStart);
6399
+ }
6400
+ const startBeat = starts[index];
6401
+ if (endBeat - startBeat > BEAT_EPS) {
6402
+ planned.push({ startBeat, endBeat, key: chord.key, roman: romans[index] });
6403
+ }
6404
+ });
6405
+ return planned;
6406
+ }
6407
+ function plannedRhythm(profile, weight) {
6408
+ const reference = profile.melody.rhythm;
6409
+ return {
6410
+ onsetLevels: reference.onsetLevels.map(
6411
+ (share, level) => lerp(weight, share, NEUTRAL_ONSET_LEVELS[level])
6412
+ ),
6413
+ interOnsetShares: [...reference.interOnsetShares],
6414
+ syncopation: lerp(weight, reference.syncopation, 0)
6415
+ };
6416
+ }
6417
+ function deriveCompositionPlan(reference, opts) {
6418
+ const profile = assertReferenceProfile(reference, "reference");
6419
+ const asked = assertOptions(opts, "opts");
6420
+ const preserve = readPreserve(asked.preserve);
6421
+ const requestedRegister = asked.register === void 0 ? void 0 : readRegister(asked.register);
6422
+ rejectSuppliedRng(asked.ctx);
6423
+ const ctx = resolveContext(asked.ctx);
6424
+ const draw = ctx.part("plan");
6425
+ const melodyRegister = profile.melody.register;
6426
+ if (profile.melody.rhythm.onsets === 0 || melodyRegister === null) {
6427
+ throw new InvalidInputError("reference must carry melody onsets to derive a plan from");
6428
+ }
6429
+ const keys = plannedKeys(profile, asked.key);
6430
+ const homeShift = mod12(
6431
+ keys[0].scale.rootPc - (profile.harmony.keys[0]?.key.scale.rootPc ?? 0)
6432
+ );
6433
+ const shift = homeShift >= 6 ? homeShift - 12 : homeShift;
6434
+ const source = { low: melodyRegister.low + shift, high: melodyRegister.high + shift };
6435
+ if (source.low < 0 || source.high > 127) {
6436
+ throw new InvalidInputError(
6437
+ `the reference melody range moved to the new key leaves the MIDI range; received ${source.low}..${source.high}`
6438
+ );
6439
+ }
6440
+ const target = requestedRegister ?? source;
6441
+ const scale = source.high === source.low ? 0 : (target.high - target.low) / (source.high - source.low);
6442
+ const offset = source.high === source.low ? (target.low + target.high) / 2 : target.low - source.low * scale;
6443
+ const mapPitch = (pitch) => (pitch + shift) * scale + offset;
6444
+ const neutralRegister = {
6445
+ low: target.low,
6446
+ high: target.high,
6447
+ mean: (target.low + target.high) / 2
6448
+ };
6449
+ const sections = plannedSections(profile, preserve.form, draw);
6450
+ const shapeWeight = preserve.registerShape;
6451
+ const spans = plannedPhraseSpans(profile, sections, preserve.phraseLengths, draw);
6452
+ const phrases = spans.map((span) => {
6453
+ const melody = profile.form.phrases[span.source]?.melody ?? null;
6454
+ const register = melody ? {
6455
+ low: Math.round(lerp(shapeWeight, mapPitch(melody.low), neutralRegister.low)),
6456
+ high: Math.round(lerp(shapeWeight, mapPitch(melody.high), neutralRegister.high)),
6457
+ mean: lerp(shapeWeight, mapPitch(melody.mean), neutralRegister.mean)
6458
+ } : { ...neutralRegister };
6459
+ return {
6460
+ startBeat: span.startBeat,
6461
+ endBeat: span.endBeat,
6462
+ section: sectionAt(sections, span.startBeat),
6463
+ cadence: span.cadence,
6464
+ shape: melody && shapeWeight >= SHAPE_KEEP_THRESHOLD ? melody.shape : NEUTRAL_SHAPE,
6465
+ peakPosition: melody ? lerp(shapeWeight, melody.peakPosition, NEUTRAL_PEAK_POSITION) : NEUTRAL_PEAK_POSITION,
6466
+ register,
6467
+ onsetDensity: melody ? melody.onsetDensity : profile.melody.rhythm.onsetDensity,
6468
+ motifs: []
6469
+ };
6470
+ });
6471
+ const graph = profile.melody.graph;
6472
+ const noteCount = (node) => (profile.melody.motifs[graph.nodes[node]?.motif ?? -1]?.intervals.length ?? 0) + 1;
6473
+ assertGenerationBudget(graph.nodes.length * phrases.length, "plan motif selection", asked.budget);
6474
+ const candidates = [];
6475
+ graph.nodes.forEach((node, index) => {
6476
+ const phrase = phraseHolding(phrases, node.startBeat, node.endBeat);
6477
+ if (phrase >= 0) {
6478
+ candidates.push({
6479
+ node: index,
6480
+ phrase,
6481
+ startBeat: node.startBeat,
6482
+ endBeat: node.endBeat,
6483
+ notes: noteCount(index)
6484
+ });
6485
+ }
6486
+ });
6487
+ const selected = scheduleMotifs(candidates, profile.meters);
6488
+ const selectedAt = new Map(selected.map((candidate, index) => [candidate.node, index]));
6489
+ const parentEdge = new Map(graph.edges.map((edge) => [edge.to, edge]));
6490
+ const motifs = selected.map((candidate, index) => {
6491
+ const { from, relation } = selectedDerivation(graph, parentEdge, selectedAt, candidate.node);
6492
+ const keepRelation = draw.prob(preserve.motifRelations, "motifRelations", index);
6493
+ const keepRhythm = from === null && draw.prob(preserve.rhythm, "rhythm", index);
6494
+ const cell = profile.melody.motifs[graph.nodes[candidate.node]?.motif ?? -1];
6495
+ phrases[candidate.phrase]?.motifs.push(index);
6496
+ return {
6497
+ phrase: candidate.phrase,
6498
+ startBeat: candidate.startBeat,
6499
+ endBeat: candidate.endBeat,
6500
+ notes: candidate.notes,
6501
+ rhythm: keepRhythm && cell !== void 0 ? cell.rhythm.map((ratio) => ratio * cell.unitBeats) : null,
6502
+ from,
6503
+ relation: keepRelation ? relation : null
6504
+ };
6505
+ });
6506
+ return {
6507
+ planVersion: COMPOSITION_PLAN_VERSION,
6508
+ seed: ctx.seed,
6509
+ algorithmVersion: ctx.algorithmVersion,
6510
+ keys,
6511
+ meters: profile.meters.map((entry) => ({
6512
+ startBeat: entry.startBeat,
6513
+ ts: entry.ts.grouping === void 0 ? { ...entry.ts } : { ...entry.ts, grouping: [...entry.ts.grouping] }
6514
+ })),
6515
+ span: { ...profile.span },
6516
+ sections,
6517
+ phrases,
6518
+ harmony: plannedHarmony(profile, keys, preserve, draw),
6519
+ motifs,
6520
+ rhythm: plannedRhythm(profile, preserve.rhythm)
6521
+ };
6522
+ }
6523
+
6524
+ // src/generate/plan/evaluate.ts
6525
+ var PEAK_POSITION_TOLERANCE = 0.2;
6526
+ var OUTLINE_POINTS = 8;
6527
+ var HELD_NOTE_BEATS = 1;
6528
+ function interiorBoundaries(plan) {
6529
+ const beats = /* @__PURE__ */ new Set();
6530
+ for (const phrase of plan.phrases) {
6531
+ for (const beat of [phrase.startBeat, phrase.endBeat]) {
6532
+ if (beat > plan.span.startBeat + BEAT_EPS && beat < plan.span.endBeat - BEAT_EPS) {
6533
+ beats.add(beat);
6534
+ }
6535
+ }
6536
+ }
6537
+ return [...beats].sort((a, b) => a - b);
6538
+ }
6539
+ function restsOf(melody, plan) {
6540
+ const rests = [];
6541
+ let reach = plan.span.startBeat;
6542
+ for (const note2 of [...melody].sort((a, b) => a.startBeat - b.startBeat)) {
6543
+ if (note2.startBeat > reach + BEAT_EPS) {
6544
+ rests.push({ startBeat: reach, endBeat: note2.startBeat });
6545
+ }
6546
+ reach = Math.max(reach, note2.startBeat + note2.durationBeat);
6547
+ }
6548
+ if (plan.span.endBeat > reach + BEAT_EPS) {
6549
+ rests.push({ startBeat: reach, endBeat: plan.span.endBeat });
6550
+ }
6551
+ return rests;
6552
+ }
6553
+ function phraseBoundaryViolations(melody, plan) {
6554
+ const rests = restsOf(melody, plan);
6555
+ const violations = [];
6556
+ for (const beat of interiorBoundaries(plan)) {
6557
+ const pulse2 = pulseBeats(meterAt(beat, plan.meters));
6558
+ const held = melody.some(
6559
+ (note2) => note2.durationBeat >= HELD_NOTE_BEATS - BEAT_EPS && Math.abs(note2.startBeat + note2.durationBeat - beat) <= pulse2 + BEAT_EPS
6560
+ );
6561
+ const rested = rests.some(
6562
+ (rest) => rest.endBeat - rest.startBeat >= pulse2 - BEAT_EPS && rest.startBeat <= beat + pulse2 + BEAT_EPS && rest.endBeat >= beat - pulse2 - BEAT_EPS
6563
+ );
6564
+ if (!held && !rested) {
6565
+ violations.push({
6566
+ kind: "phraseBoundary",
6567
+ severity: "error",
6568
+ atBeat: beat,
6569
+ expected: `a note of at least ${HELD_NOTE_BEATS} beat(s) ending, or a rest of at least ${pulse2} beat(s), within ${pulse2} beat(s)`,
6570
+ actual: "neither",
6571
+ rationale: `the plan places a phrase boundary at beat ${beat}, which the candidate does not mark`
6572
+ });
6573
+ }
6574
+ }
6575
+ return violations;
6576
+ }
6577
+ function spanViolation(melody, plan) {
6578
+ const end = melody.reduce((max, note2) => Math.max(max, note2.startBeat + note2.durationBeat), 0);
6579
+ if (end <= plan.span.endBeat + BEAT_EPS) {
6580
+ return [];
6581
+ }
6582
+ return [
6583
+ {
6584
+ kind: "span",
6585
+ severity: "error",
6586
+ atBeat: plan.span.endBeat,
6587
+ expected: `ending by beat ${plan.span.endBeat}`,
6588
+ actual: `ending at beat ${end}`,
6589
+ rationale: "the candidate's last note runs past the plan's span"
6590
+ }
6591
+ ];
6592
+ }
6593
+ function phraseLines(melody, plan) {
6594
+ return plan.phrases.map(
6595
+ (phrase) => melody.flatMap(
6596
+ (note2, index) => note2.startBeat >= phrase.startBeat - BEAT_EPS && note2.startBeat < phrase.endBeat - BEAT_EPS ? [index] : []
6597
+ )
6598
+ );
6599
+ }
6600
+ function cadenceViolations(melody, plan, timeline, lines) {
6601
+ const violations = [];
6602
+ plan.phrases.forEach((phrase, index) => {
6603
+ if (phrase.cadence === null) {
6604
+ return;
6605
+ }
6606
+ const last = lines[index]?.at(-1);
6607
+ const note2 = last === void 0 ? void 0 : melody[last];
6608
+ const allowed = note2 === void 0 ? [] : cadencePitchClasses(plan, timeline, phrase.cadence, note2.startBeat);
6609
+ if (note2 !== void 0 && (allowed === null || allowed.includes(pitchClassOf(note2.pitch)))) {
6610
+ return;
6611
+ }
6612
+ violations.push({
6613
+ kind: "cadence",
6614
+ severity: "error",
6615
+ atBeat: phrase.endBeat,
6616
+ expected: `a closing note on pitch class ${(allowed ?? []).join(" or ")} for a ${phrase.cadence} cadence`,
6617
+ actual: note2 === void 0 ? "no note" : `pitch ${note2.pitch}`,
6618
+ rationale: `the phrase ending at beat ${phrase.endBeat} is planned to close on a ${phrase.cadence} cadence`
6619
+ });
6620
+ });
6621
+ return violations;
6622
+ }
6623
+ function harmonyViolations(melody, plan, timeline, lines) {
6624
+ const misfits = planHarmonyMisfits(plan, timeline);
6625
+ const violations = [];
6626
+ for (const line of lines) {
6627
+ const read = misfits(line.map((index) => melody[index]));
6628
+ read.forEach((kinds, at2) => {
6629
+ if (kinds === null) {
6630
+ return;
6631
+ }
6632
+ const note2 = melody[line[at2]];
6633
+ const derived = plan.motifs.some(
6634
+ (motif) => motif.from !== null && note2.startBeat >= motif.startBeat - BEAT_EPS && note2.startBeat < motif.endBeat - BEAT_EPS
6635
+ );
6636
+ const strong = positionClass(note2.startBeat, plan.meters) === "strong";
6637
+ violations.push({
6638
+ kind: "harmony",
6639
+ severity: derived ? "warning" : "error",
6640
+ atBeat: note2.startBeat,
6641
+ expected: strong ? "a chord tone or an ornamental tone" : "a chord tone, an ornamental tone, or a scale tone",
6642
+ actual: kinds.length === 0 ? "no harmonic label" : kinds.join(", "),
6643
+ rationale: derived ? `the note at beat ${note2.startBeat} sits in a derived statement, whose transformation takes precedence over the harmony` : `the note at beat ${note2.startBeat} does not fit the planned harmony`
6644
+ });
6645
+ });
6646
+ }
6647
+ return violations;
6648
+ }
6649
+ function notesInRange(melody, startBeat, endBeat) {
6650
+ return melody.filter(
6651
+ (note2) => note2.startBeat >= startBeat - BEAT_EPS && note2.startBeat < endBeat - BEAT_EPS
6652
+ );
6653
+ }
6654
+ function replayLevel(source, derived, statement, sourceEnd, plan, key, closing) {
6655
+ const relation = statement.relation;
6656
+ const planned = relation.kind === "tonalTransposition" ? relation.degrees ?? 0 : relation.semitones;
6657
+ const { low, high } = plan.phrases[statement.phrase].register;
6658
+ const outside = (pitch) => pitch < low || pitch > high;
6659
+ const backward = relation.kind === "retrograde" || relation.kind === "retrogradeInversion";
6660
+ const cell = clipStatement(
6661
+ source.map((note2) => ({
6662
+ pitch: note2.pitch,
6663
+ startBeat: note2.startBeat,
6664
+ durationBeat: note2.durationBeat
6665
+ })),
6666
+ sourceEnd
6667
+ );
6668
+ const replays = relationLevels(relation.kind).map((level) => {
6669
+ const notes = clipStatement(
6670
+ replayAt(cell, relation, level, statement.startBeat, key),
6671
+ statement.endBeat
6672
+ );
6673
+ return { level, notes, outside: notes.filter((note2) => outside(note2.pitch)).length };
6674
+ });
6675
+ const fewestOutside = Math.min(...replays.map((replay) => replay.outside));
6676
+ let best = null;
6677
+ for (const { level, notes: expected, outside: away } of replays) {
6678
+ if (expected.length !== derived.length) {
6679
+ continue;
6680
+ }
6681
+ let forced = 0;
6682
+ let holds = true;
6683
+ for (let i = 0; holds && i < expected.length; i += 1) {
6684
+ const want = expected[i];
6685
+ const got = derived[i];
6686
+ if (i > 0) {
6687
+ const gap = got.startBeat - derived[i - 1].startBeat;
6688
+ holds = Math.abs(gap - (want.startBeat - expected[i - 1].startBeat)) <= BEAT_EPS;
6689
+ }
6690
+ if (!holds || got.pitch === want.pitch) {
6691
+ continue;
6692
+ }
6693
+ const origin = source[backward ? cell.length - 1 - i : i];
6694
+ forced += 1;
6695
+ holds = closing.has(got) || origin !== void 0 && closing.has(origin) || away === fewestOutside && outside(want.pitch);
6696
+ }
6697
+ const distance = Math.abs(level - planned);
6698
+ const nearer = best === null || distance < Math.abs(best.level - planned) || distance === Math.abs(best.level - planned) && level < best.level;
6699
+ if (holds && nearer) {
6700
+ best = { level, forced };
6701
+ }
6702
+ }
6703
+ return best;
6704
+ }
6705
+ function motifDerivationViolations(melody, plan, closing) {
6706
+ const keyAt = planKeyAt(plan);
6707
+ const errors = [];
6708
+ const warnings = [];
6709
+ for (const statement of plan.motifs) {
6710
+ if (statement.from === null) {
6711
+ continue;
6712
+ }
6713
+ const source = plan.motifs[statement.from];
6714
+ const sourceNotes = notesInRange(melody, source.startBeat, source.endBeat);
6715
+ const derivedNotes = notesInRange(melody, statement.startBeat, statement.endBeat);
6716
+ const relation = statement.relation;
6717
+ const rationale = `the statement at beat ${statement.startBeat} does not derive from its source the way the plan names`;
6718
+ if (sourceNotes.length === 0 || derivedNotes.length === 0) {
6719
+ errors.push({
6720
+ kind: "motifDerivation",
6721
+ severity: "error",
6722
+ atBeat: statement.startBeat,
6723
+ expected: relation === null ? "a variation" : relation.kind,
6724
+ actual: "no notes to compare",
6725
+ rationale
6726
+ });
6727
+ continue;
6728
+ }
6729
+ if (relation === null) {
6730
+ const similarity = melodicSimilarity(sourceNotes, derivedNotes);
6731
+ if (similarity < DEFAULT_VARIATION_THRESHOLD) {
6732
+ errors.push({
6733
+ kind: "motifDerivation",
6734
+ severity: "error",
6735
+ atBeat: statement.startBeat,
6736
+ expected: `a variation (melodicSimilarity >= ${DEFAULT_VARIATION_THRESHOLD})`,
6737
+ actual: `melodicSimilarity ${similarity.toFixed(3)}`,
6738
+ rationale
6739
+ });
6740
+ }
6741
+ continue;
6742
+ }
6743
+ const replay = replayLevel(
6744
+ sourceNotes,
6745
+ derivedNotes,
6746
+ statement,
6747
+ source.endBeat,
6748
+ plan,
6749
+ scaleOf(keyAt(statement.startBeat)),
6750
+ closing
6751
+ );
6752
+ const planned = relation.kind === "tonalTransposition" ? relation.degrees ?? 0 : relation.semitones;
6753
+ if (replay === null) {
6754
+ errors.push({
6755
+ kind: "motifDerivation",
6756
+ severity: "error",
6757
+ atBeat: statement.startBeat,
6758
+ expected: relation.kind,
6759
+ actual: "no pitch level replays the source",
6760
+ rationale
6761
+ });
6762
+ } else if (replay.level !== planned || replay.forced > 0) {
6763
+ warnings.push({
6764
+ kind: "motifDisplaced",
6765
+ severity: "warning",
6766
+ atBeat: statement.startBeat,
6767
+ expected: `${relation.kind} at level ${planned} with every pitch replayed`,
6768
+ actual: `level ${replay.level}, ${replay.forced} excused note(s)`,
6769
+ rationale: `the statement at beat ${statement.startBeat} carries out its planned derivation at another pitch level or with notes excused`
6770
+ });
6771
+ }
6772
+ }
6773
+ return { errors, warnings };
6774
+ }
6775
+ function phraseAt(plan, beat) {
6776
+ return plan.phrases.find(
6777
+ (phrase) => beat >= phrase.startBeat - BEAT_EPS && beat < phrase.endBeat - BEAT_EPS
6778
+ );
6779
+ }
6780
+ function registerViolations(melody, plan) {
6781
+ const violations = [];
6782
+ for (const note2 of melody) {
6783
+ const phrase = phraseAt(plan, note2.startBeat);
6784
+ if (phrase === void 0) {
6785
+ continue;
6786
+ }
6787
+ if (note2.pitch < phrase.register.low || note2.pitch > phrase.register.high) {
6788
+ violations.push({
6789
+ kind: "register",
6790
+ severity: "error",
6791
+ atBeat: note2.startBeat,
6792
+ expected: `a pitch within [${phrase.register.low}, ${phrase.register.high}]`,
6793
+ actual: `pitch ${note2.pitch}`,
6794
+ rationale: `the note at beat ${note2.startBeat} falls outside its phrase's planned register`
6795
+ });
6796
+ }
6797
+ }
6798
+ return violations;
6799
+ }
6800
+ function planOutline(phrase) {
6801
+ const amplitude = (phrase.register.high - phrase.register.low) / 2;
6802
+ const outline = [];
6803
+ for (let k = 0; k < OUTLINE_POINTS; k += 1) {
6804
+ const t = (k + 0.5) / OUTLINE_POINTS;
6805
+ outline.push(
6806
+ phrase.register.mean + plannedContourAt(phrase.shape, phrase.peakPosition, t) * amplitude
6807
+ );
6808
+ }
6809
+ return outline;
6810
+ }
6811
+ function planContourPhrase(phrase) {
6812
+ return {
6813
+ shape: phrase.shape,
6814
+ low: phrase.register.low,
6815
+ high: phrase.register.high,
6816
+ outline: planOutline(phrase)
6817
+ };
6818
+ }
6819
+ function planRegisterPlan(plan) {
6820
+ const phrases = plan.phrases.map((phrase) => ({
6821
+ low: phrase.register.low,
6822
+ high: phrase.register.high,
6823
+ mean: phrase.register.mean,
6824
+ peakPosition: phrase.peakPosition
6825
+ }));
6826
+ let weighted = 0;
6827
+ let weight = 0;
6828
+ for (const phrase of plan.phrases) {
6829
+ const length = phrase.endBeat - phrase.startBeat;
6830
+ weighted += phrase.register.mean * length;
6831
+ weight += length;
6832
+ }
6833
+ return { phrases, mean: weight > 0 ? weighted / weight : null };
6834
+ }
6835
+ function planRhythmReading(plan) {
6836
+ const hasOnsets = plan.rhythm.onsetLevels.some((share) => share > 0);
6837
+ return {
6838
+ onsets: hasOnsets ? 1 : 0,
6839
+ onsetLevels: plan.rhythm.onsetLevels,
6840
+ barPositions: [],
6841
+ interOnsetShares: plan.rhythm.interOnsetShares,
6842
+ syncopation: plan.rhythm.syncopation
6843
+ };
6844
+ }
6845
+ function peakPositionWarnings(plan, readings) {
6846
+ const violations = [];
6847
+ plan.phrases.forEach((phrase, index) => {
6848
+ const reading = readings[index];
6849
+ if (reading == null) {
6850
+ return;
6851
+ }
6852
+ const drift = Math.abs(reading.peakPosition - phrase.peakPosition);
6853
+ if (drift > PEAK_POSITION_TOLERANCE) {
6854
+ violations.push({
6855
+ kind: "peakPosition",
6856
+ severity: "warning",
6857
+ atBeat: phrase.startBeat,
6858
+ expected: `a peak position within ${PEAK_POSITION_TOLERANCE} of ${phrase.peakPosition}`,
6859
+ actual: `${reading.peakPosition}`,
6860
+ rationale: `the phrase starting at beat ${phrase.startBeat} places its melodic peak further from the planned position than the tolerance allows`
6861
+ });
6862
+ }
6863
+ });
6864
+ return violations;
6865
+ }
6866
+ function lineMeanOf(melody) {
6867
+ let weighted = 0;
6868
+ let total = 0;
6869
+ for (const note2 of melody) {
6870
+ weighted += note2.pitch * note2.durationBeat;
6871
+ total += note2.durationBeat;
6872
+ }
6873
+ return total > 0 ? weighted / total : null;
6874
+ }
6875
+ function planDensity(plan) {
6876
+ let weighted = 0;
6877
+ let bars = 0;
6878
+ for (const phrase of plan.phrases) {
6879
+ const span = barSpanOf(phrase.startBeat, phrase.endBeat, plan.meters);
6880
+ weighted += phrase.onsetDensity * span;
6881
+ bars += span;
6882
+ }
6883
+ return { onsets: 1, onsetDensity: bars > 0 ? weighted / bars : 0 };
6884
+ }
6885
+ function evaluateComposition(melody, plan, opts) {
6886
+ assertCompositionPlan(plan, "evaluated plan");
6887
+ const { budget } = assertOptions(opts, "evaluate options");
6888
+ const notes = [
6889
+ ...assertNoteEvents(melody, "evaluated melody", { budget, allowNonPositiveDuration: true })
6890
+ ].sort((a, b) => a.startBeat - b.startBeat);
6891
+ const timeline = planChordTimeline(plan);
6892
+ const lines = phraseLines(notes, plan);
6893
+ const closing = new Set(
6894
+ lines.flatMap((line) => line.length === 0 ? [] : [notes[line.at(-1)]])
6895
+ );
6896
+ const derivations = motifDerivationViolations(notes, plan, closing);
6897
+ const lineMean = lineMeanOf(notes);
6898
+ const readings = plan.phrases.map(
6899
+ (phrase) => phraseMelody(notes, phrase.startBeat, phrase.endBeat, lineMean ?? 0, plan.meters, budget)
6900
+ );
6901
+ const violations = [
6902
+ ...spanViolation(notes, plan),
6903
+ ...phraseBoundaryViolations(notes, plan),
6904
+ ...cadenceViolations(notes, plan, timeline, lines),
6905
+ ...harmonyViolations(notes, plan, timeline, lines),
6906
+ ...registerViolations(notes, plan),
6907
+ ...derivations.errors,
6908
+ ...derivations.warnings,
6909
+ ...peakPositionWarnings(plan, readings)
6910
+ ];
6911
+ const rhythm = analyzeRhythm(notes, {
6912
+ meters: plan.meters,
6913
+ totalBeats: plan.span.endBeat,
6914
+ budget
6915
+ });
6916
+ const target = planRhythmReading(plan);
6917
+ const fit = {
6918
+ contour: contourSimilarity(plan.phrases.map(planContourPhrase), readings),
6919
+ register: registerSimilarity(planRegisterPlan(plan), { phrases: readings, mean: lineMean }),
6920
+ onset: onsetSimilarity(target, rhythm),
6921
+ duration: durationSimilarity(target, rhythm),
6922
+ syncopation: syncopationSimilarity(target, rhythm),
6923
+ density: densitySimilarity(planDensity(plan), rhythm)
6924
+ };
6925
+ return { violations, fit };
6926
+ }
6927
+
4964
6928
  // src/generate/progression/index.ts
4965
6929
  var DEFAULT_TS6 = { numerator: 4, denominator: 4 };
4966
6930
  var PROG_STYLES = Object.freeze(["minimal", "dance", "idol", "rock"]);
@@ -5603,8 +7567,14 @@ export {
5603
7567
  motifToNoteEvents,
5604
7568
  transformMotif,
5605
7569
  developMotif,
7570
+ COMPOSITION_PLAN_VERSION,
7571
+ assertCompositionPlan,
7572
+ planTimeline,
7573
+ generateMelody,
5606
7574
  ORNAMENT_STYLES,
5607
7575
  ornament,
7576
+ deriveCompositionPlan,
7577
+ evaluateComposition,
5608
7578
  BORROWED_DEGREES,
5609
7579
  progressions,
5610
7580
  progressionsByStyle,
@@ -5618,4 +7588,4 @@ export {
5618
7588
  generateRhythm,
5619
7589
  rhythmDensity
5620
7590
  };
5621
- //# sourceMappingURL=chunk-R5VUXQMP.js.map
7591
+ //# sourceMappingURL=chunk-CPMB7P75.js.map