@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.
- package/README.md +27 -65
- package/README_ja.md +28 -66
- package/dist/analyze/index.cjs +11 -7
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +79 -144
- package/dist/analyze/index.d.ts +79 -144
- package/dist/analyze/index.js +20 -16
- package/dist/chunk-35NBV2FT.cjs +145 -0
- package/dist/chunk-35NBV2FT.cjs.map +1 -0
- package/dist/{chunk-UADYAYAT.cjs → chunk-3R3QVKY5.cjs} +1353 -580
- package/dist/chunk-3R3QVKY5.cjs.map +1 -0
- package/dist/{chunk-WWURE4XT.cjs → chunk-76ZBTRG2.cjs} +624 -729
- package/dist/chunk-76ZBTRG2.cjs.map +1 -0
- package/dist/{chunk-FVSI36UV.js → chunk-7KG3RTYS.js} +197 -110
- package/dist/chunk-7KG3RTYS.js.map +1 -0
- package/dist/{chunk-VTI6FOLB.js → chunk-AGOVW7U6.js} +709 -133
- package/dist/chunk-AGOVW7U6.js.map +1 -0
- package/dist/{chunk-E3F4AHAH.cjs → chunk-CGBADIO5.cjs} +221 -134
- package/dist/chunk-CGBADIO5.cjs.map +1 -0
- package/dist/{chunk-6XH6QZYQ.cjs → chunk-DSMJRXCZ.cjs} +1455 -849
- package/dist/chunk-DSMJRXCZ.cjs.map +1 -0
- package/dist/{chunk-PKFNUHDY.js → chunk-FBH75DAY.js} +1218 -612
- package/dist/chunk-FBH75DAY.js.map +1 -0
- package/dist/chunk-H2KJHBW4.js +3329 -0
- package/dist/chunk-H2KJHBW4.js.map +1 -0
- package/dist/chunk-HPVSH7SF.cjs +2294 -0
- package/dist/chunk-HPVSH7SF.cjs.map +1 -0
- package/dist/{chunk-MXO72PLY.cjs → chunk-HWHGOVO6.cjs} +21 -19
- package/dist/chunk-HWHGOVO6.cjs.map +1 -0
- package/dist/chunk-HYJJ2V2I.js +2294 -0
- package/dist/chunk-HYJJ2V2I.js.map +1 -0
- package/dist/{chunk-AFM2ULJH.cjs → chunk-J7FHVZII.cjs} +1567 -1067
- package/dist/chunk-J7FHVZII.cjs.map +1 -0
- package/dist/chunk-K6OZXIR6.js +1 -0
- package/dist/{chunk-H3VORETI.js → chunk-M5RYMAFI.js} +487 -592
- package/dist/chunk-M5RYMAFI.js.map +1 -0
- package/dist/chunk-NB25P3EH.js +363 -0
- package/dist/chunk-NB25P3EH.js.map +1 -0
- package/dist/{chunk-TTCLJX6D.js → chunk-NFJ4P6ZZ.js} +10 -8
- package/dist/chunk-NFJ4P6ZZ.js.map +1 -0
- package/dist/chunk-NM7UAMC3.cjs +457 -0
- package/dist/chunk-NM7UAMC3.cjs.map +1 -0
- package/dist/chunk-PM4XJE2Y.cjs +363 -0
- package/dist/chunk-PM4XJE2Y.cjs.map +1 -0
- package/dist/chunk-PUKJP2Z5.js +145 -0
- package/dist/chunk-PUKJP2Z5.js.map +1 -0
- package/dist/{chunk-VOHZB45S.js → chunk-Q6LFSAKV.js} +1321 -821
- package/dist/chunk-Q6LFSAKV.js.map +1 -0
- package/dist/{chunk-INHVXP6Z.js → chunk-RFIBF2RR.js} +1203 -430
- package/dist/chunk-RFIBF2RR.js.map +1 -0
- package/dist/{chunk-Q2WGKJNN.cjs → chunk-S5BKZZGM.cjs} +699 -123
- package/dist/chunk-S5BKZZGM.cjs.map +1 -0
- package/dist/chunk-SGLEQVJH.cjs +3329 -0
- package/dist/chunk-SGLEQVJH.cjs.map +1 -0
- package/dist/chunk-UGLFDG7L.cjs +1 -0
- package/dist/chunk-UGLFDG7L.cjs.map +1 -0
- package/dist/{chunk-YH3BIKHH.js → chunk-UMWZ4ZET.js} +146 -49
- package/dist/chunk-UMWZ4ZET.js.map +1 -0
- package/dist/{index-szBv3496.d.ts → coerce-DP0vYu-x.d.ts} +286 -199
- package/dist/{index-C_8o--VE.d.cts → coerce-DRERzdve.d.cts} +286 -199
- package/dist/core/index.cjs +9 -5
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +122 -108
- package/dist/core/index.d.ts +122 -108
- package/dist/core/index.js +32 -28
- package/dist/generate/index.cjs +8 -8
- package/dist/generate/index.d.cts +168 -84
- package/dist/generate/index.d.ts +168 -84
- package/dist/generate/index.js +7 -7
- package/dist/{index-CGpRv5sl.d.cts → index-9eoQs2C1.d.ts} +34 -19
- package/dist/{index-CCNJRG-P.d.cts → index-B9QLziDe.d.cts} +293 -56
- package/dist/{index-Dzr13q3d.d.cts → index-BBWNZS0f.d.cts} +135 -58
- package/dist/{index-9wcMhG73.d.ts → index-BHIUyjhZ.d.ts} +293 -56
- package/dist/{index-BGFW-8P2.d.cts → index-BJgP97N1.d.ts} +196 -114
- package/dist/{index-CunYKLiu.d.ts → index-BL5N2Oyz.d.cts} +1 -1
- package/dist/{index-Cj49F892.d.ts → index-BMweUlm5.d.cts} +63 -23
- package/dist/{index-CgHqxdps.d.ts → index-BNSM-DSk.d.cts} +196 -114
- package/dist/{index-D57QRbVn.d.ts → index-BRMbPkHE.d.ts} +480 -254
- package/dist/{index-Chbst3Jx.d.ts → index-CJBBiAmU.d.ts} +135 -58
- package/dist/{index-BkGmlyI3.d.cts → index-CL4b4gUQ.d.ts} +63 -23
- package/dist/{index-B9MNZboQ.d.ts → index-Cy15oTvh.d.cts} +52 -22
- package/dist/{index-B9MNZboQ.d.cts → index-Cy15oTvh.d.ts} +52 -22
- package/dist/{index-BO6xE9YU.d.cts → index-DGCNKAUr.d.cts} +480 -254
- package/dist/{index-B4o55NDr.d.cts → index-Dw2DP7vm.d.ts} +1 -1
- package/dist/{index-G1uwGGWQ.d.ts → index-cHvbYgJI.d.cts} +34 -19
- package/dist/index.cjs +42 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -14
- package/dist/index.d.ts +14 -14
- package/dist/index.js +82 -54
- package/dist/{masks-DYbE485J.d.cts → masks-Cc7-MzzY.d.ts} +49 -2
- package/dist/{masks-DYbE485J.d.ts → masks-pADfTOiD.d.cts} +49 -2
- package/dist/model/index.cjs +19 -12
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +504 -182
- package/dist/model/index.d.ts +504 -182
- package/dist/model/index.js +18 -11
- package/dist/{positional-Cptlj-xs.d.ts → positional-DJgs6ltD.d.ts} +85 -8
- package/dist/{positional-DvH_sd8u.d.cts → positional-DMZer-FM.d.cts} +85 -8
- package/dist/theory/index.cjs +35 -6
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +257 -62
- package/dist/theory/index.d.ts +257 -62
- package/dist/theory/index.js +41 -12
- package/package.json +7 -5
- package/dist/chunk-2LCPM3EG.js +0 -3224
- package/dist/chunk-2LCPM3EG.js.map +0 -1
- package/dist/chunk-6XH6QZYQ.cjs.map +0 -1
- package/dist/chunk-6XPTBE7Y.cjs +0 -49
- package/dist/chunk-6XPTBE7Y.cjs.map +0 -1
- package/dist/chunk-AFM2ULJH.cjs.map +0 -1
- package/dist/chunk-B6XNKWZV.cjs +0 -3224
- package/dist/chunk-B6XNKWZV.cjs.map +0 -1
- package/dist/chunk-E3F4AHAH.cjs.map +0 -1
- package/dist/chunk-ELCTZUJJ.js +0 -379
- package/dist/chunk-ELCTZUJJ.js.map +0 -1
- package/dist/chunk-FVSI36UV.js.map +0 -1
- package/dist/chunk-H3VORETI.js.map +0 -1
- package/dist/chunk-H42UHPYW.js +0 -1577
- package/dist/chunk-H42UHPYW.js.map +0 -1
- package/dist/chunk-HFPS7JFD.js +0 -1
- package/dist/chunk-INHVXP6Z.js.map +0 -1
- package/dist/chunk-K4ZJUGF4.cjs +0 -379
- package/dist/chunk-K4ZJUGF4.cjs.map +0 -1
- package/dist/chunk-KNJEKLKV.js +0 -49
- package/dist/chunk-KNJEKLKV.js.map +0 -1
- package/dist/chunk-MXO72PLY.cjs.map +0 -1
- package/dist/chunk-NLEU4QSE.cjs +0 -360
- package/dist/chunk-NLEU4QSE.cjs.map +0 -1
- package/dist/chunk-OE6WK2Y2.cjs +0 -1577
- package/dist/chunk-OE6WK2Y2.cjs.map +0 -1
- package/dist/chunk-PKFNUHDY.js.map +0 -1
- package/dist/chunk-Q2WGKJNN.cjs.map +0 -1
- package/dist/chunk-RHIJ3S4I.cjs +0 -1
- package/dist/chunk-RHIJ3S4I.cjs.map +0 -1
- package/dist/chunk-TTCLJX6D.js.map +0 -1
- package/dist/chunk-UADYAYAT.cjs.map +0 -1
- package/dist/chunk-VOHZB45S.js.map +0 -1
- package/dist/chunk-VTI6FOLB.js.map +0 -1
- package/dist/chunk-WWURE4XT.cjs.map +0 -1
- package/dist/chunk-YH3BIKHH.js.map +0 -1
- /package/dist/{chunk-HFPS7JFD.js.map → chunk-K6OZXIR6.js.map} +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
"+": "
|
|
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
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
"
|
|
448
|
-
"
|
|
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 ${
|
|
165
|
+
return new InvalidInputError(`figured bass ${describeRejected(figures)} ${detail}`);
|
|
452
166
|
}
|
|
453
167
|
function alteredBy(diatonicAlter, accidental) {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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(
|
|
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 ${
|
|
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 ${
|
|
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
|
|
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
|
|
659
|
-
const alters = keyLetterAlters(
|
|
660
|
-
const tones = spellChord(data,
|
|
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
|
-
|
|
683
|
-
if (alteration === 0) {
|
|
385
|
+
if (tone.alter === (alters[diatonicLetterOf(tone.letter)] ?? 0)) {
|
|
684
386
|
continue;
|
|
685
387
|
}
|
|
686
|
-
const glyph = ACCIDENTAL_GLYPHS
|
|
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,
|
|
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 ${
|
|
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/
|
|
719
|
-
function
|
|
720
|
-
|
|
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
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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
|
-
|
|
729
|
-
|
|
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
|
|
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
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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
|
|
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
|
|
1008
|
-
return opts?.key === void 0 ? void 0 :
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1068
|
-
|
|
1069
|
-
const
|
|
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
|
|
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
|
-
|
|
1077
|
-
|
|
1078
|
-
const
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
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 <
|
|
992
|
+
for (let candidate = 0; candidate < count; candidate += 1) {
|
|
1099
993
|
const offset = candidate * voices;
|
|
1100
|
-
|
|
1101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
1157
|
-
const
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
stack[
|
|
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] ??
|
|
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
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
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
|
|
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
|
|
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)) >
|
|
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) >
|
|
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
|
|
1835
|
-
const
|
|
1836
|
-
|
|
1837
|
-
|
|
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,
|
|
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 ${
|
|
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,
|
|
1841
|
+
const resolved = resolveDurations(counterpoint, cantusFirmus.length, species, asked);
|
|
1879
1842
|
if ("problem" in resolved) {
|
|
1880
|
-
return
|
|
1843
|
+
return inTimeOrder([
|
|
1844
|
+
violation("wrongRhythmicRatio", [1], 0, counterpoint.length - 1, resolved.problem)
|
|
1845
|
+
]);
|
|
1881
1846
|
}
|
|
1882
|
-
const counterpointAbove =
|
|
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
|
-
|
|
1888
|
-
|
|
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(
|
|
1894
|
-
for (const [from, to] of motionPairs(
|
|
1895
|
-
const previous =
|
|
1896
|
-
const current =
|
|
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(
|
|
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
|
|
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
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
}
|
|
2074
|
-
|
|
2075
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2160
|
-
|
|
2161
|
-
}
|
|
2162
|
-
|
|
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(
|
|
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 (
|
|
2072
|
+
if (asked.ranges === void 0) {
|
|
2172
2073
|
ranges = voiceCount === 4 ? SATB_RANGES : void 0;
|
|
2173
2074
|
} else {
|
|
2174
|
-
ranges = resolveRanges(
|
|
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-
|
|
2138
|
+
//# sourceMappingURL=chunk-M5RYMAFI.js.map
|