@libraz/libcantus 0.9.5 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/README.md +61 -270
  2. package/README_ja.md +61 -214
  3. package/dist/analyze/index.cjs +54 -7
  4. package/dist/analyze/index.cjs.map +1 -1
  5. package/dist/analyze/index.d.cts +200 -353
  6. package/dist/analyze/index.d.ts +200 -353
  7. package/dist/analyze/index.js +56 -9
  8. package/dist/chunk-3JXCW6DX.cjs +3220 -0
  9. package/dist/chunk-3JXCW6DX.cjs.map +1 -0
  10. package/dist/chunk-4A2RMUCC.js +7647 -0
  11. package/dist/chunk-4A2RMUCC.js.map +1 -0
  12. package/dist/chunk-6XPTBE7Y.cjs +49 -0
  13. package/dist/chunk-6XPTBE7Y.cjs.map +1 -0
  14. package/dist/chunk-AFM2ULJH.cjs +5121 -0
  15. package/dist/chunk-AFM2ULJH.cjs.map +1 -0
  16. package/dist/chunk-E3F4AHAH.cjs +690 -0
  17. package/dist/chunk-E3F4AHAH.cjs.map +1 -0
  18. package/dist/{chunk-2PTFGVD7.js → chunk-ELCTZUJJ.js} +250 -132
  19. package/dist/chunk-ELCTZUJJ.js.map +1 -0
  20. package/dist/chunk-FVSI36UV.js +690 -0
  21. package/dist/chunk-FVSI36UV.js.map +1 -0
  22. package/dist/chunk-GEDTJHAC.cjs +1 -0
  23. package/dist/chunk-GEDTJHAC.cjs.map +1 -0
  24. package/dist/chunk-GOZG57ER.js +3220 -0
  25. package/dist/chunk-GOZG57ER.js.map +1 -0
  26. package/dist/chunk-H3VORETI.js +2243 -0
  27. package/dist/chunk-H3VORETI.js.map +1 -0
  28. package/dist/chunk-H42UHPYW.js +1577 -0
  29. package/dist/chunk-H42UHPYW.js.map +1 -0
  30. package/dist/chunk-HFPS7JFD.js +1 -0
  31. package/dist/chunk-INHVXP6Z.js +3053 -0
  32. package/dist/chunk-INHVXP6Z.js.map +1 -0
  33. package/dist/chunk-K4ZJUGF4.cjs +379 -0
  34. package/dist/chunk-K4ZJUGF4.cjs.map +1 -0
  35. package/dist/chunk-KNJEKLKV.js +49 -0
  36. package/dist/chunk-KNJEKLKV.js.map +1 -0
  37. package/dist/chunk-M4CWTJWJ.cjs +7647 -0
  38. package/dist/chunk-M4CWTJWJ.cjs.map +1 -0
  39. package/dist/chunk-MXO72PLY.cjs +211 -0
  40. package/dist/chunk-MXO72PLY.cjs.map +1 -0
  41. package/dist/chunk-NLEU4QSE.cjs +360 -0
  42. package/dist/chunk-NLEU4QSE.cjs.map +1 -0
  43. package/dist/chunk-OE6WK2Y2.cjs +1577 -0
  44. package/dist/chunk-OE6WK2Y2.cjs.map +1 -0
  45. package/dist/chunk-Q2WGKJNN.cjs +1064 -0
  46. package/dist/chunk-Q2WGKJNN.cjs.map +1 -0
  47. package/dist/chunk-QYQQ5J6E.js +1 -0
  48. package/dist/chunk-QYQQ5J6E.js.map +1 -0
  49. package/dist/chunk-RHIJ3S4I.cjs +1 -0
  50. package/dist/chunk-RHIJ3S4I.cjs.map +1 -0
  51. package/dist/chunk-TTCLJX6D.js +211 -0
  52. package/dist/chunk-TTCLJX6D.js.map +1 -0
  53. package/dist/chunk-UADYAYAT.cjs +3053 -0
  54. package/dist/chunk-UADYAYAT.cjs.map +1 -0
  55. package/dist/chunk-VOHZB45S.js +5121 -0
  56. package/dist/chunk-VOHZB45S.js.map +1 -0
  57. package/dist/chunk-VTI6FOLB.js +1064 -0
  58. package/dist/chunk-VTI6FOLB.js.map +1 -0
  59. package/dist/chunk-WWURE4XT.cjs +2243 -0
  60. package/dist/chunk-WWURE4XT.cjs.map +1 -0
  61. package/dist/chunk-YH3BIKHH.js +360 -0
  62. package/dist/chunk-YH3BIKHH.js.map +1 -0
  63. package/dist/core/index.cjs +93 -6
  64. package/dist/core/index.cjs.map +1 -1
  65. package/dist/core/index.d.cts +87 -346
  66. package/dist/core/index.d.ts +87 -346
  67. package/dist/core/index.js +94 -7
  68. package/dist/generate/index.cjs +89 -10
  69. package/dist/generate/index.cjs.map +1 -1
  70. package/dist/generate/index.d.cts +436 -1094
  71. package/dist/generate/index.d.ts +436 -1094
  72. package/dist/generate/index.js +94 -15
  73. package/dist/index-9wcMhG73.d.ts +1095 -0
  74. package/dist/{index-Cfx8xgH0.d.ts → index-B4o55NDr.d.cts} +13 -9
  75. package/dist/index-B9MNZboQ.d.cts +770 -0
  76. package/dist/index-B9MNZboQ.d.ts +770 -0
  77. package/dist/index-BGFW-8P2.d.cts +2307 -0
  78. package/dist/index-BO6xE9YU.d.cts +1804 -0
  79. package/dist/index-BkGmlyI3.d.cts +470 -0
  80. package/dist/index-CCNJRG-P.d.cts +1095 -0
  81. package/dist/index-CGpRv5sl.d.cts +643 -0
  82. package/dist/index-C_8o--VE.d.cts +1242 -0
  83. package/dist/index-CgHqxdps.d.ts +2307 -0
  84. package/dist/index-Chbst3Jx.d.ts +1343 -0
  85. package/dist/index-Cj49F892.d.ts +470 -0
  86. package/dist/{index-x2vcsduE.d.cts → index-CunYKLiu.d.ts} +13 -9
  87. package/dist/index-D57QRbVn.d.ts +1804 -0
  88. package/dist/index-DInQqT8Q.d.cts +66 -0
  89. package/dist/index-DInQqT8Q.d.ts +66 -0
  90. package/dist/index-Dzr13q3d.d.cts +1343 -0
  91. package/dist/index-G1uwGGWQ.d.ts +643 -0
  92. package/dist/index-szBv3496.d.ts +1242 -0
  93. package/dist/index.cjs +348 -15
  94. package/dist/index.cjs.map +1 -1
  95. package/dist/index.d.cts +17 -15
  96. package/dist/index.d.ts +17 -15
  97. package/dist/index.js +397 -64
  98. package/dist/masks-DYbE485J.d.cts +445 -0
  99. package/dist/masks-DYbE485J.d.ts +445 -0
  100. package/dist/model/index.cjs +39 -7
  101. package/dist/model/index.cjs.map +1 -1
  102. package/dist/model/index.d.cts +4659 -249
  103. package/dist/model/index.d.ts +4659 -249
  104. package/dist/model/index.js +40 -8
  105. package/dist/positional-Cptlj-xs.d.ts +287 -0
  106. package/dist/positional-DvH_sd8u.d.cts +287 -0
  107. package/dist/theory/index.cjs +101 -7
  108. package/dist/theory/index.cjs.map +1 -1
  109. package/dist/theory/index.d.cts +511 -171
  110. package/dist/theory/index.d.ts +511 -171
  111. package/dist/theory/index.js +118 -24
  112. package/package.json +6 -4
  113. package/dist/chunk-2A2QMA74.cjs +0 -261
  114. package/dist/chunk-2A2QMA74.cjs.map +0 -1
  115. package/dist/chunk-2PTFGVD7.js.map +0 -1
  116. package/dist/chunk-357ZDPVU.cjs +0 -162
  117. package/dist/chunk-357ZDPVU.cjs.map +0 -1
  118. package/dist/chunk-3KX76RLG.js +0 -1548
  119. package/dist/chunk-3KX76RLG.js.map +0 -1
  120. package/dist/chunk-4VZ5MZAM.cjs +0 -889
  121. package/dist/chunk-4VZ5MZAM.cjs.map +0 -1
  122. package/dist/chunk-7EONYVI5.js +0 -1204
  123. package/dist/chunk-7EONYVI5.js.map +0 -1
  124. package/dist/chunk-7JEP3MFY.js +0 -162
  125. package/dist/chunk-7JEP3MFY.js.map +0 -1
  126. package/dist/chunk-AID75RN3.js +0 -3176
  127. package/dist/chunk-AID75RN3.js.map +0 -1
  128. package/dist/chunk-BSLTLGM3.cjs +0 -497
  129. package/dist/chunk-BSLTLGM3.cjs.map +0 -1
  130. package/dist/chunk-CU6VG2YM.cjs +0 -109
  131. package/dist/chunk-CU6VG2YM.cjs.map +0 -1
  132. package/dist/chunk-DZMHIGI4.js +0 -337
  133. package/dist/chunk-DZMHIGI4.js.map +0 -1
  134. package/dist/chunk-EDOTEABK.js +0 -109
  135. package/dist/chunk-EDOTEABK.js.map +0 -1
  136. package/dist/chunk-HN2OEUN2.js +0 -33
  137. package/dist/chunk-HN2OEUN2.js.map +0 -1
  138. package/dist/chunk-KBTTEX7Q.js +0 -497
  139. package/dist/chunk-KBTTEX7Q.js.map +0 -1
  140. package/dist/chunk-KQN2ZYND.cjs +0 -724
  141. package/dist/chunk-KQN2ZYND.cjs.map +0 -1
  142. package/dist/chunk-MPTVJIVG.cjs +0 -3176
  143. package/dist/chunk-MPTVJIVG.cjs.map +0 -1
  144. package/dist/chunk-MTMZTMWM.cjs +0 -53
  145. package/dist/chunk-MTMZTMWM.cjs.map +0 -1
  146. package/dist/chunk-N6IHQUOX.js +0 -1
  147. package/dist/chunk-NNB67XA2.js +0 -724
  148. package/dist/chunk-NNB67XA2.js.map +0 -1
  149. package/dist/chunk-OL3IHHSJ.cjs +0 -1204
  150. package/dist/chunk-OL3IHHSJ.cjs.map +0 -1
  151. package/dist/chunk-PI3UXRWK.cjs +0 -528
  152. package/dist/chunk-PI3UXRWK.cjs.map +0 -1
  153. package/dist/chunk-R2VVFAIN.js +0 -528
  154. package/dist/chunk-R2VVFAIN.js.map +0 -1
  155. package/dist/chunk-T64C7ZD7.cjs +0 -1
  156. package/dist/chunk-T64C7ZD7.cjs.map +0 -1
  157. package/dist/chunk-WFDXOLUB.cjs +0 -337
  158. package/dist/chunk-WFDXOLUB.cjs.map +0 -1
  159. package/dist/chunk-WTAN4ACG.cjs +0 -1548
  160. package/dist/chunk-WTAN4ACG.cjs.map +0 -1
  161. package/dist/chunk-YFJXS3AU.cjs +0 -33
  162. package/dist/chunk-YFJXS3AU.cjs.map +0 -1
  163. package/dist/chunk-YUE6TJ4M.js +0 -889
  164. package/dist/chunk-YUE6TJ4M.js.map +0 -1
  165. package/dist/chunk-ZGLJM4A4.js +0 -53
  166. package/dist/chunk-ZGLJM4A4.js.map +0 -1
  167. package/dist/function-CxixTB02.d.cts +0 -299
  168. package/dist/function-DTCt675z.d.ts +0 -299
  169. package/dist/index-B2Ulxxpo.d.cts +0 -144
  170. package/dist/index-BkYJ0xMq.d.cts +0 -152
  171. package/dist/index-BuaB-ypt.d.ts +0 -144
  172. package/dist/index-CAN7v-kw.d.ts +0 -199
  173. package/dist/index-COw1Ky9g.d.ts +0 -242
  174. package/dist/index-DRtIhlJj.d.ts +0 -152
  175. package/dist/index-DXd9JikN.d.cts +0 -246
  176. package/dist/index-DXd9JikN.d.ts +0 -246
  177. package/dist/index-alrRninU.d.cts +0 -199
  178. package/dist/index-oTQbYYIs.d.cts +0 -242
  179. package/dist/index-tR9jTn26.d.cts +0 -275
  180. package/dist/index-tR9jTn26.d.ts +0 -275
  181. package/dist/styled-Bl0ZK28h.d.ts +0 -543
  182. package/dist/styled-Crv6SA4S.d.cts +0 -543
  183. package/dist/types-BN6_Q9Kl.d.cts +0 -39
  184. package/dist/types-BN6_Q9Kl.d.ts +0 -39
  185. /package/dist/{chunk-N6IHQUOX.js.map → chunk-HFPS7JFD.js.map} +0 -0
@@ -1,889 +0,0 @@
1
- import {
2
- beatsPerBar,
3
- createNoteEventIndex,
4
- isStrongBeat,
5
- metricWeight,
6
- parseTimeSignature
7
- } from "./chunk-2PTFGVD7.js";
8
- import {
9
- detectCadence,
10
- detectChord,
11
- detectKeyFromNotes,
12
- functionOf
13
- } from "./chunk-NNB67XA2.js";
14
- import {
15
- evaluateSafety
16
- } from "./chunk-DZMHIGI4.js";
17
- import {
18
- chordPitchClasses,
19
- chordToneRole,
20
- createsVerticalDissonance,
21
- intervalAboveRoot,
22
- isChordMember,
23
- isLeadingToneResolution,
24
- isScaleTone,
25
- majorKey,
26
- makeChord
27
- } from "./chunk-KBTTEX7Q.js";
28
- import {
29
- InvalidInputError,
30
- assertFiniteNumber,
31
- assertGenerationBudget,
32
- assertNoteEvents,
33
- assertRange,
34
- assertTimeSignature,
35
- pitchClassOf
36
- } from "./chunk-R2VVFAIN.js";
37
-
38
- // src/analyze/timeline/index.ts
39
- function chordTimelineFromChords(chords, totalBeats) {
40
- assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, "timeline totalBeats");
41
- assertGenerationBudget(chords.length, "timeline chords");
42
- for (let index = 0; index < chords.length; index += 1) {
43
- assertRange(
44
- chords[index]?.startBeat ?? Number.NaN,
45
- 0,
46
- Number.MAX_SAFE_INTEGER,
47
- `chords[${index}].startBeat`
48
- );
49
- }
50
- const sorted = [...chords].filter((chord) => chord.startBeat < totalBeats).sort((a, b) => a.startBeat - b.startBeat);
51
- const segments = sorted.map((gc, i) => {
52
- const next = sorted[i + 1];
53
- const nextBeat = next ? next.startBeat : totalBeats;
54
- const endBeat = Math.min(totalBeats, Math.max(gc.startBeat, nextBeat));
55
- return {
56
- startBeat: gc.startBeat,
57
- endBeat,
58
- chord: makeChord(gc.rootPc, gc.quality, gc.bassPc)
59
- };
60
- }).filter((seg) => seg.endBeat > seg.startBeat);
61
- return { at: segmentLookup(segments), segments };
62
- }
63
- function segmentLookup(segments) {
64
- return (beat) => {
65
- assertFiniteNumber(beat, "timeline query beat");
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;
74
- }
75
- }
76
- const candidate = segments[low - 1];
77
- return candidate !== void 0 && beat >= candidate.startBeat && beat < candidate.endBeat ? candidate.chord : null;
78
- };
79
- }
80
- var EPS = 1e-9;
81
- var NOISE_THRESHOLD_RATIO = 0.2;
82
- var MAX_DETECTION_PCS = 6;
83
- var INEXACT_CONFIDENCE_FACTOR = 0.85;
84
- var DIATONIC_BONUS = 0.5;
85
- var EXACT_BONUS = 0.5;
86
- var MISMATCH_PENALTY = 0.3;
87
- function scoreMatch(match, weights, maxWeight, key) {
88
- const rootWeight = maxWeight > 0 ? (weights[match.rootPc] ?? 0) / maxWeight : 0;
89
- const tones = chordPitchClasses(makeChord(match.rootPc, match.quality));
90
- let score = rootWeight;
91
- if (tones.every((pc) => isScaleTone(pc, key))) {
92
- score += DIATONIC_BONUS;
93
- }
94
- if (match.exact) {
95
- score += EXACT_BONUS;
96
- }
97
- score -= MISMATCH_PENALTY * (match.extraPcs.length + match.missingPcs.length);
98
- return score;
99
- }
100
- function chordConfidence(chord, weights, totalWeight, exact) {
101
- const tones = new Set(chordPitchClasses(chord));
102
- let chordWeight = 0;
103
- for (let pc = 0; pc < 12; pc += 1) {
104
- if (tones.has(pc)) {
105
- chordWeight += weights[pc] ?? 0;
106
- }
107
- }
108
- const raw = totalWeight > 0 ? chordWeight / totalWeight : 0;
109
- const confidence = Math.min(1, Math.max(0, raw));
110
- return exact ? confidence : confidence * INEXACT_CONFIDENCE_FACTOR;
111
- }
112
- function analyzeWindow(notes, windowStart, windowEnd, ts, key) {
113
- const weights = new Array(12).fill(0);
114
- let lowestPitch = Number.POSITIVE_INFINITY;
115
- for (const note of notes) {
116
- const noteEnd = note.startBeat + note.durationBeat;
117
- const overlap = Math.min(noteEnd, windowEnd) - Math.max(note.startBeat, windowStart);
118
- if (overlap <= EPS) {
119
- continue;
120
- }
121
- const velocityFactor = note.velocity !== void 0 ? note.velocity / 127 : 1;
122
- const onsetInWindow = note.startBeat >= windowStart - EPS && note.startBeat < windowEnd - EPS;
123
- const accent = onsetInWindow ? 1 + metricWeight(note.startBeat, ts) / 3 : 1;
124
- const pc = pitchClassOf(note.pitch);
125
- weights[pc] = (weights[pc] ?? 0) + overlap * velocityFactor * accent;
126
- if (note.pitch < lowestPitch) {
127
- lowestPitch = note.pitch;
128
- }
129
- }
130
- let totalWeight = 0;
131
- let maxWeight = 0;
132
- for (const w of weights) {
133
- totalWeight += w;
134
- maxWeight = Math.max(maxWeight, w);
135
- }
136
- if (totalWeight <= EPS) {
137
- return null;
138
- }
139
- const ranked = weights.map((weight, pc) => ({ pc, weight })).filter(({ weight }) => weight > 0).sort((a, b) => b.weight - a.weight || a.pc - b.pc);
140
- const selected = ranked.filter(({ weight }) => weight >= maxWeight * NOISE_THRESHOLD_RATIO).slice(0, MAX_DETECTION_PCS).map(({ pc }) => pc);
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" });
145
- let bestMatch = null;
146
- let bestScore = Number.NEGATIVE_INFINITY;
147
- for (const match of matches) {
148
- const score = scoreMatch(match, weights, maxWeight, key);
149
- if (score > bestScore) {
150
- bestScore = score;
151
- bestMatch = match;
152
- }
153
- }
154
- if (bestMatch) {
155
- const chord = makeChord(bestMatch.rootPc, bestMatch.quality, bestMatch.bassPc);
156
- return { chord, confidence: chordConfidence(chord, weights, totalWeight, bestMatch.exact) };
157
- }
158
- return null;
159
- }
160
- function sameChord(a, b) {
161
- return a.rootPc === b.rootPc && a.quality === b.quality && a.bassPc === b.bassPc;
162
- }
163
- function chordTimelineFromNotes(notes, opts = {}) {
164
- const ts = opts.ts ?? parseTimeSignature("4/4");
165
- assertTimeSignature(ts);
166
- const harmonicRhythm = opts.harmonicRhythm ?? beatsPerBar(ts);
167
- assertRange(harmonicRhythm, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "harmonic rhythm");
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
- );
174
- const sounding = soundingIndex.notes.map(({ note }) => note);
175
- const lastNoteEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
176
- const totalBeats = opts.totalBeats ?? lastNoteEnd;
177
- assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, "timeline totalBeats");
178
- const key = opts.key ?? detectKeyFromNotes(sounding, { budget })[0]?.key ?? majorKey(0);
179
- const segments = [];
180
- const segmentConfidence = [];
181
- const windowCount = Math.max(0, Math.ceil(totalBeats / harmonicRhythm - EPS));
182
- assertGenerationBudget(windowCount, "timeline windows", budget);
183
- const windowNotes = Array.from({ length: windowCount }, () => []);
184
- let memberships = 0;
185
- for (const indexed of soundingIndex.notes) {
186
- const first = Math.max(0, Math.floor(indexed.note.startBeat / harmonicRhythm));
187
- const lastExclusive = Math.min(windowCount, Math.ceil(indexed.endBeat / harmonicRhythm));
188
- memberships += Math.max(0, lastExclusive - first);
189
- assertGenerationBudget(memberships, "timeline note-to-window memberships", budget);
190
- for (let window = first; window < lastExclusive; window += 1) {
191
- windowNotes[window]?.push(indexed.note);
192
- }
193
- }
194
- for (let i = 0; i < windowCount; i += 1) {
195
- const start = i * harmonicRhythm;
196
- const end = Math.min(start + harmonicRhythm, totalBeats);
197
- const inferred = analyzeWindow(windowNotes[i] ?? [], start, end, ts, key);
198
- if (!inferred) {
199
- continue;
200
- }
201
- const last = segments[segments.length - 1];
202
- if (last && sameChord(last.chord, inferred.chord) && Math.abs(last.endBeat - start) < EPS) {
203
- const lastLength = last.endBeat - last.startBeat;
204
- const length = end - start;
205
- const lastConfidence = segmentConfidence[segmentConfidence.length - 1] ?? 0;
206
- segmentConfidence[segmentConfidence.length - 1] = (lastConfidence * lastLength + inferred.confidence * length) / (lastLength + length);
207
- last.endBeat = end;
208
- } else {
209
- segments.push({ startBeat: start, endBeat: end, chord: inferred.chord });
210
- segmentConfidence.push(inferred.confidence);
211
- }
212
- }
213
- return {
214
- timeline: { at: segmentLookup(segments), segments },
215
- key,
216
- segmentConfidence
217
- };
218
- }
219
- function detectCadences(timeline, key) {
220
- const hits = [];
221
- for (let i = 1; i < timeline.segments.length; i += 1) {
222
- const prev = timeline.segments[i - 1];
223
- const cur = timeline.segments[i];
224
- if (!prev || !cur) {
225
- continue;
226
- }
227
- if (Math.abs(cur.startBeat - prev.endBeat) > EPS) {
228
- continue;
229
- }
230
- const type = detectCadence(prev.chord, cur.chord, key);
231
- if (type !== null) {
232
- hits.push({ atBeat: cur.startBeat, type, from: prev.chord, to: cur.chord });
233
- }
234
- }
235
- return hits;
236
- }
237
-
238
- // src/analyze/arrange/internal.ts
239
- var EPS2 = 1e-9;
240
- var MAX_LANE_LEAP = 24;
241
- function covers(note, beat) {
242
- return beat >= note.startBeat - EPS2 && beat < note.endBeat - EPS2;
243
- }
244
- function isPercussion(role) {
245
- return role === "drums";
246
- }
247
- function poolNotes(tracks, only) {
248
- const notes = [];
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
- }
257
- for (const note of track.notes) {
258
- if (note.durationBeat > 0) {
259
- notes.push(note);
260
- }
261
- }
262
- }
263
- return notes;
264
- }
265
- function splitIntoSubVoices(ordered) {
266
- const lanes = [];
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
- }
321
- } else {
322
- const note = block[0];
323
- if (note !== void 0 && !attachNearest(note)) {
324
- lanes.push([note]);
325
- }
326
- }
327
- start = end;
328
- }
329
- return lanes;
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
- }
344
- function prepareTracks(tracks) {
345
- let nextId = 0;
346
- return tracks.map((track, index) => {
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) => {
350
- const prev = i > 0 ? voice[i - 1] : void 0;
351
- const prevEnd = prev === void 0 ? void 0 : prev.startBeat + prev.durationBeat;
352
- const contiguous = prevEnd !== void 0 && Math.abs(prevEnd - note.startBeat) <= EPS2;
353
- return {
354
- pitch: note.pitch,
355
- prevPitch: contiguous ? prev?.pitch : void 0,
356
- startBeat: note.startBeat,
357
- endBeat: note.startBeat + note.durationBeat
358
- };
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
- });
369
- return {
370
- name: track.name ?? `track ${index + 1}`,
371
- role: track.role ?? "other",
372
- trackIndex: index,
373
- voices
374
- };
375
- });
376
- }
377
-
378
- // src/analyze/arrange/tension.ts
379
- var FUNCTION_TENSION = {
380
- tonic: 0,
381
- subdominant: 0.5,
382
- dominant: 1
383
- };
384
- var FUNCTION_WEIGHT = 0.5;
385
- var DISSONANCE_WEIGHT = 0.35;
386
- var SPAN_WEIGHT = 0.15;
387
- var SPAN_SATURATION = 24;
388
- function clamp01(value) {
389
- return Math.min(1, Math.max(0, value));
390
- }
391
- function tensionCurve(tracks, opts = {}) {
392
- const ts = opts.ts ?? parseTimeSignature("4/4");
393
- assertTimeSignature(ts);
394
- const budget = opts.budget;
395
- assertGenerationBudget(tracks.length, "arrangement tracks", budget);
396
- for (let index = 0; index < tracks.length; index += 1) {
397
- assertNoteEvents(tracks[index]?.notes ?? [], `tracks[${index}].notes`, {
398
- allowNonPositiveDuration: true,
399
- budget
400
- });
401
- }
402
- const profile = opts.profile ?? "pop";
403
- const step = opts.step ?? 1;
404
- assertRange(step, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "tension sampling step");
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, {
412
- key: opts.key,
413
- ts,
414
- harmonicRhythm: opts.harmonicRhythm,
415
- budget
416
- }) : {
417
- timeline: opts.timeline,
418
- key: opts.key ?? detectKeyFromNotes(pooled, { budget })[0]?.key ?? majorKey(0)
419
- };
420
- const prepared = prepareTracks(tracks);
421
- const points = [];
422
- const sampleCount = Math.max(0, Math.ceil(totalBeats / step - EPS2));
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
- );
430
- for (let i = 0; i < sampleCount; i += 1) {
431
- const beat = i * step;
432
- points.push({ beat, tension: sampleTension(prepared, timeline, key, ts, profile, beat) });
433
- }
434
- return points;
435
- }
436
- function tensionCurveFrom(tracks, analysis, opts = {}) {
437
- return tensionCurve(tracks, { ...opts, timeline: analysis.timeline, key: analysis.key });
438
- }
439
- function allSounding(prepared, beat) {
440
- const out = [];
441
- for (let t = 0; t < prepared.length; t += 1) {
442
- const track = prepared[t];
443
- if (!track || isPercussion(track.role)) {
444
- continue;
445
- }
446
- for (const subVoice of track.voices) {
447
- const note = subVoice.at(beat);
448
- if (note !== void 0) {
449
- out.push({ pitch: note.pitch, track: t });
450
- }
451
- }
452
- }
453
- return out;
454
- }
455
- function sampleTension(prepared, timeline, key, ts, profile, beat) {
456
- const sounding = allSounding(prepared, beat);
457
- if (sounding.length === 0) {
458
- return 0;
459
- }
460
- const chord = timeline.at(beat);
461
- const functionScore = chord ? FUNCTION_TENSION[functionOf(chord, key)] : 0;
462
- const chordPcs = chord ? new Set(chordPitchClasses(chord)) : null;
463
- const strongBeat = isStrongBeat(beat, ts);
464
- let nonChord = 0;
465
- let dissonant = 0;
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;
473
- }
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
- );
490
- if (result.safety === 2 /* Dissonant */) {
491
- dissonant += count;
492
- }
493
- }
494
- const dissonanceScore = Math.max(nonChord, dissonant) / sounding.length;
495
- let low = Number.POSITIVE_INFINITY;
496
- let high = Number.NEGATIVE_INFINITY;
497
- for (const voice of sounding) {
498
- low = Math.min(low, voice.pitch);
499
- high = Math.max(high, voice.pitch);
500
- }
501
- const spanScore = Math.min(1, (high - low) / SPAN_SATURATION);
502
- return clamp01(
503
- FUNCTION_WEIGHT * functionScore + DISSONANCE_WEIGHT * dissonanceScore + SPAN_WEIGHT * spanScore
504
- );
505
- }
506
-
507
- // src/analyze/voice/index.ts
508
- function toVoiceNotes(events) {
509
- return events.map((event, index) => ({ ...event, id: index, originalIndex: index }));
510
- }
511
- var HUMANIZE_ADJACENCY = 0.05;
512
- function intervalAboveBass(pitch, others, chord) {
513
- let bass = Number.POSITIVE_INFINITY;
514
- for (const ov of others) {
515
- bass = Math.min(bass, ov.pitch);
516
- }
517
- if (Number.isFinite(bass) && bass < pitch) {
518
- return ((pitch - bass) % 12 + 12) % 12;
519
- }
520
- const ref = chord.bassPc ?? chord.rootPc;
521
- return ((pitchClassOf(pitch) - pitchClassOf(ref)) % 12 + 12) % 12;
522
- }
523
- function tensionDegree(ic) {
524
- if (ic === 1 || ic === 2) {
525
- return 9;
526
- }
527
- if (ic === 5 || ic === 6) {
528
- return 11;
529
- }
530
- return 13;
531
- }
532
- function isStep(a, b) {
533
- const d = Math.abs(a - b);
534
- return d === 1 || d === 2;
535
- }
536
- function suspensionType(ic, delta) {
537
- if (delta > 0) {
538
- return "sus2-3";
539
- }
540
- if (ic === 2) {
541
- return "sus9-8";
542
- }
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;
553
- }
554
- function stepResolution(pitch, chord) {
555
- for (let delta = 1; delta <= 2; delta += 1) {
556
- if (isChordMember(pitch - delta, chord)) {
557
- return pitch - delta;
558
- }
559
- }
560
- return void 0;
561
- }
562
- function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat = () => []) {
563
- assertNoteEvents(voice, "voice notes", { allowNonPositiveDuration: true });
564
- const result = [];
565
- for (let i = 0; i < voice.length; i += 1) {
566
- const note = voice[i];
567
- if (!note) {
568
- continue;
569
- }
570
- const prevNote = i > 0 ? voice[i - 1] : void 0;
571
- const nextNote = i + 1 < voice.length ? voice[i + 1] : void 0;
572
- const prevEnd = prevNote === void 0 ? Number.NEGATIVE_INFINITY : prevNote.startBeat + prevNote.durationBeat;
573
- const noteEnd = note.startBeat + note.durationBeat;
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;
576
- const chord = chordAtBeat(note.startBeat);
577
- const labels = [];
578
- const member = isChordMember(note.pitch, chord);
579
- let handled = false;
580
- if (chord && member) {
581
- const role = chordToneRole(note.pitch, chord);
582
- if (role) {
583
- labels.push({ kind: "chordTone", role });
584
- } else {
585
- const ic = intervalAboveRoot(note.pitch, chord);
586
- labels.push({ kind: "tension", degree: tensionDegree(ic) });
587
- }
588
- handled = true;
589
- }
590
- if (!handled && chord && prev && next) {
591
- const prevChord = chordAtBeat(prev.startBeat);
592
- const prepared = prev.pitch === note.pitch && isChordMember(prev.pitch, prevChord);
593
- const resolves = isStep(next.pitch, note.pitch);
594
- const others = otherVoicesAtBeat(note.startBeat);
595
- const verticallyDissonant = others.some(
596
- (ov) => createsVerticalDissonance(note.pitch, ov.pitch, true)
597
- );
598
- if (prepared && verticallyDissonant && resolves) {
599
- const ic = intervalAboveBass(note.pitch, others, chord);
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
- }
605
- }
606
- }
607
- if (!handled && chord && prev && next) {
608
- const prevMember = isChordMember(prev.pitch, chordAtBeat(prev.startBeat));
609
- const nextMember = isChordMember(next.pitch, chordAtBeat(next.startBeat));
610
- const up = note.pitch - prev.pitch > 0;
611
- const contInto = next.pitch - note.pitch > 0;
612
- const sameDir = note.pitch !== prev.pitch && next.pitch !== note.pitch && up === contInto;
613
- if (prevMember && nextMember && !member && isStep(note.pitch, prev.pitch) && isStep(next.pitch, note.pitch) && sameDir) {
614
- labels.push({ kind: "passing" });
615
- handled = true;
616
- }
617
- }
618
- if (!handled && chord && prev && next && !member && prev.pitch === next.pitch && isChordMember(prev.pitch, chordAtBeat(prev.startBeat)) && isStep(note.pitch, prev.pitch)) {
619
- labels.push({ kind: "neighbor" });
620
- handled = true;
621
- }
622
- if (!handled && chord && next && !member && note.pitch === next.pitch) {
623
- const nextChord = chordAtBeat(next.startBeat);
624
- if (isChordMember(next.pitch, nextChord)) {
625
- labels.push({ kind: "anticipation" });
626
- handled = true;
627
- }
628
- }
629
- if (!handled && chord && prev && next && !member) {
630
- const prevMember = isChordMember(prev.pitch, chordAtBeat(prev.startBeat));
631
- const nextMember = isChordMember(next.pitch, chordAtBeat(next.startBeat));
632
- const stepFromPrev = isStep(note.pitch, prev.pitch);
633
- const leapToNext = Math.abs(next.pitch - note.pitch) >= 3;
634
- const opposite = note.pitch - prev.pitch > 0 !== next.pitch - note.pitch > 0;
635
- if (prevMember && nextMember && stepFromPrev && leapToNext && opposite) {
636
- labels.push({ kind: "escape" });
637
- handled = true;
638
- }
639
- }
640
- if (!handled && chord && !member) {
641
- const ic = intervalAboveRoot(note.pitch, chord);
642
- const isTension = ic === 2 || ic === 5 || ic === 9;
643
- const avoid = ic === 5 && chord.intervals.includes(4) && !chord.intervals.includes(5) || ic === 11 && chord.intervals.includes(4) && chord.intervals.includes(10);
644
- if (avoid) {
645
- labels.push({ kind: "avoid" });
646
- const resolveTo = stepResolution(note.pitch, chord);
647
- if (resolveTo !== void 0) {
648
- labels.push({ kind: "needsResolution", resolveTo });
649
- }
650
- } else if (isTension) {
651
- labels.push({ kind: "tension", degree: tensionDegree(ic) });
652
- } else {
653
- const resolveTo = stepResolution(note.pitch, chord);
654
- if (resolveTo !== void 0) {
655
- labels.push({ kind: "needsResolution", resolveTo });
656
- }
657
- }
658
- }
659
- if (next && isLeadingToneResolution(note.pitch, next.pitch, key)) {
660
- labels.push({ kind: "leadingTone", resolveTo: next.pitch });
661
- }
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);
674
- }
675
- return result;
676
- }
677
- function describe(labels) {
678
- const primary = labels[0];
679
- if (!primary) {
680
- return "Unclassified note";
681
- }
682
- switch (primary.kind) {
683
- case "chordTone":
684
- return `Chord ${primary.role}`;
685
- case "tension":
686
- return `${primary.degree}th tension`;
687
- case "avoid":
688
- return "Avoid note";
689
- case "passing":
690
- return "Passing tone between chord tones";
691
- case "neighbor":
692
- return "Neighbor tone";
693
- case "suspension":
694
- return `Suspension (${primary.type})`;
695
- case "anticipation":
696
- return "Anticipation of the next chord";
697
- case "escape":
698
- return "Escape tone";
699
- case "needsResolution":
700
- return "Unresolved dissonance";
701
- case "leadingTone":
702
- return "Leading tone resolving to the tonic";
703
- }
704
- }
705
-
706
- // src/analyze/arrange/tracks.ts
707
- function soundingVoicesAt(prepared, beat) {
708
- const out = [];
709
- for (let t = 0; t < prepared.length; t += 1) {
710
- const track = prepared[t];
711
- if (!track || isPercussion(track.role)) {
712
- continue;
713
- }
714
- for (let v = 0; v < track.voices.length; v += 1) {
715
- const note = track.voices[v]?.at(beat);
716
- if (note === void 0) {
717
- continue;
718
- }
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;
723
- }
724
- out.push({ track: t, voice: v, snapshot: snap });
725
- }
726
- }
727
- return out;
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
- }
747
- function evaluationBeats(note, timeline) {
748
- const noteEnd = note.startBeat + note.durationBeat;
749
- const beats = [note.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;
754
- }
755
- beats.push(start);
756
- }
757
- return beats;
758
- }
759
- function analyzeArrangement(tracks, opts = {}) {
760
- const ts = opts.ts ?? parseTimeSignature("4/4");
761
- assertTimeSignature(ts);
762
- const budget = opts.budget;
763
- assertGenerationBudget(tracks.length, "arrangement tracks", budget);
764
- let noteCount = 0;
765
- for (let index = 0; index < tracks.length; index += 1) {
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
775
- });
776
- noteCount += notes.length;
777
- }
778
- assertGenerationBudget(noteCount, "arrangement notes", budget);
779
- const profile = opts.profile ?? "pop";
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, {
784
- key: opts.key,
785
- ts,
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;
797
- const cadences = detectCadences(timeline, key);
798
- const prepared = prepareTracks(tracks);
799
- const soundingCache = /* @__PURE__ */ new Map();
800
- const trackAnalyses = [];
801
- const conflicts = [];
802
- for (let t = 0; t < prepared.length; t += 1) {
803
- const track = prepared[t];
804
- if (!track) {
805
- continue;
806
- }
807
- if (isPercussion(track.role)) {
808
- trackAnalyses.push({ name: track.name, role: track.role, notes: [] });
809
- continue;
810
- }
811
- const notes = [];
812
- for (let v = 0; v < track.voices.length; v += 1) {
813
- const subVoice = track.voices[v];
814
- if (!subVoice) {
815
- continue;
816
- }
817
- const analyzed = analyzeVoice(
818
- subVoice.voice,
819
- timeline.at,
820
- key,
821
- (beat) => otherVoicesSounding(prepared, t, v, beat, soundingCache)
822
- );
823
- notes.push(...analyzed.map((note) => ({ ...note, trackIndex: track.trackIndex })));
824
- const labelsById = new Map(analyzed.map((note) => [note.noteId, note.labels]));
825
- for (let noteIndex = 0; noteIndex < subVoice.voice.length; noteIndex += 1) {
826
- const note = subVoice.voice[noteIndex];
827
- const preparedNote = subVoice.sounding[noteIndex];
828
- if (note === void 0 || preparedNote === void 0) {
829
- continue;
830
- }
831
- for (const beat of evaluationBeats(note, timeline)) {
832
- const atOnset = Math.abs(beat - note.startBeat) <= EPS2;
833
- const safetyQuery = {
834
- profile,
835
- candidatePitch: note.pitch,
836
- prevPitch: atOnset ? preparedNote.prevPitch : note.pitch,
837
- chord: timeline.at(beat),
838
- key,
839
- otherVoices: otherVoicesSounding(prepared, t, v, beat, soundingCache),
840
- strongBeat: isStrongBeat(beat, ts)
841
- };
842
- let result = evaluateSafety(safetyQuery, { suggestions: false });
843
- if (result.safety !== 0 /* Safe */ && result.safety >= minSeverity) {
844
- result = evaluateSafety(safetyQuery);
845
- const conflict = {
846
- beat,
847
- trackName: track.name,
848
- trackIndex: track.trackIndex,
849
- noteId: note.id,
850
- pitch: note.pitch,
851
- labels: labelsById.get(note.id) ?? [],
852
- safety: result.safety,
853
- reasons: result.reasons
854
- };
855
- if (note.originalIndex !== void 0) {
856
- conflict.originalIndex = note.originalIndex;
857
- }
858
- if (result.rationale !== void 0) {
859
- conflict.rationale = result.rationale;
860
- }
861
- if (result.resolveTo !== void 0) {
862
- conflict.resolveTo = result.resolveTo;
863
- }
864
- if (result.suggestions !== void 0) {
865
- conflict.suggestions = [...result.suggestions];
866
- }
867
- conflicts.push(conflict);
868
- }
869
- }
870
- }
871
- }
872
- notes.sort((a, b) => a.noteId - b.noteId);
873
- trackAnalyses.push({ name: track.name, role: track.role, notes });
874
- }
875
- conflicts.sort((a, b) => b.safety - a.safety || a.beat - b.beat);
876
- return { key, timeline, segmentConfidence, cadences, tracks: trackAnalyses, conflicts };
877
- }
878
-
879
- export {
880
- chordTimelineFromChords,
881
- chordTimelineFromNotes,
882
- detectCadences,
883
- tensionCurve,
884
- tensionCurveFrom,
885
- toVoiceNotes,
886
- analyzeVoice,
887
- analyzeArrangement
888
- };
889
- //# sourceMappingURL=chunk-YUE6TJ4M.js.map