@libraz/libcantus 0.9.3 → 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 (198) hide show
  1. package/README.md +2 -2
  2. package/dist/analyze/index.cjs +39 -1947
  3. package/dist/analyze/index.cjs.map +1 -1
  4. package/dist/analyze/index.d.cts +197 -37
  5. package/dist/analyze/index.d.ts +197 -37
  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-4MHA432G.js → chunk-7NKDPF74.js} +403 -180
  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-MIWO425M.js → chunk-D2FAJBYL.js} +101 -34
  18. package/dist/chunk-D2FAJBYL.js.map +1 -0
  19. package/dist/{chunk-SGU7SPLK.js → chunk-EHVEBWM5.js} +1056 -531
  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-IT2O4XFE.js → chunk-GQSGS2N4.js} +43 -24
  26. package/dist/chunk-GQSGS2N4.js.map +1 -0
  27. package/dist/{chunk-62QZKCJQ.js → chunk-HKGYCQ34.js} +10 -6
  28. package/dist/chunk-HKGYCQ34.js.map +1 -0
  29. package/dist/{chunk-AKLYPWIN.js → chunk-I24MV65U.js} +29 -6
  30. package/dist/chunk-I24MV65U.js.map +1 -0
  31. package/dist/{chunk-CHMXELDS.js → chunk-KMHOICIY.js} +311 -181
  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-2ZWMSJH6.js → chunk-NKIBQ6PT.js} +2 -2
  36. package/dist/chunk-NKIBQ6PT.js.map +1 -0
  37. package/dist/chunk-NP4NA2FB.cjs +106 -0
  38. package/dist/chunk-NP4NA2FB.cjs.map +1 -0
  39. package/dist/chunk-NVE34YGZ.cjs +1449 -0
  40. package/dist/chunk-NVE34YGZ.cjs.map +1 -0
  41. package/dist/{chunk-EH6EM37U.js → chunk-NYYWMV2C.js} +61 -49
  42. package/dist/chunk-NYYWMV2C.js.map +1 -0
  43. package/dist/{chunk-GOAW4VYL.js → chunk-Q2XUP227.js} +243 -117
  44. package/dist/chunk-Q2XUP227.js.map +1 -0
  45. package/dist/chunk-Q6OP4U43.cjs +327 -0
  46. package/dist/chunk-Q6OP4U43.cjs.map +1 -0
  47. package/dist/chunk-RC6GBMCD.cjs +845 -0
  48. package/dist/chunk-RC6GBMCD.cjs.map +1 -0
  49. package/dist/{chunk-C5XYKUVG.js → chunk-RK35RIN7.js} +370 -157
  50. package/dist/chunk-RK35RIN7.js.map +1 -0
  51. package/dist/chunk-RUMU3UA6.cjs +53 -0
  52. package/dist/chunk-RUMU3UA6.cjs.map +1 -0
  53. package/dist/chunk-T64C7ZD7.cjs +1 -0
  54. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  55. package/dist/chunk-XEBG3G53.js +498 -0
  56. package/dist/chunk-XEBG3G53.js.map +1 -0
  57. package/dist/{chunk-I5R7ZBJS.js → chunk-YLEMGEFH.js} +103 -59
  58. package/dist/chunk-YLEMGEFH.js.map +1 -0
  59. package/dist/chunk-ZEJR4TKL.cjs +173 -0
  60. package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
  61. package/dist/core/index.cjs +130 -647
  62. package/dist/core/index.cjs.map +1 -1
  63. package/dist/core/index.d.cts +301 -39
  64. package/dist/core/index.d.ts +301 -39
  65. package/dist/core/index.js +54 -16
  66. package/dist/{function-DYt4iKsn.d.cts → function-BtiZb9i9.d.ts} +28 -7
  67. package/dist/{function-DVH4Eusr.d.ts → function-DNugAOrt.d.cts} +28 -7
  68. package/dist/generate/index.cjs +28 -4085
  69. package/dist/generate/index.cjs.map +1 -1
  70. package/dist/generate/index.d.cts +370 -62
  71. package/dist/generate/index.d.ts +370 -62
  72. package/dist/generate/index.js +23 -9
  73. package/dist/{index-CxqBQtH1.d.cts → index-6VR8-kit.d.ts} +19 -4
  74. package/dist/index-70p168VM.d.ts +192 -0
  75. package/dist/{index-CpOsP1y9.d.cts → index-Be74mcon.d.cts} +18 -17
  76. package/dist/index-BtXT6UQ0.d.cts +192 -0
  77. package/dist/{index-DxvARIP2.d.ts → index-C5UBbQ4G.d.ts} +18 -17
  78. package/dist/{index-Bt4tOUD9.d.ts → index-C83mZI1P.d.ts} +93 -8
  79. package/dist/{index-Bg5dYMmZ.d.ts → index-D6b6na69.d.cts} +14 -11
  80. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  81. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  82. package/dist/index-OG65ocOr.d.cts +272 -0
  83. package/dist/index-OG65ocOr.d.ts +272 -0
  84. package/dist/{index-Jc0Ryi79.d.ts → index-tJhkmQgE.d.cts} +19 -4
  85. package/dist/{index-Dikst44v.d.cts → index-tSQh-xwY.d.ts} +14 -11
  86. package/dist/{index-BNlGP86A.d.cts → index-y_ak9_Hi.d.cts} +93 -8
  87. package/dist/index.cjs +419 -7383
  88. package/dist/index.cjs.map +1 -1
  89. package/dist/index.d.cts +14 -14
  90. package/dist/index.d.ts +14 -14
  91. package/dist/index.js +111 -27
  92. package/dist/model/index.cjs +8 -2562
  93. package/dist/model/index.cjs.map +1 -1
  94. package/dist/model/index.d.cts +433 -75
  95. package/dist/model/index.d.ts +433 -75
  96. package/dist/model/index.js +6 -7
  97. package/dist/styled-DIRkk9Uz.d.ts +506 -0
  98. package/dist/styled-DJbkJQfj.d.cts +506 -0
  99. package/dist/theory/index.cjs +172 -1822
  100. package/dist/theory/index.cjs.map +1 -1
  101. package/dist/theory/index.d.cts +151 -233
  102. package/dist/theory/index.d.ts +151 -233
  103. package/dist/theory/index.js +30 -7
  104. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  105. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  106. package/package.json +24 -6
  107. package/dist/chunk-2ZWMSJH6.js.map +0 -1
  108. package/dist/chunk-4MHA432G.js.map +0 -1
  109. package/dist/chunk-62QZKCJQ.js.map +0 -1
  110. package/dist/chunk-AKLYPWIN.js.map +0 -1
  111. package/dist/chunk-C5XYKUVG.js.map +0 -1
  112. package/dist/chunk-CHMXELDS.js.map +0 -1
  113. package/dist/chunk-DYWFNC4A.js +0 -152
  114. package/dist/chunk-DYWFNC4A.js.map +0 -1
  115. package/dist/chunk-EH6EM37U.js.map +0 -1
  116. package/dist/chunk-FBQK3ZQN.js +0 -131
  117. package/dist/chunk-FBQK3ZQN.js.map +0 -1
  118. package/dist/chunk-GOAW4VYL.js.map +0 -1
  119. package/dist/chunk-I5R7ZBJS.js.map +0 -1
  120. package/dist/chunk-IT2O4XFE.js.map +0 -1
  121. package/dist/chunk-MIWO425M.js.map +0 -1
  122. package/dist/chunk-SGU7SPLK.js.map +0 -1
  123. package/dist/index-CGMZNCx6.d.cts +0 -140
  124. package/dist/index-CGMZNCx6.d.ts +0 -140
  125. package/dist/index-CnZCo29u.d.cts +0 -108
  126. package/dist/index-JnF4v9bx.d.ts +0 -108
  127. package/dist/styled-DZ7GjShb.d.ts +0 -282
  128. package/dist/styled-ZTbdaVFM.d.cts +0 -282
  129. package/src/analyze/arrange/index.ts +0 -21
  130. package/src/analyze/arrange/internal.ts +0 -127
  131. package/src/analyze/arrange/tension.ts +0 -199
  132. package/src/analyze/arrange/tracks.ts +0 -312
  133. package/src/analyze/detect/index.ts +0 -272
  134. package/src/analyze/functional/borrowed.ts +0 -84
  135. package/src/analyze/functional/cadence.ts +0 -66
  136. package/src/analyze/functional/function.ts +0 -184
  137. package/src/analyze/functional/index.ts +0 -23
  138. package/src/analyze/functional/internal.ts +0 -29
  139. package/src/analyze/functional/roman.ts +0 -402
  140. package/src/analyze/index.ts +0 -49
  141. package/src/analyze/timeline/index.ts +0 -451
  142. package/src/analyze/voice/index.ts +0 -339
  143. package/src/core/event-index/index.ts +0 -104
  144. package/src/core/index.ts +0 -61
  145. package/src/core/interval/index.ts +0 -81
  146. package/src/core/meter/index.ts +0 -302
  147. package/src/core/pitch/index.ts +0 -311
  148. package/src/core/random/index.ts +0 -76
  149. package/src/core/tuning/index.ts +0 -186
  150. package/src/core/types.ts +0 -29
  151. package/src/core/validation/index.ts +0 -119
  152. package/src/generate/bass/index.ts +0 -373
  153. package/src/generate/countermelody/index.ts +0 -470
  154. package/src/generate/drums/beat.ts +0 -266
  155. package/src/generate/drums/euclid.ts +0 -65
  156. package/src/generate/drums/fills.ts +0 -356
  157. package/src/generate/drums/ghost.ts +0 -119
  158. package/src/generate/drums/hihat.ts +0 -285
  159. package/src/generate/drums/hit.ts +0 -31
  160. package/src/generate/drums/index.ts +0 -361
  161. package/src/generate/drums/internal.ts +0 -257
  162. package/src/generate/drums/kick.ts +0 -160
  163. package/src/generate/drums/percussion.ts +0 -121
  164. package/src/generate/drums/rng.ts +0 -7
  165. package/src/generate/drums/swing.ts +0 -71
  166. package/src/generate/groove/index.ts +0 -351
  167. package/src/generate/harmonize/index.ts +0 -451
  168. package/src/generate/index.ts +0 -58
  169. package/src/generate/motif/index.ts +0 -422
  170. package/src/generate/progression/index.ts +0 -362
  171. package/src/generate/reharmony/index.ts +0 -323
  172. package/src/generate/rhythm/index.ts +0 -175
  173. package/src/index.ts +0 -13
  174. package/src/model/chord.ts +0 -442
  175. package/src/model/index.ts +0 -15
  176. package/src/model/interval.ts +0 -82
  177. package/src/model/key.ts +0 -266
  178. package/src/model/note.ts +0 -176
  179. package/src/model/progression.ts +0 -161
  180. package/src/model/shared.ts +0 -12
  181. package/src/theory/chord/index.ts +0 -375
  182. package/src/theory/chordscale/index.ts +0 -374
  183. package/src/theory/counterpoint/index.ts +0 -230
  184. package/src/theory/harmony/index.ts +0 -77
  185. package/src/theory/index.ts +0 -112
  186. package/src/theory/safety/index.ts +0 -488
  187. package/src/theory/scale/degrees.ts +0 -123
  188. package/src/theory/scale/index.ts +0 -28
  189. package/src/theory/scale/key.ts +0 -56
  190. package/src/theory/scale/masks.ts +0 -153
  191. package/src/theory/spelling/index.ts +0 -249
  192. package/src/theory/symbol/index.ts +0 -268
  193. package/src/theory/voicing/index.ts +0 -5
  194. package/src/theory/voicing/internal.ts +0 -168
  195. package/src/theory/voicing/leading.ts +0 -112
  196. package/src/theory/voicing/satb.ts +0 -216
  197. package/src/theory/voicing/styled.ts +0 -199
  198. package/tsconfig.json +0 -23
@@ -1,32 +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-MIWO425M.js";
12
+ } from "./chunk-D2FAJBYL.js";
19
13
  import {
14
+ InvalidInputError,
15
+ NoSolutionError,
20
16
  assertFiniteNumber,
21
17
  assertGenerationBudget,
22
18
  assertInteger,
23
- assertPositiveInt
24
- } from "./chunk-FBQK3ZQN.js";
19
+ assertPositiveInt,
20
+ diatonicLetterOf,
21
+ formatNote,
22
+ midiToNote,
23
+ naturalPitchClassOf,
24
+ noteToPitchClass,
25
+ parseNote,
26
+ pitchClassOf
27
+ } from "./chunk-XEBG3G53.js";
25
28
 
26
29
  // src/theory/chordscale/index.ts
27
- function pitchClass(value) {
28
- return (Math.trunc(value) % 12 + 12) % 12;
29
- }
30
30
  function popcount12(mask) {
31
31
  let count = 0;
32
32
  for (let n = 0; n < 12; n += 1) {
@@ -35,7 +35,7 @@ function popcount12(mask) {
35
35
  return count;
36
36
  }
37
37
  function maskHasPitchClass(mask, scaleRootPc, pc) {
38
- const offset = pitchClass(pc - scaleRootPc);
38
+ const offset = pitchClassOf(pc - scaleRootPc);
39
39
  return (mask >> offset & 1) === 1;
40
40
  }
41
41
  function scaleMatchesChord(chordPcs, scaleMask, scaleRootPc) {
@@ -48,16 +48,31 @@ function scaleMatchesChord(chordPcs, scaleMask, scaleRootPc) {
48
48
  }
49
49
  var ALIASED_SCALE_NAMES = /* @__PURE__ */ new Set(["major", "naturalMinor"]);
50
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
+ }
51
66
  function chordScales(chord) {
52
67
  const chordPcs = chordPitchClasses(chord);
53
- const rootPc = pitchClass(chord.rootPc);
68
+ const rootPc = pitchClassOf(chord.rootPc);
54
69
  const ranked = [];
55
70
  const seenMasks = /* @__PURE__ */ new Set();
56
71
  for (const name of Object.keys(NAMED_SCALES)) {
57
72
  if (name === "chromatic" || ALIASED_SCALE_NAMES.has(name)) {
58
73
  continue;
59
74
  }
60
- const mask = NAMED_SCALES[name];
75
+ const mask = namedScaleMask(name);
61
76
  if (mask === void 0 || seenMasks.has(mask)) {
62
77
  continue;
63
78
  }
@@ -84,16 +99,18 @@ function chordScales(chord) {
84
99
  if (a.size !== b.size) {
85
100
  return a.size - b.size;
86
101
  }
102
+ const rankA = conventionalRank(a.name);
103
+ const rankB = conventionalRank(b.name);
104
+ if (rankA !== rankB) {
105
+ return rankA - rankB;
106
+ }
87
107
  return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
88
108
  });
89
109
  return ranked.map((entry) => ({ name: entry.name, rootPc }));
90
110
  }
91
111
  function avoidNotes(chord, scaleName) {
92
- const mask = NAMED_SCALES[scaleName];
93
- if (mask === void 0) {
94
- return [];
95
- }
96
- const rootPc = pitchClass(chord.rootPc);
112
+ const mask = requireScaleMask(scaleName);
113
+ const rootPc = pitchClassOf(chord.rootPc);
97
114
  const chordPcs = chordPitchClasses(chord);
98
115
  if (!scaleMatchesChord(chordPcs, mask, rootPc)) {
99
116
  return [];
@@ -104,18 +121,15 @@ function avoidNotes(chord, scaleName) {
104
121
  if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc)) {
105
122
  continue;
106
123
  }
107
- if (chordSet.has(pitchClass(pc - 1))) {
124
+ if (chordSet.has(pitchClassOf(pc - 1))) {
108
125
  avoid.push(pc);
109
126
  }
110
127
  }
111
128
  return avoid.sort((a, b) => a - b);
112
129
  }
113
130
  function availableTensions(chord, scaleName) {
114
- const mask = NAMED_SCALES[scaleName];
115
- if (mask === void 0) {
116
- return [];
117
- }
118
- const rootPc = pitchClass(chord.rootPc);
131
+ const mask = requireScaleMask(scaleName);
132
+ const rootPc = pitchClassOf(chord.rootPc);
119
133
  const chordPcs = chordPitchClasses(chord);
120
134
  if (!scaleMatchesChord(chordPcs, mask, rootPc)) {
121
135
  return [];
@@ -143,14 +157,14 @@ function chordScaleReport(chord, limit) {
143
157
  }
144
158
  var RANK_PENALTY = 0.01;
145
159
  function scalePitchClassSet(name, rootPc) {
146
- const mask = NAMED_SCALES[name];
160
+ const mask = namedScaleMask(name);
147
161
  const pcs = /* @__PURE__ */ new Set();
148
162
  if (mask === void 0) {
149
163
  return pcs;
150
164
  }
151
165
  for (let n = 0; n < 12; n += 1) {
152
166
  if ((mask >> n & 1) === 1) {
153
- pcs.add(pitchClass(rootPc + n));
167
+ pcs.add(pitchClassOf(rootPc + n));
154
168
  }
155
169
  }
156
170
  return pcs;
@@ -228,64 +242,117 @@ function scalesForChanges(chords) {
228
242
  }
229
243
 
230
244
  // src/theory/spelling/index.ts
231
- var LETTER_SEMITONES = [0, 2, 4, 5, 7, 9, 11];
232
245
  var CHROMATIC_SPELLING = {
233
- 1: { degreeOffset: 1, alter: -1 },
246
+ 1: 1,
234
247
  // b2
235
- 3: { degreeOffset: 2, alter: -1 },
248
+ 2: 1,
249
+ // M2
250
+ 3: 2,
236
251
  // b3
237
- 6: { degreeOffset: 3, alter: 1 },
252
+ 4: 2,
253
+ // M3
254
+ 5: 3,
255
+ // P4
256
+ 6: 3,
238
257
  // #4
239
- 8: { degreeOffset: 5, alter: -1 },
258
+ 7: 4,
259
+ // P5
260
+ 8: 5,
240
261
  // b6
241
- 9: { degreeOffset: 5, alter: 1 },
262
+ 9: 5,
242
263
  // #6 (raised sixth of a minor key)
243
- 10: { degreeOffset: 6, alter: -1 },
264
+ 10: 6,
244
265
  // b7
245
- 11: { degreeOffset: 6, alter: 1 }
266
+ 11: 6
246
267
  // #7 (raised leading tone of a minor key)
247
268
  };
248
- function mod12(n) {
249
- return (n % 12 + 12) % 12;
250
- }
251
- function mod7(n) {
252
- return (n % 7 + 7) % 7;
253
- }
254
- function naturalPc(letter) {
255
- return LETTER_SEMITONES[mod7(letter)] ?? 0;
256
- }
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
+ };
257
298
  function alterFor(letter, pc) {
258
- let d = mod12(pc - naturalPc(letter));
299
+ let d = pitchClassOf(pc - naturalPitchClassOf(letter));
259
300
  if (d > 6) {
260
301
  d -= 12;
261
302
  }
262
303
  return d;
263
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
+ }
264
321
  function isHeptatonic(key) {
265
322
  return scaleTonesInDegreeOrder(key).length === 7;
266
323
  }
267
- function spellPitchClass(pc, tonic, key) {
268
- const tonicPc = mod12(naturalPc(tonic.letter) + tonic.alter);
269
- const offset = mod12(pc - tonicPc);
324
+ function hasMinorThird(key) {
270
325
  const tones = scaleTonesInDegreeOrder(key);
271
- if (isHeptatonic(key)) {
272
- 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));
273
338
  if (degree >= 0) {
274
- const letter = mod7(tonic.letter + degree);
339
+ const letter = diatonicLetterOf(tonic.letter + degree);
275
340
  return { letter, alter: alterFor(letter, pc) };
276
341
  }
277
- const chromatic = CHROMATIC_SPELLING[offset];
278
- if (chromatic) {
279
- 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);
280
345
  return { letter, alter: alterFor(letter, pc) };
281
346
  }
282
- }
283
- for (let letter = 0; letter < 7; letter += 1) {
284
- if (naturalPc(letter) === mod12(pc)) {
285
- 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;
286
353
  }
287
354
  }
288
- 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);
289
356
  return { letter: belowLetter, alter: alterFor(belowLetter, pc) };
290
357
  }
291
358
  function spellScale(tonic, key) {
@@ -306,9 +373,10 @@ function chordLetterOffset(interval, chord) {
306
373
  return 1;
307
374
  case 3:
308
375
  case 4:
309
- case 15:
310
376
  case 16:
311
377
  return 2;
378
+ case 15:
379
+ return chord.intervals.some((i) => i === 3 || i === 4) ? 1 : 2;
312
380
  case 5:
313
381
  case 17:
314
382
  case 18:
@@ -330,7 +398,7 @@ function chordLetterOffset(interval, chord) {
330
398
  return 6;
331
399
  default: {
332
400
  const octaves = Math.floor(Math.max(0, interval) / 12);
333
- const simple = mod12(interval);
401
+ const simple = pitchClassOf(interval);
334
402
  const simpleOffset = [0, 1, 1, 2, 2, 3, 4, 4, 4, 5, 6, 6][simple] ?? 0;
335
403
  return octaves * 7 + simpleOffset;
336
404
  }
@@ -338,14 +406,26 @@ function chordLetterOffset(interval, chord) {
338
406
  }
339
407
  function spellChordFromRoot(chord, root) {
340
408
  return chord.intervals.map((interval) => {
341
- const letter = mod7(root.letter + chordLetterOffset(interval, chord));
342
- const pc = mod12(chord.rootPc + interval);
409
+ const letter = diatonicLetterOf(root.letter + chordLetterOffset(interval, chord));
410
+ const pc = pitchClassOf(chord.rootPc + interval);
343
411
  return { letter, alter: alterFor(letter, pc) };
344
412
  });
345
413
  }
346
414
  function spellChord(chord, tonic, key) {
347
- const root = spellPitchClass(chord.rootPc, tonic, key);
348
- return spellChordFromRoot(chord, root);
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 };
349
429
  }
350
430
  function noteNames(notes) {
351
431
  return notes.map(formatNote);
@@ -411,7 +491,36 @@ var QUALITY_MAP = {
411
491
  "7b13": "7b13",
412
492
  "11": "11",
413
493
  "13": "13",
414
- "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"
415
524
  };
416
525
  var CANONICAL_SUFFIX = {
417
526
  maj: "",
@@ -443,10 +552,20 @@ var CANONICAL_SUFFIX = {
443
552
  "7b13": "7b13",
444
553
  "11": "11",
445
554
  "13": "13",
446
- "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"
447
566
  };
448
- function pitchClass2(value) {
449
- return (Math.trunc(value) % 12 + 12) % 12;
567
+ function knownQuality(suffix) {
568
+ return Object.hasOwn(QUALITY_MAP, suffix) ? QUALITY_MAP[suffix] : void 0;
450
569
  }
451
570
  function makeSpelledChord(root, quality, bass) {
452
571
  const chord = makeChord(
@@ -464,7 +583,7 @@ function parseChordSymbol(text) {
464
583
  const trimmed = text.trim();
465
584
  const rootMatch = ROOT_RE.exec(trimmed);
466
585
  if (!rootMatch) {
467
- throw new Error(`Invalid chord symbol: ${text}`);
586
+ throw new InvalidInputError(`Invalid chord symbol: ${text}`);
468
587
  }
469
588
  const rootToken = rootMatch[0];
470
589
  const rootNote = parseNote(rootToken);
@@ -473,39 +592,48 @@ function parseChordSymbol(text) {
473
592
  if (slashIndex !== -1) {
474
593
  const before = rest.slice(0, slashIndex);
475
594
  const after = rest.slice(slashIndex + 1);
476
- const beforeQuality = QUALITY_MAP[before];
595
+ const beforeQuality = knownQuality(before);
477
596
  if (beforeQuality !== void 0 && BASS_RE.test(after)) {
478
597
  return makeSpelledChord(rootNote, beforeQuality, parseNote(after));
479
598
  }
480
599
  }
481
- const quality = QUALITY_MAP[rest];
600
+ const quality = knownQuality(rest);
482
601
  if (quality === void 0) {
483
- throw new Error(`Unrecognized chord quality: ${text}`);
602
+ throw new InvalidInputError(`Unrecognized chord quality: ${text}`);
484
603
  }
485
604
  return makeSpelledChord(rootNote, quality);
486
605
  }
487
- function pitchClassName(pc, hint, flats) {
606
+ function pitchClassName(pc, hint, flats, inheritFlats) {
488
607
  if (flats === void 0 && hint !== void 0 && noteToPitchClass(hint) === pc) {
489
608
  return formatNote(hint);
490
609
  }
491
- const note = midiToNote(60 + pc, flats ? "flat" : "sharp");
610
+ const note = midiToNote(60 + pc, flats ?? inheritFlats ? "flat" : "sharp");
492
611
  return formatNote({ letter: note.letter, alter: note.alter });
493
612
  }
494
613
  function formatChordSymbol(chord, opts) {
495
- const rootPc = pitchClass2(chord.rootPc);
496
- 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);
497
624
  const suffix = CANONICAL_SUFFIX[chord.quality];
498
625
  let symbol = `${rootName}${suffix}`;
499
- if (chord.bassPc !== void 0 && pitchClass2(chord.bassPc) !== rootPc) {
500
- 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)}`;
501
629
  }
502
630
  return symbol;
503
631
  }
504
632
  function transposeChordSymbol(text, semitones, opts) {
505
633
  const chord = parseChordSymbol(text);
506
- const transposed = { ...chord, rootPc: pitchClass2(chord.rootPc + semitones) };
634
+ const transposed = { ...chord, rootPc: pitchClassOf(chord.rootPc + semitones) };
507
635
  if (chord.bassPc !== void 0) {
508
- transposed.bassPc = pitchClass2(chord.bassPc + semitones);
636
+ transposed.bassPc = pitchClassOf(chord.bassPc + semitones);
509
637
  }
510
638
  return formatChordSymbol(transposed, opts);
511
639
  }
@@ -514,15 +642,16 @@ function transposeChordSymbol(text, semitones, opts) {
514
642
  var DEFAULT_MAX_SPACING = 12;
515
643
  var VIOLATION_PENALTY = 1e3;
516
644
  var MISSING_TONE_PENALTY = 500;
645
+ var MISSING_FIFTH_PENALTY = 20;
517
646
  var POOR_DOUBLING_PENALTY = 4;
647
+ var LEADING_TONE_DOUBLING_PENALTY = 200;
648
+ var RESOLUTION_PENALTY = 40;
518
649
  var MAX_CANDIDATES = 4e3;
519
- function pitchClass3(value) {
520
- return (Math.trunc(value) % 12 + 12) % 12;
521
- }
650
+ var MAX_SEARCH_NODES = MAX_CANDIDATES * 16;
522
651
  function pitchesForPc(pc, range) {
523
652
  const result = [];
524
653
  for (let midi = Math.ceil(range.min); midi <= range.max; midi += 1) {
525
- if (pitchClass3(midi) === pc) {
654
+ if (pitchClassOf(midi) === pc) {
526
655
  result.push(midi);
527
656
  }
528
657
  }
@@ -535,65 +664,107 @@ function pitchesForPc(pc, range) {
535
664
  }
536
665
  function enumerateVoicings(chord, ranges, maxSpacing) {
537
666
  const chordPcs = chordPitchClasses(chord);
538
- 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
+ }
539
676
  const results = [];
540
677
  const current = [];
678
+ let nodes = 0;
541
679
  const build = (voice) => {
542
- if (results.length >= MAX_CANDIDATES) {
680
+ nodes += 1;
681
+ if (results.length >= MAX_CANDIDATES || nodes > MAX_SEARCH_NODES) {
543
682
  return;
544
683
  }
545
684
  if (voice === ranges.length) {
546
685
  results.push([...current]);
547
686
  return;
548
687
  }
549
- const range = ranges[voice];
550
- if (range === void 0) {
551
- return;
552
- }
553
- const pcs = voice === 0 ? [bassPc] : chordPcs;
554
688
  const prev = current[voice - 1];
555
- for (const pc of pcs) {
556
- for (const pitch of pitchesForPc(pc, range)) {
557
- if (prev !== void 0) {
558
- if (pitch < prev) {
559
- continue;
560
- }
561
- const spacingLimit = voice === 1 ? maxSpacing + 12 : maxSpacing;
562
- if (pitch - prev > spacingLimit) {
563
- continue;
564
- }
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;
565
697
  }
566
- current.push(pitch);
567
- build(voice + 1);
568
- current.pop();
569
698
  }
699
+ current.push(pitch);
700
+ build(voice + 1);
701
+ current.pop();
570
702
  }
571
703
  };
572
704
  build(0);
573
705
  return results;
574
706
  }
575
- function structuralPenalty(pitches, chord) {
707
+ function structuralPenalty(pitches, chord, key) {
576
708
  const counts = /* @__PURE__ */ new Map();
577
709
  for (const pitch of pitches) {
578
- const pc = pitchClass3(pitch);
710
+ const pc = pitchClassOf(pitch);
579
711
  counts.set(pc, (counts.get(pc) ?? 0) + 1);
580
712
  }
581
713
  let penalty = 0;
582
714
  for (const pc of chordPitchClasses(chord)) {
583
715
  if (!counts.has(pc)) {
584
- penalty += MISSING_TONE_PENALTY;
716
+ penalty += chordToneRole(pc, chord) === "fifth" ? MISSING_FIFTH_PENALTY : MISSING_TONE_PENALTY;
585
717
  }
586
718
  }
587
- const rootPc = pitchClass3(chord.rootPc);
588
- const fifthPc = pitchClass3(chord.rootPc + 7);
719
+ const rootPc = pitchClassOf(chord.rootPc);
720
+ const leadingTonePc = key === void 0 ? void 0 : pitchClassOf(key.rootPc - 1);
589
721
  for (const [pc, count] of counts) {
590
- 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") {
591
730
  penalty += (count - 1) * POOR_DOUBLING_PENALTY;
592
731
  }
593
732
  }
594
733
  return penalty;
595
734
  }
596
- 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) {
597
768
  let count = 0;
598
769
  for (let lower = 0; lower < cur.length; lower += 1) {
599
770
  for (let upper = lower + 1; upper < cur.length; upper += 1) {
@@ -607,16 +778,10 @@ function violationCount(prev, cur, maxSpacing) {
607
778
  if (createsParallelPerfect(prevUpper, curUpper, prevLower, curLower)) {
608
779
  count += 1;
609
780
  }
610
- if (createsVoiceCrossing(curUpper, curLower)) {
611
- count += 1;
612
- }
613
781
  if (upper === lower + 1) {
614
782
  if (createsVoiceOverlap(prevUpper, curUpper, prevLower, curLower)) {
615
783
  count += 1;
616
784
  }
617
- if (lower > 0 && exceedsSpacing(curUpper, curLower, maxSpacing)) {
618
- count += 1;
619
- }
620
785
  }
621
786
  }
622
787
  }
@@ -650,48 +815,57 @@ function voiceLeadingCost(from, to) {
650
815
  return total;
651
816
  }
652
817
  function nextVoicing(current, chord, opts) {
653
- 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) => {
654
828
  const centre = Math.min(127, Math.max(0, pitch));
655
829
  return { min: Math.max(0, centre - 12), max: Math.min(127, centre + 12) };
656
- });
657
- const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
830
+ }) : resolveRanges(opts);
831
+ const maxSpacing = resolveMaxSpacing(opts);
658
832
  const candidates = enumerateVoicings(chord, ranges, maxSpacing);
659
833
  let best;
660
834
  let bestScore = Number.POSITIVE_INFINITY;
661
835
  for (const candidate of candidates) {
662
- 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);
663
837
  if (score < bestScore) {
664
838
  bestScore = score;
665
839
  best = candidate;
666
840
  }
667
841
  }
668
842
  if (best === void 0) {
669
- throw new Error("no voicing satisfies the given ranges");
843
+ throw new NoSolutionError("no voicing satisfies the given ranges");
670
844
  }
671
845
  return best;
672
846
  }
673
847
 
674
848
  // src/theory/voicing/satb.ts
675
- var SATB_RANGES = [
676
- { min: 40, max: 60 },
677
- { min: 48, max: 67 },
678
- { min: 55, max: 74 },
679
- { min: 60, max: 79 }
680
- ];
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
+ ]);
681
855
  var DERIVED_LOW = 40;
682
856
  var DERIVED_HIGH = 79;
683
857
  var DERIVED_SPAN = 19;
684
858
  function resolveRanges(opts) {
685
859
  if (opts?.ranges !== void 0) {
686
860
  if (opts.ranges.length === 0) {
687
- throw new Error("ranges must contain at least one voice range");
861
+ throw new InvalidInputError("ranges must contain at least one voice range");
688
862
  }
689
863
  assertGenerationBudget(opts.ranges.length, "voice ranges", 128);
690
864
  return opts.ranges.map((range, index) => {
691
865
  assertFiniteNumber(range.min, `ranges[${index}].min`);
692
866
  assertFiniteNumber(range.max, `ranges[${index}].max`);
693
867
  if (range.min > range.max) {
694
- throw new RangeError(`ranges[${index}].min must not exceed max`);
868
+ throw new InvalidInputError(`ranges[${index}].min must not exceed max`);
695
869
  }
696
870
  assertGenerationBudget(
697
871
  Math.floor(range.max) - Math.ceil(range.min) + 1,
@@ -718,16 +892,22 @@ function resolveRanges(opts) {
718
892
  }
719
893
  return ranges;
720
894
  }
721
- function voiceChord(chord, opts) {
722
- const ranges = resolveRanges(opts);
895
+ function resolveMaxSpacing(opts) {
723
896
  const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
724
897
  assertFiniteNumber(maxSpacing, "maxSpacing");
725
- if (maxSpacing < 0) throw new RangeError("maxSpacing must be non-negative");
898
+ if (maxSpacing < 0) {
899
+ throw new InvalidInputError("maxSpacing must be non-negative");
900
+ }
901
+ return maxSpacing;
902
+ }
903
+ function voiceChord(chord, opts) {
904
+ const ranges = resolveRanges(opts);
905
+ const maxSpacing = resolveMaxSpacing(opts);
726
906
  const candidates = enumerateVoicings(chord, ranges, maxSpacing);
727
907
  let best;
728
908
  let bestScore = Number.POSITIVE_INFINITY;
729
909
  for (const candidate of candidates) {
730
- let score = structuralPenalty(candidate, chord);
910
+ let score = structuralPenalty(candidate, chord, opts?.key);
731
911
  for (let i = 0; i < candidate.length; i += 1) {
732
912
  const pitch = candidate[i];
733
913
  const range = ranges[i];
@@ -742,39 +922,63 @@ function voiceChord(chord, opts) {
742
922
  }
743
923
  }
744
924
  if (best === void 0) {
745
- throw new Error("no voicing satisfies the given ranges");
925
+ throw new NoSolutionError(
926
+ `no voicing satisfies the given ranges for ${formatChordSymbol(chord)}`
927
+ );
746
928
  }
747
929
  return best;
748
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
+ }
749
947
  function voiceProgression(chords, opts) {
750
- assertGenerationBudget(chords.length * 4e3, "voiced progression search");
948
+ assertGenerationBudget(chords.length, "voiced progression chords", opts?.budget);
751
949
  const ranges = resolveRanges(opts);
752
- const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
753
- assertFiniteNumber(maxSpacing, "maxSpacing");
754
- if (maxSpacing < 0) throw new RangeError("maxSpacing must be non-negative");
950
+ const maxSpacing = resolveMaxSpacing(opts);
755
951
  const result = [];
756
952
  let prev;
757
- 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
+ }
758
959
  if (prev === void 0) {
759
- prev = voiceChord(chord, opts);
960
+ prev = locate(index, chord, () => voiceChord(chord, opts));
961
+ prevChord = chord;
760
962
  result.push(prev);
761
963
  continue;
762
964
  }
763
- const candidates = enumerateVoicings(chord, ranges, maxSpacing);
965
+ const candidates = locate(index, chord, () => enumerateVoicings(chord, ranges, maxSpacing));
764
966
  let best;
765
967
  let bestScore = Number.POSITIVE_INFINITY;
766
968
  for (const candidate of candidates) {
767
- 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;
768
971
  if (score < bestScore) {
769
972
  bestScore = score;
770
973
  best = candidate;
771
974
  }
772
975
  }
773
976
  if (best === void 0) {
774
- throw new Error("no voicing satisfies the given ranges");
977
+ throw noSolutionAt(index, chord);
775
978
  }
776
979
  result.push(best);
777
980
  prev = best;
981
+ prevChord = chord;
778
982
  }
779
983
  return result;
780
984
  }
@@ -793,7 +997,7 @@ function dedupePcs(pcs) {
793
997
  return result;
794
998
  }
795
999
  function lowestPitchAtOrAbove(pc, floor) {
796
- return floor + ((pc - pitchClass3(floor)) % 12 + 12) % 12;
1000
+ return floor + ((pc - pitchClassOf(floor)) % 12 + 12) % 12;
797
1001
  }
798
1002
  function nearestPc(target, pcs) {
799
1003
  let best = pcs[0] ?? target;
@@ -815,10 +1019,10 @@ function voiceChordStyled(chord, opts) {
815
1019
  assertFiniteNumber(opts.topNote, "styled voicing topNote");
816
1020
  }
817
1021
  const base = 12 * (octave + 1);
818
- const rootPc = pitchClass3(chord.rootPc);
819
- const bassPc = pitchClass3(chord.bassPc ?? chord.rootPc);
1022
+ const rootPc = pitchClassOf(chord.rootPc);
1023
+ const bassPc = pitchClassOf(chord.bassPc ?? chord.rootPc);
820
1024
  const omitRoot = style === "rootless" || opts?.rootless === true;
821
- let order = dedupePcs(chord.intervals.map((interval) => pitchClass3(chord.rootPc + interval)));
1025
+ let order = dedupePcs(chord.intervals.map((interval) => pitchClassOf(chord.rootPc + interval)));
822
1026
  const bassIndex = order.indexOf(bassPc);
823
1027
  if (bassIndex > 0) {
824
1028
  order = [...order.slice(bassIndex), ...order.slice(0, bassIndex)];
@@ -840,7 +1044,7 @@ function voiceChordStyled(chord, opts) {
840
1044
  return [];
841
1045
  }
842
1046
  if (opts?.topNote !== void 0) {
843
- 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);
844
1048
  const targetIndex = order.indexOf(target);
845
1049
  order = [...order.slice(targetIndex + 1), ...order.slice(0, targetIndex + 1)];
846
1050
  }
@@ -850,7 +1054,7 @@ function voiceChordStyled(chord, opts) {
850
1054
  if (prev === void 0) {
851
1055
  prev = lowestPitchAtOrAbove(pc, base);
852
1056
  } else {
853
- let delta = ((pc - pitchClass3(prev)) % 12 + 12) % 12;
1057
+ let delta = ((pc - pitchClassOf(prev)) % 12 + 12) % 12;
854
1058
  if (delta === 0) {
855
1059
  delta = 12;
856
1060
  }
@@ -864,7 +1068,7 @@ function voiceChordStyled(chord, opts) {
864
1068
  stack[stack.length - 3] = (stack[stack.length - 3] ?? 0) - 12;
865
1069
  }
866
1070
  if (chord.bassPc !== void 0) {
867
- const bassIndexInStack = stack.findIndex((pitch) => pitchClass3(pitch) === bassPc);
1071
+ const bassIndexInStack = stack.findIndex((pitch) => pitchClassOf(pitch) === bassPc);
868
1072
  if (bassIndexInStack >= 0) {
869
1073
  const otherLowest = Math.min(
870
1074
  ...stack.filter((_pitch, index) => index !== bassIndexInStack),
@@ -875,7 +1079,15 @@ function voiceChordStyled(chord, opts) {
875
1079
  }
876
1080
  }
877
1081
  }
878
- return stack.sort((a, b) => a - b);
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;
879
1091
  }
880
1092
 
881
1093
  export {
@@ -890,6 +1102,7 @@ export {
890
1102
  spellPitchClasses,
891
1103
  spellChordFromRoot,
892
1104
  spellChord,
1105
+ spellPitch,
893
1106
  noteNames,
894
1107
  parseChordSymbol,
895
1108
  formatChordSymbol,
@@ -901,4 +1114,4 @@ export {
901
1114
  nextVoicing,
902
1115
  voiceChordStyled
903
1116
  };
904
- //# sourceMappingURL=chunk-C5XYKUVG.js.map
1117
+ //# sourceMappingURL=chunk-RK35RIN7.js.map