@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
@@ -1,3224 +0,0 @@
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
- var _chunkOE6WK2Y2cjs = require('./chunk-OE6WK2Y2.cjs');
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
- var _chunkK4ZJUGF4cjs = require('./chunk-K4ZJUGF4.cjs');
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
-
38
- var _chunkUADYAYATcjs = require('./chunk-UADYAYAT.cjs');
39
-
40
-
41
-
42
-
43
-
44
-
45
-
46
-
47
-
48
-
49
-
50
-
51
- var _chunkQ2WGKJNNcjs = require('./chunk-Q2WGKJNN.cjs');
52
-
53
- // src/analyze/grid.ts
54
- var EPS = 1e-9;
55
- var PICKUP_JITTER = 0.05;
56
- function gridOriginOf(firstOnset, slotBeats) {
57
- if (firstOnset >= -PICKUP_JITTER) {
58
- return { origin: 0, startBeat: 0 };
59
- }
60
- return { origin: Math.floor(firstOnset / slotBeats + EPS) * slotBeats, startBeat: firstOnset };
61
- }
62
-
63
- // src/analyze/histogram.ts
64
- var EPS2 = 1e-9;
65
- function bucketNotesBySlot(notes, grid, slotCount, opts) {
66
- const buckets = Array.from({ length: slotCount }, () => []);
67
- let memberships = 0;
68
- for (const note of notes) {
69
- const first = Math.max(0, Math.floor((note.startBeat - grid.origin) / grid.slotBeats));
70
- const lastExclusive = Math.min(
71
- slotCount,
72
- Math.ceil((note.startBeat + note.durationBeat - grid.origin) / grid.slotBeats)
73
- );
74
- memberships += Math.max(0, lastExclusive - first);
75
- _chunkQ2WGKJNNcjs.assertGenerationBudget.call(void 0, memberships, opts.name, opts.budget);
76
- for (let slot = first; slot < lastExclusive; slot += 1) {
77
- _optionalChain([buckets, 'access', _2 => _2[slot], 'optionalAccess', _3 => _3.push, 'call', _4 => _4(note)]);
78
- }
79
- }
80
- return buckets;
81
- }
82
- function windowWeights(notes, windowStart, windowEnd, meter) {
83
- const weights = new Array(12).fill(0);
84
- let lowestPitch = Number.POSITIVE_INFINITY;
85
- for (const note of notes) {
86
- const noteEnd = note.startBeat + note.durationBeat;
87
- const overlap = Math.min(noteEnd, windowEnd) - Math.max(note.startBeat, windowStart);
88
- if (overlap <= EPS2) {
89
- continue;
90
- }
91
- const velocityFactor = note.velocity !== void 0 ? note.velocity / 127 : 1;
92
- const onsetInWindow = note.startBeat >= windowStart - EPS2 && note.startBeat < windowEnd - EPS2;
93
- const accent = onsetInWindow ? 1 + _chunkK4ZJUGF4cjs.metricWeight.call(void 0, note.startBeat, meter) / 3 : 1;
94
- const pc = _chunkQ2WGKJNNcjs.pitchClassOf.call(void 0, note.pitch);
95
- weights[pc] = (_nullishCoalesce(weights[pc], () => ( 0))) + overlap * velocityFactor * accent;
96
- if (note.pitch < lowestPitch) {
97
- lowestPitch = note.pitch;
98
- }
99
- }
100
- let totalWeight = 0;
101
- let maxWeight = 0;
102
- for (const w of weights) {
103
- totalWeight += w;
104
- maxWeight = Math.max(maxWeight, w);
105
- }
106
- return { weights, totalWeight, maxWeight, lowestPitch };
107
- }
108
-
109
- // src/analyze/keys/index.ts
110
- var EPS3 = 1e-9;
111
- var PITCH_CLASSES = 12;
112
- var EMPTY_WEIGHTS = {
113
- weights: new Array(PITCH_CLASSES).fill(0),
114
- totalWeight: 0,
115
- maxWeight: 0,
116
- lowestPitch: Number.POSITIVE_INFINITY
117
- };
118
- var MAX_METRIC_WEIGHT = 3;
119
- var MODULATION_COST = 0.15;
120
- var PAST_CONTEXT_SHARE = 0.5;
121
- var FUTURE_CONTEXT_SHARE = 0.25;
122
- var STRONG_BEAT_DISCOUNT = 0.4;
123
- var FIFTHS_DISTANCE_COST = 0.18;
124
- var KEY_CANDIDATES = (() => {
125
- const candidates = [];
126
- for (let rootPc = 0; rootPc < 12; rootPc += 1) {
127
- candidates.push({ key: _chunkUADYAYATcjs.majorKey.call(void 0, rootPc), rootPc, minor: false });
128
- candidates.push({ key: _chunkUADYAYATcjs.minorKey.call(void 0, rootPc), rootPc, minor: true });
129
- }
130
- return candidates;
131
- })();
132
- function fifthsOf(rootPc, minor) {
133
- const majorRoot = _chunkQ2WGKJNNcjs.pitchClassOf.call(void 0, minor ? rootPc + 3 : rootPc);
134
- const raw = majorRoot * 7 % 12;
135
- return raw > 6 ? raw - 12 : raw;
136
- }
137
- function fifthsDistance(a, b) {
138
- const diff = Math.abs(a - b) % 12;
139
- return Math.min(diff, 12 - diff);
140
- }
141
- var CANDIDATE_DISTANCE = KEY_CANDIDATES.map(
142
- (from) => KEY_CANDIDATES.map(
143
- (to) => fifthsDistance(fifthsOf(from.rootPc, from.minor), fifthsOf(to.rootPc, to.minor))
144
- )
145
- );
146
- function chooseKeys(slots, changeCost, meter) {
147
- const count = KEY_CANDIDATES.length;
148
- if (slots.length === 0) {
149
- return [];
150
- }
151
- let costs = (_nullishCoalesce(_optionalChain([slots, 'access', _5 => _5[0], 'optionalAccess', _6 => _6.scores]), () => ( []))).map((score) => -score);
152
- const cameFrom = [];
153
- for (let i = 1; i < slots.length; i += 1) {
154
- const slot = slots[i];
155
- const previous = costs;
156
- const accent = _chunkK4ZJUGF4cjs.metricWeight.call(void 0, _nullishCoalesce(_optionalChain([slot, 'optionalAccess', _7 => _7.startBeat]), () => ( 0)), meter);
157
- const metricFactor = 1 - STRONG_BEAT_DISCOUNT * accent / MAX_METRIC_WEIGHT;
158
- const next = new Array(count).fill(0);
159
- const back = new Int32Array(count);
160
- for (let to = 0; to < count; to += 1) {
161
- let best2 = _nullishCoalesce(previous[to], () => ( Number.POSITIVE_INFINITY));
162
- let bestFrom = to;
163
- for (let from = 0; from < count; from += 1) {
164
- if (from === to) {
165
- continue;
166
- }
167
- const distance = _nullishCoalesce(_optionalChain([CANDIDATE_DISTANCE, 'access', _8 => _8[from], 'optionalAccess', _9 => _9[to]]), () => ( 0));
168
- const penalty = changeCost * metricFactor * (1 + FIFTHS_DISTANCE_COST * distance);
169
- const cost = (_nullishCoalesce(previous[from], () => ( Number.POSITIVE_INFINITY))) + penalty;
170
- if (cost < best2) {
171
- best2 = cost;
172
- bestFrom = from;
173
- }
174
- }
175
- back[to] = bestFrom;
176
- next[to] = best2 - (_nullishCoalesce(_optionalChain([slot, 'optionalAccess', _10 => _10.scores, 'access', _11 => _11[to]]), () => ( 0)));
177
- }
178
- costs = next;
179
- cameFrom.push(back);
180
- }
181
- let best = 0;
182
- for (let c = 1; c < count; c += 1) {
183
- if ((_nullishCoalesce(costs[c], () => ( Number.POSITIVE_INFINITY))) < (_nullishCoalesce(costs[best], () => ( Number.POSITIVE_INFINITY)))) {
184
- best = c;
185
- }
186
- }
187
- const path = new Array(slots.length);
188
- let current = best;
189
- for (let i = slots.length - 1; i >= 0; i -= 1) {
190
- path[i] = current;
191
- if (i > 0) {
192
- current = _nullishCoalesce(_optionalChain([cameFrom, 'access', _12 => _12[i - 1], 'optionalAccess', _13 => _13[current]]), () => ( current));
193
- }
194
- }
195
- return path;
196
- }
197
- function regionsFromPath(slots, path) {
198
- const regions = [];
199
- for (let i = 0; i < slots.length; i += 1) {
200
- const last = regions[regions.length - 1];
201
- const slot = slots[i];
202
- if (slot === void 0) {
203
- continue;
204
- }
205
- if (last !== void 0 && last.candidate === path[i]) {
206
- last.endBeat = slot.endBeat;
207
- last.toExclusive = i + 1;
208
- continue;
209
- }
210
- regions.push({
211
- startBeat: slot.startBeat,
212
- endBeat: slot.endBeat,
213
- candidate: _nullishCoalesce(path[i], () => ( 0)),
214
- from: i,
215
- toExclusive: i + 1
216
- });
217
- }
218
- return regions;
219
- }
220
- function enforceMinimumRegion(slots, path, minBeats) {
221
- const chosen = [...path];
222
- for (; ; ) {
223
- const regions = regionsFromPath(slots, chosen);
224
- let shortest;
225
- let before;
226
- let after;
227
- for (let i = 0; i < regions.length; i += 1) {
228
- const region = regions[i];
229
- if (region === void 0 || region.endBeat - region.startBeat >= minBeats - EPS3) {
230
- continue;
231
- }
232
- const previous = regions[i - 1];
233
- const next = regions[i + 1];
234
- const left2 = previous !== void 0 && Math.abs(previous.endBeat - region.startBeat) < EPS3 ? previous : void 0;
235
- const right2 = next !== void 0 && Math.abs(next.startBeat - region.endBeat) < EPS3 ? next : void 0;
236
- if (left2 === void 0 && right2 === void 0) {
237
- continue;
238
- }
239
- if (shortest !== void 0 && region.endBeat - region.startBeat >= shortest.endBeat - shortest.startBeat) {
240
- continue;
241
- }
242
- shortest = region;
243
- before = left2;
244
- after = right2;
245
- }
246
- if (shortest === void 0) {
247
- return chosen;
248
- }
249
- const merging = shortest;
250
- const scoreOver = (candidate) => {
251
- let total = 0;
252
- for (let i = merging.from; i < merging.toExclusive; i += 1) {
253
- total += _nullishCoalesce(_optionalChain([slots, 'access', _14 => _14[i], 'optionalAccess', _15 => _15.scores, 'access', _16 => _16[candidate]]), () => ( 0));
254
- }
255
- return total;
256
- };
257
- const left = _optionalChain([before, 'optionalAccess', _17 => _17.candidate]);
258
- const right = _optionalChain([after, 'optionalAccess', _18 => _18.candidate]);
259
- const winner = left === void 0 ? _nullishCoalesce(right, () => ( merging.candidate)) : right === void 0 || scoreOver(left) >= scoreOver(right) ? left : right;
260
- for (let i = merging.from; i < merging.toExclusive; i += 1) {
261
- chosen[i] = winner;
262
- }
263
- }
264
- }
265
- function withRelations(regions) {
266
- for (let i = 1; i < regions.length; i += 1) {
267
- const previous = regions[i - 1];
268
- const current = regions[i];
269
- if (previous === void 0 || current === void 0) {
270
- continue;
271
- }
272
- const relation = _chunkUADYAYATcjs.keyRelationBetween.call(void 0, _chunkUADYAYATcjs.spelledKeyOf.call(void 0, previous.key), _chunkUADYAYATcjs.spelledKeyOf.call(void 0, current.key));
273
- if (relation !== null && relation !== "same") {
274
- current.modulation = relation;
275
- }
276
- }
277
- return regions;
278
- }
279
- function keyTimelineFromNotes(notes, opts = {}) {
280
- const meters = _chunkK4ZJUGF4cjs.resolveMeters.call(void 0, opts, "key timeline meters");
281
- const bar = _chunkK4ZJUGF4cjs.beatsPerBarAt.call(void 0, 0, meters);
282
- const expectedKeyBeats = _nullishCoalesce(opts.expectedKeyBeats, () => ( bar * 4));
283
- _chunkQ2WGKJNNcjs.assertRange.call(void 0, expectedKeyBeats, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "expectedKeyBeats");
284
- const minKeyBeats = _nullishCoalesce(opts.minKeyBeats, () => ( bar));
285
- _chunkQ2WGKJNNcjs.assertRange.call(void 0, minKeyBeats, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "minKeyBeats");
286
- const budget = opts.budget;
287
- _chunkQ2WGKJNNcjs.assertNoteEvents.call(void 0, notes, "key timeline notes", { allowNonPositiveDuration: true, budget });
288
- const sounding = notes.filter((note) => note.durationBeat > 0);
289
- const lastEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
290
- const totalBeats = _nullishCoalesce(opts.totalBeats, () => ( lastEnd));
291
- _chunkQ2WGKJNNcjs.assertRange.call(void 0, totalBeats, 0, Number.MAX_SAFE_INTEGER, "key timeline totalBeats");
292
- const slotBeats = Math.min(minKeyBeats, expectedKeyBeats);
293
- const firstOnset = sounding.reduce((first, n) => Math.min(first, n.startBeat), 0);
294
- const { origin, startBeat: musicStart } = gridOriginOf(firstOnset, slotBeats);
295
- const slotCount = Math.max(0, Math.ceil((totalBeats - origin) / slotBeats - EPS3));
296
- _chunkQ2WGKJNNcjs.assertGenerationBudget.call(void 0, slotCount, "key timeline slots", budget);
297
- if (slotCount === 0 || sounding.length === 0) {
298
- return [];
299
- }
300
- const profile = _chunkOE6WK2Y2cjs.resolveKeyProfile.call(void 0, opts.profile);
301
- const slotNotes = bucketNotesBySlot(sounding, { origin, slotBeats }, slotCount, {
302
- name: "key timeline note-to-slot memberships",
303
- budget
304
- });
305
- const slotWeights = [];
306
- for (let i = 0; i < slotCount; i += 1) {
307
- const startBeat = origin + i * slotBeats;
308
- slotWeights.push(
309
- windowWeights(
310
- _nullishCoalesce(slotNotes[i], () => ( [])),
311
- startBeat,
312
- Math.min(startBeat + slotBeats, totalBeats),
313
- meters
314
- )
315
- );
316
- }
317
- const slots = [];
318
- let weightSum = 0;
319
- let soundingSlots = 0;
320
- for (let i = 0; i < slotCount; i += 1) {
321
- const startBeat = origin + i * slotBeats;
322
- const endBeat = Math.min(startBeat + slotBeats, totalBeats);
323
- const own = _nullishCoalesce(slotWeights[i], () => ( EMPTY_WEIGHTS));
324
- const before = _nullishCoalesce(slotWeights[i - 1], () => ( EMPTY_WEIGHTS));
325
- const after = _nullishCoalesce(slotWeights[i + 1], () => ( EMPTY_WEIGHTS));
326
- const weights = own.weights.map(
327
- (weight, pc) => weight + PAST_CONTEXT_SHARE * (_nullishCoalesce(before.weights[pc], () => ( 0))) + FUTURE_CONTEXT_SHARE * (_nullishCoalesce(after.weights[pc], () => ( 0)))
328
- );
329
- const totalWeight = own.totalWeight;
330
- const scores = KEY_CANDIDATES.map(
331
- (candidate) => _chunkOE6WK2Y2cjs.profileScore.call(void 0, weights, candidate.minor ? profile.minor : profile.major, candidate.rootPc) * totalWeight
332
- );
333
- slots.push({ startBeat, endBeat, scores, weight: totalWeight });
334
- if (totalWeight > EPS3) {
335
- weightSum += totalWeight;
336
- soundingSlots += 1;
337
- }
338
- }
339
- if (soundingSlots === 0) {
340
- return [];
341
- }
342
- const meanSlotWeight = weightSum / soundingSlots;
343
- const changeCost = MODULATION_COST * meanSlotWeight * (expectedKeyBeats / slotBeats);
344
- const path = chooseKeys(slots, changeCost, meters);
345
- const grouped = regionsFromPath(slots, path);
346
- const regions = grouped.map((region) => {
347
- const candidate = KEY_CANDIDATES[region.candidate];
348
- const key = _nullishCoalesce(_optionalChain([candidate, 'optionalAccess', _19 => _19.key]), () => ( _chunkUADYAYATcjs.majorKey.call(void 0, 0)));
349
- const startBeat = Math.max(region.startBeat, musicStart);
350
- const weights = new Array(PITCH_CLASSES).fill(0);
351
- for (let i = region.from; i < region.toExclusive; i += 1) {
352
- const slot = slotWeights[i];
353
- if (slot === void 0) {
354
- continue;
355
- }
356
- for (let pc = 0; pc < PITCH_CLASSES; pc += 1) {
357
- weights[pc] = (_nullishCoalesce(weights[pc], () => ( 0))) + (_nullishCoalesce(slot.weights[pc], () => ( 0)));
358
- }
359
- }
360
- const correlation = _chunkOE6WK2Y2cjs.profileScore.call(void 0,
361
- weights,
362
- _optionalChain([candidate, 'optionalAccess', _20 => _20.minor]) ? profile.minor : profile.major,
363
- _nullishCoalesce(_optionalChain([candidate, 'optionalAccess', _21 => _21.rootPc]), () => ( 0))
364
- );
365
- return {
366
- startBeat,
367
- endBeat: region.endBeat,
368
- key,
369
- confidence: Math.max(0, correlation)
370
- };
371
- });
372
- return withRelations(regions);
373
- }
374
- var CHORD_TONE_CREDIT = 1;
375
- var DOMINANT_CREDIT = 0.7;
376
- var TONIC_CREDIT = 0.4;
377
- var CADENCE_CREDIT = 0.6;
378
- var CHORD_MODULATION_COST = 0.6;
379
- function chordFitsKey(chord, candidate) {
380
- const tones = _chunkUADYAYATcjs.chordPitchClasses.call(void 0, chord);
381
- if (tones.length === 0) {
382
- return 0;
383
- }
384
- let inKey = 0;
385
- for (const pc of tones) {
386
- if (_chunkUADYAYATcjs.isScaleTone.call(void 0, pc, candidate.key)) {
387
- inKey += 1;
388
- }
389
- }
390
- let score = CHORD_TONE_CREDIT * ((inKey - (tones.length - inKey)) / tones.length);
391
- const rootDegree = _chunkQ2WGKJNNcjs.pitchClassOf.call(void 0, chord.rootPc - candidate.rootPc);
392
- if (rootDegree === 7 && (chord.quality === "dom7" || chord.quality === "maj")) {
393
- score += DOMINANT_CREDIT * (chord.quality === "dom7" ? 1 : 0.5);
394
- }
395
- if (rootDegree === 0) {
396
- score += TONIC_CREDIT;
397
- }
398
- return score;
399
- }
400
- function detectModulations(chords, opts = {}) {
401
- const meters = _chunkK4ZJUGF4cjs.resolveMeters.call(void 0, opts, "modulation meters");
402
- const bar = _chunkK4ZJUGF4cjs.beatsPerBarAt.call(void 0, 0, meters);
403
- const expectedKeyBeats = _nullishCoalesce(opts.expectedKeyBeats, () => ( bar * 4));
404
- _chunkQ2WGKJNNcjs.assertRange.call(void 0, expectedKeyBeats, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "expectedKeyBeats");
405
- const minKeyBeats = _nullishCoalesce(opts.minKeyBeats, () => ( bar));
406
- _chunkQ2WGKJNNcjs.assertRange.call(void 0, minKeyBeats, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "minKeyBeats");
407
- _chunkQ2WGKJNNcjs.assertGenerationBudget.call(void 0, chords.length, "modulation chord segments", opts.budget);
408
- const sorted = [...chords].filter((segment) => segment.endBeat > segment.startBeat).sort((a, b) => a.startBeat - b.startBeat);
409
- const lastChordEnd = sorted.reduce((end, segment) => Math.max(end, segment.endBeat), 0);
410
- const totalBeats = _nullishCoalesce(opts.totalBeats, () => ( lastChordEnd));
411
- _chunkQ2WGKJNNcjs.assertRange.call(void 0, totalBeats, 0, Number.MAX_SAFE_INTEGER, "modulation totalBeats");
412
- const ordered = sorted.map((segment) => ({ ...segment, endBeat: Math.min(segment.endBeat, totalBeats) })).filter((segment) => segment.endBeat > segment.startBeat);
413
- if (ordered.length === 0) {
414
- return [];
415
- }
416
- const slots = ordered.map((segment) => {
417
- const length = segment.endBeat - segment.startBeat;
418
- return {
419
- startBeat: segment.startBeat,
420
- endBeat: segment.endBeat,
421
- // A chord that holds for two bars argues twice as hard as one that
422
- // passes in half a bar, exactly as a heavier slot does in the note path.
423
- scores: KEY_CANDIDATES.map((candidate) => chordFitsKey(segment.chord, candidate) * length),
424
- weight: length
425
- };
426
- });
427
- for (let i = 1; i < slots.length; i += 1) {
428
- const arriving = _optionalChain([ordered, 'access', _22 => _22[i], 'optionalAccess', _23 => _23.chord]);
429
- const approaching = _optionalChain([ordered, 'access', _24 => _24[i - 1], 'optionalAccess', _25 => _25.chord]);
430
- const slot = slots[i];
431
- if (arriving === void 0 || approaching === void 0 || slot === void 0) {
432
- continue;
433
- }
434
- const isDominantMotion = _chunkQ2WGKJNNcjs.pitchClassOf.call(void 0, approaching.rootPc - arriving.rootPc) === 7 && approaching.quality === "dom7";
435
- if (!isDominantMotion) {
436
- continue;
437
- }
438
- const length = slot.endBeat - slot.startBeat;
439
- for (let c = 0; c < KEY_CANDIDATES.length; c += 1) {
440
- const candidate = KEY_CANDIDATES[c];
441
- if (candidate !== void 0 && _chunkQ2WGKJNNcjs.pitchClassOf.call(void 0, arriving.rootPc - candidate.rootPc) === 0) {
442
- slot.scores[c] = (_nullishCoalesce(slot.scores[c], () => ( 0))) + CADENCE_CREDIT * length;
443
- }
444
- }
445
- }
446
- const changeCost = CHORD_MODULATION_COST * expectedKeyBeats;
447
- const path = chooseKeys(slots, changeCost, meters);
448
- const grouped = regionsFromPath(
449
- slots,
450
- enforceMinimumRegion(slots, path, Math.min(minKeyBeats, expectedKeyBeats))
451
- );
452
- const profile = _chunkOE6WK2Y2cjs.resolveKeyProfile.call(void 0, opts.profile);
453
- const regions = grouped.map((region) => {
454
- const candidate = KEY_CANDIDATES[region.candidate];
455
- const key = _nullishCoalesce(_optionalChain([candidate, 'optionalAccess', _26 => _26.key]), () => ( _chunkUADYAYATcjs.majorKey.call(void 0, 0)));
456
- const weights = new Array(PITCH_CLASSES).fill(0);
457
- for (const segment of ordered) {
458
- const overlap = Math.min(segment.endBeat, region.endBeat) - Math.max(segment.startBeat, region.startBeat);
459
- if (overlap <= EPS3) {
460
- continue;
461
- }
462
- for (const pc of _chunkUADYAYATcjs.chordPitchClasses.call(void 0, segment.chord)) {
463
- weights[pc] = (_nullishCoalesce(weights[pc], () => ( 0))) + overlap;
464
- }
465
- }
466
- const correlation = _chunkOE6WK2Y2cjs.profileScore.call(void 0,
467
- weights,
468
- _optionalChain([candidate, 'optionalAccess', _27 => _27.minor]) ? profile.minor : profile.major,
469
- _nullishCoalesce(_optionalChain([candidate, 'optionalAccess', _28 => _28.rootPc]), () => ( 0))
470
- );
471
- return {
472
- startBeat: region.startBeat,
473
- endBeat: region.endBeat,
474
- key,
475
- confidence: Math.max(0, correlation)
476
- };
477
- });
478
- const last = regions[regions.length - 1];
479
- if (last !== void 0 && totalBeats > last.endBeat) {
480
- last.endBeat = totalBeats;
481
- }
482
- return attachPivots(withRelations(regions), ordered);
483
- }
484
- var THIRD_SEMITONES = [3, 4];
485
- var FIFTH_SEMITONES = [6, 7, 8];
486
- function triadCore(chord) {
487
- const root = _chunkQ2WGKJNNcjs.pitchClassOf.call(void 0, chord.rootPc);
488
- let third;
489
- let fifth;
490
- for (const interval of chord.intervals) {
491
- if (interval < 0 || interval >= 12) {
492
- continue;
493
- }
494
- if (third === void 0 && THIRD_SEMITONES.includes(interval)) {
495
- third = interval;
496
- } else if (fifth === void 0 && FIFTH_SEMITONES.includes(interval)) {
497
- fifth = interval;
498
- }
499
- }
500
- if (third === void 0 || fifth === void 0) {
501
- return null;
502
- }
503
- return [root, _chunkQ2WGKJNNcjs.pitchClassOf.call(void 0, root + third), _chunkQ2WGKJNNcjs.pitchClassOf.call(void 0, root + fifth)].sort((a, b) => a - b);
504
- }
505
- function sameTones(a, b) {
506
- return a.length === b.length && a.every((pc, index) => pc === b[index]);
507
- }
508
- function attachPivots(regions, chords) {
509
- for (let i = 1; i < regions.length; i += 1) {
510
- const previous = regions[i - 1];
511
- const current = regions[i];
512
- if (previous === void 0 || current === void 0) {
513
- continue;
514
- }
515
- let last;
516
- for (const segment of chords) {
517
- if (segment.endBeat <= current.startBeat + EPS3) {
518
- last = segment;
519
- }
520
- }
521
- if (last === void 0) {
522
- continue;
523
- }
524
- const core = triadCore(last.chord);
525
- if (core === null) {
526
- continue;
527
- }
528
- const root = _chunkQ2WGKJNNcjs.pitchClassOf.call(void 0, last.chord.rootPc);
529
- const pivot = _chunkOE6WK2Y2cjs.pivotChords.call(void 0, previous.key, current.key).find(
530
- (entry) => entry.chord.rootPc === root && sameTones(_chunkUADYAYATcjs.chordPitchClasses.call(void 0, entry.chord), core)
531
- );
532
- if (pivot !== void 0) {
533
- current.pivot = pivot;
534
- }
535
- }
536
- return regions;
537
- }
538
- function keyLookup(regions, fallback) {
539
- return (beat) => {
540
- let low = 0;
541
- let high = regions.length;
542
- while (low < high) {
543
- const middle = Math.floor((low + high) / 2);
544
- if ((_nullishCoalesce(_optionalChain([regions, 'access', _29 => _29[middle], 'optionalAccess', _30 => _30.startBeat]), () => ( Number.POSITIVE_INFINITY))) <= beat) {
545
- low = middle + 1;
546
- } else {
547
- high = middle;
548
- }
549
- }
550
- const candidate = regions[low - 1];
551
- return candidate !== void 0 && beat < candidate.endBeat ? candidate.key : fallback;
552
- };
553
- }
554
- function prevailingKeyOf(regions) {
555
- const totals = /* @__PURE__ */ new Map();
556
- for (const region of regions) {
557
- const id = `${_chunkQ2WGKJNNcjs.pitchClassOf.call(void 0, region.key.rootPc)}:${region.key.modeMask12}`;
558
- const entry = totals.get(id);
559
- const length = Math.max(0, region.endBeat - region.startBeat);
560
- if (entry === void 0) {
561
- totals.set(id, { key: region.key, length });
562
- } else {
563
- entry.length += length;
564
- }
565
- }
566
- let best = null;
567
- let bestLength = -1;
568
- for (const entry of totals.values()) {
569
- if (entry.length > bestLength) {
570
- bestLength = entry.length;
571
- best = entry.key;
572
- }
573
- }
574
- return best;
575
- }
576
-
577
- // src/analyze/timeline/index.ts
578
- function chordTimelineFromChords(chords, totalBeats) {
579
- _chunkQ2WGKJNNcjs.assertRange.call(void 0, totalBeats, 0, Number.MAX_SAFE_INTEGER, "timeline totalBeats");
580
- _chunkQ2WGKJNNcjs.assertGenerationBudget.call(void 0, chords.length, "timeline chords");
581
- for (let index = 0; index < chords.length; index += 1) {
582
- _chunkQ2WGKJNNcjs.assertRange.call(void 0,
583
- _nullishCoalesce(_optionalChain([chords, 'access', _31 => _31[index], 'optionalAccess', _32 => _32.startBeat]), () => ( Number.NaN)),
584
- -Number.MAX_SAFE_INTEGER,
585
- Number.MAX_SAFE_INTEGER,
586
- `chords[${index}].startBeat`
587
- );
588
- }
589
- const sorted = [...chords].filter((chord) => chord.startBeat < totalBeats).sort((a, b) => a.startBeat - b.startBeat);
590
- const segments = sorted.map((gc, i) => {
591
- const next = sorted[i + 1];
592
- const nextBeat = next ? next.startBeat : totalBeats;
593
- const endBeat = Math.min(totalBeats, Math.max(gc.startBeat, nextBeat));
594
- return {
595
- startBeat: gc.startBeat,
596
- endBeat,
597
- chord: _chunkUADYAYATcjs.chordFromSpan.call(void 0, gc)
598
- };
599
- }).filter((seg) => seg.endBeat > seg.startBeat);
600
- return { at: segmentLookup(segments), segments };
601
- }
602
- function segmentLookup(segments) {
603
- return (beat) => {
604
- _chunkQ2WGKJNNcjs.assertFiniteNumber.call(void 0, beat, "timeline query beat");
605
- let low = 0;
606
- let high = segments.length;
607
- while (low < high) {
608
- const middle = Math.floor((low + high) / 2);
609
- if ((_nullishCoalesce(_optionalChain([segments, 'access', _33 => _33[middle], 'optionalAccess', _34 => _34.startBeat]), () => ( Number.POSITIVE_INFINITY))) <= beat) {
610
- low = middle + 1;
611
- } else {
612
- high = middle;
613
- }
614
- }
615
- const candidate = segments[low - 1];
616
- return candidate !== void 0 && beat >= candidate.startBeat && beat < candidate.endBeat ? candidate.chord : null;
617
- };
618
- }
619
- var EPS4 = 1e-9;
620
- var NOISE_THRESHOLD_RATIO = 0.2;
621
- var MAX_DETECTION_PCS = 6;
622
- var INEXACT_CONFIDENCE_FACTOR = 0.85;
623
- var DIATONIC_BONUS = 0.5;
624
- var EXACT_BONUS = 0.5;
625
- var EXTRA_TONE_PENALTY = 0.6;
626
- var OMITTED_TONE_PENALTY = 0.15;
627
- var BASS_ROOT_STANDING = 0.95;
628
- function scoreMatch(match, weights, maxWeight, key) {
629
- const rootWeight = maxWeight > 0 ? (_nullishCoalesce(weights[match.rootPc], () => ( 0))) / maxWeight : 0;
630
- const rootStanding = match.inversion === 0 ? Math.max(rootWeight, BASS_ROOT_STANDING) : rootWeight;
631
- const tones = _chunkUADYAYATcjs.chordPitchClasses.call(void 0, _chunkUADYAYATcjs.makeChord.call(void 0, match.rootPc, match.quality));
632
- let score = rootStanding;
633
- if (tones.every((pc) => _chunkUADYAYATcjs.isScaleTone.call(void 0, pc, key))) {
634
- score += DIATONIC_BONUS;
635
- }
636
- if (match.exact) {
637
- score += EXACT_BONUS;
638
- }
639
- score -= EXTRA_TONE_PENALTY * match.extraPcs.length;
640
- score -= OMITTED_TONE_PENALTY * match.missingPcs.length;
641
- return score;
642
- }
643
- function chordConfidence(chord, weights, totalWeight, exact) {
644
- const tones = new Set(_chunkUADYAYATcjs.chordPitchClasses.call(void 0, chord));
645
- let chordWeight = 0;
646
- for (let pc = 0; pc < 12; pc += 1) {
647
- if (tones.has(pc)) {
648
- chordWeight += _nullishCoalesce(weights[pc], () => ( 0));
649
- }
650
- }
651
- const raw = totalWeight > 0 ? chordWeight / totalWeight : 0;
652
- const confidence = Math.min(1, Math.max(0, raw));
653
- return exact ? confidence : confidence * INEXACT_CONFIDENCE_FACTOR;
654
- }
655
- function analyzeWindow(notes, windowStart, windowEnd, meters, key) {
656
- const { weights, totalWeight, maxWeight, lowestPitch } = windowWeights(
657
- notes,
658
- windowStart,
659
- windowEnd,
660
- meters
661
- );
662
- if (totalWeight <= EPS4) {
663
- return null;
664
- }
665
- const ranked = weights.map((weight, pc) => ({ pc, weight })).filter(({ weight }) => weight > 0).sort((a, b) => b.weight - a.weight || a.pc - b.pc);
666
- const selected = ranked.filter(({ weight }) => weight >= maxWeight * NOISE_THRESHOLD_RATIO).slice(0, MAX_DETECTION_PCS).map(({ pc }) => pc);
667
- const bassPc = _chunkQ2WGKJNNcjs.pitchClassOf.call(void 0, lowestPitch);
668
- const hasBass = selected.includes(bassPc);
669
- const detectionPitches = hasBass ? selected.map((pc) => pc === bassPc ? pc : pc + 12) : selected;
670
- const matches = _chunkOE6WK2Y2cjs.detectChord.call(void 0, detectionPitches, { input: hasBass ? "midi" : "pitchClass" });
671
- let bestMatch = null;
672
- let bestScore = Number.NEGATIVE_INFINITY;
673
- for (const match of matches) {
674
- const score = scoreMatch(match, weights, maxWeight, key);
675
- if (score > bestScore) {
676
- bestScore = score;
677
- bestMatch = match;
678
- }
679
- }
680
- if (bestMatch) {
681
- const augmented = hasBass ? _chunkOE6WK2Y2cjs.augmentedSixthFromPitchClasses.call(void 0, selected, bassPc, key) : null;
682
- if (augmented !== null) {
683
- return {
684
- chord: augmented,
685
- confidence: chordConfidence(augmented, weights, totalWeight, true)
686
- };
687
- }
688
- const chord = _chunkUADYAYATcjs.makeChord.call(void 0, bestMatch.rootPc, bestMatch.quality, bestMatch.bassPc);
689
- return { chord, confidence: chordConfidence(chord, weights, totalWeight, bestMatch.exact) };
690
- }
691
- return null;
692
- }
693
- function sameChord(a, b) {
694
- return a.rootPc === b.rootPc && a.quality === b.quality && a.bassPc === b.bassPc;
695
- }
696
- var SEGMENTATION_QUALITIES = [
697
- "maj",
698
- "min",
699
- "dim",
700
- "aug",
701
- "dom7",
702
- "maj7",
703
- "min7",
704
- "m7b5",
705
- "dim7",
706
- "sus4",
707
- "6",
708
- "min6"
709
- ];
710
- var LEXICON_SIZE = 12 * SEGMENTATION_QUALITIES.length;
711
- var LEXICON_ROOTS = new Int32Array(LEXICON_SIZE);
712
- var LEXICON_TONE_START = new Int32Array(LEXICON_SIZE + 1);
713
- var LEXICON_TONES = (() => {
714
- const tones = [];
715
- let candidate = 0;
716
- for (let rootPc = 0; rootPc < 12; rootPc += 1) {
717
- for (const quality of SEGMENTATION_QUALITIES) {
718
- LEXICON_ROOTS[candidate] = rootPc;
719
- LEXICON_TONE_START[candidate] = tones.length;
720
- tones.push(..._chunkUADYAYATcjs.chordPitchClasses.call(void 0, _chunkUADYAYATcjs.makeChord.call(void 0, rootPc, quality)));
721
- candidate += 1;
722
- }
723
- }
724
- LEXICON_TONE_START[candidate] = tones.length;
725
- return Int32Array.from(tones);
726
- })();
727
- var OUTSIDE_TONE_PENALTY = 1;
728
- var MISSING_TONE_PENALTY = 0.5;
729
- var ROOT_PRESENCE_BONUS = 0.3;
730
- var CHANGE_COST = 0.35;
731
- var STRONG_BEAT_DISCOUNT2 = 0.5;
732
- var MAX_METRIC_WEIGHT2 = 3;
733
- function candidateScore(candidate, weights, totalWeight) {
734
- const first = _nullishCoalesce(LEXICON_TONE_START[candidate], () => ( 0));
735
- const last = _nullishCoalesce(LEXICON_TONE_START[candidate + 1], () => ( first));
736
- let covered = 0;
737
- let missing = 0;
738
- for (let index = first; index < last; index += 1) {
739
- const weight = _nullishCoalesce(weights[_nullishCoalesce(LEXICON_TONES[index], () => ( 0))], () => ( 0));
740
- covered += weight;
741
- if (weight <= EPS4) {
742
- missing += 1;
743
- }
744
- }
745
- const outside = totalWeight - covered;
746
- const averageToneWeight = totalWeight / (last - first);
747
- return covered - OUTSIDE_TONE_PENALTY * outside - MISSING_TONE_PENALTY * missing * averageToneWeight + ROOT_PRESENCE_BONUS * (_nullishCoalesce(weights[_nullishCoalesce(LEXICON_ROOTS[candidate], () => ( 0))], () => ( 0)));
748
- }
749
- function beatOfSlot(grid, index) {
750
- return grid.origin + index * grid.slotBeats;
751
- }
752
- function slotOfBeat(grid, beat) {
753
- return (beat - grid.origin) / grid.slotBeats;
754
- }
755
- function createBoundaryTables(slotCount) {
756
- return {
757
- scores: new Float64Array(slotCount * LEXICON_SIZE),
758
- back: new Int32Array(slotCount * LEXICON_SIZE),
759
- costs: new Float64Array(LEXICON_SIZE),
760
- spare: new Float64Array(LEXICON_SIZE),
761
- path: new Int32Array(slotCount)
762
- };
763
- }
764
- function scoreSlot(tables, slot, index) {
765
- const base = index * LEXICON_SIZE;
766
- if (slot.totalWeight <= EPS4) {
767
- tables.scores.fill(0, base, base + LEXICON_SIZE);
768
- return;
769
- }
770
- for (let c = 0; c < LEXICON_SIZE; c += 1) {
771
- tables.scores[base + c] = candidateScore(c, slot.weights, slot.totalWeight);
772
- }
773
- }
774
- function chooseBoundaries(tables, from, to, grid, meters, changeCost) {
775
- const width = to - from;
776
- if (width <= 0) {
777
- return [];
778
- }
779
- const { scores, back, path } = tables;
780
- let costs = tables.costs;
781
- let next = tables.spare;
782
- for (let c = 0; c < LEXICON_SIZE; c += 1) {
783
- costs[c] = -(_nullishCoalesce(scores[from * LEXICON_SIZE + c], () => ( 0)));
784
- }
785
- for (let i = 1; i < width; i += 1) {
786
- let cheapest = Number.POSITIVE_INFINITY;
787
- let cheapestIndex = 0;
788
- for (let c = 0; c < LEXICON_SIZE; c += 1) {
789
- const cost = _nullishCoalesce(costs[c], () => ( Number.POSITIVE_INFINITY));
790
- if (cost < cheapest) {
791
- cheapest = cost;
792
- cheapestIndex = c;
793
- }
794
- }
795
- const slotStart = beatOfSlot(grid, from + i);
796
- const accent = _chunkK4ZJUGF4cjs.metricWeight.call(void 0, slotStart, meters);
797
- const penalty = changeCost * (1 - STRONG_BEAT_DISCOUNT2 * accent / MAX_METRIC_WEIGHT2);
798
- const rowBase = (from + i) * LEXICON_SIZE;
799
- const change = cheapest + penalty;
800
- for (let c = 0; c < LEXICON_SIZE; c += 1) {
801
- const stay = _nullishCoalesce(costs[c], () => ( Number.POSITIVE_INFINITY));
802
- const carried = stay <= change ? stay : change;
803
- back[rowBase + c] = stay <= change ? c : cheapestIndex;
804
- next[c] = carried - (_nullishCoalesce(scores[rowBase + c], () => ( 0)));
805
- }
806
- const previous = costs;
807
- costs = next;
808
- next = previous;
809
- }
810
- let best = 0;
811
- for (let c = 1; c < LEXICON_SIZE; c += 1) {
812
- if ((_nullishCoalesce(costs[c], () => ( Number.POSITIVE_INFINITY))) < (_nullishCoalesce(costs[best], () => ( Number.POSITIVE_INFINITY)))) {
813
- best = c;
814
- }
815
- }
816
- let current = best;
817
- for (let i = width - 1; i >= 0; i -= 1) {
818
- path[from + i] = current;
819
- if (i > 0) {
820
- current = _nullishCoalesce(back[(from + i) * LEXICON_SIZE + current], () => ( current));
821
- }
822
- }
823
- const spans = [];
824
- let runStart = 0;
825
- for (let i = 1; i <= width; i += 1) {
826
- if (i === width || path[from + i] !== path[from + runStart]) {
827
- spans.push({
828
- startBeat: beatOfSlot(grid, from + runStart),
829
- endBeat: beatOfSlot(grid, from + i)
830
- });
831
- runStart = i;
832
- }
833
- }
834
- return spans;
835
- }
836
- var DEFAULT_MIN_CHORD_BEATS = 1;
837
- function slotBeatsWithin(harmonicRhythm, minChordBeats) {
838
- const divisions = Math.max(1, Math.ceil(harmonicRhythm / minChordBeats - EPS4));
839
- return harmonicRhythm / divisions;
840
- }
841
- function gridSpans(slotCount, grid) {
842
- return Array.from({ length: slotCount }, (_, i) => ({
843
- startBeat: beatOfSlot(grid, i),
844
- endBeat: beatOfSlot(grid, i + 1)
845
- }));
846
- }
847
- function dynamicSpans(slotWeights, tables, grid, harmonicRhythm, meters) {
848
- const slotBeats = grid.slotBeats;
849
- const slotCount = slotWeights.length;
850
- let soundingSlots = 0;
851
- let weightSum = 0;
852
- for (let i = 0; i < slotCount; i += 1) {
853
- const weight = _nullishCoalesce(_optionalChain([slotWeights, 'access', _35 => _35[i], 'optionalAccess', _36 => _36.totalWeight]), () => ( 0));
854
- if (weight > EPS4) {
855
- soundingSlots += 1;
856
- weightSum += weight;
857
- }
858
- }
859
- if (soundingSlots === 0) {
860
- return [];
861
- }
862
- const meanSlotWeight = weightSum / soundingSlots;
863
- const changeCost = CHANGE_COST * meanSlotWeight * (harmonicRhythm / slotBeats);
864
- const silent = slotWeights.map((slot) => slot.totalWeight <= EPS4);
865
- const breaks = new Array(slotCount).fill(false);
866
- for (let i = 0; i < slotCount; ) {
867
- if (!silent[i]) {
868
- i += 1;
869
- continue;
870
- }
871
- let end = i;
872
- while (end < slotCount && silent[end]) {
873
- end += 1;
874
- }
875
- if ((end - i) * slotBeats >= harmonicRhythm - EPS4) {
876
- for (let k = i; k < end; k += 1) {
877
- breaks[k] = true;
878
- }
879
- }
880
- i = end;
881
- }
882
- const spans = [];
883
- for (let i = 0; i < slotCount; ) {
884
- if (breaks[i]) {
885
- i += 1;
886
- continue;
887
- }
888
- let end = i;
889
- while (end < slotCount && !breaks[end]) {
890
- end += 1;
891
- }
892
- spans.push(...chooseBoundaries(tables, i, end, grid, meters, changeCost));
893
- i = end;
894
- }
895
- return spans;
896
- }
897
- function notesOfSpan(slotNotes, span, grid) {
898
- const first = Math.round(slotOfBeat(grid, span.startBeat));
899
- const lastExclusive = Math.round(slotOfBeat(grid, span.endBeat));
900
- if (lastExclusive - first === 1) {
901
- return [..._nullishCoalesce(slotNotes[first], () => ( []))];
902
- }
903
- const seen = /* @__PURE__ */ new Set();
904
- for (let i = first; i < lastExclusive; i += 1) {
905
- for (const note of _nullishCoalesce(slotNotes[i], () => ( []))) {
906
- seen.add(note);
907
- }
908
- }
909
- return [...seen];
910
- }
911
- function chordTimelineFromNotes(notes, opts = {}) {
912
- return analyzeTimeline(notes, opts).result;
913
- }
914
- var DIRTY_SLOT_MARGIN = 1;
915
- function dirtySlotsFor(evidence, startBeat, endBeat) {
916
- const first = Math.floor(slotOfBeat(evidence.grid, startBeat)) - DIRTY_SLOT_MARGIN;
917
- const lastExclusive = Math.ceil(slotOfBeat(evidence.grid, endBeat)) + DIRTY_SLOT_MARGIN;
918
- return {
919
- from: Math.max(0, Math.min(evidence.slotCount, first)),
920
- to: Math.max(0, Math.min(evidence.slotCount, lastExclusive))
921
- };
922
- }
923
- function spanIsClean(grid, span, dirtyFrom, dirtyTo) {
924
- const first = Math.round(slotOfBeat(grid, span.startBeat));
925
- const lastExclusive = Math.round(slotOfBeat(grid, span.endBeat));
926
- return lastExclusive <= dirtyFrom || first >= dirtyTo;
927
- }
928
- function keyId(key) {
929
- return `${key.rootPc}:${key.modeMask12}`;
930
- }
931
- function windowId(startBeat, endBeat, key) {
932
- return `${startBeat}|${endBeat}|${keyId(key)}`;
933
- }
934
- function evidenceFits(previous, grid, slotCount, totalBeats, segmentation, harmonicRhythm, keyGiven) {
935
- return previous.grid.origin === grid.origin && previous.grid.slotBeats === grid.slotBeats && previous.slotCount === slotCount && previous.totalBeats === totalBeats && previous.segmentation === segmentation && previous.harmonicRhythm === harmonicRhythm && previous.keyGiven === keyGiven;
936
- }
937
- function analyzeTimeline(notes, opts = {}, previous, dirty) {
938
- const meters = _chunkK4ZJUGF4cjs.resolveMeters.call(void 0, opts, "timeline meters");
939
- const harmonicRhythm = _nullishCoalesce(opts.harmonicRhythm, () => ( _chunkK4ZJUGF4cjs.beatsPerBarAt.call(void 0, 0, meters)));
940
- _chunkQ2WGKJNNcjs.assertRange.call(void 0, harmonicRhythm, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "harmonic rhythm");
941
- const segmentation = _nullishCoalesce(opts.segmentation, () => ( "dynamic"));
942
- const minChordBeats = _nullishCoalesce(opts.minChordBeats, () => ( DEFAULT_MIN_CHORD_BEATS));
943
- _chunkQ2WGKJNNcjs.assertRange.call(void 0, minChordBeats, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "minChordBeats");
944
- const budget = opts.budget;
945
- const noteOptions = { allowNonPositiveDuration: true, budget };
946
- if (opts.pickupBeats !== void 0) {
947
- _chunkQ2WGKJNNcjs.assertRange.call(void 0, opts.pickupBeats, 0, Number.MAX_SAFE_INTEGER, "timeline pickupBeats");
948
- noteOptions.minStartBeat = -opts.pickupBeats;
949
- }
950
- _chunkQ2WGKJNNcjs.assertNoteEvents.call(void 0, notes, "timeline notes", noteOptions);
951
- const soundingIndex = _chunkK4ZJUGF4cjs.createNoteEventIndex.call(void 0,
952
- notes.filter((note) => note.durationBeat > 0),
953
- { name: "timeline notes", budget }
954
- );
955
- const sounding = soundingIndex.notes.map(({ note }) => note);
956
- const lastNoteEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
957
- const totalBeats = _nullishCoalesce(opts.totalBeats, () => ( lastNoteEnd));
958
- _chunkQ2WGKJNNcjs.assertRange.call(void 0, totalBeats, 0, Number.MAX_SAFE_INTEGER, "timeline totalBeats");
959
- const slotBeats = segmentation === "grid" ? harmonicRhythm : slotBeatsWithin(harmonicRhythm, minChordBeats);
960
- const firstOnset = sounding.reduce((first, n) => Math.min(first, n.startBeat), 0);
961
- const { origin, startBeat: musicStart } = gridOriginOf(firstOnset, slotBeats);
962
- const grid = { origin, slotBeats };
963
- const keys = opts.key !== void 0 ? [{ startBeat: musicStart, endBeat: totalBeats, key: opts.key, confidence: 1 }] : keyTimelineFromNotes(sounding, { meters, totalBeats, budget });
964
- const prevailingKey = _nullishCoalesce(prevailingKeyOf(keys), () => ( _chunkUADYAYATcjs.majorKey.call(void 0, 0)));
965
- const keyAt = keyLookup(keys, prevailingKey);
966
- const segments = [];
967
- const segmentConfidence = [];
968
- const slotCount = Math.max(0, Math.ceil((totalBeats - grid.origin) / slotBeats - EPS4));
969
- _chunkQ2WGKJNNcjs.assertGenerationBudget.call(void 0, slotCount, "timeline windows", budget);
970
- const slotNotes = bucketNotesBySlot(sounding, grid, slotCount, {
971
- name: "timeline note-to-window memberships",
972
- budget
973
- });
974
- const carried = previous !== void 0 && dirty !== void 0 && evidenceFits(
975
- previous,
976
- grid,
977
- slotCount,
978
- totalBeats,
979
- segmentation,
980
- harmonicRhythm,
981
- opts.key !== void 0
982
- ) ? previous : void 0;
983
- const dirtyFrom = carried === void 0 ? 0 : Math.max(0, _nullishCoalesce(_optionalChain([dirty, 'optionalAccess', _37 => _37.from]), () => ( 0)));
984
- const dirtyTo = carried === void 0 ? slotCount : Math.min(slotCount, _nullishCoalesce(_optionalChain([dirty, 'optionalAccess', _38 => _38.to]), () => ( slotCount)));
985
- const tables = createBoundaryTables(slotCount);
986
- const slotWeights = new Array(slotCount);
987
- if (carried !== void 0) {
988
- tables.scores.set(carried.tables.scores);
989
- }
990
- for (let i = 0; i < slotCount; i += 1) {
991
- const reusable = i < dirtyFrom || i >= dirtyTo ? _optionalChain([carried, 'optionalAccess', _39 => _39.slotWeights, 'access', _40 => _40[i]]) : void 0;
992
- if (reusable !== void 0) {
993
- slotWeights[i] = reusable;
994
- continue;
995
- }
996
- const start = beatOfSlot(grid, i);
997
- const weights = windowWeights(
998
- _nullishCoalesce(slotNotes[i], () => ( [])),
999
- start,
1000
- Math.min(start + slotBeats, totalBeats),
1001
- meters
1002
- );
1003
- slotWeights[i] = weights;
1004
- scoreSlot(tables, weights, i);
1005
- }
1006
- const spans = segmentation === "grid" ? gridSpans(slotCount, grid) : dynamicSpans(slotWeights, tables, grid, harmonicRhythm, meters);
1007
- const windows = /* @__PURE__ */ new Map();
1008
- for (const span of spans) {
1009
- const start = Math.max(span.startBeat, musicStart);
1010
- const end = Math.min(span.endBeat, totalBeats);
1011
- const spanKey = keyAt(start);
1012
- const id = windowId(start, end, spanKey);
1013
- const cached = carried !== void 0 && spanIsClean(grid, span, dirtyFrom, dirtyTo) ? carried.windows.get(id) : void 0;
1014
- const inferred = cached !== void 0 ? cached : analyzeWindow(notesOfSpan(slotNotes, span, grid), start, end, meters, spanKey);
1015
- windows.set(id, inferred);
1016
- if (!inferred) {
1017
- continue;
1018
- }
1019
- const last = segments[segments.length - 1];
1020
- if (last && sameChord(last.chord, inferred.chord) && Math.abs(last.endBeat - start) < EPS4) {
1021
- const lastLength = last.endBeat - last.startBeat;
1022
- const length = end - start;
1023
- const lastConfidence = _nullishCoalesce(segmentConfidence[segmentConfidence.length - 1], () => ( 0));
1024
- segmentConfidence[segmentConfidence.length - 1] = (lastConfidence * lastLength + inferred.confidence * length) / (lastLength + length);
1025
- last.endBeat = end;
1026
- } else {
1027
- segments.push({ startBeat: start, endBeat: end, chord: inferred.chord });
1028
- segmentConfidence.push(inferred.confidence);
1029
- }
1030
- }
1031
- return {
1032
- result: {
1033
- timeline: { at: segmentLookup(segments), segments },
1034
- keys: attachPivots(keys, segments),
1035
- prevailingKey,
1036
- segmentConfidence
1037
- },
1038
- evidence: {
1039
- grid,
1040
- slotCount,
1041
- totalBeats,
1042
- segmentation,
1043
- harmonicRhythm,
1044
- keyGiven: opts.key !== void 0,
1045
- slotWeights,
1046
- tables,
1047
- windows
1048
- }
1049
- };
1050
- }
1051
- function detectCadences(timeline, key) {
1052
- const keyAt = typeof key === "function" ? key : () => key;
1053
- const hits = [];
1054
- for (let i = 1; i < timeline.segments.length; i += 1) {
1055
- const prev = timeline.segments[i - 1];
1056
- const cur = timeline.segments[i];
1057
- if (!prev || !cur) {
1058
- continue;
1059
- }
1060
- if (Math.abs(cur.startBeat - prev.endBeat) > EPS4) {
1061
- continue;
1062
- }
1063
- const before = timeline.segments[i - 2];
1064
- const approach = before !== void 0 && Math.abs(prev.startBeat - before.endBeat) <= EPS4 ? before.chord : void 0;
1065
- const cadence = _chunkOE6WK2Y2cjs.detectCadence.call(void 0, prev.chord, cur.chord, keyAt(cur.startBeat), {
1066
- ...approach === void 0 ? {} : { approach }
1067
- });
1068
- if (cadence.type !== null) {
1069
- hits.push({ atBeat: cur.startBeat, cadence, from: prev.chord, to: cur.chord });
1070
- }
1071
- }
1072
- return hits;
1073
- }
1074
-
1075
- // src/analyze/voice/index.ts
1076
- function toVoiceNotes(events) {
1077
- return events.map((event, index) => ({ ...event, id: index, originalIndex: index }));
1078
- }
1079
- var HUMANIZE_ADJACENCY = 0.05;
1080
- function intervalAboveBass(pitch, others, chord) {
1081
- let bass = Number.POSITIVE_INFINITY;
1082
- for (const ov of others) {
1083
- bass = Math.min(bass, ov.pitch);
1084
- }
1085
- if (Number.isFinite(bass) && bass < pitch) {
1086
- return ((pitch - bass) % 12 + 12) % 12;
1087
- }
1088
- const ref = _nullishCoalesce(chord.bassPc, () => ( chord.rootPc));
1089
- return ((_chunkQ2WGKJNNcjs.pitchClassOf.call(void 0, pitch) - _chunkQ2WGKJNNcjs.pitchClassOf.call(void 0, ref)) % 12 + 12) % 12;
1090
- }
1091
- var TENSION_DEGREE_BY_INTERVAL = {
1092
- 1: 9,
1093
- 2: 9,
1094
- 3: 9,
1095
- 5: 11,
1096
- 6: 11,
1097
- 8: 13,
1098
- 9: 13
1099
- };
1100
- function tensionDegree(ic) {
1101
- return TENSION_DEGREE_BY_INTERVAL[ic];
1102
- }
1103
- function isStep(a, b) {
1104
- const d = Math.abs(a - b);
1105
- return d === 1 || d === 2;
1106
- }
1107
- var LEAP_SEMITONES = 3;
1108
- function suspensionType(ic, delta) {
1109
- if (delta > 0) {
1110
- return ic === 1 || ic === 2 ? "sus2-3" : null;
1111
- }
1112
- if (ic === 2) {
1113
- return "sus9-8";
1114
- }
1115
- if (ic === 10 || ic === 11) {
1116
- return "sus7-6";
1117
- }
1118
- if (ic === 5 || ic === 6) {
1119
- return "sus4-3";
1120
- }
1121
- if (ic === 8 || ic === 9) {
1122
- return "sus6-5";
1123
- }
1124
- return null;
1125
- }
1126
- var STEP_RESOLUTION = 2;
1127
- var NEAREST_CHORD_TONE_REACH = 12;
1128
- function nearestChordTone(pitch, chord, maxDistance) {
1129
- for (let delta = 1; delta <= maxDistance; delta += 1) {
1130
- if (_chunkUADYAYATcjs.isChordMember.call(void 0, pitch - delta, chord)) {
1131
- return pitch - delta;
1132
- }
1133
- if (_chunkUADYAYATcjs.isChordMember.call(void 0, pitch + delta, chord)) {
1134
- return pitch + delta;
1135
- }
1136
- }
1137
- return void 0;
1138
- }
1139
- function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat = () => []) {
1140
- _chunkQ2WGKJNNcjs.assertNoteEvents.call(void 0, voice, "voice notes", { allowNonPositiveDuration: true });
1141
- const keyAt = typeof key === "function" ? key : () => key;
1142
- const result = [];
1143
- for (let i = 0; i < voice.length; i += 1) {
1144
- const note = voice[i];
1145
- if (!note) {
1146
- continue;
1147
- }
1148
- const prevNote = i > 0 ? voice[i - 1] : void 0;
1149
- const nextNote = i + 1 < voice.length ? voice[i + 1] : void 0;
1150
- const prevEnd = prevNote === void 0 ? Number.NEGATIVE_INFINITY : prevNote.startBeat + prevNote.durationBeat;
1151
- const noteEnd = note.startBeat + note.durationBeat;
1152
- const prev = prevNote !== void 0 && Math.abs(prevEnd - note.startBeat) <= HUMANIZE_ADJACENCY ? prevNote : void 0;
1153
- const next = nextNote !== void 0 && Math.abs(noteEnd - nextNote.startBeat) <= HUMANIZE_ADJACENCY ? nextNote : void 0;
1154
- const chord = chordAtBeat(note.startBeat);
1155
- const labels = [];
1156
- const member = _chunkUADYAYATcjs.isChordMember.call(void 0, note.pitch, chord);
1157
- let handled = false;
1158
- if (chord && member) {
1159
- const role = _chunkUADYAYATcjs.chordToneRole.call(void 0, note.pitch, chord);
1160
- const degree = role === null ? tensionDegree(_chunkUADYAYATcjs.intervalAboveRoot.call(void 0, note.pitch, chord)) : void 0;
1161
- if (role) {
1162
- labels.push({ kind: "chordTone", role });
1163
- handled = true;
1164
- } else if (degree !== void 0) {
1165
- labels.push({ kind: "tension", degree });
1166
- handled = true;
1167
- }
1168
- }
1169
- if (!handled && chord && prev && next) {
1170
- const prevChord = chordAtBeat(prev.startBeat);
1171
- const prepared = prev.pitch === note.pitch && _chunkUADYAYATcjs.isChordMember.call(void 0, prev.pitch, prevChord);
1172
- const resolves = isStep(next.pitch, note.pitch);
1173
- const others = otherVoicesAtBeat(note.startBeat);
1174
- const verticallyDissonant = others.some(
1175
- (ov) => _chunkUADYAYATcjs.createsVerticalDissonance.call(void 0, note.pitch, ov.pitch, true)
1176
- );
1177
- if (prepared && verticallyDissonant && resolves) {
1178
- const ic = intervalAboveBass(note.pitch, others, chord);
1179
- const type = suspensionType(ic, next.pitch - note.pitch);
1180
- if (type !== null) {
1181
- labels.push({ kind: "suspension", type, resolveTo: next.pitch });
1182
- handled = true;
1183
- }
1184
- }
1185
- }
1186
- if (!handled && chord && prev && next) {
1187
- const prevMember = _chunkUADYAYATcjs.isChordMember.call(void 0, prev.pitch, chordAtBeat(prev.startBeat));
1188
- const nextMember = _chunkUADYAYATcjs.isChordMember.call(void 0, next.pitch, chordAtBeat(next.startBeat));
1189
- const up = note.pitch - prev.pitch > 0;
1190
- const contInto = next.pitch - note.pitch > 0;
1191
- const sameDir = note.pitch !== prev.pitch && next.pitch !== note.pitch && up === contInto;
1192
- if (prevMember && nextMember && !member && isStep(note.pitch, prev.pitch) && isStep(next.pitch, note.pitch) && sameDir) {
1193
- labels.push({ kind: "passing" });
1194
- handled = true;
1195
- }
1196
- }
1197
- if (!handled && chord && prev && next && !member && prev.pitch === next.pitch && _chunkUADYAYATcjs.isChordMember.call(void 0, prev.pitch, chordAtBeat(prev.startBeat)) && isStep(note.pitch, prev.pitch)) {
1198
- labels.push({ kind: "neighbor" });
1199
- handled = true;
1200
- }
1201
- if (!handled && chord && next && !member && note.pitch === next.pitch) {
1202
- const nextChord = chordAtBeat(next.startBeat);
1203
- if (_chunkUADYAYATcjs.isChordMember.call(void 0, next.pitch, nextChord)) {
1204
- labels.push({ kind: "anticipation" });
1205
- handled = true;
1206
- }
1207
- }
1208
- if (!handled && chord && prev && next && !member) {
1209
- const prevMember = _chunkUADYAYATcjs.isChordMember.call(void 0, prev.pitch, chordAtBeat(prev.startBeat));
1210
- const nextMember = _chunkUADYAYATcjs.isChordMember.call(void 0, next.pitch, chordAtBeat(next.startBeat));
1211
- const leapFromPrev = Math.abs(note.pitch - prev.pitch) >= LEAP_SEMITONES;
1212
- const stepToNext = isStep(next.pitch, note.pitch);
1213
- const opposite = note.pitch - prev.pitch > 0 !== next.pitch - note.pitch > 0;
1214
- if (prevMember && nextMember && leapFromPrev && stepToNext && opposite) {
1215
- labels.push({ kind: "appoggiatura", resolveTo: next.pitch });
1216
- handled = true;
1217
- }
1218
- }
1219
- if (!handled && chord && prev && next && !member) {
1220
- const prevMember = _chunkUADYAYATcjs.isChordMember.call(void 0, prev.pitch, chordAtBeat(prev.startBeat));
1221
- const nextMember = _chunkUADYAYATcjs.isChordMember.call(void 0, next.pitch, chordAtBeat(next.startBeat));
1222
- const stepFromPrev = isStep(note.pitch, prev.pitch);
1223
- const leapToNext = Math.abs(next.pitch - note.pitch) >= LEAP_SEMITONES;
1224
- const opposite = note.pitch - prev.pitch > 0 !== next.pitch - note.pitch > 0;
1225
- if (prevMember && nextMember && stepFromPrev && leapToNext && opposite) {
1226
- labels.push({ kind: "escape" });
1227
- handled = true;
1228
- }
1229
- }
1230
- if (!handled && chord && !member) {
1231
- const ic = _chunkUADYAYATcjs.intervalAboveRoot.call(void 0, note.pitch, chord);
1232
- const degree = tensionDegree(ic);
1233
- const isTension = ic === 2 || ic === 5 || ic === 9;
1234
- const avoid = ic === 5 && chord.intervals.includes(4) && !chord.intervals.includes(5) || ic === 11 && chord.intervals.includes(4) && chord.intervals.includes(10);
1235
- if (avoid) {
1236
- labels.push({ kind: "avoid" });
1237
- const resolveTo = nearestChordTone(note.pitch, chord, STEP_RESOLUTION);
1238
- if (resolveTo !== void 0) {
1239
- labels.push({ kind: "needsResolution", resolveTo });
1240
- }
1241
- } else if (isTension && degree !== void 0) {
1242
- labels.push({ kind: "tension", degree });
1243
- } else {
1244
- const resolveTo = nearestChordTone(note.pitch, chord, NEAREST_CHORD_TONE_REACH);
1245
- labels.push(
1246
- resolveTo === void 0 ? { kind: "avoid" } : { kind: "needsResolution", resolveTo }
1247
- );
1248
- }
1249
- }
1250
- if (next && _chunkUADYAYATcjs.isLeadingToneResolution.call(void 0, note.pitch, next.pitch, keyAt(next.startBeat))) {
1251
- labels.push({ kind: "leadingTone", resolveTo: next.pitch });
1252
- }
1253
- const analyzed = {
1254
- noteId: _nullishCoalesce(note.id, () => ( i)),
1255
- pitch: note.pitch,
1256
- startBeat: note.startBeat,
1257
- durationBeat: note.durationBeat,
1258
- labels,
1259
- rationale: describe(labels)
1260
- };
1261
- if (note.originalIndex !== void 0) {
1262
- analyzed.originalIndex = note.originalIndex;
1263
- }
1264
- result.push(analyzed);
1265
- }
1266
- return result;
1267
- }
1268
- function describe(labels) {
1269
- const primary = labels[0];
1270
- if (!primary) {
1271
- return "Unclassified note";
1272
- }
1273
- switch (primary.kind) {
1274
- case "chordTone":
1275
- return `Chord ${primary.role}`;
1276
- case "tension":
1277
- return `${primary.degree}th tension`;
1278
- case "avoid":
1279
- return "Avoid note";
1280
- case "passing":
1281
- return "Passing tone between chord tones";
1282
- case "neighbor":
1283
- return "Neighbor tone";
1284
- case "suspension":
1285
- return `Suspension (${primary.type})`;
1286
- case "appoggiatura":
1287
- return "Appoggiatura leaning on the harmony, resolving by step";
1288
- case "anticipation":
1289
- return "Anticipation of the next chord";
1290
- case "escape":
1291
- return "Escape tone";
1292
- case "needsResolution":
1293
- return "Unresolved dissonance";
1294
- case "leadingTone":
1295
- return "Leading tone resolving to the tonic";
1296
- }
1297
- }
1298
-
1299
- // src/analyze/arrange/internal.ts
1300
- var EPS5 = 1e-9;
1301
- var MAX_LANE_LEAP = 24;
1302
- function covers(note, beat) {
1303
- return beat >= note.startBeat - EPS5 && beat < note.endBeat - EPS5;
1304
- }
1305
- function isPercussion(role) {
1306
- return role === "drums";
1307
- }
1308
- var DEFAULT_PROFILE = "pop";
1309
- var SAFETY_PROFILES = Object.keys(_chunkUADYAYATcjs.PROFILE_WEIGHTS);
1310
- function arrangementProfile(profile) {
1311
- return _chunkQ2WGKJNNcjs.assertOneOf.call(void 0, _nullishCoalesce(profile, () => ( DEFAULT_PROFILE)), SAFETY_PROFILES, "arrangement profile");
1312
- }
1313
- function assertTrackNotes(tracks, options) {
1314
- let noteCount = 0;
1315
- for (let index = 0; index < tracks.length; index += 1) {
1316
- const notes = _optionalChain([tracks, 'access', _41 => _41[index], 'optionalAccess', _42 => _42.notes]);
1317
- if (!Array.isArray(notes)) {
1318
- throw new (0, _chunkQ2WGKJNNcjs.InvalidInputError)(
1319
- `tracks[${index}].notes must be an array; received ${typeof notes}`
1320
- );
1321
- }
1322
- _chunkQ2WGKJNNcjs.assertNoteEvents.call(void 0, notes, `tracks[${index}].notes`, options);
1323
- noteCount += notes.length;
1324
- }
1325
- return noteCount;
1326
- }
1327
- function harmonyTrackSet(harmonyTracks, trackCount, name = "harmonyTracks") {
1328
- if (harmonyTracks === void 0) {
1329
- return void 0;
1330
- }
1331
- const only = /* @__PURE__ */ new Set();
1332
- for (let index = 0; index < harmonyTracks.length; index += 1) {
1333
- const value = harmonyTracks[index];
1334
- if (value === void 0 || !Number.isFinite(value)) {
1335
- throw new (0, _chunkQ2WGKJNNcjs.InvalidInputError)(`${name}[${index}] must be a track index; received ${value}`);
1336
- }
1337
- const track = Math.trunc(value);
1338
- if (track < 0 || track >= trackCount) {
1339
- throw new (0, _chunkQ2WGKJNNcjs.InvalidInputError)(
1340
- `${name}[${index}] ${track} is outside the arrangement's ${trackCount} tracks`
1341
- );
1342
- }
1343
- only.add(track);
1344
- }
1345
- return only;
1346
- }
1347
- function poolNotes(tracks, only) {
1348
- const notes = [];
1349
- for (let index = 0; index < tracks.length; index += 1) {
1350
- const track = tracks[index];
1351
- if (track === void 0 || isPercussion(track.role)) {
1352
- continue;
1353
- }
1354
- if (only !== void 0 && !only.has(index)) {
1355
- continue;
1356
- }
1357
- for (const note of track.notes) {
1358
- if (note.durationBeat > 0) {
1359
- notes.push(note);
1360
- }
1361
- }
1362
- }
1363
- return notes;
1364
- }
1365
- function splitIntoSubVoices(ordered) {
1366
- const lanes = [];
1367
- const attachNearest = (note, excluded = /* @__PURE__ */ new Set()) => {
1368
- let best = -1;
1369
- let bestDistance = Number.POSITIVE_INFINITY;
1370
- let bestEnd = Number.NEGATIVE_INFINITY;
1371
- for (let lane = 0; lane < lanes.length; lane += 1) {
1372
- if (excluded.has(lane)) {
1373
- continue;
1374
- }
1375
- const last = _optionalChain([lanes, 'access', _43 => _43[lane], 'optionalAccess', _44 => _44[(_nullishCoalesce(_optionalChain([lanes, 'access', _45 => _45[lane], 'optionalAccess', _46 => _46.length]), () => ( 0))) - 1]]);
1376
- if (last === void 0) {
1377
- continue;
1378
- }
1379
- const end = last.startBeat + last.durationBeat;
1380
- if (end > note.startBeat + EPS5) {
1381
- continue;
1382
- }
1383
- const distance = Math.abs(last.pitch - note.pitch);
1384
- if (distance > MAX_LANE_LEAP) {
1385
- continue;
1386
- }
1387
- if (distance < bestDistance || distance === bestDistance && end > bestEnd) {
1388
- best = lane;
1389
- bestDistance = distance;
1390
- bestEnd = end;
1391
- }
1392
- }
1393
- if (best >= 0) {
1394
- _optionalChain([lanes, 'access', _47 => _47[best], 'optionalAccess', _48 => _48.push, 'call', _49 => _49(note)]);
1395
- return true;
1396
- }
1397
- return false;
1398
- };
1399
- for (let start = 0; start < ordered.length; ) {
1400
- const onset = _nullishCoalesce(_optionalChain([ordered, 'access', _50 => _50[start], 'optionalAccess', _51 => _51.startBeat]), () => ( 0));
1401
- let end = start + 1;
1402
- while (end < ordered.length && Math.abs((_nullishCoalesce(_optionalChain([ordered, 'access', _52 => _52[end], 'optionalAccess', _53 => _53.startBeat]), () => ( 0))) - onset) <= EPS5) {
1403
- end += 1;
1404
- }
1405
- const block = ordered.slice(start, end);
1406
- if (block.length > 1) {
1407
- const freeLanes = lanes.map((lane, index) => ({ index, last: lane[lane.length - 1] })).filter(
1408
- (entry) => entry.last !== void 0 && entry.last.startBeat + entry.last.durationBeat <= onset + EPS5
1409
- ).sort((a, b) => a.last.pitch - b.last.pitch);
1410
- const paired = /* @__PURE__ */ new Set();
1411
- for (let index = 0; index < block.length; index += 1) {
1412
- const note = block[index];
1413
- const lane = freeLanes[index];
1414
- if (note !== void 0 && lane !== void 0 && Math.abs(lane.last.pitch - note.pitch) <= MAX_LANE_LEAP) {
1415
- _optionalChain([lanes, 'access', _54 => _54[lane.index], 'optionalAccess', _55 => _55.push, 'call', _56 => _56(note)]);
1416
- paired.add(lane.index);
1417
- } else if (note !== void 0 && !attachNearest(note, paired)) {
1418
- lanes.push([note]);
1419
- }
1420
- }
1421
- } else {
1422
- const note = block[0];
1423
- if (note !== void 0 && !attachNearest(note)) {
1424
- lanes.push([note]);
1425
- }
1426
- }
1427
- start = end;
1428
- }
1429
- return lanes;
1430
- }
1431
- function lastStartingAtOrBefore(sounding, beat) {
1432
- let low = 0;
1433
- let high = sounding.length;
1434
- while (low < high) {
1435
- const middle = Math.floor((low + high) / 2);
1436
- if ((_nullishCoalesce(_optionalChain([sounding, 'access', _57 => _57[middle], 'optionalAccess', _58 => _58.startBeat]), () => ( Number.POSITIVE_INFINITY))) <= beat + EPS5) {
1437
- low = middle + 1;
1438
- } else {
1439
- high = middle;
1440
- }
1441
- }
1442
- return low - 1;
1443
- }
1444
- function prepareTracks(tracks) {
1445
- let nextId = 0;
1446
- return tracks.map((track, index) => {
1447
- 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 }));
1448
- const voices = splitIntoSubVoices(ordered).map((voice) => {
1449
- const sounding = voice.map((note, i) => {
1450
- const prev = i > 0 ? voice[i - 1] : void 0;
1451
- const prevEnd = prev === void 0 ? void 0 : prev.startBeat + prev.durationBeat;
1452
- const contiguous = prevEnd !== void 0 && Math.abs(prevEnd - note.startBeat) <= EPS5;
1453
- return {
1454
- pitch: note.pitch,
1455
- prevPitch: contiguous ? _optionalChain([prev, 'optionalAccess', _59 => _59.pitch]) : void 0,
1456
- startBeat: note.startBeat,
1457
- endBeat: note.startBeat + note.durationBeat
1458
- };
1459
- });
1460
- return {
1461
- voice,
1462
- sounding,
1463
- at: (beat) => {
1464
- const candidate = sounding[lastStartingAtOrBefore(sounding, beat)];
1465
- return candidate !== void 0 && covers(candidate, beat) ? candidate : void 0;
1466
- }
1467
- };
1468
- });
1469
- return {
1470
- name: _nullishCoalesce(track.name, () => ( `track ${index + 1}`)),
1471
- role: _nullishCoalesce(track.role, () => ( "other")),
1472
- trackIndex: index,
1473
- voices
1474
- };
1475
- });
1476
- }
1477
-
1478
- // src/analyze/arrange/tracks.ts
1479
- function withGivenKeys(result, given) {
1480
- if (given === void 0) {
1481
- return result;
1482
- }
1483
- return { ...result, keys: given, prevailingKey: _nullishCoalesce(prevailingKeyOf(given), () => ( result.prevailingKey)) };
1484
- }
1485
- function callerTimeline(timeline, key, given, pooled, meters, budget) {
1486
- const totalBeats = timeline.segments.reduce((end, segment) => Math.max(end, segment.endBeat), 0);
1487
- const keys = _nullishCoalesce(given, () => ( attachPivots(
1488
- key !== void 0 ? [{ startBeat: 0, endBeat: totalBeats, key, confidence: 1 }] : keyTimelineFromNotes(pooled, { meters, totalBeats, budget }),
1489
- timeline.segments
1490
- )));
1491
- return {
1492
- timeline,
1493
- keys,
1494
- prevailingKey: _nullishCoalesce(prevailingKeyOf(keys), () => ( _chunkUADYAYATcjs.majorKey.call(void 0, 0))),
1495
- segmentConfidence: timeline.segments.map(() => 0)
1496
- };
1497
- }
1498
- function soundingVoicesAt(prepared, beat) {
1499
- const out = [];
1500
- for (let t = 0; t < prepared.length; t += 1) {
1501
- const track = prepared[t];
1502
- if (!track || isPercussion(track.role)) {
1503
- continue;
1504
- }
1505
- for (let v = 0; v < track.voices.length; v += 1) {
1506
- const note = _optionalChain([track, 'access', _60 => _60.voices, 'access', _61 => _61[v], 'optionalAccess', _62 => _62.at, 'call', _63 => _63(beat)]);
1507
- if (note === void 0) {
1508
- continue;
1509
- }
1510
- const snap = { pitch: note.pitch };
1511
- const previous = Math.abs(note.startBeat - beat) <= EPS5 ? note.prevPitch : note.pitch;
1512
- if (previous !== void 0) {
1513
- snap.prevPitch = previous;
1514
- }
1515
- out.push({ track: t, voice: v, snapshot: snap });
1516
- }
1517
- }
1518
- return out;
1519
- }
1520
- function otherVoicesSounding(prepared, excludeTrack, excludeVoice, beat, cache) {
1521
- const sounding = _nullishCoalesce(cache.get(beat), () => ( soundingVoicesAt(prepared, beat)));
1522
- cache.set(beat, sounding);
1523
- return sounding.filter(({ track, voice }) => track !== excludeTrack || voice !== excludeVoice).map(({ snapshot }) => snapshot);
1524
- }
1525
- function firstSegmentAfter(timeline, beat) {
1526
- let low = 0;
1527
- let high = timeline.segments.length;
1528
- while (low < high) {
1529
- const middle = Math.floor((low + high) / 2);
1530
- if ((_nullishCoalesce(_optionalChain([timeline, 'access', _64 => _64.segments, 'access', _65 => _65[middle], 'optionalAccess', _66 => _66.startBeat]), () => ( Number.POSITIVE_INFINITY))) <= beat + EPS5) {
1531
- low = middle + 1;
1532
- } else {
1533
- high = middle;
1534
- }
1535
- }
1536
- return low;
1537
- }
1538
- function evaluationBeats(note, timeline) {
1539
- const noteEnd = note.startBeat + note.durationBeat;
1540
- const beats = [note.startBeat];
1541
- for (let i = firstSegmentAfter(timeline, note.startBeat); i < timeline.segments.length; i += 1) {
1542
- const start = _optionalChain([timeline, 'access', _67 => _67.segments, 'access', _68 => _68[i], 'optionalAccess', _69 => _69.startBeat]);
1543
- if (start === void 0 || start >= noteEnd - EPS5) {
1544
- break;
1545
- }
1546
- beats.push(start);
1547
- }
1548
- return beats;
1549
- }
1550
- function analyzeArrangement(tracks, opts = {}) {
1551
- return analyzeArrangementWith(tracks, opts).analysis;
1552
- }
1553
- function analyzeArrangementWith(tracks, opts = {}, previous, dirty) {
1554
- const meters = _chunkK4ZJUGF4cjs.resolveMeters.call(void 0, opts, "arrangement meters");
1555
- const budget = opts.budget;
1556
- const noteOptions = { allowNonPositiveDuration: true, budget };
1557
- if (opts.pickupBeats !== void 0) {
1558
- _chunkQ2WGKJNNcjs.assertRange.call(void 0, opts.pickupBeats, 0, Number.MAX_SAFE_INTEGER, "arrangement pickupBeats");
1559
- noteOptions.minStartBeat = -opts.pickupBeats;
1560
- }
1561
- _chunkQ2WGKJNNcjs.assertGenerationBudget.call(void 0, tracks.length, "arrangement tracks", budget);
1562
- const noteCount = assertTrackNotes(tracks, noteOptions);
1563
- _chunkQ2WGKJNNcjs.assertGenerationBudget.call(void 0, noteCount, "arrangement notes", budget);
1564
- const profile = arrangementProfile(opts.profile);
1565
- const minSeverity = _nullishCoalesce(opts.minSeverity, () => ( 1)) /* Warning */;
1566
- const harmonyTracks = harmonyTrackSet(opts.harmonyTracks, tracks.length);
1567
- const prepared = prepareTracks(tracks);
1568
- const voiceCount = prepared.reduce((sum, track) => sum + track.voices.length, 0);
1569
- _chunkQ2WGKJNNcjs.assertGenerationBudget.call(void 0, noteCount * voiceCount, "arrangement note-voice comparisons", budget);
1570
- const pooled = poolNotes(tracks, harmonyTracks);
1571
- let evidence;
1572
- let inferred;
1573
- if (opts.timeline === void 0) {
1574
- const run = analyzeTimeline(
1575
- pooled,
1576
- {
1577
- key: opts.key,
1578
- meters,
1579
- pickupBeats: opts.pickupBeats,
1580
- harmonicRhythm: opts.harmonicRhythm,
1581
- budget
1582
- },
1583
- previous,
1584
- dirty
1585
- );
1586
- evidence = run.evidence;
1587
- inferred = withGivenKeys(run.result, opts.keys);
1588
- } else {
1589
- inferred = callerTimeline(opts.timeline, opts.key, opts.keys, pooled, meters, budget);
1590
- }
1591
- const { timeline, keys, prevailingKey, segmentConfidence } = inferred;
1592
- const keyAt = keyLookup(keys, prevailingKey);
1593
- const cadences = detectCadences(timeline, keyAt);
1594
- const soundingCache = /* @__PURE__ */ new Map();
1595
- const trackAnalyses = [];
1596
- const conflicts = [];
1597
- for (let t = 0; t < prepared.length; t += 1) {
1598
- const track = prepared[t];
1599
- if (!track) {
1600
- continue;
1601
- }
1602
- if (isPercussion(track.role)) {
1603
- trackAnalyses.push({ name: track.name, role: track.role, notes: [] });
1604
- continue;
1605
- }
1606
- const notes = [];
1607
- for (let v = 0; v < track.voices.length; v += 1) {
1608
- const subVoice = track.voices[v];
1609
- if (!subVoice) {
1610
- continue;
1611
- }
1612
- const analyzed = analyzeVoice(
1613
- subVoice.voice,
1614
- timeline.at,
1615
- keyAt,
1616
- (beat) => otherVoicesSounding(prepared, t, v, beat, soundingCache)
1617
- );
1618
- for (const note of analyzed) {
1619
- notes.push({ ...note, trackIndex: track.trackIndex });
1620
- }
1621
- const labelsById = new Map(analyzed.map((note) => [note.noteId, note.labels]));
1622
- for (let noteIndex = 0; noteIndex < subVoice.voice.length; noteIndex += 1) {
1623
- const note = subVoice.voice[noteIndex];
1624
- const preparedNote = subVoice.sounding[noteIndex];
1625
- if (note === void 0 || preparedNote === void 0) {
1626
- continue;
1627
- }
1628
- for (const beat of evaluationBeats(note, timeline)) {
1629
- const atOnset = Math.abs(beat - note.startBeat) <= EPS5;
1630
- const safetyQuery = {
1631
- profile,
1632
- candidatePitch: note.pitch,
1633
- prevPitch: atOnset ? preparedNote.prevPitch : note.pitch,
1634
- chord: timeline.at(beat),
1635
- key: keyAt(beat),
1636
- otherVoices: otherVoicesSounding(prepared, t, v, beat, soundingCache),
1637
- strongBeat: _chunkK4ZJUGF4cjs.isStrongBeat.call(void 0, beat, meters)
1638
- };
1639
- let result = _chunkUADYAYATcjs.evaluateSafety.call(void 0, safetyQuery, { suggestions: false });
1640
- if (result.safety !== 0 /* Safe */ && result.safety >= minSeverity) {
1641
- result = _chunkUADYAYATcjs.evaluateSafety.call(void 0, safetyQuery);
1642
- const conflict = {
1643
- beat,
1644
- trackName: track.name,
1645
- trackIndex: track.trackIndex,
1646
- noteId: note.id,
1647
- pitch: note.pitch,
1648
- labels: _nullishCoalesce(labelsById.get(note.id), () => ( [])),
1649
- safety: result.safety,
1650
- reasons: result.reasons
1651
- };
1652
- if (note.originalIndex !== void 0) {
1653
- conflict.originalIndex = note.originalIndex;
1654
- }
1655
- if (result.rationale !== void 0) {
1656
- conflict.rationale = result.rationale;
1657
- }
1658
- if (result.resolveTo !== void 0) {
1659
- conflict.resolveTo = result.resolveTo;
1660
- }
1661
- if (result.suggestions !== void 0) {
1662
- conflict.suggestions = [...result.suggestions];
1663
- }
1664
- conflicts.push(conflict);
1665
- }
1666
- }
1667
- }
1668
- }
1669
- notes.sort((a, b) => a.noteId - b.noteId);
1670
- trackAnalyses.push({ name: track.name, role: track.role, notes });
1671
- }
1672
- conflicts.sort((a, b) => b.safety - a.safety || a.beat - b.beat);
1673
- const analysis = {
1674
- keys,
1675
- prevailingKey,
1676
- timeline,
1677
- segmentConfidence,
1678
- cadences,
1679
- tracks: trackAnalyses,
1680
- conflicts
1681
- };
1682
- return evidence === void 0 ? { analysis } : { analysis, evidence };
1683
- }
1684
-
1685
- // src/analyze/arrange/session.ts
1686
- function noteId(note) {
1687
- return `${note.pitch}|${note.startBeat}|${note.durationBeat}|${_nullishCoalesce(note.velocity, () => ( ""))}`;
1688
- }
1689
- function sameOrder(before, after) {
1690
- if (before.length !== after.length) {
1691
- return false;
1692
- }
1693
- for (let index = 0; index < before.length; index += 1) {
1694
- const previous = before[index];
1695
- const current = after[index];
1696
- if (previous === void 0 || current === void 0 || noteId(previous) !== noteId(current)) {
1697
- return false;
1698
- }
1699
- }
1700
- return true;
1701
- }
1702
- function editedSpan(before, after) {
1703
- const counts = /* @__PURE__ */ new Map();
1704
- const tally = (notes, sign) => {
1705
- for (const note of notes) {
1706
- const id = noteId(note);
1707
- const entry = counts.get(id);
1708
- if (entry === void 0) {
1709
- counts.set(id, {
1710
- count: sign,
1711
- startBeat: note.startBeat,
1712
- endBeat: note.startBeat + note.durationBeat
1713
- });
1714
- } else {
1715
- entry.count += sign;
1716
- }
1717
- }
1718
- };
1719
- tally(before, 1);
1720
- tally(after, -1);
1721
- let startBeat = Number.POSITIVE_INFINITY;
1722
- let endBeat = Number.NEGATIVE_INFINITY;
1723
- for (const entry of counts.values()) {
1724
- if (entry.count === 0) {
1725
- continue;
1726
- }
1727
- startBeat = Math.min(startBeat, entry.startBeat);
1728
- endBeat = Math.max(endBeat, entry.endBeat, entry.startBeat);
1729
- }
1730
- return startBeat <= endBeat ? { startBeat, endBeat } : null;
1731
- }
1732
- function unionSpan(a, b) {
1733
- if (a === null) {
1734
- return b;
1735
- }
1736
- if (b === null) {
1737
- return a;
1738
- }
1739
- return {
1740
- startBeat: Math.min(a.startBeat, b.startBeat),
1741
- endBeat: Math.max(a.endBeat, b.endBeat)
1742
- };
1743
- }
1744
- function sessionOf(tracks, opts, analysis, evidence) {
1745
- return {
1746
- tracks,
1747
- analysis,
1748
- update(edits) {
1749
- _chunkQ2WGKJNNcjs.assertGenerationBudget.call(void 0, edits.length, "arrangement edits", opts.budget);
1750
- const next = [...tracks];
1751
- let edited = null;
1752
- let reordered = false;
1753
- for (let index = 0; index < edits.length; index += 1) {
1754
- const edit = edits[index];
1755
- if (edit === void 0) {
1756
- continue;
1757
- }
1758
- const target = next[edit.trackIndex];
1759
- if (target === void 0) {
1760
- throw new (0, _chunkQ2WGKJNNcjs.InvalidInputError)(
1761
- `edits[${index}].trackIndex ${edit.trackIndex} is outside the session's ${tracks.length} tracks`
1762
- );
1763
- }
1764
- if (!Array.isArray(edit.notes)) {
1765
- throw new (0, _chunkQ2WGKJNNcjs.InvalidInputError)(
1766
- `edits[${index}].notes must be an array; received ${typeof edit.notes}`
1767
- );
1768
- }
1769
- _chunkQ2WGKJNNcjs.assertNoteEvents.call(void 0, edit.notes, `edits[${index}].notes`, {
1770
- allowNonPositiveDuration: true,
1771
- budget: opts.budget
1772
- });
1773
- edited = unionSpan(edited, editedSpan(target.notes, edit.notes));
1774
- reordered = reordered || !sameOrder(target.notes, edit.notes);
1775
- next[edit.trackIndex] = { ...target, notes: edit.notes };
1776
- }
1777
- if (edited === null && !reordered) {
1778
- return sessionOf(next, opts, analysis, evidence);
1779
- }
1780
- const dirty = evidence === void 0 ? void 0 : edited === null ? { from: 0, to: 0 } : dirtySlotsFor(evidence, edited.startBeat, edited.endBeat);
1781
- const run = analyzeArrangementWith(next, opts, evidence, dirty);
1782
- return sessionOf(next, opts, run.analysis, run.evidence);
1783
- }
1784
- };
1785
- }
1786
- function createArrangementSession(tracks, opts = {}) {
1787
- const run = analyzeArrangementWith(tracks, opts);
1788
- return sessionOf([...tracks], opts, run.analysis, run.evidence);
1789
- }
1790
-
1791
- // src/analyze/arrange/tension.ts
1792
- var FUNCTION_TENSION = {
1793
- tonic: 0,
1794
- subdominant: 0.5,
1795
- dominant: 1
1796
- };
1797
- var FUNCTION_WEIGHT = 0.5;
1798
- var DISSONANCE_WEIGHT = 0.35;
1799
- var SPAN_WEIGHT = 0.15;
1800
- var SPAN_SATURATION = 24;
1801
- function clamp01(value) {
1802
- return Math.min(1, Math.max(0, value));
1803
- }
1804
- function tensionCurve(tracks, opts = {}) {
1805
- const meters = _chunkK4ZJUGF4cjs.resolveMeters.call(void 0, opts, "arrangement meters");
1806
- const budget = opts.budget;
1807
- const noteOptions = { allowNonPositiveDuration: true, budget };
1808
- if (opts.pickupBeats !== void 0) {
1809
- _chunkQ2WGKJNNcjs.assertRange.call(void 0, opts.pickupBeats, 0, Number.MAX_SAFE_INTEGER, "arrangement pickupBeats");
1810
- noteOptions.minStartBeat = -opts.pickupBeats;
1811
- }
1812
- _chunkQ2WGKJNNcjs.assertGenerationBudget.call(void 0, tracks.length, "arrangement tracks", budget);
1813
- const noteCount = assertTrackNotes(tracks, noteOptions);
1814
- _chunkQ2WGKJNNcjs.assertGenerationBudget.call(void 0, noteCount, "arrangement notes", budget);
1815
- const profile = arrangementProfile(opts.profile);
1816
- const step = _nullishCoalesce(opts.step, () => ( 1));
1817
- _chunkQ2WGKJNNcjs.assertRange.call(void 0, step, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "tension sampling step");
1818
- const harmonyTracks = harmonyTrackSet(opts.harmonyTracks, tracks.length);
1819
- const pooled = poolNotes(tracks, harmonyTracks);
1820
- const all = poolNotes(tracks);
1821
- const totalBeats = all.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
1822
- const firstOnset = all.reduce((first, n) => Math.min(first, n.startBeat), 0);
1823
- const sampleStart = Math.min(0, Math.floor(firstOnset / step + EPS5) * step);
1824
- const suppliedKeys = opts.timeline === void 0 ? void 0 : _nullishCoalesce(opts.keys, () => ( (opts.key !== void 0 ? [{ startBeat: 0, endBeat: totalBeats, key: opts.key, confidence: 1 }] : keyTimelineFromNotes(pooled, { meters, totalBeats, budget }))));
1825
- const { timeline, keys, prevailingKey } = opts.timeline === void 0 || suppliedKeys === void 0 ? chordTimelineFromNotes(pooled, {
1826
- key: opts.key,
1827
- meters,
1828
- pickupBeats: opts.pickupBeats,
1829
- harmonicRhythm: opts.harmonicRhythm,
1830
- budget
1831
- }) : {
1832
- timeline: opts.timeline,
1833
- keys: suppliedKeys,
1834
- prevailingKey: _nullishCoalesce(prevailingKeyOf(suppliedKeys), () => ( _chunkUADYAYATcjs.majorKey.call(void 0, 0)))
1835
- };
1836
- const regions = _nullishCoalesce(opts.keys, () => ( keys));
1837
- const keyAt = keyLookup(regions, _nullishCoalesce(prevailingKeyOf(regions), () => ( prevailingKey)));
1838
- const prepared = prepareTracks(tracks);
1839
- const points = [];
1840
- const sampleCount = Math.max(0, Math.ceil((totalBeats - sampleStart) / step - EPS5));
1841
- _chunkQ2WGKJNNcjs.assertGenerationBudget.call(void 0, sampleCount, "tension samples", budget);
1842
- const voiceCount = prepared.reduce((sum, track) => sum + track.voices.length, 0);
1843
- _chunkQ2WGKJNNcjs.assertGenerationBudget.call(void 0,
1844
- sampleCount * Math.min(128, voiceCount) * voiceCount,
1845
- "tension sample-voice comparisons",
1846
- budget
1847
- );
1848
- for (let i = 0; i < sampleCount; i += 1) {
1849
- const beat = sampleStart + i * step;
1850
- points.push({
1851
- beat,
1852
- tension: sampleTension(prepared, timeline, keyAt(beat), meters, profile, beat)
1853
- });
1854
- }
1855
- return points;
1856
- }
1857
- function tensionCurveFrom(tracks, analysis, opts = {}) {
1858
- return tensionCurve(tracks, {
1859
- ...opts,
1860
- timeline: analysis.timeline,
1861
- keys: analysis.keys
1862
- });
1863
- }
1864
- function allSounding(prepared, beat) {
1865
- const out = [];
1866
- for (let t = 0; t < prepared.length; t += 1) {
1867
- const track = prepared[t];
1868
- if (!track || isPercussion(track.role)) {
1869
- continue;
1870
- }
1871
- for (const subVoice of track.voices) {
1872
- const note = subVoice.at(beat);
1873
- if (note !== void 0) {
1874
- out.push({ pitch: note.pitch, track: t });
1875
- }
1876
- }
1877
- }
1878
- return out;
1879
- }
1880
- function sampleTension(prepared, timeline, key, meters, profile, beat) {
1881
- const sounding = allSounding(prepared, beat);
1882
- if (sounding.length === 0) {
1883
- return 0;
1884
- }
1885
- const chord = timeline.at(beat);
1886
- const functionScore = chord ? FUNCTION_TENSION[_chunkOE6WK2Y2cjs.functionOf.call(void 0, chord, key)] : 0;
1887
- const chordPcs = chord ? new Set(_chunkUADYAYATcjs.chordPitchClasses.call(void 0, chord)) : null;
1888
- const strongBeat = _chunkK4ZJUGF4cjs.isStrongBeat.call(void 0, beat, meters);
1889
- let nonChord = 0;
1890
- let dissonant = 0;
1891
- const pitches = /* @__PURE__ */ new Map();
1892
- for (const voice of sounding) {
1893
- pitches.set(voice.pitch, (_nullishCoalesce(pitches.get(voice.pitch), () => ( 0))) + 1);
1894
- }
1895
- for (const [pitch, count] of pitches) {
1896
- if (chordPcs && !chordPcs.has(_chunkQ2WGKJNNcjs.pitchClassOf.call(void 0, pitch))) {
1897
- nonChord += count;
1898
- }
1899
- const others = [...pitches].flatMap(
1900
- ([otherPitch, otherCount]) => Array.from({ length: otherCount - (otherPitch === pitch ? 1 : 0) }, () => ({
1901
- pitch: otherPitch
1902
- }))
1903
- );
1904
- const result = _chunkUADYAYATcjs.evaluateSafety.call(void 0,
1905
- {
1906
- profile,
1907
- candidatePitch: pitch,
1908
- chord,
1909
- key,
1910
- otherVoices: others,
1911
- strongBeat
1912
- },
1913
- { suggestions: false }
1914
- );
1915
- if (result.safety === 2 /* Dissonant */) {
1916
- dissonant += count;
1917
- }
1918
- }
1919
- const dissonanceScore = Math.max(nonChord, dissonant) / sounding.length;
1920
- let low = Number.POSITIVE_INFINITY;
1921
- let high = Number.NEGATIVE_INFINITY;
1922
- for (const voice of sounding) {
1923
- low = Math.min(low, voice.pitch);
1924
- high = Math.max(high, voice.pitch);
1925
- }
1926
- const spanScore = Math.min(1, (high - low) / SPAN_SATURATION);
1927
- return clamp01(
1928
- FUNCTION_WEIGHT * functionScore + DISSONANCE_WEIGHT * dissonanceScore + SPAN_WEIGHT * spanScore
1929
- );
1930
- }
1931
-
1932
- // src/analyze/form/internal.ts
1933
- var EPS6 = 1e-9;
1934
- function clamp012(value) {
1935
- return Math.min(1, Math.max(0, value));
1936
- }
1937
- function floorMod(value, modulus) {
1938
- return (value % modulus + modulus) % modulus;
1939
- }
1940
- function firstSoundingBeat(notes, fallback = 0) {
1941
- let first = Number.POSITIVE_INFINITY;
1942
- for (const note of notes) {
1943
- first = Math.min(first, note.startBeat);
1944
- }
1945
- return Number.isFinite(first) ? first : fallback;
1946
- }
1947
- function lastBarOf(meter, spanStart, spanEnd) {
1948
- const firstBar = _chunkK4ZJUGF4cjs.barIndexAt.call(void 0, spanStart, meter);
1949
- const bar = _chunkK4ZJUGF4cjs.barIndexAt.call(void 0, Math.max(spanStart, spanEnd), meter);
1950
- if (bar > firstBar && _chunkK4ZJUGF4cjs.barPositionToBeat.call(void 0, { bar, beat: 0 }, meter) >= spanEnd - EPS6) {
1951
- return bar - 1;
1952
- }
1953
- return bar;
1954
- }
1955
- function sliceBars(notes, meter, spanStart, spanEnd, budget) {
1956
- const firstBar = _chunkK4ZJUGF4cjs.barIndexAt.call(void 0, spanStart, meter);
1957
- const lastBar = lastBarOf(meter, spanStart, spanEnd);
1958
- const barCount = lastBar - firstBar + 1;
1959
- _chunkQ2WGKJNNcjs.assertGenerationBudget.call(void 0, barCount, "form bars", budget);
1960
- const buckets = Array.from({ length: barCount }, () => []);
1961
- let memberships = 0;
1962
- for (const note of notes) {
1963
- const noteEnd = note.startBeat + note.durationBeat;
1964
- const from = Math.max(firstBar, _chunkK4ZJUGF4cjs.barIndexAt.call(void 0, note.startBeat, meter));
1965
- const to = Math.min(lastBar, _chunkK4ZJUGF4cjs.barIndexAt.call(void 0, Math.max(note.startBeat, noteEnd - EPS6), meter));
1966
- memberships += Math.max(0, to - from + 1);
1967
- _chunkQ2WGKJNNcjs.assertGenerationBudget.call(void 0, memberships, "form note-to-bar memberships", budget);
1968
- for (let bar = from; bar <= to; bar += 1) {
1969
- _optionalChain([buckets, 'access', _70 => _70[bar - firstBar], 'optionalAccess', _71 => _71.push, 'call', _72 => _72(note)]);
1970
- }
1971
- }
1972
- const slices = [];
1973
- for (let bar = firstBar; bar <= lastBar; bar += 1) {
1974
- const startBeat = _chunkK4ZJUGF4cjs.barPositionToBeat.call(void 0, { bar, beat: 0 }, meter);
1975
- const endBeat = _chunkK4ZJUGF4cjs.barPositionToBeat.call(void 0, { bar: bar + 1, beat: 0 }, meter);
1976
- const barNotes = _nullishCoalesce(buckets[bar - firstBar], () => ( []));
1977
- const { weights, totalWeight } = windowWeights(barNotes, startBeat, endBeat, meter);
1978
- slices.push({ index: bar, startBeat, endBeat, notes: barNotes, weights, totalWeight });
1979
- }
1980
- return slices;
1981
- }
1982
- function weightSimilarity(a, b) {
1983
- let dot = 0;
1984
- let normA = 0;
1985
- let normB = 0;
1986
- for (let i = 0; i < a.length; i += 1) {
1987
- const x = _nullishCoalesce(a[i], () => ( 0));
1988
- const y = _nullishCoalesce(b[i], () => ( 0));
1989
- dot += x * y;
1990
- normA += x * x;
1991
- normB += y * y;
1992
- }
1993
- if (normA <= EPS6 && normB <= EPS6) {
1994
- return 1;
1995
- }
1996
- if (normA <= EPS6 || normB <= EPS6) {
1997
- return 0;
1998
- }
1999
- return clamp012(dot / Math.sqrt(normA * normB));
2000
- }
2001
- function harmonicNovelty(slices, index) {
2002
- const current = slices[index];
2003
- if (current === void 0) {
2004
- return 0;
2005
- }
2006
- const previous = slices[index - 1];
2007
- if (previous === void 0) {
2008
- return current.index === 0 ? 1 : 0;
2009
- }
2010
- return 1 - weightSimilarity(previous.weights, current.weights);
2011
- }
2012
- function combineEvidence(strengths) {
2013
- let remaining = 1;
2014
- for (const strength of strengths) {
2015
- remaining *= 1 - clamp012(strength);
2016
- }
2017
- return clamp012(1 - remaining);
2018
- }
2019
-
2020
- // src/analyze/form/hypermeter.ts
2021
- var GROUP_CANDIDATES = [2, 3, 4, 6, 8];
2022
- var GROUP_PRIOR = /* @__PURE__ */ new Map([
2023
- [2, 0.9],
2024
- [3, 0.75],
2025
- [4, 1],
2026
- [6, 0.7],
2027
- [8, 0.85]
2028
- ]);
2029
- var CONTRAST_SHARE = 0.65;
2030
- var CADENCE_SHARE = 0.35;
2031
- var GROUP_FINAL_FIT = 1;
2032
- var ELIDED_FIT = 0.5;
2033
- var MIN_GROUPS = 2;
2034
- var MARGIN_FLOOR = 0.7;
2035
- function accentContrast(novelty, slices, groupBars, phase) {
2036
- let headSum = 0;
2037
- let headCount = 0;
2038
- let otherSum = 0;
2039
- let otherCount = 0;
2040
- for (let i = 0; i < slices.length; i += 1) {
2041
- const slice = slices[i];
2042
- if (slice === void 0) {
2043
- continue;
2044
- }
2045
- const value = _nullishCoalesce(novelty[i], () => ( 0));
2046
- if (floorMod(slice.index - phase, groupBars) === 0) {
2047
- headSum += value;
2048
- headCount += 1;
2049
- } else {
2050
- otherSum += value;
2051
- otherCount += 1;
2052
- }
2053
- }
2054
- if (headCount === 0 || otherCount === 0) {
2055
- return 0;
2056
- }
2057
- return headSum / headCount - otherSum / otherCount;
2058
- }
2059
- function cadenceFitOf(cadenceBars, groupBars, phase) {
2060
- if (cadenceBars.length === 0) {
2061
- return 0;
2062
- }
2063
- let fit = 0;
2064
- for (const bar of cadenceBars) {
2065
- const position = floorMod(bar - phase, groupBars);
2066
- if (position === groupBars - 1) {
2067
- fit += GROUP_FINAL_FIT;
2068
- } else if (position === 0) {
2069
- fit += ELIDED_FIT;
2070
- }
2071
- }
2072
- return clamp012(fit / cadenceBars.length);
2073
- }
2074
- function undecidedReading(slices) {
2075
- const groupBars = Math.max(1, slices.length);
2076
- const first = slices[0];
2077
- return {
2078
- groupBars,
2079
- downbeats: first === void 0 ? [] : [first.startBeat],
2080
- confidence: 0,
2081
- rationale: `Span of ${slices.length} bar(s) is too short to establish a grouping; read as one group`
2082
- };
2083
- }
2084
- function hypermeter(notes, meter, opts = {}) {
2085
- const meters = _nullishCoalesce(meter, () => ( _chunkK4ZJUGF4cjs.resolveMeters.call(void 0, {})));
2086
- _chunkQ2WGKJNNcjs.assertNoteEvents.call(void 0, notes, "hypermeter notes", {
2087
- allowNonPositiveDuration: true,
2088
- budget: opts.budget
2089
- });
2090
- const sounding = notes.filter((note) => note.durationBeat > 0);
2091
- const firstOnset = firstSoundingBeat(sounding);
2092
- const lastEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
2093
- const spanEnd = _nullishCoalesce(opts.totalBeats, () => ( lastEnd));
2094
- _chunkQ2WGKJNNcjs.assertRange.call(void 0, spanEnd, 0, Number.MAX_SAFE_INTEGER, "hypermeter totalBeats");
2095
- const spanStart = firstOnset < 0 ? gridOriginOf(firstOnset, _chunkK4ZJUGF4cjs.beatsPerBarAt.call(void 0, firstOnset, meters)).startBeat : firstOnset;
2096
- const slices = sliceBars(sounding, meters, spanStart, spanEnd, opts.budget);
2097
- const candidates = GROUP_CANDIDATES.filter(
2098
- (groupBars) => slices.length >= groupBars * MIN_GROUPS
2099
- );
2100
- if (candidates.length === 0) {
2101
- return undecidedReading(slices);
2102
- }
2103
- _chunkQ2WGKJNNcjs.assertGenerationBudget.call(void 0,
2104
- candidates.reduce((sum, groupBars) => sum + groupBars * slices.length, 0),
2105
- "hypermeter readings",
2106
- opts.budget
2107
- );
2108
- const novelty = slices.map((_, index) => harmonicNovelty(slices, index));
2109
- const cadenceBars = (_nullishCoalesce(opts.cadenceBeats, () => ( []))).map((beat) => _chunkK4ZJUGF4cjs.barIndexAt.call(void 0, beat, meters));
2110
- const readings = [];
2111
- for (const groupBars of candidates) {
2112
- for (let phase = 0; phase < groupBars; phase += 1) {
2113
- const contrast = accentContrast(novelty, slices, groupBars, phase);
2114
- const cadenceFit = cadenceFitOf(cadenceBars, groupBars, phase);
2115
- const evidence = clamp012(
2116
- cadenceBars.length === 0 ? contrast : CONTRAST_SHARE * contrast + CADENCE_SHARE * cadenceFit
2117
- );
2118
- readings.push({
2119
- groupBars,
2120
- phase,
2121
- contrast,
2122
- cadenceFit,
2123
- evidence,
2124
- score: evidence * (_nullishCoalesce(GROUP_PRIOR.get(groupBars), () => ( 0)))
2125
- });
2126
- }
2127
- }
2128
- readings.sort(
2129
- (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
2130
- );
2131
- const best = readings[0];
2132
- if (best === void 0) {
2133
- return undecidedReading(slices);
2134
- }
2135
- const runnerUp = readings.find((reading) => reading.groupBars !== best.groupBars);
2136
- const margin = runnerUp === void 0 || best.score <= EPS6 ? 1 : clamp012((best.score - runnerUp.score) / best.score);
2137
- const downbeats = slices.filter((slice) => slice.index >= 0 && floorMod(slice.index - best.phase, best.groupBars) === 0).map((slice) => slice.startBeat);
2138
- const cadencePart = cadenceBars.length === 0 ? "no cadences given" : `${Math.round(best.cadenceFit * 100)}% of cadences arriving at a group end`;
2139
- return {
2140
- groupBars: best.groupBars,
2141
- downbeats,
2142
- // Mostly the evidence, discounted when a rival grouping explains the
2143
- // accents nearly as well. The discount is the smaller term because the
2144
- // runner-up is nearly always a multiple or a divisor of the winner — four
2145
- // bars nest inside eight — and a reading of the same accents at another
2146
- // level is weaker counter-evidence than an unrelated rival would be.
2147
- confidence: clamp012(best.evidence * (MARGIN_FLOOR + (1 - MARGIN_FLOOR) * margin)),
2148
- rationale: `${best.groupBars}-bar groups from bar ${best.phase}: harmonic change ${Math.round(best.contrast * 100)}% stronger on group heads, ${cadencePart}`
2149
- };
2150
- }
2151
-
2152
- // src/analyze/melody/index.ts
2153
- var EPS7 = 1e-9;
2154
- var RHYTHM_GRID = 64;
2155
- var MIN_CELL_NOTES = 3;
2156
- var MAX_CELL_NOTES = 8;
2157
- var MIN_OCCURRENCES = 2;
2158
- var SEQUENCE_GAP = 0.05;
2159
- var INTERVAL_TOLERANCE = 6;
2160
- var PITCH_WEIGHT = 0.75;
2161
- var RHYTHM_WEIGHT = 0.25;
2162
- var DIRECTIONAL_SHARE = 0.75;
2163
- function phraseNotes(phrase) {
2164
- return "notes" in phrase ? phrase.notes : phrase;
2165
- }
2166
- function orderedNotes(notes, name, budget) {
2167
- _chunkQ2WGKJNNcjs.assertNoteEvents.call(void 0, notes, name, { allowNonPositiveDuration: true, budget });
2168
- return notes.filter((note) => note.durationBeat > 0).sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
2169
- }
2170
- function intervalsOf(notes) {
2171
- const out = [];
2172
- for (let i = 1; i < notes.length; i += 1) {
2173
- out.push((_nullishCoalesce(_optionalChain([notes, 'access', _73 => _73[i], 'optionalAccess', _74 => _74.pitch]), () => ( 0))) - (_nullishCoalesce(_optionalChain([notes, 'access', _75 => _75[i - 1], 'optionalAccess', _76 => _76.pitch]), () => ( 0))));
2174
- }
2175
- return out;
2176
- }
2177
- function onsetGaps(notes) {
2178
- const out = [];
2179
- for (let i = 1; i < notes.length; i += 1) {
2180
- out.push((_nullishCoalesce(_optionalChain([notes, 'access', _77 => _77[i], 'optionalAccess', _78 => _78.startBeat]), () => ( 0))) - (_nullishCoalesce(_optionalChain([notes, 'access', _79 => _79[i - 1], 'optionalAccess', _80 => _80.startBeat]), () => ( 0))));
2181
- }
2182
- return out;
2183
- }
2184
- function rhythmProfile(gaps) {
2185
- const unit = _nullishCoalesce(gaps[0], () => ( 0));
2186
- if (!(unit > EPS7)) {
2187
- return gaps.length === 0 ? [] : null;
2188
- }
2189
- return gaps.map((gap) => Math.round(gap / unit * RHYTHM_GRID) / RHYTHM_GRID);
2190
- }
2191
- function sameNumbers(a, b) {
2192
- if (a.length !== b.length) {
2193
- return false;
2194
- }
2195
- for (let i = 0; i < a.length; i += 1) {
2196
- if (Math.abs((_nullishCoalesce(a[i], () => ( 0))) - (_nullishCoalesce(b[i], () => ( 0)))) > EPS7) {
2197
- return false;
2198
- }
2199
- }
2200
- return true;
2201
- }
2202
- function roundTo(value, digits) {
2203
- const scale = 10 ** digits;
2204
- return Math.round(value * scale) / scale;
2205
- }
2206
- function endBeatOf(notes) {
2207
- let end = 0;
2208
- for (const note of notes) {
2209
- end = Math.max(end, note.startBeat + note.durationBeat);
2210
- }
2211
- return end;
2212
- }
2213
- function onsetSpan(notes) {
2214
- if (notes.length < 2) {
2215
- return 0;
2216
- }
2217
- return (_nullishCoalesce(_optionalChain([notes, 'access', _81 => _81[notes.length - 1], 'optionalAccess', _82 => _82.startBeat]), () => ( 0))) - (_nullishCoalesce(_optionalChain([notes, 'access', _83 => _83[0], 'optionalAccess', _84 => _84.startBeat]), () => ( 0)));
2218
- }
2219
- function spellingOf(key) {
2220
- if (key === void 0) {
2221
- return "sharp";
2222
- }
2223
- const spelled = _chunkUADYAYATcjs.spelledKeyOf.call(void 0, key);
2224
- return _chunkUADYAYATcjs.keySignatureFifths.call(void 0, spelled.tonic, spelled.key) < 0 ? "flat" : "sharp";
2225
- }
2226
- function intervalBetween(from, to, key) {
2227
- const spelling = spellingOf(key);
2228
- return _chunkQ2WGKJNNcjs.spelledInterval.call(void 0, _chunkQ2WGKJNNcjs.midiToNote.call(void 0, from, spelling), _chunkQ2WGKJNNcjs.midiToNote.call(void 0, to, spelling));
2229
- }
2230
- function intervalName(interval) {
2231
- return `${interval.descending && interval.semitones !== 0 ? "-" : ""}${interval.quality}${interval.number}`;
2232
- }
2233
- function ladderIndices(notes, key) {
2234
- const root = _chunkQ2WGKJNNcjs.pitchClassOf.call(void 0, key.rootPc);
2235
- const offsets = _chunkUADYAYATcjs.scaleTonesInDegreeOrder.call(void 0, key).map((pc) => _chunkQ2WGKJNNcjs.pitchClassOf.call(void 0, pc - root));
2236
- if (offsets.length === 0) {
2237
- return null;
2238
- }
2239
- const out = [];
2240
- for (const note of notes) {
2241
- const semitones = note.pitch - root;
2242
- const octave = Math.floor(semitones / 12);
2243
- const degree = offsets.indexOf(semitones - octave * 12);
2244
- if (degree < 0) {
2245
- return null;
2246
- }
2247
- out.push(octave * offsets.length + degree);
2248
- }
2249
- return out;
2250
- }
2251
- function ladderSteps(indices) {
2252
- const out = [];
2253
- for (let i = 1; i < indices.length; i += 1) {
2254
- out.push((_nullishCoalesce(indices[i], () => ( 0))) - (_nullishCoalesce(indices[i - 1], () => ( 0))));
2255
- }
2256
- return out;
2257
- }
2258
- function windowSignature(notes) {
2259
- const profile = rhythmProfile(onsetGaps(notes));
2260
- if (profile === null) {
2261
- return null;
2262
- }
2263
- return `${notes.length}|${intervalsOf(notes).join(",")}|${profile.join(",")}`;
2264
- }
2265
- function nonOverlapping(starts, length) {
2266
- const picked = [];
2267
- let lastEnd = Number.NEGATIVE_INFINITY;
2268
- for (const start of starts) {
2269
- if (start > lastEnd) {
2270
- picked.push(start);
2271
- lastEnd = start + length - 1;
2272
- }
2273
- }
2274
- return picked;
2275
- }
2276
- function subsumes(outer, inner) {
2277
- return inner.starts.every(
2278
- (start) => outer.starts.some((from) => start >= from && start + inner.length <= from + outer.length)
2279
- );
2280
- }
2281
- function listBeats(occurrences) {
2282
- const shown = occurrences.slice(0, 5).map((occurrence) => roundTo(occurrence.startBeat, 3));
2283
- return occurrences.length > shown.length ? `${shown.join(", ")}, and ${occurrences.length - shown.length} more` : shown.join(", ");
2284
- }
2285
- function motifFromGroup(group, notes) {
2286
- const first = _nullishCoalesce(group.starts[0], () => ( 0));
2287
- const prime = notes.slice(first, first + group.length).map((note) => ({ ...note }));
2288
- const primePitch = _nullishCoalesce(_optionalChain([prime, 'access', _85 => _85[0], 'optionalAccess', _86 => _86.pitch]), () => ( 0));
2289
- const primeSpan = onsetSpan(prime);
2290
- const occurrences = group.starts.map((start) => {
2291
- const statement = notes.slice(start, start + group.length);
2292
- const span = onsetSpan(statement);
2293
- return {
2294
- noteIndex: start,
2295
- startBeat: _nullishCoalesce(_optionalChain([statement, 'access', _87 => _87[0], 'optionalAccess', _88 => _88.startBeat]), () => ( 0)),
2296
- endBeat: endBeatOf(statement),
2297
- transpose: (_nullishCoalesce(_optionalChain([statement, 'access', _89 => _89[0], 'optionalAccess', _90 => _90.pitch]), () => ( 0))) - primePitch,
2298
- timeRatio: primeSpan > EPS7 ? span / primeSpan : 1
2299
- };
2300
- });
2301
- const shape = melodicContour(prime).shape;
2302
- const transposed = occurrences.some((occurrence) => occurrence.transpose !== 0);
2303
- const stretched = occurrences.some((occurrence) => Math.abs(occurrence.timeRatio - 1) > EPS7);
2304
- const detail = [
2305
- transposed ? "at more than one pitch level" : "at the same pitch level",
2306
- stretched ? "and in more than one set of note values" : ""
2307
- ].filter((part) => part !== "").join(" ");
2308
- return {
2309
- notes: prime,
2310
- intervals: intervalsOf(prime),
2311
- rhythm: _nullishCoalesce(rhythmProfile(onsetGaps(prime)), () => ( [])),
2312
- occurrences,
2313
- rationale: `${group.length}-note ${shape} cell stated ${occurrences.length} ${occurrences.length === 1 ? "time" : "times"} (beats ${listBeats(occurrences)}), ${detail}`
2314
- };
2315
- }
2316
- function extractMotifs(notes, opts = {}) {
2317
- const minNotes = _chunkQ2WGKJNNcjs.assertInteger.call(void 0, _nullishCoalesce(opts.minNotes, () => ( MIN_CELL_NOTES)), "motif minNotes", 2, 64);
2318
- const maxNotes = _chunkQ2WGKJNNcjs.assertInteger.call(void 0, _nullishCoalesce(opts.maxNotes, () => ( MAX_CELL_NOTES)), "motif maxNotes", 2, 64);
2319
- if (maxNotes < minNotes) {
2320
- throw new (0, _chunkQ2WGKJNNcjs.InvalidInputError)(
2321
- `motif maxNotes must be at least minNotes ${minNotes}; received ${maxNotes}`
2322
- );
2323
- }
2324
- const minOccurrences = _chunkQ2WGKJNNcjs.assertInteger.call(void 0,
2325
- _nullishCoalesce(opts.minOccurrences, () => ( MIN_OCCURRENCES)),
2326
- "motif minOccurrences",
2327
- 2
2328
- );
2329
- const sounding = orderedNotes(notes, "melody notes", opts.budget);
2330
- if (sounding.length < minNotes) {
2331
- return [];
2332
- }
2333
- _chunkQ2WGKJNNcjs.assertGenerationBudget.call(void 0, sounding.length * (maxNotes - minNotes + 1), "motif windows", opts.budget);
2334
- const groups = /* @__PURE__ */ new Map();
2335
- for (let length = minNotes; length <= maxNotes; length += 1) {
2336
- for (let start = 0; start + length <= sounding.length; start += 1) {
2337
- const signature = windowSignature(sounding.slice(start, start + length));
2338
- if (signature === null) {
2339
- continue;
2340
- }
2341
- const group = groups.get(signature);
2342
- if (group === void 0) {
2343
- groups.set(signature, { signature, length, starts: [start] });
2344
- } else {
2345
- group.starts.push(start);
2346
- }
2347
- }
2348
- }
2349
- const candidates = [...groups.values()].map((group) => ({ ...group, starts: nonOverlapping(group.starts, group.length) })).filter((group) => group.starts.length >= minOccurrences).sort(
2350
- (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)
2351
- );
2352
- const kept = [];
2353
- for (const candidate of candidates) {
2354
- const covered = kept.some(
2355
- (outer) => outer.length > candidate.length && outer.starts.length >= candidate.starts.length && subsumes(outer, candidate)
2356
- );
2357
- if (!covered) {
2358
- kept.push(candidate);
2359
- }
2360
- }
2361
- return kept.map((group) => motifFromGroup(group, sounding));
2362
- }
2363
- function motifFromNotes(notes) {
2364
- const sounding = orderedNotes(notes, "motif notes").map((note) => ({ ...note }));
2365
- return motifFromGroup(
2366
- { signature: _nullishCoalesce(windowSignature(sounding), () => ( "")), length: sounding.length, starts: [0] },
2367
- sounding
2368
- );
2369
- }
2370
- function pitchRelation(a, b) {
2371
- if (sameNumbers(a, b)) {
2372
- return "exact";
2373
- }
2374
- if (sameNumbers(
2375
- b,
2376
- a.map((step) => -step)
2377
- )) {
2378
- return "inversion";
2379
- }
2380
- const reversed = [...a].reverse();
2381
- if (sameNumbers(
2382
- b,
2383
- reversed.map((step) => -step)
2384
- )) {
2385
- return "retrograde";
2386
- }
2387
- if (sameNumbers(b, reversed)) {
2388
- return "retrogradeInversion";
2389
- }
2390
- return null;
2391
- }
2392
- function stretchPhrase(ratio) {
2393
- return `note values ${roundTo(ratio, 3)}x the model's`;
2394
- }
2395
- function relateMotifs(a, b, key) {
2396
- const model = a.notes;
2397
- const answer = b.notes;
2398
- if (model.length === 0 || model.length !== answer.length) {
2399
- return null;
2400
- }
2401
- const modelGaps = onsetGaps(model);
2402
- const answerGaps = onsetGaps(answer);
2403
- const modelProfile = rhythmProfile(modelGaps);
2404
- const answerProfile = rhythmProfile(answerGaps);
2405
- const retroProfile = rhythmProfile([...answerGaps].reverse());
2406
- if (modelProfile === null || answerProfile === null) {
2407
- return null;
2408
- }
2409
- const forward = sameNumbers(modelProfile, answerProfile);
2410
- const backward = retroProfile !== null && sameNumbers(modelProfile, retroProfile);
2411
- const modelSpan = onsetSpan(model);
2412
- const timeRatio = modelSpan > EPS7 ? onsetSpan(answer) / modelSpan : 1;
2413
- const semitones = (_nullishCoalesce(_optionalChain([answer, 'access', _91 => _91[0], 'optionalAccess', _92 => _92.pitch]), () => ( 0))) - (_nullishCoalesce(_optionalChain([model, 'access', _93 => _93[0], 'optionalAccess', _94 => _94.pitch]), () => ( 0)));
2414
- const interval = intervalBetween(_nullishCoalesce(_optionalChain([model, 'access', _95 => _95[0], 'optionalAccess', _96 => _96.pitch]), () => ( 0)), _nullishCoalesce(_optionalChain([answer, 'access', _97 => _97[0], 'optionalAccess', _98 => _98.pitch]), () => ( 0)), key);
2415
- const gap = (_nullishCoalesce(_optionalChain([answer, 'access', _99 => _99[0], 'optionalAccess', _100 => _100.startBeat]), () => ( 0))) - endBeatOf(model);
2416
- const sequence = gap >= -SEQUENCE_GAP && gap <= SEQUENCE_GAP;
2417
- const stretched = Math.abs(timeRatio - 1) > EPS7;
2418
- const relation = pitchRelation(intervalsOf(model), intervalsOf(answer));
2419
- const base = { sequence, semitones, interval, timeRatio };
2420
- if (forward && relation === "exact") {
2421
- if (semitones === 0) {
2422
- if (!stretched) {
2423
- return {
2424
- ...base,
2425
- kind: "repetition",
2426
- rationale: "Exact repetition: the same pitches in the same rhythm"
2427
- };
2428
- }
2429
- const augmented = timeRatio > 1;
2430
- return {
2431
- ...base,
2432
- kind: augmented ? "augmentation" : "diminution",
2433
- rationale: `${augmented ? "Augmentation" : "Diminution"}: the same pitches in ${stretchPhrase(timeRatio)}`
2434
- };
2435
- }
2436
- const name = intervalName(interval);
2437
- return {
2438
- ...base,
2439
- kind: "transposition",
2440
- 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)}` : "")
2441
- };
2442
- }
2443
- if (forward && key !== void 0) {
2444
- const modelLadder = ladderIndices(model, key);
2445
- const answerLadder = ladderIndices(answer, key);
2446
- if (modelLadder !== null && answerLadder !== null) {
2447
- const degrees = (_nullishCoalesce(answerLadder[0], () => ( 0))) - (_nullishCoalesce(modelLadder[0], () => ( 0)));
2448
- if (degrees !== 0 && sameNumbers(ladderSteps(modelLadder), ladderSteps(answerLadder))) {
2449
- const direction = degrees > 0 ? "higher" : "lower";
2450
- const count = Math.abs(degrees);
2451
- return {
2452
- ...base,
2453
- kind: "tonalTransposition",
2454
- degrees,
2455
- 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`
2456
- };
2457
- }
2458
- }
2459
- }
2460
- if (forward && relation === "inversion") {
2461
- return {
2462
- ...base,
2463
- kind: "inversion",
2464
- rationale: "Inversion: every interval turned upside down about the first note" + (stretched ? `, in ${stretchPhrase(timeRatio)}` : "")
2465
- };
2466
- }
2467
- if (backward && relation === "retrograde") {
2468
- return {
2469
- ...base,
2470
- kind: "retrograde",
2471
- rationale: "Retrograde: the cell read back to front, rhythm included"
2472
- };
2473
- }
2474
- if (backward && relation === "retrogradeInversion") {
2475
- return {
2476
- ...base,
2477
- kind: "retrogradeInversion",
2478
- rationale: "Retrograde inversion: the cell read back to front and turned upside down"
2479
- };
2480
- }
2481
- return null;
2482
- }
2483
- function gradedEditDistance(a, b, cost) {
2484
- let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
2485
- for (let i = 1; i <= a.length; i += 1) {
2486
- const row = new Array(b.length + 1);
2487
- row[0] = i;
2488
- for (let j = 1; j <= b.length; j += 1) {
2489
- const substitution = (_nullishCoalesce(previous[j - 1], () => ( 0))) + cost(_nullishCoalesce(a[i - 1], () => ( 0)), _nullishCoalesce(b[j - 1], () => ( 0)));
2490
- const deletion = (_nullishCoalesce(previous[j], () => ( 0))) + 1;
2491
- const insertion = (_nullishCoalesce(row[j - 1], () => ( 0))) + 1;
2492
- row[j] = Math.min(substitution, deletion, insertion);
2493
- }
2494
- previous = row;
2495
- }
2496
- return _nullishCoalesce(previous[b.length], () => ( 0));
2497
- }
2498
- function distanceToSimilarity(distance, a, b) {
2499
- const longest = Math.max(a, b);
2500
- if (longest === 0) {
2501
- return 1;
2502
- }
2503
- return Math.min(1, Math.max(0, 1 - distance / longest));
2504
- }
2505
- function intervalCost(x, y) {
2506
- return Math.min(1, Math.abs(x - y) / INTERVAL_TOLERANCE);
2507
- }
2508
- function gapCost(x, y) {
2509
- if (!(x > EPS7) || !(y > EPS7)) {
2510
- return Math.abs(x - y) > EPS7 ? 1 : 0;
2511
- }
2512
- return Math.min(1, Math.abs(Math.log2(x / y)));
2513
- }
2514
- function compareMelodies(a, b) {
2515
- const left = orderedNotes(phraseNotes(a), "melody a");
2516
- const right = orderedNotes(phraseNotes(b), "melody b");
2517
- const leftIntervals = intervalsOf(left);
2518
- const rightIntervals = intervalsOf(right);
2519
- const pitchSimilarity = distanceToSimilarity(
2520
- gradedEditDistance(leftIntervals, rightIntervals, intervalCost),
2521
- leftIntervals.length,
2522
- rightIntervals.length
2523
- );
2524
- const leftGaps = _nullishCoalesce(rhythmProfile(onsetGaps(left)), () => ( onsetGaps(left)));
2525
- const rightGaps = _nullishCoalesce(rhythmProfile(onsetGaps(right)), () => ( onsetGaps(right)));
2526
- const rhythmSimilarity = distanceToSimilarity(
2527
- gradedEditDistance(leftGaps, rightGaps, gapCost),
2528
- leftGaps.length,
2529
- rightGaps.length
2530
- );
2531
- const similarity = PITCH_WEIGHT * pitchSimilarity + RHYTHM_WEIGHT * rhythmSimilarity;
2532
- return {
2533
- similarity,
2534
- pitchSimilarity,
2535
- rhythmSimilarity,
2536
- rationale: `Interval shapes ${Math.round(pitchSimilarity * 100)}% alike, rhythms ${Math.round(rhythmSimilarity * 100)}% alike, weighted ${PITCH_WEIGHT}/${RHYTHM_WEIGHT} into ${roundTo(similarity, 3)}`
2537
- };
2538
- }
2539
- function melodicSimilarity(a, b) {
2540
- return compareMelodies(a, b).similarity;
2541
- }
2542
- function extremes(notes) {
2543
- let peak = 0;
2544
- let trough = 0;
2545
- for (let i = 1; i < notes.length; i += 1) {
2546
- if ((_nullishCoalesce(_optionalChain([notes, 'access', _101 => _101[i], 'optionalAccess', _102 => _102.pitch]), () => ( 0))) > (_nullishCoalesce(_optionalChain([notes, 'access', _103 => _103[peak], 'optionalAccess', _104 => _104.pitch]), () => ( 0)))) {
2547
- peak = i;
2548
- }
2549
- if ((_nullishCoalesce(_optionalChain([notes, 'access', _105 => _105[i], 'optionalAccess', _106 => _106.pitch]), () => ( 0))) < (_nullishCoalesce(_optionalChain([notes, 'access', _107 => _107[trough], 'optionalAccess', _108 => _108.pitch]), () => ( 0)))) {
2550
- trough = i;
2551
- }
2552
- }
2553
- return { peak, trough };
2554
- }
2555
- function melodicContour(notes) {
2556
- const line = orderedNotes(phraseNotes(notes), "contour notes");
2557
- const intervals = intervalsOf(line);
2558
- const directions = intervals.map(
2559
- (step) => step > 0 ? "up" : step < 0 ? "down" : "same"
2560
- );
2561
- const { peak, trough } = extremes(line);
2562
- const range = (_nullishCoalesce(_optionalChain([line, 'access', _109 => _109[peak], 'optionalAccess', _110 => _110.pitch]), () => ( 0))) - (_nullishCoalesce(_optionalChain([line, 'access', _111 => _111[trough], 'optionalAccess', _112 => _112.pitch]), () => ( 0)));
2563
- const motion = intervals.reduce((sum, step) => sum + Math.abs(step), 0);
2564
- if (motion === 0) {
2565
- return {
2566
- directions,
2567
- shape: "static",
2568
- peakIndex: peak,
2569
- troughIndex: trough,
2570
- range: 0,
2571
- 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"
2572
- };
2573
- }
2574
- const first = _nullishCoalesce(_optionalChain([line, 'access', _113 => _113[0], 'optionalAccess', _114 => _114.pitch]), () => ( 0));
2575
- const last = _nullishCoalesce(_optionalChain([line, 'access', _115 => _115[line.length - 1], 'optionalAccess', _116 => _116.pitch]), () => ( 0));
2576
- const net = last - first;
2577
- const directness = Math.abs(net) / motion;
2578
- const share = Math.round(directness * 100);
2579
- if (directness >= DIRECTIONAL_SHARE) {
2580
- const shape = net > 0 ? "ascending" : "descending";
2581
- return {
2582
- directions,
2583
- shape,
2584
- peakIndex: peak,
2585
- troughIndex: trough,
2586
- range,
2587
- rationale: `${share}% of the motion runs ${net > 0 ? "up" : "down"}, so the line is ${shape}`
2588
- };
2589
- }
2590
- const peakPitch = _nullishCoalesce(_optionalChain([line, 'access', _117 => _117[peak], 'optionalAccess', _118 => _118.pitch]), () => ( 0));
2591
- const archMotion = peakPitch - first + (peakPitch - last);
2592
- if (peak > 0 && peak < line.length - 1 && archMotion >= DIRECTIONAL_SHARE * motion) {
2593
- return {
2594
- directions,
2595
- shape: "arch",
2596
- peakIndex: peak,
2597
- troughIndex: trough,
2598
- range,
2599
- rationale: `Rises to its peak at note ${peak + 1} of ${line.length} and falls back, so the line is an arch`
2600
- };
2601
- }
2602
- return {
2603
- directions,
2604
- shape: "wave",
2605
- peakIndex: peak,
2606
- troughIndex: trough,
2607
- range,
2608
- rationale: `Changes direction without settling either way (${share}% of the motion nets out), so the line is a wave`
2609
- };
2610
- }
2611
-
2612
- // src/analyze/form/phrase.ts
2613
- var CADENCE_BOUNDARY_STRENGTH = {
2614
- authentic: 0.9,
2615
- modal: 0.75,
2616
- plagal: 0.7,
2617
- phrygian: 0.65,
2618
- half: 0.6,
2619
- deceptive: 0.45
2620
- };
2621
- var CADENCE_CLOSURE = {
2622
- authentic: 0.85,
2623
- modal: 0.6,
2624
- plagal: 0.55,
2625
- phrygian: 0.4,
2626
- half: 0.35,
2627
- deceptive: 0.3
2628
- };
2629
- var REST_STRENGTH = 0.6;
2630
- var LONG_NOTE_STRENGTH = 0.4;
2631
- var REPETITION_STRENGTH = 0.5;
2632
- var REPETITION_THRESHOLD = 0.75;
2633
- var HYPERMETER_STRENGTH = 0.25;
2634
- var PIECE_END_STRENGTH = 0.5;
2635
- var PHRASE_CUT_COST = 1.3;
2636
- var CLOSING_SHARE = 0.65;
2637
- var FINAL_PHRASE_BONUS = 0.15;
2638
- var HYPERMETRIC_CLOSE_BONUS = 0.1;
2639
- var EVADED_FACTOR = 0.6;
2640
- var SIGNAL_ORDER = [
2641
- "cadence",
2642
- "rest",
2643
- "repetition",
2644
- "longNote",
2645
- "pieceEnd",
2646
- "hypermeter"
2647
- ];
2648
- function addSignal(boundaries, beat, signal, strength, cadence = null) {
2649
- const key = Math.round(beat / EPS6);
2650
- const existing = boundaries.get(key);
2651
- const boundary = _nullishCoalesce(existing, () => ( { beat, strengths: /* @__PURE__ */ new Map(), cadence: null }));
2652
- boundary.strengths.set(signal, Math.max(_nullishCoalesce(boundary.strengths.get(signal), () => ( 0)), strength));
2653
- if (cadence !== null && boundary.cadence === null) {
2654
- boundary.cadence = cadence;
2655
- }
2656
- boundaries.set(key, boundary);
2657
- }
2658
- function readBoundary(boundary) {
2659
- const signals = [...boundary.strengths.entries()].sort(
2660
- (a, b) => b[1] - a[1] || SIGNAL_ORDER.indexOf(a[0]) - SIGNAL_ORDER.indexOf(b[0])
2661
- );
2662
- return {
2663
- strength: combineEvidence(signals.map(([, strength]) => strength)),
2664
- signals: signals.map(([signal]) => signal)
2665
- };
2666
- }
2667
- function lengthFit(length, expected) {
2668
- if (!(expected > 0)) {
2669
- return 0;
2670
- }
2671
- return Math.max(-1, 1 - Math.abs(length - expected) / expected);
2672
- }
2673
- function choosePhrasePath(beats, strengths, minPhraseBeats, expected) {
2674
- const value = new Array(beats.length).fill(Number.NEGATIVE_INFINITY);
2675
- const cameFrom = new Array(beats.length).fill(-1);
2676
- value[0] = 0;
2677
- for (let j = 1; j < beats.length; j += 1) {
2678
- const endBeat = _nullishCoalesce(beats[j], () => ( 0));
2679
- for (let i = 0; i < j; i += 1) {
2680
- if (!Number.isFinite(_nullishCoalesce(value[i], () => ( Number.NEGATIVE_INFINITY)))) {
2681
- continue;
2682
- }
2683
- const length = endBeat - (_nullishCoalesce(beats[i], () => ( 0)));
2684
- if (length < minPhraseBeats - EPS6) {
2685
- continue;
2686
- }
2687
- const gain = (_nullishCoalesce(value[i], () => ( 0))) + (_nullishCoalesce(strengths[j], () => ( 0))) + lengthFit(length, expected) - PHRASE_CUT_COST;
2688
- if (gain > (_nullishCoalesce(value[j], () => ( Number.NEGATIVE_INFINITY)))) {
2689
- value[j] = gain;
2690
- cameFrom[j] = i;
2691
- }
2692
- }
2693
- }
2694
- const last = beats.length - 1;
2695
- const path = [];
2696
- if (last > 0 && Number.isFinite(_nullishCoalesce(value[last], () => ( Number.NEGATIVE_INFINITY)))) {
2697
- for (let i = last; i > 0; i = _nullishCoalesce(cameFrom[i], () => ( 0))) {
2698
- path.push(i);
2699
- }
2700
- path.reverse();
2701
- } else if (last > 0) {
2702
- path.push(last);
2703
- }
2704
- return path;
2705
- }
2706
- function arrivalEnd(timeline, atBeat) {
2707
- for (const segment of timeline.segments) {
2708
- if (Math.abs(segment.startBeat - atBeat) < EPS6) {
2709
- return segment.endBeat;
2710
- }
2711
- }
2712
- return atBeat;
2713
- }
2714
- function resolveKey(opts, notes, meters, totalBeats) {
2715
- if (opts.key !== void 0) {
2716
- return opts.key;
2717
- }
2718
- const regions = keyTimelineFromNotes(notes, {
2719
- meters,
2720
- totalBeats,
2721
- budget: opts.budget
2722
- });
2723
- return keyLookup(regions, _nullishCoalesce(prevailingKeyOf(regions), () => ( _chunkUADYAYATcjs.majorKey.call(void 0, 0))));
2724
- }
2725
- function restBoundaries(boundaries, notes, meters, restBeats) {
2726
- const ordered = [...notes].sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
2727
- let covered = Number.NEGATIVE_INFINITY;
2728
- for (const note of ordered) {
2729
- const gap = note.startBeat - covered;
2730
- if (Number.isFinite(covered) && gap >= restBeats - EPS6) {
2731
- const bar = _chunkK4ZJUGF4cjs.beatsPerBarAt.call(void 0, note.startBeat, meters);
2732
- addSignal(
2733
- boundaries,
2734
- note.startBeat,
2735
- "rest",
2736
- REST_STRENGTH * clamp012(bar > 0 ? gap / bar : 1)
2737
- );
2738
- }
2739
- covered = Math.max(covered, note.startBeat + note.durationBeat);
2740
- }
2741
- }
2742
- function repetitionBoundaries(boundaries, notes, meters, spanStart, spanEnd, windowBeats, budget) {
2743
- if (!(windowBeats > 0)) {
2744
- return;
2745
- }
2746
- const slices = sliceBars(notes, meters, spanStart, spanEnd, budget);
2747
- _chunkQ2WGKJNNcjs.assertGenerationBudget.call(void 0, slices.length * notes.length, "form repetition comparisons", budget);
2748
- const onsetsIn = (from, to) => notes.filter((note) => note.startBeat >= from - EPS6 && note.startBeat < to - EPS6);
2749
- for (const slice of slices) {
2750
- const beat = slice.startBeat;
2751
- if (beat - windowBeats < spanStart - EPS6 || beat + windowBeats > spanEnd + EPS6) {
2752
- continue;
2753
- }
2754
- const before = onsetsIn(beat - windowBeats, beat);
2755
- const after = onsetsIn(beat, beat + windowBeats);
2756
- if (before.length < 2 || after.length < 2) {
2757
- continue;
2758
- }
2759
- const similarity = melodicSimilarity(before, after);
2760
- if (similarity >= REPETITION_THRESHOLD) {
2761
- addSignal(boundaries, beat, "repetition", REPETITION_STRENGTH * similarity);
2762
- }
2763
- }
2764
- }
2765
- function structuralWeightOf(cadence, confidence, aligned, isLast) {
2766
- if (cadence === null || cadence.cadence.type === null) {
2767
- return 0;
2768
- }
2769
- const result = cadence.cadence;
2770
- let base = CADENCE_CLOSURE[result.type];
2771
- if (result.type === "authentic") {
2772
- if (result.strength === "perfect") {
2773
- base = 1;
2774
- } else if (result.strength === "imperfect") {
2775
- base *= 0.8;
2776
- }
2777
- }
2778
- if (result.evaded) {
2779
- base *= EVADED_FACTOR;
2780
- }
2781
- let weight = base * confidence;
2782
- if (isLast) {
2783
- weight += FINAL_PHRASE_BONUS;
2784
- }
2785
- if (aligned || isLast) {
2786
- weight += HYPERMETRIC_CLOSE_BONUS;
2787
- }
2788
- return clamp012(weight);
2789
- }
2790
- function describeCadence(cadence) {
2791
- if (cadence === null || cadence.cadence.type === null) {
2792
- return "no cadence";
2793
- }
2794
- const { type, strength } = cadence.cadence;
2795
- const graded = strength === null ? type : `${strength} ${type}`;
2796
- return `a ${graded} cadence at beat ${cadence.atBeat}`;
2797
- }
2798
- function phrasesFromTimeline(timeline, notes, opts = {}) {
2799
- const meters = _chunkK4ZJUGF4cjs.resolveMeters.call(void 0, opts, "phrase meters");
2800
- _chunkQ2WGKJNNcjs.assertNoteEvents.call(void 0, notes, "phrase notes", {
2801
- allowNonPositiveDuration: true,
2802
- budget: opts.budget
2803
- });
2804
- const sounding = notes.filter((note) => note.durationBeat > 0);
2805
- const segments = timeline.segments;
2806
- const firstSegment = segments[0];
2807
- const lastSegment = segments[segments.length - 1];
2808
- const spanStart = sounding.reduce(
2809
- (first, n) => Math.min(first, n.startBeat),
2810
- _nullishCoalesce(_optionalChain([firstSegment, 'optionalAccess', _119 => _119.startBeat]), () => ( 0))
2811
- );
2812
- const notesEnd = sounding.reduce(
2813
- (end, n) => Math.max(end, n.startBeat + n.durationBeat),
2814
- _nullishCoalesce(_optionalChain([lastSegment, 'optionalAccess', _120 => _120.endBeat]), () => ( 0))
2815
- );
2816
- const spanEnd = Math.max(spanStart, _nullishCoalesce(opts.totalBeats, () => ( notesEnd)));
2817
- _chunkQ2WGKJNNcjs.assertRange.call(void 0, spanEnd, 0, Number.MAX_SAFE_INTEGER, "phrase totalBeats");
2818
- const barBeats = _chunkK4ZJUGF4cjs.beatsPerBarAt.call(void 0, Math.max(0, spanStart), meters);
2819
- const restBeats = _nullishCoalesce(opts.restBeats, () => ( barBeats / 2));
2820
- _chunkQ2WGKJNNcjs.assertRange.call(void 0, restBeats, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "phrase restBeats");
2821
- const longNoteBeats = _nullishCoalesce(opts.longNoteBeats, () => ( barBeats));
2822
- _chunkQ2WGKJNNcjs.assertRange.call(void 0, longNoteBeats, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "phrase longNoteBeats");
2823
- const minPhraseBeats = _nullishCoalesce(opts.minPhraseBeats, () => ( barBeats));
2824
- _chunkQ2WGKJNNcjs.assertRange.call(void 0, minPhraseBeats, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "phrase minPhraseBeats");
2825
- const key = resolveKey(opts, sounding, meters, spanEnd);
2826
- const cadences = detectCadences(timeline, key);
2827
- const grouping = _nullishCoalesce(opts.hypermeter, () => ( hypermeter(sounding, meters, {
2828
- totalBeats: spanEnd,
2829
- cadenceBeats: cadences.map((hit) => hit.atBeat),
2830
- budget: opts.budget
2831
- })));
2832
- const hyperBeats = Math.max(
2833
- barBeats,
2834
- _chunkK4ZJUGF4cjs.barPositionToBeat.call(void 0, { bar: grouping.groupBars, beat: 0 }, meters) - _chunkK4ZJUGF4cjs.barPositionToBeat.call(void 0, { bar: 0, beat: 0 }, meters)
2835
- );
2836
- const expected = _nullishCoalesce(opts.expectedPhraseBeats, () => ( hyperBeats));
2837
- _chunkQ2WGKJNNcjs.assertRange.call(void 0, expected, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "phrase expectedPhraseBeats");
2838
- const boundaries = /* @__PURE__ */ new Map();
2839
- for (const hit of cadences) {
2840
- const type = hit.cadence.type;
2841
- if (type === null) {
2842
- continue;
2843
- }
2844
- addSignal(
2845
- boundaries,
2846
- arrivalEnd(timeline, hit.atBeat),
2847
- "cadence",
2848
- CADENCE_BOUNDARY_STRENGTH[type],
2849
- hit
2850
- );
2851
- }
2852
- restBoundaries(boundaries, sounding, meters, restBeats);
2853
- for (const note of sounding) {
2854
- if (note.durationBeat >= longNoteBeats - EPS6) {
2855
- addSignal(boundaries, note.startBeat + note.durationBeat, "longNote", LONG_NOTE_STRENGTH);
2856
- }
2857
- }
2858
- repetitionBoundaries(boundaries, sounding, meters, spanStart, spanEnd, hyperBeats, opts.budget);
2859
- for (const downbeat of grouping.downbeats) {
2860
- addSignal(boundaries, downbeat, "hypermeter", HYPERMETER_STRENGTH * grouping.confidence);
2861
- }
2862
- addSignal(boundaries, spanEnd, "pieceEnd", PIECE_END_STRENGTH);
2863
- const candidates = [...boundaries.values()].filter((boundary) => boundary.beat > spanStart + EPS6 && boundary.beat < spanEnd - EPS6).sort((a, b) => a.beat - b.beat);
2864
- const endBoundary = _nullishCoalesce(boundaries.get(Math.round(spanEnd / EPS6)), () => ( {
2865
- beat: spanEnd,
2866
- strengths: /* @__PURE__ */ new Map(),
2867
- cadence: null
2868
- }));
2869
- const stops = [
2870
- { beat: spanStart, strengths: /* @__PURE__ */ new Map(), cadence: null },
2871
- ...candidates,
2872
- endBoundary
2873
- ];
2874
- _chunkQ2WGKJNNcjs.assertGenerationBudget.call(void 0, stops.length * stops.length, "form phrase boundary pairs", opts.budget);
2875
- const readings = stops.map(readBoundary);
2876
- const path = choosePhrasePath(
2877
- stops.map((stop) => stop.beat),
2878
- readings.map((reading) => reading.strength),
2879
- minPhraseBeats,
2880
- expected
2881
- );
2882
- const downbeatKeys = new Set(grouping.downbeats.map((downbeat) => Math.round(downbeat / EPS6)));
2883
- const phrases = [];
2884
- let from = 0;
2885
- for (let n = 0; n < path.length; n += 1) {
2886
- const to = _nullishCoalesce(path[n], () => ( stops.length - 1));
2887
- const startBeat = _nullishCoalesce(_optionalChain([stops, 'access', _121 => _121[from], 'optionalAccess', _122 => _122.beat]), () => ( spanStart));
2888
- const stop = stops[to];
2889
- const endBeat = _nullishCoalesce(_optionalChain([stop, 'optionalAccess', _123 => _123.beat]), () => ( spanEnd));
2890
- const reading = _nullishCoalesce(readings[to], () => ( { strength: 0, signals: [] }));
2891
- const length = endBeat - startBeat;
2892
- const confidence = clamp012(
2893
- CLOSING_SHARE * reading.strength + (1 - CLOSING_SHARE) * Math.max(0, lengthFit(length, expected))
2894
- );
2895
- const isLast = n === path.length - 1;
2896
- const cadence = _nullishCoalesce(_optionalChain([stop, 'optionalAccess', _124 => _124.cadence]), () => ( null));
2897
- const structuralWeight = structuralWeightOf(
2898
- cadence,
2899
- confidence,
2900
- downbeatKeys.has(Math.round(endBeat / EPS6)),
2901
- isLast
2902
- );
2903
- const bars = barBeats > 0 ? Math.round(length / barBeats * 100) / 100 : 0;
2904
- phrases.push({
2905
- startBeat,
2906
- endBeat,
2907
- cadence,
2908
- signals: reading.signals,
2909
- confidence,
2910
- structuralWeight,
2911
- rationale: `Phrase of ${bars} bar(s) closing on ${describeCadence(cadence)}; boundary from ${reading.signals.length === 0 ? "the end of the span" : reading.signals.join(", ")}`
2912
- });
2913
- from = to;
2914
- }
2915
- return phrases;
2916
- }
2917
- function structuralCadences(phrases) {
2918
- const ranked = [];
2919
- for (let index = 0; index < phrases.length; index += 1) {
2920
- const phrase = phrases[index];
2921
- if (phrase === void 0 || phrase.cadence === null || phrase.cadence.cadence.type === null) {
2922
- continue;
2923
- }
2924
- const hit = phrase.cadence;
2925
- ranked.push({
2926
- phraseIndex: index,
2927
- atBeat: hit.atBeat,
2928
- endBeat: phrase.endBeat,
2929
- cadence: hit.cadence,
2930
- weight: phrase.structuralWeight,
2931
- rationale: `Closes ${describeCadence(hit)} on a phrase of confidence ${Math.round(phrase.confidence * 100) / 100}`
2932
- });
2933
- }
2934
- return ranked.sort((a, b) => b.weight - a.weight || a.atBeat - b.atBeat);
2935
- }
2936
-
2937
- // src/analyze/form/section.ts
2938
- var MELODY_SHARE = 0.6;
2939
- var HARMONY_SHARE = 0.4;
2940
- var DEFAULT_THRESHOLD = 0.8;
2941
- function labelAt(index) {
2942
- let n = index;
2943
- let label = "";
2944
- do {
2945
- label = String.fromCharCode(65 + n % 26) + label;
2946
- n = Math.floor(n / 26) - 1;
2947
- } while (n >= 0);
2948
- return label;
2949
- }
2950
- function unitSimilarity(a, b) {
2951
- const harmony = weightSimilarity(a.weights, b.weights);
2952
- if (a.notes.length < 2 || b.notes.length < 2) {
2953
- return a.notes.length === b.notes.length ? harmony : 0;
2954
- }
2955
- const melody = melodicSimilarity(a.notes, b.notes);
2956
- return clamp012(MELODY_SHARE * melody + HARMONY_SHARE * harmony);
2957
- }
2958
- function sectionsFromNotes(notes, opts = {}) {
2959
- const meters = _chunkK4ZJUGF4cjs.resolveMeters.call(void 0, opts, "section meters");
2960
- _chunkQ2WGKJNNcjs.assertNoteEvents.call(void 0, notes, "section notes", {
2961
- allowNonPositiveDuration: true,
2962
- budget: opts.budget
2963
- });
2964
- const sounding = notes.filter((note) => note.durationBeat > 0);
2965
- if (sounding.length === 0) {
2966
- return [];
2967
- }
2968
- const firstOnset = firstSoundingBeat(sounding);
2969
- const lastEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
2970
- const spanEnd = Math.max(firstOnset, _nullishCoalesce(opts.totalBeats, () => ( lastEnd)));
2971
- _chunkQ2WGKJNNcjs.assertRange.call(void 0, spanEnd, 0, Number.MAX_SAFE_INTEGER, "section totalBeats");
2972
- const threshold = _nullishCoalesce(opts.threshold, () => ( DEFAULT_THRESHOLD));
2973
- _chunkQ2WGKJNNcjs.assertRange.call(void 0, threshold, 0, 1, "section threshold");
2974
- const unitBars = _nullishCoalesce(opts.unitBars, () => ( hypermeter(sounding, meters, { totalBeats: spanEnd, budget: opts.budget }).groupBars));
2975
- _chunkQ2WGKJNNcjs.assertPositiveInt.call(void 0, unitBars, "section unitBars");
2976
- const firstBar = _chunkK4ZJUGF4cjs.barIndexAt.call(void 0, firstOnset, meters);
2977
- const lastBar = lastBarOf(meters, firstOnset, spanEnd);
2978
- const startBar = Math.max(0, firstBar);
2979
- const unitCount = Math.max(1, Math.ceil((lastBar - startBar + 1) / unitBars));
2980
- _chunkQ2WGKJNNcjs.assertGenerationBudget.call(void 0, unitCount * unitCount, "form section comparisons", opts.budget);
2981
- _chunkQ2WGKJNNcjs.assertGenerationBudget.call(void 0, unitCount * sounding.length, "form section unit notes", opts.budget);
2982
- const units = [];
2983
- for (let index = 0; index < unitCount; index += 1) {
2984
- const barStart = startBar + index * unitBars;
2985
- const startBeat = index === 0 ? firstOnset : _chunkK4ZJUGF4cjs.barPositionToBeat.call(void 0, { bar: barStart, beat: 0 }, meters);
2986
- const endBeat = index === unitCount - 1 ? spanEnd : _chunkK4ZJUGF4cjs.barPositionToBeat.call(void 0, { bar: barStart + unitBars, beat: 0 }, meters);
2987
- const unitNotes = sounding.filter((note) => note.startBeat >= startBeat - EPS6 && note.startBeat < endBeat - EPS6).sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch);
2988
- units.push({
2989
- startBeat,
2990
- endBeat,
2991
- notes: unitNotes,
2992
- weights: windowWeights(unitNotes, startBeat, endBeat, meters).weights
2993
- });
2994
- }
2995
- const labels = [];
2996
- const scores = [];
2997
- let distinct = 0;
2998
- for (let index = 0; index < units.length; index += 1) {
2999
- const unit = units[index];
3000
- if (unit === void 0) {
3001
- continue;
3002
- }
3003
- let bestScore = -1;
3004
- let bestIndex = -1;
3005
- for (let earlier = 0; earlier < index; earlier += 1) {
3006
- const other = units[earlier];
3007
- if (other === void 0) {
3008
- continue;
3009
- }
3010
- const score = unitSimilarity(unit, other);
3011
- if (score > bestScore) {
3012
- bestScore = score;
3013
- bestIndex = earlier;
3014
- }
3015
- }
3016
- if (bestIndex >= 0 && bestScore >= threshold) {
3017
- labels.push(_nullishCoalesce(labels[bestIndex], () => ( labelAt(distinct))));
3018
- scores.push(bestScore);
3019
- } else {
3020
- labels.push(labelAt(distinct));
3021
- scores.push(1);
3022
- distinct += 1;
3023
- }
3024
- }
3025
- const sections = [];
3026
- const firstByLabel = /* @__PURE__ */ new Map();
3027
- for (let index = 0; index < units.length; ) {
3028
- const label = _nullishCoalesce(labels[index], () => ( labelAt(0)));
3029
- let end = index;
3030
- let scoreSum = 0;
3031
- while (end < units.length && labels[end] === label) {
3032
- scoreSum += _nullishCoalesce(scores[end], () => ( 1));
3033
- end += 1;
3034
- }
3035
- const startBeat = _nullishCoalesce(_optionalChain([units, 'access', _125 => _125[index], 'optionalAccess', _126 => _126.startBeat]), () => ( 0));
3036
- const endBeat = _nullishCoalesce(_optionalChain([units, 'access', _127 => _127[end - 1], 'optionalAccess', _128 => _128.endBeat]), () => ( spanEnd));
3037
- const sectionIndex = sections.length;
3038
- const first = firstByLabel.get(label);
3039
- if (first === void 0) {
3040
- firstByLabel.set(label, sectionIndex);
3041
- }
3042
- const similarity = first === void 0 ? 1 : clamp012(scoreSum / (end - index));
3043
- sections.push({
3044
- label,
3045
- startBeat,
3046
- endBeat,
3047
- bars: lastBarOf(meters, startBeat, endBeat) - _chunkK4ZJUGF4cjs.barIndexAt.call(void 0, startBeat, meters) + 1,
3048
- firstOccurrence: _nullishCoalesce(first, () => ( sectionIndex)),
3049
- similarity,
3050
- 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', _129 => _129[first], 'optionalAccess', _130 => _130.startBeat]), () => ( 0))}`
3051
- });
3052
- index = end;
3053
- }
3054
- return sections;
3055
- }
3056
-
3057
- // src/analyze/reduction/index.ts
3058
- var ADJACENCY_EPS = 1e-9;
3059
- var TONIC_ARRIVING = /* @__PURE__ */ new Set(["authentic", "plagal", "modal", "deceptive"]);
3060
- function signedStep(fromPc, toPc) {
3061
- return (_chunkQ2WGKJNNcjs.pitchClassOf.call(void 0, toPc) - _chunkQ2WGKJNNcjs.pitchClassOf.call(void 0, fromPc) + 18) % 12 - 6;
3062
- }
3063
- function isStep2(distance) {
3064
- const size = Math.abs(distance);
3065
- return size === 1 || size === 2;
3066
- }
3067
- function touches(before, after) {
3068
- return Math.abs(after.startBeat - before.endBeat) <= ADJACENCY_EPS;
3069
- }
3070
- function median(values) {
3071
- if (values.length === 0) {
3072
- return 0;
3073
- }
3074
- const sorted = [...values].sort((a, b) => a - b);
3075
- const middle = sorted.length >> 1;
3076
- if (sorted.length % 2 === 1) {
3077
- return _nullishCoalesce(sorted[middle], () => ( 0));
3078
- }
3079
- return ((_nullishCoalesce(sorted[middle - 1], () => ( 0))) + (_nullishCoalesce(sorted[middle], () => ( 0)))) / 2;
3080
- }
3081
- function isTonicChord(chord, key) {
3082
- return _chunkQ2WGKJNNcjs.pitchClassOf.call(void 0, chord.rootPc - key.rootPc) === 0;
3083
- }
3084
- function isDominantChord(chord, key) {
3085
- return _chunkQ2WGKJNNcjs.pitchClassOf.call(void 0, chord.rootPc - key.rootPc) === 7 && chord.intervals.some((iv) => _chunkQ2WGKJNNcjs.pitchClassOf.call(void 0, iv) === 4);
3086
- }
3087
- function functionalReason(segment, before, after, keyAt) {
3088
- const key = keyAt(segment.startBeat);
3089
- if (after !== void 0 && _chunkOE6WK2Y2cjs.isCadentialSixFour.call(void 0, segment.chord, after.chord, key)) {
3090
- return { kind: "cadentialSixFour" };
3091
- }
3092
- if (isTonicChord(segment.chord, key)) {
3093
- return { kind: "tonic" };
3094
- }
3095
- if (isDominantChord(segment.chord, key)) {
3096
- return { kind: "dominant" };
3097
- }
3098
- if (before !== void 0) {
3099
- const arrival = _chunkOE6WK2Y2cjs.detectCadence.call(void 0, before.chord, segment.chord, key).type;
3100
- if (arrival !== null) {
3101
- return { kind: "cadenceArrival", cadence: arrival };
3102
- }
3103
- }
3104
- if (after !== void 0) {
3105
- const agent = _chunkOE6WK2Y2cjs.detectCadence.call(void 0, segment.chord, after.chord, keyAt(after.startBeat)).type;
3106
- if (agent !== null && TONIC_ARRIVING.has(agent)) {
3107
- return { kind: "cadenceAgent", cadence: agent };
3108
- }
3109
- }
3110
- return null;
3111
- }
3112
- function figureReason(segment, before, after) {
3113
- if (before === void 0 || after === void 0) {
3114
- return null;
3115
- }
3116
- const incoming = signedStep(before.chord.rootPc, segment.chord.rootPc);
3117
- const outgoing = signedStep(segment.chord.rootPc, after.chord.rootPc);
3118
- if (!isStep2(incoming) || !isStep2(outgoing)) {
3119
- return null;
3120
- }
3121
- if (incoming > 0 === outgoing > 0) {
3122
- return { kind: "passing" };
3123
- }
3124
- return _chunkQ2WGKJNNcjs.pitchClassOf.call(void 0, before.chord.rootPc) === _chunkQ2WGKJNNcjs.pitchClassOf.call(void 0, after.chord.rootPc) ? { kind: "neighbor" } : null;
3125
- }
3126
- function describe2(reason) {
3127
- switch (reason.kind) {
3128
- case "tonic":
3129
- return "Structural: the tonic of the key, which the progression is heard against";
3130
- case "dominant":
3131
- return "Structural: the dominant of the key, the chord its tonic is approached from";
3132
- case "cadentialSixFour":
3133
- return "Structural: a tonic six-four on the bass of the dominant that follows, which prolongs that dominant rather than framing the tonic";
3134
- case "cadenceArrival":
3135
- return `Structural: the arrival of a ${reason.cadence} cadence`;
3136
- case "cadenceAgent":
3137
- return `Structural: the chord that drives a ${reason.cadence} cadence`;
3138
- case "salient":
3139
- return "Structural: it holds the harmony at least as long as the median chord";
3140
- case "first":
3141
- return "Structural: the first chord of the progression frames it";
3142
- case "last":
3143
- return "Structural: the last chord of the progression frames it";
3144
- case "unfigured":
3145
- return "Structural: its root neither steps through to the next chord nor returns to the last, so it embellishes nothing";
3146
- case "passing":
3147
- return "Passing: its root steps in from the previous chord and on to the next in the same direction";
3148
- case "neighbor":
3149
- return "Neighbor: its root steps away from the surrounding harmony and returns to it";
3150
- }
3151
- }
3152
- var BASIS_READINGS = {
3153
- function: ({ segment, before, after, keyAt }) => functionalReason(segment, before, after, keyAt),
3154
- duration: ({ segment, salienceFloor }) => segment.endBeat - segment.startBeat >= salienceFloor ? { kind: "salient" } : null
3155
- };
3156
- var REDUCTION_BASES = Object.keys(BASIS_READINGS);
3157
- function levelOf(reason) {
3158
- if (reason.kind === "passing") {
3159
- return "passing";
3160
- }
3161
- return reason.kind === "neighbor" ? "neighbor" : "structural";
3162
- }
3163
- function reduceProgression(timeline, key, opts = {}) {
3164
- const keyAt = typeof key === "function" ? key : () => key;
3165
- const basis = _chunkQ2WGKJNNcjs.assertOneOf.call(void 0, _nullishCoalesce(opts.basis, () => ( "function")), REDUCTION_BASES, "reduction basis");
3166
- const reading = BASIS_READINGS[basis];
3167
- const segments = timeline.segments;
3168
- const salienceFloor = basis === "duration" ? median(segments.map((segment) => segment.endBeat - segment.startBeat)) : 0;
3169
- const result = [];
3170
- for (let index = 0; index < segments.length; index += 1) {
3171
- const segment = segments[index];
3172
- if (segment === void 0) {
3173
- continue;
3174
- }
3175
- const previous = segments[index - 1];
3176
- const following = segments[index + 1];
3177
- const before = previous !== void 0 && touches(previous, segment) ? previous : void 0;
3178
- const after = following !== void 0 && touches(segment, following) ? following : void 0;
3179
- let reason = reading({ segment, before, after, keyAt, salienceFloor });
3180
- if (reason === null && index === 0) {
3181
- reason = { kind: "first" };
3182
- }
3183
- if (reason === null && index === segments.length - 1) {
3184
- reason = { kind: "last" };
3185
- }
3186
- reason ??= _nullishCoalesce(figureReason(segment, before, after), () => ( { kind: "unfigured" }));
3187
- result.push({
3188
- chord: segment.chord,
3189
- startBeat: segment.startBeat,
3190
- endBeat: segment.endBeat,
3191
- level: levelOf(reason),
3192
- rationale: describe2(reason)
3193
- });
3194
- }
3195
- return result;
3196
- }
3197
-
3198
-
3199
-
3200
-
3201
-
3202
-
3203
-
3204
-
3205
-
3206
-
3207
-
3208
-
3209
-
3210
-
3211
-
3212
-
3213
-
3214
-
3215
-
3216
-
3217
-
3218
-
3219
-
3220
-
3221
-
3222
-
3223
- exports.keyTimelineFromNotes = keyTimelineFromNotes; exports.detectModulations = detectModulations; exports.keyLookup = keyLookup; exports.prevailingKeyOf = prevailingKeyOf; exports.chordTimelineFromChords = chordTimelineFromChords; exports.chordTimelineFromNotes = chordTimelineFromNotes; exports.detectCadences = detectCadences; exports.toVoiceNotes = toVoiceNotes; exports.analyzeVoice = analyzeVoice; exports.analyzeArrangement = analyzeArrangement; exports.createArrangementSession = createArrangementSession; exports.tensionCurve = tensionCurve; exports.tensionCurveFrom = tensionCurveFrom; exports.hypermeter = hypermeter; exports.extractMotifs = extractMotifs; exports.motifFromNotes = motifFromNotes; exports.relateMotifs = relateMotifs; exports.compareMelodies = compareMelodies; exports.melodicSimilarity = melodicSimilarity; exports.melodicContour = melodicContour; exports.phrasesFromTimeline = phrasesFromTimeline; exports.structuralCadences = structuralCadences; exports.sectionsFromNotes = sectionsFromNotes; exports.reduceProgression = reduceProgression;
3224
- //# sourceMappingURL=chunk-B6XNKWZV.cjs.map