@libraz/libcantus 0.9.3 → 0.9.4

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 (198) hide show
  1. package/README.md +2 -2
  2. package/dist/analyze/index.cjs +39 -1947
  3. package/dist/analyze/index.cjs.map +1 -1
  4. package/dist/analyze/index.d.cts +197 -37
  5. package/dist/analyze/index.d.ts +197 -37
  6. package/dist/analyze/index.js +19 -8
  7. package/dist/chunk-36N73KZ3.cjs +246 -0
  8. package/dist/chunk-36N73KZ3.cjs.map +1 -0
  9. package/dist/chunk-4HLLZPXT.cjs +3077 -0
  10. package/dist/chunk-4HLLZPXT.cjs.map +1 -0
  11. package/dist/chunk-7MGZ7D62.cjs +498 -0
  12. package/dist/chunk-7MGZ7D62.cjs.map +1 -0
  13. package/dist/{chunk-4MHA432G.js → chunk-7NKDPF74.js} +403 -180
  14. package/dist/chunk-7NKDPF74.js.map +1 -0
  15. package/dist/chunk-CAN76NCU.cjs +679 -0
  16. package/dist/chunk-CAN76NCU.cjs.map +1 -0
  17. package/dist/{chunk-MIWO425M.js → chunk-D2FAJBYL.js} +101 -34
  18. package/dist/chunk-D2FAJBYL.js.map +1 -0
  19. package/dist/{chunk-SGU7SPLK.js → chunk-EHVEBWM5.js} +1056 -531
  20. package/dist/chunk-EHVEBWM5.js.map +1 -0
  21. package/dist/chunk-FCANXBAT.cjs +30 -0
  22. package/dist/chunk-FCANXBAT.cjs.map +1 -0
  23. package/dist/chunk-FNDWXRRI.cjs +479 -0
  24. package/dist/chunk-FNDWXRRI.cjs.map +1 -0
  25. package/dist/{chunk-IT2O4XFE.js → chunk-GQSGS2N4.js} +43 -24
  26. package/dist/chunk-GQSGS2N4.js.map +1 -0
  27. package/dist/{chunk-62QZKCJQ.js → chunk-HKGYCQ34.js} +10 -6
  28. package/dist/chunk-HKGYCQ34.js.map +1 -0
  29. package/dist/{chunk-AKLYPWIN.js → chunk-I24MV65U.js} +29 -6
  30. package/dist/chunk-I24MV65U.js.map +1 -0
  31. package/dist/{chunk-CHMXELDS.js → chunk-KMHOICIY.js} +311 -181
  32. package/dist/chunk-KMHOICIY.js.map +1 -0
  33. package/dist/chunk-LW53AHQU.cjs +1117 -0
  34. package/dist/chunk-LW53AHQU.cjs.map +1 -0
  35. package/dist/{chunk-2ZWMSJH6.js → chunk-NKIBQ6PT.js} +2 -2
  36. package/dist/chunk-NKIBQ6PT.js.map +1 -0
  37. package/dist/chunk-NP4NA2FB.cjs +106 -0
  38. package/dist/chunk-NP4NA2FB.cjs.map +1 -0
  39. package/dist/chunk-NVE34YGZ.cjs +1449 -0
  40. package/dist/chunk-NVE34YGZ.cjs.map +1 -0
  41. package/dist/{chunk-EH6EM37U.js → chunk-NYYWMV2C.js} +61 -49
  42. package/dist/chunk-NYYWMV2C.js.map +1 -0
  43. package/dist/{chunk-GOAW4VYL.js → chunk-Q2XUP227.js} +243 -117
  44. package/dist/chunk-Q2XUP227.js.map +1 -0
  45. package/dist/chunk-Q6OP4U43.cjs +327 -0
  46. package/dist/chunk-Q6OP4U43.cjs.map +1 -0
  47. package/dist/chunk-RC6GBMCD.cjs +845 -0
  48. package/dist/chunk-RC6GBMCD.cjs.map +1 -0
  49. package/dist/{chunk-C5XYKUVG.js → chunk-RK35RIN7.js} +370 -157
  50. package/dist/chunk-RK35RIN7.js.map +1 -0
  51. package/dist/chunk-RUMU3UA6.cjs +53 -0
  52. package/dist/chunk-RUMU3UA6.cjs.map +1 -0
  53. package/dist/chunk-T64C7ZD7.cjs +1 -0
  54. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  55. package/dist/chunk-XEBG3G53.js +498 -0
  56. package/dist/chunk-XEBG3G53.js.map +1 -0
  57. package/dist/{chunk-I5R7ZBJS.js → chunk-YLEMGEFH.js} +103 -59
  58. package/dist/chunk-YLEMGEFH.js.map +1 -0
  59. package/dist/chunk-ZEJR4TKL.cjs +173 -0
  60. package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
  61. package/dist/core/index.cjs +130 -647
  62. package/dist/core/index.cjs.map +1 -1
  63. package/dist/core/index.d.cts +301 -39
  64. package/dist/core/index.d.ts +301 -39
  65. package/dist/core/index.js +54 -16
  66. package/dist/{function-DYt4iKsn.d.cts → function-BtiZb9i9.d.ts} +28 -7
  67. package/dist/{function-DVH4Eusr.d.ts → function-DNugAOrt.d.cts} +28 -7
  68. package/dist/generate/index.cjs +28 -4085
  69. package/dist/generate/index.cjs.map +1 -1
  70. package/dist/generate/index.d.cts +370 -62
  71. package/dist/generate/index.d.ts +370 -62
  72. package/dist/generate/index.js +23 -9
  73. package/dist/{index-CxqBQtH1.d.cts → index-6VR8-kit.d.ts} +19 -4
  74. package/dist/index-70p168VM.d.ts +192 -0
  75. package/dist/{index-CpOsP1y9.d.cts → index-Be74mcon.d.cts} +18 -17
  76. package/dist/index-BtXT6UQ0.d.cts +192 -0
  77. package/dist/{index-DxvARIP2.d.ts → index-C5UBbQ4G.d.ts} +18 -17
  78. package/dist/{index-Bt4tOUD9.d.ts → index-C83mZI1P.d.ts} +93 -8
  79. package/dist/{index-Bg5dYMmZ.d.ts → index-D6b6na69.d.cts} +14 -11
  80. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  81. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  82. package/dist/index-OG65ocOr.d.cts +272 -0
  83. package/dist/index-OG65ocOr.d.ts +272 -0
  84. package/dist/{index-Jc0Ryi79.d.ts → index-tJhkmQgE.d.cts} +19 -4
  85. package/dist/{index-Dikst44v.d.cts → index-tSQh-xwY.d.ts} +14 -11
  86. package/dist/{index-BNlGP86A.d.cts → index-y_ak9_Hi.d.cts} +93 -8
  87. package/dist/index.cjs +419 -7383
  88. package/dist/index.cjs.map +1 -1
  89. package/dist/index.d.cts +14 -14
  90. package/dist/index.d.ts +14 -14
  91. package/dist/index.js +111 -27
  92. package/dist/model/index.cjs +8 -2562
  93. package/dist/model/index.cjs.map +1 -1
  94. package/dist/model/index.d.cts +433 -75
  95. package/dist/model/index.d.ts +433 -75
  96. package/dist/model/index.js +6 -7
  97. package/dist/styled-DIRkk9Uz.d.ts +506 -0
  98. package/dist/styled-DJbkJQfj.d.cts +506 -0
  99. package/dist/theory/index.cjs +172 -1822
  100. package/dist/theory/index.cjs.map +1 -1
  101. package/dist/theory/index.d.cts +151 -233
  102. package/dist/theory/index.d.ts +151 -233
  103. package/dist/theory/index.js +30 -7
  104. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  105. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  106. package/package.json +24 -6
  107. package/dist/chunk-2ZWMSJH6.js.map +0 -1
  108. package/dist/chunk-4MHA432G.js.map +0 -1
  109. package/dist/chunk-62QZKCJQ.js.map +0 -1
  110. package/dist/chunk-AKLYPWIN.js.map +0 -1
  111. package/dist/chunk-C5XYKUVG.js.map +0 -1
  112. package/dist/chunk-CHMXELDS.js.map +0 -1
  113. package/dist/chunk-DYWFNC4A.js +0 -152
  114. package/dist/chunk-DYWFNC4A.js.map +0 -1
  115. package/dist/chunk-EH6EM37U.js.map +0 -1
  116. package/dist/chunk-FBQK3ZQN.js +0 -131
  117. package/dist/chunk-FBQK3ZQN.js.map +0 -1
  118. package/dist/chunk-GOAW4VYL.js.map +0 -1
  119. package/dist/chunk-I5R7ZBJS.js.map +0 -1
  120. package/dist/chunk-IT2O4XFE.js.map +0 -1
  121. package/dist/chunk-MIWO425M.js.map +0 -1
  122. package/dist/chunk-SGU7SPLK.js.map +0 -1
  123. package/dist/index-CGMZNCx6.d.cts +0 -140
  124. package/dist/index-CGMZNCx6.d.ts +0 -140
  125. package/dist/index-CnZCo29u.d.cts +0 -108
  126. package/dist/index-JnF4v9bx.d.ts +0 -108
  127. package/dist/styled-DZ7GjShb.d.ts +0 -282
  128. package/dist/styled-ZTbdaVFM.d.cts +0 -282
  129. package/src/analyze/arrange/index.ts +0 -21
  130. package/src/analyze/arrange/internal.ts +0 -127
  131. package/src/analyze/arrange/tension.ts +0 -199
  132. package/src/analyze/arrange/tracks.ts +0 -312
  133. package/src/analyze/detect/index.ts +0 -272
  134. package/src/analyze/functional/borrowed.ts +0 -84
  135. package/src/analyze/functional/cadence.ts +0 -66
  136. package/src/analyze/functional/function.ts +0 -184
  137. package/src/analyze/functional/index.ts +0 -23
  138. package/src/analyze/functional/internal.ts +0 -29
  139. package/src/analyze/functional/roman.ts +0 -402
  140. package/src/analyze/index.ts +0 -49
  141. package/src/analyze/timeline/index.ts +0 -451
  142. package/src/analyze/voice/index.ts +0 -339
  143. package/src/core/event-index/index.ts +0 -104
  144. package/src/core/index.ts +0 -61
  145. package/src/core/interval/index.ts +0 -81
  146. package/src/core/meter/index.ts +0 -302
  147. package/src/core/pitch/index.ts +0 -311
  148. package/src/core/random/index.ts +0 -76
  149. package/src/core/tuning/index.ts +0 -186
  150. package/src/core/types.ts +0 -29
  151. package/src/core/validation/index.ts +0 -119
  152. package/src/generate/bass/index.ts +0 -373
  153. package/src/generate/countermelody/index.ts +0 -470
  154. package/src/generate/drums/beat.ts +0 -266
  155. package/src/generate/drums/euclid.ts +0 -65
  156. package/src/generate/drums/fills.ts +0 -356
  157. package/src/generate/drums/ghost.ts +0 -119
  158. package/src/generate/drums/hihat.ts +0 -285
  159. package/src/generate/drums/hit.ts +0 -31
  160. package/src/generate/drums/index.ts +0 -361
  161. package/src/generate/drums/internal.ts +0 -257
  162. package/src/generate/drums/kick.ts +0 -160
  163. package/src/generate/drums/percussion.ts +0 -121
  164. package/src/generate/drums/rng.ts +0 -7
  165. package/src/generate/drums/swing.ts +0 -71
  166. package/src/generate/groove/index.ts +0 -351
  167. package/src/generate/harmonize/index.ts +0 -451
  168. package/src/generate/index.ts +0 -58
  169. package/src/generate/motif/index.ts +0 -422
  170. package/src/generate/progression/index.ts +0 -362
  171. package/src/generate/reharmony/index.ts +0 -323
  172. package/src/generate/rhythm/index.ts +0 -175
  173. package/src/index.ts +0 -13
  174. package/src/model/chord.ts +0 -442
  175. package/src/model/index.ts +0 -15
  176. package/src/model/interval.ts +0 -82
  177. package/src/model/key.ts +0 -266
  178. package/src/model/note.ts +0 -176
  179. package/src/model/progression.ts +0 -161
  180. package/src/model/shared.ts +0 -12
  181. package/src/theory/chord/index.ts +0 -375
  182. package/src/theory/chordscale/index.ts +0 -374
  183. package/src/theory/counterpoint/index.ts +0 -230
  184. package/src/theory/harmony/index.ts +0 -77
  185. package/src/theory/index.ts +0 -112
  186. package/src/theory/safety/index.ts +0 -488
  187. package/src/theory/scale/degrees.ts +0 -123
  188. package/src/theory/scale/index.ts +0 -28
  189. package/src/theory/scale/key.ts +0 -56
  190. package/src/theory/scale/masks.ts +0 -153
  191. package/src/theory/spelling/index.ts +0 -249
  192. package/src/theory/symbol/index.ts +0 -268
  193. package/src/theory/voicing/index.ts +0 -5
  194. package/src/theory/voicing/internal.ts +0 -168
  195. package/src/theory/voicing/leading.ts +0 -112
  196. package/src/theory/voicing/satb.ts +0 -216
  197. package/src/theory/voicing/styled.ts +0 -199
  198. package/tsconfig.json +0 -23
@@ -4,33 +4,37 @@ import {
4
4
  isStrongBeat,
5
5
  metricWeight,
6
6
  parseTimeSignature
7
- } from "./chunk-I5R7ZBJS.js";
7
+ } from "./chunk-YLEMGEFH.js";
8
8
  import {
9
9
  detectCadence,
10
10
  detectChord,
11
11
  detectChordBest,
12
- detectKey,
12
+ detectKeyFromNotes,
13
13
  functionOf
14
- } from "./chunk-CHMXELDS.js";
14
+ } from "./chunk-KMHOICIY.js";
15
15
  import {
16
16
  evaluateSafety
17
- } from "./chunk-EH6EM37U.js";
17
+ } from "./chunk-NYYWMV2C.js";
18
18
  import {
19
19
  chordPitchClasses,
20
20
  chordToneRole,
21
21
  createsVerticalDissonance,
22
+ intervalAboveRoot,
23
+ isChordMember,
22
24
  isLeadingToneResolution,
23
25
  isScaleTone,
24
26
  majorKey,
25
27
  makeChord
26
- } from "./chunk-MIWO425M.js";
28
+ } from "./chunk-D2FAJBYL.js";
27
29
  import {
30
+ InvalidInputError,
28
31
  assertFiniteNumber,
29
32
  assertGenerationBudget,
30
33
  assertNoteEvents,
31
34
  assertRange,
32
- assertTimeSignature
33
- } from "./chunk-FBQK3ZQN.js";
35
+ assertTimeSignature,
36
+ pitchClassOf
37
+ } from "./chunk-XEBG3G53.js";
34
38
 
35
39
  // src/analyze/timeline/index.ts
36
40
  function chordTimelineFromChords(chords, totalBeats) {
@@ -60,12 +64,18 @@ function chordTimelineFromChords(chords, totalBeats) {
60
64
  function segmentLookup(segments) {
61
65
  return (beat) => {
62
66
  assertFiniteNumber(beat, "timeline query beat");
63
- for (const seg of segments) {
64
- if (beat >= seg.startBeat && beat < seg.endBeat) {
65
- return seg.chord;
67
+ let low = 0;
68
+ let high = segments.length;
69
+ while (low < high) {
70
+ const middle = Math.floor((low + high) / 2);
71
+ if ((segments[middle]?.startBeat ?? Number.POSITIVE_INFINITY) <= beat) {
72
+ low = middle + 1;
73
+ } else {
74
+ high = middle;
66
75
  }
67
76
  }
68
- return null;
77
+ const candidate = segments[low - 1];
78
+ return candidate !== void 0 && beat >= candidate.startBeat && beat < candidate.endBeat ? candidate.chord : null;
69
79
  };
70
80
  }
71
81
  var EPS = 1e-9;
@@ -76,9 +86,6 @@ var INEXACT_CONFIDENCE_FACTOR = 0.85;
76
86
  var DIATONIC_BONUS = 0.5;
77
87
  var EXACT_BONUS = 0.5;
78
88
  var MISMATCH_PENALTY = 0.3;
79
- function pitchClass(value) {
80
- return (Math.trunc(value) % 12 + 12) % 12;
81
- }
82
89
  function scoreMatch(match, weights, maxWeight, key) {
83
90
  const rootWeight = maxWeight > 0 ? (weights[match.rootPc] ?? 0) / maxWeight : 0;
84
91
  const tones = chordPitchClasses(makeChord(match.rootPc, match.quality));
@@ -116,7 +123,7 @@ function analyzeWindow(notes, windowStart, windowEnd, ts, key) {
116
123
  const velocityFactor = note.velocity !== void 0 ? note.velocity / 127 : 1;
117
124
  const onsetInWindow = note.startBeat >= windowStart - EPS && note.startBeat < windowEnd - EPS;
118
125
  const accent = onsetInWindow ? 1 + metricWeight(note.startBeat, ts) / 3 : 1;
119
- const pc = pitchClass(note.pitch);
126
+ const pc = pitchClassOf(note.pitch);
120
127
  weights[pc] = (weights[pc] ?? 0) + overlap * velocityFactor * accent;
121
128
  if (note.pitch < lowestPitch) {
122
129
  lowestPitch = note.pitch;
@@ -133,7 +140,7 @@ function analyzeWindow(notes, windowStart, windowEnd, ts, key) {
133
140
  }
134
141
  const ranked = weights.map((weight, pc) => ({ pc, weight })).filter(({ weight }) => weight > 0).sort((a, b) => b.weight - a.weight || a.pc - b.pc);
135
142
  const selected = ranked.filter(({ weight }) => weight >= maxWeight * NOISE_THRESHOLD_RATIO).slice(0, MAX_DETECTION_PCS).map(({ pc }) => pc);
136
- const bassPc = pitchClass(lowestPitch);
143
+ const bassPc = pitchClassOf(lowestPitch);
137
144
  const anchorPc = selected.includes(bassPc) ? bassPc : selected[0] ?? bassPc;
138
145
  const detectionPitches = selected.map((pc) => pc === anchorPc ? pc : pc + 12);
139
146
  const matches = detectChord(detectionPitches);
@@ -164,24 +171,28 @@ function chordTimelineFromNotes(notes, opts = {}) {
164
171
  assertTimeSignature(ts);
165
172
  const harmonicRhythm = opts.harmonicRhythm ?? beatsPerBar(ts);
166
173
  assertRange(harmonicRhythm, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "harmonic rhythm");
167
- assertNoteEvents(notes, "timeline notes", { allowNonPositiveDuration: true });
168
- const soundingIndex = createNoteEventIndex(notes.filter((note) => note.durationBeat > 0));
174
+ const budget = opts.budget;
175
+ assertNoteEvents(notes, "timeline notes", { allowNonPositiveDuration: true, budget });
176
+ const soundingIndex = createNoteEventIndex(
177
+ notes.filter((note) => note.durationBeat > 0),
178
+ { name: "timeline notes", budget }
179
+ );
169
180
  const sounding = soundingIndex.notes.map(({ note }) => note);
170
181
  const lastNoteEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
171
182
  const totalBeats = opts.totalBeats ?? lastNoteEnd;
172
183
  assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, "timeline totalBeats");
173
- const key = opts.key ?? detectKey(sounding.map((n) => n.pitch))[0]?.key ?? majorKey(0);
184
+ const key = opts.key ?? detectKeyFromNotes(sounding)[0]?.key ?? majorKey(0);
174
185
  const segments = [];
175
186
  const segmentConfidence = [];
176
187
  const windowCount = Math.max(0, Math.ceil(totalBeats / harmonicRhythm - EPS));
177
- assertGenerationBudget(windowCount, "timeline windows");
188
+ assertGenerationBudget(windowCount, "timeline windows", budget);
178
189
  const windowNotes = Array.from({ length: windowCount }, () => []);
179
190
  let memberships = 0;
180
191
  for (const indexed of soundingIndex.notes) {
181
192
  const first = Math.max(0, Math.floor(indexed.note.startBeat / harmonicRhythm));
182
193
  const lastExclusive = Math.min(windowCount, Math.ceil(indexed.endBeat / harmonicRhythm));
183
194
  memberships += Math.max(0, lastExclusive - first);
184
- assertGenerationBudget(memberships, "timeline note-to-window memberships");
195
+ assertGenerationBudget(memberships, "timeline note-to-window memberships", budget);
185
196
  for (let window = first; window < lastExclusive; window += 1) {
186
197
  windowNotes[window]?.push(indexed.note);
187
198
  }
@@ -232,12 +243,23 @@ function detectCadences(timeline, key) {
232
243
 
233
244
  // src/analyze/arrange/internal.ts
234
245
  var EPS2 = 1e-9;
246
+ var MAX_LANE_LEAP = 24;
235
247
  function covers(note, beat) {
236
248
  return beat >= note.startBeat - EPS2 && beat < note.endBeat - EPS2;
237
249
  }
238
- function poolNotes(tracks) {
250
+ function isPercussion(role) {
251
+ return role === "drums";
252
+ }
253
+ function poolNotes(tracks, only) {
239
254
  const notes = [];
240
- for (const track of tracks) {
255
+ for (let index = 0; index < tracks.length; index += 1) {
256
+ const track = tracks[index];
257
+ if (track === void 0 || isPercussion(track.role)) {
258
+ continue;
259
+ }
260
+ if (only !== void 0 && !only.has(index)) {
261
+ continue;
262
+ }
241
263
  for (const note of track.notes) {
242
264
  if (note.durationBeat > 0) {
243
265
  notes.push(note);
@@ -249,25 +271,55 @@ function poolNotes(tracks) {
249
271
  function splitIntoSubVoices(ordered) {
250
272
  const lanes = [];
251
273
  for (const note of ordered) {
252
- const lane = lanes.find((candidate) => {
253
- const last = candidate[candidate.length - 1];
254
- return last !== void 0 && last.startBeat + last.durationBeat <= note.startBeat + EPS2;
255
- });
256
- if (lane) {
257
- lane.push(note);
274
+ let best = -1;
275
+ let bestDistance = Number.POSITIVE_INFINITY;
276
+ let bestEnd = Number.NEGATIVE_INFINITY;
277
+ for (let lane = 0; lane < lanes.length; lane += 1) {
278
+ const last = lanes[lane]?.[(lanes[lane]?.length ?? 0) - 1];
279
+ if (last === void 0) {
280
+ continue;
281
+ }
282
+ const end = last.startBeat + last.durationBeat;
283
+ if (end > note.startBeat + EPS2) {
284
+ continue;
285
+ }
286
+ const distance = Math.abs(last.pitch - note.pitch);
287
+ if (distance > MAX_LANE_LEAP) {
288
+ continue;
289
+ }
290
+ if (distance < bestDistance || distance === bestDistance && end > bestEnd) {
291
+ best = lane;
292
+ bestDistance = distance;
293
+ bestEnd = end;
294
+ }
295
+ }
296
+ if (best >= 0) {
297
+ lanes[best]?.push(note);
258
298
  } else {
259
299
  lanes.push([note]);
260
300
  }
261
301
  }
262
302
  return lanes;
263
303
  }
304
+ function lastStartingAtOrBefore(sounding, beat) {
305
+ let low = 0;
306
+ let high = sounding.length;
307
+ while (low < high) {
308
+ const middle = Math.floor((low + high) / 2);
309
+ if ((sounding[middle]?.startBeat ?? Number.POSITIVE_INFINITY) <= beat + EPS2) {
310
+ low = middle + 1;
311
+ } else {
312
+ high = middle;
313
+ }
314
+ }
315
+ return low - 1;
316
+ }
264
317
  function prepareTracks(tracks) {
265
318
  let nextId = 0;
266
319
  return tracks.map((track, index) => {
267
- const ordered = track.notes.filter((note) => note.durationBeat > 0).sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch).map((note) => ({ ...note, id: nextId++ }));
268
- const voices = splitIntoSubVoices(ordered).map((voice) => ({
269
- voice,
270
- sounding: voice.map((note, i) => {
320
+ 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 }));
321
+ const voices = splitIntoSubVoices(ordered).map((voice) => {
322
+ const sounding = voice.map((note, i) => {
271
323
  const prev = i > 0 ? voice[i - 1] : void 0;
272
324
  const prevEnd = prev === void 0 ? void 0 : prev.startBeat + prev.durationBeat;
273
325
  const contiguous = prevEnd !== void 0 && Math.abs(prevEnd - note.startBeat) <= EPS2;
@@ -277,11 +329,20 @@ function prepareTracks(tracks) {
277
329
  startBeat: note.startBeat,
278
330
  endBeat: note.startBeat + note.durationBeat
279
331
  };
280
- })
281
- }));
332
+ });
333
+ return {
334
+ voice,
335
+ sounding,
336
+ at: (beat) => {
337
+ const candidate = sounding[lastStartingAtOrBefore(sounding, beat)];
338
+ return candidate !== void 0 && covers(candidate, beat) ? candidate : void 0;
339
+ }
340
+ };
341
+ });
282
342
  return {
283
343
  name: track.name ?? `track ${index + 1}`,
284
344
  role: track.role ?? "other",
345
+ trackIndex: index,
285
346
  voices
286
347
  };
287
348
  });
@@ -297,53 +358,64 @@ var FUNCTION_WEIGHT = 0.5;
297
358
  var DISSONANCE_WEIGHT = 0.35;
298
359
  var SPAN_WEIGHT = 0.15;
299
360
  var SPAN_SATURATION = 24;
300
- function pitchClass2(pitch) {
301
- return (Math.trunc(pitch) % 12 + 12) % 12;
302
- }
303
361
  function clamp01(value) {
304
362
  return Math.min(1, Math.max(0, value));
305
363
  }
306
364
  function tensionCurve(tracks, opts = {}) {
307
365
  const ts = opts.ts ?? parseTimeSignature("4/4");
308
366
  assertTimeSignature(ts);
309
- assertGenerationBudget(tracks.length, "arrangement tracks");
367
+ const budget = opts.budget;
368
+ assertGenerationBudget(tracks.length, "arrangement tracks", budget);
310
369
  for (let index = 0; index < tracks.length; index += 1) {
311
370
  assertNoteEvents(tracks[index]?.notes ?? [], `tracks[${index}].notes`, {
312
- allowNonPositiveDuration: true
371
+ allowNonPositiveDuration: true,
372
+ budget
313
373
  });
314
374
  }
315
375
  const profile = opts.profile ?? "pop";
316
376
  const step = opts.step ?? 1;
317
377
  assertRange(step, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "tension sampling step");
318
- const pooled = poolNotes(tracks);
319
- const totalBeats = pooled.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
320
- const { timeline, key } = chordTimelineFromNotes(pooled, {
378
+ const harmonyTracks = opts.harmonyTracks === void 0 ? void 0 : new Set(opts.harmonyTracks.map(Math.trunc));
379
+ const pooled = poolNotes(tracks, harmonyTracks);
380
+ const totalBeats = poolNotes(tracks).reduce(
381
+ (end, n) => Math.max(end, n.startBeat + n.durationBeat),
382
+ 0
383
+ );
384
+ const { timeline, key } = opts.timeline === void 0 ? chordTimelineFromNotes(pooled, {
321
385
  key: opts.key,
322
386
  ts,
323
- harmonicRhythm: opts.harmonicRhythm
324
- });
387
+ harmonicRhythm: opts.harmonicRhythm,
388
+ budget
389
+ }) : {
390
+ timeline: opts.timeline,
391
+ key: opts.key ?? detectKeyFromNotes(pooled)[0]?.key ?? majorKey(0)
392
+ };
325
393
  const prepared = prepareTracks(tracks);
326
394
  const points = [];
327
395
  const sampleCount = Math.max(0, Math.ceil(totalBeats / step - EPS2));
328
- assertGenerationBudget(sampleCount, "tension samples");
396
+ assertGenerationBudget(sampleCount, "tension samples", budget);
397
+ const voiceCount = prepared.reduce((sum, track) => sum + track.voices.length, 0);
398
+ assertGenerationBudget(sampleCount * voiceCount, "tension sample-voice lookups", budget);
329
399
  for (let i = 0; i < sampleCount; i += 1) {
330
400
  const beat = i * step;
331
401
  points.push({ beat, tension: sampleTension(prepared, timeline, key, ts, profile, beat) });
332
402
  }
333
403
  return points;
334
404
  }
405
+ function tensionCurveFrom(tracks, analysis, opts = {}) {
406
+ return tensionCurve(tracks, { ...opts, timeline: analysis.timeline, key: analysis.key });
407
+ }
335
408
  function allSounding(prepared, beat) {
336
409
  const out = [];
337
410
  for (let t = 0; t < prepared.length; t += 1) {
338
411
  const track = prepared[t];
339
- if (!track) {
412
+ if (!track || isPercussion(track.role)) {
340
413
  continue;
341
414
  }
342
415
  for (const subVoice of track.voices) {
343
- for (const note of subVoice.sounding) {
344
- if (covers(note, beat)) {
345
- out.push({ pitch: note.pitch, track: t });
346
- }
416
+ const note = subVoice.at(beat);
417
+ if (note !== void 0) {
418
+ out.push({ pitch: note.pitch, track: t });
347
419
  }
348
420
  }
349
421
  }
@@ -365,18 +437,21 @@ function sampleTension(prepared, timeline, key, ts, profile, beat) {
365
437
  if (!voice) {
366
438
  continue;
367
439
  }
368
- if (chordPcs && !chordPcs.has(pitchClass2(voice.pitch))) {
440
+ if (chordPcs && !chordPcs.has(pitchClassOf(voice.pitch))) {
369
441
  nonChord += 1;
370
442
  }
371
443
  const others = sounding.filter((_, j) => j !== i).map((s) => ({ pitch: s.pitch }));
372
- const result = evaluateSafety({
373
- profile,
374
- candidatePitch: voice.pitch,
375
- chord,
376
- key,
377
- otherVoices: others,
378
- strongBeat
379
- });
444
+ const result = evaluateSafety(
445
+ {
446
+ profile,
447
+ candidatePitch: voice.pitch,
448
+ chord,
449
+ key,
450
+ otherVoices: others,
451
+ strongBeat
452
+ },
453
+ { suggestions: false }
454
+ );
380
455
  if (result.safety === 2 /* Dissonant */) {
381
456
  dissonant += 1;
382
457
  }
@@ -395,13 +470,10 @@ function sampleTension(prepared, timeline, key, ts, profile, beat) {
395
470
  }
396
471
 
397
472
  // src/analyze/voice/index.ts
398
- var EPS3 = 1e-9;
399
- function pitchClass3(pitch) {
400
- return (Math.trunc(pitch) % 12 + 12) % 12;
401
- }
402
- function intervalAboveRoot(pitch, chord) {
403
- return ((pitchClass3(pitch) - pitchClass3(chord.rootPc)) % 12 + 12) % 12;
473
+ function toVoiceNotes(events) {
474
+ return events.map((event, index) => ({ ...event, id: index, originalIndex: index }));
404
475
  }
476
+ var EPS3 = 1e-9;
405
477
  function intervalAboveBass(pitch, others, chord) {
406
478
  let bass = Number.POSITIVE_INFINITY;
407
479
  for (const ov of others) {
@@ -411,7 +483,7 @@ function intervalAboveBass(pitch, others, chord) {
411
483
  return ((pitch - bass) % 12 + 12) % 12;
412
484
  }
413
485
  const ref = chord.bassPc ?? chord.rootPc;
414
- return ((pitchClass3(pitch) - pitchClass3(ref)) % 12 + 12) % 12;
486
+ return ((pitchClassOf(pitch) - pitchClassOf(ref)) % 12 + 12) % 12;
415
487
  }
416
488
  function tensionDegree(ic) {
417
489
  if (ic === 1 || ic === 2) {
@@ -422,25 +494,27 @@ function tensionDegree(ic) {
422
494
  }
423
495
  return 13;
424
496
  }
425
- function isChordMember(pitch, chord) {
426
- return chord ? chordPitchClasses(chord).includes(pitchClass3(pitch)) : false;
427
- }
428
497
  function isStep(a, b) {
429
498
  const d = Math.abs(a - b);
430
499
  return d === 1 || d === 2;
431
500
  }
432
501
  function suspensionType(ic, delta) {
433
- let type;
434
502
  if (delta > 0) {
435
- type = "sus2-3";
436
- } else if (ic === 2) {
437
- type = "sus9-8";
438
- } else if (ic === 10 || ic === 11) {
439
- type = "sus7-6";
440
- } else {
441
- type = "sus4-3";
442
- }
443
- return { kind: "suspension", type, resolveTo: 0 };
503
+ return "sus2-3";
504
+ }
505
+ if (ic === 2) {
506
+ return "sus9-8";
507
+ }
508
+ if (ic === 10 || ic === 11) {
509
+ return "sus7-6";
510
+ }
511
+ if (ic === 5 || ic === 6) {
512
+ return "sus4-3";
513
+ }
514
+ if (ic === 8 || ic === 9) {
515
+ return "sus6-5";
516
+ }
517
+ return null;
444
518
  }
445
519
  function stepResolution(pitch, chord) {
446
520
  for (let delta = 1; delta <= 2; delta += 1) {
@@ -450,7 +524,7 @@ function stepResolution(pitch, chord) {
450
524
  }
451
525
  return void 0;
452
526
  }
453
- function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
527
+ function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat = () => []) {
454
528
  assertNoteEvents(voice, "voice notes", { allowNonPositiveDuration: true });
455
529
  const result = [];
456
530
  for (let i = 0; i < voice.length; i += 1) {
@@ -488,10 +562,11 @@ function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
488
562
  );
489
563
  if (prepared && verticallyDissonant && resolves) {
490
564
  const ic = intervalAboveBass(note.pitch, others, chord);
491
- const sus = suspensionType(ic, next.pitch - note.pitch);
492
- sus.resolveTo = next.pitch;
493
- labels.push(sus);
494
- handled = true;
565
+ const type = suspensionType(ic, next.pitch - note.pitch);
566
+ if (type !== null) {
567
+ labels.push({ kind: "suspension", type, resolveTo: next.pitch });
568
+ handled = true;
569
+ }
495
570
  }
496
571
  }
497
572
  if (!handled && chord && prev && next) {
@@ -549,7 +624,18 @@ function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
549
624
  if (next && isLeadingToneResolution(note.pitch, next.pitch, key)) {
550
625
  labels.push({ kind: "leadingTone", resolveTo: next.pitch });
551
626
  }
552
- result.push({ noteId: note.id, labels, rationale: describe(labels) });
627
+ const analyzed = {
628
+ noteId: note.id ?? i,
629
+ pitch: note.pitch,
630
+ startBeat: note.startBeat,
631
+ durationBeat: note.durationBeat,
632
+ labels,
633
+ rationale: describe(labels)
634
+ };
635
+ if (note.originalIndex !== void 0) {
636
+ analyzed.originalIndex = note.originalIndex;
637
+ }
638
+ result.push(analyzed);
553
639
  }
554
640
  return result;
555
641
  }
@@ -587,58 +673,86 @@ function otherVoicesSounding(prepared, excludeTrack, excludeVoice, beat) {
587
673
  const out = [];
588
674
  for (let t = 0; t < prepared.length; t += 1) {
589
675
  const track = prepared[t];
590
- if (!track) {
676
+ if (!track || isPercussion(track.role)) {
591
677
  continue;
592
678
  }
593
679
  for (let v = 0; v < track.voices.length; v += 1) {
594
680
  if (t === excludeTrack && v === excludeVoice) {
595
681
  continue;
596
682
  }
597
- const subVoice = track.voices[v];
598
- if (!subVoice) {
683
+ const note = track.voices[v]?.at(beat);
684
+ if (note === void 0) {
599
685
  continue;
600
686
  }
601
- for (const note of subVoice.sounding) {
602
- if (!covers(note, beat)) {
603
- continue;
604
- }
605
- const snap = { pitch: note.pitch };
606
- const previous = Math.abs(note.startBeat - beat) <= EPS2 ? note.prevPitch : note.pitch;
607
- if (previous !== void 0) {
608
- snap.prevPitch = previous;
609
- }
610
- out.push(snap);
687
+ const snap = { pitch: note.pitch };
688
+ const previous = Math.abs(note.startBeat - beat) <= EPS2 ? note.prevPitch : note.pitch;
689
+ if (previous !== void 0) {
690
+ snap.prevPitch = previous;
611
691
  }
692
+ out.push(snap);
612
693
  }
613
694
  }
614
695
  return out;
615
696
  }
697
+ function firstSegmentAfter(timeline, beat) {
698
+ let low = 0;
699
+ let high = timeline.segments.length;
700
+ while (low < high) {
701
+ const middle = Math.floor((low + high) / 2);
702
+ if ((timeline.segments[middle]?.startBeat ?? Number.POSITIVE_INFINITY) <= beat + EPS2) {
703
+ low = middle + 1;
704
+ } else {
705
+ high = middle;
706
+ }
707
+ }
708
+ return low;
709
+ }
616
710
  function evaluationBeats(note, timeline) {
617
711
  const noteEnd = note.startBeat + note.durationBeat;
618
712
  const beats = [note.startBeat];
619
- for (const segment of timeline.segments) {
620
- if (segment.startBeat > note.startBeat + EPS2 && segment.startBeat < noteEnd - EPS2) {
621
- beats.push(segment.startBeat);
713
+ for (let i = firstSegmentAfter(timeline, note.startBeat); i < timeline.segments.length; i += 1) {
714
+ const start = timeline.segments[i]?.startBeat;
715
+ if (start === void 0 || start >= noteEnd - EPS2) {
716
+ break;
622
717
  }
718
+ beats.push(start);
623
719
  }
624
720
  return beats;
625
721
  }
626
722
  function analyzeArrangement(tracks, opts = {}) {
627
723
  const ts = opts.ts ?? parseTimeSignature("4/4");
628
724
  assertTimeSignature(ts);
629
- assertGenerationBudget(tracks.length, "arrangement tracks");
725
+ const budget = opts.budget;
726
+ assertGenerationBudget(tracks.length, "arrangement tracks", budget);
727
+ let noteCount = 0;
630
728
  for (let index = 0; index < tracks.length; index += 1) {
631
- assertNoteEvents(tracks[index]?.notes ?? [], `tracks[${index}].notes`, {
632
- allowNonPositiveDuration: true
729
+ const notes = tracks[index]?.notes;
730
+ if (!Array.isArray(notes)) {
731
+ throw new InvalidInputError(
732
+ `tracks[${index}].notes must be an array; received ${typeof notes}`
733
+ );
734
+ }
735
+ assertNoteEvents(notes, `tracks[${index}].notes`, {
736
+ allowNonPositiveDuration: true,
737
+ budget
633
738
  });
739
+ noteCount += notes.length;
634
740
  }
741
+ assertGenerationBudget(noteCount, "arrangement notes", budget);
635
742
  const profile = opts.profile ?? "pop";
636
- const pooled = poolNotes(tracks);
637
- const { timeline, key, segmentConfidence } = chordTimelineFromNotes(pooled, {
743
+ const minSeverity = opts.minSeverity ?? 1 /* Warning */;
744
+ const harmonyTracks = opts.harmonyTracks === void 0 ? void 0 : new Set(opts.harmonyTracks.map(Math.trunc));
745
+ const pooled = poolNotes(tracks, harmonyTracks);
746
+ const inferred = opts.timeline === void 0 ? chordTimelineFromNotes(pooled, {
638
747
  key: opts.key,
639
748
  ts,
640
749
  harmonicRhythm: opts.harmonicRhythm
641
- });
750
+ }) : {
751
+ timeline: opts.timeline,
752
+ key: opts.key ?? detectKeyFromNotes(pooled)[0]?.key ?? majorKey(0),
753
+ segmentConfidence: opts.timeline.segments.map(() => 1)
754
+ };
755
+ const { timeline, key, segmentConfidence } = inferred;
642
756
  const cadences = detectCadences(timeline, key);
643
757
  const prepared = prepareTracks(tracks);
644
758
  const trackAnalyses = [];
@@ -648,20 +762,24 @@ function analyzeArrangement(tracks, opts = {}) {
648
762
  if (!track) {
649
763
  continue;
650
764
  }
765
+ if (isPercussion(track.role)) {
766
+ trackAnalyses.push({ name: track.name, role: track.role, notes: [] });
767
+ continue;
768
+ }
651
769
  const notes = [];
652
770
  for (let v = 0; v < track.voices.length; v += 1) {
653
771
  const subVoice = track.voices[v];
654
772
  if (!subVoice) {
655
773
  continue;
656
774
  }
657
- notes.push(
658
- ...analyzeVoice(
659
- subVoice.voice,
660
- timeline.at,
661
- key,
662
- (beat) => otherVoicesSounding(prepared, t, v, beat)
663
- )
775
+ const analyzed = analyzeVoice(
776
+ subVoice.voice,
777
+ timeline.at,
778
+ key,
779
+ (beat) => otherVoicesSounding(prepared, t, v, beat)
664
780
  );
781
+ notes.push(...analyzed.map((note) => ({ ...note, trackIndex: track.trackIndex })));
782
+ const labelsById = new Map(analyzed.map((note) => [note.noteId, note.labels]));
665
783
  for (let noteIndex = 0; noteIndex < subVoice.voice.length; noteIndex += 1) {
666
784
  const note = subVoice.voice[noteIndex];
667
785
  const preparedNote = subVoice.sounding[noteIndex];
@@ -679,14 +797,20 @@ function analyzeArrangement(tracks, opts = {}) {
679
797
  otherVoices: otherVoicesSounding(prepared, t, v, beat),
680
798
  strongBeat: isStrongBeat(beat, ts)
681
799
  });
682
- if (result.safety !== 0 /* Safe */) {
800
+ if (result.safety !== 0 /* Safe */ && result.safety >= minSeverity) {
683
801
  const conflict = {
684
802
  beat,
685
803
  trackName: track.name,
804
+ trackIndex: track.trackIndex,
805
+ noteId: note.id,
686
806
  pitch: note.pitch,
807
+ labels: labelsById.get(note.id) ?? [],
687
808
  safety: result.safety,
688
809
  reasons: result.reasons
689
810
  };
811
+ if (note.originalIndex !== void 0) {
812
+ conflict.originalIndex = note.originalIndex;
813
+ }
690
814
  if (result.rationale !== void 0) {
691
815
  conflict.rationale = result.rationale;
692
816
  }
@@ -713,7 +837,9 @@ export {
713
837
  chordTimelineFromNotes,
714
838
  detectCadences,
715
839
  tensionCurve,
840
+ tensionCurveFrom,
841
+ toVoiceNotes,
716
842
  analyzeVoice,
717
843
  analyzeArrangement
718
844
  };
719
- //# sourceMappingURL=chunk-GOAW4VYL.js.map
845
+ //# sourceMappingURL=chunk-Q2XUP227.js.map