@libraz/libcantus 1.0.1 → 1.2.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 (142) hide show
  1. package/README.md +27 -65
  2. package/README_ja.md +28 -66
  3. package/dist/analyze/index.cjs +11 -7
  4. package/dist/analyze/index.cjs.map +1 -1
  5. package/dist/analyze/index.d.cts +79 -144
  6. package/dist/analyze/index.d.ts +79 -144
  7. package/dist/analyze/index.js +20 -16
  8. package/dist/chunk-35NBV2FT.cjs +145 -0
  9. package/dist/chunk-35NBV2FT.cjs.map +1 -0
  10. package/dist/{chunk-UADYAYAT.cjs → chunk-3R3QVKY5.cjs} +1353 -580
  11. package/dist/chunk-3R3QVKY5.cjs.map +1 -0
  12. package/dist/{chunk-WWURE4XT.cjs → chunk-76ZBTRG2.cjs} +624 -729
  13. package/dist/chunk-76ZBTRG2.cjs.map +1 -0
  14. package/dist/{chunk-FVSI36UV.js → chunk-7KG3RTYS.js} +197 -110
  15. package/dist/chunk-7KG3RTYS.js.map +1 -0
  16. package/dist/{chunk-VTI6FOLB.js → chunk-AGOVW7U6.js} +709 -133
  17. package/dist/chunk-AGOVW7U6.js.map +1 -0
  18. package/dist/{chunk-E3F4AHAH.cjs → chunk-CGBADIO5.cjs} +221 -134
  19. package/dist/chunk-CGBADIO5.cjs.map +1 -0
  20. package/dist/{chunk-6XH6QZYQ.cjs → chunk-DSMJRXCZ.cjs} +1455 -849
  21. package/dist/chunk-DSMJRXCZ.cjs.map +1 -0
  22. package/dist/{chunk-PKFNUHDY.js → chunk-FBH75DAY.js} +1218 -612
  23. package/dist/chunk-FBH75DAY.js.map +1 -0
  24. package/dist/chunk-H2KJHBW4.js +3329 -0
  25. package/dist/chunk-H2KJHBW4.js.map +1 -0
  26. package/dist/chunk-HPVSH7SF.cjs +2294 -0
  27. package/dist/chunk-HPVSH7SF.cjs.map +1 -0
  28. package/dist/{chunk-MXO72PLY.cjs → chunk-HWHGOVO6.cjs} +21 -19
  29. package/dist/chunk-HWHGOVO6.cjs.map +1 -0
  30. package/dist/chunk-HYJJ2V2I.js +2294 -0
  31. package/dist/chunk-HYJJ2V2I.js.map +1 -0
  32. package/dist/{chunk-AFM2ULJH.cjs → chunk-J7FHVZII.cjs} +1567 -1067
  33. package/dist/chunk-J7FHVZII.cjs.map +1 -0
  34. package/dist/chunk-K6OZXIR6.js +1 -0
  35. package/dist/{chunk-H3VORETI.js → chunk-M5RYMAFI.js} +487 -592
  36. package/dist/chunk-M5RYMAFI.js.map +1 -0
  37. package/dist/chunk-NB25P3EH.js +363 -0
  38. package/dist/chunk-NB25P3EH.js.map +1 -0
  39. package/dist/{chunk-TTCLJX6D.js → chunk-NFJ4P6ZZ.js} +10 -8
  40. package/dist/chunk-NFJ4P6ZZ.js.map +1 -0
  41. package/dist/chunk-NM7UAMC3.cjs +457 -0
  42. package/dist/chunk-NM7UAMC3.cjs.map +1 -0
  43. package/dist/chunk-PM4XJE2Y.cjs +363 -0
  44. package/dist/chunk-PM4XJE2Y.cjs.map +1 -0
  45. package/dist/chunk-PUKJP2Z5.js +145 -0
  46. package/dist/chunk-PUKJP2Z5.js.map +1 -0
  47. package/dist/{chunk-VOHZB45S.js → chunk-Q6LFSAKV.js} +1321 -821
  48. package/dist/chunk-Q6LFSAKV.js.map +1 -0
  49. package/dist/{chunk-INHVXP6Z.js → chunk-RFIBF2RR.js} +1203 -430
  50. package/dist/chunk-RFIBF2RR.js.map +1 -0
  51. package/dist/{chunk-Q2WGKJNN.cjs → chunk-S5BKZZGM.cjs} +699 -123
  52. package/dist/chunk-S5BKZZGM.cjs.map +1 -0
  53. package/dist/chunk-SGLEQVJH.cjs +3329 -0
  54. package/dist/chunk-SGLEQVJH.cjs.map +1 -0
  55. package/dist/chunk-UGLFDG7L.cjs +1 -0
  56. package/dist/chunk-UGLFDG7L.cjs.map +1 -0
  57. package/dist/{chunk-YH3BIKHH.js → chunk-UMWZ4ZET.js} +146 -49
  58. package/dist/chunk-UMWZ4ZET.js.map +1 -0
  59. package/dist/{index-szBv3496.d.ts → coerce-DP0vYu-x.d.ts} +286 -199
  60. package/dist/{index-C_8o--VE.d.cts → coerce-DRERzdve.d.cts} +286 -199
  61. package/dist/core/index.cjs +9 -5
  62. package/dist/core/index.cjs.map +1 -1
  63. package/dist/core/index.d.cts +122 -108
  64. package/dist/core/index.d.ts +122 -108
  65. package/dist/core/index.js +32 -28
  66. package/dist/generate/index.cjs +8 -8
  67. package/dist/generate/index.d.cts +168 -84
  68. package/dist/generate/index.d.ts +168 -84
  69. package/dist/generate/index.js +7 -7
  70. package/dist/{index-CGpRv5sl.d.cts → index-9eoQs2C1.d.ts} +34 -19
  71. package/dist/{index-CCNJRG-P.d.cts → index-B9QLziDe.d.cts} +293 -56
  72. package/dist/{index-Dzr13q3d.d.cts → index-BBWNZS0f.d.cts} +135 -58
  73. package/dist/{index-9wcMhG73.d.ts → index-BHIUyjhZ.d.ts} +293 -56
  74. package/dist/{index-BGFW-8P2.d.cts → index-BJgP97N1.d.ts} +196 -114
  75. package/dist/{index-CunYKLiu.d.ts → index-BL5N2Oyz.d.cts} +1 -1
  76. package/dist/{index-Cj49F892.d.ts → index-BMweUlm5.d.cts} +63 -23
  77. package/dist/{index-CgHqxdps.d.ts → index-BNSM-DSk.d.cts} +196 -114
  78. package/dist/{index-D57QRbVn.d.ts → index-BRMbPkHE.d.ts} +480 -254
  79. package/dist/{index-Chbst3Jx.d.ts → index-CJBBiAmU.d.ts} +135 -58
  80. package/dist/{index-BkGmlyI3.d.cts → index-CL4b4gUQ.d.ts} +63 -23
  81. package/dist/{index-B9MNZboQ.d.ts → index-Cy15oTvh.d.cts} +52 -22
  82. package/dist/{index-B9MNZboQ.d.cts → index-Cy15oTvh.d.ts} +52 -22
  83. package/dist/{index-BO6xE9YU.d.cts → index-DGCNKAUr.d.cts} +480 -254
  84. package/dist/{index-B4o55NDr.d.cts → index-Dw2DP7vm.d.ts} +1 -1
  85. package/dist/{index-G1uwGGWQ.d.ts → index-cHvbYgJI.d.cts} +34 -19
  86. package/dist/index.cjs +42 -14
  87. package/dist/index.cjs.map +1 -1
  88. package/dist/index.d.cts +14 -14
  89. package/dist/index.d.ts +14 -14
  90. package/dist/index.js +82 -54
  91. package/dist/{masks-DYbE485J.d.cts → masks-Cc7-MzzY.d.ts} +49 -2
  92. package/dist/{masks-DYbE485J.d.ts → masks-pADfTOiD.d.cts} +49 -2
  93. package/dist/model/index.cjs +19 -12
  94. package/dist/model/index.cjs.map +1 -1
  95. package/dist/model/index.d.cts +504 -182
  96. package/dist/model/index.d.ts +504 -182
  97. package/dist/model/index.js +18 -11
  98. package/dist/{positional-Cptlj-xs.d.ts → positional-DJgs6ltD.d.ts} +85 -8
  99. package/dist/{positional-DvH_sd8u.d.cts → positional-DMZer-FM.d.cts} +85 -8
  100. package/dist/theory/index.cjs +35 -6
  101. package/dist/theory/index.cjs.map +1 -1
  102. package/dist/theory/index.d.cts +257 -62
  103. package/dist/theory/index.d.ts +257 -62
  104. package/dist/theory/index.js +41 -12
  105. package/package.json +7 -5
  106. package/dist/chunk-2LCPM3EG.js +0 -3224
  107. package/dist/chunk-2LCPM3EG.js.map +0 -1
  108. package/dist/chunk-6XH6QZYQ.cjs.map +0 -1
  109. package/dist/chunk-6XPTBE7Y.cjs +0 -49
  110. package/dist/chunk-6XPTBE7Y.cjs.map +0 -1
  111. package/dist/chunk-AFM2ULJH.cjs.map +0 -1
  112. package/dist/chunk-B6XNKWZV.cjs +0 -3224
  113. package/dist/chunk-B6XNKWZV.cjs.map +0 -1
  114. package/dist/chunk-E3F4AHAH.cjs.map +0 -1
  115. package/dist/chunk-ELCTZUJJ.js +0 -379
  116. package/dist/chunk-ELCTZUJJ.js.map +0 -1
  117. package/dist/chunk-FVSI36UV.js.map +0 -1
  118. package/dist/chunk-H3VORETI.js.map +0 -1
  119. package/dist/chunk-H42UHPYW.js +0 -1577
  120. package/dist/chunk-H42UHPYW.js.map +0 -1
  121. package/dist/chunk-HFPS7JFD.js +0 -1
  122. package/dist/chunk-INHVXP6Z.js.map +0 -1
  123. package/dist/chunk-K4ZJUGF4.cjs +0 -379
  124. package/dist/chunk-K4ZJUGF4.cjs.map +0 -1
  125. package/dist/chunk-KNJEKLKV.js +0 -49
  126. package/dist/chunk-KNJEKLKV.js.map +0 -1
  127. package/dist/chunk-MXO72PLY.cjs.map +0 -1
  128. package/dist/chunk-NLEU4QSE.cjs +0 -360
  129. package/dist/chunk-NLEU4QSE.cjs.map +0 -1
  130. package/dist/chunk-OE6WK2Y2.cjs +0 -1577
  131. package/dist/chunk-OE6WK2Y2.cjs.map +0 -1
  132. package/dist/chunk-PKFNUHDY.js.map +0 -1
  133. package/dist/chunk-Q2WGKJNN.cjs.map +0 -1
  134. package/dist/chunk-RHIJ3S4I.cjs +0 -1
  135. package/dist/chunk-RHIJ3S4I.cjs.map +0 -1
  136. package/dist/chunk-TTCLJX6D.js.map +0 -1
  137. package/dist/chunk-UADYAYAT.cjs.map +0 -1
  138. package/dist/chunk-VOHZB45S.js.map +0 -1
  139. package/dist/chunk-VTI6FOLB.js.map +0 -1
  140. package/dist/chunk-WWURE4XT.cjs.map +0 -1
  141. package/dist/chunk-YH3BIKHH.js.map +0 -1
  142. /package/dist/{chunk-HFPS7JFD.js.map → chunk-K6OZXIR6.js.map} +0 -0
@@ -0,0 +1,2294 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+ var _chunkSGLEQVJHcjs = require('./chunk-SGLEQVJH.cjs');
34
+
35
+
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+
44
+ var _chunk3R3QVKY5cjs = require('./chunk-3R3QVKY5.cjs');
45
+
46
+
47
+
48
+
49
+
50
+
51
+
52
+
53
+
54
+
55
+
56
+
57
+
58
+
59
+
60
+
61
+
62
+
63
+
64
+
65
+
66
+
67
+
68
+ var _chunkS5BKZZGMcjs = require('./chunk-S5BKZZGM.cjs');
69
+
70
+ // src/analyze/arrange/internal.ts
71
+ var MAX_LANE_LEAP = 24;
72
+ function covers(note, beat) {
73
+ return beat >= note.startBeat - _chunkS5BKZZGMcjs.BEAT_EPS && beat < note.endBeat - _chunkS5BKZZGMcjs.BEAT_EPS;
74
+ }
75
+ function isPercussion(role) {
76
+ return role === "drums";
77
+ }
78
+ var TRACK_ROLE_TABLE = {
79
+ melody: true,
80
+ harmony: true,
81
+ bass: true,
82
+ drums: true,
83
+ other: true
84
+ };
85
+ var TRACK_ROLES = Object.keys(TRACK_ROLE_TABLE);
86
+ function trackRoleOf(role, name) {
87
+ return role === void 0 ? "other" : _chunkS5BKZZGMcjs.assertOneOf.call(void 0, role, TRACK_ROLES, name);
88
+ }
89
+ var DEFAULT_PROFILE = "pop";
90
+ var SAFETY_PROFILES = Object.keys(_chunk3R3QVKY5cjs.PROFILE_WEIGHTS);
91
+ function arrangementProfile(profile) {
92
+ return _chunkS5BKZZGMcjs.assertOneOf.call(void 0, _nullishCoalesce(profile, () => ( DEFAULT_PROFILE)), SAFETY_PROFILES, "arrangement profile");
93
+ }
94
+ function assertTrackNotes(tracks, options) {
95
+ _chunkS5BKZZGMcjs.assertArray.call(void 0, tracks, "arrangement tracks");
96
+ let noteCount = 0;
97
+ for (let index = 0; index < tracks.length; index += 1) {
98
+ const notes = _optionalChain([tracks, 'access', _2 => _2[index], 'optionalAccess', _3 => _3.notes]);
99
+ if (!Array.isArray(notes)) {
100
+ throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(
101
+ `tracks[${index}].notes must be an array; received ${typeof notes}`
102
+ );
103
+ }
104
+ _chunkS5BKZZGMcjs.assertNoteEvents.call(void 0, notes, `tracks[${index}].notes`, options);
105
+ noteCount += notes.length;
106
+ }
107
+ return noteCount;
108
+ }
109
+ function assertGivenTimeline(timeline, name) {
110
+ if (timeline !== void 0) {
111
+ _chunkSGLEQVJHcjs.assertChordTimeline.call(void 0, timeline, name);
112
+ }
113
+ }
114
+ function assertGivenKeys(keys, name) {
115
+ if (keys === void 0) {
116
+ return;
117
+ }
118
+ if (!Array.isArray(keys)) {
119
+ throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(
120
+ `${name} must be an array of key regions; received ${_chunkS5BKZZGMcjs.describeRejected.call(void 0, keys)}`
121
+ );
122
+ }
123
+ for (let index = 0; index < keys.length; index += 1) {
124
+ const region = keys[index];
125
+ if (typeof region !== "object" || region === null) {
126
+ throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(
127
+ `${name}[${index}] must be a key region; received ${_chunkS5BKZZGMcjs.describeRejected.call(void 0, region)}`
128
+ );
129
+ }
130
+ _chunkS5BKZZGMcjs.assertFiniteNumber.call(void 0, region.startBeat, `${name}[${index}].startBeat`);
131
+ _chunkS5BKZZGMcjs.assertFiniteNumber.call(void 0, region.endBeat, `${name}[${index}].endBeat`);
132
+ if (region.key === void 0 || region.key === null || typeof region.key !== "string" && typeof region.key !== "object") {
133
+ throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(
134
+ `${name}[${index}].key must name a key; received ${_chunkS5BKZZGMcjs.describeRejected.call(void 0, region.key)}`
135
+ );
136
+ }
137
+ _chunk3R3QVKY5cjs.resolveKey.call(void 0, region.key);
138
+ }
139
+ }
140
+ function harmonyTrackSet(harmonyTracks, tracks, name = "harmonyTracks") {
141
+ if (harmonyTracks === void 0) {
142
+ return void 0;
143
+ }
144
+ const only = /* @__PURE__ */ new Set();
145
+ for (let index = 0; index < harmonyTracks.length; index += 1) {
146
+ const value = harmonyTracks[index];
147
+ if (value === void 0 || !Number.isFinite(value)) {
148
+ throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(`${name}[${index}] must be a track index; received ${value}`);
149
+ }
150
+ const track = Math.trunc(value);
151
+ if (track < 0 || track >= tracks.length) {
152
+ throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(
153
+ `${name}[${index}] ${track} is outside the arrangement's ${tracks.length} tracks`
154
+ );
155
+ }
156
+ if (isPercussion(_optionalChain([tracks, 'access', _4 => _4[track], 'optionalAccess', _5 => _5.role]))) {
157
+ throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(
158
+ `${name}[${index}] ${track} names a percussion track, whose pitches state no harmony`
159
+ );
160
+ }
161
+ only.add(track);
162
+ }
163
+ return only;
164
+ }
165
+ function poolNotes(tracks, only) {
166
+ const notes = [];
167
+ for (let index = 0; index < tracks.length; index += 1) {
168
+ const track = tracks[index];
169
+ if (track === void 0 || isPercussion(track.role)) {
170
+ continue;
171
+ }
172
+ if (only !== void 0 && !only.has(index)) {
173
+ continue;
174
+ }
175
+ for (const note of track.notes) {
176
+ if (note.durationBeat > 0) {
177
+ notes.push(note);
178
+ }
179
+ }
180
+ }
181
+ return notes;
182
+ }
183
+ function splitIntoSubVoices(ordered) {
184
+ const lanes = [];
185
+ const attachNearest = (note, excluded = /* @__PURE__ */ new Set()) => {
186
+ let best = -1;
187
+ let bestDistance = Number.POSITIVE_INFINITY;
188
+ let bestEnd = Number.NEGATIVE_INFINITY;
189
+ for (let lane = 0; lane < lanes.length; lane += 1) {
190
+ if (excluded.has(lane)) {
191
+ continue;
192
+ }
193
+ const last = _optionalChain([lanes, 'access', _6 => _6[lane], 'optionalAccess', _7 => _7[(_nullishCoalesce(_optionalChain([lanes, 'access', _8 => _8[lane], 'optionalAccess', _9 => _9.length]), () => ( 0))) - 1]]);
194
+ if (last === void 0) {
195
+ continue;
196
+ }
197
+ const end = last.startBeat + last.durationBeat;
198
+ if (!_chunkSGLEQVJHcjs.hasEnded.call(void 0, end, note.startBeat)) {
199
+ continue;
200
+ }
201
+ const distance = Math.abs(last.pitch - note.pitch);
202
+ if (distance > MAX_LANE_LEAP) {
203
+ continue;
204
+ }
205
+ if (distance < bestDistance || distance === bestDistance && end > bestEnd) {
206
+ best = lane;
207
+ bestDistance = distance;
208
+ bestEnd = end;
209
+ }
210
+ }
211
+ if (best >= 0) {
212
+ _optionalChain([lanes, 'access', _10 => _10[best], 'optionalAccess', _11 => _11.push, 'call', _12 => _12(note)]);
213
+ }
214
+ return best;
215
+ };
216
+ for (let start = 0; start < ordered.length; ) {
217
+ const onset = _nullishCoalesce(_optionalChain([ordered, 'access', _13 => _13[start], 'optionalAccess', _14 => _14.startBeat]), () => ( 0));
218
+ let end = start + 1;
219
+ while (end < ordered.length && _chunkSGLEQVJHcjs.sameInstant.call(void 0, _nullishCoalesce(_optionalChain([ordered, 'access', _15 => _15[end], 'optionalAccess', _16 => _16.startBeat]), () => ( 0)), onset)) {
220
+ end += 1;
221
+ }
222
+ const block = ordered.slice(start, end);
223
+ if (block.length > 1) {
224
+ const freeLanes = lanes.map((lane, index) => ({ index, last: lane[lane.length - 1] })).filter(
225
+ (entry) => entry.last !== void 0 && _chunkSGLEQVJHcjs.hasEnded.call(void 0, entry.last.startBeat + entry.last.durationBeat, onset)
226
+ ).sort((a, b) => a.last.pitch - b.last.pitch);
227
+ const assignment = pairWithFreeLanes(block, freeLanes);
228
+ const taken = new Set(
229
+ assignment.filter((lane) => lane !== void 0)
230
+ );
231
+ for (let index = 0; index < block.length; index += 1) {
232
+ const note = block[index];
233
+ if (note === void 0) {
234
+ continue;
235
+ }
236
+ const lane = assignment[index];
237
+ if (lane !== void 0) {
238
+ _optionalChain([lanes, 'access', _17 => _17[lane], 'optionalAccess', _18 => _18.push, 'call', _19 => _19(note)]);
239
+ continue;
240
+ }
241
+ const attached = attachNearest(note, taken);
242
+ if (attached < 0) {
243
+ lanes.push([note]);
244
+ } else {
245
+ taken.add(attached);
246
+ }
247
+ }
248
+ } else {
249
+ const note = block[0];
250
+ if (note !== void 0 && attachNearest(note) < 0) {
251
+ lanes.push([note]);
252
+ }
253
+ }
254
+ start = end;
255
+ }
256
+ return lanes;
257
+ }
258
+ function pairWithFreeLanes(block, freeLanes) {
259
+ const noteCount = block.length;
260
+ const laneCount = freeLanes.length;
261
+ const assignment = new Array(noteCount).fill(void 0);
262
+ if (laneCount === 0) {
263
+ return assignment;
264
+ }
265
+ const width = laneCount + 1;
266
+ const cost = new Float64Array((noteCount + 1) * width);
267
+ const step = new Uint8Array((noteCount + 1) * width);
268
+ for (let i = 1; i <= noteCount; i += 1) {
269
+ cost[i * width] = i * MAX_LANE_LEAP;
270
+ step[i * width] = 2;
271
+ }
272
+ for (let i = 1; i <= noteCount; i += 1) {
273
+ for (let j = 1; j <= laneCount; j += 1) {
274
+ let best = _nullishCoalesce(cost[i * width + j - 1], () => ( 0));
275
+ let choice = 0;
276
+ const unpaired = (_nullishCoalesce(cost[(i - 1) * width + j], () => ( 0))) + MAX_LANE_LEAP;
277
+ if (unpaired < best) {
278
+ best = unpaired;
279
+ choice = 2;
280
+ }
281
+ const note = block[i - 1];
282
+ const lane = freeLanes[j - 1];
283
+ if (note !== void 0 && lane !== void 0) {
284
+ const distance = Math.abs(lane.last.pitch - note.pitch);
285
+ const paired = (_nullishCoalesce(cost[(i - 1) * width + j - 1], () => ( 0))) + distance;
286
+ if (distance <= MAX_LANE_LEAP && paired < best) {
287
+ best = paired;
288
+ choice = 1;
289
+ }
290
+ }
291
+ cost[i * width + j] = best;
292
+ step[i * width + j] = choice;
293
+ }
294
+ }
295
+ for (let i = noteCount, j = laneCount; i > 0; ) {
296
+ if (j === 0 || step[i * width + j] === 2) {
297
+ i -= 1;
298
+ } else if (step[i * width + j] === 1) {
299
+ assignment[i - 1] = _optionalChain([freeLanes, 'access', _20 => _20[j - 1], 'optionalAccess', _21 => _21.index]);
300
+ i -= 1;
301
+ j -= 1;
302
+ } else {
303
+ j -= 1;
304
+ }
305
+ }
306
+ return assignment;
307
+ }
308
+ function lastStartingAtOrBefore(sounding, beat) {
309
+ let low = 0;
310
+ let high = sounding.length;
311
+ while (low < high) {
312
+ const middle = Math.floor((low + high) / 2);
313
+ if ((_nullishCoalesce(_optionalChain([sounding, 'access', _22 => _22[middle], 'optionalAccess', _23 => _23.startBeat]), () => ( Number.POSITIVE_INFINITY))) <= beat + _chunkS5BKZZGMcjs.BEAT_EPS) {
314
+ low = middle + 1;
315
+ } else {
316
+ high = middle;
317
+ }
318
+ }
319
+ return low - 1;
320
+ }
321
+ function prepareTracks(tracks) {
322
+ let nextId = 0;
323
+ return tracks.map((track, index) => {
324
+ const ordered = track.notes.map((note, originalIndex) => ({ note, originalIndex })).filter(({ note }) => note.durationBeat > 0).sort((a, b) => a.note.startBeat - b.note.startBeat || a.note.pitch - b.note.pitch).map(({ note, originalIndex }) => ({ ...note, id: nextId++, originalIndex }));
325
+ const voices = splitIntoSubVoices(ordered).map((voice) => {
326
+ const sounding = voice.map((note, i) => {
327
+ const prev = i > 0 ? voice[i - 1] : void 0;
328
+ const prevEnd = prev === void 0 ? void 0 : prev.startBeat + prev.durationBeat;
329
+ const contiguous = prevEnd !== void 0 && _chunkSGLEQVJHcjs.adjacent.call(void 0, prevEnd, note.startBeat);
330
+ return {
331
+ id: note.id,
332
+ originalIndex: note.originalIndex,
333
+ pitch: note.pitch,
334
+ prevPitch: contiguous ? _optionalChain([prev, 'optionalAccess', _24 => _24.pitch]) : void 0,
335
+ startBeat: note.startBeat,
336
+ endBeat: note.startBeat + note.durationBeat
337
+ };
338
+ });
339
+ return {
340
+ voice,
341
+ sounding,
342
+ at: (beat) => {
343
+ const candidate = sounding[lastStartingAtOrBefore(sounding, beat)];
344
+ return candidate !== void 0 && covers(candidate, beat) ? candidate : void 0;
345
+ }
346
+ };
347
+ });
348
+ return {
349
+ name: _nullishCoalesce(track.name, () => ( `track ${index + 1}`)),
350
+ role: trackRoleOf(track.role, `tracks[${index}].role`),
351
+ trackIndex: index,
352
+ voices
353
+ };
354
+ });
355
+ }
356
+
357
+ // src/analyze/arrange/tracks.ts
358
+ function withGivenKeys(result, given) {
359
+ if (given === void 0) {
360
+ return result;
361
+ }
362
+ return { ...result, keys: given, prevailingKey: _nullishCoalesce(_chunkSGLEQVJHcjs.prevailingKeyOf.call(void 0, given), () => ( result.prevailingKey)) };
363
+ }
364
+ function callerTimeline(timeline, key, given, pooled, meters, budget) {
365
+ const chartEnd = timeline.segments.reduce((end, segment) => Math.max(end, segment.endBeat), 0);
366
+ const soundingEnd = pooled.reduce(
367
+ (end, note) => Math.max(end, note.startBeat + note.durationBeat),
368
+ 0
369
+ );
370
+ const totalBeats = Math.max(chartEnd, soundingEnd);
371
+ const { startBeat: musicStart } = _chunkSGLEQVJHcjs.gridForNotes.call(void 0, pooled, _chunkS5BKZZGMcjs.beatsPerBarAt.call(void 0, 0, meters));
372
+ const keys = _nullishCoalesce(given, () => ( _chunkSGLEQVJHcjs.attachPivots.call(void 0,
373
+ key !== void 0 ? [{ startBeat: musicStart, endBeat: totalBeats, key: _chunk3R3QVKY5cjs.resolveKey.call(void 0, key), confidence: 1 }] : _chunkSGLEQVJHcjs.keyTimelineFromNotes.call(void 0, pooled, { meters, totalBeats, budget }),
374
+ timeline.segments
375
+ )));
376
+ return {
377
+ timeline,
378
+ keys,
379
+ prevailingKey: _nullishCoalesce(_chunkSGLEQVJHcjs.prevailingKeyOf.call(void 0, keys), () => ( _chunk3R3QVKY5cjs.resolveKey.call(void 0, _chunk3R3QVKY5cjs.majorKey.call(void 0, 0)))),
380
+ segmentConfidence: timeline.segments.map(() => 0)
381
+ };
382
+ }
383
+ function soundingVoicesAt(prepared, beat) {
384
+ const out = [];
385
+ for (let t = 0; t < prepared.length; t += 1) {
386
+ const track = prepared[t];
387
+ if (!track || isPercussion(track.role)) {
388
+ continue;
389
+ }
390
+ for (let v = 0; v < track.voices.length; v += 1) {
391
+ const note = _optionalChain([track, 'access', _25 => _25.voices, 'access', _26 => _26[v], 'optionalAccess', _27 => _27.at, 'call', _28 => _28(beat)]);
392
+ if (note === void 0) {
393
+ continue;
394
+ }
395
+ const snap = { pitch: note.pitch };
396
+ const previous = _chunkSGLEQVJHcjs.sameInstant.call(void 0, note.startBeat, beat) ? note.prevPitch : note.pitch;
397
+ if (previous !== void 0) {
398
+ snap.prevPitch = previous;
399
+ }
400
+ out.push({ track: t, voice: v, trackIndex: track.trackIndex, note, snapshot: snap });
401
+ }
402
+ }
403
+ return out;
404
+ }
405
+ function otherVoicesSounding(prepared, excludeTrack, excludeVoice, beat, cache) {
406
+ const sounding = _nullishCoalesce(cache.get(beat), () => ( soundingVoicesAt(prepared, beat)));
407
+ cache.set(beat, sounding);
408
+ return sounding.filter(({ track, voice }) => track !== excludeTrack || voice !== excludeVoice);
409
+ }
410
+ function conflictPartner(sounding, reasons) {
411
+ const partner = {
412
+ trackIndex: sounding.trackIndex,
413
+ noteId: sounding.note.id,
414
+ reasons
415
+ };
416
+ if (sounding.note.originalIndex !== void 0) {
417
+ partner.originalIndex = sounding.note.originalIndex;
418
+ }
419
+ return partner;
420
+ }
421
+ function snapshotsOf(voices) {
422
+ return voices.map(({ snapshot }) => snapshot);
423
+ }
424
+ function analyzePolyphony(notes, chordAtBeat, key) {
425
+ const passage = _chunkS5BKZZGMcjs.assertNoteEvents.call(void 0, notes, "notes", { allowNonPositiveDuration: true });
426
+ _chunkS5BKZZGMcjs.assertFunction.call(void 0, chordAtBeat, "chordAtBeat");
427
+ const prepared = prepareTracks([{ notes: [...passage] }]);
428
+ const track = prepared[0];
429
+ _chunkS5BKZZGMcjs.assertGenerationBudget.call(void 0,
430
+ passage.length * (_nullishCoalesce(_optionalChain([track, 'optionalAccess', _29 => _29.voices, 'access', _30 => _30.length]), () => ( 0))),
431
+ "polyphony note-voice comparisons"
432
+ );
433
+ const soundingCache = /* @__PURE__ */ new Map();
434
+ const byIndex = /* @__PURE__ */ new Map();
435
+ for (let v = 0; v < (_nullishCoalesce(_optionalChain([track, 'optionalAccess', _31 => _31.voices, 'access', _32 => _32.length]), () => ( 0))); v += 1) {
436
+ const subVoice = _optionalChain([track, 'optionalAccess', _33 => _33.voices, 'access', _34 => _34[v]]);
437
+ if (subVoice === void 0) {
438
+ continue;
439
+ }
440
+ const analyzed = _chunkSGLEQVJHcjs.analyzeVoice.call(void 0,
441
+ subVoice.voice,
442
+ chordAtBeat,
443
+ key,
444
+ (beat) => snapshotsOf(otherVoicesSounding(prepared, 0, v, beat, soundingCache))
445
+ );
446
+ for (const note of analyzed) {
447
+ if (note.originalIndex !== void 0) {
448
+ const { originalIndex, ...annotation } = note;
449
+ byIndex.set(originalIndex, { ...annotation, noteId: originalIndex });
450
+ }
451
+ }
452
+ }
453
+ return notes.map((note, index) => {
454
+ const analyzed = byIndex.get(index);
455
+ if (analyzed !== void 0) {
456
+ return analyzed;
457
+ }
458
+ const [only] = _chunkSGLEQVJHcjs.analyzeVoice.call(void 0, [{ ...note, id: index }], chordAtBeat, key);
459
+ return _nullishCoalesce(only, () => ( {
460
+ noteId: index,
461
+ pitch: note.pitch,
462
+ startBeat: note.startBeat,
463
+ durationBeat: note.durationBeat,
464
+ labels: []
465
+ }));
466
+ });
467
+ }
468
+ function firstSegmentAfter(timeline, beat) {
469
+ let low = 0;
470
+ let high = timeline.segments.length;
471
+ while (low < high) {
472
+ const middle = Math.floor((low + high) / 2);
473
+ if ((_nullishCoalesce(_optionalChain([timeline, 'access', _35 => _35.segments, 'access', _36 => _36[middle], 'optionalAccess', _37 => _37.startBeat]), () => ( Number.POSITIVE_INFINITY))) <= beat + _chunkS5BKZZGMcjs.BEAT_EPS) {
474
+ low = middle + 1;
475
+ } else {
476
+ high = middle;
477
+ }
478
+ }
479
+ return low;
480
+ }
481
+ function evaluationBeats(note, timeline) {
482
+ const noteEnd = note.startBeat + note.durationBeat;
483
+ const beats = [note.startBeat];
484
+ for (let i = firstSegmentAfter(timeline, note.startBeat); i < timeline.segments.length; i += 1) {
485
+ const start = _optionalChain([timeline, 'access', _38 => _38.segments, 'access', _39 => _39[i], 'optionalAccess', _40 => _40.startBeat]);
486
+ if (start === void 0 || start >= noteEnd - _chunkS5BKZZGMcjs.BEAT_EPS) {
487
+ break;
488
+ }
489
+ beats.push(start);
490
+ }
491
+ return beats;
492
+ }
493
+ function analyzeArrangement(tracks, opts = {}) {
494
+ return analyzeArrangementWith(tracks, opts).analysis;
495
+ }
496
+ function analyzeArrangementWith(tracks, opts = {}, previous, dirty) {
497
+ const meters = _chunkS5BKZZGMcjs.resolveMeters.call(void 0, opts, "arrangement meters");
498
+ const budget = opts.budget;
499
+ const noteOptions = { allowNonPositiveDuration: true, budget };
500
+ if (opts.pickupBeats !== void 0) {
501
+ _chunkS5BKZZGMcjs.assertRange.call(void 0, opts.pickupBeats, 0, Number.MAX_SAFE_INTEGER, "arrangement pickupBeats");
502
+ noteOptions.minStartBeat = -opts.pickupBeats;
503
+ }
504
+ _chunkS5BKZZGMcjs.assertArray.call(void 0, tracks, "arrangement tracks");
505
+ _chunkS5BKZZGMcjs.assertGenerationBudget.call(void 0, tracks.length, "arrangement tracks", budget);
506
+ const noteCount = assertTrackNotes(tracks, noteOptions);
507
+ _chunkS5BKZZGMcjs.assertGenerationBudget.call(void 0, noteCount, "arrangement notes", budget);
508
+ const profile = arrangementProfile(opts.profile);
509
+ const minSeverity = opts.minSeverity === void 0 ? 1 /* Warning */ : _chunkS5BKZZGMcjs.assertInteger.call(void 0,
510
+ opts.minSeverity,
511
+ "arrangement minSeverity",
512
+ 0 /* Safe */,
513
+ 2 /* Dissonant */
514
+ );
515
+ assertGivenTimeline(opts.timeline, "arrangement timeline");
516
+ assertGivenKeys(opts.keys, "arrangement keys");
517
+ const harmonyTracks = harmonyTrackSet(opts.harmonyTracks, tracks);
518
+ const prepared = prepareTracks(tracks);
519
+ const voiceCount = prepared.reduce((sum, track) => sum + track.voices.length, 0);
520
+ _chunkS5BKZZGMcjs.assertGenerationBudget.call(void 0, noteCount * voiceCount, "arrangement note-voice comparisons", budget);
521
+ const pooled = poolNotes(tracks, harmonyTracks);
522
+ let evidence;
523
+ let inferred;
524
+ if (opts.timeline === void 0) {
525
+ const run = _chunkSGLEQVJHcjs.analyzeTimeline.call(void 0,
526
+ pooled,
527
+ {
528
+ key: opts.key,
529
+ meters,
530
+ pickupBeats: opts.pickupBeats,
531
+ harmonicRhythm: opts.harmonicRhythm,
532
+ budget
533
+ },
534
+ previous,
535
+ dirty
536
+ );
537
+ evidence = run.evidence;
538
+ inferred = withGivenKeys(run.result, opts.keys);
539
+ } else {
540
+ inferred = callerTimeline(opts.timeline, opts.key, opts.keys, pooled, meters, budget);
541
+ }
542
+ const { timeline, keys, prevailingKey, segmentConfidence } = inferred;
543
+ const keyAt = _chunkSGLEQVJHcjs.keyLookup.call(void 0, keys, prevailingKey);
544
+ const scaleAt = (beat) => _chunk3R3QVKY5cjs.scaleOf.call(void 0, keyAt(beat));
545
+ const cadences = _chunkSGLEQVJHcjs.detectCadences.call(void 0, timeline, scaleAt);
546
+ const soundingCache = /* @__PURE__ */ new Map();
547
+ const trackAnalyses = [];
548
+ const conflicts = [];
549
+ for (let t = 0; t < prepared.length; t += 1) {
550
+ const track = prepared[t];
551
+ if (!track) {
552
+ continue;
553
+ }
554
+ if (isPercussion(track.role)) {
555
+ trackAnalyses.push({ name: track.name, role: track.role, notes: [] });
556
+ continue;
557
+ }
558
+ const notes = [];
559
+ for (let v = 0; v < track.voices.length; v += 1) {
560
+ const subVoice = track.voices[v];
561
+ if (!subVoice) {
562
+ continue;
563
+ }
564
+ const analyzed = _chunkSGLEQVJHcjs.analyzeVoice.call(void 0,
565
+ subVoice.voice,
566
+ timeline.at,
567
+ scaleAt,
568
+ (beat) => snapshotsOf(otherVoicesSounding(prepared, t, v, beat, soundingCache))
569
+ );
570
+ for (const note of analyzed) {
571
+ notes.push({ ...note, trackIndex: track.trackIndex });
572
+ }
573
+ const labelsById = new Map(analyzed.map((note) => [note.noteId, note.labels]));
574
+ for (let noteIndex = 0; noteIndex < subVoice.voice.length; noteIndex += 1) {
575
+ const note = subVoice.voice[noteIndex];
576
+ const preparedNote = subVoice.sounding[noteIndex];
577
+ if (note === void 0 || preparedNote === void 0) {
578
+ continue;
579
+ }
580
+ for (const beat of evaluationBeats(note, timeline)) {
581
+ const atOnset = _chunkSGLEQVJHcjs.sameInstant.call(void 0, beat, note.startBeat);
582
+ const others = otherVoicesSounding(prepared, t, v, beat, soundingCache);
583
+ const safetyQuery = {
584
+ profile,
585
+ candidatePitch: note.pitch,
586
+ prevPitch: atOnset ? preparedNote.prevPitch : note.pitch,
587
+ chord: timeline.at(beat),
588
+ key: scaleAt(beat),
589
+ otherVoices: snapshotsOf(others),
590
+ strongBeat: _chunkS5BKZZGMcjs.isStrongBeat.call(void 0, beat, meters)
591
+ };
592
+ let result = _chunk3R3QVKY5cjs.evaluateSafety.call(void 0, safetyQuery, { suggestions: false });
593
+ if (result.safety !== 0 /* Safe */ && result.safety >= minSeverity) {
594
+ result = _chunk3R3QVKY5cjs.evaluateSafety.call(void 0, safetyQuery, { partners: true });
595
+ const conflict = {
596
+ beat,
597
+ trackName: track.name,
598
+ trackIndex: track.trackIndex,
599
+ noteId: note.id,
600
+ pitch: note.pitch,
601
+ labels: _nullishCoalesce(labelsById.get(note.id), () => ( [])),
602
+ safety: result.safety,
603
+ reasons: result.reasons
604
+ };
605
+ if (note.originalIndex !== void 0) {
606
+ conflict.originalIndex = note.originalIndex;
607
+ }
608
+ if (result.rationale !== void 0) {
609
+ conflict.rationale = result.rationale;
610
+ }
611
+ if (result.resolveTo !== void 0) {
612
+ conflict.resolveTo = result.resolveTo;
613
+ }
614
+ if (result.suggestions !== void 0) {
615
+ conflict.suggestions = [...result.suggestions];
616
+ }
617
+ if (result.partners !== void 0) {
618
+ conflict.partners = result.partners.map(
619
+ ({ index, reasons }) => conflictPartner(others[index], reasons)
620
+ );
621
+ }
622
+ conflicts.push(conflict);
623
+ }
624
+ }
625
+ }
626
+ }
627
+ notes.sort((a, b) => a.noteId - b.noteId);
628
+ trackAnalyses.push({ name: track.name, role: track.role, notes });
629
+ }
630
+ conflicts.sort((a, b) => b.safety - a.safety || a.beat - b.beat);
631
+ const analysis = {
632
+ keys,
633
+ prevailingKey,
634
+ timeline,
635
+ segmentConfidence,
636
+ cadences,
637
+ tracks: trackAnalyses,
638
+ conflicts
639
+ };
640
+ return evidence === void 0 ? { analysis } : { analysis, evidence };
641
+ }
642
+
643
+ // src/analyze/arrange/session.ts
644
+ function noteId(note) {
645
+ return `${note.pitch}|${note.startBeat}|${note.durationBeat}|${_nullishCoalesce(note.velocity, () => ( ""))}`;
646
+ }
647
+ function sameOrder(before, after) {
648
+ if (before.length !== after.length) {
649
+ return false;
650
+ }
651
+ for (let index = 0; index < before.length; index += 1) {
652
+ const previous = before[index];
653
+ const current = after[index];
654
+ if (previous === void 0 || current === void 0 || noteId(previous) !== noteId(current)) {
655
+ return false;
656
+ }
657
+ }
658
+ return true;
659
+ }
660
+ function editedSpan(before, after) {
661
+ const counts = /* @__PURE__ */ new Map();
662
+ const tally = (notes, sign) => {
663
+ for (const note of notes) {
664
+ const id = noteId(note);
665
+ const entry = counts.get(id);
666
+ if (entry === void 0) {
667
+ counts.set(id, {
668
+ count: sign,
669
+ startBeat: note.startBeat,
670
+ endBeat: note.startBeat + note.durationBeat
671
+ });
672
+ } else {
673
+ entry.count += sign;
674
+ }
675
+ }
676
+ };
677
+ tally(before, 1);
678
+ tally(after, -1);
679
+ let startBeat = Number.POSITIVE_INFINITY;
680
+ let endBeat = Number.NEGATIVE_INFINITY;
681
+ for (const entry of counts.values()) {
682
+ if (entry.count === 0) {
683
+ continue;
684
+ }
685
+ startBeat = Math.min(startBeat, entry.startBeat);
686
+ endBeat = Math.max(endBeat, entry.endBeat, entry.startBeat);
687
+ }
688
+ return startBeat <= endBeat ? { startBeat, endBeat } : null;
689
+ }
690
+ function unionSpan(a, b) {
691
+ if (a === null) {
692
+ return b;
693
+ }
694
+ if (b === null) {
695
+ return a;
696
+ }
697
+ return {
698
+ startBeat: Math.min(a.startBeat, b.startBeat),
699
+ endBeat: Math.max(a.endBeat, b.endBeat)
700
+ };
701
+ }
702
+ function sessionOf(tracks, opts, analysis, evidence) {
703
+ return {
704
+ tracks,
705
+ analysis,
706
+ update(edits) {
707
+ _chunkS5BKZZGMcjs.assertGenerationBudget.call(void 0, edits.length, "arrangement edits", opts.budget);
708
+ const next = [...tracks];
709
+ let edited = null;
710
+ let reordered = false;
711
+ for (let index = 0; index < edits.length; index += 1) {
712
+ const edit = edits[index];
713
+ if (edit === void 0) {
714
+ continue;
715
+ }
716
+ const target = next[edit.trackIndex];
717
+ if (target === void 0) {
718
+ throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(
719
+ `edits[${index}].trackIndex ${edit.trackIndex} is outside the session's ${tracks.length} tracks`
720
+ );
721
+ }
722
+ if (!Array.isArray(edit.notes)) {
723
+ throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(
724
+ `edits[${index}].notes must be an array; received ${typeof edit.notes}`
725
+ );
726
+ }
727
+ _chunkS5BKZZGMcjs.assertNoteEvents.call(void 0, edit.notes, `edits[${index}].notes`, {
728
+ allowNonPositiveDuration: true,
729
+ budget: opts.budget
730
+ });
731
+ edited = unionSpan(edited, editedSpan(target.notes, edit.notes));
732
+ reordered = reordered || !sameOrder(target.notes, edit.notes);
733
+ next[edit.trackIndex] = { ...target, notes: edit.notes };
734
+ }
735
+ if (edited === null && !reordered) {
736
+ return sessionOf(next, opts, analysis, evidence);
737
+ }
738
+ const dirty = evidence === void 0 ? void 0 : edited === null ? { from: 0, to: 0 } : _chunkSGLEQVJHcjs.dirtySlotsFor.call(void 0, evidence, edited.startBeat, edited.endBeat);
739
+ const run = analyzeArrangementWith(next, opts, evidence, dirty);
740
+ return sessionOf(next, opts, run.analysis, run.evidence);
741
+ }
742
+ };
743
+ }
744
+ function createArrangementSession(tracks, opts = {}) {
745
+ const run = analyzeArrangementWith(tracks, opts);
746
+ return sessionOf([...tracks], opts, run.analysis, run.evidence);
747
+ }
748
+
749
+ // src/analyze/arrange/tension.ts
750
+ var FUNCTION_TENSION = {
751
+ tonic: 0,
752
+ subdominant: 0.5,
753
+ dominant: 1
754
+ };
755
+ var FUNCTION_WEIGHT = 0.5;
756
+ var DISSONANCE_WEIGHT = 0.35;
757
+ var SPAN_WEIGHT = 0.15;
758
+ var SPAN_SATURATION = 24;
759
+ function clamp012(value) {
760
+ return Math.min(1, Math.max(0, value));
761
+ }
762
+ function tensionCurve(tracks, opts = {}) {
763
+ const meters = _chunkS5BKZZGMcjs.resolveMeters.call(void 0, opts, "arrangement meters");
764
+ const budget = opts.budget;
765
+ const noteOptions = { allowNonPositiveDuration: true, budget };
766
+ if (opts.pickupBeats !== void 0) {
767
+ _chunkS5BKZZGMcjs.assertRange.call(void 0, opts.pickupBeats, 0, Number.MAX_SAFE_INTEGER, "arrangement pickupBeats");
768
+ noteOptions.minStartBeat = -opts.pickupBeats;
769
+ }
770
+ _chunkS5BKZZGMcjs.assertArray.call(void 0, tracks, "arrangement tracks");
771
+ _chunkS5BKZZGMcjs.assertGenerationBudget.call(void 0, tracks.length, "arrangement tracks", budget);
772
+ const noteCount = assertTrackNotes(tracks, noteOptions);
773
+ _chunkS5BKZZGMcjs.assertGenerationBudget.call(void 0, noteCount, "arrangement notes", budget);
774
+ const profile = arrangementProfile(opts.profile);
775
+ const step = _nullishCoalesce(opts.step, () => ( 1));
776
+ _chunkS5BKZZGMcjs.assertRange.call(void 0, step, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "tension sampling step");
777
+ if (opts.minSeverity !== void 0) {
778
+ _chunkS5BKZZGMcjs.assertInteger.call(void 0,
779
+ opts.minSeverity,
780
+ "arrangement minSeverity",
781
+ 0 /* Safe */,
782
+ 2 /* Dissonant */
783
+ );
784
+ }
785
+ assertGivenTimeline(opts.timeline, "arrangement timeline");
786
+ assertGivenKeys(opts.keys, "arrangement keys");
787
+ const harmonyTracks = harmonyTrackSet(opts.harmonyTracks, tracks);
788
+ const pooled = poolNotes(tracks, harmonyTracks);
789
+ const all = poolNotes(tracks);
790
+ const totalBeats = all.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
791
+ const { origin: sampleStart, startBeat: musicStart } = _chunkSGLEQVJHcjs.gridForNotes.call(void 0, all, step);
792
+ const suppliedKeys = opts.timeline === void 0 ? void 0 : _nullishCoalesce(opts.keys, () => ( (opts.key !== void 0 ? [
793
+ {
794
+ // The one region a named key holds starts where the music does,
795
+ // as the searched regions and the timeline's own key window do:
796
+ // a key cannot be in force over beats nothing sounds in.
797
+ startBeat: musicStart,
798
+ endBeat: totalBeats,
799
+ key: _chunk3R3QVKY5cjs.resolveKey.call(void 0, opts.key),
800
+ confidence: 1
801
+ }
802
+ ] : _chunkSGLEQVJHcjs.keyTimelineFromNotes.call(void 0, pooled, { meters, totalBeats, budget }))));
803
+ const { timeline, keys, prevailingKey } = opts.timeline === void 0 || suppliedKeys === void 0 ? _chunkSGLEQVJHcjs.chordTimelineFromNotes.call(void 0, pooled, {
804
+ key: opts.key,
805
+ meters,
806
+ pickupBeats: opts.pickupBeats,
807
+ harmonicRhythm: opts.harmonicRhythm,
808
+ budget
809
+ }) : {
810
+ timeline: opts.timeline,
811
+ keys: suppliedKeys,
812
+ prevailingKey: _nullishCoalesce(_chunkSGLEQVJHcjs.prevailingKeyOf.call(void 0, suppliedKeys), () => ( _chunk3R3QVKY5cjs.resolveKey.call(void 0, _chunk3R3QVKY5cjs.majorKey.call(void 0, 0))))
813
+ };
814
+ const regions = _nullishCoalesce(opts.keys, () => ( keys));
815
+ const keyAt = _chunkSGLEQVJHcjs.keyLookup.call(void 0, regions, _nullishCoalesce(_chunkSGLEQVJHcjs.prevailingKeyOf.call(void 0, regions), () => ( prevailingKey)));
816
+ const prepared = prepareTracks(tracks);
817
+ const points = [];
818
+ const sampleCount = Math.max(0, Math.ceil((totalBeats - sampleStart) / step - _chunkS5BKZZGMcjs.BEAT_EPS));
819
+ _chunkS5BKZZGMcjs.assertGenerationBudget.call(void 0, sampleCount, "tension samples", budget);
820
+ const voiceCount = prepared.reduce((sum, track) => sum + track.voices.length, 0);
821
+ _chunkS5BKZZGMcjs.assertGenerationBudget.call(void 0,
822
+ sampleCount * Math.min(128, voiceCount) * voiceCount,
823
+ "tension sample-voice comparisons",
824
+ budget
825
+ );
826
+ for (let i = 0; i < sampleCount; i += 1) {
827
+ const beat = i === 0 ? Math.max(sampleStart, musicStart) : sampleStart + i * step;
828
+ points.push({
829
+ beat,
830
+ // The pitch classes alone: tension is scored from what sounds against
831
+ // the tonic, and no reading below here asks how the key is written.
832
+ tension: sampleTension(prepared, timeline, _chunk3R3QVKY5cjs.scaleOf.call(void 0, keyAt(beat)), meters, profile, beat)
833
+ });
834
+ }
835
+ return points;
836
+ }
837
+ function tensionCurveFrom(tracks, analysis, opts = {}) {
838
+ const asked = _chunkS5BKZZGMcjs.assertRecord.call(void 0, opts, "opts");
839
+ const carried = carriedHarmony(_chunkS5BKZZGMcjs.assertRecord.call(void 0, analysis, "analysis"), asked);
840
+ return tensionCurve(tracks, { ...carried, ...asked });
841
+ }
842
+ function carriedHarmony(analysis, opts) {
843
+ if (opts.harmonyTracks !== void 0) {
844
+ return {};
845
+ }
846
+ if (opts.key !== void 0 || opts.keys !== void 0) {
847
+ return { timeline: analysis.timeline };
848
+ }
849
+ return { timeline: analysis.timeline, keys: analysis.keys };
850
+ }
851
+ function allSounding(prepared, beat) {
852
+ const out = [];
853
+ for (let t = 0; t < prepared.length; t += 1) {
854
+ const track = prepared[t];
855
+ if (!track || isPercussion(track.role)) {
856
+ continue;
857
+ }
858
+ for (const subVoice of track.voices) {
859
+ const note = subVoice.at(beat);
860
+ if (note !== void 0) {
861
+ out.push({ pitch: note.pitch, track: t });
862
+ }
863
+ }
864
+ }
865
+ return out;
866
+ }
867
+ function sampleTension(prepared, timeline, key, meters, profile, beat) {
868
+ const sounding = allSounding(prepared, beat);
869
+ if (sounding.length === 0) {
870
+ return 0;
871
+ }
872
+ const chord = timeline.at(beat);
873
+ const functionScore = chord ? FUNCTION_TENSION[_chunkSGLEQVJHcjs.functionOf.call(void 0, chord, key)] : 0;
874
+ const chordPcs = chord ? new Set(_chunk3R3QVKY5cjs.chordPitchClasses.call(void 0, chord)) : null;
875
+ const strongBeat = _chunkS5BKZZGMcjs.isStrongBeat.call(void 0, beat, meters);
876
+ let nonChord = 0;
877
+ let dissonant = 0;
878
+ const pitches = /* @__PURE__ */ new Map();
879
+ for (const voice of sounding) {
880
+ pitches.set(voice.pitch, (_nullishCoalesce(pitches.get(voice.pitch), () => ( 0))) + 1);
881
+ }
882
+ const others = [];
883
+ const firstOf = /* @__PURE__ */ new Map();
884
+ for (const [pitch, count] of pitches) {
885
+ firstOf.set(pitch, others.length);
886
+ for (let i = 0; i < count; i += 1) {
887
+ others.push({ pitch });
888
+ }
889
+ }
890
+ for (const [pitch, count] of pitches) {
891
+ if (chordPcs && !chordPcs.has(_chunkS5BKZZGMcjs.pitchClassOf.call(void 0, pitch))) {
892
+ nonChord += count;
893
+ }
894
+ const at = _nullishCoalesce(firstOf.get(pitch), () => ( 0));
895
+ const last = others.length - 1;
896
+ const held = others[at];
897
+ const tail = others[last];
898
+ if (held !== void 0 && tail !== void 0) {
899
+ others[at] = tail;
900
+ others.pop();
901
+ }
902
+ const result = _chunk3R3QVKY5cjs.evaluateSafety.call(void 0,
903
+ { profile, candidatePitch: pitch, chord, key, otherVoices: others, strongBeat },
904
+ { suggestions: false }
905
+ );
906
+ if (held !== void 0 && tail !== void 0) {
907
+ others.push(tail);
908
+ others[at] = held;
909
+ }
910
+ if (result.safety === 2 /* Dissonant */) {
911
+ dissonant += count;
912
+ }
913
+ }
914
+ const dissonanceScore = Math.max(nonChord, dissonant) / sounding.length;
915
+ let low = Number.POSITIVE_INFINITY;
916
+ let high = Number.NEGATIVE_INFINITY;
917
+ for (const voice of sounding) {
918
+ low = Math.min(low, voice.pitch);
919
+ high = Math.max(high, voice.pitch);
920
+ }
921
+ const spanScore = Math.min(1, (high - low) / SPAN_SATURATION);
922
+ return clamp012(
923
+ FUNCTION_WEIGHT * functionScore + DISSONANCE_WEIGHT * dissonanceScore + SPAN_WEIGHT * spanScore
924
+ );
925
+ }
926
+
927
+ // src/analyze/form/hypermeter.ts
928
+ var GROUP_CANDIDATES = [2, 3, 4, 6, 8];
929
+ var GROUP_PRIOR = /* @__PURE__ */ new Map([
930
+ [2, 0.9],
931
+ [3, 0.75],
932
+ [4, 1],
933
+ [6, 0.7],
934
+ [8, 0.85]
935
+ ]);
936
+ var CONTRAST_SHARE = 0.65;
937
+ var CADENCE_SHARE = 0.35;
938
+ var GROUP_FINAL_FIT = 1;
939
+ var ELIDED_FIT = 0.5;
940
+ var MIN_GROUPS = 2;
941
+ var MARGIN_FLOOR = 0.7;
942
+ function accentContrast(novelty, slices, groupBars, phase) {
943
+ let headSum = 0;
944
+ let headCount = 0;
945
+ let otherSum = 0;
946
+ let otherCount = 0;
947
+ for (let i = 0; i < slices.length; i += 1) {
948
+ const slice = slices[i];
949
+ if (slice === void 0) {
950
+ continue;
951
+ }
952
+ const value = _nullishCoalesce(novelty[i], () => ( 0));
953
+ if (_chunkSGLEQVJHcjs.floorMod.call(void 0, slice.index - phase, groupBars) === 0) {
954
+ headSum += value;
955
+ headCount += 1;
956
+ } else {
957
+ otherSum += value;
958
+ otherCount += 1;
959
+ }
960
+ }
961
+ if (headCount === 0 || otherCount === 0) {
962
+ return 0;
963
+ }
964
+ return headSum / headCount - otherSum / otherCount;
965
+ }
966
+ function cadenceFitOf(cadenceBars, slices, groupBars, phase) {
967
+ const lastBar = _nullishCoalesce(_optionalChain([slices, 'access', _41 => _41[slices.length - 1], 'optionalAccess', _42 => _42.index]), () => ( -1));
968
+ let ends = 0;
969
+ let fit = 0;
970
+ for (const slice of slices) {
971
+ if (slice.index < 0 || _chunkSGLEQVJHcjs.floorMod.call(void 0, slice.index - phase, groupBars) !== groupBars - 1) {
972
+ continue;
973
+ }
974
+ ends += 1;
975
+ if (cadenceBars.has(slice.index)) {
976
+ fit += GROUP_FINAL_FIT;
977
+ } else if (slice.index < lastBar && cadenceBars.has(slice.index + 1)) {
978
+ fit += ELIDED_FIT;
979
+ }
980
+ }
981
+ return ends === 0 ? 0 : _chunkSGLEQVJHcjs.clamp01.call(void 0, fit / ends);
982
+ }
983
+ function undecidedReading(slices) {
984
+ const fullBars = slices.filter((slice) => slice.index >= 0);
985
+ const groupBars = Math.max(1, fullBars.length);
986
+ const first = fullBars[0];
987
+ return {
988
+ groupBars,
989
+ downbeats: first === void 0 ? [] : [first.startBeat],
990
+ confidence: 0,
991
+ rationale: `Span of ${fullBars.length} bar(s) is too short to establish a grouping; read as one group`
992
+ };
993
+ }
994
+ function hypermeter(notes, meter, opts = {}) {
995
+ const asked = _chunkS5BKZZGMcjs.assertOptions.call(void 0, opts, "opts");
996
+ const meters = _chunkS5BKZZGMcjs.resolveMeters.call(void 0, { meters: meter }, "meter");
997
+ _chunkS5BKZZGMcjs.assertNoteEvents.call(void 0, notes, "hypermeter notes", {
998
+ allowNonPositiveDuration: true,
999
+ budget: asked.budget
1000
+ });
1001
+ const sounding = notes.filter((note) => note.durationBeat > 0);
1002
+ const firstOnset = _chunkSGLEQVJHcjs.firstSoundingBeat.call(void 0, sounding);
1003
+ const lastEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
1004
+ const spanEnd = _nullishCoalesce(asked.totalBeats, () => ( lastEnd));
1005
+ _chunkS5BKZZGMcjs.assertRange.call(void 0, spanEnd, 0, Number.MAX_SAFE_INTEGER, "hypermeter totalBeats");
1006
+ const spanStart = _chunkSGLEQVJHcjs.barGridStart.call(void 0, firstOnset, meters);
1007
+ const slices = _chunkSGLEQVJHcjs.sliceBars.call(void 0, sounding, meters, spanStart, spanEnd, asked.budget);
1008
+ const fullBars = slices.reduce((count, slice) => slice.index >= 0 ? count + 1 : count, 0);
1009
+ const candidates = GROUP_CANDIDATES.filter((groupBars) => fullBars >= groupBars * MIN_GROUPS);
1010
+ if (candidates.length === 0) {
1011
+ return undecidedReading(slices);
1012
+ }
1013
+ _chunkS5BKZZGMcjs.assertGenerationBudget.call(void 0,
1014
+ candidates.reduce((sum, groupBars) => sum + groupBars * slices.length, 0),
1015
+ "hypermeter readings",
1016
+ asked.budget
1017
+ );
1018
+ const novelty = slices.map((_, index) => _chunkSGLEQVJHcjs.harmonicNovelty.call(void 0, slices, index));
1019
+ const cadenceBars = new Set((_nullishCoalesce(asked.cadenceBeats, () => ( []))).map((beat) => _chunkS5BKZZGMcjs.barIndexAt.call(void 0, beat, meters)));
1020
+ const hasCadences = cadenceBars.size > 0;
1021
+ const readings = [];
1022
+ for (const groupBars of candidates) {
1023
+ const phases = Array.from({ length: groupBars }, (_, phase) => ({
1024
+ phase,
1025
+ contrast: accentContrast(novelty, slices, groupBars, phase),
1026
+ cadenceFit: cadenceFitOf(cadenceBars, slices, groupBars, phase)
1027
+ }));
1028
+ const bestFit = phases.reduce((best2, reading) => Math.max(best2, reading.cadenceFit), 0);
1029
+ for (const { phase, contrast, cadenceFit } of phases) {
1030
+ if (hasCadences && cadenceFit < bestFit - _chunkS5BKZZGMcjs.BEAT_EPS) {
1031
+ continue;
1032
+ }
1033
+ const evidence = _chunkSGLEQVJHcjs.clamp01.call(void 0,
1034
+ hasCadences ? (
1035
+ // Floored at zero rather than summed raw: group heads that restate
1036
+ // the harmony their group opened with — the commonest way a phrase
1037
+ // begins — change less than the bars between them, and a negative
1038
+ // contrast subtracted from the cadence term would count that
1039
+ // restatement as evidence against the very phase it confirms.
1040
+ CONTRAST_SHARE * _chunkSGLEQVJHcjs.clamp01.call(void 0, contrast) + CADENCE_SHARE * cadenceFit
1041
+ ) : contrast
1042
+ );
1043
+ readings.push({
1044
+ groupBars,
1045
+ phase,
1046
+ contrast,
1047
+ cadenceFit,
1048
+ evidence,
1049
+ score: evidence * (_nullishCoalesce(GROUP_PRIOR.get(groupBars), () => ( 0)))
1050
+ });
1051
+ }
1052
+ }
1053
+ readings.sort(
1054
+ (a, b) => b.score - a.score || (_nullishCoalesce(GROUP_PRIOR.get(b.groupBars), () => ( 0))) - (_nullishCoalesce(GROUP_PRIOR.get(a.groupBars), () => ( 0))) || a.groupBars - b.groupBars || a.phase - b.phase
1055
+ );
1056
+ const best = readings[0];
1057
+ if (best === void 0) {
1058
+ return undecidedReading(slices);
1059
+ }
1060
+ const runnerUp = readings.find((reading) => reading.groupBars !== best.groupBars);
1061
+ const margin = runnerUp === void 0 || best.score <= _chunkS5BKZZGMcjs.BEAT_EPS ? 1 : _chunkSGLEQVJHcjs.clamp01.call(void 0, (best.score - runnerUp.score) / best.score);
1062
+ const downbeats = slices.filter((slice) => slice.index >= 0 && _chunkSGLEQVJHcjs.floorMod.call(void 0, slice.index - best.phase, best.groupBars) === 0).map((slice) => slice.startBeat);
1063
+ const cadencePart = hasCadences ? `${Math.round(best.cadenceFit * 100)}% of group ends closed by a cadence` : "no cadences given";
1064
+ return {
1065
+ groupBars: best.groupBars,
1066
+ downbeats,
1067
+ // Mostly the evidence, discounted when a rival grouping explains the
1068
+ // accents nearly as well. The discount is the smaller term because the
1069
+ // runner-up is nearly always a multiple or a divisor of the winner — four
1070
+ // bars nest inside eight — and a reading of the same accents at another
1071
+ // level is weaker counter-evidence than an unrelated rival would be.
1072
+ confidence: _chunkSGLEQVJHcjs.clamp01.call(void 0, best.evidence * (MARGIN_FLOOR + (1 - MARGIN_FLOOR) * margin)),
1073
+ rationale: `${best.groupBars}-bar groups from bar ${best.phase}: harmonic change ${Math.round(best.contrast * 100)}% stronger on group heads, ${cadencePart}`
1074
+ };
1075
+ }
1076
+
1077
+ // src/analyze/melody/internal.ts
1078
+ var RHYTHM_GRID = 64;
1079
+ function phraseNotes(phrase) {
1080
+ if (Array.isArray(phrase)) {
1081
+ return phrase;
1082
+ }
1083
+ return _chunkS5BKZZGMcjs.assertRecord.call(void 0, phrase, "melodic phrase").notes;
1084
+ }
1085
+ function orderedNotes(notes, name, budget) {
1086
+ _chunkS5BKZZGMcjs.assertNoteEvents.call(void 0, notes, name, { allowNonPositiveDuration: true, budget });
1087
+ const sounding = notes.filter((note) => note.durationBeat > 0).sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
1088
+ const line = [];
1089
+ for (const note of sounding) {
1090
+ const previous = line[line.length - 1];
1091
+ if (previous !== void 0 && Math.abs(note.startBeat - previous.startBeat) <= _chunkS5BKZZGMcjs.BEAT_EPS) {
1092
+ line[line.length - 1] = note;
1093
+ continue;
1094
+ }
1095
+ line.push(note);
1096
+ }
1097
+ return line;
1098
+ }
1099
+ function intervalsOf(notes) {
1100
+ const out = [];
1101
+ for (let i = 1; i < notes.length; i += 1) {
1102
+ out.push((_nullishCoalesce(_optionalChain([notes, 'access', _43 => _43[i], 'optionalAccess', _44 => _44.pitch]), () => ( 0))) - (_nullishCoalesce(_optionalChain([notes, 'access', _45 => _45[i - 1], 'optionalAccess', _46 => _46.pitch]), () => ( 0))));
1103
+ }
1104
+ return out;
1105
+ }
1106
+ function onsetGaps(notes) {
1107
+ const out = [];
1108
+ for (let i = 1; i < notes.length; i += 1) {
1109
+ out.push((_nullishCoalesce(_optionalChain([notes, 'access', _47 => _47[i], 'optionalAccess', _48 => _48.startBeat]), () => ( 0))) - (_nullishCoalesce(_optionalChain([notes, 'access', _49 => _49[i - 1], 'optionalAccess', _50 => _50.startBeat]), () => ( 0))));
1110
+ }
1111
+ return out;
1112
+ }
1113
+ function retrogradeGaps(notes) {
1114
+ const out = [];
1115
+ for (let i = notes.length - 1; i >= 1; i -= 1) {
1116
+ const later = notes[i];
1117
+ const earlier = notes[i - 1];
1118
+ out.push(
1119
+ (_nullishCoalesce(_optionalChain([later, 'optionalAccess', _51 => _51.startBeat]), () => ( 0))) + (_nullishCoalesce(_optionalChain([later, 'optionalAccess', _52 => _52.durationBeat]), () => ( 0))) - ((_nullishCoalesce(_optionalChain([earlier, 'optionalAccess', _53 => _53.startBeat]), () => ( 0))) + (_nullishCoalesce(_optionalChain([earlier, 'optionalAccess', _54 => _54.durationBeat]), () => ( 0))))
1120
+ );
1121
+ }
1122
+ return out;
1123
+ }
1124
+ function rhythmProfile(gaps) {
1125
+ const unit = _nullishCoalesce(gaps[0], () => ( 0));
1126
+ if (!(unit > _chunkS5BKZZGMcjs.BEAT_EPS)) {
1127
+ return gaps.length === 0 ? [] : null;
1128
+ }
1129
+ return gaps.map((gap) => Math.round(gap / unit * RHYTHM_GRID) / RHYTHM_GRID);
1130
+ }
1131
+ function sameNumbers(a, b) {
1132
+ if (a.length !== b.length) {
1133
+ return false;
1134
+ }
1135
+ for (let i = 0; i < a.length; i += 1) {
1136
+ if (Math.abs((_nullishCoalesce(a[i], () => ( 0))) - (_nullishCoalesce(b[i], () => ( 0)))) > _chunkS5BKZZGMcjs.BEAT_EPS) {
1137
+ return false;
1138
+ }
1139
+ }
1140
+ return true;
1141
+ }
1142
+ function roundTo(value, digits) {
1143
+ const scale = 10 ** digits;
1144
+ return Math.round(value * scale) / scale;
1145
+ }
1146
+ function endBeatOf(notes) {
1147
+ let end = 0;
1148
+ for (const note of notes) {
1149
+ end = Math.max(end, note.startBeat + note.durationBeat);
1150
+ }
1151
+ return end;
1152
+ }
1153
+ function onsetSpan(notes) {
1154
+ if (notes.length < 2) {
1155
+ return 0;
1156
+ }
1157
+ return (_nullishCoalesce(_optionalChain([notes, 'access', _55 => _55[notes.length - 1], 'optionalAccess', _56 => _56.startBeat]), () => ( 0))) - (_nullishCoalesce(_optionalChain([notes, 'access', _57 => _57[0], 'optionalAccess', _58 => _58.startBeat]), () => ( 0)));
1158
+ }
1159
+
1160
+ // src/analyze/melody/contour.ts
1161
+ var DIRECTIONAL_SHARE = 0.75;
1162
+ function extremes(notes) {
1163
+ if (notes.length === 0) {
1164
+ return { peak: -1, trough: -1 };
1165
+ }
1166
+ let peak = 0;
1167
+ let trough = 0;
1168
+ for (let i = 1; i < notes.length; i += 1) {
1169
+ if ((_nullishCoalesce(_optionalChain([notes, 'access', _59 => _59[i], 'optionalAccess', _60 => _60.pitch]), () => ( 0))) > (_nullishCoalesce(_optionalChain([notes, 'access', _61 => _61[peak], 'optionalAccess', _62 => _62.pitch]), () => ( 0)))) {
1170
+ peak = i;
1171
+ }
1172
+ if ((_nullishCoalesce(_optionalChain([notes, 'access', _63 => _63[i], 'optionalAccess', _64 => _64.pitch]), () => ( 0))) < (_nullishCoalesce(_optionalChain([notes, 'access', _65 => _65[trough], 'optionalAccess', _66 => _66.pitch]), () => ( 0)))) {
1173
+ trough = i;
1174
+ }
1175
+ }
1176
+ return { peak, trough };
1177
+ }
1178
+ function melodicContour(notes) {
1179
+ const line = orderedNotes(phraseNotes(notes), "contour notes");
1180
+ const intervals = intervalsOf(line);
1181
+ const directions = intervals.map(
1182
+ (step) => step > 0 ? "up" : step < 0 ? "down" : "same"
1183
+ );
1184
+ const { peak, trough } = extremes(line);
1185
+ const range = (_nullishCoalesce(_optionalChain([line, 'access', _67 => _67[peak], 'optionalAccess', _68 => _68.pitch]), () => ( 0))) - (_nullishCoalesce(_optionalChain([line, 'access', _69 => _69[trough], 'optionalAccess', _70 => _70.pitch]), () => ( 0)));
1186
+ const motion = intervals.reduce((sum, step) => sum + Math.abs(step), 0);
1187
+ if (motion === 0) {
1188
+ return {
1189
+ directions,
1190
+ shape: "static",
1191
+ peakIndex: peak,
1192
+ troughIndex: trough,
1193
+ range: 0,
1194
+ rationale: line.length < 2 ? "Too short to have a direction, so the line is static" : "Every note at the same pitch, so the line is static"
1195
+ };
1196
+ }
1197
+ const first = _nullishCoalesce(_optionalChain([line, 'access', _71 => _71[0], 'optionalAccess', _72 => _72.pitch]), () => ( 0));
1198
+ const last = _nullishCoalesce(_optionalChain([line, 'access', _73 => _73[line.length - 1], 'optionalAccess', _74 => _74.pitch]), () => ( 0));
1199
+ const net = last - first;
1200
+ const directness = Math.abs(net) / motion;
1201
+ const share = Math.round(directness * 100);
1202
+ if (directness >= DIRECTIONAL_SHARE) {
1203
+ const shape = net > 0 ? "ascending" : "descending";
1204
+ return {
1205
+ directions,
1206
+ shape,
1207
+ peakIndex: peak,
1208
+ troughIndex: trough,
1209
+ range,
1210
+ rationale: `${share}% of the motion runs ${net > 0 ? "up" : "down"}, so the line is ${shape}`
1211
+ };
1212
+ }
1213
+ const peakPitch = _nullishCoalesce(_optionalChain([line, 'access', _75 => _75[peak], 'optionalAccess', _76 => _76.pitch]), () => ( 0));
1214
+ const archMotion = peakPitch - first + (peakPitch - last);
1215
+ if (peak > 0 && peak < line.length - 1 && archMotion >= DIRECTIONAL_SHARE * motion) {
1216
+ return {
1217
+ directions,
1218
+ shape: "arch",
1219
+ peakIndex: peak,
1220
+ troughIndex: trough,
1221
+ range,
1222
+ rationale: `Rises to its peak at note ${peak + 1} of ${line.length} and falls back, so the line is an arch`
1223
+ };
1224
+ }
1225
+ return {
1226
+ directions,
1227
+ shape: "wave",
1228
+ peakIndex: peak,
1229
+ troughIndex: trough,
1230
+ range,
1231
+ rationale: `Changes direction without settling either way (${share}% of the motion nets out), so the line is a wave`
1232
+ };
1233
+ }
1234
+
1235
+ // src/analyze/melody/motifs.ts
1236
+ var MIN_CELL_NOTES = 3;
1237
+ var MAX_CELL_NOTES = 8;
1238
+ var MIN_OCCURRENCES = 2;
1239
+ function windowSignature(notes) {
1240
+ const profile = rhythmProfile(onsetGaps(notes));
1241
+ if (profile === null) {
1242
+ return null;
1243
+ }
1244
+ return `${notes.length}|${intervalsOf(notes).join(",")}|${profile.join(",")}`;
1245
+ }
1246
+ function nonOverlapping(starts, length) {
1247
+ const picked = [];
1248
+ let lastEnd = Number.NEGATIVE_INFINITY;
1249
+ for (const start of starts) {
1250
+ if (start > lastEnd) {
1251
+ picked.push(start);
1252
+ lastEnd = start + length - 1;
1253
+ }
1254
+ }
1255
+ return picked;
1256
+ }
1257
+ function subsumes(outer, inner) {
1258
+ let at = 0;
1259
+ for (const start of inner.starts) {
1260
+ while (at + 1 < outer.starts.length && (_nullishCoalesce(outer.starts[at + 1], () => ( 0))) <= start) {
1261
+ at += 1;
1262
+ }
1263
+ const from = outer.starts[at];
1264
+ if (from === void 0 || start < from || start + inner.length > from + outer.length) {
1265
+ return false;
1266
+ }
1267
+ }
1268
+ return true;
1269
+ }
1270
+ function listBeats(occurrences) {
1271
+ const shown = occurrences.slice(0, 5).map((occurrence) => roundTo(occurrence.startBeat, 3));
1272
+ return occurrences.length > shown.length ? `${shown.join(", ")}, and ${occurrences.length - shown.length} more` : shown.join(", ");
1273
+ }
1274
+ function motifFromGroup(group, notes) {
1275
+ const first = _nullishCoalesce(group.starts[0], () => ( 0));
1276
+ const prime = notes.slice(first, first + group.length).map((note) => ({ ...note }));
1277
+ const primePitch = _nullishCoalesce(_optionalChain([prime, 'access', _77 => _77[0], 'optionalAccess', _78 => _78.pitch]), () => ( 0));
1278
+ const primeSpan = onsetSpan(prime);
1279
+ const occurrences = group.starts.map((start) => {
1280
+ const statement = notes.slice(start, start + group.length);
1281
+ const span = onsetSpan(statement);
1282
+ return {
1283
+ noteIndex: start,
1284
+ startBeat: _nullishCoalesce(_optionalChain([statement, 'access', _79 => _79[0], 'optionalAccess', _80 => _80.startBeat]), () => ( 0)),
1285
+ endBeat: endBeatOf(statement),
1286
+ transpose: (_nullishCoalesce(_optionalChain([statement, 'access', _81 => _81[0], 'optionalAccess', _82 => _82.pitch]), () => ( 0))) - primePitch,
1287
+ timeRatio: primeSpan > _chunkS5BKZZGMcjs.BEAT_EPS ? span / primeSpan : 1
1288
+ };
1289
+ });
1290
+ const shape = melodicContour(prime).shape;
1291
+ const transposed = occurrences.some((occurrence) => occurrence.transpose !== 0);
1292
+ const stretched = occurrences.some((occurrence) => Math.abs(occurrence.timeRatio - 1) > _chunkS5BKZZGMcjs.BEAT_EPS);
1293
+ const detail = [
1294
+ transposed ? "at more than one pitch level" : "at the same pitch level",
1295
+ stretched ? "and in more than one set of note values" : ""
1296
+ ].filter((part) => part !== "").join(" ");
1297
+ return {
1298
+ notes: prime,
1299
+ intervals: intervalsOf(prime),
1300
+ rhythm: _nullishCoalesce(rhythmProfile(onsetGaps(prime)), () => ( [])),
1301
+ occurrences,
1302
+ rationale: `${group.length}-note ${shape} cell stated ${occurrences.length} ${occurrences.length === 1 ? "time" : "times"} (beats ${listBeats(occurrences)}), ${detail}`
1303
+ };
1304
+ }
1305
+ function extractMotifs(notes, opts = {}) {
1306
+ const asked = _chunkS5BKZZGMcjs.assertOptions.call(void 0, opts, "opts");
1307
+ const minNotes = _chunkS5BKZZGMcjs.assertInteger.call(void 0, _nullishCoalesce(asked.minNotes, () => ( MIN_CELL_NOTES)), "motif minNotes", 2, 64);
1308
+ const maxNotes = _chunkS5BKZZGMcjs.assertInteger.call(void 0, _nullishCoalesce(asked.maxNotes, () => ( MAX_CELL_NOTES)), "motif maxNotes", 2, 64);
1309
+ if (maxNotes < minNotes) {
1310
+ throw new (0, _chunkS5BKZZGMcjs.InvalidInputError)(
1311
+ `motif maxNotes must be at least minNotes ${minNotes}; received ${maxNotes}`
1312
+ );
1313
+ }
1314
+ const minOccurrences = _chunkS5BKZZGMcjs.assertInteger.call(void 0,
1315
+ _nullishCoalesce(asked.minOccurrences, () => ( MIN_OCCURRENCES)),
1316
+ "motif minOccurrences",
1317
+ 2
1318
+ );
1319
+ const sounding = orderedNotes(notes, "melody notes", asked.budget);
1320
+ if (sounding.length < minNotes) {
1321
+ return [];
1322
+ }
1323
+ _chunkS5BKZZGMcjs.assertGenerationBudget.call(void 0,
1324
+ sounding.length * (maxNotes - minNotes + 1),
1325
+ "motif windows",
1326
+ asked.budget
1327
+ );
1328
+ const groups = /* @__PURE__ */ new Map();
1329
+ for (let length = minNotes; length <= maxNotes; length += 1) {
1330
+ for (let start = 0; start + length <= sounding.length; start += 1) {
1331
+ const signature = windowSignature(sounding.slice(start, start + length));
1332
+ if (signature === null) {
1333
+ continue;
1334
+ }
1335
+ const group = groups.get(signature);
1336
+ if (group === void 0) {
1337
+ groups.set(signature, { signature, length, starts: [start] });
1338
+ } else {
1339
+ group.starts.push(start);
1340
+ }
1341
+ }
1342
+ }
1343
+ const candidates = [...groups.values()].map((group) => ({ ...group, starts: nonOverlapping(group.starts, group.length) })).filter((group) => group.starts.length >= minOccurrences).sort(
1344
+ (a, b) => b.length - a.length || b.starts.length - a.starts.length || (_nullishCoalesce(a.starts[0], () => ( 0))) - (_nullishCoalesce(b.starts[0], () => ( 0))) || (a.signature < b.signature ? -1 : a.signature > b.signature ? 1 : 0)
1345
+ );
1346
+ const keptByLength = /* @__PURE__ */ new Map();
1347
+ const kept = [];
1348
+ let comparisons = 0;
1349
+ for (const candidate of candidates) {
1350
+ const first = _nullishCoalesce(candidate.starts[0], () => ( 0));
1351
+ let covered = false;
1352
+ for (let length = candidate.length + 1; length <= maxNotes && !covered; length += 1) {
1353
+ const byStart = keptByLength.get(length);
1354
+ if (byStart === void 0) {
1355
+ continue;
1356
+ }
1357
+ for (let from = Math.max(0, first - (length - candidate.length)); from <= first; from += 1) {
1358
+ comparisons += 1;
1359
+ const outer = byStart.get(from);
1360
+ if (outer === void 0 || outer.starts.length < candidate.starts.length) {
1361
+ continue;
1362
+ }
1363
+ comparisons += outer.starts.length + candidate.starts.length;
1364
+ if (subsumes(outer, candidate)) {
1365
+ covered = true;
1366
+ break;
1367
+ }
1368
+ }
1369
+ }
1370
+ _chunkS5BKZZGMcjs.assertGenerationBudget.call(void 0, comparisons, "motif subsumption comparisons", asked.budget);
1371
+ if (!covered) {
1372
+ kept.push(candidate);
1373
+ let byStart = keptByLength.get(candidate.length);
1374
+ if (byStart === void 0) {
1375
+ byStart = /* @__PURE__ */ new Map();
1376
+ keptByLength.set(candidate.length, byStart);
1377
+ }
1378
+ for (const start of candidate.starts) {
1379
+ byStart.set(start, candidate);
1380
+ }
1381
+ }
1382
+ }
1383
+ return kept.map((group) => motifFromGroup(group, sounding));
1384
+ }
1385
+ function motifFromNotes(notes) {
1386
+ const sounding = orderedNotes(notes, "motif notes").map((note) => ({ ...note }));
1387
+ return motifFromGroup(
1388
+ { signature: _nullishCoalesce(windowSignature(sounding), () => ( "")), length: sounding.length, starts: [0] },
1389
+ sounding
1390
+ );
1391
+ }
1392
+
1393
+ // src/analyze/melody/relation.ts
1394
+ function spellingOf(key) {
1395
+ if (key === void 0) {
1396
+ return "sharp";
1397
+ }
1398
+ return _chunk3R3QVKY5cjs.keySignatureFifths.call(void 0, key.tonic, key.scale) < 0 ? "flat" : "sharp";
1399
+ }
1400
+ function intervalBetween(from, to, key) {
1401
+ const spelling = spellingOf(key);
1402
+ return _chunkS5BKZZGMcjs.spelledInterval.call(void 0, _chunkS5BKZZGMcjs.midiToNote.call(void 0, from, spelling), _chunkS5BKZZGMcjs.midiToNote.call(void 0, to, spelling));
1403
+ }
1404
+ function intervalName(interval) {
1405
+ return `${interval.descending && interval.semitones !== 0 ? "-" : ""}${interval.quality}${interval.number}`;
1406
+ }
1407
+ function ladderIndices(notes, key) {
1408
+ const root = _chunkS5BKZZGMcjs.pitchClassOf.call(void 0, key.rootPc);
1409
+ const offsets = _chunk3R3QVKY5cjs.scaleTonesInDegreeOrder.call(void 0, key).map((pc) => _chunkS5BKZZGMcjs.pitchClassOf.call(void 0, pc - root));
1410
+ if (offsets.length === 0) {
1411
+ return null;
1412
+ }
1413
+ const out = [];
1414
+ for (const note of notes) {
1415
+ const semitones = note.pitch - root;
1416
+ const octave = Math.floor(semitones / 12);
1417
+ const degree = offsets.indexOf(semitones - octave * 12);
1418
+ if (degree < 0) {
1419
+ return null;
1420
+ }
1421
+ out.push(octave * offsets.length + degree);
1422
+ }
1423
+ return out;
1424
+ }
1425
+ function ladderSteps(indices) {
1426
+ const out = [];
1427
+ for (let i = 1; i < indices.length; i += 1) {
1428
+ out.push((_nullishCoalesce(indices[i], () => ( 0))) - (_nullishCoalesce(indices[i - 1], () => ( 0))));
1429
+ }
1430
+ return out;
1431
+ }
1432
+ function pitchShapes(a, b) {
1433
+ const shapes = [];
1434
+ if (sameNumbers(a, b)) {
1435
+ shapes.push("exact");
1436
+ }
1437
+ if (sameNumbers(
1438
+ b,
1439
+ a.map((step) => -step)
1440
+ )) {
1441
+ shapes.push("inversion");
1442
+ }
1443
+ const reversed = [...a].reverse();
1444
+ if (sameNumbers(
1445
+ b,
1446
+ reversed.map((step) => -step)
1447
+ )) {
1448
+ shapes.push("retrograde");
1449
+ }
1450
+ if (sameNumbers(b, reversed)) {
1451
+ shapes.push("retrogradeInversion");
1452
+ }
1453
+ return shapes;
1454
+ }
1455
+ function totalGap(gaps) {
1456
+ return gaps.reduce((sum, gap) => sum + gap, 0);
1457
+ }
1458
+ function stretchRatio(modelGaps, answerGaps) {
1459
+ const span = totalGap(modelGaps);
1460
+ return span > _chunkS5BKZZGMcjs.BEAT_EPS ? totalGap(answerGaps) / span : 1;
1461
+ }
1462
+ function stretchPhrase(ratio) {
1463
+ return `note values ${roundTo(ratio, 3)}x the model's`;
1464
+ }
1465
+ function relateMotifs(a, b, keyLike) {
1466
+ const key = keyLike === void 0 ? void 0 : _chunk3R3QVKY5cjs.resolveKey.call(void 0, keyLike);
1467
+ const model = _chunkS5BKZZGMcjs.assertArray.call(void 0, _chunkS5BKZZGMcjs.assertRecord.call(void 0, a, "a").notes, "a.notes");
1468
+ const answer = _chunkS5BKZZGMcjs.assertArray.call(void 0, _chunkS5BKZZGMcjs.assertRecord.call(void 0, b, "b").notes, "b.notes");
1469
+ if (model.length === 0 || model.length !== answer.length) {
1470
+ return null;
1471
+ }
1472
+ const modelGaps = onsetGaps(model);
1473
+ const answerGaps = onsetGaps(answer);
1474
+ const modelProfile = rhythmProfile(modelGaps);
1475
+ const answerProfile = rhythmProfile(answerGaps);
1476
+ const retroGaps = retrogradeGaps(model);
1477
+ const retroProfile = rhythmProfile(retroGaps);
1478
+ if (modelProfile === null || answerProfile === null) {
1479
+ return null;
1480
+ }
1481
+ const forward = sameNumbers(modelProfile, answerProfile);
1482
+ const backward = retroProfile !== null && sameNumbers(retroProfile, answerProfile);
1483
+ const semitones = (_nullishCoalesce(_optionalChain([answer, 'access', _83 => _83[0], 'optionalAccess', _84 => _84.pitch]), () => ( 0))) - (_nullishCoalesce(_optionalChain([model, 'access', _85 => _85[0], 'optionalAccess', _86 => _86.pitch]), () => ( 0)));
1484
+ const interval = intervalBetween(_nullishCoalesce(_optionalChain([model, 'access', _87 => _87[0], 'optionalAccess', _88 => _88.pitch]), () => ( 0)), _nullishCoalesce(_optionalChain([answer, 'access', _89 => _89[0], 'optionalAccess', _90 => _90.pitch]), () => ( 0)), key);
1485
+ const gap = (_nullishCoalesce(_optionalChain([answer, 'access', _91 => _91[0], 'optionalAccess', _92 => _92.startBeat]), () => ( 0))) - endBeatOf(model);
1486
+ const sequence = gap >= -_chunkSGLEQVJHcjs.HUMANIZE_ADJACENCY && gap <= _chunkSGLEQVJHcjs.HUMANIZE_ADJACENCY;
1487
+ const timeRatio = stretchRatio(modelGaps, answerGaps);
1488
+ const stretched = Math.abs(timeRatio - 1) > _chunkS5BKZZGMcjs.BEAT_EPS;
1489
+ const shapes = pitchShapes(intervalsOf(model), intervalsOf(answer));
1490
+ const base = { sequence, semitones, interval, timeRatio };
1491
+ const backwardBase = { ...base, timeRatio: stretchRatio(retroGaps, answerGaps) };
1492
+ if (forward && shapes.includes("exact")) {
1493
+ if (semitones === 0) {
1494
+ if (!stretched) {
1495
+ return {
1496
+ ...base,
1497
+ kind: "repetition",
1498
+ rationale: "Exact repetition: the same pitches in the same rhythm"
1499
+ };
1500
+ }
1501
+ const augmented = timeRatio > 1;
1502
+ return {
1503
+ ...base,
1504
+ kind: augmented ? "augmentation" : "diminution",
1505
+ rationale: `${augmented ? "Augmentation" : "Diminution"}: the same pitches in ${stretchPhrase(timeRatio)}`
1506
+ };
1507
+ }
1508
+ const name = intervalName(interval);
1509
+ return {
1510
+ ...base,
1511
+ kind: "transposition",
1512
+ rationale: sequence ? `Real sequence: the cell restated ${name} away with every interval kept literally` + (stretched ? `, in ${stretchPhrase(timeRatio)}` : "") : `Transposition by ${name}: every interval kept literally` + (stretched ? `, in ${stretchPhrase(timeRatio)}` : "")
1513
+ };
1514
+ }
1515
+ if (forward && key !== void 0) {
1516
+ const modelLadder = ladderIndices(model, key.scale);
1517
+ const answerLadder = ladderIndices(answer, key.scale);
1518
+ if (modelLadder !== null && answerLadder !== null) {
1519
+ const degrees = (_nullishCoalesce(answerLadder[0], () => ( 0))) - (_nullishCoalesce(modelLadder[0], () => ( 0)));
1520
+ if (degrees !== 0 && sameNumbers(ladderSteps(modelLadder), ladderSteps(answerLadder))) {
1521
+ const direction = degrees > 0 ? "higher" : "lower";
1522
+ const count = Math.abs(degrees);
1523
+ return {
1524
+ ...base,
1525
+ kind: "tonalTransposition",
1526
+ degrees,
1527
+ rationale: `${sequence ? "Tonal sequence" : "Tonal transposition"}: the cell restated ${count} scale ${count === 1 ? "degree" : "degrees"} ${direction} inside the key, so the key resizes its intervals \u2014 a real one would have kept them literal`
1528
+ };
1529
+ }
1530
+ }
1531
+ }
1532
+ if (forward && shapes.includes("inversion")) {
1533
+ return {
1534
+ ...base,
1535
+ kind: "inversion",
1536
+ rationale: "Inversion: every interval turned upside down about the first note" + (stretched ? `, in ${stretchPhrase(timeRatio)}` : "")
1537
+ };
1538
+ }
1539
+ if (backward && shapes.includes("retrograde")) {
1540
+ return {
1541
+ ...backwardBase,
1542
+ kind: "retrograde",
1543
+ rationale: "Retrograde: the cell read back to front, rhythm included"
1544
+ };
1545
+ }
1546
+ if (backward && shapes.includes("retrogradeInversion")) {
1547
+ return {
1548
+ ...backwardBase,
1549
+ kind: "retrogradeInversion",
1550
+ rationale: "Retrograde inversion: the cell read back to front and turned upside down"
1551
+ };
1552
+ }
1553
+ return null;
1554
+ }
1555
+
1556
+ // src/analyze/melody/similarity.ts
1557
+ var INTERVAL_TOLERANCE = 6;
1558
+ var PITCH_WEIGHT = 0.75;
1559
+ var RHYTHM_WEIGHT = 0.25;
1560
+ function gradedEditDistance(a, b, cost) {
1561
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
1562
+ for (let i = 1; i <= a.length; i += 1) {
1563
+ const row = new Array(b.length + 1);
1564
+ row[0] = i;
1565
+ for (let j = 1; j <= b.length; j += 1) {
1566
+ const substitution = (_nullishCoalesce(previous[j - 1], () => ( 0))) + cost(_nullishCoalesce(a[i - 1], () => ( 0)), _nullishCoalesce(b[j - 1], () => ( 0)));
1567
+ const deletion = (_nullishCoalesce(previous[j], () => ( 0))) + 1;
1568
+ const insertion = (_nullishCoalesce(row[j - 1], () => ( 0))) + 1;
1569
+ row[j] = Math.min(substitution, deletion, insertion);
1570
+ }
1571
+ previous = row;
1572
+ }
1573
+ return _nullishCoalesce(previous[b.length], () => ( 0));
1574
+ }
1575
+ function distanceToSimilarity(distance, a, b) {
1576
+ const longest = Math.max(a, b);
1577
+ if (longest === 0) {
1578
+ return 1;
1579
+ }
1580
+ return Math.min(1, Math.max(0, 1 - distance / longest));
1581
+ }
1582
+ function intervalCost(x, y) {
1583
+ return Math.min(1, Math.abs(x - y) / INTERVAL_TOLERANCE);
1584
+ }
1585
+ function gapCost(x, y) {
1586
+ if (!(x > _chunkS5BKZZGMcjs.BEAT_EPS) || !(y > _chunkS5BKZZGMcjs.BEAT_EPS)) {
1587
+ return Math.abs(x - y) > _chunkS5BKZZGMcjs.BEAT_EPS ? 1 : 0;
1588
+ }
1589
+ return Math.min(1, Math.abs(Math.log2(x / y)));
1590
+ }
1591
+ function compareMelodies(a, b) {
1592
+ const left = orderedNotes(phraseNotes(a), "melody a");
1593
+ const right = orderedNotes(phraseNotes(b), "melody b");
1594
+ const leftIntervals = intervalsOf(left);
1595
+ const rightIntervals = intervalsOf(right);
1596
+ const pitchSimilarity = distanceToSimilarity(
1597
+ gradedEditDistance(leftIntervals, rightIntervals, intervalCost),
1598
+ leftIntervals.length,
1599
+ rightIntervals.length
1600
+ );
1601
+ const leftGaps = _nullishCoalesce(rhythmProfile(onsetGaps(left)), () => ( onsetGaps(left)));
1602
+ const rightGaps = _nullishCoalesce(rhythmProfile(onsetGaps(right)), () => ( onsetGaps(right)));
1603
+ const rhythmSimilarity = distanceToSimilarity(
1604
+ gradedEditDistance(leftGaps, rightGaps, gapCost),
1605
+ leftGaps.length,
1606
+ rightGaps.length
1607
+ );
1608
+ const similarity = PITCH_WEIGHT * pitchSimilarity + RHYTHM_WEIGHT * rhythmSimilarity;
1609
+ return {
1610
+ similarity,
1611
+ pitchSimilarity,
1612
+ rhythmSimilarity,
1613
+ rationale: `Interval shapes ${Math.round(pitchSimilarity * 100)}% alike, rhythms ${Math.round(rhythmSimilarity * 100)}% alike, weighted ${PITCH_WEIGHT}/${RHYTHM_WEIGHT} into ${roundTo(similarity, 3)}`
1614
+ };
1615
+ }
1616
+ function melodicSimilarity(a, b) {
1617
+ return compareMelodies(a, b).similarity;
1618
+ }
1619
+
1620
+ // src/analyze/form/phrase.ts
1621
+ var CADENCE_BOUNDARY_STRENGTH = {
1622
+ authentic: 0.9,
1623
+ modal: 0.75,
1624
+ plagal: 0.7,
1625
+ phrygian: 0.65,
1626
+ half: 0.6,
1627
+ deceptive: 0.45
1628
+ };
1629
+ var CADENCE_CLOSURE = {
1630
+ authentic: 0.85,
1631
+ modal: 0.6,
1632
+ plagal: 0.55,
1633
+ phrygian: 0.4,
1634
+ half: 0.35,
1635
+ deceptive: 0.3
1636
+ };
1637
+ var REST_STRENGTH = 0.6;
1638
+ var LONG_NOTE_STRENGTH = 0.4;
1639
+ var REPETITION_STRENGTH = 0.5;
1640
+ var REPETITION_THRESHOLD = 0.75;
1641
+ var HYPERMETER_STRENGTH = 0.25;
1642
+ var PIECE_END_STRENGTH = 0.5;
1643
+ var PHRASE_CUT_COST = 1.3;
1644
+ var CLOSING_SHARE = 0.65;
1645
+ var FINAL_PHRASE_BONUS = 0.15;
1646
+ var HYPERMETRIC_CLOSE_BONUS = 0.1;
1647
+ var EVADED_FACTOR = 0.6;
1648
+ var SIGNAL_ORDER = [
1649
+ "cadence",
1650
+ "rest",
1651
+ "repetition",
1652
+ "longNote",
1653
+ "pieceEnd",
1654
+ "hypermeter"
1655
+ ];
1656
+ function addSignal(boundaries, beat, signal, strength, cadence = null) {
1657
+ const key = Math.round(beat / _chunkS5BKZZGMcjs.BEAT_EPS);
1658
+ const existing = boundaries.get(key);
1659
+ const boundary = _nullishCoalesce(existing, () => ( { beat, strengths: /* @__PURE__ */ new Map(), cadence: null }));
1660
+ boundary.strengths.set(signal, Math.max(_nullishCoalesce(boundary.strengths.get(signal), () => ( 0)), strength));
1661
+ if (cadence !== null && boundary.cadence === null) {
1662
+ boundary.cadence = cadence;
1663
+ }
1664
+ boundaries.set(key, boundary);
1665
+ }
1666
+ function readBoundary(boundary) {
1667
+ const signals = [...boundary.strengths.entries()].sort(
1668
+ (a, b) => b[1] - a[1] || SIGNAL_ORDER.indexOf(a[0]) - SIGNAL_ORDER.indexOf(b[0])
1669
+ );
1670
+ return {
1671
+ strength: _chunkSGLEQVJHcjs.combineEvidence.call(void 0, signals.map(([, strength]) => strength)),
1672
+ signals: signals.map(([signal]) => signal)
1673
+ };
1674
+ }
1675
+ function lengthFit(length, expected) {
1676
+ if (!(expected > 0)) {
1677
+ return 0;
1678
+ }
1679
+ return Math.max(-1, 1 - Math.abs(length - expected) / expected);
1680
+ }
1681
+ function choosePhrasePath(beats, strengths, minPhraseBeats, expected) {
1682
+ const value = new Array(beats.length).fill(Number.NEGATIVE_INFINITY);
1683
+ const cameFrom = new Array(beats.length).fill(-1);
1684
+ value[0] = 0;
1685
+ for (let j = 1; j < beats.length; j += 1) {
1686
+ const endBeat = _nullishCoalesce(beats[j], () => ( 0));
1687
+ for (let i = 0; i < j; i += 1) {
1688
+ if (!Number.isFinite(_nullishCoalesce(value[i], () => ( Number.NEGATIVE_INFINITY)))) {
1689
+ continue;
1690
+ }
1691
+ const length = endBeat - (_nullishCoalesce(beats[i], () => ( 0)));
1692
+ if (length < minPhraseBeats - _chunkS5BKZZGMcjs.BEAT_EPS) {
1693
+ continue;
1694
+ }
1695
+ const gain = (_nullishCoalesce(value[i], () => ( 0))) + (_nullishCoalesce(strengths[j], () => ( 0))) + lengthFit(length, expected) - PHRASE_CUT_COST;
1696
+ if (gain > (_nullishCoalesce(value[j], () => ( Number.NEGATIVE_INFINITY)))) {
1697
+ value[j] = gain;
1698
+ cameFrom[j] = i;
1699
+ }
1700
+ }
1701
+ }
1702
+ const last = beats.length - 1;
1703
+ const path = [];
1704
+ if (last > 0 && Number.isFinite(_nullishCoalesce(value[last], () => ( Number.NEGATIVE_INFINITY)))) {
1705
+ for (let i = last; i > 0; i = _nullishCoalesce(cameFrom[i], () => ( 0))) {
1706
+ path.push(i);
1707
+ }
1708
+ path.reverse();
1709
+ } else if (last > 0) {
1710
+ path.push(last);
1711
+ }
1712
+ return path;
1713
+ }
1714
+ function arrivalEnd(timeline, atBeat) {
1715
+ return _nullishCoalesce(_optionalChain([_chunkSGLEQVJHcjs.segmentStartingAt.call(void 0, timeline.segments, atBeat), 'optionalAccess', _93 => _93.endBeat]), () => ( atBeat));
1716
+ }
1717
+ function barSpanOf(startBeat, endBeat, meters) {
1718
+ const firstBar = _chunkS5BKZZGMcjs.barIndexAt.call(void 0, startBeat, meters);
1719
+ const lastBar = _chunkS5BKZZGMcjs.barIndexAt.call(void 0, endBeat, meters);
1720
+ const partOf = (beat, bar) => {
1721
+ const barStart = _chunkS5BKZZGMcjs.barPositionToBeat.call(void 0, { bar, beat: 0 }, meters);
1722
+ const barLength = _chunkS5BKZZGMcjs.beatsPerBarAt.call(void 0, barStart, meters);
1723
+ return barLength > 0 ? (beat - barStart) / barLength : 0;
1724
+ };
1725
+ const bars = lastBar - firstBar + partOf(endBeat, lastBar) - partOf(startBeat, firstBar);
1726
+ return Math.round(bars * 100) / 100;
1727
+ }
1728
+ function keyContextFor(opts, notes, meters, totalBeats) {
1729
+ if (opts.key !== void 0) {
1730
+ return opts.key;
1731
+ }
1732
+ const regions = _chunkSGLEQVJHcjs.keyTimelineFromNotes.call(void 0, notes, {
1733
+ meters,
1734
+ totalBeats,
1735
+ budget: opts.budget
1736
+ });
1737
+ const keyAt = _chunkSGLEQVJHcjs.keyLookup.call(void 0, regions, _nullishCoalesce(_chunkSGLEQVJHcjs.prevailingKeyOf.call(void 0, regions), () => ( _chunk3R3QVKY5cjs.resolveKey.call(void 0, _chunk3R3QVKY5cjs.majorKey.call(void 0, 0)))));
1738
+ return (beat) => _chunk3R3QVKY5cjs.scaleOf.call(void 0, keyAt(beat));
1739
+ }
1740
+ function restBoundaries(boundaries, notes, meters, restBeats) {
1741
+ const ordered = [...notes].sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
1742
+ let covered = Number.NEGATIVE_INFINITY;
1743
+ for (const note of ordered) {
1744
+ const gap = note.startBeat - covered;
1745
+ if (Number.isFinite(covered) && gap >= restBeats - _chunkS5BKZZGMcjs.BEAT_EPS) {
1746
+ const bar = _chunkS5BKZZGMcjs.beatsPerBarAt.call(void 0, note.startBeat, meters);
1747
+ addSignal(
1748
+ boundaries,
1749
+ note.startBeat,
1750
+ "rest",
1751
+ REST_STRENGTH * _chunkSGLEQVJHcjs.clamp01.call(void 0, bar > 0 ? gap / bar : 1)
1752
+ );
1753
+ }
1754
+ covered = Math.max(covered, note.startBeat + note.durationBeat);
1755
+ }
1756
+ }
1757
+ function repetitionBoundaries(boundaries, notes, meters, spanStart, spanEnd, windowBeats, budget) {
1758
+ if (!(windowBeats > 0)) {
1759
+ return;
1760
+ }
1761
+ const slices = _chunkSGLEQVJHcjs.sliceBars.call(void 0, notes, meters, spanStart, spanEnd, budget);
1762
+ _chunkS5BKZZGMcjs.assertGenerationBudget.call(void 0, slices.length * notes.length, "form repetition comparisons", budget);
1763
+ const onsetsIn = (from, to) => notes.filter((note) => note.startBeat >= from - _chunkS5BKZZGMcjs.BEAT_EPS && note.startBeat < to - _chunkS5BKZZGMcjs.BEAT_EPS);
1764
+ let comparisons = 0;
1765
+ for (const slice of slices) {
1766
+ const beat = slice.startBeat;
1767
+ if (beat - windowBeats < spanStart - _chunkS5BKZZGMcjs.BEAT_EPS || beat + windowBeats > spanEnd + _chunkS5BKZZGMcjs.BEAT_EPS) {
1768
+ continue;
1769
+ }
1770
+ const before = onsetsIn(beat - windowBeats, beat);
1771
+ const after = onsetsIn(beat, beat + windowBeats);
1772
+ if (before.length < 2 || after.length < 2) {
1773
+ continue;
1774
+ }
1775
+ comparisons += before.length * after.length;
1776
+ _chunkS5BKZZGMcjs.assertGenerationBudget.call(void 0, comparisons, "form repetition melody comparisons", budget);
1777
+ const similarity = melodicSimilarity(before, after);
1778
+ if (similarity >= REPETITION_THRESHOLD) {
1779
+ addSignal(boundaries, beat, "repetition", REPETITION_STRENGTH * similarity);
1780
+ }
1781
+ }
1782
+ }
1783
+ function structuralWeightOf(cadence, confidence, aligned, isLast) {
1784
+ if (cadence === null || cadence.cadence.type === null) {
1785
+ return 0;
1786
+ }
1787
+ const result = cadence.cadence;
1788
+ let base = CADENCE_CLOSURE[result.type];
1789
+ if (result.type === "authentic") {
1790
+ if (result.strength === "perfect") {
1791
+ base = 1;
1792
+ } else if (result.strength === "imperfect") {
1793
+ base *= 0.8;
1794
+ }
1795
+ }
1796
+ if (result.evaded) {
1797
+ base *= EVADED_FACTOR;
1798
+ }
1799
+ let weight = base * confidence;
1800
+ if (isLast) {
1801
+ weight += FINAL_PHRASE_BONUS;
1802
+ }
1803
+ if (aligned) {
1804
+ weight += HYPERMETRIC_CLOSE_BONUS;
1805
+ }
1806
+ return _chunkSGLEQVJHcjs.clamp01.call(void 0, weight);
1807
+ }
1808
+ function describeCadence(cadence) {
1809
+ if (cadence === null || cadence.cadence.type === null) {
1810
+ return "no cadence";
1811
+ }
1812
+ const { type, strength } = cadence.cadence;
1813
+ const graded = strength === null ? type : `${strength} ${type}`;
1814
+ return `a ${graded} cadence at beat ${cadence.atBeat}`;
1815
+ }
1816
+ function phrasesFromTimeline(timeline, notes, opts = {}) {
1817
+ _chunkSGLEQVJHcjs.assertChordTimeline.call(void 0, timeline);
1818
+ const meters = _chunkS5BKZZGMcjs.resolveMeters.call(void 0, opts, "phrase meters");
1819
+ _chunkS5BKZZGMcjs.assertNoteEvents.call(void 0, notes, "phrase notes", {
1820
+ allowNonPositiveDuration: true,
1821
+ budget: opts.budget
1822
+ });
1823
+ if (opts.hypermeter !== void 0) {
1824
+ _chunkS5BKZZGMcjs.assertRange.call(void 0, opts.hypermeter.confidence, 0, 1, "phrase hypermeter confidence");
1825
+ _chunkS5BKZZGMcjs.assertPositiveInt.call(void 0, opts.hypermeter.groupBars, "phrase hypermeter groupBars");
1826
+ }
1827
+ const sounding = notes.filter((note) => note.durationBeat > 0);
1828
+ const segments = timeline.segments;
1829
+ const firstSegment = segments[0];
1830
+ const lastSegment = segments[segments.length - 1];
1831
+ const firstOnset = sounding.reduce(
1832
+ (first, n) => Math.min(first, n.startBeat),
1833
+ _nullishCoalesce(_optionalChain([firstSegment, 'optionalAccess', _94 => _94.startBeat]), () => ( 0))
1834
+ );
1835
+ const spanStart = _chunkSGLEQVJHcjs.barGridStart.call(void 0, firstOnset, meters);
1836
+ const notesEnd = sounding.reduce(
1837
+ (end, n) => Math.max(end, n.startBeat + n.durationBeat),
1838
+ _nullishCoalesce(_optionalChain([lastSegment, 'optionalAccess', _95 => _95.endBeat]), () => ( 0))
1839
+ );
1840
+ const spanEnd = Math.max(spanStart, _nullishCoalesce(opts.totalBeats, () => ( notesEnd)));
1841
+ _chunkS5BKZZGMcjs.assertRange.call(void 0, spanEnd, 0, Number.MAX_SAFE_INTEGER, "phrase totalBeats");
1842
+ if (spanEnd <= spanStart + _chunkS5BKZZGMcjs.BEAT_EPS) {
1843
+ return [];
1844
+ }
1845
+ const barBeats = _chunkS5BKZZGMcjs.beatsPerBarAt.call(void 0, Math.max(0, spanStart), meters);
1846
+ const restBeats = _nullishCoalesce(opts.restBeats, () => ( barBeats / 2));
1847
+ _chunkS5BKZZGMcjs.assertRange.call(void 0, restBeats, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "phrase restBeats");
1848
+ const longNoteBeats = _nullishCoalesce(opts.longNoteBeats, () => ( barBeats));
1849
+ _chunkS5BKZZGMcjs.assertRange.call(void 0, longNoteBeats, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "phrase longNoteBeats");
1850
+ const minPhraseBeats = _nullishCoalesce(opts.minPhraseBeats, () => ( barBeats));
1851
+ _chunkS5BKZZGMcjs.assertRange.call(void 0, minPhraseBeats, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "phrase minPhraseBeats");
1852
+ const key = keyContextFor(opts, sounding, meters, spanEnd);
1853
+ const cadences = _chunkSGLEQVJHcjs.detectCadences.call(void 0, timeline, key);
1854
+ const grouping = _nullishCoalesce(opts.hypermeter, () => ( hypermeter(sounding, meters, {
1855
+ totalBeats: spanEnd,
1856
+ cadenceBeats: cadences.map((hit) => hit.atBeat),
1857
+ budget: opts.budget
1858
+ })));
1859
+ const firstBar = _chunkS5BKZZGMcjs.barIndexAt.call(void 0, Math.max(0, spanStart), meters);
1860
+ const hyperBeats = Math.max(
1861
+ barBeats,
1862
+ _chunkS5BKZZGMcjs.barPositionToBeat.call(void 0, { bar: firstBar + grouping.groupBars, beat: 0 }, meters) - _chunkS5BKZZGMcjs.barPositionToBeat.call(void 0, { bar: firstBar, beat: 0 }, meters)
1863
+ );
1864
+ const expected = _nullishCoalesce(opts.expectedPhraseBeats, () => ( hyperBeats));
1865
+ _chunkS5BKZZGMcjs.assertRange.call(void 0, expected, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "phrase expectedPhraseBeats");
1866
+ const boundaries = /* @__PURE__ */ new Map();
1867
+ for (const hit of cadences) {
1868
+ const type = hit.cadence.type;
1869
+ if (type === null) {
1870
+ continue;
1871
+ }
1872
+ const strength = CADENCE_BOUNDARY_STRENGTH[type];
1873
+ const chordEnd = arrivalEnd(timeline, hit.atBeat);
1874
+ addSignal(boundaries, chordEnd, "cadence", strength, hit);
1875
+ const barEnd = _chunkS5BKZZGMcjs.barPositionToBeat.call(void 0, { bar: _chunkS5BKZZGMcjs.barIndexAt.call(void 0, hit.atBeat, meters) + 1, beat: 0 }, meters);
1876
+ if (chordEnd > barEnd + _chunkS5BKZZGMcjs.BEAT_EPS) {
1877
+ addSignal(boundaries, barEnd, "cadence", strength, hit);
1878
+ }
1879
+ }
1880
+ restBoundaries(boundaries, sounding, meters, restBeats);
1881
+ for (const note of sounding) {
1882
+ if (note.durationBeat >= longNoteBeats - _chunkS5BKZZGMcjs.BEAT_EPS) {
1883
+ addSignal(boundaries, note.startBeat + note.durationBeat, "longNote", LONG_NOTE_STRENGTH);
1884
+ }
1885
+ }
1886
+ repetitionBoundaries(boundaries, sounding, meters, spanStart, spanEnd, hyperBeats, opts.budget);
1887
+ for (const downbeat of grouping.downbeats) {
1888
+ addSignal(boundaries, downbeat, "hypermeter", HYPERMETER_STRENGTH * grouping.confidence);
1889
+ }
1890
+ addSignal(boundaries, spanEnd, "pieceEnd", PIECE_END_STRENGTH);
1891
+ const candidates = [...boundaries.values()].filter(
1892
+ (boundary) => boundary.beat > spanStart + _chunkS5BKZZGMcjs.BEAT_EPS && boundary.beat < spanEnd - _chunkS5BKZZGMcjs.BEAT_EPS
1893
+ ).filter((boundary) => readBoundary(boundary).strength > 0).sort((a, b) => a.beat - b.beat);
1894
+ const endBoundary = _nullishCoalesce(boundaries.get(Math.round(spanEnd / _chunkS5BKZZGMcjs.BEAT_EPS)), () => ( {
1895
+ beat: spanEnd,
1896
+ strengths: /* @__PURE__ */ new Map(),
1897
+ cadence: null
1898
+ }));
1899
+ const stops = [
1900
+ { beat: spanStart, strengths: /* @__PURE__ */ new Map(), cadence: null },
1901
+ ...candidates,
1902
+ endBoundary
1903
+ ];
1904
+ _chunkS5BKZZGMcjs.assertGenerationBudget.call(void 0, stops.length * stops.length, "form phrase boundary pairs", opts.budget);
1905
+ const readings = stops.map(readBoundary);
1906
+ const path = choosePhrasePath(
1907
+ stops.map((stop) => stop.beat),
1908
+ readings.map((reading) => reading.strength),
1909
+ minPhraseBeats,
1910
+ expected
1911
+ );
1912
+ const downbeatKeys = new Set(
1913
+ grouping.downbeats.map((downbeat) => Math.round(downbeat / _chunkS5BKZZGMcjs.BEAT_EPS))
1914
+ );
1915
+ const phrases = [];
1916
+ let from = 0;
1917
+ for (let n = 0; n < path.length; n += 1) {
1918
+ const to = _nullishCoalesce(path[n], () => ( stops.length - 1));
1919
+ const startBeat = _nullishCoalesce(_optionalChain([stops, 'access', _96 => _96[from], 'optionalAccess', _97 => _97.beat]), () => ( spanStart));
1920
+ const stop = stops[to];
1921
+ const endBeat = _nullishCoalesce(_optionalChain([stop, 'optionalAccess', _98 => _98.beat]), () => ( spanEnd));
1922
+ const reading = _nullishCoalesce(readings[to], () => ( { strength: 0, signals: [] }));
1923
+ const length = endBeat - startBeat;
1924
+ const confidence = _chunkSGLEQVJHcjs.clamp01.call(void 0,
1925
+ CLOSING_SHARE * reading.strength + (1 - CLOSING_SHARE) * Math.max(0, lengthFit(length, expected))
1926
+ );
1927
+ const isLast = n === path.length - 1;
1928
+ const closing = _nullishCoalesce(_optionalChain([stop, 'optionalAccess', _99 => _99.cadence]), () => ( null));
1929
+ const cadence = closing !== null && closing.atBeat >= startBeat - _chunkS5BKZZGMcjs.BEAT_EPS && closing.atBeat < endBeat - _chunkS5BKZZGMcjs.BEAT_EPS ? closing : null;
1930
+ const structuralWeight = structuralWeightOf(
1931
+ cadence,
1932
+ confidence,
1933
+ downbeatKeys.has(Math.round(endBeat / _chunkS5BKZZGMcjs.BEAT_EPS)),
1934
+ isLast
1935
+ );
1936
+ const signals = cadence === null ? reading.signals.filter((signal) => signal !== "cadence") : reading.signals;
1937
+ const bars = barSpanOf(startBeat, endBeat, meters);
1938
+ phrases.push({
1939
+ startBeat,
1940
+ endBeat,
1941
+ cadence,
1942
+ signals,
1943
+ confidence,
1944
+ structuralWeight,
1945
+ rationale: `Phrase of ${bars} bar(s) closing on ${describeCadence(cadence)}; boundary from ${signals.length === 0 ? "the end of the span" : signals.join(", ")}`
1946
+ });
1947
+ from = to;
1948
+ }
1949
+ return phrases;
1950
+ }
1951
+ function structuralCadences(phrases) {
1952
+ _chunkS5BKZZGMcjs.assertArray.call(void 0, phrases, "phrases");
1953
+ const ranked = [];
1954
+ for (let index = 0; index < phrases.length; index += 1) {
1955
+ const phrase = _chunkS5BKZZGMcjs.assertRecord.call(void 0, phrases[index], `phrases[${index}]`);
1956
+ if (phrase.cadence === null || phrase.cadence === void 0 || phrase.cadence.cadence.type === null) {
1957
+ continue;
1958
+ }
1959
+ const hit = phrase.cadence;
1960
+ ranked.push({
1961
+ phraseIndex: index,
1962
+ atBeat: hit.atBeat,
1963
+ endBeat: phrase.endBeat,
1964
+ cadence: hit.cadence,
1965
+ weight: phrase.structuralWeight,
1966
+ rationale: `Closes ${describeCadence(hit)} on a phrase of confidence ${Math.round(phrase.confidence * 100) / 100}`
1967
+ });
1968
+ }
1969
+ return ranked.sort((a, b) => b.weight - a.weight || a.atBeat - b.atBeat);
1970
+ }
1971
+
1972
+ // src/analyze/form/section.ts
1973
+ var MELODY_SHARE = 0.6;
1974
+ var HARMONY_SHARE = 0.4;
1975
+ var DEFAULT_THRESHOLD = 0.8;
1976
+ function labelAt(index) {
1977
+ let n = index;
1978
+ let label = "";
1979
+ do {
1980
+ label = String.fromCharCode(65 + n % 26) + label;
1981
+ n = Math.floor(n / 26) - 1;
1982
+ } while (n >= 0);
1983
+ return label;
1984
+ }
1985
+ function isSilent(unit) {
1986
+ return unit.notes.length === 0;
1987
+ }
1988
+ function unitSimilarity(a, b) {
1989
+ const harmony = _chunkSGLEQVJHcjs.weightSimilarity.call(void 0, a.weights, b.weights);
1990
+ if (a.notes.length < 2 || b.notes.length < 2) {
1991
+ return a.notes.length === b.notes.length ? harmony : 0;
1992
+ }
1993
+ const melody = melodicSimilarity(a.notes, b.notes);
1994
+ return _chunkSGLEQVJHcjs.clamp01.call(void 0, MELODY_SHARE * melody + HARMONY_SHARE * harmony);
1995
+ }
1996
+ function sectionsFromNotes(notes, opts = {}) {
1997
+ const meters = _chunkS5BKZZGMcjs.resolveMeters.call(void 0, opts, "section meters");
1998
+ _chunkS5BKZZGMcjs.assertNoteEvents.call(void 0, notes, "section notes", {
1999
+ allowNonPositiveDuration: true,
2000
+ budget: opts.budget
2001
+ });
2002
+ const sounding = notes.filter((note) => note.durationBeat > 0);
2003
+ if (sounding.length === 0) {
2004
+ return [];
2005
+ }
2006
+ const firstOnset = _chunkSGLEQVJHcjs.barGridStart.call(void 0, _chunkSGLEQVJHcjs.firstSoundingBeat.call(void 0, sounding), meters);
2007
+ const lastEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
2008
+ const spanEnd = Math.max(firstOnset, _nullishCoalesce(opts.totalBeats, () => ( lastEnd)));
2009
+ _chunkS5BKZZGMcjs.assertRange.call(void 0, spanEnd, 0, Number.MAX_SAFE_INTEGER, "section totalBeats");
2010
+ const threshold = _nullishCoalesce(opts.threshold, () => ( DEFAULT_THRESHOLD));
2011
+ _chunkS5BKZZGMcjs.assertRange.call(void 0, threshold, 0, 1, "section threshold");
2012
+ const unitBars = _nullishCoalesce(opts.unitBars, () => ( hypermeter(sounding, meters, { totalBeats: spanEnd, budget: opts.budget }).groupBars));
2013
+ _chunkS5BKZZGMcjs.assertPositiveInt.call(void 0, unitBars, "section unitBars");
2014
+ const firstBar = _chunkS5BKZZGMcjs.barIndexAt.call(void 0, firstOnset, meters);
2015
+ const lastBar = _chunkSGLEQVJHcjs.lastBarOf.call(void 0, meters, firstOnset, spanEnd);
2016
+ const startBar = Math.max(0, firstBar);
2017
+ const unitCount = Math.max(1, Math.ceil((lastBar - startBar + 1) / unitBars));
2018
+ _chunkS5BKZZGMcjs.assertGenerationBudget.call(void 0, unitCount * unitCount, "form section comparisons", opts.budget);
2019
+ _chunkS5BKZZGMcjs.assertGenerationBudget.call(void 0, unitCount * sounding.length, "form section unit notes", opts.budget);
2020
+ const units = [];
2021
+ for (let index = 0; index < unitCount; index += 1) {
2022
+ const barStart = startBar + index * unitBars;
2023
+ const startBeat = index === 0 ? firstOnset : _chunkS5BKZZGMcjs.barPositionToBeat.call(void 0, { bar: barStart, beat: 0 }, meters);
2024
+ const endBeat = index === unitCount - 1 ? spanEnd : _chunkS5BKZZGMcjs.barPositionToBeat.call(void 0, { bar: barStart + unitBars, beat: 0 }, meters);
2025
+ const unitNotes = sounding.filter(
2026
+ (note) => note.startBeat >= startBeat - _chunkS5BKZZGMcjs.BEAT_EPS && note.startBeat < endBeat - _chunkS5BKZZGMcjs.BEAT_EPS
2027
+ ).sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
2028
+ units.push({
2029
+ startBeat,
2030
+ endBeat,
2031
+ notes: unitNotes,
2032
+ weights: _chunkSGLEQVJHcjs.windowWeights.call(void 0, unitNotes, startBeat, endBeat, meters).weights
2033
+ });
2034
+ }
2035
+ let comparisons = 0;
2036
+ let earlierNotes = 0;
2037
+ for (const unit of units) {
2038
+ comparisons += unit.notes.length * earlierNotes;
2039
+ earlierNotes += unit.notes.length;
2040
+ }
2041
+ _chunkS5BKZZGMcjs.assertGenerationBudget.call(void 0, comparisons, "form section melody comparisons", opts.budget);
2042
+ const labels = [];
2043
+ const scores = [];
2044
+ let distinct = 0;
2045
+ for (let index = 0; index < units.length; index += 1) {
2046
+ const unit = units[index];
2047
+ if (unit === void 0) {
2048
+ continue;
2049
+ }
2050
+ if (isSilent(unit)) {
2051
+ labels.push(null);
2052
+ scores.push(_nullishCoalesce(scores[index - 1], () => ( 1)));
2053
+ continue;
2054
+ }
2055
+ let bestScore = -1;
2056
+ let bestIndex = -1;
2057
+ for (let earlier = 0; earlier < index; earlier += 1) {
2058
+ const other = units[earlier];
2059
+ if (other === void 0 || isSilent(other)) {
2060
+ continue;
2061
+ }
2062
+ const score = unitSimilarity(unit, other);
2063
+ if (score > bestScore) {
2064
+ bestScore = score;
2065
+ bestIndex = earlier;
2066
+ }
2067
+ }
2068
+ if (bestIndex >= 0 && bestScore >= threshold) {
2069
+ labels.push(_nullishCoalesce(labels[bestIndex], () => ( labelAt(distinct))));
2070
+ scores.push(bestScore);
2071
+ } else {
2072
+ labels.push(labelAt(distinct));
2073
+ scores.push(1);
2074
+ distinct += 1;
2075
+ }
2076
+ }
2077
+ const sections = [];
2078
+ const firstByLabel = /* @__PURE__ */ new Map();
2079
+ for (let index = 0; index < units.length; ) {
2080
+ const label = _nullishCoalesce(labels[index], () => ( null));
2081
+ if (label === null) {
2082
+ index += 1;
2083
+ continue;
2084
+ }
2085
+ let end = index;
2086
+ let scoreSum = 0;
2087
+ let sounded = 0;
2088
+ while (end < units.length && labels[end] === label) {
2089
+ scoreSum += _nullishCoalesce(scores[end], () => ( 1));
2090
+ sounded += 1;
2091
+ end += 1;
2092
+ }
2093
+ while (end < units.length && labels[end] === null) {
2094
+ end += 1;
2095
+ }
2096
+ const startBeat = _nullishCoalesce(_optionalChain([units, 'access', _100 => _100[index], 'optionalAccess', _101 => _101.startBeat]), () => ( 0));
2097
+ const endBeat = _nullishCoalesce(_optionalChain([units, 'access', _102 => _102[end - 1], 'optionalAccess', _103 => _103.endBeat]), () => ( spanEnd));
2098
+ const sectionFirstBar = startBar + index * unitBars;
2099
+ const sectionLastBar = Math.max(
2100
+ sectionFirstBar,
2101
+ Math.min(lastBar, startBar + end * unitBars - 1)
2102
+ );
2103
+ const sectionIndex = sections.length;
2104
+ const first = firstByLabel.get(label);
2105
+ if (first === void 0) {
2106
+ firstByLabel.set(label, sectionIndex);
2107
+ }
2108
+ const similarity = first === void 0 ? 1 : _chunkSGLEQVJHcjs.clamp01.call(void 0, scoreSum / Math.max(1, sounded));
2109
+ sections.push({
2110
+ label,
2111
+ startBeat,
2112
+ endBeat,
2113
+ bars: sectionLastBar - sectionFirstBar + 1,
2114
+ firstOccurrence: _nullishCoalesce(first, () => ( sectionIndex)),
2115
+ similarity,
2116
+ rationale: first === void 0 ? `Section ${label}: material first heard at beat ${startBeat}` : `Section ${label}: ${Math.round(similarity * 100)}% alike the ${label} at beat ${_nullishCoalesce(_optionalChain([sections, 'access', _104 => _104[first], 'optionalAccess', _105 => _105.startBeat]), () => ( 0))}`
2117
+ });
2118
+ index = end;
2119
+ }
2120
+ return sections;
2121
+ }
2122
+
2123
+ // src/analyze/reduction/index.ts
2124
+ var TONIC_ARRIVING = /* @__PURE__ */ new Set(["authentic", "plagal", "modal", "deceptive"]);
2125
+ function signedStep(fromPc, toPc) {
2126
+ return (_chunkS5BKZZGMcjs.pitchClassOf.call(void 0, toPc) - _chunkS5BKZZGMcjs.pitchClassOf.call(void 0, fromPc) + 18) % 12 - 6;
2127
+ }
2128
+ function isStep(distance) {
2129
+ const size = Math.abs(distance);
2130
+ return size === 1 || size === 2;
2131
+ }
2132
+ function touches(before, after) {
2133
+ return Math.abs(after.startBeat - before.endBeat) <= _chunkS5BKZZGMcjs.BEAT_EPS;
2134
+ }
2135
+ function salienceFloors(durations) {
2136
+ const order = durations.map((_, index) => index).sort((a, b) => {
2137
+ const left = _nullishCoalesce(durations[a], () => ( 0));
2138
+ const right = _nullishCoalesce(durations[b], () => ( 0));
2139
+ return left - right || a - b;
2140
+ });
2141
+ const rankOf = new Array(durations.length).fill(0);
2142
+ for (let rank = 0; rank < order.length; rank += 1) {
2143
+ rankOf[_nullishCoalesce(order[rank], () => ( 0))] = rank;
2144
+ }
2145
+ const floors = [];
2146
+ const remaining = durations.length - 1;
2147
+ for (let index = 0; index < durations.length; index += 1) {
2148
+ if (remaining <= 0) {
2149
+ floors.push(0);
2150
+ continue;
2151
+ }
2152
+ const skipped = _nullishCoalesce(rankOf[index], () => ( 0));
2153
+ const at = (position) => _nullishCoalesce(durations[_nullishCoalesce(order[position < skipped ? position : position + 1], () => ( 0))], () => ( 0));
2154
+ const middle = remaining >> 1;
2155
+ floors.push(remaining % 2 === 1 ? at(middle) : (at(middle - 1) + at(middle)) / 2);
2156
+ }
2157
+ return floors;
2158
+ }
2159
+ function isTonicChord(chord, key) {
2160
+ return _chunkS5BKZZGMcjs.pitchClassOf.call(void 0, chord.rootPc - key.rootPc) === 0 && !_chunkSGLEQVJHcjs.isAppliedDominant.call(void 0, chord, key);
2161
+ }
2162
+ function functionalReason(segment, before, after, keyAt) {
2163
+ const key = keyAt(segment.startBeat);
2164
+ if (after !== void 0 && _chunkSGLEQVJHcjs.isCadentialSixFour.call(void 0, segment.chord, after.chord, key)) {
2165
+ return { kind: "cadentialSixFour" };
2166
+ }
2167
+ if (isTonicChord(segment.chord, key)) {
2168
+ return { kind: "tonic" };
2169
+ }
2170
+ if (_chunk3R3QVKY5cjs.isDominantChordOf.call(void 0, segment.chord, key)) {
2171
+ return { kind: "dominant" };
2172
+ }
2173
+ if (before !== void 0) {
2174
+ const arrival = _chunkSGLEQVJHcjs.detectCadence.call(void 0, before.chord, segment.chord, key).type;
2175
+ if (arrival !== null) {
2176
+ return { kind: "cadenceArrival", cadence: arrival };
2177
+ }
2178
+ }
2179
+ if (after !== void 0) {
2180
+ const agent = _chunkSGLEQVJHcjs.detectCadence.call(void 0, segment.chord, after.chord, keyAt(after.startBeat)).type;
2181
+ if (agent !== null && TONIC_ARRIVING.has(agent)) {
2182
+ return { kind: "cadenceAgent", cadence: agent };
2183
+ }
2184
+ }
2185
+ return null;
2186
+ }
2187
+ function figureReason(segment, before, after) {
2188
+ if (before === void 0 || after === void 0) {
2189
+ return null;
2190
+ }
2191
+ const incoming = signedStep(before.chord.rootPc, segment.chord.rootPc);
2192
+ const outgoing = signedStep(segment.chord.rootPc, after.chord.rootPc);
2193
+ if (!isStep(incoming) || !isStep(outgoing)) {
2194
+ return null;
2195
+ }
2196
+ if (incoming > 0 === outgoing > 0) {
2197
+ return { kind: "passing" };
2198
+ }
2199
+ return _chunkS5BKZZGMcjs.pitchClassOf.call(void 0, before.chord.rootPc) === _chunkS5BKZZGMcjs.pitchClassOf.call(void 0, after.chord.rootPc) ? { kind: "neighbor" } : null;
2200
+ }
2201
+ function describe(reason) {
2202
+ switch (reason.kind) {
2203
+ case "tonic":
2204
+ return "Structural: the tonic of the key, which the progression is heard against";
2205
+ case "dominant":
2206
+ return "Structural: the dominant of the key, the chord its tonic is approached from";
2207
+ case "cadentialSixFour":
2208
+ return "Structural: a tonic six-four on the bass of the dominant that follows, which prolongs that dominant rather than framing the tonic";
2209
+ case "cadenceArrival":
2210
+ return `Structural: the arrival of a ${reason.cadence} cadence`;
2211
+ case "cadenceAgent":
2212
+ return `Structural: the chord that drives a ${reason.cadence} cadence`;
2213
+ case "salient":
2214
+ return "Structural: it holds the harmony longer than the chords around it";
2215
+ case "first":
2216
+ return "Structural: the first chord of the progression frames it";
2217
+ case "last":
2218
+ return "Structural: the last chord of the progression frames it";
2219
+ case "unfigured":
2220
+ return "Structural: its root neither steps through to the next chord nor returns to the last, so it embellishes nothing";
2221
+ case "passing":
2222
+ return "Passing: its root steps in from the previous chord and on to the next in the same direction";
2223
+ case "neighbor":
2224
+ return "Neighbor: its root steps away from the surrounding harmony and returns to it";
2225
+ }
2226
+ }
2227
+ var BASIS_READINGS = {
2228
+ function: ({ segment, before, after, keyAt }) => functionalReason(segment, before, after, keyAt),
2229
+ duration: ({ segment, salienceFloor }) => segment.endBeat - segment.startBeat > salienceFloor ? { kind: "salient" } : null
2230
+ };
2231
+ var REDUCTION_BASES = Object.keys(BASIS_READINGS);
2232
+ function levelOf(reason) {
2233
+ if (reason.kind === "passing") {
2234
+ return "passing";
2235
+ }
2236
+ return reason.kind === "neighbor" ? "neighbor" : "structural";
2237
+ }
2238
+ function reduceProgression(timeline, key, opts = {}) {
2239
+ _chunkSGLEQVJHcjs.assertChordTimeline.call(void 0, timeline);
2240
+ const asked = _chunkS5BKZZGMcjs.assertOptions.call(void 0, opts, "opts");
2241
+ const keyAt = _chunkSGLEQVJHcjs.keyScaleAt.call(void 0, key);
2242
+ const basis = _chunkS5BKZZGMcjs.assertOneOf.call(void 0, _nullishCoalesce(asked.basis, () => ( "function")), REDUCTION_BASES, "reduction basis");
2243
+ const reading = BASIS_READINGS[basis];
2244
+ const segments = timeline.segments;
2245
+ const floors = basis === "duration" ? salienceFloors(segments.map((segment) => segment.endBeat - segment.startBeat)) : [];
2246
+ const result = [];
2247
+ for (let index = 0; index < segments.length; index += 1) {
2248
+ const segment = segments[index];
2249
+ if (segment === void 0) {
2250
+ continue;
2251
+ }
2252
+ const previous = segments[index - 1];
2253
+ const following = segments[index + 1];
2254
+ const before = previous !== void 0 && touches(previous, segment) ? previous : void 0;
2255
+ const after = following !== void 0 && touches(segment, following) ? following : void 0;
2256
+ let reason = reading({ segment, before, after, keyAt, salienceFloor: _nullishCoalesce(floors[index], () => ( 0)) });
2257
+ if (reason === null && index === 0) {
2258
+ reason = { kind: "first" };
2259
+ }
2260
+ if (reason === null && index === segments.length - 1) {
2261
+ reason = { kind: "last" };
2262
+ }
2263
+ reason ??= _nullishCoalesce(figureReason(segment, before, after), () => ( { kind: "unfigured" }));
2264
+ result.push({
2265
+ chord: segment.chord,
2266
+ startBeat: segment.startBeat,
2267
+ endBeat: segment.endBeat,
2268
+ level: levelOf(reason),
2269
+ rationale: describe(reason)
2270
+ });
2271
+ }
2272
+ return result;
2273
+ }
2274
+
2275
+
2276
+
2277
+
2278
+
2279
+
2280
+
2281
+
2282
+
2283
+
2284
+
2285
+
2286
+
2287
+
2288
+
2289
+
2290
+
2291
+
2292
+
2293
+ exports.trackRoleOf = trackRoleOf; exports.analyzePolyphony = analyzePolyphony; exports.analyzeArrangement = analyzeArrangement; exports.createArrangementSession = createArrangementSession; exports.tensionCurve = tensionCurve; exports.tensionCurveFrom = tensionCurveFrom; exports.hypermeter = hypermeter; exports.melodicContour = melodicContour; exports.extractMotifs = extractMotifs; exports.motifFromNotes = motifFromNotes; exports.relateMotifs = relateMotifs; exports.compareMelodies = compareMelodies; exports.melodicSimilarity = melodicSimilarity; exports.phrasesFromTimeline = phrasesFromTimeline; exports.structuralCadences = structuralCadences; exports.sectionsFromNotes = sectionsFromNotes; exports.reduceProgression = reduceProgression;
2294
+ //# sourceMappingURL=chunk-HPVSH7SF.cjs.map