@libraz/libcantus 0.9.2 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/README.md +6 -5
  2. package/dist/analyze/index.cjs +41 -1676
  3. package/dist/analyze/index.cjs.map +1 -1
  4. package/dist/analyze/index.d.cts +194 -122
  5. package/dist/analyze/index.d.ts +194 -122
  6. package/dist/analyze/index.js +19 -8
  7. package/dist/chunk-36N73KZ3.cjs +246 -0
  8. package/dist/chunk-36N73KZ3.cjs.map +1 -0
  9. package/dist/chunk-4HLLZPXT.cjs +3077 -0
  10. package/dist/chunk-4HLLZPXT.cjs.map +1 -0
  11. package/dist/chunk-7MGZ7D62.cjs +498 -0
  12. package/dist/chunk-7MGZ7D62.cjs.map +1 -0
  13. package/dist/{chunk-R3S2LUBH.js → chunk-7NKDPF74.js} +660 -305
  14. package/dist/chunk-7NKDPF74.js.map +1 -0
  15. package/dist/chunk-CAN76NCU.cjs +679 -0
  16. package/dist/chunk-CAN76NCU.cjs.map +1 -0
  17. package/dist/{chunk-7Y7WF64D.js → chunk-D2FAJBYL.js} +104 -35
  18. package/dist/chunk-D2FAJBYL.js.map +1 -0
  19. package/dist/{chunk-COKTX2G5.js → chunk-EHVEBWM5.js} +1052 -510
  20. package/dist/chunk-EHVEBWM5.js.map +1 -0
  21. package/dist/chunk-FCANXBAT.cjs +30 -0
  22. package/dist/chunk-FCANXBAT.cjs.map +1 -0
  23. package/dist/chunk-FNDWXRRI.cjs +479 -0
  24. package/dist/chunk-FNDWXRRI.cjs.map +1 -0
  25. package/dist/{chunk-GQKN5KDT.js → chunk-GQSGS2N4.js} +43 -29
  26. package/dist/chunk-GQSGS2N4.js.map +1 -0
  27. package/dist/chunk-HKGYCQ34.js +53 -0
  28. package/dist/chunk-HKGYCQ34.js.map +1 -0
  29. package/dist/chunk-I24MV65U.js +106 -0
  30. package/dist/chunk-I24MV65U.js.map +1 -0
  31. package/dist/{chunk-LN7ZZMD2.js → chunk-KMHOICIY.js} +322 -173
  32. package/dist/chunk-KMHOICIY.js.map +1 -0
  33. package/dist/chunk-LW53AHQU.cjs +1117 -0
  34. package/dist/chunk-LW53AHQU.cjs.map +1 -0
  35. package/dist/chunk-N6IHQUOX.js +1 -0
  36. package/dist/chunk-N6IHQUOX.js.map +1 -0
  37. package/dist/chunk-NKIBQ6PT.js +30 -0
  38. package/dist/chunk-NKIBQ6PT.js.map +1 -0
  39. package/dist/chunk-NP4NA2FB.cjs +106 -0
  40. package/dist/chunk-NP4NA2FB.cjs.map +1 -0
  41. package/dist/chunk-NVE34YGZ.cjs +1449 -0
  42. package/dist/chunk-NVE34YGZ.cjs.map +1 -0
  43. package/dist/{chunk-A4532ZFR.js → chunk-NYYWMV2C.js} +115 -47
  44. package/dist/chunk-NYYWMV2C.js.map +1 -0
  45. package/dist/{chunk-5HTNMQW4.js → chunk-Q2XUP227.js} +310 -118
  46. package/dist/chunk-Q2XUP227.js.map +1 -0
  47. package/dist/chunk-Q6OP4U43.cjs +327 -0
  48. package/dist/chunk-Q6OP4U43.cjs.map +1 -0
  49. package/dist/chunk-RC6GBMCD.cjs +845 -0
  50. package/dist/chunk-RC6GBMCD.cjs.map +1 -0
  51. package/dist/{chunk-XTC3X6MS.js → chunk-RK35RIN7.js} +453 -152
  52. package/dist/chunk-RK35RIN7.js.map +1 -0
  53. package/dist/chunk-RUMU3UA6.cjs +53 -0
  54. package/dist/chunk-RUMU3UA6.cjs.map +1 -0
  55. package/dist/chunk-T64C7ZD7.cjs +1 -0
  56. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  57. package/dist/chunk-XEBG3G53.js +498 -0
  58. package/dist/chunk-XEBG3G53.js.map +1 -0
  59. package/dist/chunk-YLEMGEFH.js +246 -0
  60. package/dist/chunk-YLEMGEFH.js.map +1 -0
  61. package/dist/chunk-ZEJR4TKL.cjs +173 -0
  62. package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
  63. package/dist/core/index.cjs +130 -431
  64. package/dist/core/index.cjs.map +1 -1
  65. package/dist/core/index.d.cts +331 -17
  66. package/dist/core/index.d.ts +331 -17
  67. package/dist/core/index.js +71 -13
  68. package/dist/{function-2bJW1Zp_.d.ts → function-BtiZb9i9.d.ts} +28 -7
  69. package/dist/{function-r2sh-jOO.d.cts → function-DNugAOrt.d.cts} +28 -7
  70. package/dist/generate/index.cjs +30 -3751
  71. package/dist/generate/index.cjs.map +1 -1
  72. package/dist/generate/index.d.cts +373 -59
  73. package/dist/generate/index.d.ts +373 -59
  74. package/dist/generate/index.js +23 -9
  75. package/dist/{index-DZ1M21do.d.cts → index-6VR8-kit.d.ts} +20 -4
  76. package/dist/index-70p168VM.d.ts +192 -0
  77. package/dist/{index-D_ZTUNKp.d.cts → index-Be74mcon.d.cts} +18 -17
  78. package/dist/index-BtXT6UQ0.d.cts +192 -0
  79. package/dist/{index-BMPxwjwV.d.ts → index-C5UBbQ4G.d.ts} +18 -17
  80. package/dist/{index-BOBXBNwP.d.cts → index-C83mZI1P.d.ts} +93 -8
  81. package/dist/{index-BdeQ1r_p.d.cts → index-D6b6na69.d.cts} +14 -11
  82. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  83. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  84. package/dist/index-OG65ocOr.d.cts +272 -0
  85. package/dist/index-OG65ocOr.d.ts +272 -0
  86. package/dist/{index-BreoJRw0.d.ts → index-tJhkmQgE.d.cts} +20 -4
  87. package/dist/{index-CP3wAfEH.d.ts → index-tSQh-xwY.d.ts} +14 -11
  88. package/dist/{index-CM5W4rcm.d.ts → index-y_ak9_Hi.d.cts} +93 -8
  89. package/dist/index.cjs +419 -6848
  90. package/dist/index.cjs.map +1 -1
  91. package/dist/index.d.cts +14 -13
  92. package/dist/index.d.ts +14 -13
  93. package/dist/index.js +132 -27
  94. package/dist/model/index.cjs +8 -2423
  95. package/dist/model/index.cjs.map +1 -1
  96. package/dist/model/index.d.cts +433 -74
  97. package/dist/model/index.d.ts +433 -74
  98. package/dist/model/index.js +6 -7
  99. package/dist/styled-DIRkk9Uz.d.ts +506 -0
  100. package/dist/styled-DJbkJQfj.d.cts +506 -0
  101. package/dist/theory/index.cjs +172 -1676
  102. package/dist/theory/index.cjs.map +1 -1
  103. package/dist/theory/index.d.cts +151 -233
  104. package/dist/theory/index.d.ts +151 -233
  105. package/dist/theory/index.js +31 -7
  106. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  107. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  108. package/package.json +73 -24
  109. package/dist/chunk-5HTNMQW4.js.map +0 -1
  110. package/dist/chunk-7Y7WF64D.js.map +0 -1
  111. package/dist/chunk-A4532ZFR.js.map +0 -1
  112. package/dist/chunk-BHUT2S5G.js +0 -22
  113. package/dist/chunk-BHUT2S5G.js.map +0 -1
  114. package/dist/chunk-CCBLVAPK.js +0 -42
  115. package/dist/chunk-CCBLVAPK.js.map +0 -1
  116. package/dist/chunk-COKTX2G5.js.map +0 -1
  117. package/dist/chunk-DYWFNC4A.js +0 -152
  118. package/dist/chunk-DYWFNC4A.js.map +0 -1
  119. package/dist/chunk-GQKN5KDT.js.map +0 -1
  120. package/dist/chunk-LN7ZZMD2.js.map +0 -1
  121. package/dist/chunk-MSWFZF7J.js +0 -118
  122. package/dist/chunk-MSWFZF7J.js.map +0 -1
  123. package/dist/chunk-R3S2LUBH.js.map +0 -1
  124. package/dist/chunk-SDM3VJJ5.js +0 -41
  125. package/dist/chunk-SDM3VJJ5.js.map +0 -1
  126. package/dist/chunk-XTC3X6MS.js.map +0 -1
  127. package/dist/chunk-ZXW5YIWK.js +0 -55
  128. package/dist/chunk-ZXW5YIWK.js.map +0 -1
  129. package/dist/index-CGMZNCx6.d.cts +0 -140
  130. package/dist/index-CGMZNCx6.d.ts +0 -140
  131. package/dist/styled-BjXtOFNk.d.cts +0 -282
  132. package/dist/styled-D7BAx3S3.d.ts +0 -282
  133. package/src/analyze/arrange/index.ts +0 -21
  134. package/src/analyze/arrange/internal.ts +0 -125
  135. package/src/analyze/arrange/tension.ts +0 -187
  136. package/src/analyze/arrange/tracks.ts +0 -279
  137. package/src/analyze/detect/index.ts +0 -235
  138. package/src/analyze/functional/borrowed.ts +0 -84
  139. package/src/analyze/functional/cadence.ts +0 -66
  140. package/src/analyze/functional/function.ts +0 -184
  141. package/src/analyze/functional/index.ts +0 -23
  142. package/src/analyze/functional/internal.ts +0 -29
  143. package/src/analyze/functional/roman.ts +0 -393
  144. package/src/analyze/index.ts +0 -49
  145. package/src/analyze/timeline/index.ts +0 -415
  146. package/src/analyze/voice/index.ts +0 -330
  147. package/src/core/index.ts +0 -47
  148. package/src/core/interval/index.ts +0 -81
  149. package/src/core/meter/index.ts +0 -282
  150. package/src/core/pitch/index.ts +0 -311
  151. package/src/core/random/index.ts +0 -53
  152. package/src/core/tuning/index.ts +0 -156
  153. package/src/core/types.ts +0 -29
  154. package/src/generate/bass/index.ts +0 -350
  155. package/src/generate/countermelody/index.ts +0 -379
  156. package/src/generate/drums/beat.ts +0 -266
  157. package/src/generate/drums/euclid.ts +0 -58
  158. package/src/generate/drums/fills.ts +0 -356
  159. package/src/generate/drums/ghost.ts +0 -119
  160. package/src/generate/drums/hihat.ts +0 -285
  161. package/src/generate/drums/hit.ts +0 -31
  162. package/src/generate/drums/index.ts +0 -329
  163. package/src/generate/drums/internal.ts +0 -257
  164. package/src/generate/drums/kick.ts +0 -160
  165. package/src/generate/drums/percussion.ts +0 -121
  166. package/src/generate/drums/rng.ts +0 -7
  167. package/src/generate/drums/swing.ts +0 -71
  168. package/src/generate/groove/index.ts +0 -326
  169. package/src/generate/harmonize/index.ts +0 -440
  170. package/src/generate/index.ts +0 -59
  171. package/src/generate/motif/index.ts +0 -398
  172. package/src/generate/progression/index.ts +0 -370
  173. package/src/generate/reharmony/index.ts +0 -330
  174. package/src/generate/rhythm/index.ts +0 -165
  175. package/src/index.ts +0 -247
  176. package/src/model/chord.ts +0 -423
  177. package/src/model/index.ts +0 -15
  178. package/src/model/interval.ts +0 -82
  179. package/src/model/key.ts +0 -266
  180. package/src/model/note.ts +0 -176
  181. package/src/model/progression.ts +0 -161
  182. package/src/model/shared.ts +0 -12
  183. package/src/theory/chord/index.ts +0 -373
  184. package/src/theory/chordscale/index.ts +0 -374
  185. package/src/theory/counterpoint/index.ts +0 -230
  186. package/src/theory/harmony/index.ts +0 -92
  187. package/src/theory/index.ts +0 -107
  188. package/src/theory/safety/index.ts +0 -419
  189. package/src/theory/scale/degrees.ts +0 -123
  190. package/src/theory/scale/index.ts +0 -28
  191. package/src/theory/scale/key.ts +0 -56
  192. package/src/theory/scale/masks.ts +0 -153
  193. package/src/theory/spelling/index.ts +0 -182
  194. package/src/theory/symbol/index.ts +0 -268
  195. package/src/theory/voicing/index.ts +0 -5
  196. package/src/theory/voicing/internal.ts +0 -168
  197. package/src/theory/voicing/leading.ts +0 -112
  198. package/src/theory/voicing/satb.ts +0 -195
  199. package/src/theory/voicing/styled.ts +0 -176
  200. package/tsconfig.json +0 -23
@@ -1,26 +1,32 @@
1
- import {
2
- formatNote,
3
- midiToNote,
4
- noteToPitchClass,
5
- parseNote
6
- } from "./chunk-DYWFNC4A.js";
7
1
  import {
8
2
  NAMED_SCALES,
9
3
  chordPitchClasses,
10
4
  chordToneRole,
11
5
  createsHiddenParallelPerfect,
12
6
  createsParallelPerfect,
13
- createsVoiceCrossing,
14
7
  createsVoiceOverlap,
15
- exceedsSpacing,
16
8
  makeChord,
9
+ namedScaleMask,
10
+ requireScaleMask,
17
11
  scaleTonesInDegreeOrder
18
- } from "./chunk-7Y7WF64D.js";
12
+ } from "./chunk-D2FAJBYL.js";
13
+ import {
14
+ InvalidInputError,
15
+ NoSolutionError,
16
+ assertFiniteNumber,
17
+ assertGenerationBudget,
18
+ assertInteger,
19
+ assertPositiveInt,
20
+ diatonicLetterOf,
21
+ formatNote,
22
+ midiToNote,
23
+ naturalPitchClassOf,
24
+ noteToPitchClass,
25
+ parseNote,
26
+ pitchClassOf
27
+ } from "./chunk-XEBG3G53.js";
19
28
 
20
29
  // src/theory/chordscale/index.ts
21
- function pitchClass(value) {
22
- return (Math.trunc(value) % 12 + 12) % 12;
23
- }
24
30
  function popcount12(mask) {
25
31
  let count = 0;
26
32
  for (let n = 0; n < 12; n += 1) {
@@ -29,7 +35,7 @@ function popcount12(mask) {
29
35
  return count;
30
36
  }
31
37
  function maskHasPitchClass(mask, scaleRootPc, pc) {
32
- const offset = pitchClass(pc - scaleRootPc);
38
+ const offset = pitchClassOf(pc - scaleRootPc);
33
39
  return (mask >> offset & 1) === 1;
34
40
  }
35
41
  function scaleMatchesChord(chordPcs, scaleMask, scaleRootPc) {
@@ -42,16 +48,31 @@ function scaleMatchesChord(chordPcs, scaleMask, scaleRootPc) {
42
48
  }
43
49
  var ALIASED_SCALE_NAMES = /* @__PURE__ */ new Set(["major", "naturalMinor"]);
44
50
  var HEPTATONIC_SIZE = 7;
51
+ var SCALE_PREFERENCE = [
52
+ "ionian",
53
+ "major",
54
+ "mixolydian",
55
+ "dorian",
56
+ "lydian",
57
+ "aeolian",
58
+ "naturalMinor",
59
+ "phrygian",
60
+ "locrian"
61
+ ];
62
+ function conventionalRank(name) {
63
+ const index = SCALE_PREFERENCE.indexOf(name);
64
+ return index < 0 ? SCALE_PREFERENCE.length : index;
65
+ }
45
66
  function chordScales(chord) {
46
67
  const chordPcs = chordPitchClasses(chord);
47
- const rootPc = pitchClass(chord.rootPc);
68
+ const rootPc = pitchClassOf(chord.rootPc);
48
69
  const ranked = [];
49
70
  const seenMasks = /* @__PURE__ */ new Set();
50
71
  for (const name of Object.keys(NAMED_SCALES)) {
51
72
  if (name === "chromatic" || ALIASED_SCALE_NAMES.has(name)) {
52
73
  continue;
53
74
  }
54
- const mask = NAMED_SCALES[name];
75
+ const mask = namedScaleMask(name);
55
76
  if (mask === void 0 || seenMasks.has(mask)) {
56
77
  continue;
57
78
  }
@@ -78,16 +99,18 @@ function chordScales(chord) {
78
99
  if (a.size !== b.size) {
79
100
  return a.size - b.size;
80
101
  }
102
+ const rankA = conventionalRank(a.name);
103
+ const rankB = conventionalRank(b.name);
104
+ if (rankA !== rankB) {
105
+ return rankA - rankB;
106
+ }
81
107
  return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
82
108
  });
83
109
  return ranked.map((entry) => ({ name: entry.name, rootPc }));
84
110
  }
85
111
  function avoidNotes(chord, scaleName) {
86
- const mask = NAMED_SCALES[scaleName];
87
- if (mask === void 0) {
88
- return [];
89
- }
90
- const rootPc = pitchClass(chord.rootPc);
112
+ const mask = requireScaleMask(scaleName);
113
+ const rootPc = pitchClassOf(chord.rootPc);
91
114
  const chordPcs = chordPitchClasses(chord);
92
115
  if (!scaleMatchesChord(chordPcs, mask, rootPc)) {
93
116
  return [];
@@ -98,18 +121,15 @@ function avoidNotes(chord, scaleName) {
98
121
  if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc)) {
99
122
  continue;
100
123
  }
101
- if (chordSet.has(pitchClass(pc - 1))) {
124
+ if (chordSet.has(pitchClassOf(pc - 1))) {
102
125
  avoid.push(pc);
103
126
  }
104
127
  }
105
128
  return avoid.sort((a, b) => a - b);
106
129
  }
107
130
  function availableTensions(chord, scaleName) {
108
- const mask = NAMED_SCALES[scaleName];
109
- if (mask === void 0) {
110
- return [];
111
- }
112
- const rootPc = pitchClass(chord.rootPc);
131
+ const mask = requireScaleMask(scaleName);
132
+ const rootPc = pitchClassOf(chord.rootPc);
113
133
  const chordPcs = chordPitchClasses(chord);
114
134
  if (!scaleMatchesChord(chordPcs, mask, rootPc)) {
115
135
  return [];
@@ -137,14 +157,14 @@ function chordScaleReport(chord, limit) {
137
157
  }
138
158
  var RANK_PENALTY = 0.01;
139
159
  function scalePitchClassSet(name, rootPc) {
140
- const mask = NAMED_SCALES[name];
160
+ const mask = namedScaleMask(name);
141
161
  const pcs = /* @__PURE__ */ new Set();
142
162
  if (mask === void 0) {
143
163
  return pcs;
144
164
  }
145
165
  for (let n = 0; n < 12; n += 1) {
146
166
  if ((mask >> n & 1) === 1) {
147
- pcs.add(pitchClass(rootPc + n));
167
+ pcs.add(pitchClassOf(rootPc + n));
148
168
  }
149
169
  }
150
170
  return pcs;
@@ -222,64 +242,117 @@ function scalesForChanges(chords) {
222
242
  }
223
243
 
224
244
  // src/theory/spelling/index.ts
225
- var LETTER_SEMITONES = [0, 2, 4, 5, 7, 9, 11];
226
245
  var CHROMATIC_SPELLING = {
227
- 1: { degreeOffset: 1, alter: -1 },
246
+ 1: 1,
228
247
  // b2
229
- 3: { degreeOffset: 2, alter: -1 },
248
+ 2: 1,
249
+ // M2
250
+ 3: 2,
230
251
  // b3
231
- 6: { degreeOffset: 3, alter: 1 },
252
+ 4: 2,
253
+ // M3
254
+ 5: 3,
255
+ // P4
256
+ 6: 3,
232
257
  // #4
233
- 8: { degreeOffset: 5, alter: -1 },
258
+ 7: 4,
259
+ // P5
260
+ 8: 5,
234
261
  // b6
235
- 9: { degreeOffset: 5, alter: 1 },
262
+ 9: 5,
236
263
  // #6 (raised sixth of a minor key)
237
- 10: { degreeOffset: 6, alter: -1 },
264
+ 10: 6,
238
265
  // b7
239
- 11: { degreeOffset: 6, alter: 1 }
266
+ 11: 6
240
267
  // #7 (raised leading tone of a minor key)
241
268
  };
242
- function mod12(n) {
243
- return (n % 12 + 12) % 12;
244
- }
245
- function mod7(n) {
246
- return (n % 7 + 7) % 7;
247
- }
248
- function naturalPc(letter) {
249
- return LETTER_SEMITONES[mod7(letter)] ?? 0;
250
- }
269
+ var CHROMATIC_SPELLING_FLAT = {
270
+ ...CHROMATIC_SPELLING,
271
+ 6: 4
272
+ // b5
273
+ };
274
+ var CHROMATIC_SPELLING_SHARP = {
275
+ 1: 0,
276
+ // #1
277
+ 2: 1,
278
+ // M2
279
+ 3: 1,
280
+ // #2
281
+ 4: 2,
282
+ // M3
283
+ 5: 3,
284
+ // P4
285
+ 6: 3,
286
+ // #4
287
+ 7: 4,
288
+ // P5
289
+ 8: 4,
290
+ // #5
291
+ 9: 5,
292
+ // M6
293
+ 10: 5,
294
+ // #6
295
+ 11: 6
296
+ // M7
297
+ };
251
298
  function alterFor(letter, pc) {
252
- let d = mod12(pc - naturalPc(letter));
299
+ let d = pitchClassOf(pc - naturalPitchClassOf(letter));
253
300
  if (d > 6) {
254
301
  d -= 12;
255
302
  }
256
303
  return d;
257
304
  }
305
+ function letterFor(tonic, degreeOffset, pc) {
306
+ if (degreeOffset === void 0) {
307
+ return void 0;
308
+ }
309
+ const letter = diatonicLetterOf(tonic.letter + degreeOffset);
310
+ return { letter, alter: alterFor(letter, pc) };
311
+ }
312
+ function pickSpelling(flat, sharp, preferFlat) {
313
+ if (flat === void 0 || sharp === void 0) {
314
+ return flat ?? sharp;
315
+ }
316
+ if (Math.abs(flat.alter) !== Math.abs(sharp.alter)) {
317
+ return Math.abs(flat.alter) < Math.abs(sharp.alter) ? flat : sharp;
318
+ }
319
+ return preferFlat ? flat : sharp;
320
+ }
258
321
  function isHeptatonic(key) {
259
322
  return scaleTonesInDegreeOrder(key).length === 7;
260
323
  }
261
- function spellPitchClass(pc, tonic, key) {
262
- const tonicPc = mod12(naturalPc(tonic.letter) + tonic.alter);
263
- const offset = mod12(pc - tonicPc);
324
+ function hasMinorThird(key) {
264
325
  const tones = scaleTonesInDegreeOrder(key);
265
- if (isHeptatonic(key)) {
266
- const degree = tones.indexOf(mod12(pc));
326
+ const root = pitchClassOf(key.rootPc);
327
+ return tones.includes(pitchClassOf(root + 3)) && !tones.includes(pitchClassOf(root + 4));
328
+ }
329
+ function spellPitchClass(pc, tonic, key) {
330
+ const tonicPc = pitchClassOf(naturalPitchClassOf(tonic.letter) + tonic.alter);
331
+ const offset = pitchClassOf(pc - tonicPc);
332
+ if (offset === 0) {
333
+ return { letter: diatonicLetterOf(tonic.letter), alter: tonic.alter };
334
+ }
335
+ const heptatonic = isHeptatonic(key);
336
+ if (heptatonic) {
337
+ const degree = scaleTonesInDegreeOrder(key).indexOf(pitchClassOf(pc));
267
338
  if (degree >= 0) {
268
- const letter = mod7(tonic.letter + degree);
339
+ const letter = diatonicLetterOf(tonic.letter + degree);
269
340
  return { letter, alter: alterFor(letter, pc) };
270
341
  }
271
- const chromatic = CHROMATIC_SPELLING[offset];
272
- if (chromatic) {
273
- const letter = mod7(tonic.letter + chromatic.degreeOffset);
342
+ const degreeOffset = CHROMATIC_SPELLING[offset];
343
+ if (degreeOffset !== void 0) {
344
+ const letter = diatonicLetterOf(tonic.letter + degreeOffset);
274
345
  return { letter, alter: alterFor(letter, pc) };
275
346
  }
276
- }
277
- for (let letter = 0; letter < 7; letter += 1) {
278
- if (naturalPc(letter) === mod12(pc)) {
279
- return { letter, alter: 0 };
347
+ } else {
348
+ const flat = letterFor(tonic, CHROMATIC_SPELLING_FLAT[offset], pc);
349
+ const sharp = letterFor(tonic, CHROMATIC_SPELLING_SHARP[offset], pc);
350
+ const best = pickSpelling(flat, sharp, tonic.alter < 0 || hasMinorThird(key));
351
+ if (best !== void 0) {
352
+ return best;
280
353
  }
281
354
  }
282
- const belowLetter = mod7([0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6][mod12(pc)] ?? 0);
355
+ const belowLetter = diatonicLetterOf([0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6][pitchClassOf(pc)] ?? 0);
283
356
  return { letter: belowLetter, alter: alterFor(belowLetter, pc) };
284
357
  }
285
358
  function spellScale(tonic, key) {
@@ -288,10 +361,71 @@ function spellScale(tonic, key) {
288
361
  function spellPitchClasses(pcs, tonic, key) {
289
362
  return pcs.map((pc) => spellPitchClass(pc, tonic, key));
290
363
  }
364
+ function chordLetterOffset(interval, chord) {
365
+ switch (interval) {
366
+ case 0:
367
+ case 12:
368
+ return 0;
369
+ case 1:
370
+ case 2:
371
+ case 13:
372
+ case 14:
373
+ return 1;
374
+ case 3:
375
+ case 4:
376
+ case 16:
377
+ return 2;
378
+ case 15:
379
+ return chord.intervals.some((i) => i === 3 || i === 4) ? 1 : 2;
380
+ case 5:
381
+ case 17:
382
+ case 18:
383
+ return 3;
384
+ case 6:
385
+ case 7:
386
+ case 8:
387
+ case 19:
388
+ return 4;
389
+ case 9:
390
+ return chord.quality === "dim7" ? 6 : 5;
391
+ case 20:
392
+ case 21:
393
+ return 5;
394
+ case 10:
395
+ case 11:
396
+ case 22:
397
+ case 23:
398
+ return 6;
399
+ default: {
400
+ const octaves = Math.floor(Math.max(0, interval) / 12);
401
+ const simple = pitchClassOf(interval);
402
+ const simpleOffset = [0, 1, 1, 2, 2, 3, 4, 4, 4, 5, 6, 6][simple] ?? 0;
403
+ return octaves * 7 + simpleOffset;
404
+ }
405
+ }
406
+ }
407
+ function spellChordFromRoot(chord, root) {
408
+ return chord.intervals.map((interval) => {
409
+ const letter = diatonicLetterOf(root.letter + chordLetterOffset(interval, chord));
410
+ const pc = pitchClassOf(chord.rootPc + interval);
411
+ return { letter, alter: alterFor(letter, pc) };
412
+ });
413
+ }
291
414
  function spellChord(chord, tonic, key) {
292
- return chord.intervals.map(
293
- (interval) => spellPitchClass(mod12(chord.rootPc + interval), tonic, key)
294
- );
415
+ return spellChordFromRoot(chord, chordRootSpelling(chord, tonic, key));
416
+ }
417
+ function chordRootSpelling(chord, tonic, key) {
418
+ const hint = chord.rootSpelling;
419
+ if (hint !== void 0 && pitchClassOf(naturalPitchClassOf(hint.letter) + hint.alter) === pitchClassOf(chord.rootPc)) {
420
+ return { letter: diatonicLetterOf(hint.letter), alter: hint.alter };
421
+ }
422
+ return spellPitchClass(chord.rootPc, tonic, key);
423
+ }
424
+ function spellPitch(pitch, tonic, key) {
425
+ const rounded = Math.round(pitch);
426
+ const spelled = spellPitchClass(pitchClassOf(rounded), tonic, key);
427
+ const octave = (rounded - naturalPitchClassOf(spelled.letter) - spelled.alter) / 12 - 1;
428
+ return { letter: spelled.letter, alter: spelled.alter, octave };
295
429
  }
296
430
  function noteNames(notes) {
297
431
  return notes.map(formatNote);
@@ -357,7 +491,36 @@ var QUALITY_MAP = {
357
491
  "7b13": "7b13",
358
492
  "11": "11",
359
493
  "13": "13",
360
- "5": "5"
494
+ "5": "5",
495
+ "7sus4": "7sus4",
496
+ "7sus": "7sus4",
497
+ "9sus4": "11",
498
+ "9sus": "11",
499
+ "7b5": "7b5",
500
+ "7alt": "7alt",
501
+ alt: "7alt",
502
+ "13b9": "13b9",
503
+ maj13: "maj13",
504
+ M13: "maj13",
505
+ "maj7#11": "maj7#11",
506
+ "M7#11": "maj7#11",
507
+ m11: "min11",
508
+ min11: "min11",
509
+ m13: "min13",
510
+ min13: "min13",
511
+ madd9: "minAdd9",
512
+ "min(add9)": "minAdd9",
513
+ "m(add9)": "minAdd9",
514
+ "6add9": "6/9",
515
+ "m6/9": "min6/9",
516
+ m69: "min6/9",
517
+ "min6/9": "min6/9",
518
+ // ASCII stand-ins for the degree and half-diminished glyphs, which most
519
+ // lead sheets and chord-chart text files use instead of the typographic ones.
520
+ o: "dim",
521
+ o7: "dim7",
522
+ h: "m7b5",
523
+ h7: "m7b5"
361
524
  };
362
525
  var CANONICAL_SUFFIX = {
363
526
  maj: "",
@@ -389,10 +552,20 @@ var CANONICAL_SUFFIX = {
389
552
  "7b13": "7b13",
390
553
  "11": "11",
391
554
  "13": "13",
392
- "5": "5"
555
+ "5": "5",
556
+ "7sus4": "7sus4",
557
+ "7b5": "7b5",
558
+ "7alt": "7alt",
559
+ "13b9": "13b9",
560
+ maj13: "maj13",
561
+ "maj7#11": "maj7#11",
562
+ min11: "m11",
563
+ min13: "m13",
564
+ minAdd9: "madd9",
565
+ "min6/9": "m6/9"
393
566
  };
394
- function pitchClass2(value) {
395
- return (Math.trunc(value) % 12 + 12) % 12;
567
+ function knownQuality(suffix) {
568
+ return Object.hasOwn(QUALITY_MAP, suffix) ? QUALITY_MAP[suffix] : void 0;
396
569
  }
397
570
  function makeSpelledChord(root, quality, bass) {
398
571
  const chord = makeChord(
@@ -410,7 +583,7 @@ function parseChordSymbol(text) {
410
583
  const trimmed = text.trim();
411
584
  const rootMatch = ROOT_RE.exec(trimmed);
412
585
  if (!rootMatch) {
413
- throw new Error(`Invalid chord symbol: ${text}`);
586
+ throw new InvalidInputError(`Invalid chord symbol: ${text}`);
414
587
  }
415
588
  const rootToken = rootMatch[0];
416
589
  const rootNote = parseNote(rootToken);
@@ -419,39 +592,48 @@ function parseChordSymbol(text) {
419
592
  if (slashIndex !== -1) {
420
593
  const before = rest.slice(0, slashIndex);
421
594
  const after = rest.slice(slashIndex + 1);
422
- const beforeQuality = QUALITY_MAP[before];
595
+ const beforeQuality = knownQuality(before);
423
596
  if (beforeQuality !== void 0 && BASS_RE.test(after)) {
424
597
  return makeSpelledChord(rootNote, beforeQuality, parseNote(after));
425
598
  }
426
599
  }
427
- const quality = QUALITY_MAP[rest];
600
+ const quality = knownQuality(rest);
428
601
  if (quality === void 0) {
429
- throw new Error(`Unrecognized chord quality: ${text}`);
602
+ throw new InvalidInputError(`Unrecognized chord quality: ${text}`);
430
603
  }
431
604
  return makeSpelledChord(rootNote, quality);
432
605
  }
433
- function pitchClassName(pc, hint, flats) {
606
+ function pitchClassName(pc, hint, flats, inheritFlats) {
434
607
  if (flats === void 0 && hint !== void 0 && noteToPitchClass(hint) === pc) {
435
608
  return formatNote(hint);
436
609
  }
437
- const note = midiToNote(60 + pc, flats ? "flat" : "sharp");
610
+ const note = midiToNote(60 + pc, flats ?? inheritFlats ? "flat" : "sharp");
438
611
  return formatNote({ letter: note.letter, alter: note.alter });
439
612
  }
440
613
  function formatChordSymbol(chord, opts) {
441
- const rootPc = pitchClass2(chord.rootPc);
442
- const rootName = pitchClassName(rootPc, chord.rootSpelling, opts?.flats);
614
+ assertFiniteNumber(chord.rootPc, "chord rootPc");
615
+ if (chord.bassPc !== void 0) {
616
+ assertFiniteNumber(chord.bassPc, "chord bassPc");
617
+ }
618
+ if (!Object.hasOwn(CANONICAL_SUFFIX, chord.quality)) {
619
+ throw new InvalidInputError(`Unknown chord quality: ${String(chord.quality)}`);
620
+ }
621
+ const rootPc = pitchClassOf(chord.rootPc);
622
+ const rootHint = chord.rootSpelling;
623
+ const rootName = pitchClassName(rootPc, rootHint, opts?.flats);
443
624
  const suffix = CANONICAL_SUFFIX[chord.quality];
444
625
  let symbol = `${rootName}${suffix}`;
445
- if (chord.bassPc !== void 0 && pitchClass2(chord.bassPc) !== rootPc) {
446
- symbol += `/${pitchClassName(pitchClass2(chord.bassPc), chord.bassSpelling, opts?.flats)}`;
626
+ if (chord.bassPc !== void 0 && pitchClassOf(chord.bassPc) !== rootPc) {
627
+ const inheritFlats = rootHint !== void 0 && noteToPitchClass(rootHint) === rootPc ? rootHint.alter < 0 : void 0;
628
+ symbol += `/${pitchClassName(pitchClassOf(chord.bassPc), chord.bassSpelling, opts?.flats, inheritFlats)}`;
447
629
  }
448
630
  return symbol;
449
631
  }
450
632
  function transposeChordSymbol(text, semitones, opts) {
451
633
  const chord = parseChordSymbol(text);
452
- const transposed = { ...chord, rootPc: pitchClass2(chord.rootPc + semitones) };
634
+ const transposed = { ...chord, rootPc: pitchClassOf(chord.rootPc + semitones) };
453
635
  if (chord.bassPc !== void 0) {
454
- transposed.bassPc = pitchClass2(chord.bassPc + semitones);
636
+ transposed.bassPc = pitchClassOf(chord.bassPc + semitones);
455
637
  }
456
638
  return formatChordSymbol(transposed, opts);
457
639
  }
@@ -460,15 +642,16 @@ function transposeChordSymbol(text, semitones, opts) {
460
642
  var DEFAULT_MAX_SPACING = 12;
461
643
  var VIOLATION_PENALTY = 1e3;
462
644
  var MISSING_TONE_PENALTY = 500;
645
+ var MISSING_FIFTH_PENALTY = 20;
463
646
  var POOR_DOUBLING_PENALTY = 4;
647
+ var LEADING_TONE_DOUBLING_PENALTY = 200;
648
+ var RESOLUTION_PENALTY = 40;
464
649
  var MAX_CANDIDATES = 4e3;
465
- function pitchClass3(value) {
466
- return (Math.trunc(value) % 12 + 12) % 12;
467
- }
650
+ var MAX_SEARCH_NODES = MAX_CANDIDATES * 16;
468
651
  function pitchesForPc(pc, range) {
469
652
  const result = [];
470
653
  for (let midi = Math.ceil(range.min); midi <= range.max; midi += 1) {
471
- if (pitchClass3(midi) === pc) {
654
+ if (pitchClassOf(midi) === pc) {
472
655
  result.push(midi);
473
656
  }
474
657
  }
@@ -481,65 +664,107 @@ function pitchesForPc(pc, range) {
481
664
  }
482
665
  function enumerateVoicings(chord, ranges, maxSpacing) {
483
666
  const chordPcs = chordPitchClasses(chord);
484
- const bassPc = pitchClass3(chord.bassPc ?? chord.rootPc);
667
+ const bassPc = pitchClassOf(chord.bassPc ?? chord.rootPc);
668
+ const byVoice = ranges.map(
669
+ (range, voice) => (voice === 0 ? [bassPc] : chordPcs).flatMap((pc) => pitchesForPc(pc, range))
670
+ );
671
+ for (let voice = 0; voice < byVoice.length; voice += 1) {
672
+ if ((byVoice[voice] ?? []).length === 0) {
673
+ throw new NoSolutionError("no voicing satisfies the given ranges");
674
+ }
675
+ }
485
676
  const results = [];
486
677
  const current = [];
678
+ let nodes = 0;
487
679
  const build = (voice) => {
488
- if (results.length >= MAX_CANDIDATES) {
680
+ nodes += 1;
681
+ if (results.length >= MAX_CANDIDATES || nodes > MAX_SEARCH_NODES) {
489
682
  return;
490
683
  }
491
684
  if (voice === ranges.length) {
492
685
  results.push([...current]);
493
686
  return;
494
687
  }
495
- const range = ranges[voice];
496
- if (range === void 0) {
497
- return;
498
- }
499
- const pcs = voice === 0 ? [bassPc] : chordPcs;
500
688
  const prev = current[voice - 1];
501
- for (const pc of pcs) {
502
- for (const pitch of pitchesForPc(pc, range)) {
503
- if (prev !== void 0) {
504
- if (pitch < prev) {
505
- continue;
506
- }
507
- const spacingLimit = voice === 1 ? maxSpacing + 12 : maxSpacing;
508
- if (pitch - prev > spacingLimit) {
509
- continue;
510
- }
689
+ for (const pitch of byVoice[voice] ?? []) {
690
+ if (prev !== void 0) {
691
+ if (pitch < prev) {
692
+ continue;
693
+ }
694
+ const spacingLimit = voice === 1 ? maxSpacing + 12 : maxSpacing;
695
+ if (pitch - prev > spacingLimit) {
696
+ continue;
511
697
  }
512
- current.push(pitch);
513
- build(voice + 1);
514
- current.pop();
515
698
  }
699
+ current.push(pitch);
700
+ build(voice + 1);
701
+ current.pop();
516
702
  }
517
703
  };
518
704
  build(0);
519
705
  return results;
520
706
  }
521
- function structuralPenalty(pitches, chord) {
707
+ function structuralPenalty(pitches, chord, key) {
522
708
  const counts = /* @__PURE__ */ new Map();
523
709
  for (const pitch of pitches) {
524
- const pc = pitchClass3(pitch);
710
+ const pc = pitchClassOf(pitch);
525
711
  counts.set(pc, (counts.get(pc) ?? 0) + 1);
526
712
  }
527
713
  let penalty = 0;
528
714
  for (const pc of chordPitchClasses(chord)) {
529
715
  if (!counts.has(pc)) {
530
- penalty += MISSING_TONE_PENALTY;
716
+ penalty += chordToneRole(pc, chord) === "fifth" ? MISSING_FIFTH_PENALTY : MISSING_TONE_PENALTY;
531
717
  }
532
718
  }
533
- const rootPc = pitchClass3(chord.rootPc);
534
- const fifthPc = pitchClass3(chord.rootPc + 7);
719
+ const rootPc = pitchClassOf(chord.rootPc);
720
+ const leadingTonePc = key === void 0 ? void 0 : pitchClassOf(key.rootPc - 1);
535
721
  for (const [pc, count] of counts) {
536
- if (count > 1 && pc !== rootPc && pc !== fifthPc) {
722
+ if (count <= 1) {
723
+ continue;
724
+ }
725
+ if (pc === leadingTonePc) {
726
+ penalty += (count - 1) * LEADING_TONE_DOUBLING_PENALTY;
727
+ continue;
728
+ }
729
+ if (pc !== rootPc && chordToneRole(pc, chord) !== "fifth") {
537
730
  penalty += (count - 1) * POOR_DOUBLING_PENALTY;
538
731
  }
539
732
  }
540
733
  return penalty;
541
734
  }
542
- function violationCount(prev, cur, maxSpacing) {
735
+ function seventhPcOf(chord) {
736
+ for (const interval of chord.intervals) {
737
+ const pc = pitchClassOf(chord.rootPc + interval);
738
+ if (chordToneRole(pc, chord) === "seventh") {
739
+ return pc;
740
+ }
741
+ }
742
+ return void 0;
743
+ }
744
+ function resolutionViolations(prev, cur, prevChord, nextChord, key) {
745
+ const seventhPc = seventhPcOf(prevChord);
746
+ const leadingTonePc = key === void 0 ? void 0 : pitchClassOf(key.rootPc - 1);
747
+ const tonicPc = key === void 0 ? void 0 : pitchClassOf(key.rootPc);
748
+ const nextPcs = new Set(chordPitchClasses(nextChord));
749
+ let count = 0;
750
+ for (let voice = 0; voice < prev.length && voice < cur.length; voice += 1) {
751
+ const from = prev[voice];
752
+ const to = cur[voice];
753
+ if (from === void 0 || to === void 0) {
754
+ continue;
755
+ }
756
+ const fromPc = pitchClassOf(from);
757
+ const motion = to - from;
758
+ if (fromPc === seventhPc && !nextPcs.has(fromPc) && motion !== -1 && motion !== -2) {
759
+ count += 1;
760
+ }
761
+ if (fromPc === leadingTonePc && tonicPc !== void 0 && nextPcs.has(tonicPc) && !nextPcs.has(fromPc) && motion !== 1) {
762
+ count += 1;
763
+ }
764
+ }
765
+ return count;
766
+ }
767
+ function violationCount(prev, cur) {
543
768
  let count = 0;
544
769
  for (let lower = 0; lower < cur.length; lower += 1) {
545
770
  for (let upper = lower + 1; upper < cur.length; upper += 1) {
@@ -553,16 +778,10 @@ function violationCount(prev, cur, maxSpacing) {
553
778
  if (createsParallelPerfect(prevUpper, curUpper, prevLower, curLower)) {
554
779
  count += 1;
555
780
  }
556
- if (createsVoiceCrossing(curUpper, curLower)) {
557
- count += 1;
558
- }
559
781
  if (upper === lower + 1) {
560
782
  if (createsVoiceOverlap(prevUpper, curUpper, prevLower, curLower)) {
561
783
  count += 1;
562
784
  }
563
- if (lower > 0 && exceedsSpacing(curUpper, curLower, maxSpacing)) {
564
- count += 1;
565
- }
566
785
  }
567
786
  }
568
787
  }
@@ -596,48 +815,68 @@ function voiceLeadingCost(from, to) {
596
815
  return total;
597
816
  }
598
817
  function nextVoicing(current, chord, opts) {
599
- const ranges = opts?.ranges !== void 0 || opts?.voices !== void 0 ? resolveRanges(opts) : current.map((pitch) => {
818
+ const derived = opts?.ranges === void 0 && opts?.voices === void 0;
819
+ if (derived) {
820
+ if (current.length === 0) {
821
+ throw new InvalidInputError("current must contain at least one pitch");
822
+ }
823
+ for (let index = 0; index < current.length; index += 1) {
824
+ assertFiniteNumber(current[index] ?? Number.NaN, `current[${index}]`);
825
+ }
826
+ }
827
+ const ranges = derived ? current.map((pitch) => {
600
828
  const centre = Math.min(127, Math.max(0, pitch));
601
829
  return { min: Math.max(0, centre - 12), max: Math.min(127, centre + 12) };
602
- });
603
- const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
830
+ }) : resolveRanges(opts);
831
+ const maxSpacing = resolveMaxSpacing(opts);
604
832
  const candidates = enumerateVoicings(chord, ranges, maxSpacing);
605
833
  let best;
606
834
  let bestScore = Number.POSITIVE_INFINITY;
607
835
  for (const candidate of candidates) {
608
- const score = structuralPenalty(candidate, chord) + voiceLeadingCost(current, candidate) + VIOLATION_PENALTY * violationCount(current, candidate, maxSpacing);
836
+ const score = structuralPenalty(candidate, chord, opts?.key) + voiceLeadingCost(current, candidate) + VIOLATION_PENALTY * violationCount(current, candidate);
609
837
  if (score < bestScore) {
610
838
  bestScore = score;
611
839
  best = candidate;
612
840
  }
613
841
  }
614
842
  if (best === void 0) {
615
- throw new Error("no voicing satisfies the given ranges");
843
+ throw new NoSolutionError("no voicing satisfies the given ranges");
616
844
  }
617
845
  return best;
618
846
  }
619
847
 
620
848
  // src/theory/voicing/satb.ts
621
- var SATB_RANGES = [
622
- { min: 40, max: 60 },
623
- { min: 48, max: 67 },
624
- { min: 55, max: 74 },
625
- { min: 60, max: 79 }
626
- ];
849
+ var SATB_RANGES = Object.freeze([
850
+ Object.freeze({ min: 40, max: 60 }),
851
+ Object.freeze({ min: 48, max: 67 }),
852
+ Object.freeze({ min: 55, max: 74 }),
853
+ Object.freeze({ min: 60, max: 79 })
854
+ ]);
627
855
  var DERIVED_LOW = 40;
628
856
  var DERIVED_HIGH = 79;
629
857
  var DERIVED_SPAN = 19;
630
858
  function resolveRanges(opts) {
631
859
  if (opts?.ranges !== void 0) {
632
860
  if (opts.ranges.length === 0) {
633
- throw new Error("ranges must contain at least one voice range");
861
+ throw new InvalidInputError("ranges must contain at least one voice range");
634
862
  }
635
- return opts.ranges.map((range) => ({ ...range }));
863
+ assertGenerationBudget(opts.ranges.length, "voice ranges", 128);
864
+ return opts.ranges.map((range, index) => {
865
+ assertFiniteNumber(range.min, `ranges[${index}].min`);
866
+ assertFiniteNumber(range.max, `ranges[${index}].max`);
867
+ if (range.min > range.max) {
868
+ throw new InvalidInputError(`ranges[${index}].min must not exceed max`);
869
+ }
870
+ assertGenerationBudget(
871
+ Math.floor(range.max) - Math.ceil(range.min) + 1,
872
+ `ranges[${index}] span`,
873
+ 4096
874
+ );
875
+ return { ...range };
876
+ });
636
877
  }
637
878
  const voices = opts?.voices ?? 4;
638
- if (voices < 1) {
639
- throw new Error("voices must be at least 1");
640
- }
879
+ assertPositiveInt(voices, "voices", 128);
641
880
  if (voices === 4) {
642
881
  return SATB_RANGES.map((range) => ({ ...range }));
643
882
  }
@@ -653,14 +892,22 @@ function resolveRanges(opts) {
653
892
  }
654
893
  return ranges;
655
894
  }
895
+ function resolveMaxSpacing(opts) {
896
+ const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
897
+ assertFiniteNumber(maxSpacing, "maxSpacing");
898
+ if (maxSpacing < 0) {
899
+ throw new InvalidInputError("maxSpacing must be non-negative");
900
+ }
901
+ return maxSpacing;
902
+ }
656
903
  function voiceChord(chord, opts) {
657
904
  const ranges = resolveRanges(opts);
658
- const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
905
+ const maxSpacing = resolveMaxSpacing(opts);
659
906
  const candidates = enumerateVoicings(chord, ranges, maxSpacing);
660
907
  let best;
661
908
  let bestScore = Number.POSITIVE_INFINITY;
662
909
  for (const candidate of candidates) {
663
- let score = structuralPenalty(candidate, chord);
910
+ let score = structuralPenalty(candidate, chord, opts?.key);
664
911
  for (let i = 0; i < candidate.length; i += 1) {
665
912
  const pitch = candidate[i];
666
913
  const range = ranges[i];
@@ -675,36 +922,63 @@ function voiceChord(chord, opts) {
675
922
  }
676
923
  }
677
924
  if (best === void 0) {
678
- throw new Error("no voicing satisfies the given ranges");
925
+ throw new NoSolutionError(
926
+ `no voicing satisfies the given ranges for ${formatChordSymbol(chord)}`
927
+ );
679
928
  }
680
929
  return best;
681
930
  }
931
+ function noSolutionAt(index, chord) {
932
+ return new NoSolutionError(
933
+ `no voicing satisfies the given ranges for ${formatChordSymbol(chord)} at index ${index}`,
934
+ { at: index }
935
+ );
936
+ }
937
+ function locate(index, chord, work) {
938
+ try {
939
+ return work();
940
+ } catch (error) {
941
+ if (error instanceof NoSolutionError) {
942
+ throw noSolutionAt(index, chord);
943
+ }
944
+ throw error;
945
+ }
946
+ }
682
947
  function voiceProgression(chords, opts) {
948
+ assertGenerationBudget(chords.length, "voiced progression chords", opts?.budget);
683
949
  const ranges = resolveRanges(opts);
684
- const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
950
+ const maxSpacing = resolveMaxSpacing(opts);
685
951
  const result = [];
686
952
  let prev;
687
- for (const chord of chords) {
953
+ let prevChord;
954
+ for (let index = 0; index < chords.length; index += 1) {
955
+ const chord = chords[index];
956
+ if (chord === void 0) {
957
+ continue;
958
+ }
688
959
  if (prev === void 0) {
689
- prev = voiceChord(chord, opts);
960
+ prev = locate(index, chord, () => voiceChord(chord, opts));
961
+ prevChord = chord;
690
962
  result.push(prev);
691
963
  continue;
692
964
  }
693
- const candidates = enumerateVoicings(chord, ranges, maxSpacing);
965
+ const candidates = locate(index, chord, () => enumerateVoicings(chord, ranges, maxSpacing));
694
966
  let best;
695
967
  let bestScore = Number.POSITIVE_INFINITY;
696
968
  for (const candidate of candidates) {
697
- const score = structuralPenalty(candidate, chord) + voiceLeadingCost(prev, candidate) + VIOLATION_PENALTY * violationCount(prev, candidate, maxSpacing);
969
+ const unresolved = prevChord === void 0 ? 0 : resolutionViolations(prev, candidate, prevChord, chord, opts?.key);
970
+ const score = structuralPenalty(candidate, chord, opts?.key) + voiceLeadingCost(prev, candidate) + VIOLATION_PENALTY * violationCount(prev, candidate) + RESOLUTION_PENALTY * unresolved;
698
971
  if (score < bestScore) {
699
972
  bestScore = score;
700
973
  best = candidate;
701
974
  }
702
975
  }
703
976
  if (best === void 0) {
704
- throw new Error("no voicing satisfies the given ranges");
977
+ throw noSolutionAt(index, chord);
705
978
  }
706
979
  result.push(best);
707
980
  prev = best;
981
+ prevChord = chord;
708
982
  }
709
983
  return result;
710
984
  }
@@ -723,7 +997,7 @@ function dedupePcs(pcs) {
723
997
  return result;
724
998
  }
725
999
  function lowestPitchAtOrAbove(pc, floor) {
726
- return floor + ((pc - pitchClass3(floor)) % 12 + 12) % 12;
1000
+ return floor + ((pc - pitchClassOf(floor)) % 12 + 12) % 12;
727
1001
  }
728
1002
  function nearestPc(target, pcs) {
729
1003
  let best = pcs[0] ?? target;
@@ -739,11 +1013,16 @@ function nearestPc(target, pcs) {
739
1013
  }
740
1014
  function voiceChordStyled(chord, opts) {
741
1015
  const style = opts?.style ?? "close";
742
- const base = 12 * ((opts?.octave ?? DEFAULT_STYLE_OCTAVE) + 1);
743
- const rootPc = pitchClass3(chord.rootPc);
744
- const bassPc = pitchClass3(chord.bassPc ?? chord.rootPc);
1016
+ const octave = opts?.octave ?? DEFAULT_STYLE_OCTAVE;
1017
+ assertInteger(octave, "styled voicing octave", -10, 10);
1018
+ if (opts?.topNote !== void 0) {
1019
+ assertFiniteNumber(opts.topNote, "styled voicing topNote");
1020
+ }
1021
+ const base = 12 * (octave + 1);
1022
+ const rootPc = pitchClassOf(chord.rootPc);
1023
+ const bassPc = pitchClassOf(chord.bassPc ?? chord.rootPc);
745
1024
  const omitRoot = style === "rootless" || opts?.rootless === true;
746
- let order = dedupePcs(chord.intervals.map((interval) => pitchClass3(chord.rootPc + interval)));
1025
+ let order = dedupePcs(chord.intervals.map((interval) => pitchClassOf(chord.rootPc + interval)));
747
1026
  const bassIndex = order.indexOf(bassPc);
748
1027
  if (bassIndex > 0) {
749
1028
  order = [...order.slice(bassIndex), ...order.slice(0, bassIndex)];
@@ -755,17 +1034,17 @@ function voiceChordStyled(chord, opts) {
755
1034
  const guide = roles.includes("seventh") ? "seventh" : roles.includes("sixth") ? "sixth" : "fifth";
756
1035
  order = order.filter((_pc, i) => {
757
1036
  const role = roles[i];
758
- return role === "root" || role === "third" || role === guide;
1037
+ return _pc === bassPc || role === "root" || role === "third" || role === guide;
759
1038
  });
760
1039
  }
761
1040
  if (omitRoot) {
762
- order = order.filter((pc) => pc !== rootPc);
1041
+ order = order.filter((pc) => pc !== rootPc || chord.bassPc !== void 0 && pc === bassPc);
763
1042
  }
764
1043
  if (order.length === 0) {
765
1044
  return [];
766
1045
  }
767
1046
  if (opts?.topNote !== void 0) {
768
- const target = order.includes(pitchClass3(opts.topNote)) ? pitchClass3(opts.topNote) : nearestPc(pitchClass3(opts.topNote), order);
1047
+ const target = order.includes(pitchClassOf(opts.topNote)) ? pitchClassOf(opts.topNote) : nearestPc(pitchClassOf(opts.topNote), order);
769
1048
  const targetIndex = order.indexOf(target);
770
1049
  order = [...order.slice(targetIndex + 1), ...order.slice(0, targetIndex + 1)];
771
1050
  }
@@ -775,7 +1054,7 @@ function voiceChordStyled(chord, opts) {
775
1054
  if (prev === void 0) {
776
1055
  prev = lowestPitchAtOrAbove(pc, base);
777
1056
  } else {
778
- let delta = ((pc - pitchClass3(prev)) % 12 + 12) % 12;
1057
+ let delta = ((pc - pitchClassOf(prev)) % 12 + 12) % 12;
779
1058
  if (delta === 0) {
780
1059
  delta = 12;
781
1060
  }
@@ -788,7 +1067,27 @@ function voiceChordStyled(chord, opts) {
788
1067
  } else if (style === "drop3" && stack.length >= 3) {
789
1068
  stack[stack.length - 3] = (stack[stack.length - 3] ?? 0) - 12;
790
1069
  }
791
- return stack.sort((a, b) => a - b);
1070
+ if (chord.bassPc !== void 0) {
1071
+ const bassIndexInStack = stack.findIndex((pitch) => pitchClassOf(pitch) === bassPc);
1072
+ if (bassIndexInStack >= 0) {
1073
+ const otherLowest = Math.min(
1074
+ ...stack.filter((_pitch, index) => index !== bassIndexInStack),
1075
+ Number.POSITIVE_INFINITY
1076
+ );
1077
+ while ((stack[bassIndexInStack] ?? 0) >= otherLowest) {
1078
+ stack[bassIndexInStack] = (stack[bassIndexInStack] ?? 0) - 12;
1079
+ }
1080
+ }
1081
+ }
1082
+ const voicing = stack.sort((a, b) => a - b);
1083
+ const lowest = voicing[0] ?? 0;
1084
+ const highest = voicing[voicing.length - 1] ?? 0;
1085
+ if (lowest < 0 || highest > 127) {
1086
+ throw new InvalidInputError(
1087
+ `styled voicing at octave ${octave} spans MIDI ${lowest}..${highest}, outside 0..127`
1088
+ );
1089
+ }
1090
+ return voicing;
792
1091
  }
793
1092
 
794
1093
  export {
@@ -801,7 +1100,9 @@ export {
801
1100
  spellPitchClass,
802
1101
  spellScale,
803
1102
  spellPitchClasses,
1103
+ spellChordFromRoot,
804
1104
  spellChord,
1105
+ spellPitch,
805
1106
  noteNames,
806
1107
  parseChordSymbol,
807
1108
  formatChordSymbol,
@@ -813,4 +1114,4 @@ export {
813
1114
  nextVoicing,
814
1115
  voiceChordStyled
815
1116
  };
816
- //# sourceMappingURL=chunk-XTC3X6MS.js.map
1117
+ //# sourceMappingURL=chunk-RK35RIN7.js.map