@libraz/libcantus 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +27 -65
  2. package/README_ja.md +28 -66
  3. package/dist/analyze/index.cjs +11 -7
  4. package/dist/analyze/index.cjs.map +1 -1
  5. package/dist/analyze/index.d.cts +79 -144
  6. package/dist/analyze/index.d.ts +79 -144
  7. package/dist/analyze/index.js +20 -16
  8. package/dist/chunk-35NBV2FT.cjs +145 -0
  9. package/dist/chunk-35NBV2FT.cjs.map +1 -0
  10. package/dist/{chunk-UADYAYAT.cjs → chunk-3R3QVKY5.cjs} +1353 -580
  11. package/dist/chunk-3R3QVKY5.cjs.map +1 -0
  12. package/dist/{chunk-WWURE4XT.cjs → chunk-76ZBTRG2.cjs} +624 -729
  13. package/dist/chunk-76ZBTRG2.cjs.map +1 -0
  14. package/dist/{chunk-FVSI36UV.js → chunk-7KG3RTYS.js} +197 -110
  15. package/dist/chunk-7KG3RTYS.js.map +1 -0
  16. package/dist/{chunk-VTI6FOLB.js → chunk-AGOVW7U6.js} +709 -133
  17. package/dist/chunk-AGOVW7U6.js.map +1 -0
  18. package/dist/{chunk-E3F4AHAH.cjs → chunk-CGBADIO5.cjs} +221 -134
  19. package/dist/chunk-CGBADIO5.cjs.map +1 -0
  20. package/dist/{chunk-6XH6QZYQ.cjs → chunk-DSMJRXCZ.cjs} +1455 -849
  21. package/dist/chunk-DSMJRXCZ.cjs.map +1 -0
  22. package/dist/{chunk-PKFNUHDY.js → chunk-FBH75DAY.js} +1218 -612
  23. package/dist/chunk-FBH75DAY.js.map +1 -0
  24. package/dist/chunk-H2KJHBW4.js +3329 -0
  25. package/dist/chunk-H2KJHBW4.js.map +1 -0
  26. package/dist/chunk-HPVSH7SF.cjs +2294 -0
  27. package/dist/chunk-HPVSH7SF.cjs.map +1 -0
  28. package/dist/{chunk-MXO72PLY.cjs → chunk-HWHGOVO6.cjs} +21 -19
  29. package/dist/chunk-HWHGOVO6.cjs.map +1 -0
  30. package/dist/chunk-HYJJ2V2I.js +2294 -0
  31. package/dist/chunk-HYJJ2V2I.js.map +1 -0
  32. package/dist/{chunk-AFM2ULJH.cjs → chunk-J7FHVZII.cjs} +1567 -1067
  33. package/dist/chunk-J7FHVZII.cjs.map +1 -0
  34. package/dist/chunk-K6OZXIR6.js +1 -0
  35. package/dist/{chunk-H3VORETI.js → chunk-M5RYMAFI.js} +487 -592
  36. package/dist/chunk-M5RYMAFI.js.map +1 -0
  37. package/dist/chunk-NB25P3EH.js +363 -0
  38. package/dist/chunk-NB25P3EH.js.map +1 -0
  39. package/dist/{chunk-TTCLJX6D.js → chunk-NFJ4P6ZZ.js} +10 -8
  40. package/dist/chunk-NFJ4P6ZZ.js.map +1 -0
  41. package/dist/chunk-NM7UAMC3.cjs +457 -0
  42. package/dist/chunk-NM7UAMC3.cjs.map +1 -0
  43. package/dist/chunk-PM4XJE2Y.cjs +363 -0
  44. package/dist/chunk-PM4XJE2Y.cjs.map +1 -0
  45. package/dist/chunk-PUKJP2Z5.js +145 -0
  46. package/dist/chunk-PUKJP2Z5.js.map +1 -0
  47. package/dist/{chunk-VOHZB45S.js → chunk-Q6LFSAKV.js} +1321 -821
  48. package/dist/chunk-Q6LFSAKV.js.map +1 -0
  49. package/dist/{chunk-INHVXP6Z.js → chunk-RFIBF2RR.js} +1203 -430
  50. package/dist/chunk-RFIBF2RR.js.map +1 -0
  51. package/dist/{chunk-Q2WGKJNN.cjs → chunk-S5BKZZGM.cjs} +699 -123
  52. package/dist/chunk-S5BKZZGM.cjs.map +1 -0
  53. package/dist/chunk-SGLEQVJH.cjs +3329 -0
  54. package/dist/chunk-SGLEQVJH.cjs.map +1 -0
  55. package/dist/chunk-UGLFDG7L.cjs +1 -0
  56. package/dist/chunk-UGLFDG7L.cjs.map +1 -0
  57. package/dist/{chunk-YH3BIKHH.js → chunk-UMWZ4ZET.js} +146 -49
  58. package/dist/chunk-UMWZ4ZET.js.map +1 -0
  59. package/dist/{index-szBv3496.d.ts → coerce-DP0vYu-x.d.ts} +286 -199
  60. package/dist/{index-C_8o--VE.d.cts → coerce-DRERzdve.d.cts} +286 -199
  61. package/dist/core/index.cjs +9 -5
  62. package/dist/core/index.cjs.map +1 -1
  63. package/dist/core/index.d.cts +122 -108
  64. package/dist/core/index.d.ts +122 -108
  65. package/dist/core/index.js +32 -28
  66. package/dist/generate/index.cjs +8 -8
  67. package/dist/generate/index.d.cts +168 -84
  68. package/dist/generate/index.d.ts +168 -84
  69. package/dist/generate/index.js +7 -7
  70. package/dist/{index-CGpRv5sl.d.cts → index-9eoQs2C1.d.ts} +34 -19
  71. package/dist/{index-CCNJRG-P.d.cts → index-B9QLziDe.d.cts} +293 -56
  72. package/dist/{index-Dzr13q3d.d.cts → index-BBWNZS0f.d.cts} +135 -58
  73. package/dist/{index-9wcMhG73.d.ts → index-BHIUyjhZ.d.ts} +293 -56
  74. package/dist/{index-BGFW-8P2.d.cts → index-BJgP97N1.d.ts} +196 -114
  75. package/dist/{index-CunYKLiu.d.ts → index-BL5N2Oyz.d.cts} +1 -1
  76. package/dist/{index-Cj49F892.d.ts → index-BMweUlm5.d.cts} +63 -23
  77. package/dist/{index-CgHqxdps.d.ts → index-BNSM-DSk.d.cts} +196 -114
  78. package/dist/{index-D57QRbVn.d.ts → index-BRMbPkHE.d.ts} +480 -254
  79. package/dist/{index-Chbst3Jx.d.ts → index-CJBBiAmU.d.ts} +135 -58
  80. package/dist/{index-BkGmlyI3.d.cts → index-CL4b4gUQ.d.ts} +63 -23
  81. package/dist/{index-B9MNZboQ.d.ts → index-Cy15oTvh.d.cts} +52 -22
  82. package/dist/{index-B9MNZboQ.d.cts → index-Cy15oTvh.d.ts} +52 -22
  83. package/dist/{index-BO6xE9YU.d.cts → index-DGCNKAUr.d.cts} +480 -254
  84. package/dist/{index-B4o55NDr.d.cts → index-Dw2DP7vm.d.ts} +1 -1
  85. package/dist/{index-G1uwGGWQ.d.ts → index-cHvbYgJI.d.cts} +34 -19
  86. package/dist/index.cjs +42 -14
  87. package/dist/index.cjs.map +1 -1
  88. package/dist/index.d.cts +14 -14
  89. package/dist/index.d.ts +14 -14
  90. package/dist/index.js +82 -54
  91. package/dist/{masks-DYbE485J.d.cts → masks-Cc7-MzzY.d.ts} +49 -2
  92. package/dist/{masks-DYbE485J.d.ts → masks-pADfTOiD.d.cts} +49 -2
  93. package/dist/model/index.cjs +19 -12
  94. package/dist/model/index.cjs.map +1 -1
  95. package/dist/model/index.d.cts +504 -182
  96. package/dist/model/index.d.ts +504 -182
  97. package/dist/model/index.js +18 -11
  98. package/dist/{positional-Cptlj-xs.d.ts → positional-DJgs6ltD.d.ts} +85 -8
  99. package/dist/{positional-DvH_sd8u.d.cts → positional-DMZer-FM.d.cts} +85 -8
  100. package/dist/theory/index.cjs +35 -6
  101. package/dist/theory/index.cjs.map +1 -1
  102. package/dist/theory/index.d.cts +257 -62
  103. package/dist/theory/index.d.ts +257 -62
  104. package/dist/theory/index.js +41 -12
  105. package/package.json +7 -5
  106. package/dist/chunk-2LCPM3EG.js +0 -3224
  107. package/dist/chunk-2LCPM3EG.js.map +0 -1
  108. package/dist/chunk-6XH6QZYQ.cjs.map +0 -1
  109. package/dist/chunk-6XPTBE7Y.cjs +0 -49
  110. package/dist/chunk-6XPTBE7Y.cjs.map +0 -1
  111. package/dist/chunk-AFM2ULJH.cjs.map +0 -1
  112. package/dist/chunk-B6XNKWZV.cjs +0 -3224
  113. package/dist/chunk-B6XNKWZV.cjs.map +0 -1
  114. package/dist/chunk-E3F4AHAH.cjs.map +0 -1
  115. package/dist/chunk-ELCTZUJJ.js +0 -379
  116. package/dist/chunk-ELCTZUJJ.js.map +0 -1
  117. package/dist/chunk-FVSI36UV.js.map +0 -1
  118. package/dist/chunk-H3VORETI.js.map +0 -1
  119. package/dist/chunk-H42UHPYW.js +0 -1577
  120. package/dist/chunk-H42UHPYW.js.map +0 -1
  121. package/dist/chunk-HFPS7JFD.js +0 -1
  122. package/dist/chunk-INHVXP6Z.js.map +0 -1
  123. package/dist/chunk-K4ZJUGF4.cjs +0 -379
  124. package/dist/chunk-K4ZJUGF4.cjs.map +0 -1
  125. package/dist/chunk-KNJEKLKV.js +0 -49
  126. package/dist/chunk-KNJEKLKV.js.map +0 -1
  127. package/dist/chunk-MXO72PLY.cjs.map +0 -1
  128. package/dist/chunk-NLEU4QSE.cjs +0 -360
  129. package/dist/chunk-NLEU4QSE.cjs.map +0 -1
  130. package/dist/chunk-OE6WK2Y2.cjs +0 -1577
  131. package/dist/chunk-OE6WK2Y2.cjs.map +0 -1
  132. package/dist/chunk-PKFNUHDY.js.map +0 -1
  133. package/dist/chunk-Q2WGKJNN.cjs.map +0 -1
  134. package/dist/chunk-RHIJ3S4I.cjs +0 -1
  135. package/dist/chunk-RHIJ3S4I.cjs.map +0 -1
  136. package/dist/chunk-TTCLJX6D.js.map +0 -1
  137. package/dist/chunk-UADYAYAT.cjs.map +0 -1
  138. package/dist/chunk-VOHZB45S.js.map +0 -1
  139. package/dist/chunk-VTI6FOLB.js.map +0 -1
  140. package/dist/chunk-WWURE4XT.cjs.map +0 -1
  141. package/dist/chunk-YH3BIKHH.js.map +0 -1
  142. /package/dist/{chunk-HFPS7JFD.js.map → chunk-K6OZXIR6.js.map} +0 -0
@@ -1,5 +1,6 @@
1
1
  import {
2
- NAMED_SCALES,
2
+ appliedDominantTarget,
3
+ assertChord,
3
4
  chordPitchClasses,
4
5
  chordQualities,
5
6
  chordToneRole,
@@ -11,29 +12,39 @@ import {
11
12
  createsVoiceOverlap,
12
13
  exceedsSpacing,
13
14
  formatChordSymbol,
15
+ hasDominantSonority,
14
16
  isAugmentedMelodicInterval,
17
+ isDescendingStep,
15
18
  isForbiddenMelodicLeap,
19
+ isFrustratedLeadingTone,
20
+ isFunctioningLeadingTone,
16
21
  isLeadingToneResolution,
22
+ isNeapolitanChordOf,
23
+ leadingTonePcOf,
17
24
  makeChord,
18
- namedScaleMask,
19
25
  noteNames,
20
- requireScaleMask,
26
+ resolveKey,
21
27
  scaleTonesInDegreeOrder,
28
+ seventhPcOf,
22
29
  simpleIntervalNumber,
23
30
  spellChord,
24
31
  spellPitch,
32
+ spellPitchClass,
25
33
  spellScale,
26
- spelledKeyOf,
27
34
  toChordData,
28
35
  toKeyScale
29
- } from "./chunk-INHVXP6Z.js";
36
+ } from "./chunk-RFIBF2RR.js";
30
37
  import {
38
+ BEAT_EPS,
31
39
  InvalidInputError,
32
40
  NoSolutionError,
41
+ assertArray,
33
42
  assertFiniteNumber,
34
43
  assertGenerationBudget,
35
44
  assertInteger,
45
+ assertOptions,
36
46
  assertPositiveInt,
47
+ describeRejected,
37
48
  diatonicLetterOf,
38
49
  formatNote,
39
50
  noteToMidi,
@@ -44,7 +55,7 @@ import {
44
55
  toNoteData,
45
56
  toSpelledInterval,
46
57
  transposeByInterval
47
- } from "./chunk-VTI6FOLB.js";
58
+ } from "./chunk-AGOVW7U6.js";
48
59
 
49
60
  // src/theory/transposition/index.ts
50
61
  var TRANSPOSING_INSTRUMENTS = Object.freeze({
@@ -74,12 +85,16 @@ var TRANSPOSING_INSTRUMENTS = Object.freeze({
74
85
  contrabassoon: "-P8",
75
86
  guitar: "-P8"
76
87
  });
88
+ var PROFILE_NAME_ALIASES = Object.freeze({
89
+ "guitar (drop D)": "guitar"
90
+ });
77
91
  function instrumentTransposition(instrument) {
78
92
  if (typeof instrument !== "string") {
79
93
  return toSpelledInterval(instrument);
80
94
  }
81
- if (Object.hasOwn(TRANSPOSING_INSTRUMENTS, instrument)) {
82
- return parseInterval(TRANSPOSING_INSTRUMENTS[instrument]);
95
+ const name = Object.hasOwn(PROFILE_NAME_ALIASES, instrument) ? PROFILE_NAME_ALIASES[instrument] : instrument;
96
+ if (Object.hasOwn(TRANSPOSING_INSTRUMENTS, name)) {
97
+ return parseInterval(TRANSPOSING_INSTRUMENTS[name]);
83
98
  }
84
99
  try {
85
100
  return parseInterval(instrument);
@@ -93,16 +108,13 @@ function instrumentTransposition(instrument) {
93
108
  }
94
109
  }
95
110
  function reversedInterval(step) {
96
- const reversed = {
111
+ return {
97
112
  number: step.number,
98
113
  quality: step.quality,
99
114
  // Negating a zero span yields -0, which compares unequal to 0 under Object.is.
100
- semitones: step.semitones === 0 ? 0 : -step.semitones
115
+ semitones: step.semitones === 0 ? 0 : -step.semitones,
116
+ descending: !step.descending
101
117
  };
102
- if (!(step.descending ?? step.semitones < 0)) {
103
- reversed.descending = true;
104
- }
105
- return reversed;
106
118
  }
107
119
  function toSoundingPitch(note, instrument) {
108
120
  return transposeByInterval(toNoteData(note), instrumentTransposition(instrument));
@@ -114,311 +126,6 @@ function toWrittenPitch(note, instrument) {
114
126
  );
115
127
  }
116
128
 
117
- // src/theory/chordscale/index.ts
118
- function popcount12(mask) {
119
- let count = 0;
120
- for (let n = 0; n < 12; n += 1) {
121
- count += mask >> n & 1;
122
- }
123
- return count;
124
- }
125
- function maskHasPitchClass(mask, scaleRootPc, pc) {
126
- const offset = pitchClassOf(pc - scaleRootPc);
127
- return (mask >> offset & 1) === 1;
128
- }
129
- function scaleMatchesChord(chordPcs, scaleMask, scaleRootPc) {
130
- for (const pc of chordPcs) {
131
- if (!maskHasPitchClass(scaleMask, scaleRootPc, pc)) {
132
- return false;
133
- }
134
- }
135
- return true;
136
- }
137
- var ALIASED_SCALE_NAMES = /* @__PURE__ */ new Set(["major", "naturalMinor"]);
138
- var HEPTATONIC_SIZE = 7;
139
- var SCALE_PREFERENCE = [
140
- "ionian",
141
- "major",
142
- "mixolydian",
143
- "dorian",
144
- "lydian",
145
- "aeolian",
146
- "naturalMinor",
147
- "phrygian",
148
- "locrian"
149
- ];
150
- var IDIOMATIC_SCALES = {
151
- aug: ["wholeTone"],
152
- m7b5: ["locrian"],
153
- // The diminished scale played over a diminished seventh is the whole-half
154
- // octatonic. Its half-whole rotation belongs to the dominant it colours: over
155
- // a diminished seventh every one of its non-chord tones lands a semitone
156
- // above a chord tone, so it offers no tension at all.
157
- dim7: ["octatonicWholeHalf"],
158
- minMaj7: ["melodicMinor"]
159
- };
160
- function conventionalRank(name) {
161
- const index = SCALE_PREFERENCE.indexOf(name);
162
- return index < 0 ? SCALE_PREFERENCE.length : index;
163
- }
164
- function idiomaticRank(chord, name) {
165
- const choices = IDIOMATIC_SCALES[chord.quality];
166
- if (choices === void 0) {
167
- return 0;
168
- }
169
- const index = choices.indexOf(name);
170
- return index < 0 ? choices.length + 1 : index;
171
- }
172
- function isAlteredDominant(chord, scaleName) {
173
- return chord.quality === "dom7" && scaleName === "altered";
174
- }
175
- function chordScales(chord) {
176
- const data = toChordData(chord);
177
- const chordPcs = chordPitchClasses(data);
178
- const rootPc = pitchClassOf(data.rootPc);
179
- const ranked = [];
180
- const seenMasks = /* @__PURE__ */ new Set();
181
- for (const name of Object.keys(NAMED_SCALES)) {
182
- if (name === "chromatic" || ALIASED_SCALE_NAMES.has(name)) {
183
- continue;
184
- }
185
- const mask = namedScaleMask(name);
186
- if (mask === void 0 || seenMasks.has(mask)) {
187
- continue;
188
- }
189
- seenMasks.add(mask);
190
- if (scaleMatchesChord(chordPcs, mask, rootPc) || isAlteredDominant(data, name)) {
191
- const size = popcount12(mask);
192
- ranked.push({
193
- name,
194
- extra: size - chordPcs.length,
195
- size,
196
- avoid: avoidNotes(data, name).length
197
- });
198
- }
199
- }
200
- if (ranked.length === 0) {
201
- if (NAMED_SCALES.chromatic !== void 0) {
202
- return [{ name: "chromatic", rootPc }];
203
- }
204
- return [];
205
- }
206
- const preferHeptatonic = chordPcs.length <= 3;
207
- ranked.sort((a, b) => {
208
- const idiomaticA = idiomaticRank(data, a.name);
209
- const idiomaticB = idiomaticRank(data, b.name);
210
- if (idiomaticA !== idiomaticB) {
211
- return idiomaticA - idiomaticB;
212
- }
213
- if (preferHeptatonic && a.size === HEPTATONIC_SIZE !== (b.size === HEPTATONIC_SIZE)) {
214
- return a.size === HEPTATONIC_SIZE ? -1 : 1;
215
- }
216
- if (a.extra !== b.extra) {
217
- return a.extra - b.extra;
218
- }
219
- const rankA = conventionalRank(a.name);
220
- const rankB = conventionalRank(b.name);
221
- if (rankA !== rankB) {
222
- return rankA - rankB;
223
- }
224
- if (a.avoid !== b.avoid) {
225
- return a.avoid - b.avoid;
226
- }
227
- return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
228
- });
229
- return ranked.map((entry) => ({ name: entry.name, rootPc }));
230
- }
231
- function suspendedTone(chordPcs, rootPc) {
232
- const sounds = (semitones) => chordPcs.includes(pitchClassOf(rootPc + semitones));
233
- if (sounds(3) || sounds(4)) {
234
- return void 0;
235
- }
236
- if (sounds(5)) {
237
- return pitchClassOf(rootPc + 5);
238
- }
239
- if (sounds(2)) {
240
- return pitchClassOf(rootPc + 2);
241
- }
242
- return void 0;
243
- }
244
- function avoidNotes(chord, scaleName, opts = {}) {
245
- const mask = requireScaleMask(scaleName);
246
- const data = toChordData(chord);
247
- const rootPc = pitchClassOf(data.rootPc);
248
- const chordPcs = chordPitchClasses(data);
249
- const alteredDominant = isAlteredDominant(data, scaleName);
250
- if (!scaleMatchesChord(chordPcs, mask, rootPc) && !alteredDominant) {
251
- return [];
252
- }
253
- const chordSet = new Set(chordPcs);
254
- const suspended = suspendedTone(chordPcs, rootPc);
255
- const displacedThird = suspended === void 0 ? void 0 : pitchClassOf(suspended - 1);
256
- const melodic = opts.use === "melodic";
257
- const semitoneAboveRoot = pitchClassOf(rootPc + 1);
258
- const avoid = [];
259
- for (let pc = 0; pc < 12; pc += 1) {
260
- if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc)) {
261
- continue;
262
- }
263
- if (melodic && pc !== semitoneAboveRoot) {
264
- continue;
265
- }
266
- if (!alteredDominant && (chordSet.has(pitchClassOf(pc - 1)) || pc === displacedThird)) {
267
- avoid.push(pc);
268
- }
269
- }
270
- return avoid.sort((a, b) => a - b);
271
- }
272
- var MINOR_RESOLUTION_TENSIONS = [1, 3, 8];
273
- function resolvesToMinorTonic(chord, chordPcs, target) {
274
- if (target === void 0) {
275
- return false;
276
- }
277
- const rootPc = pitchClassOf(chord.rootPc);
278
- const sounds = (semitones) => chordPcs.includes(pitchClassOf(rootPc + semitones));
279
- if (!sounds(4) || !sounds(10)) {
280
- return false;
281
- }
282
- const targetRootPc = pitchClassOf(target.rootPc);
283
- if (targetRootPc !== pitchClassOf(rootPc + 5)) {
284
- return false;
285
- }
286
- return chordPitchClasses(target).includes(pitchClassOf(targetRootPc + 3));
287
- }
288
- function availableTensions(chord, scaleName, opts = {}) {
289
- const mask = requireScaleMask(scaleName);
290
- const data = toChordData(chord);
291
- const rootPc = pitchClassOf(data.rootPc);
292
- const chordPcs = chordPitchClasses(data);
293
- const alteredDominant = isAlteredDominant(data, scaleName);
294
- if (!scaleMatchesChord(chordPcs, mask, rootPc) && !alteredDominant) {
295
- return [];
296
- }
297
- const chordSet = new Set(chordPcs);
298
- const avoidSet = alteredDominant ? /* @__PURE__ */ new Set() : new Set(avoidNotes(data, scaleName));
299
- const functional = resolvesToMinorTonic(
300
- data,
301
- chordPcs,
302
- opts.resolvesTo === void 0 ? void 0 : toChordData(opts.resolvesTo)
303
- ) ? new Set(MINOR_RESOLUTION_TENSIONS.map((semitones) => pitchClassOf(rootPc + semitones))) : void 0;
304
- const tensions = [];
305
- for (let pc = 0; pc < 12; pc += 1) {
306
- if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc)) {
307
- continue;
308
- }
309
- if (avoidSet.has(pc) && functional?.has(pc) !== true) {
310
- continue;
311
- }
312
- tensions.push(pc);
313
- }
314
- return tensions.sort((a, b) => a - b);
315
- }
316
- function chordScaleReport(chord, limit) {
317
- if (limit !== void 0) {
318
- assertPositiveInt(limit, "chord-scale report limit");
319
- }
320
- const data = toChordData(chord);
321
- const matches = chordScales(data);
322
- const chosen = limit === void 0 ? matches : matches.slice(0, limit);
323
- return chosen.map((match) => {
324
- const harmonic = avoidNotes(data, match.name);
325
- const melodic = new Set(avoidNotes(data, match.name, { use: "melodic" }));
326
- return {
327
- name: match.name,
328
- rootPc: match.rootPc,
329
- avoid: harmonic.filter((pc) => melodic.has(pc)),
330
- passing: harmonic.filter((pc) => !melodic.has(pc)),
331
- tensions: availableTensions(data, match.name)
332
- };
333
- });
334
- }
335
- var RANK_PENALTY = 0.01;
336
- function scalePitchClassSet(name, rootPc) {
337
- const mask = namedScaleMask(name);
338
- const pcs = /* @__PURE__ */ new Set();
339
- if (mask === void 0) {
340
- return pcs;
341
- }
342
- for (let n = 0; n < 12; n += 1) {
343
- if ((mask >> n & 1) === 1) {
344
- pcs.add(pitchClassOf(rootPc + n));
345
- }
346
- }
347
- return pcs;
348
- }
349
- function symmetricDifferenceSize(a, b) {
350
- let count = 0;
351
- for (const pc of a) {
352
- if (!b.has(pc)) {
353
- count += 1;
354
- }
355
- }
356
- for (const pc of b) {
357
- if (!a.has(pc)) {
358
- count += 1;
359
- }
360
- }
361
- return count;
362
- }
363
- function scalesForChanges(chords) {
364
- assertGenerationBudget(chords.length, "chord-scale changes");
365
- if (chords.length === 0) {
366
- return [];
367
- }
368
- const candidateLists = chords.map((chord) => chordScales(chord));
369
- const pcSets = candidateLists.map(
370
- (candidates) => candidates.map((match) => scalePitchClassSet(match.name, match.rootPc))
371
- );
372
- const first = candidateLists[0] ?? [];
373
- const dp = [first.map((_, j) => j * RANK_PENALTY)];
374
- const back = [first.map(() => -1)];
375
- for (let i = 1; i < candidateLists.length; i += 1) {
376
- const candidates = candidateLists[i] ?? [];
377
- const prevCosts = dp[i - 1] ?? [];
378
- const prevSets = pcSets[i - 1] ?? [];
379
- const curSets = pcSets[i] ?? [];
380
- const stageCosts = [];
381
- const stageBack = [];
382
- for (let j = 0; j < candidates.length; j += 1) {
383
- let bestCost = Number.POSITIVE_INFINITY;
384
- let bestPrev = -1;
385
- for (let k = 0; k < prevCosts.length; k += 1) {
386
- const transition = symmetricDifferenceSize(
387
- prevSets[k] ?? /* @__PURE__ */ new Set(),
388
- curSets[j] ?? /* @__PURE__ */ new Set()
389
- );
390
- const cost = (prevCosts[k] ?? 0) + transition;
391
- if (cost < bestCost) {
392
- bestCost = cost;
393
- bestPrev = k;
394
- }
395
- }
396
- stageCosts.push(bestCost + j * RANK_PENALTY);
397
- stageBack.push(bestPrev);
398
- }
399
- dp.push(stageCosts);
400
- back.push(stageBack);
401
- }
402
- const lastCosts = dp[dp.length - 1] ?? [];
403
- let bestFinal = 0;
404
- for (let j = 1; j < lastCosts.length; j += 1) {
405
- if ((lastCosts[j] ?? Number.POSITIVE_INFINITY) < (lastCosts[bestFinal] ?? 0)) {
406
- bestFinal = j;
407
- }
408
- }
409
- const chosenIndices = new Array(candidateLists.length);
410
- let current = bestFinal;
411
- for (let i = candidateLists.length - 1; i >= 0; i -= 1) {
412
- chosenIndices[i] = current;
413
- current = back[i]?.[current] ?? -1;
414
- }
415
- return chords.map((chord, i) => {
416
- const candidates = candidateLists[i] ?? [];
417
- const scale = candidates[chosenIndices[i] ?? 0] ?? { name: "chromatic", rootPc: chord.rootPc };
418
- return { chord, scale };
419
- });
420
- }
421
-
422
129
  // src/theory/figured-bass/index.ts
423
130
  var MIN_FIGURE_NUMBER = 2;
424
131
  var MAX_FIGURE_NUMBER = 9;
@@ -433,7 +140,7 @@ var DOUBLE_ACCIDENTALS = {
433
140
  var SINGLE_ACCIDENTALS = {
434
141
  "#": "sharp",
435
142
  "\u266F": "sharp",
436
- "+": "sharp",
143
+ "+": "raised",
437
144
  b: "flat",
438
145
  "\u266D": "flat",
439
146
  n: "natural",
@@ -441,31 +148,28 @@ var SINGLE_ACCIDENTALS = {
441
148
  };
442
149
  var FIGURE_SEPARATORS = /* @__PURE__ */ new Set([" ", " ", ",", "/"]);
443
150
  var MOTION_MARKS = /* @__PURE__ */ new Set(["-", "\u2013"]);
444
- var ACCIDENTAL_GLYPHS = {
445
- 1: "#",
446
- 2: "##",
447
- "-1": "b",
448
- "-2": "bb"
449
- };
151
+ var PRINTED_ACCIDENTALS = [
152
+ ["natural", "n", 0],
153
+ ["sharp", "#", 1],
154
+ ["doubleSharp", "##", 2],
155
+ ["flat", "b", -1],
156
+ ["doubleFlat", "bb", -2]
157
+ ];
158
+ var PRINTED_ALTERS = new Map(
159
+ PRINTED_ACCIDENTALS.map(([accidental, , alter]) => [accidental, alter])
160
+ );
161
+ var ACCIDENTAL_GLYPHS = new Map(
162
+ PRINTED_ACCIDENTALS.map(([, glyph, alter]) => [alter, glyph])
163
+ );
450
164
  function invalidFigures(figures, detail) {
451
- return new InvalidInputError(`figured bass ${JSON.stringify(figures)} ${detail}`);
165
+ return new InvalidInputError(`figured bass ${describeRejected(figures)} ${detail}`);
452
166
  }
453
167
  function alteredBy(diatonicAlter, accidental) {
454
- switch (accidental) {
455
- case "sharp":
456
- return diatonicAlter + 1;
457
- case "doubleSharp":
458
- return diatonicAlter + 2;
459
- case "flat":
460
- return diatonicAlter - 1;
461
- case "doubleFlat":
462
- return diatonicAlter - 2;
463
- // A natural sign names the natural note, whatever the key would have given.
464
- case "natural":
465
- return 0;
466
- default:
467
- return diatonicAlter;
168
+ const printed = PRINTED_ALTERS.get(accidental);
169
+ if (printed !== void 0) {
170
+ return printed;
468
171
  }
172
+ return accidental === "raised" ? diatonicAlter + 1 : diatonicAlter;
469
173
  }
470
174
  function readFigure(text, start) {
471
175
  let index = start;
@@ -575,12 +279,12 @@ function tertianQualityIndex() {
575
279
  }
576
280
  var TERTIAN_QUALITIES = tertianQualityIndex();
577
281
  function keyLetterAlters(key) {
578
- const size = scaleTonesInDegreeOrder(key).length;
282
+ const size = scaleTonesInDegreeOrder(key.scale).length;
579
283
  if (size !== 7) {
580
284
  throw new InvalidInputError(`figured bass requires a heptatonic key (received ${size} tones)`);
581
285
  }
582
286
  const alters = [];
583
- for (const note of spellScale(spelledKeyOf(key).tonic, key)) {
287
+ for (const note of spellScale(key.tonic, key.scale)) {
584
288
  alters[diatonicLetterOf(note.letter)] = note.alter;
585
289
  }
586
290
  return alters;
@@ -610,7 +314,7 @@ function realizeSonority(bass, resolved, text, alters) {
610
314
  const tone = tones.find((candidate) => diatonicLetterOf(candidate.letter - root.letter) === step * 2);
611
315
  if (tone === void 0) {
612
316
  throw new NoSolutionError(
613
- `figured bass ${JSON.stringify(text)} sounds ${noteNames(tones).join(" ")}, which does not stack in thirds`
317
+ `figured bass ${describeRejected(text)} sounds ${noteNames(tones).join(" ")}, which does not stack in thirds`
614
318
  );
615
319
  }
616
320
  stack.push(pitchClassOf(noteToPitchClass(tone) - rootPc));
@@ -618,7 +322,7 @@ function realizeSonority(bass, resolved, text, alters) {
618
322
  const quality = TERTIAN_QUALITIES.get(stack.join(","));
619
323
  if (quality === void 0) {
620
324
  throw new NoSolutionError(
621
- `figured bass ${JSON.stringify(text)} sounds ${noteNames(tones).join(" ")}, which no chord quality names`
325
+ `figured bass ${describeRejected(text)} sounds ${noteNames(tones).join(" ")}, which no chord quality names`
622
326
  );
623
327
  }
624
328
  const chord = makeChord(rootPc, quality, noteToPitchClass(bass));
@@ -631,8 +335,7 @@ function figuredBassRealization(bass, figures, key) {
631
335
  throw new InvalidInputError(`figured bass must be a string; received ${typeof figures}`);
632
336
  }
633
337
  const bassData = toNoteData(bass);
634
- const scale = toKeyScale(key);
635
- const alters = keyLetterAlters(scale);
338
+ const alters = keyLetterAlters(resolveKey(key));
636
339
  noteToPitchClass(bassData);
637
340
  const bassNote = bareNote(bassData);
638
341
  const text = figures.trim();
@@ -655,9 +358,9 @@ function realizeFiguredBass(bass, figures, key) {
655
358
  }
656
359
  function figuredBassOf(chord, key) {
657
360
  const data = toChordData(chord);
658
- const scale = toKeyScale(key);
659
- const alters = keyLetterAlters(scale);
660
- const tones = spellChord(data, spelledKeyOf(scale).tonic, scale);
361
+ const resolved = resolveKey(key);
362
+ const alters = keyLetterAlters(resolved);
363
+ const tones = spellChord(data, resolved.tonic, resolved.scale);
661
364
  const bassPc = pitchClassOf(data.bassPc ?? data.rootPc);
662
365
  const bass = tones.find((tone) => noteToPitchClass(tone) === bassPc);
663
366
  if (bass === void 0) {
@@ -679,20 +382,17 @@ function figuredBassOf(chord, key) {
679
382
  }
680
383
  const accidentals = /* @__PURE__ */ new Map();
681
384
  for (const [number, tone] of written) {
682
- const alteration = tone.alter - (alters[diatonicLetterOf(tone.letter)] ?? 0);
683
- if (alteration === 0) {
385
+ if (tone.alter === (alters[diatonicLetterOf(tone.letter)] ?? 0)) {
684
386
  continue;
685
387
  }
686
- const glyph = ACCIDENTAL_GLYPHS[alteration];
388
+ const glyph = ACCIDENTAL_GLYPHS.get(tone.alter);
687
389
  if (glyph === void 0) {
688
- throw new NoSolutionError(
689
- `no figure names an interval altered by ${alteration} semitones from the key`
690
- );
390
+ throw new NoSolutionError(`no accidental writes a note altered by ${tone.alter} semitones`);
691
391
  }
692
392
  accidentals.set(number, glyph);
693
393
  }
694
394
  const figures = [.../* @__PURE__ */ new Set([...digits, ...accidentals.keys()])].sort((a, b) => b - a).map((number) => `${accidentals.get(number) ?? ""}${number}`).join("");
695
- assertRealizesAs(bass, figures, scale, tones);
395
+ assertRealizesAs(bass, figures, resolved, tones);
696
396
  return figures;
697
397
  }
698
398
  function pitchClassesOf(notes) {
@@ -700,7 +400,7 @@ function pitchClassesOf(notes) {
700
400
  }
701
401
  function assertRealizesAs(bass, figures, key, tones) {
702
402
  const noFigures = (detail) => new NoSolutionError(
703
- `no figured bass names ${noteNames(tones).join(" ")} over ${noteNames([bass])[0]}: the figures ${JSON.stringify(figures)} ${detail}`
403
+ `no figured bass names ${noteNames(tones).join(" ")} over ${noteNames([bass])[0]}: the figures ${describeRejected(figures)} ${detail}`
704
404
  );
705
405
  let realized;
706
406
  try {
@@ -715,29 +415,112 @@ function assertRealizesAs(bass, figures, key, tones) {
715
415
  }
716
416
  }
717
417
 
718
- // src/theory/voicing/tendency.ts
719
- function leadingTonePcOf(key) {
720
- return pitchClassOf(key.rootPc - 1);
418
+ // src/theory/partwriting/cross-relation.ts
419
+ function resolvesAsAppliedDominant(chord, next, key) {
420
+ if (next === void 0 || !hasDominantSonority(chord)) {
421
+ return false;
422
+ }
423
+ const target = appliedDominantTarget(chord.rootPc + 5, key);
424
+ return target !== null && pitchClassOf(next.rootPc) === target.rootPc;
721
425
  }
722
- var LEADING_TONE_QUALITIES = /* @__PURE__ */ new Set(["dim", "dim7", "m7b5"]);
723
- function isFunctioningLeadingTone(chord, key) {
724
- const role = chordToneRole(leadingTonePcOf(key), chord);
725
- if (role === "third") {
726
- return pitchClassOf(chord.rootPc) === pitchClassOf(key.rootPc + 7);
426
+ function soundsAugmentedSixth(moment, key) {
427
+ const bass = moment.notes[0];
428
+ if (bass === void 0 || noteToPitchClass(bass) !== pitchClassOf(key.rootPc + 8)) {
429
+ return false;
727
430
  }
728
- if (role === "root") {
729
- return LEADING_TONE_QUALITIES.has(chord.quality);
431
+ return moment.notes.some((note, voice) => {
432
+ if (voice === 0) {
433
+ return false;
434
+ }
435
+ const interval = spelledInterval(bass, note);
436
+ return (interval.number - 1) % 7 + 1 === 6 && pitchClassOf(interval.semitones) === 10;
437
+ });
438
+ }
439
+ function isChromaticHarmony(moment, next, key) {
440
+ return resolvesAsAppliedDominant(moment.chord, next, key) || isNeapolitanChordOf(moment.chord, key) || soundsAugmentedSixth(moment, key);
441
+ }
442
+ function approachedByStep(before, moment, voice) {
443
+ const earlier = before?.[voice];
444
+ const later = moment[voice];
445
+ if (earlier === void 0 || later === void 0) {
446
+ return false;
730
447
  }
731
- return false;
448
+ return spelledInterval(earlier, later).number === 2;
449
+ }
450
+ function isOuterVoice(voice, notes) {
451
+ return voice === 0 || voice === notes.length - 1;
732
452
  }
733
- function seventhPcOf(chord) {
734
- for (const interval of chord.intervals) {
735
- const pc = pitchClassOf(chord.rootPc + interval);
736
- if (chordToneRole(pc, chord) === "seventh") {
737
- return pc;
453
+ function crossRelations(from, to, key, context = {}) {
454
+ const found = [];
455
+ const chromatic = isChromaticHarmony(from, to.chord, key) || isChromaticHarmony(to, context.after, key);
456
+ if (chromatic) {
457
+ return found;
458
+ }
459
+ const reported = /* @__PURE__ */ new Set();
460
+ for (let first = 0; first < from.notes.length; first += 1) {
461
+ for (let second = 0; second < to.notes.length; second += 1) {
462
+ if (first === second) {
463
+ continue;
464
+ }
465
+ const earlier = from.notes[first];
466
+ const later = to.notes[second];
467
+ if (earlier === void 0 || later === void 0) {
468
+ continue;
469
+ }
470
+ const letter = diatonicLetterOf(earlier.letter);
471
+ if (letter !== diatonicLetterOf(later.letter) || earlier.alter === later.alter) {
472
+ continue;
473
+ }
474
+ if (approachedByStep(context.before, from.notes, first) || approachedByStep(from.notes, to.notes, second) || !isOuterVoice(first, from.notes) && !isOuterVoice(second, to.notes)) {
475
+ continue;
476
+ }
477
+ const pair = `${letter}:${Math.min(first, second)}:${Math.max(first, second)}`;
478
+ if (reported.has(pair)) {
479
+ continue;
480
+ }
481
+ reported.add(pair);
482
+ found.push({ earlierVoice: first, laterVoice: second, earlier, later });
738
483
  }
739
484
  }
740
- return void 0;
485
+ return found;
486
+ }
487
+
488
+ // src/theory/voicing/tendency.ts
489
+ function spellingTable(keyLike, chord) {
490
+ const { tonic, scale: key } = resolveKey(keyLike);
491
+ const notes = [];
492
+ const bases = [];
493
+ for (let pc = 0; pc < 12; pc += 1) {
494
+ const note = chord === void 0 ? spellPitchClass(pc, tonic, key) : spellPitchClass(pc, tonic, key, { chord });
495
+ notes.push(note);
496
+ bases.push(noteToMidi({ letter: note.letter, alter: note.alter, octave: 0 }));
497
+ }
498
+ return { notes, bases };
499
+ }
500
+ function spelledAt(table, pitch) {
501
+ const pc = pitchClassOf(pitch);
502
+ const note = table.notes[pc];
503
+ const base = table.bases[pc];
504
+ if (note === void 0 || base === void 0) {
505
+ return void 0;
506
+ }
507
+ return { letter: note.letter, alter: note.alter, octave: (pitch - base) / 12 };
508
+ }
509
+ function movesByAugmentedInterval(from, fromPitch, to, toPitch) {
510
+ const earlier = spelledAt(from, fromPitch);
511
+ const later = spelledAt(to, toPitch);
512
+ if (earlier === void 0 || later === void 0) {
513
+ return false;
514
+ }
515
+ return isAugmentedMelodicInterval(earlier, later);
516
+ }
517
+ function movesByDescendingStep(from, fromPitch, to, toPitch) {
518
+ const earlier = spelledAt(from, fromPitch);
519
+ const later = spelledAt(to, toPitch);
520
+ if (earlier === void 0 || later === void 0) {
521
+ return false;
522
+ }
523
+ return isDescendingStep(spelledInterval(earlier, later));
741
524
  }
742
525
 
743
526
  // src/theory/voicing/internal.ts
@@ -750,8 +533,11 @@ var UNISON_PENALTY = 100;
750
533
  var LEADING_TONE_DOUBLING_PENALTY = 200;
751
534
  var RESOLUTION_PENALTY = 200;
752
535
  var DEFAULT_MAX_CANDIDATES = 4e3;
536
+ var MAX_LOOKAHEAD_PAIRS = 1e6;
753
537
  var SEARCH_NODES_PER_CANDIDATE = 16;
754
- var HIDDEN_PERFECT_PENALTY = 6;
538
+ var HIDDEN_PERFECT_PENALTY = 100;
539
+ var CROSS_RELATION_PENALTY = HIDDEN_PERFECT_PENALTY;
540
+ var AUGMENTED_MELODIC_PENALTY = VIOLATION_PENALTY;
755
541
  var INITIAL_CANDIDATE_CAPACITY = 512;
756
542
  function createCandidateBuffer() {
757
543
  return { pitches: new Int32Array(0), count: 0, voices: 0 };
@@ -881,11 +667,12 @@ function resolutionTables(prevChord, nextChord, key) {
881
667
  seventhPc: seventhPcOf(prevChord) ?? -1,
882
668
  leadingTonePc: key !== void 0 && isFunctioningLeadingTone(prevChord, key) ? leadingTonePcOf(key) : -1,
883
669
  tonicPc: key === void 0 ? -1 : pitchClassOf(key.rootPc),
884
- nextHas
670
+ nextHas,
671
+ nextChord
885
672
  };
886
673
  }
887
- function resolutionViolations(tables, prev, prevOffset, cur, curOffset, voices) {
888
- const { seventhPc, leadingTonePc, tonicPc, nextHas } = tables;
674
+ function resolutionViolations(tables, spelling, prev, prevOffset, cur, curOffset, voices) {
675
+ const { seventhPc, leadingTonePc, tonicPc, nextHas, nextChord } = tables;
889
676
  let count = 0;
890
677
  for (let voice = 0; voice < voices; voice += 1) {
891
678
  const from = prev[prevOffset + voice];
@@ -895,10 +682,12 @@ function resolutionViolations(tables, prev, prevOffset, cur, curOffset, voices)
895
682
  }
896
683
  const fromPc = pitchClassOf(from);
897
684
  const motion = to - from;
898
- if (fromPc === seventhPc && nextHas[fromPc] === 0 && motion !== -1 && motion !== -2) {
685
+ const resolved = spelling === void 0 ? motion === -1 || motion === -2 : movesByDescendingStep(spelling.from, from, spelling.to, to);
686
+ if (fromPc === seventhPc && nextHas[fromPc] === 0 && !resolved) {
899
687
  count += 1;
900
688
  }
901
- if (fromPc === leadingTonePc && tonicPc >= 0 && nextHas[tonicPc] === 1 && nextHas[fromPc] === 0 && motion !== 1) {
689
+ const inner = voice > 0 && voice < voices - 1;
690
+ if (fromPc === leadingTonePc && tonicPc >= 0 && nextHas[tonicPc] === 1 && nextHas[fromPc] === 0 && motion !== 1 && !(inner && isFrustratedLeadingTone(from, to, nextChord))) {
902
691
  count += 1;
903
692
  }
904
693
  }
@@ -914,16 +703,76 @@ function leadingCost(prev, prevOffset, cur, curOffset, voices) {
914
703
  }
915
704
  total += Math.abs(b - a);
916
705
  }
917
- if (voices >= 2) {
918
- const bassPrev = prev[prevOffset];
919
- const bassCur = cur[curOffset];
920
- const sopPrev = prev[prevOffset + voices - 1];
921
- const sopCur = cur[curOffset + voices - 1];
922
- if (bassPrev !== void 0 && bassCur !== void 0 && sopPrev !== void 0 && sopCur !== void 0 && createsHiddenParallelPerfect(bassPrev, bassCur, sopPrev, sopCur)) {
923
- total += HIDDEN_PERFECT_PENALTY;
706
+ return total;
707
+ }
708
+ function createsOuterHiddenPerfect(prev, prevOffset, cur, curOffset, voices) {
709
+ if (voices < 2) {
710
+ return false;
711
+ }
712
+ const bassPrev = prev[prevOffset];
713
+ const bassCur = cur[curOffset];
714
+ const sopPrev = prev[prevOffset + voices - 1];
715
+ const sopCur = cur[curOffset + voices - 1];
716
+ if (bassPrev === void 0 || bassCur === void 0 || sopPrev === void 0 || sopCur === void 0) {
717
+ return false;
718
+ }
719
+ return createsHiddenParallelPerfect(sopPrev, sopCur, bassPrev, bassCur);
720
+ }
721
+ function augmentedMelodicCount(spelling, prev, prevOffset, cur, curOffset, voices) {
722
+ let count = 0;
723
+ for (let voice = 0; voice < voices; voice += 1) {
724
+ const from = prev[prevOffset + voice];
725
+ const to = cur[curOffset + voice];
726
+ if (from === void 0 || to === void 0) {
727
+ continue;
728
+ }
729
+ if (movesByAugmentedInterval(spelling.from, from, spelling.to, to)) {
730
+ count += 1;
924
731
  }
925
732
  }
926
- return total;
733
+ return count;
734
+ }
735
+ function spellRow(table, pitches, offset, voices) {
736
+ const notes = [];
737
+ for (let voice = 0; voice < voices; voice += 1) {
738
+ const pitch = pitches[offset + voice];
739
+ const note = pitch === void 0 ? void 0 : spelledAt(table, pitch);
740
+ if (note !== void 0) {
741
+ notes.push(note);
742
+ }
743
+ }
744
+ return notes;
745
+ }
746
+ function contradictsALetter(spelling, prev, prevOffset, cur, curOffset, voices) {
747
+ for (let voice = 0; voice < voices; voice += 1) {
748
+ const from = prev[prevOffset + voice];
749
+ const earlier = from === void 0 ? void 0 : spelling.from.notes[pitchClassOf(from)];
750
+ if (earlier === void 0) {
751
+ continue;
752
+ }
753
+ for (let other = 0; other < voices; other += 1) {
754
+ if (other === voice) {
755
+ continue;
756
+ }
757
+ const to = cur[curOffset + other];
758
+ const later = to === void 0 ? void 0 : spelling.to.notes[pitchClassOf(to)];
759
+ if (later !== void 0 && earlier.alter !== later.alter && diatonicLetterOf(earlier.letter) === diatonicLetterOf(later.letter)) {
760
+ return true;
761
+ }
762
+ }
763
+ }
764
+ return false;
765
+ }
766
+ function crossRelationCount(spelling, tables, prev, prevOffset, cur, curOffset, voices) {
767
+ if (!contradictsALetter(spelling, prev, prevOffset, cur, curOffset, voices)) {
768
+ return 0;
769
+ }
770
+ return crossRelations(
771
+ { notes: spellRow(spelling.from, prev, prevOffset, voices), chord: tables.fromChord },
772
+ { notes: spellRow(spelling.to, cur, curOffset, voices), chord: tables.toChord },
773
+ tables.key,
774
+ { after: tables.afterChord }
775
+ ).length;
927
776
  }
928
777
  function violationCount(prev, prevOffset, cur, curOffset, voices) {
929
778
  let count = 0;
@@ -948,6 +797,24 @@ function violationCount(prev, prevOffset, cur, curOffset, voices) {
948
797
  }
949
798
  return count;
950
799
  }
800
+ function moveScoring(prevChord, chord, key, nextChord) {
801
+ return {
802
+ structure: structuralTables(chord, key?.scale),
803
+ resolution: prevChord === void 0 ? void 0 : resolutionTables(prevChord, chord, key?.scale),
804
+ spelling: key === void 0 ? void 0 : { from: spellingTable(key, prevChord), to: spellingTable(key, chord) },
805
+ crossRelation: prevChord === void 0 || key === void 0 ? void 0 : { fromChord: prevChord, toChord: chord, afterChord: nextChord, key: key.scale }
806
+ };
807
+ }
808
+ function violationWeight(scoring, prev, prevOffset, cur, curOffset, voices) {
809
+ const { resolution, spelling, crossRelation } = scoring;
810
+ const unresolved = resolution === void 0 ? 0 : resolutionViolations(resolution, spelling, prev, prevOffset, cur, curOffset, voices);
811
+ const augmented = spelling === void 0 ? 0 : augmentedMelodicCount(spelling, prev, prevOffset, cur, curOffset, voices);
812
+ const contradictions = spelling === void 0 || crossRelation === void 0 ? 0 : crossRelationCount(spelling, crossRelation, prev, prevOffset, cur, curOffset, voices);
813
+ return VIOLATION_PENALTY * violationCount(prev, prevOffset, cur, curOffset, voices) + RESOLUTION_PENALTY * unresolved + AUGMENTED_MELODIC_PENALTY * augmented + CROSS_RELATION_PENALTY * contradictions + (createsOuterHiddenPerfect(prev, prevOffset, cur, curOffset, voices) ? HIDDEN_PERFECT_PENALTY : 0);
814
+ }
815
+ function moveScore(scoring, prev, prevOffset, cur, curOffset, voices) {
816
+ return structuralPenalty(scoring.structure, cur, curOffset, voices) + leadingCost(prev, prevOffset, cur, curOffset, voices) + violationWeight(scoring, prev, prevOffset, cur, curOffset, voices);
817
+ }
951
818
 
952
819
  // src/theory/voicing/satb.ts
953
820
  var SATB_RANGES = Object.freeze([
@@ -960,6 +827,7 @@ var DERIVED_LOW = 40;
960
827
  var DERIVED_HIGH = 79;
961
828
  var DERIVED_SPAN = 19;
962
829
  function resolveRanges(opts) {
830
+ assertOptions(opts, "opts");
963
831
  if (opts?.ranges !== void 0) {
964
832
  if (opts.ranges.length === 0) {
965
833
  throw new InvalidInputError("ranges must contain at least one voice range");
@@ -1004,8 +872,8 @@ function resolveMaxSpacing(opts) {
1004
872
  }
1005
873
  return maxSpacing;
1006
874
  }
1007
- function resolveKey(opts) {
1008
- return opts?.key === void 0 ? void 0 : toKeyScale(opts.key);
875
+ function resolvedKeyOf(opts) {
876
+ return opts?.key === void 0 ? void 0 : resolveKey(opts.key);
1009
877
  }
1010
878
  function resolvePreviousChord(opts) {
1011
879
  return opts?.previousChord === void 0 ? void 0 : toChordData(opts.previousChord);
@@ -1015,26 +883,30 @@ function resolveMaxCandidates(opts) {
1015
883
  assertPositiveInt(opts.maxCandidates, "maxCandidates", 1e6);
1016
884
  return opts.maxCandidates;
1017
885
  }
886
+ function centeringPenalty(ranges, pitches, offset, voices) {
887
+ let penalty = 0;
888
+ for (let voice = 0; voice < voices; voice += 1) {
889
+ const pitch = pitches[offset + voice];
890
+ const range = ranges[voice];
891
+ if (pitch === void 0 || range === void 0) {
892
+ continue;
893
+ }
894
+ penalty += Math.abs(pitch - (range.min + range.max) / 2);
895
+ }
896
+ return penalty;
897
+ }
1018
898
  function voiceChord(chord, opts) {
1019
899
  const data = toChordData(chord);
1020
900
  const ranges = resolveRanges(opts);
1021
901
  const maxSpacing = resolveMaxSpacing(opts);
1022
902
  const candidates = enumerateVoicings(data, ranges, maxSpacing, resolveMaxCandidates(opts));
1023
- const tables = structuralTables(data, resolveKey(opts));
903
+ const { structure } = moveScoring(void 0, data, resolvedKeyOf(opts));
1024
904
  const { pitches, voices } = candidates;
1025
905
  let bestOffset = -1;
1026
906
  let bestScore = Number.POSITIVE_INFINITY;
1027
907
  for (let index = 0; index < candidates.count; index += 1) {
1028
908
  const offset = index * voices;
1029
- let score = structuralPenalty(tables, pitches, offset, voices);
1030
- for (let i = 0; i < voices; i += 1) {
1031
- const pitch = pitches[offset + i];
1032
- const range = ranges[i];
1033
- if (pitch === void 0 || range === void 0) {
1034
- continue;
1035
- }
1036
- score += Math.abs(pitch - (range.min + range.max) / 2);
1037
- }
909
+ const score = structuralPenalty(structure, pitches, offset, voices) + centeringPenalty(ranges, pitches, offset, voices);
1038
910
  if (score < bestScore) {
1039
911
  bestScore = score;
1040
912
  bestOffset = offset;
@@ -1063,42 +935,66 @@ function locate(index, chord, work) {
1063
935
  throw error;
1064
936
  }
1065
937
  }
938
+ function successorWeight(next, cur, curOffset, voices) {
939
+ const { candidates, scoring } = next;
940
+ const { pitches } = candidates;
941
+ let bestScore = Number.POSITIVE_INFINITY;
942
+ let bestWeight = 0;
943
+ for (let candidate = 0; candidate < candidates.count; candidate += 1) {
944
+ const offset = candidate * voices;
945
+ const weight = violationWeight(scoring, cur, curOffset, pitches, offset, voices);
946
+ const score = weight + structuralPenalty(scoring.structure, pitches, offset, voices) + leadingCost(cur, curOffset, pitches, offset, voices);
947
+ if (score < bestScore) {
948
+ bestScore = score;
949
+ bestWeight = weight;
950
+ }
951
+ }
952
+ return bestWeight;
953
+ }
1066
954
  function voiceProgression(chords, opts) {
1067
- assertGenerationBudget(chords.length, "voiced progression chords", opts?.budget);
1068
- const data = chords.map((chord) => toChordData(chord));
1069
- const key = resolveKey(opts);
955
+ const progression = assertArray(chords, "chords");
956
+ assertGenerationBudget(progression.length, "voiced progression chords", opts?.budget);
957
+ const data = progression.map((chord) => toChordData(chord));
958
+ const key = resolvedKeyOf(opts);
1070
959
  const ranges = resolveRanges(opts);
1071
960
  const maxSpacing = resolveMaxSpacing(opts);
1072
961
  const maxCandidates = resolveMaxCandidates(opts);
1073
962
  const result = [];
1074
- const buffer = createCandidateBuffer();
963
+ const first = data[0];
964
+ if (first === void 0) {
965
+ return result;
966
+ }
967
+ let current = locate(
968
+ 0,
969
+ first,
970
+ () => enumerateVoicings(first, ranges, maxSpacing, maxCandidates, createCandidateBuffer())
971
+ );
972
+ let spare = createCandidateBuffer();
973
+ let scoring = moveScoring(void 0, first, key);
1075
974
  let prev;
1076
- let prevChord;
1077
- for (let index = 0; index < data.length; index += 1) {
1078
- const chord = data[index];
1079
- if (chord === void 0) {
1080
- continue;
1081
- }
1082
- if (prev === void 0) {
1083
- prev = locate(index, chord, () => voiceChord(chord, opts));
1084
- prevChord = chord;
1085
- result.push(prev);
1086
- continue;
1087
- }
1088
- const candidates = locate(
1089
- index,
1090
- chord,
1091
- () => enumerateVoicings(chord, ranges, maxSpacing, maxCandidates, buffer)
1092
- );
1093
- const { pitches, voices } = candidates;
1094
- const structure = structuralTables(chord, key);
1095
- const resolution = prevChord === void 0 ? void 0 : resolutionTables(prevChord, chord, key);
975
+ for (const [index, chord] of data.entries()) {
976
+ const nextChord = data[index + 1];
977
+ const lookahead = nextChord === void 0 ? void 0 : {
978
+ candidates: locate(
979
+ index + 1,
980
+ nextChord,
981
+ () => enumerateVoicings(nextChord, ranges, maxSpacing, maxCandidates, spare)
982
+ ),
983
+ // The chord after the one being reached goes in too: it is what
984
+ // says whether the chromatic tone the next chord introduces is the
985
+ // one an applied dominant is written with.
986
+ scoring: moveScoring(chord, nextChord, key, data[index + 2])
987
+ };
988
+ const { pitches, voices, count } = current;
989
+ const weighed = lookahead !== void 0 && count * lookahead.candidates.count <= MAX_LOOKAHEAD_PAIRS ? lookahead : void 0;
1096
990
  let bestOffset = -1;
1097
991
  let bestScore = Number.POSITIVE_INFINITY;
1098
- for (let candidate = 0; candidate < candidates.count; candidate += 1) {
992
+ for (let candidate = 0; candidate < count; candidate += 1) {
1099
993
  const offset = candidate * voices;
1100
- const unresolved = resolution === void 0 ? 0 : resolutionViolations(resolution, prev, 0, pitches, offset, voices);
1101
- const score = structuralPenalty(structure, pitches, offset, voices) + leadingCost(prev, 0, pitches, offset, voices) + VIOLATION_PENALTY * violationCount(prev, 0, pitches, offset, voices) + RESOLUTION_PENALTY * unresolved;
994
+ let score = prev === void 0 ? structuralPenalty(scoring.structure, pitches, offset, voices) + centeringPenalty(ranges, pitches, offset, voices) : moveScore(scoring, prev, 0, pitches, offset, voices);
995
+ if (weighed !== void 0) {
996
+ score += successorWeight(weighed, pitches, offset, voices);
997
+ }
1102
998
  if (score < bestScore) {
1103
999
  bestScore = score;
1104
1000
  bestOffset = offset;
@@ -1110,28 +1006,41 @@ function voiceProgression(chords, opts) {
1110
1006
  const best = [...pitches.subarray(bestOffset, bestOffset + voices)];
1111
1007
  result.push(best);
1112
1008
  prev = best;
1113
- prevChord = chord;
1009
+ if (lookahead !== void 0) {
1010
+ spare = current;
1011
+ current = lookahead.candidates;
1012
+ scoring = lookahead.scoring;
1013
+ }
1114
1014
  }
1115
1015
  return result;
1116
1016
  }
1117
1017
 
1118
1018
  // src/theory/voicing/leading.ts
1019
+ function assertVoicing(pitches, name) {
1020
+ const voices = assertArray(pitches, name);
1021
+ for (let index = 0; index < voices.length; index += 1) {
1022
+ assertFiniteNumber(voices[index] ?? Number.NaN, `${name}[${index}]`);
1023
+ }
1024
+ return voices;
1025
+ }
1119
1026
  function voiceLeadingCost(from, to) {
1120
- if (from.length !== to.length) {
1027
+ const previous = assertVoicing(from, "from");
1028
+ const next = assertVoicing(to, "to");
1029
+ if (previous.length !== next.length) {
1121
1030
  return Number.POSITIVE_INFINITY;
1122
1031
  }
1123
- return leadingCost(from, 0, to, 0, from.length);
1032
+ return leadingCost(previous, 0, next, 0, previous.length);
1124
1033
  }
1125
1034
  function nextVoicing(current, chord, opts) {
1035
+ assertOptions(opts, "opts");
1036
+ assertVoicing(current, "current");
1126
1037
  const data = toChordData(chord);
1127
1038
  const derived = opts?.ranges === void 0 && opts?.voices === void 0;
1128
1039
  if (derived) {
1129
1040
  if (current.length === 0) {
1130
1041
  throw new InvalidInputError("current must contain at least one pitch");
1131
1042
  }
1132
- for (let index = 0; index < current.length; index += 1) {
1133
- assertFiniteNumber(current[index] ?? Number.NaN, `current[${index}]`);
1134
- }
1043
+ assertVoicing(current, "current");
1135
1044
  }
1136
1045
  const ranges = derived ? resolveRanges({
1137
1046
  ranges: current.map((pitch) => {
@@ -1139,9 +1048,7 @@ function nextVoicing(current, chord, opts) {
1139
1048
  return { min: Math.max(0, centre - 12), max: Math.min(127, centre + 12) };
1140
1049
  })
1141
1050
  }) : resolveRanges(opts);
1142
- for (let index = 0; index < current.length; index += 1) {
1143
- assertFiniteNumber(current[index] ?? Number.NaN, `current[${index}]`);
1144
- }
1051
+ assertVoicing(current, "current");
1145
1052
  if (current.length < ranges.length) {
1146
1053
  throw new InvalidInputError(
1147
1054
  `current has ${current.length} voices but the requested ranges require ${ranges.length}`
@@ -1153,16 +1060,14 @@ function nextVoicing(current, chord, opts) {
1153
1060
  });
1154
1061
  const maxSpacing = resolveMaxSpacing(opts);
1155
1062
  const candidates = enumerateVoicings(data, ranges, maxSpacing, resolveMaxCandidates(opts));
1156
- const key = resolveKey(opts);
1157
- const previousChord = resolvePreviousChord(opts);
1158
- const structure = structuralTables(data, key);
1159
- const resolution = previousChord === void 0 ? void 0 : resolutionTables(previousChord, data, key);
1063
+ const key = resolvedKeyOf(opts);
1064
+ const scoring = moveScoring(resolvePreviousChord(opts), data, key);
1160
1065
  const { pitches, voices } = candidates;
1161
1066
  let bestOffset = -1;
1162
1067
  let bestScore = Number.POSITIVE_INFINITY;
1163
1068
  for (let candidate = 0; candidate < candidates.count; candidate += 1) {
1164
1069
  const offset = candidate * voices;
1165
- const score = structuralPenalty(structure, pitches, offset, voices) + leadingCost(source, 0, pitches, offset, voices) + VIOLATION_PENALTY * violationCount(source, 0, pitches, offset, voices) + RESOLUTION_PENALTY * (resolution === void 0 ? 0 : resolutionViolations(resolution, source, 0, pitches, offset, voices));
1070
+ const score = moveScore(scoring, source, 0, pitches, offset, voices);
1166
1071
  if (score < bestScore) {
1167
1072
  bestScore = score;
1168
1073
  bestOffset = offset;
@@ -1176,6 +1081,12 @@ function nextVoicing(current, chord, opts) {
1176
1081
 
1177
1082
  // src/theory/voicing/styled.ts
1178
1083
  var DEFAULT_STYLE_OCTAVE = 4;
1084
+ function dropPositionFromTop(style) {
1085
+ if (style === "drop2") {
1086
+ return 2;
1087
+ }
1088
+ return style === "drop3" ? 3 : 0;
1089
+ }
1179
1090
  function dedupePcs(pcs) {
1180
1091
  const seen = /* @__PURE__ */ new Set();
1181
1092
  const result = [];
@@ -1202,7 +1113,9 @@ function nearestPc(target, pcs) {
1202
1113
  }
1203
1114
  return best;
1204
1115
  }
1205
- function voiceChordStyled(chord, opts) {
1116
+ function voiceChordStyled(given, opts) {
1117
+ assertOptions(opts, "opts");
1118
+ const chord = toChordData(given);
1206
1119
  const style = opts?.style ?? "close";
1207
1120
  const octave = opts?.octave ?? DEFAULT_STYLE_OCTAVE;
1208
1121
  assertInteger(octave, "styled voicing octave", -10, 10);
@@ -1215,6 +1128,7 @@ function voiceChordStyled(chord, opts) {
1215
1128
  const omitRoot = style === "rootless" || opts?.rootless === true;
1216
1129
  let order = dedupePcs(chord.intervals.map((interval) => pitchClassOf(chord.rootPc + interval)));
1217
1130
  const bassIndex = order.indexOf(bassPc);
1131
+ const bassIsChordTone = bassIndex >= 0;
1218
1132
  if (bassIndex > 0) {
1219
1133
  order = [...order.slice(bassIndex), ...order.slice(0, bassIndex)];
1220
1134
  } else if (bassIndex < 0) {
@@ -1239,6 +1153,15 @@ function voiceChordStyled(chord, opts) {
1239
1153
  const targetIndex = order.indexOf(target);
1240
1154
  order = [...order.slice(targetIndex + 1), ...order.slice(0, targetIndex + 1)];
1241
1155
  }
1156
+ const dropFromTop = dropPositionFromTop(style);
1157
+ const dropsToNamedBass = dropFromTop > 0 && bassIsChordTone && chord.bassPc !== void 0 && order.length > dropFromTop;
1158
+ if (dropsToNamedBass) {
1159
+ const bassAt = order.indexOf(bassPc);
1160
+ if (bassAt >= 0) {
1161
+ const shift = ((bassAt - (order.length - dropFromTop)) % order.length + order.length) % order.length;
1162
+ order = [...order.slice(shift), ...order.slice(0, shift)];
1163
+ }
1164
+ }
1242
1165
  const stack = [];
1243
1166
  let prev;
1244
1167
  for (const pc of order) {
@@ -1253,10 +1176,10 @@ function voiceChordStyled(chord, opts) {
1253
1176
  }
1254
1177
  stack.push(prev);
1255
1178
  }
1256
- if (style === "drop2" && stack.length >= 2) {
1257
- stack[stack.length - 2] = (stack[stack.length - 2] ?? 0) - 12;
1258
- } else if (style === "drop3" && stack.length >= 3) {
1259
- stack[stack.length - 3] = (stack[stack.length - 3] ?? 0) - 12;
1179
+ const stackedFloor = bassIsChordTone ? 0 : 1;
1180
+ if (dropFromTop > 0 && stack.length - dropFromTop > stackedFloor) {
1181
+ const dropIndex = stack.length - dropFromTop;
1182
+ stack[dropIndex] = (stack[dropIndex] ?? 0) - 12;
1260
1183
  }
1261
1184
  if (chord.bassPc !== void 0) {
1262
1185
  const bassIndexInStack = stack.findIndex((pitch) => pitchClassOf(pitch) === bassPc);
@@ -1290,17 +1213,26 @@ var INTERVAL_WORDS = [
1290
1213
  "fifth",
1291
1214
  "sixth",
1292
1215
  "seventh",
1293
- "octave"
1216
+ "octave",
1217
+ "ninth",
1218
+ "tenth",
1219
+ "eleventh",
1220
+ "twelfth",
1221
+ "thirteenth",
1222
+ "fourteenth",
1223
+ "fifteenth"
1294
1224
  ];
1295
1225
  function intervalWord(numberValue) {
1296
- return INTERVAL_WORDS[numberValue - 1] ?? "interval";
1226
+ return INTERVAL_WORDS[numberValue - 1] ?? `${numberValue}th`;
1227
+ }
1228
+ function intervalArticle(word) {
1229
+ return /^[aeiou]/.test(word) ? "an" : "a";
1297
1230
  }
1298
1231
  function violation(kind, voices, fromIndex, toIndex, rationale) {
1299
1232
  return { kind, voices, fromIndex, toIndex, rationale };
1300
1233
  }
1301
1234
 
1302
1235
  // src/theory/partwriting/species.ts
1303
- var EPS = 1e-9;
1304
1236
  var SPECIES_RATIO = { 1: 1, 2: 2, 3: 4, 4: 2, 5: 0 };
1305
1237
  var SPECIES_NUMBERS = Object.keys(SPECIES_RATIO).map(Number);
1306
1238
  var MIN_FLORID_VALUE = 0.125;
@@ -1324,7 +1256,7 @@ function direction(interval) {
1324
1256
  function sameNote(a, b) {
1325
1257
  return a.letter === b.letter && a.alter === b.alter && a.octave === b.octave;
1326
1258
  }
1327
- function isPassedThrough(entries, position) {
1259
+ function isPassedThrough(entries, position, species) {
1328
1260
  const previous = entries[position - 1];
1329
1261
  const current = entries[position];
1330
1262
  const next = entries[position + 1];
@@ -1336,9 +1268,10 @@ function isPassedThrough(entries, position) {
1336
1268
  if (!isStep(incoming) || !isStep(outgoing)) {
1337
1269
  return false;
1338
1270
  }
1339
- const passing = direction(incoming) === direction(outgoing);
1340
- const neighbour = sameNote(previous.note, next.note);
1341
- return passing || neighbour;
1271
+ if (direction(incoming) === direction(outgoing)) {
1272
+ return true;
1273
+ }
1274
+ return sameNote(previous.note, next.note) && (species === 3 || species === 5);
1342
1275
  }
1343
1276
  function isThird(interval) {
1344
1277
  return interval.number === 3;
@@ -1399,7 +1332,7 @@ function licenceFor(species, downbeat) {
1399
1332
  }
1400
1333
  return downbeat ? "none" : "passing";
1401
1334
  }
1402
- function verticalViolations(entries, position, species, counterpointAbove) {
1335
+ function verticalViolations(entries, position, species, counterpointAbove, crossed) {
1403
1336
  const entry = entries[position];
1404
1337
  if (entry === void 0) {
1405
1338
  return [];
@@ -1407,7 +1340,8 @@ function verticalViolations(entries, position, species, counterpointAbove) {
1407
1340
  const found = [];
1408
1341
  const upper = counterpointAbove ? entry.note : entry.against;
1409
1342
  const lower = counterpointAbove ? entry.against : entry.note;
1410
- if (createsVoiceCrossing(upper, lower)) {
1343
+ if (!crossed.has(entry.index) && createsVoiceCrossing(upper, lower)) {
1344
+ crossed.add(entry.index);
1411
1345
  found.push(
1412
1346
  violation(
1413
1347
  "voiceCrossing",
@@ -1422,7 +1356,7 @@ function verticalViolations(entries, position, species, counterpointAbove) {
1422
1356
  return found;
1423
1357
  }
1424
1358
  const licence = licenceFor(species, entry.downbeat);
1425
- if (licence === "passing" && (isPassedThrough(entries, position) || isFiguredDissonance(entries, position, species))) {
1359
+ if (licence === "passing" && (isPassedThrough(entries, position, species) || isFiguredDissonance(entries, position, species))) {
1426
1360
  return found;
1427
1361
  }
1428
1362
  if (licence === "suspension") {
@@ -1471,6 +1405,17 @@ function suspensionViolations(entries, position) {
1471
1405
  )
1472
1406
  ];
1473
1407
  }
1408
+ if (next.consonance === 2 /* Dissonance */) {
1409
+ return [
1410
+ violation(
1411
+ "unresolvedSuspension",
1412
+ [0, 1],
1413
+ entry.index,
1414
+ next.index,
1415
+ `The suspension falls onto a ${next.interval.quality}${next.interval.number}, which is no resolution`
1416
+ )
1417
+ ];
1418
+ }
1474
1419
  return [];
1475
1420
  }
1476
1421
  function melodicViolations(from, to) {
@@ -1607,7 +1552,7 @@ function leapAnswerViolations(moves) {
1607
1552
  [1],
1608
1553
  leap.from.index,
1609
1554
  answer.to.index,
1610
- `The leap of ${word === "octave" ? "an" : "a"} ${word} is not answered by a step the other way`
1555
+ `The leap of ${intervalArticle(word)} ${word} is not answered by a step the other way`
1611
1556
  )
1612
1557
  );
1613
1558
  }
@@ -1673,6 +1618,11 @@ function melodicShapeViolations(entries, species) {
1673
1618
  ];
1674
1619
  return found.sort((left, right) => left.fromIndex - right.fromIndex);
1675
1620
  }
1621
+ function inTimeOrder(violations) {
1622
+ return [...violations].sort(
1623
+ (left, right) => left.fromIndex - right.fromIndex || left.toIndex - right.toIndex
1624
+ );
1625
+ }
1676
1626
  function motionViolations(from, to, counterpointAbove) {
1677
1627
  const found = [];
1678
1628
  const upperPrev = counterpointAbove ? from.note : from.against;
@@ -1691,14 +1641,14 @@ function motionViolations(from, to, counterpointAbove) {
1691
1641
  )
1692
1642
  );
1693
1643
  }
1694
- if (createsHiddenParallelPerfect(upperPrev, upperCur, lowerPrev, lowerCur)) {
1644
+ if (createsHiddenParallelPerfect(upperPrev, upperCur, lowerPrev, lowerCur, "twoVoice")) {
1695
1645
  found.push(
1696
1646
  violation(
1697
1647
  "hiddenPerfect",
1698
1648
  [0, 1],
1699
1649
  from.index,
1700
1650
  to.index,
1701
- "The voices leap into a perfect interval by similar motion"
1651
+ "The voices move into a perfect interval by similar motion"
1702
1652
  )
1703
1653
  );
1704
1654
  }
@@ -1796,14 +1746,14 @@ function resolveDurations(counterpoint, measures, species, opts) {
1796
1746
  }
1797
1747
  for (const duration of given) {
1798
1748
  const units = duration / MIN_FLORID_VALUE;
1799
- if (!(duration > 0) || duration > 1 || Math.abs(units - Math.round(units)) > EPS) {
1749
+ if (!(duration > 0) || duration > 1 || Math.abs(units - Math.round(units)) > BEAT_EPS) {
1800
1750
  return {
1801
1751
  problem: `A note lasts ${duration} of a measure, which this style does not write`
1802
1752
  };
1803
1753
  }
1804
1754
  }
1805
1755
  const total = given.reduce((sum, duration) => sum + duration, 0);
1806
- if (Math.abs(total - measures) > EPS) {
1756
+ if (Math.abs(total - measures) > BEAT_EPS) {
1807
1757
  return { problem: `The counterpoint fills ${total} of the ${measures} measures` };
1808
1758
  }
1809
1759
  return { durations: [...given] };
@@ -1831,23 +1781,32 @@ function buildEntries(cantusFirmus, counterpoint, durations, counterpointAbove)
1831
1781
  if (note === void 0) {
1832
1782
  continue;
1833
1783
  }
1834
- const againstIndex = Math.min(Math.floor(onset + EPS), cantusFirmus.length - 1);
1835
- const against = cantusFirmus[againstIndex];
1836
- if (against === void 0) {
1837
- continue;
1784
+ const struckOver = Math.min(Math.floor(onset + BEAT_EPS), cantusFirmus.length - 1);
1785
+ const lastOver = Math.min(
1786
+ Math.max(Math.ceil(onset + duration - BEAT_EPS) - 1, struckOver),
1787
+ cantusFirmus.length - 1
1788
+ );
1789
+ for (let againstIndex = struckOver; againstIndex <= lastOver; againstIndex += 1) {
1790
+ const against = cantusFirmus[againstIndex];
1791
+ if (against === void 0) {
1792
+ continue;
1793
+ }
1794
+ const held = againstIndex > struckOver;
1795
+ const begins = held ? againstIndex : onset;
1796
+ const interval = counterpointAbove ? spelledInterval(against, note) : spelledInterval(note, against);
1797
+ entries.push({
1798
+ note,
1799
+ index,
1800
+ onset: begins,
1801
+ duration: onset + duration - begins,
1802
+ held,
1803
+ against,
1804
+ againstIndex,
1805
+ downbeat: Math.abs(begins - Math.round(begins)) < BEAT_EPS,
1806
+ interval,
1807
+ consonance: classifySpelledInterval(interval)
1808
+ });
1838
1809
  }
1839
- const interval = counterpointAbove ? spelledInterval(against, note) : spelledInterval(note, against);
1840
- entries.push({
1841
- note,
1842
- index,
1843
- onset,
1844
- duration,
1845
- against,
1846
- againstIndex,
1847
- downbeat: Math.abs(onset - Math.round(onset)) < EPS,
1848
- interval,
1849
- consonance: classifySpelledInterval(interval)
1850
- });
1851
1810
  onset += duration;
1852
1811
  }
1853
1812
  return entries;
@@ -1866,40 +1825,50 @@ function motionPairs(entries) {
1866
1825
  }
1867
1826
  return pairs;
1868
1827
  }
1869
- function checkSpecies(cantusFirmus, counterpoint, species, mode, opts) {
1828
+ function checkSpecies(cantusFirmus, counterpoint, species, modeLike, opts) {
1829
+ assertArray(cantusFirmus, "cantus firmus");
1830
+ assertArray(counterpoint, "counterpoint");
1831
+ const asked = assertOptions(opts, "opts");
1832
+ const mode = toKeyScale(modeLike);
1870
1833
  if (!SPECIES_NUMBERS.includes(species)) {
1871
1834
  throw new InvalidInputError(
1872
- `species must be one of ${SPECIES_NUMBERS.join(", ")}; received ${JSON.stringify(species)}`
1835
+ `species must be one of ${SPECIES_NUMBERS.join(", ")}; received ${describeRejected(species)}`
1873
1836
  );
1874
1837
  }
1875
1838
  if (cantusFirmus.length === 0 || counterpoint.length === 0) {
1876
1839
  throw new InvalidInputError("checkSpecies needs both a cantus firmus and a counterpoint");
1877
1840
  }
1878
- const resolved = resolveDurations(counterpoint, cantusFirmus.length, species, opts);
1841
+ const resolved = resolveDurations(counterpoint, cantusFirmus.length, species, asked);
1879
1842
  if ("problem" in resolved) {
1880
- return [violation("wrongRhythmicRatio", [1], 0, counterpoint.length - 1, resolved.problem)];
1843
+ return inTimeOrder([
1844
+ violation("wrongRhythmicRatio", [1], 0, counterpoint.length - 1, resolved.problem)
1845
+ ]);
1881
1846
  }
1882
- const counterpointAbove = opts?.counterpointAbove ?? meanPitch(counterpoint) >= meanPitch(cantusFirmus);
1847
+ const counterpointAbove = asked.counterpointAbove ?? meanPitch(counterpoint) >= meanPitch(cantusFirmus);
1883
1848
  const entries = buildEntries(cantusFirmus, counterpoint, resolved.durations, counterpointAbove);
1849
+ const struck = entries.filter((entry) => !entry.held);
1884
1850
  const violations = [];
1851
+ const crossed = /* @__PURE__ */ new Set();
1885
1852
  for (let position = 0; position < entries.length; position += 1) {
1886
- violations.push(...verticalViolations(entries, position, species, counterpointAbove));
1887
- const previous = entries[position - 1];
1888
- const current = entries[position];
1853
+ violations.push(...verticalViolations(entries, position, species, counterpointAbove, crossed));
1854
+ }
1855
+ for (let position = 1; position < struck.length; position += 1) {
1856
+ const previous = struck[position - 1];
1857
+ const current = struck[position];
1889
1858
  if (previous !== void 0 && current !== void 0) {
1890
1859
  violations.push(...melodicViolations(previous, current));
1891
1860
  }
1892
1861
  }
1893
- violations.push(...melodicShapeViolations(entries, species));
1894
- for (const [from, to] of motionPairs(entries)) {
1895
- const previous = entries[from];
1896
- const current = entries[to];
1862
+ violations.push(...melodicShapeViolations(struck, species));
1863
+ for (const [from, to] of motionPairs(struck)) {
1864
+ const previous = struck[from];
1865
+ const current = struck[to];
1897
1866
  if (previous !== void 0 && current !== void 0) {
1898
1867
  violations.push(...motionViolations(previous, current, counterpointAbove));
1899
1868
  }
1900
1869
  }
1901
- violations.push(...cadenceViolations(entries, mode, counterpointAbove));
1902
- return violations;
1870
+ violations.push(...cadenceViolations(struck, mode, counterpointAbove));
1871
+ return inTimeOrder(violations);
1903
1872
  }
1904
1873
 
1905
1874
  // src/theory/partwriting/index.ts
@@ -1912,9 +1881,6 @@ function midiOf(note, index, voice) {
1912
1881
  }
1913
1882
  return noteToMidi(note);
1914
1883
  }
1915
- function isDescendingStep(interval) {
1916
- return interval.number === 2 && (interval.semitones === -1 || interval.semitones === -2);
1917
- }
1918
1884
  function verticalViolations2(moment, ranges, maxSpacing) {
1919
1885
  const found = [];
1920
1886
  const { pitches, index } = moment;
@@ -2011,95 +1977,23 @@ function pairViolations(transition) {
2011
1977
  [0, voices - 1],
2012
1978
  from.index,
2013
1979
  to.index,
2014
- "The outer voices leap into a perfect interval by similar motion"
1980
+ "The outer voices reach a perfect interval by similar motion, the upper one by leap"
2015
1981
  )
2016
1982
  );
2017
1983
  }
2018
1984
  return found;
2019
1985
  }
2020
- function inKey(pc, key) {
2021
- return (key.modeMask12 >> pitchClassOf(pc - key.rootPc) & 1) === 1;
2022
- }
2023
- function isDominantSonority(chord) {
2024
- const has = (semitones) => chord.intervals.some((interval) => pitchClassOf(interval) === semitones);
2025
- return chord.quality === "maj" || has(4) && has(10);
2026
- }
2027
- function isAppliedDominant(chord, next, key) {
2028
- if (next === void 0 || !isDominantSonority(chord)) {
2029
- return false;
2030
- }
2031
- const target = pitchClassOf(chord.rootPc + 5);
2032
- return target !== pitchClassOf(key.rootPc) && inKey(target, key) && pitchClassOf(next.rootPc) === target;
2033
- }
2034
- function isNeapolitan(chord, key) {
2035
- return chord.quality === "maj" && pitchClassOf(chord.rootPc - key.rootPc) === 1;
2036
- }
2037
- function soundsAugmentedSixth(moment, key) {
2038
- const bass = moment.notes[0];
2039
- if (bass === void 0 || noteToPitchClass(bass) !== pitchClassOf(key.rootPc + 8)) {
2040
- return false;
2041
- }
2042
- return moment.notes.some((note, voice) => {
2043
- if (voice === 0) {
2044
- return false;
2045
- }
2046
- const interval = spelledInterval(bass, note);
2047
- return (interval.number - 1) % 7 + 1 === 6 && pitchClassOf(interval.semitones) === 10;
2048
- });
2049
- }
2050
- function isChromaticHarmony(moment, next, key) {
2051
- return isAppliedDominant(moment.chord, next, key) || isNeapolitan(moment.chord, key) || soundsAugmentedSixth(moment, key);
2052
- }
2053
- function approachedByStep(before, moment, voice) {
2054
- const earlier = before?.notes[voice];
2055
- const later = moment.notes[voice];
2056
- if (earlier === void 0 || later === void 0) {
2057
- return false;
2058
- }
2059
- return spelledInterval(earlier, later).number === 2;
2060
- }
2061
- function isOuterVoice(voice, moment) {
2062
- return voice === 0 || voice === moment.notes.length - 1;
2063
- }
2064
1986
  function crossRelationViolations(transition) {
2065
- const found = [];
2066
1987
  const { from, to, key, before, after } = transition;
2067
- const chromatic = isChromaticHarmony(from, to.chord, key) || isChromaticHarmony(to, after?.chord, key);
2068
- const reported = /* @__PURE__ */ new Set();
2069
- for (let first = 0; first < from.notes.length; first += 1) {
2070
- for (let second = 0; second < to.notes.length; second += 1) {
2071
- if (first === second) {
2072
- continue;
2073
- }
2074
- const earlier = from.notes[first];
2075
- const later = to.notes[second];
2076
- if (earlier === void 0 || later === void 0) {
2077
- continue;
2078
- }
2079
- const letter = diatonicLetterOf(earlier.letter);
2080
- if (letter !== diatonicLetterOf(later.letter) || earlier.alter === later.alter) {
2081
- continue;
2082
- }
2083
- if (chromatic || approachedByStep(before, from, first) || approachedByStep(from, to, second) || !isOuterVoice(first, from) && !isOuterVoice(second, to)) {
2084
- continue;
2085
- }
2086
- const pair = `${letter}:${Math.min(first, second)}:${Math.max(first, second)}`;
2087
- if (reported.has(pair)) {
2088
- continue;
2089
- }
2090
- reported.add(pair);
2091
- found.push(
2092
- violation(
2093
- "crossRelation",
2094
- [first, second],
2095
- from.index,
2096
- to.index,
2097
- `${formatNote(bare(earlier))} is contradicted by ${formatNote(bare(later))} in another voice`
2098
- )
2099
- );
2100
- }
2101
- }
2102
- return found;
1988
+ return crossRelations(from, to, key, { before: before?.notes, after: after?.chord }).map(
1989
+ (clash) => violation(
1990
+ "crossRelation",
1991
+ [clash.earlierVoice, clash.laterVoice],
1992
+ from.index,
1993
+ to.index,
1994
+ `${formatNote(bare(clash.earlier))} is contradicted by ${formatNote(bare(clash.later))} in another voice`
1995
+ )
1996
+ );
2103
1997
  }
2104
1998
  function melodicViolations2(transition) {
2105
1999
  const found = [];
@@ -2141,7 +2035,8 @@ function melodicViolations2(transition) {
2141
2035
  )
2142
2036
  );
2143
2037
  }
2144
- if (earlierPc === leadingTonePc && isFunctioningLeadingTone(from.chord, key) && nextPcs.has(tonicPc) && !nextPcs.has(earlierPc) && !isLeadingToneResolution(earlierPitch, laterPitch, key)) {
2038
+ const inner = !isOuterVoice(voice, from.notes) && !isOuterVoice(voice, to.notes);
2039
+ if (earlierPc === leadingTonePc && isFunctioningLeadingTone(from.chord, key) && nextPcs.has(tonicPc) && !nextPcs.has(earlierPc) && !isLeadingToneResolution(earlierPitch, laterPitch, key) && !(inner && isFrustratedLeadingTone(earlierPitch, laterPitch, to.chord))) {
2145
2040
  found.push(
2146
2041
  violation(
2147
2042
  "unresolvedLeadingTone",
@@ -2156,22 +2051,28 @@ function melodicViolations2(transition) {
2156
2051
  return found;
2157
2052
  }
2158
2053
  function spellVoicing(voicing, chord, key) {
2159
- const { tonic } = spelledKeyOf(key);
2160
- return voicing.map((pitch) => spellPitch(pitch, tonic, key, { chordRoot: chord.rootPc }));
2161
- }
2162
- function checkPartWriting(voicings, chords, key, opts) {
2054
+ assertArray(voicing, "voicing");
2055
+ assertChord(chord);
2056
+ const { tonic, scale } = resolveKey(key);
2057
+ return voicing.map((pitch) => spellPitch(pitch, tonic, scale, { chord }));
2058
+ }
2059
+ function checkPartWriting(voicings, chords, keyLike, opts) {
2060
+ assertArray(voicings, "voicings");
2061
+ assertArray(chords, "chords");
2062
+ const asked = assertOptions(opts, "opts");
2063
+ const key = toKeyScale(keyLike);
2163
2064
  if (voicings.length !== chords.length) {
2164
2065
  throw new InvalidInputError(
2165
2066
  `checkPartWriting needs one chord per voicing; received ${voicings.length} voicings and ${chords.length} chords`
2166
2067
  );
2167
2068
  }
2168
- const maxSpacing = resolveMaxSpacing(opts);
2069
+ const maxSpacing = resolveMaxSpacing(asked);
2169
2070
  const voiceCount = voicings.reduce((widest, voicing) => Math.max(widest, voicing.length), 0);
2170
2071
  let ranges;
2171
- if (opts?.ranges === void 0) {
2072
+ if (asked.ranges === void 0) {
2172
2073
  ranges = voiceCount === 4 ? SATB_RANGES : void 0;
2173
2074
  } else {
2174
- ranges = resolveRanges(opts);
2075
+ ranges = resolveRanges(asked);
2175
2076
  if (ranges.length < voiceCount) {
2176
2077
  throw new InvalidInputError(
2177
2078
  `checkPartWriting needs one range per voice; received ${ranges.length} ranges for ${voiceCount} voices`
@@ -2221,12 +2122,6 @@ export {
2221
2122
  instrumentTransposition,
2222
2123
  toSoundingPitch,
2223
2124
  toWrittenPitch,
2224
- scaleMatchesChord,
2225
- chordScales,
2226
- avoidNotes,
2227
- availableTensions,
2228
- chordScaleReport,
2229
- scalesForChanges,
2230
2125
  figuredBassRealization,
2231
2126
  realizeFiguredBass,
2232
2127
  figuredBassOf,
@@ -2240,4 +2135,4 @@ export {
2240
2135
  spellVoicing,
2241
2136
  checkPartWriting
2242
2137
  };
2243
- //# sourceMappingURL=chunk-H3VORETI.js.map
2138
+ //# sourceMappingURL=chunk-M5RYMAFI.js.map