@libraz/libcantus 0.9.3 → 0.9.5

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 (201) hide show
  1. package/README.md +29 -20
  2. package/README_ja.md +268 -0
  3. package/dist/analyze/index.cjs +39 -1947
  4. package/dist/analyze/index.cjs.map +1 -1
  5. package/dist/analyze/index.d.cts +161 -91
  6. package/dist/analyze/index.d.ts +161 -91
  7. package/dist/analyze/index.js +19 -8
  8. package/dist/chunk-2A2QMA74.cjs +261 -0
  9. package/dist/chunk-2A2QMA74.cjs.map +1 -0
  10. package/dist/chunk-2PTFGVD7.js +261 -0
  11. package/dist/chunk-2PTFGVD7.js.map +1 -0
  12. package/dist/chunk-357ZDPVU.cjs +162 -0
  13. package/dist/chunk-357ZDPVU.cjs.map +1 -0
  14. package/dist/chunk-3KX76RLG.js +1548 -0
  15. package/dist/chunk-3KX76RLG.js.map +1 -0
  16. package/dist/chunk-4VZ5MZAM.cjs +889 -0
  17. package/dist/chunk-4VZ5MZAM.cjs.map +1 -0
  18. package/dist/{chunk-C5XYKUVG.js → chunk-7EONYVI5.js} +471 -171
  19. package/dist/chunk-7EONYVI5.js.map +1 -0
  20. package/dist/{chunk-IT2O4XFE.js → chunk-7JEP3MFY.js} +42 -34
  21. package/dist/chunk-7JEP3MFY.js.map +1 -0
  22. package/dist/{chunk-4MHA432G.js → chunk-AID75RN3.js} +572 -250
  23. package/dist/chunk-AID75RN3.js.map +1 -0
  24. package/dist/chunk-BSLTLGM3.cjs +497 -0
  25. package/dist/chunk-BSLTLGM3.cjs.map +1 -0
  26. package/dist/chunk-CU6VG2YM.cjs +109 -0
  27. package/dist/chunk-CU6VG2YM.cjs.map +1 -0
  28. package/dist/{chunk-EH6EM37U.js → chunk-DZMHIGI4.js} +81 -59
  29. package/dist/chunk-DZMHIGI4.js.map +1 -0
  30. package/dist/{chunk-AKLYPWIN.js → chunk-EDOTEABK.js} +45 -19
  31. package/dist/chunk-EDOTEABK.js.map +1 -0
  32. package/dist/{chunk-2ZWMSJH6.js → chunk-HN2OEUN2.js} +7 -4
  33. package/dist/chunk-HN2OEUN2.js.map +1 -0
  34. package/dist/{chunk-MIWO425M.js → chunk-KBTTEX7Q.js} +128 -43
  35. package/dist/chunk-KBTTEX7Q.js.map +1 -0
  36. package/dist/chunk-KQN2ZYND.cjs +724 -0
  37. package/dist/chunk-KQN2ZYND.cjs.map +1 -0
  38. package/dist/chunk-MPTVJIVG.cjs +3176 -0
  39. package/dist/chunk-MPTVJIVG.cjs.map +1 -0
  40. package/dist/chunk-MTMZTMWM.cjs +53 -0
  41. package/dist/chunk-MTMZTMWM.cjs.map +1 -0
  42. package/dist/{chunk-CHMXELDS.js → chunk-NNB67XA2.js} +372 -197
  43. package/dist/chunk-NNB67XA2.js.map +1 -0
  44. package/dist/chunk-OL3IHHSJ.cjs +1204 -0
  45. package/dist/chunk-OL3IHHSJ.cjs.map +1 -0
  46. package/dist/chunk-PI3UXRWK.cjs +528 -0
  47. package/dist/chunk-PI3UXRWK.cjs.map +1 -0
  48. package/dist/chunk-R2VVFAIN.js +528 -0
  49. package/dist/chunk-R2VVFAIN.js.map +1 -0
  50. package/dist/chunk-T64C7ZD7.cjs +1 -0
  51. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  52. package/dist/chunk-WFDXOLUB.cjs +337 -0
  53. package/dist/chunk-WFDXOLUB.cjs.map +1 -0
  54. package/dist/chunk-WTAN4ACG.cjs +1548 -0
  55. package/dist/chunk-WTAN4ACG.cjs.map +1 -0
  56. package/dist/chunk-YFJXS3AU.cjs +33 -0
  57. package/dist/chunk-YFJXS3AU.cjs.map +1 -0
  58. package/dist/{chunk-GOAW4VYL.js → chunk-YUE6TJ4M.js} +315 -145
  59. package/dist/chunk-YUE6TJ4M.js.map +1 -0
  60. package/dist/{chunk-62QZKCJQ.js → chunk-ZGLJM4A4.js} +10 -6
  61. package/dist/chunk-ZGLJM4A4.js.map +1 -0
  62. package/dist/core/index.cjs +140 -647
  63. package/dist/core/index.cjs.map +1 -1
  64. package/dist/core/index.d.cts +313 -41
  65. package/dist/core/index.d.ts +313 -41
  66. package/dist/core/index.js +64 -16
  67. package/dist/{function-DVH4Eusr.d.ts → function-CxixTB02.d.cts} +122 -10
  68. package/dist/{function-DYt4iKsn.d.cts → function-DTCt675z.d.ts} +122 -10
  69. package/dist/generate/index.cjs +28 -4085
  70. package/dist/generate/index.cjs.map +1 -1
  71. package/dist/generate/index.d.cts +374 -66
  72. package/dist/generate/index.d.ts +374 -66
  73. package/dist/generate/index.js +23 -9
  74. package/dist/{index-Jc0Ryi79.d.ts → index-B2Ulxxpo.d.cts} +21 -5
  75. package/dist/{index-CpOsP1y9.d.cts → index-BkYJ0xMq.d.cts} +18 -17
  76. package/dist/{index-CxqBQtH1.d.cts → index-BuaB-ypt.d.ts} +21 -5
  77. package/dist/index-CAN7v-kw.d.ts +199 -0
  78. package/dist/{index-Bt4tOUD9.d.ts → index-COw1Ky9g.d.ts} +93 -8
  79. package/dist/{index-Dikst44v.d.cts → index-Cfx8xgH0.d.ts} +14 -11
  80. package/dist/{index-DxvARIP2.d.ts → index-DRtIhlJj.d.ts} +18 -17
  81. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  82. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  83. package/dist/index-alrRninU.d.cts +199 -0
  84. package/dist/{index-BNlGP86A.d.cts → index-oTQbYYIs.d.cts} +93 -8
  85. package/dist/index-tR9jTn26.d.cts +275 -0
  86. package/dist/index-tR9jTn26.d.ts +275 -0
  87. package/dist/{index-Bg5dYMmZ.d.ts → index-x2vcsduE.d.cts} +14 -11
  88. package/dist/index.cjs +429 -7383
  89. package/dist/index.cjs.map +1 -1
  90. package/dist/index.d.cts +15 -15
  91. package/dist/index.d.ts +15 -15
  92. package/dist/index.js +121 -27
  93. package/dist/model/index.cjs +8 -2562
  94. package/dist/model/index.cjs.map +1 -1
  95. package/dist/model/index.d.cts +471 -81
  96. package/dist/model/index.d.ts +471 -81
  97. package/dist/model/index.js +6 -7
  98. package/dist/styled-Bl0ZK28h.d.ts +543 -0
  99. package/dist/styled-Crv6SA4S.d.cts +543 -0
  100. package/dist/theory/index.cjs +172 -1822
  101. package/dist/theory/index.cjs.map +1 -1
  102. package/dist/theory/index.d.cts +151 -233
  103. package/dist/theory/index.d.ts +151 -233
  104. package/dist/theory/index.js +30 -7
  105. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  106. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  107. package/package.json +34 -13
  108. package/dist/chunk-2ZWMSJH6.js.map +0 -1
  109. package/dist/chunk-4MHA432G.js.map +0 -1
  110. package/dist/chunk-62QZKCJQ.js.map +0 -1
  111. package/dist/chunk-AKLYPWIN.js.map +0 -1
  112. package/dist/chunk-C5XYKUVG.js.map +0 -1
  113. package/dist/chunk-CHMXELDS.js.map +0 -1
  114. package/dist/chunk-DYWFNC4A.js +0 -152
  115. package/dist/chunk-DYWFNC4A.js.map +0 -1
  116. package/dist/chunk-EH6EM37U.js.map +0 -1
  117. package/dist/chunk-FBQK3ZQN.js +0 -131
  118. package/dist/chunk-FBQK3ZQN.js.map +0 -1
  119. package/dist/chunk-GOAW4VYL.js.map +0 -1
  120. package/dist/chunk-I5R7ZBJS.js +0 -202
  121. package/dist/chunk-I5R7ZBJS.js.map +0 -1
  122. package/dist/chunk-IT2O4XFE.js.map +0 -1
  123. package/dist/chunk-MIWO425M.js.map +0 -1
  124. package/dist/chunk-SGU7SPLK.js +0 -924
  125. package/dist/chunk-SGU7SPLK.js.map +0 -1
  126. package/dist/index-CGMZNCx6.d.cts +0 -140
  127. package/dist/index-CGMZNCx6.d.ts +0 -140
  128. package/dist/index-CnZCo29u.d.cts +0 -108
  129. package/dist/index-JnF4v9bx.d.ts +0 -108
  130. package/dist/styled-DZ7GjShb.d.ts +0 -282
  131. package/dist/styled-ZTbdaVFM.d.cts +0 -282
  132. package/src/analyze/arrange/index.ts +0 -21
  133. package/src/analyze/arrange/internal.ts +0 -127
  134. package/src/analyze/arrange/tension.ts +0 -199
  135. package/src/analyze/arrange/tracks.ts +0 -312
  136. package/src/analyze/detect/index.ts +0 -272
  137. package/src/analyze/functional/borrowed.ts +0 -84
  138. package/src/analyze/functional/cadence.ts +0 -66
  139. package/src/analyze/functional/function.ts +0 -184
  140. package/src/analyze/functional/index.ts +0 -23
  141. package/src/analyze/functional/internal.ts +0 -29
  142. package/src/analyze/functional/roman.ts +0 -402
  143. package/src/analyze/index.ts +0 -49
  144. package/src/analyze/timeline/index.ts +0 -451
  145. package/src/analyze/voice/index.ts +0 -339
  146. package/src/core/event-index/index.ts +0 -104
  147. package/src/core/index.ts +0 -61
  148. package/src/core/interval/index.ts +0 -81
  149. package/src/core/meter/index.ts +0 -302
  150. package/src/core/pitch/index.ts +0 -311
  151. package/src/core/random/index.ts +0 -76
  152. package/src/core/tuning/index.ts +0 -186
  153. package/src/core/types.ts +0 -29
  154. package/src/core/validation/index.ts +0 -119
  155. package/src/generate/bass/index.ts +0 -373
  156. package/src/generate/countermelody/index.ts +0 -470
  157. package/src/generate/drums/beat.ts +0 -266
  158. package/src/generate/drums/euclid.ts +0 -65
  159. package/src/generate/drums/fills.ts +0 -356
  160. package/src/generate/drums/ghost.ts +0 -119
  161. package/src/generate/drums/hihat.ts +0 -285
  162. package/src/generate/drums/hit.ts +0 -31
  163. package/src/generate/drums/index.ts +0 -361
  164. package/src/generate/drums/internal.ts +0 -257
  165. package/src/generate/drums/kick.ts +0 -160
  166. package/src/generate/drums/percussion.ts +0 -121
  167. package/src/generate/drums/rng.ts +0 -7
  168. package/src/generate/drums/swing.ts +0 -71
  169. package/src/generate/groove/index.ts +0 -351
  170. package/src/generate/harmonize/index.ts +0 -451
  171. package/src/generate/index.ts +0 -58
  172. package/src/generate/motif/index.ts +0 -422
  173. package/src/generate/progression/index.ts +0 -362
  174. package/src/generate/reharmony/index.ts +0 -323
  175. package/src/generate/rhythm/index.ts +0 -175
  176. package/src/index.ts +0 -13
  177. package/src/model/chord.ts +0 -442
  178. package/src/model/index.ts +0 -15
  179. package/src/model/interval.ts +0 -82
  180. package/src/model/key.ts +0 -266
  181. package/src/model/note.ts +0 -176
  182. package/src/model/progression.ts +0 -161
  183. package/src/model/shared.ts +0 -12
  184. package/src/theory/chord/index.ts +0 -375
  185. package/src/theory/chordscale/index.ts +0 -374
  186. package/src/theory/counterpoint/index.ts +0 -230
  187. package/src/theory/harmony/index.ts +0 -77
  188. package/src/theory/index.ts +0 -112
  189. package/src/theory/safety/index.ts +0 -488
  190. package/src/theory/scale/degrees.ts +0 -123
  191. package/src/theory/scale/index.ts +0 -28
  192. package/src/theory/scale/key.ts +0 -56
  193. package/src/theory/scale/masks.ts +0 -153
  194. package/src/theory/spelling/index.ts +0 -249
  195. package/src/theory/symbol/index.ts +0 -268
  196. package/src/theory/voicing/index.ts +0 -5
  197. package/src/theory/voicing/internal.ts +0 -168
  198. package/src/theory/voicing/leading.ts +0 -112
  199. package/src/theory/voicing/satb.ts +0 -216
  200. package/src/theory/voicing/styled.ts +0 -199
  201. package/tsconfig.json +0 -23
@@ -4,33 +4,36 @@ import {
4
4
  isStrongBeat,
5
5
  metricWeight,
6
6
  parseTimeSignature
7
- } from "./chunk-I5R7ZBJS.js";
7
+ } from "./chunk-2PTFGVD7.js";
8
8
  import {
9
9
  detectCadence,
10
10
  detectChord,
11
- detectChordBest,
12
- detectKey,
11
+ detectKeyFromNotes,
13
12
  functionOf
14
- } from "./chunk-CHMXELDS.js";
13
+ } from "./chunk-NNB67XA2.js";
15
14
  import {
16
15
  evaluateSafety
17
- } from "./chunk-EH6EM37U.js";
16
+ } from "./chunk-DZMHIGI4.js";
18
17
  import {
19
18
  chordPitchClasses,
20
19
  chordToneRole,
21
20
  createsVerticalDissonance,
21
+ intervalAboveRoot,
22
+ isChordMember,
22
23
  isLeadingToneResolution,
23
24
  isScaleTone,
24
25
  majorKey,
25
26
  makeChord
26
- } from "./chunk-MIWO425M.js";
27
+ } from "./chunk-KBTTEX7Q.js";
27
28
  import {
29
+ InvalidInputError,
28
30
  assertFiniteNumber,
29
31
  assertGenerationBudget,
30
32
  assertNoteEvents,
31
33
  assertRange,
32
- assertTimeSignature
33
- } from "./chunk-FBQK3ZQN.js";
34
+ assertTimeSignature,
35
+ pitchClassOf
36
+ } from "./chunk-R2VVFAIN.js";
34
37
 
35
38
  // src/analyze/timeline/index.ts
36
39
  function chordTimelineFromChords(chords, totalBeats) {
@@ -60,25 +63,27 @@ function chordTimelineFromChords(chords, totalBeats) {
60
63
  function segmentLookup(segments) {
61
64
  return (beat) => {
62
65
  assertFiniteNumber(beat, "timeline query beat");
63
- for (const seg of segments) {
64
- if (beat >= seg.startBeat && beat < seg.endBeat) {
65
- return seg.chord;
66
+ let low = 0;
67
+ let high = segments.length;
68
+ while (low < high) {
69
+ const middle = Math.floor((low + high) / 2);
70
+ if ((segments[middle]?.startBeat ?? Number.POSITIVE_INFINITY) <= beat) {
71
+ low = middle + 1;
72
+ } else {
73
+ high = middle;
66
74
  }
67
75
  }
68
- return null;
76
+ const candidate = segments[low - 1];
77
+ return candidate !== void 0 && beat >= candidate.startBeat && beat < candidate.endBeat ? candidate.chord : null;
69
78
  };
70
79
  }
71
80
  var EPS = 1e-9;
72
81
  var NOISE_THRESHOLD_RATIO = 0.2;
73
82
  var MAX_DETECTION_PCS = 6;
74
- var FALLBACK_PCS = 3;
75
83
  var INEXACT_CONFIDENCE_FACTOR = 0.85;
76
84
  var DIATONIC_BONUS = 0.5;
77
85
  var EXACT_BONUS = 0.5;
78
86
  var MISMATCH_PENALTY = 0.3;
79
- function pitchClass(value) {
80
- return (Math.trunc(value) % 12 + 12) % 12;
81
- }
82
87
  function scoreMatch(match, weights, maxWeight, key) {
83
88
  const rootWeight = maxWeight > 0 ? (weights[match.rootPc] ?? 0) / maxWeight : 0;
84
89
  const tones = chordPitchClasses(makeChord(match.rootPc, match.quality));
@@ -116,7 +121,7 @@ function analyzeWindow(notes, windowStart, windowEnd, ts, key) {
116
121
  const velocityFactor = note.velocity !== void 0 ? note.velocity / 127 : 1;
117
122
  const onsetInWindow = note.startBeat >= windowStart - EPS && note.startBeat < windowEnd - EPS;
118
123
  const accent = onsetInWindow ? 1 + metricWeight(note.startBeat, ts) / 3 : 1;
119
- const pc = pitchClass(note.pitch);
124
+ const pc = pitchClassOf(note.pitch);
120
125
  weights[pc] = (weights[pc] ?? 0) + overlap * velocityFactor * accent;
121
126
  if (note.pitch < lowestPitch) {
122
127
  lowestPitch = note.pitch;
@@ -133,10 +138,10 @@ function analyzeWindow(notes, windowStart, windowEnd, ts, key) {
133
138
  }
134
139
  const ranked = weights.map((weight, pc) => ({ pc, weight })).filter(({ weight }) => weight > 0).sort((a, b) => b.weight - a.weight || a.pc - b.pc);
135
140
  const selected = ranked.filter(({ weight }) => weight >= maxWeight * NOISE_THRESHOLD_RATIO).slice(0, MAX_DETECTION_PCS).map(({ pc }) => pc);
136
- const bassPc = pitchClass(lowestPitch);
137
- const anchorPc = selected.includes(bassPc) ? bassPc : selected[0] ?? bassPc;
138
- const detectionPitches = selected.map((pc) => pc === anchorPc ? pc : pc + 12);
139
- const matches = detectChord(detectionPitches);
141
+ const bassPc = pitchClassOf(lowestPitch);
142
+ const hasBass = selected.includes(bassPc);
143
+ const detectionPitches = hasBass ? selected.map((pc) => pc === bassPc ? pc : pc + 12) : selected;
144
+ const matches = detectChord(detectionPitches, { input: hasBass ? "midi" : "pitchClass" });
140
145
  let bestMatch = null;
141
146
  let bestScore = Number.NEGATIVE_INFINITY;
142
147
  for (const match of matches) {
@@ -150,10 +155,6 @@ function analyzeWindow(notes, windowStart, windowEnd, ts, key) {
150
155
  const chord = makeChord(bestMatch.rootPc, bestMatch.quality, bestMatch.bassPc);
151
156
  return { chord, confidence: chordConfidence(chord, weights, totalWeight, bestMatch.exact) };
152
157
  }
153
- const fallback = detectChordBest(selected.slice(0, FALLBACK_PCS));
154
- if (fallback) {
155
- return { chord: fallback, confidence: chordConfidence(fallback, weights, totalWeight, false) };
156
- }
157
158
  return null;
158
159
  }
159
160
  function sameChord(a, b) {
@@ -164,24 +165,28 @@ function chordTimelineFromNotes(notes, opts = {}) {
164
165
  assertTimeSignature(ts);
165
166
  const harmonicRhythm = opts.harmonicRhythm ?? beatsPerBar(ts);
166
167
  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));
168
+ const budget = opts.budget;
169
+ assertNoteEvents(notes, "timeline notes", { allowNonPositiveDuration: true, budget });
170
+ const soundingIndex = createNoteEventIndex(
171
+ notes.filter((note) => note.durationBeat > 0),
172
+ { name: "timeline notes", budget }
173
+ );
169
174
  const sounding = soundingIndex.notes.map(({ note }) => note);
170
175
  const lastNoteEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
171
176
  const totalBeats = opts.totalBeats ?? lastNoteEnd;
172
177
  assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, "timeline totalBeats");
173
- const key = opts.key ?? detectKey(sounding.map((n) => n.pitch))[0]?.key ?? majorKey(0);
178
+ const key = opts.key ?? detectKeyFromNotes(sounding, { budget })[0]?.key ?? majorKey(0);
174
179
  const segments = [];
175
180
  const segmentConfidence = [];
176
181
  const windowCount = Math.max(0, Math.ceil(totalBeats / harmonicRhythm - EPS));
177
- assertGenerationBudget(windowCount, "timeline windows");
182
+ assertGenerationBudget(windowCount, "timeline windows", budget);
178
183
  const windowNotes = Array.from({ length: windowCount }, () => []);
179
184
  let memberships = 0;
180
185
  for (const indexed of soundingIndex.notes) {
181
186
  const first = Math.max(0, Math.floor(indexed.note.startBeat / harmonicRhythm));
182
187
  const lastExclusive = Math.min(windowCount, Math.ceil(indexed.endBeat / harmonicRhythm));
183
188
  memberships += Math.max(0, lastExclusive - first);
184
- assertGenerationBudget(memberships, "timeline note-to-window memberships");
189
+ assertGenerationBudget(memberships, "timeline note-to-window memberships", budget);
185
190
  for (let window = first; window < lastExclusive; window += 1) {
186
191
  windowNotes[window]?.push(indexed.note);
187
192
  }
@@ -232,12 +237,23 @@ function detectCadences(timeline, key) {
232
237
 
233
238
  // src/analyze/arrange/internal.ts
234
239
  var EPS2 = 1e-9;
240
+ var MAX_LANE_LEAP = 24;
235
241
  function covers(note, beat) {
236
242
  return beat >= note.startBeat - EPS2 && beat < note.endBeat - EPS2;
237
243
  }
238
- function poolNotes(tracks) {
244
+ function isPercussion(role) {
245
+ return role === "drums";
246
+ }
247
+ function poolNotes(tracks, only) {
239
248
  const notes = [];
240
- for (const track of tracks) {
249
+ for (let index = 0; index < tracks.length; index += 1) {
250
+ const track = tracks[index];
251
+ if (track === void 0 || isPercussion(track.role)) {
252
+ continue;
253
+ }
254
+ if (only !== void 0 && !only.has(index)) {
255
+ continue;
256
+ }
241
257
  for (const note of track.notes) {
242
258
  if (note.durationBeat > 0) {
243
259
  notes.push(note);
@@ -248,26 +264,89 @@ function poolNotes(tracks) {
248
264
  }
249
265
  function splitIntoSubVoices(ordered) {
250
266
  const lanes = [];
251
- 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);
267
+ const attachNearest = (note, excluded = /* @__PURE__ */ new Set()) => {
268
+ let best = -1;
269
+ let bestDistance = Number.POSITIVE_INFINITY;
270
+ let bestEnd = Number.NEGATIVE_INFINITY;
271
+ for (let lane = 0; lane < lanes.length; lane += 1) {
272
+ if (excluded.has(lane)) {
273
+ continue;
274
+ }
275
+ const last = lanes[lane]?.[(lanes[lane]?.length ?? 0) - 1];
276
+ if (last === void 0) {
277
+ continue;
278
+ }
279
+ const end = last.startBeat + last.durationBeat;
280
+ if (end > note.startBeat + EPS2) {
281
+ continue;
282
+ }
283
+ const distance = Math.abs(last.pitch - note.pitch);
284
+ if (distance > MAX_LANE_LEAP) {
285
+ continue;
286
+ }
287
+ if (distance < bestDistance || distance === bestDistance && end > bestEnd) {
288
+ best = lane;
289
+ bestDistance = distance;
290
+ bestEnd = end;
291
+ }
292
+ }
293
+ if (best >= 0) {
294
+ lanes[best]?.push(note);
295
+ return true;
296
+ }
297
+ return false;
298
+ };
299
+ for (let start = 0; start < ordered.length; ) {
300
+ const onset = ordered[start]?.startBeat ?? 0;
301
+ let end = start + 1;
302
+ while (end < ordered.length && Math.abs((ordered[end]?.startBeat ?? 0) - onset) <= EPS2) {
303
+ end += 1;
304
+ }
305
+ const block = ordered.slice(start, end);
306
+ if (block.length > 1) {
307
+ const freeLanes = lanes.map((lane, index) => ({ index, last: lane[lane.length - 1] })).filter(
308
+ (entry) => entry.last !== void 0 && entry.last.startBeat + entry.last.durationBeat <= onset + EPS2
309
+ ).sort((a, b) => a.last.pitch - b.last.pitch);
310
+ const paired = /* @__PURE__ */ new Set();
311
+ for (let index = 0; index < block.length; index += 1) {
312
+ const note = block[index];
313
+ const lane = freeLanes[index];
314
+ if (note !== void 0 && lane !== void 0 && Math.abs(lane.last.pitch - note.pitch) <= MAX_LANE_LEAP) {
315
+ lanes[lane.index]?.push(note);
316
+ paired.add(lane.index);
317
+ } else if (note !== void 0 && !attachNearest(note, paired)) {
318
+ lanes.push([note]);
319
+ }
320
+ }
258
321
  } else {
259
- lanes.push([note]);
322
+ const note = block[0];
323
+ if (note !== void 0 && !attachNearest(note)) {
324
+ lanes.push([note]);
325
+ }
260
326
  }
327
+ start = end;
261
328
  }
262
329
  return lanes;
263
330
  }
331
+ function lastStartingAtOrBefore(sounding, beat) {
332
+ let low = 0;
333
+ let high = sounding.length;
334
+ while (low < high) {
335
+ const middle = Math.floor((low + high) / 2);
336
+ if ((sounding[middle]?.startBeat ?? Number.POSITIVE_INFINITY) <= beat + EPS2) {
337
+ low = middle + 1;
338
+ } else {
339
+ high = middle;
340
+ }
341
+ }
342
+ return low - 1;
343
+ }
264
344
  function prepareTracks(tracks) {
265
345
  let nextId = 0;
266
346
  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) => {
347
+ 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 }));
348
+ const voices = splitIntoSubVoices(ordered).map((voice) => {
349
+ const sounding = voice.map((note, i) => {
271
350
  const prev = i > 0 ? voice[i - 1] : void 0;
272
351
  const prevEnd = prev === void 0 ? void 0 : prev.startBeat + prev.durationBeat;
273
352
  const contiguous = prevEnd !== void 0 && Math.abs(prevEnd - note.startBeat) <= EPS2;
@@ -277,11 +356,20 @@ function prepareTracks(tracks) {
277
356
  startBeat: note.startBeat,
278
357
  endBeat: note.startBeat + note.durationBeat
279
358
  };
280
- })
281
- }));
359
+ });
360
+ return {
361
+ voice,
362
+ sounding,
363
+ at: (beat) => {
364
+ const candidate = sounding[lastStartingAtOrBefore(sounding, beat)];
365
+ return candidate !== void 0 && covers(candidate, beat) ? candidate : void 0;
366
+ }
367
+ };
368
+ });
282
369
  return {
283
370
  name: track.name ?? `track ${index + 1}`,
284
371
  role: track.role ?? "other",
372
+ trackIndex: index,
285
373
  voices
286
374
  };
287
375
  });
@@ -297,53 +385,68 @@ var FUNCTION_WEIGHT = 0.5;
297
385
  var DISSONANCE_WEIGHT = 0.35;
298
386
  var SPAN_WEIGHT = 0.15;
299
387
  var SPAN_SATURATION = 24;
300
- function pitchClass2(pitch) {
301
- return (Math.trunc(pitch) % 12 + 12) % 12;
302
- }
303
388
  function clamp01(value) {
304
389
  return Math.min(1, Math.max(0, value));
305
390
  }
306
391
  function tensionCurve(tracks, opts = {}) {
307
392
  const ts = opts.ts ?? parseTimeSignature("4/4");
308
393
  assertTimeSignature(ts);
309
- assertGenerationBudget(tracks.length, "arrangement tracks");
394
+ const budget = opts.budget;
395
+ assertGenerationBudget(tracks.length, "arrangement tracks", budget);
310
396
  for (let index = 0; index < tracks.length; index += 1) {
311
397
  assertNoteEvents(tracks[index]?.notes ?? [], `tracks[${index}].notes`, {
312
- allowNonPositiveDuration: true
398
+ allowNonPositiveDuration: true,
399
+ budget
313
400
  });
314
401
  }
315
402
  const profile = opts.profile ?? "pop";
316
403
  const step = opts.step ?? 1;
317
404
  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, {
405
+ const harmonyTracks = opts.harmonyTracks === void 0 ? void 0 : new Set(opts.harmonyTracks.map(Math.trunc));
406
+ const pooled = poolNotes(tracks, harmonyTracks);
407
+ const totalBeats = poolNotes(tracks).reduce(
408
+ (end, n) => Math.max(end, n.startBeat + n.durationBeat),
409
+ 0
410
+ );
411
+ const { timeline, key } = opts.timeline === void 0 ? chordTimelineFromNotes(pooled, {
321
412
  key: opts.key,
322
413
  ts,
323
- harmonicRhythm: opts.harmonicRhythm
324
- });
414
+ harmonicRhythm: opts.harmonicRhythm,
415
+ budget
416
+ }) : {
417
+ timeline: opts.timeline,
418
+ key: opts.key ?? detectKeyFromNotes(pooled, { budget })[0]?.key ?? majorKey(0)
419
+ };
325
420
  const prepared = prepareTracks(tracks);
326
421
  const points = [];
327
422
  const sampleCount = Math.max(0, Math.ceil(totalBeats / step - EPS2));
328
- assertGenerationBudget(sampleCount, "tension samples");
423
+ assertGenerationBudget(sampleCount, "tension samples", budget);
424
+ const voiceCount = prepared.reduce((sum, track) => sum + track.voices.length, 0);
425
+ assertGenerationBudget(
426
+ sampleCount * Math.min(128, voiceCount) * voiceCount,
427
+ "tension sample-voice comparisons",
428
+ budget
429
+ );
329
430
  for (let i = 0; i < sampleCount; i += 1) {
330
431
  const beat = i * step;
331
432
  points.push({ beat, tension: sampleTension(prepared, timeline, key, ts, profile, beat) });
332
433
  }
333
434
  return points;
334
435
  }
436
+ function tensionCurveFrom(tracks, analysis, opts = {}) {
437
+ return tensionCurve(tracks, { ...opts, timeline: analysis.timeline, key: analysis.key });
438
+ }
335
439
  function allSounding(prepared, beat) {
336
440
  const out = [];
337
441
  for (let t = 0; t < prepared.length; t += 1) {
338
442
  const track = prepared[t];
339
- if (!track) {
443
+ if (!track || isPercussion(track.role)) {
340
444
  continue;
341
445
  }
342
446
  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
- }
447
+ const note = subVoice.at(beat);
448
+ if (note !== void 0) {
449
+ out.push({ pitch: note.pitch, track: t });
347
450
  }
348
451
  }
349
452
  }
@@ -360,25 +463,32 @@ function sampleTension(prepared, timeline, key, ts, profile, beat) {
360
463
  const strongBeat = isStrongBeat(beat, ts);
361
464
  let nonChord = 0;
362
465
  let dissonant = 0;
363
- for (let i = 0; i < sounding.length; i += 1) {
364
- const voice = sounding[i];
365
- if (!voice) {
366
- continue;
466
+ const pitches = /* @__PURE__ */ new Map();
467
+ for (const voice of sounding) {
468
+ pitches.set(voice.pitch, (pitches.get(voice.pitch) ?? 0) + 1);
469
+ }
470
+ for (const [pitch, count] of pitches) {
471
+ if (chordPcs && !chordPcs.has(pitchClassOf(pitch))) {
472
+ nonChord += count;
367
473
  }
368
- if (chordPcs && !chordPcs.has(pitchClass2(voice.pitch))) {
369
- nonChord += 1;
370
- }
371
- 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
- });
474
+ const others = [...pitches].flatMap(
475
+ ([otherPitch, otherCount]) => Array.from({ length: otherCount - (otherPitch === pitch ? 1 : 0) }, () => ({
476
+ pitch: otherPitch
477
+ }))
478
+ );
479
+ const result = evaluateSafety(
480
+ {
481
+ profile,
482
+ candidatePitch: pitch,
483
+ chord,
484
+ key,
485
+ otherVoices: others,
486
+ strongBeat
487
+ },
488
+ { suggestions: false }
489
+ );
380
490
  if (result.safety === 2 /* Dissonant */) {
381
- dissonant += 1;
491
+ dissonant += count;
382
492
  }
383
493
  }
384
494
  const dissonanceScore = Math.max(nonChord, dissonant) / sounding.length;
@@ -395,13 +505,10 @@ function sampleTension(prepared, timeline, key, ts, profile, beat) {
395
505
  }
396
506
 
397
507
  // 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;
508
+ function toVoiceNotes(events) {
509
+ return events.map((event, index) => ({ ...event, id: index, originalIndex: index }));
404
510
  }
511
+ var HUMANIZE_ADJACENCY = 0.05;
405
512
  function intervalAboveBass(pitch, others, chord) {
406
513
  let bass = Number.POSITIVE_INFINITY;
407
514
  for (const ov of others) {
@@ -411,7 +518,7 @@ function intervalAboveBass(pitch, others, chord) {
411
518
  return ((pitch - bass) % 12 + 12) % 12;
412
519
  }
413
520
  const ref = chord.bassPc ?? chord.rootPc;
414
- return ((pitchClass3(pitch) - pitchClass3(ref)) % 12 + 12) % 12;
521
+ return ((pitchClassOf(pitch) - pitchClassOf(ref)) % 12 + 12) % 12;
415
522
  }
416
523
  function tensionDegree(ic) {
417
524
  if (ic === 1 || ic === 2) {
@@ -422,25 +529,27 @@ function tensionDegree(ic) {
422
529
  }
423
530
  return 13;
424
531
  }
425
- function isChordMember(pitch, chord) {
426
- return chord ? chordPitchClasses(chord).includes(pitchClass3(pitch)) : false;
427
- }
428
532
  function isStep(a, b) {
429
533
  const d = Math.abs(a - b);
430
534
  return d === 1 || d === 2;
431
535
  }
432
536
  function suspensionType(ic, delta) {
433
- let type;
434
537
  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";
538
+ return "sus2-3";
539
+ }
540
+ if (ic === 2) {
541
+ return "sus9-8";
442
542
  }
443
- return { kind: "suspension", type, resolveTo: 0 };
543
+ if (ic === 10 || ic === 11) {
544
+ return "sus7-6";
545
+ }
546
+ if (ic === 5 || ic === 6) {
547
+ return "sus4-3";
548
+ }
549
+ if (ic === 8 || ic === 9) {
550
+ return "sus6-5";
551
+ }
552
+ return null;
444
553
  }
445
554
  function stepResolution(pitch, chord) {
446
555
  for (let delta = 1; delta <= 2; delta += 1) {
@@ -450,7 +559,7 @@ function stepResolution(pitch, chord) {
450
559
  }
451
560
  return void 0;
452
561
  }
453
- function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
562
+ function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat = () => []) {
454
563
  assertNoteEvents(voice, "voice notes", { allowNonPositiveDuration: true });
455
564
  const result = [];
456
565
  for (let i = 0; i < voice.length; i += 1) {
@@ -462,8 +571,8 @@ function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
462
571
  const nextNote = i + 1 < voice.length ? voice[i + 1] : void 0;
463
572
  const prevEnd = prevNote === void 0 ? Number.NEGATIVE_INFINITY : prevNote.startBeat + prevNote.durationBeat;
464
573
  const noteEnd = note.startBeat + note.durationBeat;
465
- const prev = prevNote !== void 0 && Math.abs(prevEnd - note.startBeat) <= EPS3 ? prevNote : void 0;
466
- const next = nextNote !== void 0 && Math.abs(noteEnd - nextNote.startBeat) <= EPS3 ? nextNote : void 0;
574
+ const prev = prevNote !== void 0 && Math.abs(prevEnd - note.startBeat) <= HUMANIZE_ADJACENCY ? prevNote : void 0;
575
+ const next = nextNote !== void 0 && Math.abs(noteEnd - nextNote.startBeat) <= HUMANIZE_ADJACENCY ? nextNote : void 0;
467
576
  const chord = chordAtBeat(note.startBeat);
468
577
  const labels = [];
469
578
  const member = isChordMember(note.pitch, chord);
@@ -488,10 +597,11 @@ function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
488
597
  );
489
598
  if (prepared && verticallyDissonant && resolves) {
490
599
  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;
600
+ const type = suspensionType(ic, next.pitch - note.pitch);
601
+ if (type !== null) {
602
+ labels.push({ kind: "suspension", type, resolveTo: next.pitch });
603
+ handled = true;
604
+ }
495
605
  }
496
606
  }
497
607
  if (!handled && chord && prev && next) {
@@ -549,7 +659,18 @@ function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
549
659
  if (next && isLeadingToneResolution(note.pitch, next.pitch, key)) {
550
660
  labels.push({ kind: "leadingTone", resolveTo: next.pitch });
551
661
  }
552
- result.push({ noteId: note.id, labels, rationale: describe(labels) });
662
+ const analyzed = {
663
+ noteId: note.id ?? i,
664
+ pitch: note.pitch,
665
+ startBeat: note.startBeat,
666
+ durationBeat: note.durationBeat,
667
+ labels,
668
+ rationale: describe(labels)
669
+ };
670
+ if (note.originalIndex !== void 0) {
671
+ analyzed.originalIndex = note.originalIndex;
672
+ }
673
+ result.push(analyzed);
553
674
  }
554
675
  return result;
555
676
  }
@@ -583,64 +704,99 @@ function describe(labels) {
583
704
  }
584
705
 
585
706
  // src/analyze/arrange/tracks.ts
586
- function otherVoicesSounding(prepared, excludeTrack, excludeVoice, beat) {
707
+ function soundingVoicesAt(prepared, beat) {
587
708
  const out = [];
588
709
  for (let t = 0; t < prepared.length; t += 1) {
589
710
  const track = prepared[t];
590
- if (!track) {
711
+ if (!track || isPercussion(track.role)) {
591
712
  continue;
592
713
  }
593
714
  for (let v = 0; v < track.voices.length; v += 1) {
594
- if (t === excludeTrack && v === excludeVoice) {
715
+ const note = track.voices[v]?.at(beat);
716
+ if (note === void 0) {
595
717
  continue;
596
718
  }
597
- const subVoice = track.voices[v];
598
- if (!subVoice) {
599
- continue;
600
- }
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);
719
+ const snap = { pitch: note.pitch };
720
+ const previous = Math.abs(note.startBeat - beat) <= EPS2 ? note.prevPitch : note.pitch;
721
+ if (previous !== void 0) {
722
+ snap.prevPitch = previous;
611
723
  }
724
+ out.push({ track: t, voice: v, snapshot: snap });
612
725
  }
613
726
  }
614
727
  return out;
615
728
  }
729
+ function otherVoicesSounding(prepared, excludeTrack, excludeVoice, beat, cache) {
730
+ const sounding = cache.get(beat) ?? soundingVoicesAt(prepared, beat);
731
+ cache.set(beat, sounding);
732
+ return sounding.filter(({ track, voice }) => track !== excludeTrack || voice !== excludeVoice).map(({ snapshot }) => snapshot);
733
+ }
734
+ function firstSegmentAfter(timeline, beat) {
735
+ let low = 0;
736
+ let high = timeline.segments.length;
737
+ while (low < high) {
738
+ const middle = Math.floor((low + high) / 2);
739
+ if ((timeline.segments[middle]?.startBeat ?? Number.POSITIVE_INFINITY) <= beat + EPS2) {
740
+ low = middle + 1;
741
+ } else {
742
+ high = middle;
743
+ }
744
+ }
745
+ return low;
746
+ }
616
747
  function evaluationBeats(note, timeline) {
617
748
  const noteEnd = note.startBeat + note.durationBeat;
618
749
  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);
750
+ for (let i = firstSegmentAfter(timeline, note.startBeat); i < timeline.segments.length; i += 1) {
751
+ const start = timeline.segments[i]?.startBeat;
752
+ if (start === void 0 || start >= noteEnd - EPS2) {
753
+ break;
622
754
  }
755
+ beats.push(start);
623
756
  }
624
757
  return beats;
625
758
  }
626
759
  function analyzeArrangement(tracks, opts = {}) {
627
760
  const ts = opts.ts ?? parseTimeSignature("4/4");
628
761
  assertTimeSignature(ts);
629
- assertGenerationBudget(tracks.length, "arrangement tracks");
762
+ const budget = opts.budget;
763
+ assertGenerationBudget(tracks.length, "arrangement tracks", budget);
764
+ let noteCount = 0;
630
765
  for (let index = 0; index < tracks.length; index += 1) {
631
- assertNoteEvents(tracks[index]?.notes ?? [], `tracks[${index}].notes`, {
632
- allowNonPositiveDuration: true
766
+ const notes = tracks[index]?.notes;
767
+ if (!Array.isArray(notes)) {
768
+ throw new InvalidInputError(
769
+ `tracks[${index}].notes must be an array; received ${typeof notes}`
770
+ );
771
+ }
772
+ assertNoteEvents(notes, `tracks[${index}].notes`, {
773
+ allowNonPositiveDuration: true,
774
+ budget
633
775
  });
776
+ noteCount += notes.length;
634
777
  }
778
+ assertGenerationBudget(noteCount, "arrangement notes", budget);
635
779
  const profile = opts.profile ?? "pop";
636
- const pooled = poolNotes(tracks);
637
- const { timeline, key, segmentConfidence } = chordTimelineFromNotes(pooled, {
780
+ const minSeverity = opts.minSeverity ?? 1 /* Warning */;
781
+ const harmonyTracks = opts.harmonyTracks === void 0 ? void 0 : new Set(opts.harmonyTracks.map(Math.trunc));
782
+ const pooled = poolNotes(tracks, harmonyTracks);
783
+ const inferred = opts.timeline === void 0 ? chordTimelineFromNotes(pooled, {
638
784
  key: opts.key,
639
785
  ts,
640
- harmonicRhythm: opts.harmonicRhythm
641
- });
786
+ harmonicRhythm: opts.harmonicRhythm,
787
+ budget
788
+ }) : {
789
+ timeline: opts.timeline,
790
+ key: opts.key ?? detectKeyFromNotes(pooled, { budget })[0]?.key ?? majorKey(0),
791
+ // A caller-provided timeline was not inferred from these notes, so
792
+ // it has no measured confidence. Reporting 1 would turn confidence
793
+ // gates into unconditional passes.
794
+ segmentConfidence: opts.timeline.segments.map(() => 0)
795
+ };
796
+ const { timeline, key, segmentConfidence } = inferred;
642
797
  const cadences = detectCadences(timeline, key);
643
798
  const prepared = prepareTracks(tracks);
799
+ const soundingCache = /* @__PURE__ */ new Map();
644
800
  const trackAnalyses = [];
645
801
  const conflicts = [];
646
802
  for (let t = 0; t < prepared.length; t += 1) {
@@ -648,20 +804,24 @@ function analyzeArrangement(tracks, opts = {}) {
648
804
  if (!track) {
649
805
  continue;
650
806
  }
807
+ if (isPercussion(track.role)) {
808
+ trackAnalyses.push({ name: track.name, role: track.role, notes: [] });
809
+ continue;
810
+ }
651
811
  const notes = [];
652
812
  for (let v = 0; v < track.voices.length; v += 1) {
653
813
  const subVoice = track.voices[v];
654
814
  if (!subVoice) {
655
815
  continue;
656
816
  }
657
- notes.push(
658
- ...analyzeVoice(
659
- subVoice.voice,
660
- timeline.at,
661
- key,
662
- (beat) => otherVoicesSounding(prepared, t, v, beat)
663
- )
817
+ const analyzed = analyzeVoice(
818
+ subVoice.voice,
819
+ timeline.at,
820
+ key,
821
+ (beat) => otherVoicesSounding(prepared, t, v, beat, soundingCache)
664
822
  );
823
+ notes.push(...analyzed.map((note) => ({ ...note, trackIndex: track.trackIndex })));
824
+ const labelsById = new Map(analyzed.map((note) => [note.noteId, note.labels]));
665
825
  for (let noteIndex = 0; noteIndex < subVoice.voice.length; noteIndex += 1) {
666
826
  const note = subVoice.voice[noteIndex];
667
827
  const preparedNote = subVoice.sounding[noteIndex];
@@ -670,23 +830,31 @@ function analyzeArrangement(tracks, opts = {}) {
670
830
  }
671
831
  for (const beat of evaluationBeats(note, timeline)) {
672
832
  const atOnset = Math.abs(beat - note.startBeat) <= EPS2;
673
- const result = evaluateSafety({
833
+ const safetyQuery = {
674
834
  profile,
675
835
  candidatePitch: note.pitch,
676
836
  prevPitch: atOnset ? preparedNote.prevPitch : note.pitch,
677
837
  chord: timeline.at(beat),
678
838
  key,
679
- otherVoices: otherVoicesSounding(prepared, t, v, beat),
839
+ otherVoices: otherVoicesSounding(prepared, t, v, beat, soundingCache),
680
840
  strongBeat: isStrongBeat(beat, ts)
681
- });
682
- if (result.safety !== 0 /* Safe */) {
841
+ };
842
+ let result = evaluateSafety(safetyQuery, { suggestions: false });
843
+ if (result.safety !== 0 /* Safe */ && result.safety >= minSeverity) {
844
+ result = evaluateSafety(safetyQuery);
683
845
  const conflict = {
684
846
  beat,
685
847
  trackName: track.name,
848
+ trackIndex: track.trackIndex,
849
+ noteId: note.id,
686
850
  pitch: note.pitch,
851
+ labels: labelsById.get(note.id) ?? [],
687
852
  safety: result.safety,
688
853
  reasons: result.reasons
689
854
  };
855
+ if (note.originalIndex !== void 0) {
856
+ conflict.originalIndex = note.originalIndex;
857
+ }
690
858
  if (result.rationale !== void 0) {
691
859
  conflict.rationale = result.rationale;
692
860
  }
@@ -713,7 +881,9 @@ export {
713
881
  chordTimelineFromNotes,
714
882
  detectCadences,
715
883
  tensionCurve,
884
+ tensionCurveFrom,
885
+ toVoiceNotes,
716
886
  analyzeVoice,
717
887
  analyzeArrangement
718
888
  };
719
- //# sourceMappingURL=chunk-GOAW4VYL.js.map
889
+ //# sourceMappingURL=chunk-YUE6TJ4M.js.map