@libraz/libcantus 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +27 -65
  2. package/README_ja.md +28 -66
  3. package/dist/analyze/index.cjs +11 -7
  4. package/dist/analyze/index.cjs.map +1 -1
  5. package/dist/analyze/index.d.cts +79 -144
  6. package/dist/analyze/index.d.ts +79 -144
  7. package/dist/analyze/index.js +20 -16
  8. package/dist/chunk-35NBV2FT.cjs +145 -0
  9. package/dist/chunk-35NBV2FT.cjs.map +1 -0
  10. package/dist/{chunk-UADYAYAT.cjs → chunk-3R3QVKY5.cjs} +1353 -580
  11. package/dist/chunk-3R3QVKY5.cjs.map +1 -0
  12. package/dist/{chunk-WWURE4XT.cjs → chunk-76ZBTRG2.cjs} +624 -729
  13. package/dist/chunk-76ZBTRG2.cjs.map +1 -0
  14. package/dist/{chunk-FVSI36UV.js → chunk-7KG3RTYS.js} +197 -110
  15. package/dist/chunk-7KG3RTYS.js.map +1 -0
  16. package/dist/{chunk-VTI6FOLB.js → chunk-AGOVW7U6.js} +709 -133
  17. package/dist/chunk-AGOVW7U6.js.map +1 -0
  18. package/dist/{chunk-E3F4AHAH.cjs → chunk-CGBADIO5.cjs} +221 -134
  19. package/dist/chunk-CGBADIO5.cjs.map +1 -0
  20. package/dist/{chunk-6XH6QZYQ.cjs → chunk-DSMJRXCZ.cjs} +1455 -849
  21. package/dist/chunk-DSMJRXCZ.cjs.map +1 -0
  22. package/dist/{chunk-PKFNUHDY.js → chunk-FBH75DAY.js} +1218 -612
  23. package/dist/chunk-FBH75DAY.js.map +1 -0
  24. package/dist/chunk-H2KJHBW4.js +3329 -0
  25. package/dist/chunk-H2KJHBW4.js.map +1 -0
  26. package/dist/chunk-HPVSH7SF.cjs +2294 -0
  27. package/dist/chunk-HPVSH7SF.cjs.map +1 -0
  28. package/dist/{chunk-MXO72PLY.cjs → chunk-HWHGOVO6.cjs} +21 -19
  29. package/dist/chunk-HWHGOVO6.cjs.map +1 -0
  30. package/dist/chunk-HYJJ2V2I.js +2294 -0
  31. package/dist/chunk-HYJJ2V2I.js.map +1 -0
  32. package/dist/{chunk-AFM2ULJH.cjs → chunk-J7FHVZII.cjs} +1567 -1067
  33. package/dist/chunk-J7FHVZII.cjs.map +1 -0
  34. package/dist/chunk-K6OZXIR6.js +1 -0
  35. package/dist/{chunk-H3VORETI.js → chunk-M5RYMAFI.js} +487 -592
  36. package/dist/chunk-M5RYMAFI.js.map +1 -0
  37. package/dist/chunk-NB25P3EH.js +363 -0
  38. package/dist/chunk-NB25P3EH.js.map +1 -0
  39. package/dist/{chunk-TTCLJX6D.js → chunk-NFJ4P6ZZ.js} +10 -8
  40. package/dist/chunk-NFJ4P6ZZ.js.map +1 -0
  41. package/dist/chunk-NM7UAMC3.cjs +457 -0
  42. package/dist/chunk-NM7UAMC3.cjs.map +1 -0
  43. package/dist/chunk-PM4XJE2Y.cjs +363 -0
  44. package/dist/chunk-PM4XJE2Y.cjs.map +1 -0
  45. package/dist/chunk-PUKJP2Z5.js +145 -0
  46. package/dist/chunk-PUKJP2Z5.js.map +1 -0
  47. package/dist/{chunk-VOHZB45S.js → chunk-Q6LFSAKV.js} +1321 -821
  48. package/dist/chunk-Q6LFSAKV.js.map +1 -0
  49. package/dist/{chunk-INHVXP6Z.js → chunk-RFIBF2RR.js} +1203 -430
  50. package/dist/chunk-RFIBF2RR.js.map +1 -0
  51. package/dist/{chunk-Q2WGKJNN.cjs → chunk-S5BKZZGM.cjs} +699 -123
  52. package/dist/chunk-S5BKZZGM.cjs.map +1 -0
  53. package/dist/chunk-SGLEQVJH.cjs +3329 -0
  54. package/dist/chunk-SGLEQVJH.cjs.map +1 -0
  55. package/dist/chunk-UGLFDG7L.cjs +1 -0
  56. package/dist/chunk-UGLFDG7L.cjs.map +1 -0
  57. package/dist/{chunk-YH3BIKHH.js → chunk-UMWZ4ZET.js} +146 -49
  58. package/dist/chunk-UMWZ4ZET.js.map +1 -0
  59. package/dist/{index-szBv3496.d.ts → coerce-DP0vYu-x.d.ts} +286 -199
  60. package/dist/{index-C_8o--VE.d.cts → coerce-DRERzdve.d.cts} +286 -199
  61. package/dist/core/index.cjs +9 -5
  62. package/dist/core/index.cjs.map +1 -1
  63. package/dist/core/index.d.cts +122 -108
  64. package/dist/core/index.d.ts +122 -108
  65. package/dist/core/index.js +32 -28
  66. package/dist/generate/index.cjs +8 -8
  67. package/dist/generate/index.d.cts +168 -84
  68. package/dist/generate/index.d.ts +168 -84
  69. package/dist/generate/index.js +7 -7
  70. package/dist/{index-CGpRv5sl.d.cts → index-9eoQs2C1.d.ts} +34 -19
  71. package/dist/{index-CCNJRG-P.d.cts → index-B9QLziDe.d.cts} +293 -56
  72. package/dist/{index-Dzr13q3d.d.cts → index-BBWNZS0f.d.cts} +135 -58
  73. package/dist/{index-9wcMhG73.d.ts → index-BHIUyjhZ.d.ts} +293 -56
  74. package/dist/{index-BGFW-8P2.d.cts → index-BJgP97N1.d.ts} +196 -114
  75. package/dist/{index-CunYKLiu.d.ts → index-BL5N2Oyz.d.cts} +1 -1
  76. package/dist/{index-Cj49F892.d.ts → index-BMweUlm5.d.cts} +63 -23
  77. package/dist/{index-CgHqxdps.d.ts → index-BNSM-DSk.d.cts} +196 -114
  78. package/dist/{index-D57QRbVn.d.ts → index-BRMbPkHE.d.ts} +480 -254
  79. package/dist/{index-Chbst3Jx.d.ts → index-CJBBiAmU.d.ts} +135 -58
  80. package/dist/{index-BkGmlyI3.d.cts → index-CL4b4gUQ.d.ts} +63 -23
  81. package/dist/{index-B9MNZboQ.d.ts → index-Cy15oTvh.d.cts} +52 -22
  82. package/dist/{index-B9MNZboQ.d.cts → index-Cy15oTvh.d.ts} +52 -22
  83. package/dist/{index-BO6xE9YU.d.cts → index-DGCNKAUr.d.cts} +480 -254
  84. package/dist/{index-B4o55NDr.d.cts → index-Dw2DP7vm.d.ts} +1 -1
  85. package/dist/{index-G1uwGGWQ.d.ts → index-cHvbYgJI.d.cts} +34 -19
  86. package/dist/index.cjs +42 -14
  87. package/dist/index.cjs.map +1 -1
  88. package/dist/index.d.cts +14 -14
  89. package/dist/index.d.ts +14 -14
  90. package/dist/index.js +82 -54
  91. package/dist/{masks-DYbE485J.d.cts → masks-Cc7-MzzY.d.ts} +49 -2
  92. package/dist/{masks-DYbE485J.d.ts → masks-pADfTOiD.d.cts} +49 -2
  93. package/dist/model/index.cjs +19 -12
  94. package/dist/model/index.cjs.map +1 -1
  95. package/dist/model/index.d.cts +504 -182
  96. package/dist/model/index.d.ts +504 -182
  97. package/dist/model/index.js +18 -11
  98. package/dist/{positional-Cptlj-xs.d.ts → positional-DJgs6ltD.d.ts} +85 -8
  99. package/dist/{positional-DvH_sd8u.d.cts → positional-DMZer-FM.d.cts} +85 -8
  100. package/dist/theory/index.cjs +35 -6
  101. package/dist/theory/index.cjs.map +1 -1
  102. package/dist/theory/index.d.cts +257 -62
  103. package/dist/theory/index.d.ts +257 -62
  104. package/dist/theory/index.js +41 -12
  105. package/package.json +7 -5
  106. package/dist/chunk-2LCPM3EG.js +0 -3224
  107. package/dist/chunk-2LCPM3EG.js.map +0 -1
  108. package/dist/chunk-6XH6QZYQ.cjs.map +0 -1
  109. package/dist/chunk-6XPTBE7Y.cjs +0 -49
  110. package/dist/chunk-6XPTBE7Y.cjs.map +0 -1
  111. package/dist/chunk-AFM2ULJH.cjs.map +0 -1
  112. package/dist/chunk-B6XNKWZV.cjs +0 -3224
  113. package/dist/chunk-B6XNKWZV.cjs.map +0 -1
  114. package/dist/chunk-E3F4AHAH.cjs.map +0 -1
  115. package/dist/chunk-ELCTZUJJ.js +0 -379
  116. package/dist/chunk-ELCTZUJJ.js.map +0 -1
  117. package/dist/chunk-FVSI36UV.js.map +0 -1
  118. package/dist/chunk-H3VORETI.js.map +0 -1
  119. package/dist/chunk-H42UHPYW.js +0 -1577
  120. package/dist/chunk-H42UHPYW.js.map +0 -1
  121. package/dist/chunk-HFPS7JFD.js +0 -1
  122. package/dist/chunk-INHVXP6Z.js.map +0 -1
  123. package/dist/chunk-K4ZJUGF4.cjs +0 -379
  124. package/dist/chunk-K4ZJUGF4.cjs.map +0 -1
  125. package/dist/chunk-KNJEKLKV.js +0 -49
  126. package/dist/chunk-KNJEKLKV.js.map +0 -1
  127. package/dist/chunk-MXO72PLY.cjs.map +0 -1
  128. package/dist/chunk-NLEU4QSE.cjs +0 -360
  129. package/dist/chunk-NLEU4QSE.cjs.map +0 -1
  130. package/dist/chunk-OE6WK2Y2.cjs +0 -1577
  131. package/dist/chunk-OE6WK2Y2.cjs.map +0 -1
  132. package/dist/chunk-PKFNUHDY.js.map +0 -1
  133. package/dist/chunk-Q2WGKJNN.cjs.map +0 -1
  134. package/dist/chunk-RHIJ3S4I.cjs +0 -1
  135. package/dist/chunk-RHIJ3S4I.cjs.map +0 -1
  136. package/dist/chunk-TTCLJX6D.js.map +0 -1
  137. package/dist/chunk-UADYAYAT.cjs.map +0 -1
  138. package/dist/chunk-VOHZB45S.js.map +0 -1
  139. package/dist/chunk-VTI6FOLB.js.map +0 -1
  140. package/dist/chunk-WWURE4XT.cjs.map +0 -1
  141. package/dist/chunk-YH3BIKHH.js.map +0 -1
  142. /package/dist/{chunk-HFPS7JFD.js.map → chunk-K6OZXIR6.js.map} +0 -0
@@ -0,0 +1,363 @@
1
+ import {
2
+ NAMED_SCALES,
3
+ assertModeMask,
4
+ chordPitchClasses,
5
+ chordToneRole,
6
+ displacedThirds,
7
+ namedScaleMask,
8
+ requireScaleMask,
9
+ resolveScaleName,
10
+ thirdSlotOf,
11
+ toChordData
12
+ } from "./chunk-RFIBF2RR.js";
13
+ import {
14
+ InvalidInputError,
15
+ assertArray,
16
+ assertFiniteNumber,
17
+ assertGenerationBudget,
18
+ assertOptions,
19
+ assertPitchClass,
20
+ assertPositiveInt,
21
+ pitchClassOf
22
+ } from "./chunk-AGOVW7U6.js";
23
+
24
+ // src/theory/chordscale/index.ts
25
+ function requireChordScaleMask(name, label = "scale") {
26
+ const canonical = resolveScaleName(name);
27
+ if (canonical !== void 0 && !Object.hasOwn(NAMED_SCALES, canonical)) {
28
+ throw new InvalidInputError(
29
+ `${label} must name a scale chord-scale theory ranks; ${String(name)} is a world scale`
30
+ );
31
+ }
32
+ return requireScaleMask(name, label);
33
+ }
34
+ function popcount12(mask) {
35
+ let count = 0;
36
+ for (let n = 0; n < 12; n += 1) {
37
+ count += mask >> n & 1;
38
+ }
39
+ return count;
40
+ }
41
+ function maskHasPitchClass(mask, scaleRootPc, pc) {
42
+ const offset = pitchClassOf(pc - scaleRootPc);
43
+ return (mask >> offset & 1) === 1;
44
+ }
45
+ function scaleMatchesChord(chordPcs, scaleMask, scaleRootPc) {
46
+ assertModeMask(scaleMask, "scaleMask");
47
+ assertPitchClass(scaleRootPc, "scaleRootPc");
48
+ const pcs = assertArray(chordPcs, "chord pitch classes");
49
+ for (let index = 0; index < pcs.length; index += 1) {
50
+ const pc = assertPitchClass(pcs[index] ?? Number.NaN, `chord pitch classes[${index}]`);
51
+ if (!maskHasPitchClass(scaleMask, scaleRootPc, pc)) {
52
+ return false;
53
+ }
54
+ }
55
+ return true;
56
+ }
57
+ var ALIASED_SCALE_NAMES = /* @__PURE__ */ new Set(["major", "naturalMinor"]);
58
+ var HEPTATONIC_SIZE = 7;
59
+ var SCALE_PREFERENCE = [
60
+ "ionian",
61
+ "major",
62
+ "mixolydian",
63
+ "dorian",
64
+ "lydian",
65
+ "aeolian",
66
+ "naturalMinor",
67
+ "phrygian",
68
+ "locrian"
69
+ ];
70
+ var IDIOMATIC_SCALES = {
71
+ aug: ["wholeTone"],
72
+ m7b5: ["locrian"],
73
+ // The diminished scale played over a diminished seventh is the whole-half
74
+ // octatonic. Its half-whole rotation belongs to the dominant it colours: over
75
+ // a diminished seventh every one of its non-chord tones lands a semitone
76
+ // above a chord tone, so it offers no tension at all.
77
+ dim7: ["octatonicWholeHalf"],
78
+ minMaj7: ["melodicMinor"]
79
+ };
80
+ function conventionalRank(name) {
81
+ const index = SCALE_PREFERENCE.indexOf(name);
82
+ return index < 0 ? SCALE_PREFERENCE.length : index;
83
+ }
84
+ function idiomaticRank(chord, name) {
85
+ const choices = IDIOMATIC_SCALES[chord.quality];
86
+ if (choices === void 0) {
87
+ return 0;
88
+ }
89
+ const index = choices.indexOf(name);
90
+ return index < 0 ? choices.length + 1 : index;
91
+ }
92
+ function isAlteredDominant(chord, scaleName) {
93
+ return chord.quality === "dom7" && scaleName === "altered";
94
+ }
95
+ function chordScales(chord) {
96
+ const data = toChordData(chord);
97
+ const chordPcs = chordPitchClasses(data);
98
+ const rootPc = pitchClassOf(data.rootPc);
99
+ const ranked = [];
100
+ const seenMasks = /* @__PURE__ */ new Set();
101
+ for (const name of Object.keys(NAMED_SCALES)) {
102
+ if (name === "chromatic" || ALIASED_SCALE_NAMES.has(name)) {
103
+ continue;
104
+ }
105
+ const mask = namedScaleMask(name);
106
+ if (mask === void 0 || seenMasks.has(mask)) {
107
+ continue;
108
+ }
109
+ seenMasks.add(mask);
110
+ if (scaleMatchesChord(chordPcs, mask, rootPc) || isAlteredDominant(data, name)) {
111
+ const size = popcount12(mask);
112
+ ranked.push({
113
+ name,
114
+ extra: size - chordPcs.length,
115
+ size,
116
+ avoid: avoidNotes(data, name).length
117
+ });
118
+ }
119
+ }
120
+ if (ranked.length === 0) {
121
+ if (NAMED_SCALES.chromatic !== void 0) {
122
+ return [{ name: "chromatic", rootPc }];
123
+ }
124
+ return [];
125
+ }
126
+ const preferHeptatonic = chordPcs.length <= 3;
127
+ ranked.sort((a, b) => {
128
+ const idiomaticA = idiomaticRank(data, a.name);
129
+ const idiomaticB = idiomaticRank(data, b.name);
130
+ if (idiomaticA !== idiomaticB) {
131
+ return idiomaticA - idiomaticB;
132
+ }
133
+ if (preferHeptatonic && a.size === HEPTATONIC_SIZE !== (b.size === HEPTATONIC_SIZE)) {
134
+ return a.size === HEPTATONIC_SIZE ? -1 : 1;
135
+ }
136
+ if (a.extra !== b.extra) {
137
+ return a.extra - b.extra;
138
+ }
139
+ const rankA = conventionalRank(a.name);
140
+ const rankB = conventionalRank(b.name);
141
+ if (rankA !== rankB) {
142
+ return rankA - rankB;
143
+ }
144
+ if (a.avoid !== b.avoid) {
145
+ return a.avoid - b.avoid;
146
+ }
147
+ return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
148
+ });
149
+ return ranked.map((entry) => ({ name: entry.name, rootPc }));
150
+ }
151
+ function avoidNotes(chord, scaleName, opts = {}) {
152
+ const asked = assertOptions(opts, "opts");
153
+ const mask = requireChordScaleMask(scaleName);
154
+ const data = toChordData(chord);
155
+ const rootPc = pitchClassOf(data.rootPc);
156
+ const chordPcs = chordPitchClasses(data);
157
+ const alteredDominant = isAlteredDominant(data, scaleName);
158
+ if (!scaleMatchesChord(chordPcs, mask, rootPc) && !alteredDominant) {
159
+ return [];
160
+ }
161
+ const chordSet = new Set(chordPcs);
162
+ const displaced = new Set(displacedThirds(data));
163
+ const melodic = asked.use === "melodic";
164
+ const semitoneAboveRoot = pitchClassOf(rootPc + 1);
165
+ const avoid = [];
166
+ for (let pc = 0; pc < 12; pc += 1) {
167
+ if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc)) {
168
+ continue;
169
+ }
170
+ if (melodic && pc !== semitoneAboveRoot) {
171
+ continue;
172
+ }
173
+ if (!alteredDominant && (chordSet.has(pitchClassOf(pc - 1)) || displaced.has(pc))) {
174
+ avoid.push(pc);
175
+ }
176
+ }
177
+ return avoid.sort((a, b) => a - b);
178
+ }
179
+ var MINOR_RESOLUTION_TENSIONS = [1, 3, 8];
180
+ function resolvesToMinorTonic(chord, chordPcs, target) {
181
+ if (target === void 0) {
182
+ return false;
183
+ }
184
+ const rootPc = pitchClassOf(chord.rootPc);
185
+ const sounds = (semitones) => chordPcs.includes(pitchClassOf(rootPc + semitones));
186
+ if (!sounds(4) || !sounds(10)) {
187
+ return false;
188
+ }
189
+ const targetRootPc = pitchClassOf(target.rootPc);
190
+ if (targetRootPc !== pitchClassOf(rootPc + 5)) {
191
+ return false;
192
+ }
193
+ return chordPitchClasses(target).includes(pitchClassOf(targetRootPc + 3));
194
+ }
195
+ function availableTensions(chord, scaleName, opts = {}) {
196
+ const asked = assertOptions(opts, "opts");
197
+ const mask = requireChordScaleMask(scaleName);
198
+ const data = toChordData(chord);
199
+ const rootPc = pitchClassOf(data.rootPc);
200
+ const chordPcs = chordPitchClasses(data);
201
+ const alteredDominant = isAlteredDominant(data, scaleName);
202
+ if (!scaleMatchesChord(chordPcs, mask, rootPc) && !alteredDominant) {
203
+ return [];
204
+ }
205
+ const chordSet = new Set(chordPcs);
206
+ const avoidSet = alteredDominant ? /* @__PURE__ */ new Set() : new Set(avoidNotes(data, scaleName));
207
+ const functional = resolvesToMinorTonic(
208
+ data,
209
+ chordPcs,
210
+ asked.resolvesTo === void 0 ? void 0 : toChordData(asked.resolvesTo)
211
+ ) ? new Set(MINOR_RESOLUTION_TENSIONS.map((semitones) => pitchClassOf(rootPc + semitones))) : void 0;
212
+ const tensions = [];
213
+ for (let pc = 0; pc < 12; pc += 1) {
214
+ if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc)) {
215
+ continue;
216
+ }
217
+ if (avoidSet.has(pc) && functional?.has(pc) !== true) {
218
+ continue;
219
+ }
220
+ tensions.push(pc);
221
+ }
222
+ return tensions.sort((a, b) => a - b);
223
+ }
224
+ function chordScaleReport(chord, limit) {
225
+ if (limit !== void 0) {
226
+ assertPositiveInt(limit, "chord-scale report limit");
227
+ }
228
+ const data = toChordData(chord);
229
+ const matches = chordScales(data);
230
+ const chosen = limit === void 0 ? matches : matches.slice(0, limit);
231
+ return chosen.map((match) => {
232
+ const harmonic = avoidNotes(data, match.name);
233
+ const melodic = new Set(avoidNotes(data, match.name, { use: "melodic" }));
234
+ return {
235
+ name: match.name,
236
+ rootPc: match.rootPc,
237
+ avoid: harmonic.filter((pc) => melodic.has(pc)),
238
+ passing: harmonic.filter((pc) => !melodic.has(pc)),
239
+ tensions: availableTensions(data, match.name)
240
+ };
241
+ });
242
+ }
243
+ var RANK_PENALTY = 0.01;
244
+ function scalePitchClassSet(name, rootPc) {
245
+ const mask = namedScaleMask(name);
246
+ const pcs = /* @__PURE__ */ new Set();
247
+ if (mask === void 0) {
248
+ return pcs;
249
+ }
250
+ for (let n = 0; n < 12; n += 1) {
251
+ if ((mask >> n & 1) === 1) {
252
+ pcs.add(pitchClassOf(rootPc + n));
253
+ }
254
+ }
255
+ return pcs;
256
+ }
257
+ function symmetricDifferenceSize(a, b) {
258
+ let count = 0;
259
+ for (const pc of a) {
260
+ if (!b.has(pc)) {
261
+ count += 1;
262
+ }
263
+ }
264
+ for (const pc of b) {
265
+ if (!a.has(pc)) {
266
+ count += 1;
267
+ }
268
+ }
269
+ return count;
270
+ }
271
+ function scalesForChanges(chords) {
272
+ assertArray(chords, "chord-scale changes");
273
+ assertGenerationBudget(chords.length, "chord-scale changes");
274
+ if (chords.length === 0) {
275
+ return [];
276
+ }
277
+ const candidateLists = chords.map((chord) => chordScales(chord));
278
+ const pcSets = candidateLists.map(
279
+ (candidates) => candidates.map((match) => scalePitchClassSet(match.name, match.rootPc))
280
+ );
281
+ const first = candidateLists[0] ?? [];
282
+ const dp = [first.map((_, j) => j * RANK_PENALTY)];
283
+ const back = [first.map(() => -1)];
284
+ for (let i = 1; i < candidateLists.length; i += 1) {
285
+ const candidates = candidateLists[i] ?? [];
286
+ const prevCosts = dp[i - 1] ?? [];
287
+ const prevSets = pcSets[i - 1] ?? [];
288
+ const curSets = pcSets[i] ?? [];
289
+ const stageCosts = [];
290
+ const stageBack = [];
291
+ for (let j = 0; j < candidates.length; j += 1) {
292
+ let bestCost = Number.POSITIVE_INFINITY;
293
+ let bestPrev = -1;
294
+ for (let k = 0; k < prevCosts.length; k += 1) {
295
+ const transition = symmetricDifferenceSize(
296
+ prevSets[k] ?? /* @__PURE__ */ new Set(),
297
+ curSets[j] ?? /* @__PURE__ */ new Set()
298
+ );
299
+ const cost = (prevCosts[k] ?? 0) + transition;
300
+ if (cost < bestCost) {
301
+ bestCost = cost;
302
+ bestPrev = k;
303
+ }
304
+ }
305
+ stageCosts.push(bestCost + j * RANK_PENALTY);
306
+ stageBack.push(bestPrev);
307
+ }
308
+ dp.push(stageCosts);
309
+ back.push(stageBack);
310
+ }
311
+ const lastCosts = dp[dp.length - 1] ?? [];
312
+ let bestFinal = 0;
313
+ for (let j = 1; j < lastCosts.length; j += 1) {
314
+ if ((lastCosts[j] ?? Number.POSITIVE_INFINITY) < (lastCosts[bestFinal] ?? 0)) {
315
+ bestFinal = j;
316
+ }
317
+ }
318
+ const chosenIndices = new Array(candidateLists.length);
319
+ let current = bestFinal;
320
+ for (let i = candidateLists.length - 1; i >= 0; i -= 1) {
321
+ chosenIndices[i] = current;
322
+ current = back[i]?.[current] ?? -1;
323
+ }
324
+ return chords.map((chord, i) => {
325
+ const candidates = candidateLists[i] ?? [];
326
+ const scale = candidates[chosenIndices[i] ?? 0] ?? { name: "chromatic", rootPc: chord.rootPc };
327
+ return { chord, scale };
328
+ });
329
+ }
330
+
331
+ // src/theory/harmony/index.ts
332
+ function roleOf(pitch, chord, chordId = 0) {
333
+ assertFiniteNumber(chordId, "chordId");
334
+ const data = toChordData(chord);
335
+ const interval = (pitchClassOf(pitch) - pitchClassOf(data.rootPc) + 12) % 12;
336
+ const slot = thirdSlotOf(data);
337
+ const isSuspendedTone = slot.kind === "suspension" && interval === slot.interval;
338
+ const chordRole = chordToneRole(pitch, data);
339
+ let role;
340
+ let lock;
341
+ if (isSuspendedTone) {
342
+ role = "third";
343
+ lock = "quality";
344
+ } else if (chordRole !== null) {
345
+ role = chordRole;
346
+ lock = chordRole === "root" ? "identity" : chordRole === "third" ? "quality" : "voicing";
347
+ } else {
348
+ role = "tension";
349
+ lock = "voicing";
350
+ }
351
+ return { role, lock, belongsToChordId: chordId };
352
+ }
353
+
354
+ export {
355
+ scaleMatchesChord,
356
+ chordScales,
357
+ avoidNotes,
358
+ availableTensions,
359
+ chordScaleReport,
360
+ scalesForChanges,
361
+ roleOf
362
+ };
363
+ //# sourceMappingURL=chunk-NB25P3EH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/theory/chordscale/index.ts","../src/theory/harmony/index.ts"],"sourcesContent":["import { InvalidInputError } from '../../core/errors/index.js';\nimport { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport {\n assertArray,\n assertGenerationBudget,\n assertOptions,\n assertPitchClass,\n assertPositiveInt,\n} from '../../core/validation/index.js';\nimport type { Chord } from '../chord/index.js';\nimport { chordPitchClasses, displacedThirds } from '../chord/index.js';\nimport type { ScaleNameInput } from '../scale/index.js';\nimport {\n assertModeMask,\n NAMED_SCALES,\n namedScaleMask,\n requireScaleMask,\n resolveScaleName,\n} from '../scale/index.js';\nimport { type ChordLike, toChordData } from '../symbol/index.js';\n\n/**\n * The mask of a scale a chord-scale question may be asked about.\n *\n * Chord-scale theory is a Western practice, so the register of world scales is\n * not a vocabulary these functions answer from: a raga offered as the scale\n * over an altered dominant would read as an answer while being a category\n * error. {@link chordScales} proposes {@link NAMED_SCALES} and nothing else,\n * and the questions asked about one chosen scale accept exactly what it can\n * propose — an alias of a Western scale included, a world scale not.\n *\n * @param name The scale name or alias.\n * @param label What the name is, for the error message.\n * @returns The 12-bit mask.\n * @throws If the name is not a built-in scale, or names a world scale.\n */\nfunction requireChordScaleMask(name: ScaleNameInput, label = 'scale'): number {\n const canonical = resolveScaleName(name);\n if (canonical !== undefined && !Object.hasOwn(NAMED_SCALES, canonical)) {\n throw new InvalidInputError(\n `${label} must name a scale chord-scale theory ranks; ${String(name)} is a world scale`,\n );\n }\n return requireScaleMask(name, label);\n}\n\n/** Count the set bits (scale tones) in a 12-bit mode mask. */\nfunction popcount12(mask: number): number {\n let count = 0;\n for (let n = 0; n < 12; n += 1) {\n count += (mask >> n) & 1;\n }\n return count;\n}\n\n/** Test whether a pitch class belongs to a mode mask rooted on `scaleRootPc`. */\nfunction maskHasPitchClass(mask: number, scaleRootPc: number, pc: number): boolean {\n const offset = pitchClass(pc - scaleRootPc);\n return ((mask >> offset) & 1) === 1;\n}\n\n/**\n * Test whether every chord pitch class is contained in a scale.\n *\n * @param chordPcs The chord's pitch classes (0..11).\n * @param scaleMask The scale's 12-bit mode mask.\n * @param scaleRootPc The pitch class the mask is rooted on.\n * @returns True if the scale is a superset of the chord.\n *\n * @category Scales\n */\nexport function scaleMatchesChord(\n chordPcs: number[],\n scaleMask: number,\n scaleRootPc: number,\n): boolean {\n // The mask is read with bit operations and a pitch class shifts it, so both\n // coerce: an entry that is not a pitch class would silently be read as one.\n assertModeMask(scaleMask, 'scaleMask');\n assertPitchClass(scaleRootPc, 'scaleRootPc');\n const pcs = assertArray<number>(chordPcs, 'chord pitch classes');\n for (let index = 0; index < pcs.length; index += 1) {\n const pc = assertPitchClass(pcs[index] ?? Number.NaN, `chord pitch classes[${index}]`);\n if (!maskHasPitchClass(scaleMask, scaleRootPc, pc)) {\n return false;\n }\n }\n return true;\n}\n\n/**\n * A named scale rooted on a pitch class that fits over a chord.\n *\n * @category Scales\n */\nexport type ChordScaleMatch = {\n name: string;\n rootPc: number;\n};\n\n/**\n * Scale names whose mask duplicates another {@link NAMED_SCALES} entry\n * (major/ionian, naturalMinor/aeolian). Chord-scale analysis conventionally\n * speaks in mode names, so these aliases are skipped and only the modal name\n * is reported.\n */\nconst ALIASED_SCALE_NAMES = new Set(['major', 'naturalMinor']);\n\n/** Number of tones in a heptatonic (seven-note) scale. */\nconst HEPTATONIC_SIZE = 7;\n\n/**\n * Conventional preference among equally-fitting scales, brightest first.\n *\n * Several modes fit a chord that states no third or sixth — a power chord, a\n * suspension — equally well. Alphabetical order made `aeolian` the answer for\n * those, naming a minor mode as the best fit for a mode-neutral chord.\n */\nconst SCALE_PREFERENCE = [\n 'ionian',\n 'major',\n 'mixolydian',\n 'dorian',\n 'lydian',\n 'aeolian',\n 'naturalMinor',\n 'phrygian',\n 'locrian',\n] as const;\n\n/** Conventional first choices that pure set-fit ranking cannot express. */\nconst IDIOMATIC_SCALES: Partial<Record<Chord['quality'], readonly string[]>> = {\n aug: ['wholeTone'],\n m7b5: ['locrian'],\n // The diminished scale played over a diminished seventh is the whole-half\n // octatonic. Its half-whole rotation belongs to the dominant it colours: over\n // a diminished seventh every one of its non-chord tones lands a semitone\n // above a chord tone, so it offers no tension at all.\n dim7: ['octatonicWholeHalf'],\n minMaj7: ['melodicMinor'],\n};\n\n/** Position of a scale in {@link SCALE_PREFERENCE}, or last when unlisted. */\nfunction conventionalRank(name: string): number {\n const index = (SCALE_PREFERENCE as readonly string[]).indexOf(name);\n return index < 0 ? SCALE_PREFERENCE.length : index;\n}\n\n/** Whether this scale is an idiomatic color choice for the chord quality. */\nfunction idiomaticRank(chord: Chord, name: string): number {\n const choices = IDIOMATIC_SCALES[chord.quality];\n if (choices === undefined) {\n return 0;\n }\n const index = choices.indexOf(name);\n return index < 0 ? choices.length + 1 : index;\n}\n\n/**\n * An ordinary dominant seventh may use altered colors while retaining its\n * perfect fifth in the voicing; the `7alt` quality makes that alteration\n * explicit, but lead sheets commonly write the shorter `7` here.\n */\nfunction isAlteredDominant(chord: Chord, scaleName: string): boolean {\n return chord.quality === 'dom7' && scaleName === 'altered';\n}\n\n/**\n * List the scales that fit over a chord, best fit first.\n *\n * Only the chord root is considered as the scale root, matching the\n * conventional chord-scale relationship. Every entry of {@link NAMED_SCALES}\n * whose pitch-class set is a superset of the chord's is returned once per\n * distinct pitch-class set (aliased masks such as major/ionian report only the\n * modal name). The chromatic scale is only returned as a fallback when no other\n * scale contains the chord.\n *\n * The ranking applies these keys in order:\n *\n * 1. The idiomatic scales for the chord quality, in the order they are named\n * there, ahead of every scale that is not one of them.\n * 2. For bare triads and smaller chords, heptatonic scales ahead of pentatonics\n * and other sizes: a pentatonic adds no modal color over a triad, so the\n * seven-note modes are the more useful answer.\n * 3. Fewest extra scale tones beyond the chord.\n * 4. Conventional modal preference.\n * 5. Fewest avoid notes, which separates the symmetric and melodic-minor\n * scales the modal preference does not name.\n * 6. Alphabetical order, so the list is stable.\n *\n * The idiomatic key comes first, so the head of the list is the scale a player\n * would name rather than the tightest fit: over `Cm7b5` the locrian mode leads\n * even though the blues scale adds fewer tones, because locrian is what a\n * half-diminished chord is read as. Read further down the list, or pick by name,\n * when the closest fit is what is wanted; {@link scalesForChanges} makes the\n * choice for a whole progression.\n *\n * @param chord The chord to fit scales over, as a chord symbol, chord data, or\n * a `Chord`.\n * @returns The matching scales rooted on the chord root, best fit first.\n *\n * @example\n * ```ts\n * import { makeChord, chordScales } from '@libraz/libcantus';\n * const scales = chordScales(makeChord(0, 'maj7'));\n * // scales for Cmaj7, best fit first; each is { name, rootPc: 0 }\n * ```\n *\n * @category Scales\n */\nexport function chordScales(chord: ChordLike): ChordScaleMatch[] {\n const data = toChordData(chord);\n const chordPcs = chordPitchClasses(data);\n const rootPc = pitchClass(data.rootPc);\n const ranked: { name: string; extra: number; size: number; avoid: number }[] = [];\n const seenMasks = new Set<number>();\n for (const name of Object.keys(NAMED_SCALES)) {\n if (name === 'chromatic' || ALIASED_SCALE_NAMES.has(name)) {\n continue;\n }\n const mask = namedScaleMask(name);\n if (mask === undefined || seenMasks.has(mask)) {\n continue;\n }\n seenMasks.add(mask);\n if (scaleMatchesChord(chordPcs, mask, rootPc) || isAlteredDominant(data, name)) {\n const size = popcount12(mask);\n ranked.push({\n name,\n extra: size - chordPcs.length,\n size,\n avoid: avoidNotes(data, name).length,\n });\n }\n }\n if (ranked.length === 0) {\n // No diatonic-style scale contains the chord; fall back to the chromatic\n // scale, which trivially contains every pitch class.\n if (NAMED_SCALES.chromatic !== undefined) {\n return [{ name: 'chromatic', rootPc }];\n }\n return [];\n }\n const preferHeptatonic = chordPcs.length <= 3;\n ranked.sort((a, b) => {\n const idiomaticA = idiomaticRank(data, a.name);\n const idiomaticB = idiomaticRank(data, b.name);\n if (idiomaticA !== idiomaticB) {\n return idiomaticA - idiomaticB;\n }\n if (preferHeptatonic && (a.size === HEPTATONIC_SIZE) !== (b.size === HEPTATONIC_SIZE)) {\n return a.size === HEPTATONIC_SIZE ? -1 : 1;\n }\n if (a.extra !== b.extra) {\n return a.extra - b.extra;\n }\n const rankA = conventionalRank(a.name);\n const rankB = conventionalRank(b.name);\n if (rankA !== rankB) {\n return rankA - rankB;\n }\n // Among scales the modal preference does not name — the symmetric and\n // melodic-minor scales — the one that clashes with the chord least is the\n // one a player reaches for. Alphabetical order answered with the rotation\n // that leaves nothing playable.\n if (a.avoid !== b.avoid) {\n return a.avoid - b.avoid;\n }\n return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;\n });\n return ranked.map((entry) => ({ name: entry.name, rootPc }));\n}\n\n/**\n * Which use an avoid note is judged for: sounding it, or passing through it.\n *\n * `'harmonic'` is the Berklee-style rule in full — a tone that must not be\n * sounded against the chord. `'melodic'` keeps only what a line cannot pass\n * through either, which is the semitone above the root; every other clash\n * resolves as the line moves on.\n *\n * @category Scales\n */\nexport type AvoidNoteUse = 'harmonic' | 'melodic';\n\n/**\n * Options for {@link avoidNotes}.\n *\n * @category Scales\n */\nexport type AvoidNotesOptions = {\n use?: AvoidNoteUse;\n};\n\n/**\n * List a scale's avoid notes over a chord.\n *\n * An avoid note is a non-chord scale tone that lies a semitone directly above a\n * chord tone, or the third a suspension displaced; sounding it against the\n * chord clashes. The scale is rooted on the chord root. If the scale does not\n * contain the chord, no avoid notes exist.\n *\n * The rule states which tones must not be *sounded* against the chord, which is\n * not the same question as which tones a line may not touch. Pass\n * `{ use: 'melodic' }` for the melodic reading: only the semitone above the\n * root survives it, so the result is always a subset of the harmonic one.\n *\n * @param chord The chord providing the chord tones, as a chord symbol, chord\n * data, or a `Chord`.\n * @param scaleName A key of {@link NAMED_SCALES}, or an alias of one. A world\n * scale is not one: chord-scale theory is a Western practice, so a raga named\n * here is rejected rather than answered with a number.\n * @param opts Set `use: 'melodic'` to judge a line rather than a voicing.\n * @returns The avoid-note pitch classes, sorted ascending in [0, 11].\n * @throws If `scaleName` is not a built-in scale. An empty result already\n * means \"this scale has no avoid notes over this chord\"; answering a typo\n * the same way would make the two indistinguishable.\n *\n * @example\n * ```ts\n * import { makeChord, avoidNotes } from '@libraz/libcantus';\n * avoidNotes(makeChord(0, 'maj7'), 'ionian'); // [5] — F clashes with the third\n * avoidNotes(makeChord(0, 'maj7'), 'ionian', { use: 'melodic' }); // [] — a line may pass through it\n * ```\n *\n * @category Scales\n */\nexport function avoidNotes(\n chord: ChordLike,\n scaleName: ScaleNameInput,\n opts: AvoidNotesOptions = {},\n): number[] {\n const asked = assertOptions(opts, 'opts');\n const mask = requireChordScaleMask(scaleName);\n const data = toChordData(chord);\n const rootPc = pitchClass(data.rootPc);\n const chordPcs = chordPitchClasses(data);\n const alteredDominant = isAlteredDominant(data, scaleName);\n if (!scaleMatchesChord(chordPcs, mask, rootPc) && !alteredDominant) {\n return [];\n }\n const chordSet = new Set(chordPcs);\n // Sounding the third a suspension stands in for is what undoes the\n // suspension, so it is an avoid note however consonant it is against the\n // scale. Both thirds are named: a suspension does not say which one it\n // displaced — that is what suspending is — so meeting either one ends it.\n const displaced = new Set(displacedThirds(data));\n const melodic = asked.use === 'melodic';\n const semitoneAboveRoot = pitchClass(rootPc + 1);\n const avoid: number[] = [];\n for (let pc = 0; pc < 12; pc += 1) {\n if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc)) {\n continue;\n }\n if (melodic && pc !== semitoneAboveRoot) {\n continue;\n }\n if (!alteredDominant && (chordSet.has(pitchClass(pc - 1)) || displaced.has(pc))) {\n avoid.push(pc);\n }\n }\n return avoid.sort((a, b) => a - b);\n}\n\n/**\n * Semitones above the root of the colors a dominant takes when it resolves to a\n * minor tonic: the flat ninth, the sharp ninth, and the flat thirteenth.\n *\n * Over a dominant read on its own, each of them sits a semitone above a chord\n * tone and is an avoid note. Heard as the dominant of a minor key they are the\n * key's own tones, and standard practice, so the function the chord serves is\n * what decides.\n */\nconst MINOR_RESOLUTION_TENSIONS = [1, 3, 8] as const;\n\n/** Whether this chord is a dominant seventh resolving down a fifth to minor. */\nfunction resolvesToMinorTonic(\n chord: Chord,\n chordPcs: number[],\n target: Chord | undefined,\n): boolean {\n if (target === undefined) {\n return false;\n }\n const rootPc = pitchClass(chord.rootPc);\n const sounds = (semitones: number): boolean => chordPcs.includes(pitchClass(rootPc + semitones));\n if (!sounds(4) || !sounds(10)) {\n return false;\n }\n const targetRootPc = pitchClass(target.rootPc);\n if (targetRootPc !== pitchClass(rootPc + 5)) {\n return false;\n }\n return chordPitchClasses(target).includes(pitchClass(targetRootPc + 3));\n}\n\n/**\n * Options for {@link availableTensions}.\n *\n * @category Scales\n */\nexport type AvailableTensionsOptions = {\n /**\n * The chord this one resolves to, as a chord symbol, chord data, or a\n * `Chord`.\n */\n resolvesTo?: ChordLike;\n};\n\n/**\n * List a scale's available tensions over a chord.\n *\n * Available tensions are scale tones that are neither chord tones nor avoid\n * notes, i.e. the usable color tones (typically the 9/11/13 region). The scale\n * is rooted on the chord root. If the scale does not contain the chord, there\n * are no available tensions.\n *\n * A tension's availability also depends on what the chord is doing. Pass\n * `resolvesTo` to say which chord this one resolves to: a dominant seventh\n * resolving down a fifth to a minor tonic takes its flat ninth, sharp ninth and\n * flat thirteenth, which read on their own would each be an avoid note. Only\n * scale tones are ever added, so the result stays within the scale.\n *\n * @param chord The chord providing the chord tones, as a chord symbol, chord\n * data, or a `Chord`.\n * @param scaleName A key of {@link NAMED_SCALES}, or an alias of one; a world\n * scale is outside chord-scale theory, as in {@link avoidNotes}.\n * @param opts Set `resolvesTo` to the chord this one resolves to.\n * @returns The available-tension pitch classes, sorted ascending in [0, 11].\n * @throws If `scaleName` is not a built-in scale, as {@link avoidNotes} does.\n *\n * @example\n * ```ts\n * import { makeChord, availableTensions } from '@libraz/libcantus';\n * availableTensions(makeChord(0, 'maj7'), 'ionian'); // [2, 9] — color tones over Cmaj7\n * availableTensions(makeChord(7, 'dom7'), 'phrygianDominant', { resolvesTo: makeChord(0, 'min') });\n * // [3, 8] — the b13 and b9 the resolution to C minor makes available\n * ```\n *\n * @category Scales\n */\nexport function availableTensions(\n chord: ChordLike,\n scaleName: ScaleNameInput,\n opts: AvailableTensionsOptions = {},\n): number[] {\n const asked = assertOptions(opts, 'opts');\n const mask = requireChordScaleMask(scaleName);\n const data = toChordData(chord);\n const rootPc = pitchClass(data.rootPc);\n const chordPcs = chordPitchClasses(data);\n const alteredDominant = isAlteredDominant(data, scaleName);\n if (!scaleMatchesChord(chordPcs, mask, rootPc) && !alteredDominant) {\n return [];\n }\n const chordSet = new Set(chordPcs);\n const avoidSet = alteredDominant ? new Set<number>() : new Set(avoidNotes(data, scaleName));\n const functional = resolvesToMinorTonic(\n data,\n chordPcs,\n asked.resolvesTo === undefined ? undefined : toChordData(asked.resolvesTo),\n )\n ? new Set(MINOR_RESOLUTION_TENSIONS.map((semitones) => pitchClass(rootPc + semitones)))\n : undefined;\n const tensions: number[] = [];\n for (let pc = 0; pc < 12; pc += 1) {\n if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc)) {\n continue;\n }\n if (avoidSet.has(pc) && functional?.has(pc) !== true) {\n continue;\n }\n tensions.push(pc);\n }\n return tensions.sort((a, b) => a - b);\n}\n\n/**\n * A scale fit over a chord, with its non-chord tones sorted by what may be done\n * with them.\n *\n * `avoid`, `passing` and `tensions` partition the scale tones the chord does\n * not state: `avoid` may not be played at all, `passing` may be passed through\n * melodically but not sounded against the chord, and `tensions` may be added\n * freely as color. `avoid` is {@link avoidNotes} under `use: 'melodic'`, and\n * `avoid` together with `passing` is the harmonic reading.\n *\n * @category Scales\n */\nexport type ChordScaleReportEntry = {\n name: string;\n rootPc: number;\n avoid: number[];\n passing: number[];\n tensions: number[];\n};\n\n/**\n * Report the best-fitting scales for a chord with their avoid notes and tensions.\n *\n * Combines {@link chordScales}, {@link avoidNotes}, and {@link availableTensions}\n * into a single ergonomic result, ordered best fit first. The report is read as\n * playing advice, so the tones a line may pass through are reported apart from\n * the ones it may not touch; see {@link ChordScaleReportEntry}.\n *\n * @param chord The chord to analyze, as a chord symbol, chord data, or a\n * `Chord`.\n * @param limit Optional maximum number of scales to report; all by default.\n * @returns One entry per reported scale, best fit first.\n *\n * @category Scales\n */\nexport function chordScaleReport(chord: ChordLike, limit?: number): ChordScaleReportEntry[] {\n if (limit !== undefined) {\n assertPositiveInt(limit, 'chord-scale report limit');\n }\n const data = toChordData(chord);\n const matches = chordScales(data);\n const chosen = limit === undefined ? matches : matches.slice(0, limit);\n return chosen.map((match) => {\n const harmonic = avoidNotes(data, match.name);\n const melodic = new Set(avoidNotes(data, match.name, { use: 'melodic' }));\n return {\n name: match.name,\n rootPc: match.rootPc,\n avoid: harmonic.filter((pc) => melodic.has(pc)),\n passing: harmonic.filter((pc) => !melodic.has(pc)),\n tensions: availableTensions(data, match.name),\n };\n });\n}\n\n/**\n * A chord paired with the scale chosen for it by {@link scalesForChanges}.\n *\n * @category Scales\n */\nexport type ScaleChoice = {\n chord: Chord;\n scale: ChordScaleMatch;\n};\n\n/**\n * Small per-candidate penalty added in {@link scalesForChanges} so that, among\n * choices of otherwise-equal transition cost, the tighter best-fit scale wins.\n */\nconst RANK_PENALTY = 0.01;\n\n/** Build the pitch-class set of a named scale rooted on `rootPc`. */\nfunction scalePitchClassSet(name: string, rootPc: number): Set<number> {\n const mask = namedScaleMask(name);\n const pcs = new Set<number>();\n if (mask === undefined) {\n return pcs;\n }\n for (let n = 0; n < 12; n += 1) {\n if (((mask >> n) & 1) === 1) {\n pcs.add(pitchClass(rootPc + n));\n }\n }\n return pcs;\n}\n\n/** Count pitch classes that belong to exactly one of two sets. */\nfunction symmetricDifferenceSize(a: Set<number>, b: Set<number>): number {\n let count = 0;\n for (const pc of a) {\n if (!b.has(pc)) {\n count += 1;\n }\n }\n for (const pc of b) {\n if (!a.has(pc)) {\n count += 1;\n }\n }\n return count;\n}\n\n/**\n * Choose one scale per chord across a progression, favoring continuity.\n *\n * Each chord's {@link chordScales} candidates form a stage in a Viterbi-style\n * dynamic program. The transition cost between adjacent choices is the number\n * of pitch classes that differ between their pitch-class sets (the symmetric\n * difference), plus a small penalty for straying from a chord's best-fit scale\n * so that ties break toward the tighter fit. The minimum-total-cost path is\n * returned, one {@link ScaleChoice} per input chord in the original order.\n *\n * Each choice pairs its scale with the chord exactly as it was handed in, so\n * this takes plain chord data rather than the wider forms the entry points\n * above accept.\n *\n * @param chords The chord sequence to choose scales for.\n * @returns One scale choice per chord, in input order.\n *\n * @category Scales\n */\nexport function scalesForChanges(chords: readonly Chord[]): ScaleChoice[] {\n assertArray(chords, 'chord-scale changes');\n assertGenerationBudget(chords.length, 'chord-scale changes');\n if (chords.length === 0) {\n return [];\n }\n const candidateLists = chords.map((chord) => chordScales(chord));\n const pcSets = candidateLists.map((candidates) =>\n candidates.map((match) => scalePitchClassSet(match.name, match.rootPc)),\n );\n\n const first = candidateLists[0] ?? [];\n const dp: number[][] = [first.map((_, j) => j * RANK_PENALTY)];\n const back: number[][] = [first.map(() => -1)];\n\n for (let i = 1; i < candidateLists.length; i += 1) {\n const candidates = candidateLists[i] ?? [];\n const prevCosts = dp[i - 1] ?? [];\n const prevSets = pcSets[i - 1] ?? [];\n const curSets = pcSets[i] ?? [];\n const stageCosts: number[] = [];\n const stageBack: number[] = [];\n for (let j = 0; j < candidates.length; j += 1) {\n let bestCost = Number.POSITIVE_INFINITY;\n let bestPrev = -1;\n for (let k = 0; k < prevCosts.length; k += 1) {\n const transition = symmetricDifferenceSize(\n prevSets[k] ?? new Set(),\n curSets[j] ?? new Set(),\n );\n const cost = (prevCosts[k] ?? 0) + transition;\n if (cost < bestCost) {\n bestCost = cost;\n bestPrev = k;\n }\n }\n stageCosts.push(bestCost + j * RANK_PENALTY);\n stageBack.push(bestPrev);\n }\n dp.push(stageCosts);\n back.push(stageBack);\n }\n\n const lastCosts = dp[dp.length - 1] ?? [];\n let bestFinal = 0;\n for (let j = 1; j < lastCosts.length; j += 1) {\n if ((lastCosts[j] ?? Number.POSITIVE_INFINITY) < (lastCosts[bestFinal] ?? 0)) {\n bestFinal = j;\n }\n }\n\n const chosenIndices: number[] = new Array(candidateLists.length);\n let current = bestFinal;\n for (let i = candidateLists.length - 1; i >= 0; i -= 1) {\n chosenIndices[i] = current;\n current = back[i]?.[current] ?? -1;\n }\n\n return chords.map((chord, i) => {\n const candidates = candidateLists[i] ?? [];\n const scale = candidates[chosenIndices[i] ?? 0] ?? { name: 'chromatic', rootPc: chord.rootPc };\n return { chord, scale };\n });\n}\n","import { pitchClassOf } from '../../core/pitch/index.js';\nimport { assertFiniteNumber } from '../../core/validation/index.js';\nimport { type ChordToneRole, chordToneRole, thirdSlotOf } from '../chord/index.js';\nimport { type ChordLike, toChordData } from '../symbol/index.js';\n\n/**\n * Harmonic role a pitch plays within a chord.\n *\n * @category Functional Harmony\n */\nexport type HarmonyRole = ChordToneRole | 'tension' | 'doubling';\n\n/**\n * How firmly a pitch is locked to the chord's identity.\n *\n * - `identity`: moving it produces a different chord (the root).\n * - `quality`: moving it flips the chord quality (the third).\n * - `voicing`: it can move freely without changing chord identity or quality.\n *\n * @category Functional Harmony\n */\nexport type LockLevel = 'identity' | 'quality' | 'voicing';\n\n/**\n * A pitch's role, lock level, and owning chord.\n *\n * @category Functional Harmony\n */\nexport type VoicedRole = {\n role: HarmonyRole;\n lock: LockLevel;\n belongsToChordId: number;\n};\n\n/**\n * Classify a pitch's harmonic role and lock level within a chord.\n *\n * The role comes from the pitch's interval class above the chord root, read\n * against the chord's own template: root (0), third (3/4), fifth (6/7/8),\n * sixth or seventh (9/10/11). Every pitch the template does not claim is\n * `'tension'` — including one that is simply foreign to the chord, which this\n * single-pitch query cannot tell from a colour tone. The root locks the chord\n * identity, the third locks its quality, and everything else is free voicing.\n * Where a chord has no third, the tone standing in its place — the fourth of a\n * `sus4`, the second of a `sus2`, the eleventh of an eleventh chord — takes the\n * third's slot and is locked to `quality`, since moving it changes the chord.\n * At most one interval class of a chord is ever reported as its third, so a\n * consumer reading `lock: 'quality'` as \"this tone identifies the chord\" gets\n * one answer rather than two. Detecting an octave doubling requires the\n * surrounding voicing, which this query does not carry, so `'doubling'` is part\n * of the type — the shared vocabulary of harmonic roles — but is never returned\n * here.\n *\n * @param pitch MIDI pitch or bare pitch class.\n * @param chord The chord providing the root reference, as a chord symbol, chord\n * data, or a `Chord`.\n * @param chordId Identifier stored on the result (defaults to 0).\n * @returns The pitch's role, lock level, and owning chord id.\n * @example\n * ```ts\n * import { roleOf, makeChord } from '@libraz/libcantus';\n * roleOf(64, makeChord(0, 'maj')); // E over C major\n * // { role: 'third', lock: 'quality', belongsToChordId: 0 }\n * ```\n * @category Functional Harmony\n */\nexport function roleOf(pitch: number, chord: ChordLike, chordId = 0): VoicedRole {\n // The id is carried into the result, so a value that is not one would be\n // reported back as the chord this role belongs to.\n assertFiniteNumber(chordId, 'chordId');\n const data = toChordData(chord);\n const interval = (pitchClassOf(pitch) - pitchClassOf(data.rootPc) + 12) % 12;\n // What stands in the third's slot is read where the chord is read, not here:\n // the tone that identifies a chord and the tone the avoid-note rule refuses\n // against it are two consequences of one fact about the chord.\n const slot = thirdSlotOf(data);\n const isSuspendedTone = slot.kind === 'suspension' && interval === slot.interval;\n const chordRole = chordToneRole(pitch, data);\n let role: HarmonyRole;\n let lock: LockLevel;\n if (isSuspendedTone) {\n role = 'third'; // suspended tone occupies the third's quality-defining slot\n lock = 'quality';\n } else if (chordRole !== null) {\n role = chordRole;\n lock = chordRole === 'root' ? 'identity' : chordRole === 'third' ? 'quality' : 'voicing';\n } else {\n role = 'tension';\n lock = 'voicing';\n }\n return { role, lock, belongsToChordId: chordId };\n}\n"],"mappings":"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","names":[]}
@@ -1,19 +1,19 @@
1
1
  import {
2
2
  assertTonicOf,
3
3
  isScaleTone,
4
+ resolveKey,
4
5
  spellChord,
5
- spellPitchClass,
6
- spelledKeyOf,
7
- toKeyScale
8
- } from "./chunk-INHVXP6Z.js";
6
+ spellPitchClass
7
+ } from "./chunk-RFIBF2RR.js";
9
8
  import {
10
9
  assertGenerationBudget,
11
10
  assertNoteEvents,
11
+ assertOptions,
12
12
  diatonicLetterOf,
13
13
  naturalPitchClassOf,
14
14
  pitchClassOf,
15
15
  toNoteData
16
- } from "./chunk-VTI6FOLB.js";
16
+ } from "./chunk-AGOVW7U6.js";
17
17
 
18
18
  // src/analyze/spelling/index.ts
19
19
  var CHORD_MISMATCH = 64;
@@ -141,7 +141,9 @@ function transitionCost(from, source, to, target) {
141
141
  return letterStepCost(source, target, semitones) + directionCost(target, to.chromatic, semitones) + directionCost(source, from.chromatic, semitones);
142
142
  }
143
143
  function spellLine(notes, timeline, key, opts = {}) {
144
- const scale = toKeyScale(key);
144
+ assertOptions(opts, "opts");
145
+ const resolved = resolveKey(key);
146
+ const scale = resolved.scale;
145
147
  const given = opts.tonic === void 0 ? void 0 : toNoteData(opts.tonic);
146
148
  assertNoteEvents(notes, "line notes", {
147
149
  allowNonPositiveDuration: true,
@@ -158,7 +160,7 @@ function spellLine(notes, timeline, key, opts = {}) {
158
160
  if (notes.length === 0) {
159
161
  return [];
160
162
  }
161
- const tonic = given ?? spelledKeyOf(scale).tonic;
163
+ const tonic = given ?? resolved.tonic;
162
164
  const states = notes.map(
163
165
  (note) => stateFor(note.pitch, timeline?.at(note.startBeat) ?? null, tonic, scale)
164
166
  );
@@ -208,4 +210,4 @@ function spellLine(notes, timeline, key, opts = {}) {
208
210
  export {
209
211
  spellLine
210
212
  };
211
- //# sourceMappingURL=chunk-TTCLJX6D.js.map
213
+ //# sourceMappingURL=chunk-NFJ4P6ZZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/analyze/spelling/index.ts"],"sourcesContent":["/**\n * Line spelling: name every note of a melodic line in one pass.\n *\n * {@link spellPitch} decides one note from the key, the root of the chord under\n * it and its two immediate neighbours. That is enough for a single note, but a\n * line spelled note by note can zig-zag between the two readings of one idea —\n * a rising whole-tone figure comes out F# Ab Bb, with a diminished third in the\n * middle of it.\n *\n * This module solves the whole line at once. The theory layer's own judgment\n * becomes the cost of emitting a spelling, the melodic rules become the cost of\n * moving from one spelling to the next, and a dynamic program takes the\n * cheapest path across the line. Nothing here re-decides what\n * {@link spellPitchClass} and {@link spellChord} already decide: it only weighs\n * their answers against the shape of the line they sit in.\n */\n\nimport type { Note, NoteLike } from '../../core/pitch/index.js';\nimport {\n diatonicLetterOf as mod7,\n pitchClassOf as mod12,\n naturalPitchClassOf as naturalPc,\n toNoteData,\n} from '../../core/pitch/index.js';\nimport type { KeyScale, NoteEvent } from '../../core/types.js';\nimport {\n assertGenerationBudget,\n assertNoteEvents,\n assertOptions,\n} from '../../core/validation/index.js';\nimport type { Chord } from '../../theory/chord/index.js';\nimport { isScaleTone, resolveKey, type SpelledKeyLike } from '../../theory/scale/index.js';\nimport { assertTonicOf, spellChord, spellPitchClass } from '../../theory/spelling/index.js';\nimport type { ChordTimeline } from '../timeline/index.js';\n\n/**\n * The four rules, and what happens where they pull against each other.\n *\n * The costs below are spread far enough apart that a rule can never be\n * outvoted by any number of the rules under it, which fixes the precedence:\n *\n * 1. **The sounding chord.** A note the chord names takes the chord's own\n * spelling of that tone. This is the one place the line departs from\n * {@link spellPitchClass}, which never respells a tone of the key: over a\n * Db7 the seventh has to read Cb even in C major, where B is a scale tone,\n * because a dominant seventh spelled as a sixth is not the chord that was\n * played.\n * 2. **The key's scale.** Anything else the key itself spells — a scale tone,\n * and the tonic — keeps that spelling, exactly as the note-by-note path\n * keeps it. No amount of melodic pressure moves it.\n * 3. **The shape of the line, then its direction.** A chromatic note is spelled\n * so the letters of the intervals around it match the intervals actually\n * sounding, and a stepwise line then prefers sharps rising and flats\n * falling. Shape outranks direction because a rising line spelled with\n * sharps is still wrong if it spells a whole tone as a diminished third.\n * 4. **The key's chromatic convention, then the fewest accidentals.** What the\n * key alone would have said, as the tiebreak it is: the direction rule beats\n * it, which is why a chromatic ascent in C major spells C# D# G# A# rather\n * than the flats the key prefers, but one-sided evidence — a chromatic note\n * with only one stepwise neighbour — does not.\n */\n\n/** Emission cost for a spelling the chord sounding under the note does not name. */\nconst CHORD_MISMATCH = 64;\n\n/** Emission cost for respelling a tone the key's own scale already names. */\nconst SCALE_TONE_MISMATCH = 32;\n\n/** Transition cost per diatonic step an interval's letters miss its written size by. */\nconst LETTER_STEP_DEVIATION = 4;\n\n/**\n * How many steps of deviation are counted before the cost stops growing.\n *\n * Two letters out is already an interval no reading supports, and capping there\n * keeps the whole melodic term below {@link SCALE_TONE_MISMATCH}, so a badly\n * shaped line can never argue a scale tone out of its key spelling.\n */\nconst MAX_COUNTED_DEVIATION = 2;\n\n/** Transition cost for an accidental leaning against the direction of a stepwise line. */\nconst AGAINST_DIRECTION = 3;\n\n/** Emission cost for a chromatic spelling the key alone would not choose. */\nconst KEY_MISMATCH = 4;\n\n/** Emission cost per semitone of alteration: what minimises accidentals overall. */\nconst ACCIDENTAL = 1;\n\n/** Widest melodic interval still counted as a step, where the direction rule applies. */\nconst MAX_STEP = 2;\n\n/** Widest alteration a spelling may carry: a double accidental. */\nconst MAX_ALTER = 2;\n\n/**\n * Spellings each pitch class has within a double accidental.\n *\n * Always exactly three — a natural or its accidental, one letter above, one\n * letter below (C is C, B# and Dbb) — which is what bounds the work the dynamic\n * program is allowed to do.\n */\nconst SPELLINGS_PER_PITCH = 3;\n\n/**\n * The diatonic letter distances a melodic interval of a given size is\n * conventionally written with, indexed by its semitone class.\n *\n * Most sizes read only one way: a whole tone is a second, a minor third is a\n * third, so spelling one as anything else costs. Two sizes read two ways and\n * both are free — a semitone is either a chromatic inflection of one letter\n * (F to F#) or a diatonic step (E to F), and the tritone is either an augmented\n * fourth or a diminished fifth. Direction chooses between those.\n */\nconst CONVENTIONAL_LETTER_STEPS: readonly (readonly number[])[] = [\n [0], // unison\n [0, 1], // chromatic semitone or diatonic semitone\n [1], // second\n [2], // minor third\n [2], // major third\n [3], // fourth\n [3, 4], // augmented fourth or diminished fifth\n [4], // fifth\n [5], // minor sixth\n [5], // major sixth\n [6], // minor seventh\n [6], // major seventh\n];\n\n/**\n * Options for {@link spellLine}.\n *\n * @category Arrangement & Analysis\n */\nexport type SpellLineOptions = {\n /**\n * Tonic spelling to anchor the key, for a caller that already knows how the\n * piece is written. It must sound the key's own root pitch class; anything\n * else is rejected rather than spelled. Defaults to the conventional\n * spelling the key itself is written with. Accepted as a note name, a MIDI\n * number, note data, or a `Note`.\n */\n tonic?: NoteLike;\n /** Upper bound on the work the line may cost; defaults to the generation budget. */\n budget?: number;\n};\n\n/** Shortest signed alteration (in [-6, 6]) taking a letter's natural pc to `pc`. */\nfunction alterationFor(letter: number, pc: number): number {\n const distance = mod12(pc - naturalPc(letter));\n return distance > 6 ? distance - 12 : distance;\n}\n\n/** The octave that reproduces `pitch` from a letter and its alteration. */\nfunction octaveFor(pitch: number, letter: number, alter: number): number {\n return (pitch - naturalPc(letter) - alter) / 12 - 1;\n}\n\n/** Whether two spellings name the same letter with the same alteration. */\nfunction sameSpelling(a: Note, b: Note): boolean {\n return a.letter === b.letter && a.alter === b.alter;\n}\n\n/**\n * Every spelling of `pitch` within a double accidental, `preferred` first.\n *\n * The order is what settles a tie: two readings that cost the same are equally\n * defensible, and the one the theory layer would have chosen alone is then the\n * one that stands.\n */\nfunction candidatesFor(pitch: number, preferred: Note): Note[] {\n const pc = mod12(pitch);\n const rest: Note[] = [];\n for (let letter = 0; letter < 7; letter += 1) {\n const alter = alterationFor(letter, pc);\n if (Math.abs(alter) > MAX_ALTER || letter === preferred.letter) {\n continue;\n }\n rest.push({ letter, alter, octave: octaveFor(pitch, letter, alter) });\n }\n rest.sort((a, b) => Math.abs(a.alter) - Math.abs(b.alter) || a.letter - b.letter);\n return [\n {\n letter: preferred.letter,\n alter: preferred.alter,\n octave: octaveFor(pitch, preferred.letter, preferred.alter),\n },\n ...rest,\n ];\n}\n\n/**\n * The chord's own spelling of a pitch class, or undefined when the chord does\n * not sound it.\n *\n * The tones come from {@link spellChord}, so a chord carrying its own spellings\n * — a parsed symbol, an augmented sixth — is read by them; a slash bass outside\n * the chord's tones is spelled from its hint when it has one.\n */\nfunction chordSpellingOf(pc: number, chord: Chord, tonic: Note, key: KeyScale): Note | undefined {\n for (const tone of spellChord(chord, tonic, key)) {\n if (mod12(naturalPc(tone.letter) + tone.alter) === pc) {\n return tone;\n }\n }\n const bass = chord.bassPc;\n if (bass === undefined || mod12(bass) !== pc) {\n return undefined;\n }\n const hint = chord.bassSpelling;\n return hint !== undefined && mod12(naturalPc(hint.letter) + hint.alter) === pc\n ? { letter: mod7(hint.letter), alter: hint.alter }\n : spellPitchClass(pc, tonic, key);\n}\n\n/** One note of the line, with the spellings open to it and what each costs. */\ntype LineState = {\n pitch: number;\n /** True when the key does not already spell this pitch, so direction may decide. */\n chromatic: boolean;\n candidates: Note[];\n emissions: number[];\n /** The spelling the note-by-note path would have chosen, used as the fallback. */\n reference: Note;\n};\n\n/**\n * Weigh every spelling open to one note against what the chord and the key say\n * about it.\n *\n * The reference spelling is the theory layer's own answer — the chord's, when\n * the chord names the note, and the key's otherwise — and the cost of departing\n * from it is what ranks the three rules that do not depend on the neighbours.\n */\nfunction stateFor(pitch: number, chord: Chord | null, tonic: Note, key: KeyScale): LineState {\n const pc = mod12(pitch);\n const fromChord = chord === null ? undefined : chordSpellingOf(pc, chord, tonic, key);\n const scaleTone = isScaleTone(pc, key);\n const reference = fromChord ?? spellPitchClass(pc, tonic, key);\n const mismatch =\n fromChord !== undefined ? CHORD_MISMATCH : scaleTone ? SCALE_TONE_MISMATCH : KEY_MISMATCH;\n const candidates = candidatesFor(pitch, reference);\n return {\n pitch,\n chromatic: !scaleTone,\n candidates,\n emissions: candidates.map(\n (candidate) =>\n (sameSpelling(candidate, reference) ? 0 : mismatch) +\n ACCIDENTAL * Math.abs(candidate.alter),\n ),\n reference,\n };\n}\n\n/** A spelling's position on the diatonic ladder, octave included. */\nfunction diatonicIndex(note: Note): number {\n return note.letter + 7 * (note.octave ?? 0);\n}\n\n/**\n * What the letters of a melodic interval cost when they do not match the\n * interval that actually sounds.\n *\n * Both notes are already spelled, so the sounding interval and the written one\n * are both known: a whole tone written F# to Ab is two letters apart where a\n * second is one, and pays for the difference.\n */\nfunction letterStepCost(from: Note, to: Note, semitones: number): number {\n const written = diatonicIndex(to) - diatonicIndex(from);\n const size = Math.abs(semitones);\n const octaves = Math.floor(size / 12);\n const direction = semitones < 0 ? -1 : 1;\n let deviation = Number.POSITIVE_INFINITY;\n for (const steps of CONVENTIONAL_LETTER_STEPS[size % 12] ?? []) {\n deviation = Math.min(deviation, Math.abs(written - direction * (octaves * 7 + steps)));\n }\n if (!Number.isFinite(deviation)) {\n return 0;\n }\n return LETTER_STEP_DEVIATION * Math.min(deviation, MAX_COUNTED_DEVIATION);\n}\n\n/**\n * What an accidental costs for leaning the wrong way in a stepwise line: a\n * rising line is written with sharps and a falling one with flats.\n *\n * Only steps are judged. The rule describes chromatic motion, and a leap onto a\n * flat says nothing about the flat — the key and the chord have the whole story\n * there.\n */\nfunction directionCost(note: Note, chromatic: boolean, semitones: number): number {\n if (!chromatic || semitones === 0 || Math.abs(semitones) > MAX_STEP) {\n return 0;\n }\n const rising = semitones > 0;\n return (rising && note.alter < 0) || (!rising && note.alter > 0) ? AGAINST_DIRECTION : 0;\n}\n\n/**\n * The cost of following one spelling with another.\n *\n * Direction is charged to both notes: a chromatic note is judged by the step it\n * arrives on and by the step it leaves on, so a note inside a run collects the\n * rule twice and a note at the end of one collects it once.\n */\nfunction transitionCost(from: LineState, source: Note, to: LineState, target: Note): number {\n const semitones = to.pitch - from.pitch;\n return (\n letterStepCost(source, target, semitones) +\n directionCost(target, to.chromatic, semitones) +\n directionCost(source, from.chromatic, semitones)\n );\n}\n\n/**\n * Spell a whole melodic line at once, so its notes agree with each other.\n *\n * Every note is spelled together with the rest of the line rather than one at a\n * time: each pitch offers the three spellings a double accidental allows, the\n * chord and the key price them, the intervals between neighbours price the\n * pairs, and the cheapest reading of the whole line wins. Ties fall to the\n * spelling {@link spellPitchClass} would have chosen alone, so the answer never\n * drifts from the theory layer without a reason.\n *\n * Four rules decide, in this order:\n *\n * 1. A note the sounding chord names takes that chord's spelling of it, even\n * when the key spells the pitch otherwise — the seventh of a Db7 reads Cb in\n * C major, not B.\n * 2. Anything else the key's scale spells keeps its key spelling, exactly as\n * the note-by-note path keeps it.\n * 3. A chromatic note is spelled so the written intervals around it match the\n * sounding ones, and then so a rising step carries a sharp and a falling\n * step a flat.\n * 4. Failing all of that, the key's own chromatic spelling stands, and the\n * fewest accidentals win.\n *\n * Without a timeline — pass `null` — rules 2 to 4 still apply in full, so a\n * diatonic line and a chromatic run come out the same. What is lost is rule 1:\n * a chromatic chord can no longer name its own tones, so the tones of a\n * borrowed or altered chord fall back to the key's convention (that Cb becomes\n * B), and an enharmonically ambiguous tone under a chord that would have fixed\n * it is decided by the line's direction instead.\n *\n * @param notes The line, monophonic and in time order. Notes sharing an onset\n * are read as consecutive, so split polyphonic material into voices first.\n * @param timeline The chords sounding under the line, or null when only the key\n * is known. Each note is judged against the chord at its own onset.\n * @param key The key the line is written in, as a key name, a key/scale, or a\n * `Key`.\n * @param opts Optional tonic spelling and work budget.\n * @returns One spelled note per input note, in input order, each carrying the\n * octave that reproduces its pitch.\n * @throws If `opts.tonic` does not sound the key's root pitch class, if a note\n * event is malformed, or if the line is longer than the budget allows.\n * @example\n * ```ts\n * import { chordTimelineFromChords, majorKey, noteNames, spellLine } from '@libraz/libcantus';\n * // A chromatic ascent spells with sharps, whatever the key would say alone.\n * const rising = [60, 61, 62, 63, 64].map((pitch, index) => ({\n * pitch,\n * startBeat: index,\n * durationBeat: 1,\n * }));\n * noteNames(spellLine(rising, null, majorKey(0)));\n * // ['C4', 'C#4', 'D4', 'D#4', 'E4']\n * // Under a Db7 the same B sounds as the chord's seventh.\n * const timeline = chordTimelineFromChords([{ rootPc: 1, quality: 'dom7', startBeat: 0 }], 4);\n * noteNames(spellLine([{ pitch: 71, startBeat: 0, durationBeat: 1 }], timeline, majorKey(0)));\n * // ['Cb5']\n * ```\n * @category Arrangement & Analysis\n */\nexport function spellLine(\n notes: readonly NoteEvent[],\n timeline: ChordTimeline | null,\n key: SpelledKeyLike,\n opts: SpellLineOptions = {},\n): Note[] {\n assertOptions(opts, 'opts');\n // Read whole rather than reduced: a line in Ab minor is written on flats, and\n // a key handed in spelled that way would otherwise have its spelling derived\n // back from the pitch classes and come out in G# minor.\n const resolved = resolveKey(key);\n const scale = resolved.scale;\n const given = opts.tonic === undefined ? undefined : toNoteData(opts.tonic);\n assertNoteEvents(notes, 'line notes', {\n allowNonPositiveDuration: true,\n budget: opts.budget,\n });\n // The dynamic program compares every spelling of a note with every spelling\n // of the one before it, so the work is bounded before any of it is done.\n assertGenerationBudget(\n notes.length * SPELLINGS_PER_PITCH * SPELLINGS_PER_PITCH,\n 'line spelling states',\n opts.budget,\n );\n if (given !== undefined) {\n assertTonicOf(given, scale, 'spellLine');\n }\n if (notes.length === 0) {\n return [];\n }\n const tonic = given ?? resolved.tonic;\n const states = notes.map((note) =>\n stateFor(note.pitch, timeline?.at(note.startBeat) ?? null, tonic, scale),\n );\n\n // Viterbi across the line: `costs` holds the cheapest path reaching each\n // spelling of the current note, and `back` remembers which spelling of the\n // previous note that path came through.\n let costs: number[] = [];\n let previous: LineState | undefined;\n const back: number[][] = [];\n for (const current of states) {\n if (previous === undefined) {\n costs = [...current.emissions];\n back.push(current.candidates.map(() => 0));\n previous = current;\n continue;\n }\n const reached: number[] = [];\n const chosen: number[] = [];\n for (const [target, candidate] of current.candidates.entries()) {\n let best = Number.POSITIVE_INFINITY;\n let bestSource = 0;\n for (const [source, sourceNote] of previous.candidates.entries()) {\n const total =\n (costs[source] ?? Number.POSITIVE_INFINITY) +\n transitionCost(previous, sourceNote, current, candidate);\n if (total < best) {\n best = total;\n bestSource = source;\n }\n }\n reached.push(best + (current.emissions[target] ?? 0));\n chosen.push(bestSource);\n }\n costs = reached;\n back.push(chosen);\n previous = current;\n }\n\n let cursor = 0;\n for (const [index, cost] of costs.entries()) {\n if (cost < (costs[cursor] ?? Number.POSITIVE_INFINITY)) {\n cursor = index;\n }\n }\n const picked: number[] = new Array(states.length).fill(0);\n for (let index = states.length - 1; index >= 0; index -= 1) {\n picked[index] = cursor;\n cursor = back[index]?.[cursor] ?? 0;\n }\n return states.map((state, index) => state.candidates[picked[index] ?? 0] ?? state.reference);\n}\n"],"mappings":"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","names":[]}