@libraz/libcantus 0.9.3 → 0.9.5

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 (201) hide show
  1. package/README.md +29 -20
  2. package/README_ja.md +268 -0
  3. package/dist/analyze/index.cjs +39 -1947
  4. package/dist/analyze/index.cjs.map +1 -1
  5. package/dist/analyze/index.d.cts +161 -91
  6. package/dist/analyze/index.d.ts +161 -91
  7. package/dist/analyze/index.js +19 -8
  8. package/dist/chunk-2A2QMA74.cjs +261 -0
  9. package/dist/chunk-2A2QMA74.cjs.map +1 -0
  10. package/dist/chunk-2PTFGVD7.js +261 -0
  11. package/dist/chunk-2PTFGVD7.js.map +1 -0
  12. package/dist/chunk-357ZDPVU.cjs +162 -0
  13. package/dist/chunk-357ZDPVU.cjs.map +1 -0
  14. package/dist/chunk-3KX76RLG.js +1548 -0
  15. package/dist/chunk-3KX76RLG.js.map +1 -0
  16. package/dist/chunk-4VZ5MZAM.cjs +889 -0
  17. package/dist/chunk-4VZ5MZAM.cjs.map +1 -0
  18. package/dist/{chunk-C5XYKUVG.js → chunk-7EONYVI5.js} +471 -171
  19. package/dist/chunk-7EONYVI5.js.map +1 -0
  20. package/dist/{chunk-IT2O4XFE.js → chunk-7JEP3MFY.js} +42 -34
  21. package/dist/chunk-7JEP3MFY.js.map +1 -0
  22. package/dist/{chunk-4MHA432G.js → chunk-AID75RN3.js} +572 -250
  23. package/dist/chunk-AID75RN3.js.map +1 -0
  24. package/dist/chunk-BSLTLGM3.cjs +497 -0
  25. package/dist/chunk-BSLTLGM3.cjs.map +1 -0
  26. package/dist/chunk-CU6VG2YM.cjs +109 -0
  27. package/dist/chunk-CU6VG2YM.cjs.map +1 -0
  28. package/dist/{chunk-EH6EM37U.js → chunk-DZMHIGI4.js} +81 -59
  29. package/dist/chunk-DZMHIGI4.js.map +1 -0
  30. package/dist/{chunk-AKLYPWIN.js → chunk-EDOTEABK.js} +45 -19
  31. package/dist/chunk-EDOTEABK.js.map +1 -0
  32. package/dist/{chunk-2ZWMSJH6.js → chunk-HN2OEUN2.js} +7 -4
  33. package/dist/chunk-HN2OEUN2.js.map +1 -0
  34. package/dist/{chunk-MIWO425M.js → chunk-KBTTEX7Q.js} +128 -43
  35. package/dist/chunk-KBTTEX7Q.js.map +1 -0
  36. package/dist/chunk-KQN2ZYND.cjs +724 -0
  37. package/dist/chunk-KQN2ZYND.cjs.map +1 -0
  38. package/dist/chunk-MPTVJIVG.cjs +3176 -0
  39. package/dist/chunk-MPTVJIVG.cjs.map +1 -0
  40. package/dist/chunk-MTMZTMWM.cjs +53 -0
  41. package/dist/chunk-MTMZTMWM.cjs.map +1 -0
  42. package/dist/{chunk-CHMXELDS.js → chunk-NNB67XA2.js} +372 -197
  43. package/dist/chunk-NNB67XA2.js.map +1 -0
  44. package/dist/chunk-OL3IHHSJ.cjs +1204 -0
  45. package/dist/chunk-OL3IHHSJ.cjs.map +1 -0
  46. package/dist/chunk-PI3UXRWK.cjs +528 -0
  47. package/dist/chunk-PI3UXRWK.cjs.map +1 -0
  48. package/dist/chunk-R2VVFAIN.js +528 -0
  49. package/dist/chunk-R2VVFAIN.js.map +1 -0
  50. package/dist/chunk-T64C7ZD7.cjs +1 -0
  51. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  52. package/dist/chunk-WFDXOLUB.cjs +337 -0
  53. package/dist/chunk-WFDXOLUB.cjs.map +1 -0
  54. package/dist/chunk-WTAN4ACG.cjs +1548 -0
  55. package/dist/chunk-WTAN4ACG.cjs.map +1 -0
  56. package/dist/chunk-YFJXS3AU.cjs +33 -0
  57. package/dist/chunk-YFJXS3AU.cjs.map +1 -0
  58. package/dist/{chunk-GOAW4VYL.js → chunk-YUE6TJ4M.js} +315 -145
  59. package/dist/chunk-YUE6TJ4M.js.map +1 -0
  60. package/dist/{chunk-62QZKCJQ.js → chunk-ZGLJM4A4.js} +10 -6
  61. package/dist/chunk-ZGLJM4A4.js.map +1 -0
  62. package/dist/core/index.cjs +140 -647
  63. package/dist/core/index.cjs.map +1 -1
  64. package/dist/core/index.d.cts +313 -41
  65. package/dist/core/index.d.ts +313 -41
  66. package/dist/core/index.js +64 -16
  67. package/dist/{function-DVH4Eusr.d.ts → function-CxixTB02.d.cts} +122 -10
  68. package/dist/{function-DYt4iKsn.d.cts → function-DTCt675z.d.ts} +122 -10
  69. package/dist/generate/index.cjs +28 -4085
  70. package/dist/generate/index.cjs.map +1 -1
  71. package/dist/generate/index.d.cts +374 -66
  72. package/dist/generate/index.d.ts +374 -66
  73. package/dist/generate/index.js +23 -9
  74. package/dist/{index-Jc0Ryi79.d.ts → index-B2Ulxxpo.d.cts} +21 -5
  75. package/dist/{index-CpOsP1y9.d.cts → index-BkYJ0xMq.d.cts} +18 -17
  76. package/dist/{index-CxqBQtH1.d.cts → index-BuaB-ypt.d.ts} +21 -5
  77. package/dist/index-CAN7v-kw.d.ts +199 -0
  78. package/dist/{index-Bt4tOUD9.d.ts → index-COw1Ky9g.d.ts} +93 -8
  79. package/dist/{index-Dikst44v.d.cts → index-Cfx8xgH0.d.ts} +14 -11
  80. package/dist/{index-DxvARIP2.d.ts → index-DRtIhlJj.d.ts} +18 -17
  81. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  82. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  83. package/dist/index-alrRninU.d.cts +199 -0
  84. package/dist/{index-BNlGP86A.d.cts → index-oTQbYYIs.d.cts} +93 -8
  85. package/dist/index-tR9jTn26.d.cts +275 -0
  86. package/dist/index-tR9jTn26.d.ts +275 -0
  87. package/dist/{index-Bg5dYMmZ.d.ts → index-x2vcsduE.d.cts} +14 -11
  88. package/dist/index.cjs +429 -7383
  89. package/dist/index.cjs.map +1 -1
  90. package/dist/index.d.cts +15 -15
  91. package/dist/index.d.ts +15 -15
  92. package/dist/index.js +121 -27
  93. package/dist/model/index.cjs +8 -2562
  94. package/dist/model/index.cjs.map +1 -1
  95. package/dist/model/index.d.cts +471 -81
  96. package/dist/model/index.d.ts +471 -81
  97. package/dist/model/index.js +6 -7
  98. package/dist/styled-Bl0ZK28h.d.ts +543 -0
  99. package/dist/styled-Crv6SA4S.d.cts +543 -0
  100. package/dist/theory/index.cjs +172 -1822
  101. package/dist/theory/index.cjs.map +1 -1
  102. package/dist/theory/index.d.cts +151 -233
  103. package/dist/theory/index.d.ts +151 -233
  104. package/dist/theory/index.js +30 -7
  105. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  106. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  107. package/package.json +34 -13
  108. package/dist/chunk-2ZWMSJH6.js.map +0 -1
  109. package/dist/chunk-4MHA432G.js.map +0 -1
  110. package/dist/chunk-62QZKCJQ.js.map +0 -1
  111. package/dist/chunk-AKLYPWIN.js.map +0 -1
  112. package/dist/chunk-C5XYKUVG.js.map +0 -1
  113. package/dist/chunk-CHMXELDS.js.map +0 -1
  114. package/dist/chunk-DYWFNC4A.js +0 -152
  115. package/dist/chunk-DYWFNC4A.js.map +0 -1
  116. package/dist/chunk-EH6EM37U.js.map +0 -1
  117. package/dist/chunk-FBQK3ZQN.js +0 -131
  118. package/dist/chunk-FBQK3ZQN.js.map +0 -1
  119. package/dist/chunk-GOAW4VYL.js.map +0 -1
  120. package/dist/chunk-I5R7ZBJS.js +0 -202
  121. package/dist/chunk-I5R7ZBJS.js.map +0 -1
  122. package/dist/chunk-IT2O4XFE.js.map +0 -1
  123. package/dist/chunk-MIWO425M.js.map +0 -1
  124. package/dist/chunk-SGU7SPLK.js +0 -924
  125. package/dist/chunk-SGU7SPLK.js.map +0 -1
  126. package/dist/index-CGMZNCx6.d.cts +0 -140
  127. package/dist/index-CGMZNCx6.d.ts +0 -140
  128. package/dist/index-CnZCo29u.d.cts +0 -108
  129. package/dist/index-JnF4v9bx.d.ts +0 -108
  130. package/dist/styled-DZ7GjShb.d.ts +0 -282
  131. package/dist/styled-ZTbdaVFM.d.cts +0 -282
  132. package/src/analyze/arrange/index.ts +0 -21
  133. package/src/analyze/arrange/internal.ts +0 -127
  134. package/src/analyze/arrange/tension.ts +0 -199
  135. package/src/analyze/arrange/tracks.ts +0 -312
  136. package/src/analyze/detect/index.ts +0 -272
  137. package/src/analyze/functional/borrowed.ts +0 -84
  138. package/src/analyze/functional/cadence.ts +0 -66
  139. package/src/analyze/functional/function.ts +0 -184
  140. package/src/analyze/functional/index.ts +0 -23
  141. package/src/analyze/functional/internal.ts +0 -29
  142. package/src/analyze/functional/roman.ts +0 -402
  143. package/src/analyze/index.ts +0 -49
  144. package/src/analyze/timeline/index.ts +0 -451
  145. package/src/analyze/voice/index.ts +0 -339
  146. package/src/core/event-index/index.ts +0 -104
  147. package/src/core/index.ts +0 -61
  148. package/src/core/interval/index.ts +0 -81
  149. package/src/core/meter/index.ts +0 -302
  150. package/src/core/pitch/index.ts +0 -311
  151. package/src/core/random/index.ts +0 -76
  152. package/src/core/tuning/index.ts +0 -186
  153. package/src/core/types.ts +0 -29
  154. package/src/core/validation/index.ts +0 -119
  155. package/src/generate/bass/index.ts +0 -373
  156. package/src/generate/countermelody/index.ts +0 -470
  157. package/src/generate/drums/beat.ts +0 -266
  158. package/src/generate/drums/euclid.ts +0 -65
  159. package/src/generate/drums/fills.ts +0 -356
  160. package/src/generate/drums/ghost.ts +0 -119
  161. package/src/generate/drums/hihat.ts +0 -285
  162. package/src/generate/drums/hit.ts +0 -31
  163. package/src/generate/drums/index.ts +0 -361
  164. package/src/generate/drums/internal.ts +0 -257
  165. package/src/generate/drums/kick.ts +0 -160
  166. package/src/generate/drums/percussion.ts +0 -121
  167. package/src/generate/drums/rng.ts +0 -7
  168. package/src/generate/drums/swing.ts +0 -71
  169. package/src/generate/groove/index.ts +0 -351
  170. package/src/generate/harmonize/index.ts +0 -451
  171. package/src/generate/index.ts +0 -58
  172. package/src/generate/motif/index.ts +0 -422
  173. package/src/generate/progression/index.ts +0 -362
  174. package/src/generate/reharmony/index.ts +0 -323
  175. package/src/generate/rhythm/index.ts +0 -175
  176. package/src/index.ts +0 -13
  177. package/src/model/chord.ts +0 -442
  178. package/src/model/index.ts +0 -15
  179. package/src/model/interval.ts +0 -82
  180. package/src/model/key.ts +0 -266
  181. package/src/model/note.ts +0 -176
  182. package/src/model/progression.ts +0 -161
  183. package/src/model/shared.ts +0 -12
  184. package/src/theory/chord/index.ts +0 -375
  185. package/src/theory/chordscale/index.ts +0 -374
  186. package/src/theory/counterpoint/index.ts +0 -230
  187. package/src/theory/harmony/index.ts +0 -77
  188. package/src/theory/index.ts +0 -112
  189. package/src/theory/safety/index.ts +0 -488
  190. package/src/theory/scale/degrees.ts +0 -123
  191. package/src/theory/scale/index.ts +0 -28
  192. package/src/theory/scale/key.ts +0 -56
  193. package/src/theory/scale/masks.ts +0 -153
  194. package/src/theory/spelling/index.ts +0 -249
  195. package/src/theory/symbol/index.ts +0 -268
  196. package/src/theory/voicing/index.ts +0 -5
  197. package/src/theory/voicing/internal.ts +0 -168
  198. package/src/theory/voicing/leading.ts +0 -112
  199. package/src/theory/voicing/satb.ts +0 -216
  200. package/src/theory/voicing/styled.ts +0 -199
  201. package/tsconfig.json +0 -23
@@ -1,32 +1,34 @@
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-KBTTEX7Q.js";
19
13
  import {
14
+ InvalidInputError,
15
+ NoSolutionError,
20
16
  assertFiniteNumber,
17
+ assertFiniteSemitones,
21
18
  assertGenerationBudget,
22
19
  assertInteger,
23
- assertPositiveInt
24
- } from "./chunk-FBQK3ZQN.js";
20
+ assertPositiveInt,
21
+ diatonicLetterOf,
22
+ formatNote,
23
+ midiToNote,
24
+ naturalPitchClassOf,
25
+ noteToPitchClass,
26
+ parseNote,
27
+ pitchClassOf,
28
+ transposeNote
29
+ } from "./chunk-R2VVFAIN.js";
25
30
 
26
31
  // src/theory/chordscale/index.ts
27
- function pitchClass(value) {
28
- return (Math.trunc(value) % 12 + 12) % 12;
29
- }
30
32
  function popcount12(mask) {
31
33
  let count = 0;
32
34
  for (let n = 0; n < 12; n += 1) {
@@ -35,7 +37,7 @@ function popcount12(mask) {
35
37
  return count;
36
38
  }
37
39
  function maskHasPitchClass(mask, scaleRootPc, pc) {
38
- const offset = pitchClass(pc - scaleRootPc);
40
+ const offset = pitchClassOf(pc - scaleRootPc);
39
41
  return (mask >> offset & 1) === 1;
40
42
  }
41
43
  function scaleMatchesChord(chordPcs, scaleMask, scaleRootPc) {
@@ -48,21 +50,51 @@ function scaleMatchesChord(chordPcs, scaleMask, scaleRootPc) {
48
50
  }
49
51
  var ALIASED_SCALE_NAMES = /* @__PURE__ */ new Set(["major", "naturalMinor"]);
50
52
  var HEPTATONIC_SIZE = 7;
53
+ var SCALE_PREFERENCE = [
54
+ "ionian",
55
+ "major",
56
+ "mixolydian",
57
+ "dorian",
58
+ "lydian",
59
+ "aeolian",
60
+ "naturalMinor",
61
+ "phrygian",
62
+ "locrian"
63
+ ];
64
+ var IDIOMATIC_SCALES = {
65
+ aug: ["wholeTone"],
66
+ m7b5: ["locrian"]
67
+ };
68
+ function conventionalRank(name) {
69
+ const index = SCALE_PREFERENCE.indexOf(name);
70
+ return index < 0 ? SCALE_PREFERENCE.length : index;
71
+ }
72
+ function idiomaticRank(chord, name) {
73
+ const choices = IDIOMATIC_SCALES[chord.quality];
74
+ if (choices === void 0) {
75
+ return 0;
76
+ }
77
+ const index = choices.indexOf(name);
78
+ return index < 0 ? choices.length + 1 : index;
79
+ }
80
+ function isAlteredDominant(chord, scaleName) {
81
+ return chord.quality === "dom7" && scaleName === "altered";
82
+ }
51
83
  function chordScales(chord) {
52
84
  const chordPcs = chordPitchClasses(chord);
53
- const rootPc = pitchClass(chord.rootPc);
85
+ const rootPc = pitchClassOf(chord.rootPc);
54
86
  const ranked = [];
55
87
  const seenMasks = /* @__PURE__ */ new Set();
56
88
  for (const name of Object.keys(NAMED_SCALES)) {
57
89
  if (name === "chromatic" || ALIASED_SCALE_NAMES.has(name)) {
58
90
  continue;
59
91
  }
60
- const mask = NAMED_SCALES[name];
92
+ const mask = namedScaleMask(name);
61
93
  if (mask === void 0 || seenMasks.has(mask)) {
62
94
  continue;
63
95
  }
64
96
  seenMasks.add(mask);
65
- if (scaleMatchesChord(chordPcs, mask, rootPc)) {
97
+ if (scaleMatchesChord(chordPcs, mask, rootPc) || isAlteredDominant(chord, name)) {
66
98
  const size = popcount12(mask);
67
99
  ranked.push({ name, extra: size - chordPcs.length, size });
68
100
  }
@@ -75,27 +107,32 @@ function chordScales(chord) {
75
107
  }
76
108
  const preferHeptatonic = chordPcs.length <= 3;
77
109
  ranked.sort((a, b) => {
110
+ const idiomaticA = idiomaticRank(chord, a.name);
111
+ const idiomaticB = idiomaticRank(chord, b.name);
112
+ if (idiomaticA !== idiomaticB) {
113
+ return idiomaticA - idiomaticB;
114
+ }
78
115
  if (preferHeptatonic && a.size === HEPTATONIC_SIZE !== (b.size === HEPTATONIC_SIZE)) {
79
116
  return a.size === HEPTATONIC_SIZE ? -1 : 1;
80
117
  }
81
118
  if (a.extra !== b.extra) {
82
119
  return a.extra - b.extra;
83
120
  }
84
- if (a.size !== b.size) {
85
- return a.size - b.size;
121
+ const rankA = conventionalRank(a.name);
122
+ const rankB = conventionalRank(b.name);
123
+ if (rankA !== rankB) {
124
+ return rankA - rankB;
86
125
  }
87
126
  return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
88
127
  });
89
128
  return ranked.map((entry) => ({ name: entry.name, rootPc }));
90
129
  }
91
130
  function avoidNotes(chord, scaleName) {
92
- const mask = NAMED_SCALES[scaleName];
93
- if (mask === void 0) {
94
- return [];
95
- }
96
- const rootPc = pitchClass(chord.rootPc);
131
+ const mask = requireScaleMask(scaleName);
132
+ const rootPc = pitchClassOf(chord.rootPc);
97
133
  const chordPcs = chordPitchClasses(chord);
98
- if (!scaleMatchesChord(chordPcs, mask, rootPc)) {
134
+ const alteredDominant = isAlteredDominant(chord, scaleName);
135
+ if (!scaleMatchesChord(chordPcs, mask, rootPc) && !alteredDominant) {
99
136
  return [];
100
137
  }
101
138
  const chordSet = new Set(chordPcs);
@@ -104,24 +141,23 @@ function avoidNotes(chord, scaleName) {
104
141
  if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc)) {
105
142
  continue;
106
143
  }
107
- if (chordSet.has(pitchClass(pc - 1))) {
144
+ const suspendedThird = chord.quality === "sus4" || chord.quality === "7sus4" ? pc === pitchClassOf(rootPc + 4) : false;
145
+ if (!alteredDominant && (chordSet.has(pitchClassOf(pc - 1)) || suspendedThird)) {
108
146
  avoid.push(pc);
109
147
  }
110
148
  }
111
149
  return avoid.sort((a, b) => a - b);
112
150
  }
113
151
  function availableTensions(chord, scaleName) {
114
- const mask = NAMED_SCALES[scaleName];
115
- if (mask === void 0) {
116
- return [];
117
- }
118
- const rootPc = pitchClass(chord.rootPc);
152
+ const mask = requireScaleMask(scaleName);
153
+ const rootPc = pitchClassOf(chord.rootPc);
119
154
  const chordPcs = chordPitchClasses(chord);
120
- if (!scaleMatchesChord(chordPcs, mask, rootPc)) {
155
+ const alteredDominant = isAlteredDominant(chord, scaleName);
156
+ if (!scaleMatchesChord(chordPcs, mask, rootPc) && !alteredDominant) {
121
157
  return [];
122
158
  }
123
159
  const chordSet = new Set(chordPcs);
124
- const avoidSet = new Set(avoidNotes(chord, scaleName));
160
+ const avoidSet = alteredDominant ? /* @__PURE__ */ new Set() : new Set(avoidNotes(chord, scaleName));
125
161
  const tensions = [];
126
162
  for (let pc = 0; pc < 12; pc += 1) {
127
163
  if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc) || avoidSet.has(pc)) {
@@ -132,8 +168,11 @@ function availableTensions(chord, scaleName) {
132
168
  return tensions.sort((a, b) => a - b);
133
169
  }
134
170
  function chordScaleReport(chord, limit) {
171
+ if (limit !== void 0) {
172
+ assertPositiveInt(limit, "chord-scale report limit");
173
+ }
135
174
  const matches = chordScales(chord);
136
- const chosen = limit === void 0 ? matches : matches.slice(0, Math.max(0, limit));
175
+ const chosen = limit === void 0 ? matches : matches.slice(0, limit);
137
176
  return chosen.map((match) => ({
138
177
  name: match.name,
139
178
  rootPc: match.rootPc,
@@ -143,14 +182,14 @@ function chordScaleReport(chord, limit) {
143
182
  }
144
183
  var RANK_PENALTY = 0.01;
145
184
  function scalePitchClassSet(name, rootPc) {
146
- const mask = NAMED_SCALES[name];
185
+ const mask = namedScaleMask(name);
147
186
  const pcs = /* @__PURE__ */ new Set();
148
187
  if (mask === void 0) {
149
188
  return pcs;
150
189
  }
151
190
  for (let n = 0; n < 12; n += 1) {
152
191
  if ((mask >> n & 1) === 1) {
153
- pcs.add(pitchClass(rootPc + n));
192
+ pcs.add(pitchClassOf(rootPc + n));
154
193
  }
155
194
  }
156
195
  return pcs;
@@ -170,6 +209,7 @@ function symmetricDifferenceSize(a, b) {
170
209
  return count;
171
210
  }
172
211
  function scalesForChanges(chords) {
212
+ assertGenerationBudget(chords.length, "chord-scale changes");
173
213
  if (chords.length === 0) {
174
214
  return [];
175
215
  }
@@ -228,64 +268,123 @@ function scalesForChanges(chords) {
228
268
  }
229
269
 
230
270
  // src/theory/spelling/index.ts
231
- var LETTER_SEMITONES = [0, 2, 4, 5, 7, 9, 11];
232
271
  var CHROMATIC_SPELLING = {
233
- 1: { degreeOffset: 1, alter: -1 },
272
+ 1: 1,
234
273
  // b2
235
- 3: { degreeOffset: 2, alter: -1 },
274
+ 2: 1,
275
+ // M2
276
+ 3: 2,
236
277
  // b3
237
- 6: { degreeOffset: 3, alter: 1 },
278
+ 4: 2,
279
+ // M3
280
+ 5: 3,
281
+ // P4
282
+ 6: 3,
238
283
  // #4
239
- 8: { degreeOffset: 5, alter: -1 },
284
+ 7: 4,
285
+ // P5
286
+ 8: 5,
240
287
  // b6
241
- 9: { degreeOffset: 5, alter: 1 },
288
+ 9: 5,
242
289
  // #6 (raised sixth of a minor key)
243
- 10: { degreeOffset: 6, alter: -1 },
290
+ 10: 6,
244
291
  // b7
245
- 11: { degreeOffset: 6, alter: 1 }
292
+ 11: 6
246
293
  // #7 (raised leading tone of a minor key)
247
294
  };
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
- }
295
+ var CHROMATIC_SPELLING_FLAT = {
296
+ ...CHROMATIC_SPELLING,
297
+ 6: 4
298
+ // b5
299
+ };
300
+ var CHROMATIC_SPELLING_SHARP = {
301
+ 1: 0,
302
+ // #1
303
+ 2: 1,
304
+ // M2
305
+ 3: 1,
306
+ // #2
307
+ 4: 2,
308
+ // M3
309
+ 5: 3,
310
+ // P4
311
+ 6: 3,
312
+ // #4
313
+ 7: 4,
314
+ // P5
315
+ 8: 4,
316
+ // #5
317
+ 9: 5,
318
+ // M6
319
+ 10: 5,
320
+ // #6
321
+ 11: 6
322
+ // M7
323
+ };
257
324
  function alterFor(letter, pc) {
258
- let d = mod12(pc - naturalPc(letter));
325
+ let d = pitchClassOf(pc - naturalPitchClassOf(letter));
259
326
  if (d > 6) {
260
327
  d -= 12;
261
328
  }
262
329
  return d;
263
330
  }
331
+ function letterFor(tonic, degreeOffset, pc) {
332
+ if (degreeOffset === void 0) {
333
+ return void 0;
334
+ }
335
+ const letter = diatonicLetterOf(tonic.letter + degreeOffset);
336
+ return { letter, alter: alterFor(letter, pc) };
337
+ }
338
+ function pickSpelling(flat, sharp, preferFlat) {
339
+ if (flat === void 0 || sharp === void 0) {
340
+ return flat ?? sharp;
341
+ }
342
+ if (Math.abs(flat.alter) !== Math.abs(sharp.alter)) {
343
+ return Math.abs(flat.alter) < Math.abs(sharp.alter) ? flat : sharp;
344
+ }
345
+ return preferFlat ? flat : sharp;
346
+ }
347
+ function capChromaticAccidentals(preferred, alternative) {
348
+ return alternative !== void 0 && Math.abs(preferred.alter) - Math.abs(alternative.alter) >= 2 ? alternative : preferred;
349
+ }
264
350
  function isHeptatonic(key) {
265
351
  return scaleTonesInDegreeOrder(key).length === 7;
266
352
  }
267
- function spellPitchClass(pc, tonic, key) {
268
- const tonicPc = mod12(naturalPc(tonic.letter) + tonic.alter);
269
- const offset = mod12(pc - tonicPc);
353
+ function hasMinorThird(key) {
270
354
  const tones = scaleTonesInDegreeOrder(key);
271
- if (isHeptatonic(key)) {
272
- const degree = tones.indexOf(mod12(pc));
355
+ const root = pitchClassOf(key.rootPc);
356
+ return tones.includes(pitchClassOf(root + 3)) && !tones.includes(pitchClassOf(root + 4));
357
+ }
358
+ function spellPitchClass(pc, tonic, key) {
359
+ const tonicPc = pitchClassOf(naturalPitchClassOf(tonic.letter) + tonic.alter);
360
+ const offset = pitchClassOf(pc - tonicPc);
361
+ if (offset === 0) {
362
+ return { letter: diatonicLetterOf(tonic.letter), alter: tonic.alter };
363
+ }
364
+ const heptatonic = isHeptatonic(key);
365
+ if (heptatonic) {
366
+ const degree = scaleTonesInDegreeOrder(key).indexOf(pitchClassOf(pc));
273
367
  if (degree >= 0) {
274
- const letter = mod7(tonic.letter + degree);
368
+ const letter = diatonicLetterOf(tonic.letter + degree);
275
369
  return { letter, alter: alterFor(letter, pc) };
276
370
  }
277
- const chromatic = CHROMATIC_SPELLING[offset];
278
- if (chromatic) {
279
- const letter = mod7(tonic.letter + chromatic.degreeOffset);
280
- return { letter, alter: alterFor(letter, pc) };
371
+ const degreeOffset = CHROMATIC_SPELLING[offset];
372
+ const preferred = letterFor(tonic, degreeOffset, pc);
373
+ if (preferred !== void 0) {
374
+ const flat = letterFor(tonic, CHROMATIC_SPELLING_FLAT[offset], pc);
375
+ const sharp = letterFor(tonic, CHROMATIC_SPELLING_SHARP[offset], pc);
376
+ const alternative = flat?.letter === preferred.letter ? sharp : sharp?.letter === preferred.letter ? flat : void 0;
377
+ return capChromaticAccidentals(preferred, alternative);
281
378
  }
282
- }
283
- for (let letter = 0; letter < 7; letter += 1) {
284
- if (naturalPc(letter) === mod12(pc)) {
285
- return { letter, alter: 0 };
379
+ } else {
380
+ const flat = letterFor(tonic, CHROMATIC_SPELLING_FLAT[offset], pc);
381
+ const sharp = letterFor(tonic, CHROMATIC_SPELLING_SHARP[offset], pc);
382
+ const best = pickSpelling(flat, sharp, tonic.alter < 0 || hasMinorThird(key));
383
+ if (best !== void 0) {
384
+ return best;
286
385
  }
287
386
  }
288
- const belowLetter = mod7([0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6][mod12(pc)] ?? 0);
387
+ const belowLetter = diatonicLetterOf([0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6][pitchClassOf(pc)] ?? 0);
289
388
  return { letter: belowLetter, alter: alterFor(belowLetter, pc) };
290
389
  }
291
390
  function spellScale(tonic, key) {
@@ -306,9 +405,10 @@ function chordLetterOffset(interval, chord) {
306
405
  return 1;
307
406
  case 3:
308
407
  case 4:
309
- case 15:
310
408
  case 16:
311
409
  return 2;
410
+ case 15:
411
+ return chord.intervals.some((i) => i === 3 || i === 4) ? 1 : 2;
312
412
  case 5:
313
413
  case 17:
314
414
  case 18:
@@ -330,32 +430,48 @@ function chordLetterOffset(interval, chord) {
330
430
  return 6;
331
431
  default: {
332
432
  const octaves = Math.floor(Math.max(0, interval) / 12);
333
- const simple = mod12(interval);
433
+ const simple = pitchClassOf(interval);
334
434
  const simpleOffset = [0, 1, 1, 2, 2, 3, 4, 4, 4, 5, 6, 6][simple] ?? 0;
335
435
  return octaves * 7 + simpleOffset;
336
436
  }
337
437
  }
338
438
  }
339
439
  function spellChordFromRoot(chord, root) {
440
+ if (pitchClassOf(naturalPitchClassOf(root.letter) + root.alter) !== pitchClassOf(chord.rootPc)) {
441
+ throw new InvalidInputError("chord root spelling must match chord.rootPc");
442
+ }
340
443
  return chord.intervals.map((interval) => {
341
- const letter = mod7(root.letter + chordLetterOffset(interval, chord));
342
- const pc = mod12(chord.rootPc + interval);
444
+ const letter = diatonicLetterOf(root.letter + chordLetterOffset(interval, chord));
445
+ const pc = pitchClassOf(chord.rootPc + interval);
343
446
  return { letter, alter: alterFor(letter, pc) };
344
447
  });
345
448
  }
346
449
  function spellChord(chord, tonic, key) {
347
- const root = spellPitchClass(chord.rootPc, tonic, key);
348
- return spellChordFromRoot(chord, root);
450
+ return spellChordFromRoot(chord, chordRootSpelling(chord, tonic, key));
451
+ }
452
+ function chordRootSpelling(chord, tonic, key) {
453
+ const hint = chord.rootSpelling;
454
+ if (hint !== void 0 && pitchClassOf(naturalPitchClassOf(hint.letter) + hint.alter) === pitchClassOf(chord.rootPc)) {
455
+ return { letter: diatonicLetterOf(hint.letter), alter: hint.alter };
456
+ }
457
+ return spellPitchClass(chord.rootPc, tonic, key);
458
+ }
459
+ function spellPitch(pitch, tonic, key) {
460
+ const rounded = Math.round(pitch);
461
+ const spelled = spellPitchClass(pitchClassOf(rounded), tonic, key);
462
+ const octave = (rounded - naturalPitchClassOf(spelled.letter) - spelled.alter) / 12 - 1;
463
+ return { letter: spelled.letter, alter: spelled.alter, octave };
349
464
  }
350
465
  function noteNames(notes) {
351
466
  return notes.map(formatNote);
352
467
  }
353
468
 
354
469
  // src/theory/symbol/index.ts
355
- var ROOT_RE = /^[A-Ga-g](?:#{1,2}|b{1,2})?/;
356
- var BASS_RE = /^[A-Ga-g](?:#{1,2}|b{1,2})?$/;
470
+ var ROOT_RE = /^[A-Ga-g](?:#{1,2}|b{1,2}|x)?/;
471
+ var BASS_RE = /^[A-Ga-g](?:#{1,2}|b{1,2}|x)?$/;
357
472
  var QUALITY_MAP = {
358
473
  "": "maj",
474
+ maj: "maj",
359
475
  m: "min",
360
476
  min: "min",
361
477
  "-": "min",
@@ -380,6 +496,15 @@ var QUALITY_MAP = {
380
496
  mMaj7: "minMaj7",
381
497
  mM7: "minMaj7",
382
498
  minMaj7: "minMaj7",
499
+ mMaj9: "minMaj9",
500
+ mM9: "minMaj9",
501
+ minMaj9: "minMaj9",
502
+ mMaj11: "minMaj11",
503
+ mM11: "minMaj11",
504
+ minMaj11: "minMaj11",
505
+ mMaj13: "minMaj13",
506
+ mM13: "minMaj13",
507
+ minMaj13: "minMaj13",
383
508
  aug7: "aug7",
384
509
  "+7": "aug7",
385
510
  "7#5": "aug7",
@@ -411,7 +536,36 @@ var QUALITY_MAP = {
411
536
  "7b13": "7b13",
412
537
  "11": "11",
413
538
  "13": "13",
414
- "5": "5"
539
+ "5": "5",
540
+ "7sus4": "7sus4",
541
+ "7sus": "7sus4",
542
+ "9sus4": "11",
543
+ "9sus": "11",
544
+ "7b5": "7b5",
545
+ "7alt": "7alt",
546
+ alt: "7alt",
547
+ "13b9": "13b9",
548
+ maj13: "maj13",
549
+ M13: "maj13",
550
+ "maj7#11": "maj7#11",
551
+ "M7#11": "maj7#11",
552
+ m11: "min11",
553
+ min11: "min11",
554
+ m13: "min13",
555
+ min13: "min13",
556
+ madd9: "minAdd9",
557
+ "min(add9)": "minAdd9",
558
+ "m(add9)": "minAdd9",
559
+ "6add9": "6/9",
560
+ "m6/9": "min6/9",
561
+ m69: "min6/9",
562
+ "min6/9": "min6/9",
563
+ // ASCII stand-ins for the degree and half-diminished glyphs, which most
564
+ // lead sheets and chord-chart text files use instead of the typographic ones.
565
+ o: "dim",
566
+ o7: "dim7",
567
+ h: "m7b5",
568
+ h7: "m7b5"
415
569
  };
416
570
  var CANONICAL_SUFFIX = {
417
571
  maj: "",
@@ -424,6 +578,9 @@ var CANONICAL_SUFFIX = {
424
578
  dim7: "dim7",
425
579
  m7b5: "m7b5",
426
580
  minMaj7: "mMaj7",
581
+ minMaj9: "mMaj9",
582
+ minMaj11: "mMaj11",
583
+ minMaj13: "mMaj13",
427
584
  aug7: "aug7",
428
585
  augMaj7: "augMaj7",
429
586
  majb5: "maj(b5)",
@@ -443,10 +600,20 @@ var CANONICAL_SUFFIX = {
443
600
  "7b13": "7b13",
444
601
  "11": "11",
445
602
  "13": "13",
446
- "5": "5"
603
+ "5": "5",
604
+ "7sus4": "7sus4",
605
+ "7b5": "7b5",
606
+ "7alt": "7alt",
607
+ "13b9": "13b9",
608
+ maj13: "maj13",
609
+ "maj7#11": "maj7#11",
610
+ min11: "m11",
611
+ min13: "m13",
612
+ minAdd9: "madd9",
613
+ "min6/9": "m6/9"
447
614
  };
448
- function pitchClass2(value) {
449
- return (Math.trunc(value) % 12 + 12) % 12;
615
+ function knownQuality(suffix) {
616
+ return Object.hasOwn(QUALITY_MAP, suffix) ? QUALITY_MAP[suffix] : void 0;
450
617
  }
451
618
  function makeSpelledChord(root, quality, bass) {
452
619
  const chord = makeChord(
@@ -461,10 +628,10 @@ function makeSpelledChord(root, quality, bass) {
461
628
  return chord;
462
629
  }
463
630
  function parseChordSymbol(text) {
464
- const trimmed = text.trim();
631
+ const trimmed = text.trim().replaceAll("\u266F", "#").replaceAll("\u266D", "b").replaceAll("\u{1D12A}", "x").replaceAll("\u{1D12B}", "bb");
465
632
  const rootMatch = ROOT_RE.exec(trimmed);
466
633
  if (!rootMatch) {
467
- throw new Error(`Invalid chord symbol: ${text}`);
634
+ throw new InvalidInputError(`Invalid chord symbol: ${text}`);
468
635
  }
469
636
  const rootToken = rootMatch[0];
470
637
  const rootNote = parseNote(rootToken);
@@ -473,39 +640,57 @@ function parseChordSymbol(text) {
473
640
  if (slashIndex !== -1) {
474
641
  const before = rest.slice(0, slashIndex);
475
642
  const after = rest.slice(slashIndex + 1);
476
- const beforeQuality = QUALITY_MAP[before];
643
+ const beforeQuality = knownQuality(before);
477
644
  if (beforeQuality !== void 0 && BASS_RE.test(after)) {
478
645
  return makeSpelledChord(rootNote, beforeQuality, parseNote(after));
479
646
  }
480
647
  }
481
- const quality = QUALITY_MAP[rest];
648
+ const quality = knownQuality(rest);
482
649
  if (quality === void 0) {
483
- throw new Error(`Unrecognized chord quality: ${text}`);
650
+ throw new InvalidInputError(`Unrecognized chord quality: ${text}`);
484
651
  }
485
652
  return makeSpelledChord(rootNote, quality);
486
653
  }
487
- function pitchClassName(pc, hint, flats) {
654
+ function pitchClassName(pc, hint, flats, inheritFlats) {
488
655
  if (flats === void 0 && hint !== void 0 && noteToPitchClass(hint) === pc) {
489
656
  return formatNote(hint);
490
657
  }
491
- const note = midiToNote(60 + pc, flats ? "flat" : "sharp");
658
+ const note = midiToNote(60 + pc, flats ?? inheritFlats ? "flat" : "sharp");
492
659
  return formatNote({ letter: note.letter, alter: note.alter });
493
660
  }
494
661
  function formatChordSymbol(chord, opts) {
495
- const rootPc = pitchClass2(chord.rootPc);
496
- const rootName = pitchClassName(rootPc, chord.rootSpelling, opts?.flats);
662
+ assertFiniteNumber(chord.rootPc, "chord rootPc");
663
+ if (chord.bassPc !== void 0) {
664
+ assertFiniteNumber(chord.bassPc, "chord bassPc");
665
+ }
666
+ if (!Object.hasOwn(CANONICAL_SUFFIX, chord.quality)) {
667
+ throw new InvalidInputError(`Unknown chord quality: ${String(chord.quality)}`);
668
+ }
669
+ const rootPc = pitchClassOf(chord.rootPc);
670
+ const rootHint = chord.rootSpelling;
671
+ const rootName = pitchClassName(rootPc, rootHint, opts?.flats);
497
672
  const suffix = CANONICAL_SUFFIX[chord.quality];
498
673
  let symbol = `${rootName}${suffix}`;
499
- if (chord.bassPc !== void 0 && pitchClass2(chord.bassPc) !== rootPc) {
500
- symbol += `/${pitchClassName(pitchClass2(chord.bassPc), chord.bassSpelling, opts?.flats)}`;
674
+ if (chord.bassPc !== void 0 && pitchClassOf(chord.bassPc) !== rootPc) {
675
+ const inheritFlats = rootHint !== void 0 && noteToPitchClass(rootHint) === rootPc ? rootHint.alter < 0 : void 0;
676
+ symbol += `/${pitchClassName(pitchClassOf(chord.bassPc), chord.bassSpelling, opts?.flats, inheritFlats)}`;
501
677
  }
502
678
  return symbol;
503
679
  }
504
680
  function transposeChordSymbol(text, semitones, opts) {
681
+ assertFiniteSemitones(semitones);
505
682
  const chord = parseChordSymbol(text);
506
- const transposed = { ...chord, rootPc: pitchClass2(chord.rootPc + semitones) };
683
+ const transposed = { ...chord, rootPc: pitchClassOf(chord.rootPc + semitones) };
684
+ if (chord.rootSpelling !== void 0) {
685
+ const hint = transposeNote(chord.rootSpelling, semitones);
686
+ transposed.rootSpelling = { letter: hint.letter, alter: hint.alter };
687
+ }
507
688
  if (chord.bassPc !== void 0) {
508
- transposed.bassPc = pitchClass2(chord.bassPc + semitones);
689
+ transposed.bassPc = pitchClassOf(chord.bassPc + semitones);
690
+ }
691
+ if (chord.bassSpelling !== void 0) {
692
+ const hint = transposeNote(chord.bassSpelling, semitones);
693
+ transposed.bassSpelling = { letter: hint.letter, alter: hint.alter };
509
694
  }
510
695
  return formatChordSymbol(transposed, opts);
511
696
  }
@@ -514,15 +699,17 @@ function transposeChordSymbol(text, semitones, opts) {
514
699
  var DEFAULT_MAX_SPACING = 12;
515
700
  var VIOLATION_PENALTY = 1e3;
516
701
  var MISSING_TONE_PENALTY = 500;
702
+ var MISSING_FIFTH_PENALTY = 20;
517
703
  var POOR_DOUBLING_PENALTY = 4;
518
- var MAX_CANDIDATES = 4e3;
519
- function pitchClass3(value) {
520
- return (Math.trunc(value) % 12 + 12) % 12;
521
- }
704
+ var UNISON_PENALTY = 100;
705
+ var LEADING_TONE_DOUBLING_PENALTY = 200;
706
+ var RESOLUTION_PENALTY = 200;
707
+ var DEFAULT_MAX_CANDIDATES = 4e3;
708
+ var SEARCH_NODES_PER_CANDIDATE = 16;
522
709
  function pitchesForPc(pc, range) {
523
710
  const result = [];
524
711
  for (let midi = Math.ceil(range.min); midi <= range.max; midi += 1) {
525
- if (pitchClass3(midi) === pc) {
712
+ if (pitchClassOf(midi) === pc) {
526
713
  result.push(midi);
527
714
  }
528
715
  }
@@ -533,67 +720,115 @@ function pitchesForPc(pc, range) {
533
720
  return da === db ? a - b : da - db;
534
721
  });
535
722
  }
536
- function enumerateVoicings(chord, ranges, maxSpacing) {
723
+ function enumerateVoicings(chord, ranges, maxSpacing, maxCandidates = DEFAULT_MAX_CANDIDATES) {
537
724
  const chordPcs = chordPitchClasses(chord);
538
- const bassPc = pitchClass3(chord.bassPc ?? chord.rootPc);
725
+ const bassPc = pitchClassOf(chord.bassPc ?? chord.rootPc);
726
+ const byVoice = ranges.map(
727
+ (range, voice) => (voice === 0 ? [bassPc] : chordPcs).flatMap((pc) => pitchesForPc(pc, range))
728
+ );
729
+ for (let voice = 0; voice < byVoice.length; voice += 1) {
730
+ if ((byVoice[voice] ?? []).length === 0) {
731
+ throw new NoSolutionError("no voicing satisfies the given ranges");
732
+ }
733
+ }
539
734
  const results = [];
540
735
  const current = [];
736
+ let nodes = 0;
737
+ const maxSearchNodes = maxCandidates * SEARCH_NODES_PER_CANDIDATE;
541
738
  const build = (voice) => {
542
- if (results.length >= MAX_CANDIDATES) {
739
+ nodes += 1;
740
+ if (results.length >= maxCandidates || nodes > maxSearchNodes) {
543
741
  return;
544
742
  }
545
743
  if (voice === ranges.length) {
546
744
  results.push([...current]);
547
745
  return;
548
746
  }
549
- const range = ranges[voice];
550
- if (range === void 0) {
551
- return;
552
- }
553
- const pcs = voice === 0 ? [bassPc] : chordPcs;
554
747
  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
- }
748
+ for (const pitch of byVoice[voice] ?? []) {
749
+ if (prev !== void 0) {
750
+ if (pitch < prev) {
751
+ continue;
752
+ }
753
+ const spacingLimit = voice === 1 ? maxSpacing + 12 : maxSpacing;
754
+ if (pitch - prev > spacingLimit) {
755
+ continue;
565
756
  }
566
- current.push(pitch);
567
- build(voice + 1);
568
- current.pop();
569
757
  }
758
+ current.push(pitch);
759
+ build(voice + 1);
760
+ current.pop();
570
761
  }
571
762
  };
572
763
  build(0);
573
764
  return results;
574
765
  }
575
- function structuralPenalty(pitches, chord) {
766
+ function structuralPenalty(pitches, chord, key) {
576
767
  const counts = /* @__PURE__ */ new Map();
577
768
  for (const pitch of pitches) {
578
- const pc = pitchClass3(pitch);
769
+ const pc = pitchClassOf(pitch);
579
770
  counts.set(pc, (counts.get(pc) ?? 0) + 1);
580
771
  }
581
772
  let penalty = 0;
773
+ for (let index = 1; index < pitches.length; index += 1) {
774
+ if (pitches[index] === pitches[index - 1]) {
775
+ penalty += UNISON_PENALTY;
776
+ }
777
+ }
582
778
  for (const pc of chordPitchClasses(chord)) {
583
779
  if (!counts.has(pc)) {
584
- penalty += MISSING_TONE_PENALTY;
780
+ penalty += chordToneRole(pc, chord) === "fifth" ? MISSING_FIFTH_PENALTY : MISSING_TONE_PENALTY;
585
781
  }
586
782
  }
587
- const rootPc = pitchClass3(chord.rootPc);
588
- const fifthPc = pitchClass3(chord.rootPc + 7);
783
+ const rootPc = pitchClassOf(chord.rootPc);
784
+ const leadingTonePc = key === void 0 ? void 0 : pitchClassOf(key.rootPc - 1);
589
785
  for (const [pc, count] of counts) {
590
- if (count > 1 && pc !== rootPc && pc !== fifthPc) {
786
+ if (count <= 1) {
787
+ continue;
788
+ }
789
+ if (pc === leadingTonePc) {
790
+ penalty += (count - 1) * LEADING_TONE_DOUBLING_PENALTY;
791
+ continue;
792
+ }
793
+ if (pc !== rootPc && chordToneRole(pc, chord) !== "fifth") {
591
794
  penalty += (count - 1) * POOR_DOUBLING_PENALTY;
592
795
  }
593
796
  }
594
797
  return penalty;
595
798
  }
596
- function violationCount(prev, cur, maxSpacing) {
799
+ function seventhPcOf(chord) {
800
+ for (const interval of chord.intervals) {
801
+ const pc = pitchClassOf(chord.rootPc + interval);
802
+ if (chordToneRole(pc, chord) === "seventh") {
803
+ return pc;
804
+ }
805
+ }
806
+ return void 0;
807
+ }
808
+ function resolutionViolations(prev, cur, prevChord, nextChord, key) {
809
+ const seventhPc = seventhPcOf(prevChord);
810
+ const leadingTonePc = key === void 0 ? void 0 : pitchClassOf(key.rootPc - 1);
811
+ const tonicPc = key === void 0 ? void 0 : pitchClassOf(key.rootPc);
812
+ const nextPcs = new Set(chordPitchClasses(nextChord));
813
+ let count = 0;
814
+ for (let voice = 0; voice < prev.length && voice < cur.length; voice += 1) {
815
+ const from = prev[voice];
816
+ const to = cur[voice];
817
+ if (from === void 0 || to === void 0) {
818
+ continue;
819
+ }
820
+ const fromPc = pitchClassOf(from);
821
+ const motion = to - from;
822
+ if (fromPc === seventhPc && !nextPcs.has(fromPc) && motion !== -1 && motion !== -2) {
823
+ count += 1;
824
+ }
825
+ if (fromPc === leadingTonePc && tonicPc !== void 0 && nextPcs.has(tonicPc) && !nextPcs.has(fromPc) && motion !== 1) {
826
+ count += 1;
827
+ }
828
+ }
829
+ return count;
830
+ }
831
+ function violationCount(prev, cur) {
597
832
  let count = 0;
598
833
  for (let lower = 0; lower < cur.length; lower += 1) {
599
834
  for (let upper = lower + 1; upper < cur.length; upper += 1) {
@@ -607,16 +842,10 @@ function violationCount(prev, cur, maxSpacing) {
607
842
  if (createsParallelPerfect(prevUpper, curUpper, prevLower, curLower)) {
608
843
  count += 1;
609
844
  }
610
- if (createsVoiceCrossing(curUpper, curLower)) {
611
- count += 1;
612
- }
613
845
  if (upper === lower + 1) {
614
846
  if (createsVoiceOverlap(prevUpper, curUpper, prevLower, curLower)) {
615
847
  count += 1;
616
848
  }
617
- if (lower > 0 && exceedsSpacing(curUpper, curLower, maxSpacing)) {
618
- count += 1;
619
- }
620
849
  }
621
850
  }
622
851
  }
@@ -650,48 +879,70 @@ function voiceLeadingCost(from, to) {
650
879
  return total;
651
880
  }
652
881
  function nextVoicing(current, chord, opts) {
653
- const ranges = opts?.ranges !== void 0 || opts?.voices !== void 0 ? resolveRanges(opts) : current.map((pitch) => {
882
+ const derived = opts?.ranges === void 0 && opts?.voices === void 0;
883
+ if (derived) {
884
+ if (current.length === 0) {
885
+ throw new InvalidInputError("current must contain at least one pitch");
886
+ }
887
+ for (let index = 0; index < current.length; index += 1) {
888
+ assertFiniteNumber(current[index] ?? Number.NaN, `current[${index}]`);
889
+ }
890
+ }
891
+ const ranges = derived ? current.map((pitch) => {
654
892
  const centre = Math.min(127, Math.max(0, pitch));
655
893
  return { min: Math.max(0, centre - 12), max: Math.min(127, centre + 12) };
894
+ }) : resolveRanges(opts);
895
+ for (let index = 0; index < current.length; index += 1) {
896
+ assertFiniteNumber(current[index] ?? Number.NaN, `current[${index}]`);
897
+ }
898
+ if (current.length < ranges.length) {
899
+ throw new InvalidInputError(
900
+ `current has ${current.length} voices but the requested ranges require ${ranges.length}`
901
+ );
902
+ }
903
+ const source = current.length === ranges.length ? current : ranges.length === 1 ? [current[0] ?? Number.NaN] : ranges.map((_, index) => {
904
+ const sourceIndex = Math.round(index * (current.length - 1) / (ranges.length - 1));
905
+ return current[sourceIndex] ?? Number.NaN;
656
906
  });
657
- const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
658
- const candidates = enumerateVoicings(chord, ranges, maxSpacing);
907
+ const maxSpacing = resolveMaxSpacing(opts);
908
+ const candidates = enumerateVoicings(chord, ranges, maxSpacing, resolveMaxCandidates(opts));
909
+ const previousChord = opts?.previousChord;
659
910
  let best;
660
911
  let bestScore = Number.POSITIVE_INFINITY;
661
912
  for (const candidate of candidates) {
662
- const score = structuralPenalty(candidate, chord) + voiceLeadingCost(current, candidate) + VIOLATION_PENALTY * violationCount(current, candidate, maxSpacing);
913
+ const score = structuralPenalty(candidate, chord, opts?.key) + voiceLeadingCost(source, candidate) + VIOLATION_PENALTY * violationCount(source, candidate) + RESOLUTION_PENALTY * (previousChord === void 0 ? 0 : resolutionViolations(source, candidate, previousChord, chord, opts?.key));
663
914
  if (score < bestScore) {
664
915
  bestScore = score;
665
916
  best = candidate;
666
917
  }
667
918
  }
668
919
  if (best === void 0) {
669
- throw new Error("no voicing satisfies the given ranges");
920
+ throw new NoSolutionError("no voicing satisfies the given ranges");
670
921
  }
671
922
  return best;
672
923
  }
673
924
 
674
925
  // 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
- ];
926
+ var SATB_RANGES = Object.freeze([
927
+ Object.freeze({ min: 40, max: 60 }),
928
+ Object.freeze({ min: 48, max: 67 }),
929
+ Object.freeze({ min: 55, max: 74 }),
930
+ Object.freeze({ min: 60, max: 79 })
931
+ ]);
681
932
  var DERIVED_LOW = 40;
682
933
  var DERIVED_HIGH = 79;
683
934
  var DERIVED_SPAN = 19;
684
935
  function resolveRanges(opts) {
685
936
  if (opts?.ranges !== void 0) {
686
937
  if (opts.ranges.length === 0) {
687
- throw new Error("ranges must contain at least one voice range");
938
+ throw new InvalidInputError("ranges must contain at least one voice range");
688
939
  }
689
940
  assertGenerationBudget(opts.ranges.length, "voice ranges", 128);
690
941
  return opts.ranges.map((range, index) => {
691
942
  assertFiniteNumber(range.min, `ranges[${index}].min`);
692
943
  assertFiniteNumber(range.max, `ranges[${index}].max`);
693
944
  if (range.min > range.max) {
694
- throw new RangeError(`ranges[${index}].min must not exceed max`);
945
+ throw new InvalidInputError(`ranges[${index}].min must not exceed max`);
695
946
  }
696
947
  assertGenerationBudget(
697
948
  Math.floor(range.max) - Math.ceil(range.min) + 1,
@@ -718,16 +969,27 @@ function resolveRanges(opts) {
718
969
  }
719
970
  return ranges;
720
971
  }
721
- function voiceChord(chord, opts) {
722
- const ranges = resolveRanges(opts);
972
+ function resolveMaxSpacing(opts) {
723
973
  const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
724
974
  assertFiniteNumber(maxSpacing, "maxSpacing");
725
- if (maxSpacing < 0) throw new RangeError("maxSpacing must be non-negative");
726
- const candidates = enumerateVoicings(chord, ranges, maxSpacing);
975
+ if (maxSpacing < 0) {
976
+ throw new InvalidInputError("maxSpacing must be non-negative");
977
+ }
978
+ return maxSpacing;
979
+ }
980
+ function resolveMaxCandidates(opts) {
981
+ if (opts?.maxCandidates === void 0) return void 0;
982
+ assertPositiveInt(opts.maxCandidates, "maxCandidates", 1e6);
983
+ return opts.maxCandidates;
984
+ }
985
+ function voiceChord(chord, opts) {
986
+ const ranges = resolveRanges(opts);
987
+ const maxSpacing = resolveMaxSpacing(opts);
988
+ const candidates = enumerateVoicings(chord, ranges, maxSpacing, resolveMaxCandidates(opts));
727
989
  let best;
728
990
  let bestScore = Number.POSITIVE_INFINITY;
729
991
  for (const candidate of candidates) {
730
- let score = structuralPenalty(candidate, chord);
992
+ let score = structuralPenalty(candidate, chord, opts?.key);
731
993
  for (let i = 0; i < candidate.length; i += 1) {
732
994
  const pitch = candidate[i];
733
995
  const range = ranges[i];
@@ -742,39 +1004,68 @@ function voiceChord(chord, opts) {
742
1004
  }
743
1005
  }
744
1006
  if (best === void 0) {
745
- throw new Error("no voicing satisfies the given ranges");
1007
+ throw new NoSolutionError(
1008
+ `no voicing satisfies the given ranges for ${formatChordSymbol(chord)}`
1009
+ );
746
1010
  }
747
1011
  return best;
748
1012
  }
1013
+ function noSolutionAt(index, chord) {
1014
+ return new NoSolutionError(
1015
+ `no voicing satisfies the given ranges for ${formatChordSymbol(chord)} at index ${index}`,
1016
+ { at: index }
1017
+ );
1018
+ }
1019
+ function locate(index, chord, work) {
1020
+ try {
1021
+ return work();
1022
+ } catch (error) {
1023
+ if (error instanceof NoSolutionError) {
1024
+ throw noSolutionAt(index, chord);
1025
+ }
1026
+ throw error;
1027
+ }
1028
+ }
749
1029
  function voiceProgression(chords, opts) {
750
- assertGenerationBudget(chords.length * 4e3, "voiced progression search");
1030
+ assertGenerationBudget(chords.length, "voiced progression chords", opts?.budget);
751
1031
  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");
1032
+ const maxSpacing = resolveMaxSpacing(opts);
1033
+ const maxCandidates = resolveMaxCandidates(opts);
755
1034
  const result = [];
756
1035
  let prev;
757
- for (const chord of chords) {
1036
+ let prevChord;
1037
+ for (let index = 0; index < chords.length; index += 1) {
1038
+ const chord = chords[index];
1039
+ if (chord === void 0) {
1040
+ continue;
1041
+ }
758
1042
  if (prev === void 0) {
759
- prev = voiceChord(chord, opts);
1043
+ prev = locate(index, chord, () => voiceChord(chord, opts));
1044
+ prevChord = chord;
760
1045
  result.push(prev);
761
1046
  continue;
762
1047
  }
763
- const candidates = enumerateVoicings(chord, ranges, maxSpacing);
1048
+ const candidates = locate(
1049
+ index,
1050
+ chord,
1051
+ () => enumerateVoicings(chord, ranges, maxSpacing, maxCandidates)
1052
+ );
764
1053
  let best;
765
1054
  let bestScore = Number.POSITIVE_INFINITY;
766
1055
  for (const candidate of candidates) {
767
- const score = structuralPenalty(candidate, chord) + voiceLeadingCost(prev, candidate) + VIOLATION_PENALTY * violationCount(prev, candidate, maxSpacing);
1056
+ const unresolved = prevChord === void 0 ? 0 : resolutionViolations(prev, candidate, prevChord, chord, opts?.key);
1057
+ const score = structuralPenalty(candidate, chord, opts?.key) + voiceLeadingCost(prev, candidate) + VIOLATION_PENALTY * violationCount(prev, candidate) + RESOLUTION_PENALTY * unresolved;
768
1058
  if (score < bestScore) {
769
1059
  bestScore = score;
770
1060
  best = candidate;
771
1061
  }
772
1062
  }
773
1063
  if (best === void 0) {
774
- throw new Error("no voicing satisfies the given ranges");
1064
+ throw noSolutionAt(index, chord);
775
1065
  }
776
1066
  result.push(best);
777
1067
  prev = best;
1068
+ prevChord = chord;
778
1069
  }
779
1070
  return result;
780
1071
  }
@@ -793,7 +1084,7 @@ function dedupePcs(pcs) {
793
1084
  return result;
794
1085
  }
795
1086
  function lowestPitchAtOrAbove(pc, floor) {
796
- return floor + ((pc - pitchClass3(floor)) % 12 + 12) % 12;
1087
+ return floor + ((pc - pitchClassOf(floor)) % 12 + 12) % 12;
797
1088
  }
798
1089
  function nearestPc(target, pcs) {
799
1090
  let best = pcs[0] ?? target;
@@ -815,10 +1106,10 @@ function voiceChordStyled(chord, opts) {
815
1106
  assertFiniteNumber(opts.topNote, "styled voicing topNote");
816
1107
  }
817
1108
  const base = 12 * (octave + 1);
818
- const rootPc = pitchClass3(chord.rootPc);
819
- const bassPc = pitchClass3(chord.bassPc ?? chord.rootPc);
1109
+ const rootPc = pitchClassOf(chord.rootPc);
1110
+ const bassPc = pitchClassOf(chord.bassPc ?? chord.rootPc);
820
1111
  const omitRoot = style === "rootless" || opts?.rootless === true;
821
- let order = dedupePcs(chord.intervals.map((interval) => pitchClass3(chord.rootPc + interval)));
1112
+ let order = dedupePcs(chord.intervals.map((interval) => pitchClassOf(chord.rootPc + interval)));
822
1113
  const bassIndex = order.indexOf(bassPc);
823
1114
  if (bassIndex > 0) {
824
1115
  order = [...order.slice(bassIndex), ...order.slice(0, bassIndex)];
@@ -840,7 +1131,7 @@ function voiceChordStyled(chord, opts) {
840
1131
  return [];
841
1132
  }
842
1133
  if (opts?.topNote !== void 0) {
843
- const target = order.includes(pitchClass3(opts.topNote)) ? pitchClass3(opts.topNote) : nearestPc(pitchClass3(opts.topNote), order);
1134
+ const target = order.includes(pitchClassOf(opts.topNote)) ? pitchClassOf(opts.topNote) : nearestPc(pitchClassOf(opts.topNote), order);
844
1135
  const targetIndex = order.indexOf(target);
845
1136
  order = [...order.slice(targetIndex + 1), ...order.slice(0, targetIndex + 1)];
846
1137
  }
@@ -850,7 +1141,7 @@ function voiceChordStyled(chord, opts) {
850
1141
  if (prev === void 0) {
851
1142
  prev = lowestPitchAtOrAbove(pc, base);
852
1143
  } else {
853
- let delta = ((pc - pitchClass3(prev)) % 12 + 12) % 12;
1144
+ let delta = ((pc - pitchClassOf(prev)) % 12 + 12) % 12;
854
1145
  if (delta === 0) {
855
1146
  delta = 12;
856
1147
  }
@@ -864,7 +1155,7 @@ function voiceChordStyled(chord, opts) {
864
1155
  stack[stack.length - 3] = (stack[stack.length - 3] ?? 0) - 12;
865
1156
  }
866
1157
  if (chord.bassPc !== void 0) {
867
- const bassIndexInStack = stack.findIndex((pitch) => pitchClass3(pitch) === bassPc);
1158
+ const bassIndexInStack = stack.findIndex((pitch) => pitchClassOf(pitch) === bassPc);
868
1159
  if (bassIndexInStack >= 0) {
869
1160
  const otherLowest = Math.min(
870
1161
  ...stack.filter((_pitch, index) => index !== bassIndexInStack),
@@ -875,7 +1166,15 @@ function voiceChordStyled(chord, opts) {
875
1166
  }
876
1167
  }
877
1168
  }
878
- return stack.sort((a, b) => a - b);
1169
+ const voicing = stack.sort((a, b) => a - b);
1170
+ const lowest = voicing[0] ?? 0;
1171
+ const highest = voicing[voicing.length - 1] ?? 0;
1172
+ if (lowest < 0 || highest > 127) {
1173
+ throw new InvalidInputError(
1174
+ `styled voicing at octave ${octave} spans MIDI ${lowest}..${highest}, outside 0..127`
1175
+ );
1176
+ }
1177
+ return voicing;
879
1178
  }
880
1179
 
881
1180
  export {
@@ -890,6 +1189,7 @@ export {
890
1189
  spellPitchClasses,
891
1190
  spellChordFromRoot,
892
1191
  spellChord,
1192
+ spellPitch,
893
1193
  noteNames,
894
1194
  parseChordSymbol,
895
1195
  formatChordSymbol,
@@ -901,4 +1201,4 @@ export {
901
1201
  nextVoicing,
902
1202
  voiceChordStyled
903
1203
  };
904
- //# sourceMappingURL=chunk-C5XYKUVG.js.map
1204
+ //# sourceMappingURL=chunk-7EONYVI5.js.map