@libraz/libcantus 0.9.2 → 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 (200) hide show
  1. package/README.md +6 -5
  2. package/dist/analyze/index.cjs +41 -1676
  3. package/dist/analyze/index.cjs.map +1 -1
  4. package/dist/analyze/index.d.cts +194 -122
  5. package/dist/analyze/index.d.ts +194 -122
  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-R3S2LUBH.js → chunk-7NKDPF74.js} +660 -305
  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-7Y7WF64D.js → chunk-D2FAJBYL.js} +104 -35
  18. package/dist/chunk-D2FAJBYL.js.map +1 -0
  19. package/dist/{chunk-COKTX2G5.js → chunk-EHVEBWM5.js} +1052 -510
  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-GQKN5KDT.js → chunk-GQSGS2N4.js} +43 -29
  26. package/dist/chunk-GQSGS2N4.js.map +1 -0
  27. package/dist/chunk-HKGYCQ34.js +53 -0
  28. package/dist/chunk-HKGYCQ34.js.map +1 -0
  29. package/dist/chunk-I24MV65U.js +106 -0
  30. package/dist/chunk-I24MV65U.js.map +1 -0
  31. package/dist/{chunk-LN7ZZMD2.js → chunk-KMHOICIY.js} +322 -173
  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-N6IHQUOX.js +1 -0
  36. package/dist/chunk-N6IHQUOX.js.map +1 -0
  37. package/dist/chunk-NKIBQ6PT.js +30 -0
  38. package/dist/chunk-NKIBQ6PT.js.map +1 -0
  39. package/dist/chunk-NP4NA2FB.cjs +106 -0
  40. package/dist/chunk-NP4NA2FB.cjs.map +1 -0
  41. package/dist/chunk-NVE34YGZ.cjs +1449 -0
  42. package/dist/chunk-NVE34YGZ.cjs.map +1 -0
  43. package/dist/{chunk-A4532ZFR.js → chunk-NYYWMV2C.js} +115 -47
  44. package/dist/chunk-NYYWMV2C.js.map +1 -0
  45. package/dist/{chunk-5HTNMQW4.js → chunk-Q2XUP227.js} +310 -118
  46. package/dist/chunk-Q2XUP227.js.map +1 -0
  47. package/dist/chunk-Q6OP4U43.cjs +327 -0
  48. package/dist/chunk-Q6OP4U43.cjs.map +1 -0
  49. package/dist/chunk-RC6GBMCD.cjs +845 -0
  50. package/dist/chunk-RC6GBMCD.cjs.map +1 -0
  51. package/dist/{chunk-XTC3X6MS.js → chunk-RK35RIN7.js} +453 -152
  52. package/dist/chunk-RK35RIN7.js.map +1 -0
  53. package/dist/chunk-RUMU3UA6.cjs +53 -0
  54. package/dist/chunk-RUMU3UA6.cjs.map +1 -0
  55. package/dist/chunk-T64C7ZD7.cjs +1 -0
  56. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  57. package/dist/chunk-XEBG3G53.js +498 -0
  58. package/dist/chunk-XEBG3G53.js.map +1 -0
  59. package/dist/chunk-YLEMGEFH.js +246 -0
  60. package/dist/chunk-YLEMGEFH.js.map +1 -0
  61. package/dist/chunk-ZEJR4TKL.cjs +173 -0
  62. package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
  63. package/dist/core/index.cjs +130 -431
  64. package/dist/core/index.cjs.map +1 -1
  65. package/dist/core/index.d.cts +331 -17
  66. package/dist/core/index.d.ts +331 -17
  67. package/dist/core/index.js +71 -13
  68. package/dist/{function-2bJW1Zp_.d.ts → function-BtiZb9i9.d.ts} +28 -7
  69. package/dist/{function-r2sh-jOO.d.cts → function-DNugAOrt.d.cts} +28 -7
  70. package/dist/generate/index.cjs +30 -3751
  71. package/dist/generate/index.cjs.map +1 -1
  72. package/dist/generate/index.d.cts +373 -59
  73. package/dist/generate/index.d.ts +373 -59
  74. package/dist/generate/index.js +23 -9
  75. package/dist/{index-DZ1M21do.d.cts → index-6VR8-kit.d.ts} +20 -4
  76. package/dist/index-70p168VM.d.ts +192 -0
  77. package/dist/{index-D_ZTUNKp.d.cts → index-Be74mcon.d.cts} +18 -17
  78. package/dist/index-BtXT6UQ0.d.cts +192 -0
  79. package/dist/{index-BMPxwjwV.d.ts → index-C5UBbQ4G.d.ts} +18 -17
  80. package/dist/{index-BOBXBNwP.d.cts → index-C83mZI1P.d.ts} +93 -8
  81. package/dist/{index-BdeQ1r_p.d.cts → index-D6b6na69.d.cts} +14 -11
  82. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  83. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  84. package/dist/index-OG65ocOr.d.cts +272 -0
  85. package/dist/index-OG65ocOr.d.ts +272 -0
  86. package/dist/{index-BreoJRw0.d.ts → index-tJhkmQgE.d.cts} +20 -4
  87. package/dist/{index-CP3wAfEH.d.ts → index-tSQh-xwY.d.ts} +14 -11
  88. package/dist/{index-CM5W4rcm.d.ts → index-y_ak9_Hi.d.cts} +93 -8
  89. package/dist/index.cjs +419 -6848
  90. package/dist/index.cjs.map +1 -1
  91. package/dist/index.d.cts +14 -13
  92. package/dist/index.d.ts +14 -13
  93. package/dist/index.js +132 -27
  94. package/dist/model/index.cjs +8 -2423
  95. package/dist/model/index.cjs.map +1 -1
  96. package/dist/model/index.d.cts +433 -74
  97. package/dist/model/index.d.ts +433 -74
  98. package/dist/model/index.js +6 -7
  99. package/dist/styled-DIRkk9Uz.d.ts +506 -0
  100. package/dist/styled-DJbkJQfj.d.cts +506 -0
  101. package/dist/theory/index.cjs +172 -1676
  102. package/dist/theory/index.cjs.map +1 -1
  103. package/dist/theory/index.d.cts +151 -233
  104. package/dist/theory/index.d.ts +151 -233
  105. package/dist/theory/index.js +31 -7
  106. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  107. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  108. package/package.json +73 -24
  109. package/dist/chunk-5HTNMQW4.js.map +0 -1
  110. package/dist/chunk-7Y7WF64D.js.map +0 -1
  111. package/dist/chunk-A4532ZFR.js.map +0 -1
  112. package/dist/chunk-BHUT2S5G.js +0 -22
  113. package/dist/chunk-BHUT2S5G.js.map +0 -1
  114. package/dist/chunk-CCBLVAPK.js +0 -42
  115. package/dist/chunk-CCBLVAPK.js.map +0 -1
  116. package/dist/chunk-COKTX2G5.js.map +0 -1
  117. package/dist/chunk-DYWFNC4A.js +0 -152
  118. package/dist/chunk-DYWFNC4A.js.map +0 -1
  119. package/dist/chunk-GQKN5KDT.js.map +0 -1
  120. package/dist/chunk-LN7ZZMD2.js.map +0 -1
  121. package/dist/chunk-MSWFZF7J.js +0 -118
  122. package/dist/chunk-MSWFZF7J.js.map +0 -1
  123. package/dist/chunk-R3S2LUBH.js.map +0 -1
  124. package/dist/chunk-SDM3VJJ5.js +0 -41
  125. package/dist/chunk-SDM3VJJ5.js.map +0 -1
  126. package/dist/chunk-XTC3X6MS.js.map +0 -1
  127. package/dist/chunk-ZXW5YIWK.js +0 -55
  128. package/dist/chunk-ZXW5YIWK.js.map +0 -1
  129. package/dist/index-CGMZNCx6.d.cts +0 -140
  130. package/dist/index-CGMZNCx6.d.ts +0 -140
  131. package/dist/styled-BjXtOFNk.d.cts +0 -282
  132. package/dist/styled-D7BAx3S3.d.ts +0 -282
  133. package/src/analyze/arrange/index.ts +0 -21
  134. package/src/analyze/arrange/internal.ts +0 -125
  135. package/src/analyze/arrange/tension.ts +0 -187
  136. package/src/analyze/arrange/tracks.ts +0 -279
  137. package/src/analyze/detect/index.ts +0 -235
  138. package/src/analyze/functional/borrowed.ts +0 -84
  139. package/src/analyze/functional/cadence.ts +0 -66
  140. package/src/analyze/functional/function.ts +0 -184
  141. package/src/analyze/functional/index.ts +0 -23
  142. package/src/analyze/functional/internal.ts +0 -29
  143. package/src/analyze/functional/roman.ts +0 -393
  144. package/src/analyze/index.ts +0 -49
  145. package/src/analyze/timeline/index.ts +0 -415
  146. package/src/analyze/voice/index.ts +0 -330
  147. package/src/core/index.ts +0 -47
  148. package/src/core/interval/index.ts +0 -81
  149. package/src/core/meter/index.ts +0 -282
  150. package/src/core/pitch/index.ts +0 -311
  151. package/src/core/random/index.ts +0 -53
  152. package/src/core/tuning/index.ts +0 -156
  153. package/src/core/types.ts +0 -29
  154. package/src/generate/bass/index.ts +0 -350
  155. package/src/generate/countermelody/index.ts +0 -379
  156. package/src/generate/drums/beat.ts +0 -266
  157. package/src/generate/drums/euclid.ts +0 -58
  158. package/src/generate/drums/fills.ts +0 -356
  159. package/src/generate/drums/ghost.ts +0 -119
  160. package/src/generate/drums/hihat.ts +0 -285
  161. package/src/generate/drums/hit.ts +0 -31
  162. package/src/generate/drums/index.ts +0 -329
  163. package/src/generate/drums/internal.ts +0 -257
  164. package/src/generate/drums/kick.ts +0 -160
  165. package/src/generate/drums/percussion.ts +0 -121
  166. package/src/generate/drums/rng.ts +0 -7
  167. package/src/generate/drums/swing.ts +0 -71
  168. package/src/generate/groove/index.ts +0 -326
  169. package/src/generate/harmonize/index.ts +0 -440
  170. package/src/generate/index.ts +0 -59
  171. package/src/generate/motif/index.ts +0 -398
  172. package/src/generate/progression/index.ts +0 -370
  173. package/src/generate/reharmony/index.ts +0 -330
  174. package/src/generate/rhythm/index.ts +0 -165
  175. package/src/index.ts +0 -247
  176. package/src/model/chord.ts +0 -423
  177. package/src/model/index.ts +0 -15
  178. package/src/model/interval.ts +0 -82
  179. package/src/model/key.ts +0 -266
  180. package/src/model/note.ts +0 -176
  181. package/src/model/progression.ts +0 -161
  182. package/src/model/shared.ts +0 -12
  183. package/src/theory/chord/index.ts +0 -373
  184. package/src/theory/chordscale/index.ts +0 -374
  185. package/src/theory/counterpoint/index.ts +0 -230
  186. package/src/theory/harmony/index.ts +0 -92
  187. package/src/theory/index.ts +0 -107
  188. package/src/theory/safety/index.ts +0 -419
  189. package/src/theory/scale/degrees.ts +0 -123
  190. package/src/theory/scale/index.ts +0 -28
  191. package/src/theory/scale/key.ts +0 -56
  192. package/src/theory/scale/masks.ts +0 -153
  193. package/src/theory/spelling/index.ts +0 -182
  194. package/src/theory/symbol/index.ts +0 -268
  195. package/src/theory/voicing/index.ts +0 -5
  196. package/src/theory/voicing/internal.ts +0 -168
  197. package/src/theory/voicing/leading.ts +0 -112
  198. package/src/theory/voicing/satb.ts +0 -195
  199. package/src/theory/voicing/styled.ts +0 -176
  200. package/tsconfig.json +0 -23
@@ -1,35 +1,58 @@
1
1
  import {
2
2
  beatsPerBar,
3
+ createNoteEventIndex,
3
4
  isStrongBeat,
4
5
  metricWeight,
5
6
  parseTimeSignature
6
- } from "./chunk-MSWFZF7J.js";
7
+ } from "./chunk-YLEMGEFH.js";
7
8
  import {
8
9
  detectCadence,
9
10
  detectChord,
10
11
  detectChordBest,
11
- detectKey,
12
+ detectKeyFromNotes,
12
13
  functionOf
13
- } from "./chunk-LN7ZZMD2.js";
14
+ } from "./chunk-KMHOICIY.js";
14
15
  import {
15
16
  evaluateSafety
16
- } from "./chunk-A4532ZFR.js";
17
+ } from "./chunk-NYYWMV2C.js";
17
18
  import {
18
19
  chordPitchClasses,
19
20
  chordToneRole,
20
21
  createsVerticalDissonance,
22
+ intervalAboveRoot,
23
+ isChordMember,
21
24
  isLeadingToneResolution,
22
25
  isScaleTone,
23
26
  majorKey,
24
27
  makeChord
25
- } from "./chunk-7Y7WF64D.js";
28
+ } from "./chunk-D2FAJBYL.js";
29
+ import {
30
+ InvalidInputError,
31
+ assertFiniteNumber,
32
+ assertGenerationBudget,
33
+ assertNoteEvents,
34
+ assertRange,
35
+ assertTimeSignature,
36
+ pitchClassOf
37
+ } from "./chunk-XEBG3G53.js";
26
38
 
27
39
  // src/analyze/timeline/index.ts
28
40
  function chordTimelineFromChords(chords, totalBeats) {
29
- const sorted = [...chords].sort((a, b) => a.startBeat - b.startBeat);
41
+ assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, "timeline totalBeats");
42
+ assertGenerationBudget(chords.length, "timeline chords");
43
+ for (let index = 0; index < chords.length; index += 1) {
44
+ assertRange(
45
+ chords[index]?.startBeat ?? Number.NaN,
46
+ 0,
47
+ Number.MAX_SAFE_INTEGER,
48
+ `chords[${index}].startBeat`
49
+ );
50
+ }
51
+ const sorted = [...chords].filter((chord) => chord.startBeat < totalBeats).sort((a, b) => a.startBeat - b.startBeat);
30
52
  const segments = sorted.map((gc, i) => {
31
53
  const next = sorted[i + 1];
32
- const endBeat = Math.max(gc.startBeat, next ? next.startBeat : totalBeats);
54
+ const nextBeat = next ? next.startBeat : totalBeats;
55
+ const endBeat = Math.min(totalBeats, Math.max(gc.startBeat, nextBeat));
33
56
  return {
34
57
  startBeat: gc.startBeat,
35
58
  endBeat,
@@ -40,12 +63,19 @@ function chordTimelineFromChords(chords, totalBeats) {
40
63
  }
41
64
  function segmentLookup(segments) {
42
65
  return (beat) => {
43
- for (const seg of segments) {
44
- if (beat >= seg.startBeat && beat < seg.endBeat) {
45
- return seg.chord;
66
+ assertFiniteNumber(beat, "timeline query beat");
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;
46
75
  }
47
76
  }
48
- return null;
77
+ const candidate = segments[low - 1];
78
+ return candidate !== void 0 && beat >= candidate.startBeat && beat < candidate.endBeat ? candidate.chord : null;
49
79
  };
50
80
  }
51
81
  var EPS = 1e-9;
@@ -56,9 +86,6 @@ var INEXACT_CONFIDENCE_FACTOR = 0.85;
56
86
  var DIATONIC_BONUS = 0.5;
57
87
  var EXACT_BONUS = 0.5;
58
88
  var MISMATCH_PENALTY = 0.3;
59
- function pitchClass(value) {
60
- return (Math.trunc(value) % 12 + 12) % 12;
61
- }
62
89
  function scoreMatch(match, weights, maxWeight, key) {
63
90
  const rootWeight = maxWeight > 0 ? (weights[match.rootPc] ?? 0) / maxWeight : 0;
64
91
  const tones = chordPitchClasses(makeChord(match.rootPc, match.quality));
@@ -96,7 +123,7 @@ function analyzeWindow(notes, windowStart, windowEnd, ts, key) {
96
123
  const velocityFactor = note.velocity !== void 0 ? note.velocity / 127 : 1;
97
124
  const onsetInWindow = note.startBeat >= windowStart - EPS && note.startBeat < windowEnd - EPS;
98
125
  const accent = onsetInWindow ? 1 + metricWeight(note.startBeat, ts) / 3 : 1;
99
- const pc = pitchClass(note.pitch);
126
+ const pc = pitchClassOf(note.pitch);
100
127
  weights[pc] = (weights[pc] ?? 0) + overlap * velocityFactor * accent;
101
128
  if (note.pitch < lowestPitch) {
102
129
  lowestPitch = note.pitch;
@@ -113,7 +140,7 @@ function analyzeWindow(notes, windowStart, windowEnd, ts, key) {
113
140
  }
114
141
  const ranked = weights.map((weight, pc) => ({ pc, weight })).filter(({ weight }) => weight > 0).sort((a, b) => b.weight - a.weight || a.pc - b.pc);
115
142
  const selected = ranked.filter(({ weight }) => weight >= maxWeight * NOISE_THRESHOLD_RATIO).slice(0, MAX_DETECTION_PCS).map(({ pc }) => pc);
116
- const bassPc = pitchClass(lowestPitch);
143
+ const bassPc = pitchClassOf(lowestPitch);
117
144
  const anchorPc = selected.includes(bassPc) ? bassPc : selected[0] ?? bassPc;
118
145
  const detectionPitches = selected.map((pc) => pc === anchorPc ? pc : pc + 12);
119
146
  const matches = detectChord(detectionPitches);
@@ -141,21 +168,39 @@ function sameChord(a, b) {
141
168
  }
142
169
  function chordTimelineFromNotes(notes, opts = {}) {
143
170
  const ts = opts.ts ?? parseTimeSignature("4/4");
171
+ assertTimeSignature(ts);
144
172
  const harmonicRhythm = opts.harmonicRhythm ?? beatsPerBar(ts);
145
- if (!(harmonicRhythm > 0)) {
146
- throw new Error(`Invalid harmonic rhythm: ${harmonicRhythm}`);
147
- }
148
- const sounding = notes.filter((note) => note.durationBeat > 0);
173
+ assertRange(harmonicRhythm, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "harmonic rhythm");
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
+ );
180
+ const sounding = soundingIndex.notes.map(({ note }) => note);
149
181
  const lastNoteEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
150
182
  const totalBeats = opts.totalBeats ?? lastNoteEnd;
151
- const key = opts.key ?? detectKey(sounding.map((n) => n.pitch))[0]?.key ?? majorKey(0);
183
+ assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, "timeline totalBeats");
184
+ const key = opts.key ?? detectKeyFromNotes(sounding)[0]?.key ?? majorKey(0);
152
185
  const segments = [];
153
186
  const segmentConfidence = [];
154
187
  const windowCount = Math.max(0, Math.ceil(totalBeats / harmonicRhythm - EPS));
188
+ assertGenerationBudget(windowCount, "timeline windows", budget);
189
+ const windowNotes = Array.from({ length: windowCount }, () => []);
190
+ let memberships = 0;
191
+ for (const indexed of soundingIndex.notes) {
192
+ const first = Math.max(0, Math.floor(indexed.note.startBeat / harmonicRhythm));
193
+ const lastExclusive = Math.min(windowCount, Math.ceil(indexed.endBeat / harmonicRhythm));
194
+ memberships += Math.max(0, lastExclusive - first);
195
+ assertGenerationBudget(memberships, "timeline note-to-window memberships", budget);
196
+ for (let window = first; window < lastExclusive; window += 1) {
197
+ windowNotes[window]?.push(indexed.note);
198
+ }
199
+ }
155
200
  for (let i = 0; i < windowCount; i += 1) {
156
201
  const start = i * harmonicRhythm;
157
202
  const end = Math.min(start + harmonicRhythm, totalBeats);
158
- const inferred = analyzeWindow(sounding, start, end, ts, key);
203
+ const inferred = analyzeWindow(windowNotes[i] ?? [], start, end, ts, key);
159
204
  if (!inferred) {
160
205
  continue;
161
206
  }
@@ -198,12 +243,23 @@ function detectCadences(timeline, key) {
198
243
 
199
244
  // src/analyze/arrange/internal.ts
200
245
  var EPS2 = 1e-9;
246
+ var MAX_LANE_LEAP = 24;
201
247
  function covers(note, beat) {
202
248
  return beat >= note.startBeat - EPS2 && beat < note.endBeat - EPS2;
203
249
  }
204
- function poolNotes(tracks) {
250
+ function isPercussion(role) {
251
+ return role === "drums";
252
+ }
253
+ function poolNotes(tracks, only) {
205
254
  const notes = [];
206
- 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
+ }
207
263
  for (const note of track.notes) {
208
264
  if (note.durationBeat > 0) {
209
265
  notes.push(note);
@@ -215,37 +271,78 @@ function poolNotes(tracks) {
215
271
  function splitIntoSubVoices(ordered) {
216
272
  const lanes = [];
217
273
  for (const note of ordered) {
218
- const lane = lanes.find((candidate) => {
219
- const last = candidate[candidate.length - 1];
220
- return last !== void 0 && last.startBeat + last.durationBeat <= note.startBeat + EPS2;
221
- });
222
- if (lane) {
223
- 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);
224
298
  } else {
225
299
  lanes.push([note]);
226
300
  }
227
301
  }
228
302
  return lanes;
229
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
+ }
230
317
  function prepareTracks(tracks) {
231
318
  let nextId = 0;
232
319
  return tracks.map((track, index) => {
233
- 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++ }));
234
- const voices = splitIntoSubVoices(ordered).map((voice) => ({
235
- voice,
236
- 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) => {
237
323
  const prev = i > 0 ? voice[i - 1] : void 0;
324
+ const prevEnd = prev === void 0 ? void 0 : prev.startBeat + prev.durationBeat;
325
+ const contiguous = prevEnd !== void 0 && Math.abs(prevEnd - note.startBeat) <= EPS2;
238
326
  return {
239
327
  pitch: note.pitch,
240
- prevPitch: prev?.pitch,
328
+ prevPitch: contiguous ? prev?.pitch : void 0,
241
329
  startBeat: note.startBeat,
242
330
  endBeat: note.startBeat + note.durationBeat
243
331
  };
244
- })
245
- }));
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
+ });
246
342
  return {
247
343
  name: track.name ?? `track ${index + 1}`,
248
344
  role: track.role ?? "other",
345
+ trackIndex: index,
249
346
  voices
250
347
  };
251
348
  });
@@ -261,47 +358,64 @@ var FUNCTION_WEIGHT = 0.5;
261
358
  var DISSONANCE_WEIGHT = 0.35;
262
359
  var SPAN_WEIGHT = 0.15;
263
360
  var SPAN_SATURATION = 24;
264
- function pitchClass2(pitch) {
265
- return (Math.trunc(pitch) % 12 + 12) % 12;
266
- }
267
361
  function clamp01(value) {
268
362
  return Math.min(1, Math.max(0, value));
269
363
  }
270
364
  function tensionCurve(tracks, opts = {}) {
271
365
  const ts = opts.ts ?? parseTimeSignature("4/4");
366
+ assertTimeSignature(ts);
367
+ const budget = opts.budget;
368
+ assertGenerationBudget(tracks.length, "arrangement tracks", budget);
369
+ for (let index = 0; index < tracks.length; index += 1) {
370
+ assertNoteEvents(tracks[index]?.notes ?? [], `tracks[${index}].notes`, {
371
+ allowNonPositiveDuration: true,
372
+ budget
373
+ });
374
+ }
272
375
  const profile = opts.profile ?? "pop";
273
376
  const step = opts.step ?? 1;
274
- if (!(step > 0)) {
275
- throw new Error(`Invalid tension sampling step: ${step}`);
276
- }
277
- const pooled = poolNotes(tracks);
278
- const totalBeats = pooled.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
279
- const { timeline, key } = chordTimelineFromNotes(pooled, {
377
+ assertRange(step, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "tension sampling step");
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, {
280
385
  key: opts.key,
281
386
  ts,
282
- harmonicRhythm: opts.harmonicRhythm
283
- });
387
+ harmonicRhythm: opts.harmonicRhythm,
388
+ budget
389
+ }) : {
390
+ timeline: opts.timeline,
391
+ key: opts.key ?? detectKeyFromNotes(pooled)[0]?.key ?? majorKey(0)
392
+ };
284
393
  const prepared = prepareTracks(tracks);
285
394
  const points = [];
286
395
  const sampleCount = Math.max(0, Math.ceil(totalBeats / step - EPS2));
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);
287
399
  for (let i = 0; i < sampleCount; i += 1) {
288
400
  const beat = i * step;
289
401
  points.push({ beat, tension: sampleTension(prepared, timeline, key, ts, profile, beat) });
290
402
  }
291
403
  return points;
292
404
  }
405
+ function tensionCurveFrom(tracks, analysis, opts = {}) {
406
+ return tensionCurve(tracks, { ...opts, timeline: analysis.timeline, key: analysis.key });
407
+ }
293
408
  function allSounding(prepared, beat) {
294
409
  const out = [];
295
410
  for (let t = 0; t < prepared.length; t += 1) {
296
411
  const track = prepared[t];
297
- if (!track) {
412
+ if (!track || isPercussion(track.role)) {
298
413
  continue;
299
414
  }
300
415
  for (const subVoice of track.voices) {
301
- for (const note of subVoice.sounding) {
302
- if (covers(note, beat)) {
303
- out.push({ pitch: note.pitch, track: t });
304
- }
416
+ const note = subVoice.at(beat);
417
+ if (note !== void 0) {
418
+ out.push({ pitch: note.pitch, track: t });
305
419
  }
306
420
  }
307
421
  }
@@ -323,18 +437,21 @@ function sampleTension(prepared, timeline, key, ts, profile, beat) {
323
437
  if (!voice) {
324
438
  continue;
325
439
  }
326
- if (chordPcs && !chordPcs.has(pitchClass2(voice.pitch))) {
440
+ if (chordPcs && !chordPcs.has(pitchClassOf(voice.pitch))) {
327
441
  nonChord += 1;
328
442
  }
329
443
  const others = sounding.filter((_, j) => j !== i).map((s) => ({ pitch: s.pitch }));
330
- const result = evaluateSafety({
331
- profile,
332
- candidatePitch: voice.pitch,
333
- chord,
334
- key,
335
- otherVoices: others,
336
- strongBeat
337
- });
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
+ );
338
455
  if (result.safety === 2 /* Dissonant */) {
339
456
  dissonant += 1;
340
457
  }
@@ -353,13 +470,10 @@ function sampleTension(prepared, timeline, key, ts, profile, beat) {
353
470
  }
354
471
 
355
472
  // src/analyze/voice/index.ts
356
- var EPS3 = 1e-9;
357
- function pitchClass3(pitch) {
358
- return (Math.trunc(pitch) % 12 + 12) % 12;
359
- }
360
- function intervalAboveRoot(pitch, chord) {
361
- return ((pitchClass3(pitch) - pitchClass3(chord.rootPc)) % 12 + 12) % 12;
473
+ function toVoiceNotes(events) {
474
+ return events.map((event, index) => ({ ...event, id: index, originalIndex: index }));
362
475
  }
476
+ var EPS3 = 1e-9;
363
477
  function intervalAboveBass(pitch, others, chord) {
364
478
  let bass = Number.POSITIVE_INFINITY;
365
479
  for (const ov of others) {
@@ -369,7 +483,7 @@ function intervalAboveBass(pitch, others, chord) {
369
483
  return ((pitch - bass) % 12 + 12) % 12;
370
484
  }
371
485
  const ref = chord.bassPc ?? chord.rootPc;
372
- return ((pitchClass3(pitch) - pitchClass3(ref)) % 12 + 12) % 12;
486
+ return ((pitchClassOf(pitch) - pitchClassOf(ref)) % 12 + 12) % 12;
373
487
  }
374
488
  function tensionDegree(ic) {
375
489
  if (ic === 1 || ic === 2) {
@@ -380,25 +494,27 @@ function tensionDegree(ic) {
380
494
  }
381
495
  return 13;
382
496
  }
383
- function isChordMember(pitch, chord) {
384
- return chord ? chordPitchClasses(chord).includes(pitchClass3(pitch)) : false;
385
- }
386
497
  function isStep(a, b) {
387
498
  const d = Math.abs(a - b);
388
499
  return d === 1 || d === 2;
389
500
  }
390
501
  function suspensionType(ic, delta) {
391
- let type;
392
502
  if (delta > 0) {
393
- type = "sus2-3";
394
- } else if (ic === 2) {
395
- type = "sus9-8";
396
- } else if (ic === 10 || ic === 11) {
397
- type = "sus7-6";
398
- } else {
399
- type = "sus4-3";
400
- }
401
- 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;
402
518
  }
403
519
  function stepResolution(pitch, chord) {
404
520
  for (let delta = 1; delta <= 2; delta += 1) {
@@ -408,7 +524,8 @@ function stepResolution(pitch, chord) {
408
524
  }
409
525
  return void 0;
410
526
  }
411
- function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
527
+ function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat = () => []) {
528
+ assertNoteEvents(voice, "voice notes", { allowNonPositiveDuration: true });
412
529
  const result = [];
413
530
  for (let i = 0; i < voice.length; i += 1) {
414
531
  const note = voice[i];
@@ -417,8 +534,10 @@ function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
417
534
  }
418
535
  const prevNote = i > 0 ? voice[i - 1] : void 0;
419
536
  const nextNote = i + 1 < voice.length ? voice[i + 1] : void 0;
420
- const prev = prevNote !== void 0 && prevNote.startBeat < note.startBeat - EPS3 ? prevNote : void 0;
421
- const next = nextNote !== void 0 && nextNote.startBeat > note.startBeat + EPS3 ? nextNote : void 0;
537
+ const prevEnd = prevNote === void 0 ? Number.NEGATIVE_INFINITY : prevNote.startBeat + prevNote.durationBeat;
538
+ const noteEnd = note.startBeat + note.durationBeat;
539
+ const prev = prevNote !== void 0 && Math.abs(prevEnd - note.startBeat) <= EPS3 ? prevNote : void 0;
540
+ const next = nextNote !== void 0 && Math.abs(noteEnd - nextNote.startBeat) <= EPS3 ? nextNote : void 0;
422
541
  const chord = chordAtBeat(note.startBeat);
423
542
  const labels = [];
424
543
  const member = isChordMember(note.pitch, chord);
@@ -443,10 +562,11 @@ function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
443
562
  );
444
563
  if (prepared && verticallyDissonant && resolves) {
445
564
  const ic = intervalAboveBass(note.pitch, others, chord);
446
- const sus = suspensionType(ic, next.pitch - note.pitch);
447
- sus.resolveTo = next.pitch;
448
- labels.push(sus);
449
- 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
+ }
450
570
  }
451
571
  }
452
572
  if (!handled && chord && prev && next) {
@@ -504,7 +624,18 @@ function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
504
624
  if (next && isLeadingToneResolution(note.pitch, next.pitch, key)) {
505
625
  labels.push({ kind: "leadingTone", resolveTo: next.pitch });
506
626
  }
507
- 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);
508
639
  }
509
640
  return result;
510
641
  }
@@ -542,50 +673,86 @@ function otherVoicesSounding(prepared, excludeTrack, excludeVoice, beat) {
542
673
  const out = [];
543
674
  for (let t = 0; t < prepared.length; t += 1) {
544
675
  const track = prepared[t];
545
- if (!track) {
676
+ if (!track || isPercussion(track.role)) {
546
677
  continue;
547
678
  }
548
679
  for (let v = 0; v < track.voices.length; v += 1) {
549
680
  if (t === excludeTrack && v === excludeVoice) {
550
681
  continue;
551
682
  }
552
- const subVoice = track.voices[v];
553
- if (!subVoice) {
683
+ const note = track.voices[v]?.at(beat);
684
+ if (note === void 0) {
554
685
  continue;
555
686
  }
556
- for (const note of subVoice.sounding) {
557
- if (!covers(note, beat)) {
558
- continue;
559
- }
560
- const snap = { pitch: note.pitch };
561
- if (note.prevPitch !== void 0) {
562
- snap.prevPitch = note.prevPitch;
563
- }
564
- 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;
565
691
  }
692
+ out.push(snap);
566
693
  }
567
694
  }
568
695
  return out;
569
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
+ }
570
710
  function evaluationBeats(note, timeline) {
571
711
  const noteEnd = note.startBeat + note.durationBeat;
572
712
  const beats = [note.startBeat];
573
- for (const segment of timeline.segments) {
574
- if (segment.startBeat > note.startBeat + EPS2 && segment.startBeat < noteEnd - EPS2) {
575
- 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;
576
717
  }
718
+ beats.push(start);
577
719
  }
578
720
  return beats;
579
721
  }
580
722
  function analyzeArrangement(tracks, opts = {}) {
581
723
  const ts = opts.ts ?? parseTimeSignature("4/4");
724
+ assertTimeSignature(ts);
725
+ const budget = opts.budget;
726
+ assertGenerationBudget(tracks.length, "arrangement tracks", budget);
727
+ let noteCount = 0;
728
+ for (let index = 0; index < tracks.length; index += 1) {
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
738
+ });
739
+ noteCount += notes.length;
740
+ }
741
+ assertGenerationBudget(noteCount, "arrangement notes", budget);
582
742
  const profile = opts.profile ?? "pop";
583
- const pooled = poolNotes(tracks);
584
- 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, {
585
747
  key: opts.key,
586
748
  ts,
587
749
  harmonicRhythm: opts.harmonicRhythm
588
- });
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;
589
756
  const cadences = detectCadences(timeline, key);
590
757
  const prepared = prepareTracks(tracks);
591
758
  const trackAnalyses = [];
@@ -595,41 +762,64 @@ function analyzeArrangement(tracks, opts = {}) {
595
762
  if (!track) {
596
763
  continue;
597
764
  }
765
+ if (isPercussion(track.role)) {
766
+ trackAnalyses.push({ name: track.name, role: track.role, notes: [] });
767
+ continue;
768
+ }
598
769
  const notes = [];
599
770
  for (let v = 0; v < track.voices.length; v += 1) {
600
771
  const subVoice = track.voices[v];
601
772
  if (!subVoice) {
602
773
  continue;
603
774
  }
604
- notes.push(
605
- ...analyzeVoice(
606
- subVoice.voice,
607
- timeline.at,
608
- key,
609
- (beat) => otherVoicesSounding(prepared, t, v, beat)
610
- )
775
+ const analyzed = analyzeVoice(
776
+ subVoice.voice,
777
+ timeline.at,
778
+ key,
779
+ (beat) => otherVoicesSounding(prepared, t, v, beat)
611
780
  );
612
- for (const note of subVoice.voice) {
781
+ notes.push(...analyzed.map((note) => ({ ...note, trackIndex: track.trackIndex })));
782
+ const labelsById = new Map(analyzed.map((note) => [note.noteId, note.labels]));
783
+ for (let noteIndex = 0; noteIndex < subVoice.voice.length; noteIndex += 1) {
784
+ const note = subVoice.voice[noteIndex];
785
+ const preparedNote = subVoice.sounding[noteIndex];
786
+ if (note === void 0 || preparedNote === void 0) {
787
+ continue;
788
+ }
613
789
  for (const beat of evaluationBeats(note, timeline)) {
790
+ const atOnset = Math.abs(beat - note.startBeat) <= EPS2;
614
791
  const result = evaluateSafety({
615
792
  profile,
616
793
  candidatePitch: note.pitch,
794
+ prevPitch: atOnset ? preparedNote.prevPitch : note.pitch,
617
795
  chord: timeline.at(beat),
618
796
  key,
619
797
  otherVoices: otherVoicesSounding(prepared, t, v, beat),
620
798
  strongBeat: isStrongBeat(beat, ts)
621
799
  });
622
- if (result.safety !== 0 /* Safe */) {
800
+ if (result.safety !== 0 /* Safe */ && result.safety >= minSeverity) {
623
801
  const conflict = {
624
802
  beat,
625
803
  trackName: track.name,
804
+ trackIndex: track.trackIndex,
805
+ noteId: note.id,
626
806
  pitch: note.pitch,
807
+ labels: labelsById.get(note.id) ?? [],
627
808
  safety: result.safety,
628
809
  reasons: result.reasons
629
810
  };
811
+ if (note.originalIndex !== void 0) {
812
+ conflict.originalIndex = note.originalIndex;
813
+ }
630
814
  if (result.rationale !== void 0) {
631
815
  conflict.rationale = result.rationale;
632
816
  }
817
+ if (result.resolveTo !== void 0) {
818
+ conflict.resolveTo = result.resolveTo;
819
+ }
820
+ if (result.suggestions !== void 0) {
821
+ conflict.suggestions = [...result.suggestions];
822
+ }
633
823
  conflicts.push(conflict);
634
824
  }
635
825
  }
@@ -647,7 +837,9 @@ export {
647
837
  chordTimelineFromNotes,
648
838
  detectCadences,
649
839
  tensionCurve,
840
+ tensionCurveFrom,
841
+ toVoiceNotes,
650
842
  analyzeVoice,
651
843
  analyzeArrangement
652
844
  };
653
- //# sourceMappingURL=chunk-5HTNMQW4.js.map
845
+ //# sourceMappingURL=chunk-Q2XUP227.js.map