@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,33 +1,43 @@
|
|
|
1
1
|
import {
|
|
2
2
|
InvalidInputError,
|
|
3
|
+
MAX_NAME_ACCIDENTALS,
|
|
4
|
+
assertArray,
|
|
3
5
|
assertDegree,
|
|
4
6
|
assertFiniteNumber,
|
|
5
7
|
assertFiniteSemitones,
|
|
8
|
+
assertFlag,
|
|
6
9
|
assertGenerationBudget,
|
|
7
10
|
assertInteger,
|
|
8
11
|
assertMidiPitch,
|
|
9
12
|
assertOneOf,
|
|
13
|
+
assertOptions,
|
|
14
|
+
assertPitchClass,
|
|
15
|
+
assertRecord,
|
|
10
16
|
clampToMidi,
|
|
17
|
+
describeRejected,
|
|
11
18
|
diatonicLetterOf,
|
|
12
19
|
formatNote,
|
|
13
20
|
isConsonantInterval,
|
|
21
|
+
maxNoteNameLength,
|
|
14
22
|
midiToNote,
|
|
15
23
|
naturalPitchClassOf,
|
|
16
24
|
noteToMidi,
|
|
17
25
|
noteToPitchClass,
|
|
18
26
|
parseFailure,
|
|
19
|
-
parseKeyName,
|
|
20
27
|
parseNote,
|
|
21
28
|
pitchClassOf,
|
|
22
29
|
spelledInterval,
|
|
23
30
|
toNoteData,
|
|
31
|
+
transposeByInterval,
|
|
24
32
|
transposeNote,
|
|
33
|
+
tryParseKeyName,
|
|
25
34
|
tryParseNote,
|
|
26
35
|
unwrapParse
|
|
27
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-AGOVW7U6.js";
|
|
28
37
|
|
|
29
38
|
// src/theory/scale/masks.ts
|
|
30
39
|
function maskFromOffsets(offsets) {
|
|
40
|
+
assertArray(offsets, "offsets");
|
|
31
41
|
let mask = 1;
|
|
32
42
|
for (const [index, offset] of offsets.entries()) {
|
|
33
43
|
assertInteger(offset, `offsets[${index}]`);
|
|
@@ -51,6 +61,12 @@ var WHOLE_TONE_MASK = maskFromOffsets([0, 2, 4, 6, 8, 10]);
|
|
|
51
61
|
var OCTATONIC_HALF_WHOLE_MASK = maskFromOffsets([0, 1, 3, 4, 6, 7, 9, 10]);
|
|
52
62
|
var OCTATONIC_WHOLE_HALF_MASK = maskFromOffsets([0, 2, 3, 5, 6, 8, 9, 11]);
|
|
53
63
|
var CHROMATIC_MASK = 4095;
|
|
64
|
+
function assertModeMask(modeMask12, name = "modeMask12") {
|
|
65
|
+
return assertInteger(modeMask12, name, 1, CHROMATIC_MASK);
|
|
66
|
+
}
|
|
67
|
+
function maskOf(scale, name = "key") {
|
|
68
|
+
return assertModeMask(assertRecord(scale, name).modeMask12, `${name}.modeMask12`);
|
|
69
|
+
}
|
|
54
70
|
var LYDIAN_DOMINANT_MASK = maskFromOffsets([0, 2, 4, 6, 7, 9, 10]);
|
|
55
71
|
var MIXOLYDIAN_B13_MASK = maskFromOffsets([0, 2, 4, 5, 7, 8, 10]);
|
|
56
72
|
var LOCRIAN_NATURAL2_MASK = maskFromOffsets([0, 2, 3, 5, 6, 8, 10]);
|
|
@@ -156,6 +172,9 @@ var SCALE_ALIASES = Object.freeze({
|
|
|
156
172
|
poorvi: "purvi"
|
|
157
173
|
});
|
|
158
174
|
function resolveScaleName(name) {
|
|
175
|
+
if (typeof name !== "string") {
|
|
176
|
+
throw new InvalidInputError(`scale name must be a string; received ${describeRejected(name)}`);
|
|
177
|
+
}
|
|
159
178
|
if (Object.hasOwn(NAMED_SCALES, name)) {
|
|
160
179
|
return name;
|
|
161
180
|
}
|
|
@@ -178,21 +197,119 @@ function requireScaleMask(name, label = "scale") {
|
|
|
178
197
|
}
|
|
179
198
|
return mask;
|
|
180
199
|
}
|
|
200
|
+
var VARIANT_MASKS = {
|
|
201
|
+
major: MAJOR_MASK,
|
|
202
|
+
natural: NATURAL_MINOR_MASK,
|
|
203
|
+
harmonic: HARMONIC_MINOR_MASK,
|
|
204
|
+
melodic: MELODIC_MINOR_MASK
|
|
205
|
+
};
|
|
206
|
+
function isMinorMask(modeMask12) {
|
|
207
|
+
return (modeMask12 >> 3 & 1) === 1 && (modeMask12 >> 4 & 1) === 0;
|
|
208
|
+
}
|
|
209
|
+
function variantOfMask(modeMask12) {
|
|
210
|
+
assertModeMask(modeMask12);
|
|
211
|
+
for (const [variant, mask] of Object.entries(VARIANT_MASKS)) {
|
|
212
|
+
if (mask === modeMask12) {
|
|
213
|
+
return variant;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return "modal";
|
|
217
|
+
}
|
|
218
|
+
var KEY_VARIANTS = [
|
|
219
|
+
...Object.keys(VARIANT_MASKS),
|
|
220
|
+
"modal"
|
|
221
|
+
];
|
|
222
|
+
function assertKeyVariant(variant, modeMask12) {
|
|
223
|
+
assertOneOf(variant, KEY_VARIANTS, "variant");
|
|
224
|
+
assertModeMask(modeMask12);
|
|
225
|
+
const named = VARIANT_MASKS[variant];
|
|
226
|
+
const matches = named === void 0 ? !Object.values(VARIANT_MASKS).includes(modeMask12) : named === modeMask12;
|
|
227
|
+
if (!matches) {
|
|
228
|
+
throw new InvalidInputError(
|
|
229
|
+
`variant ${variant} does not match the scale mask ${modeMask12}; pass the scale that variant names, or omit the variant`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
181
233
|
|
|
182
|
-
// src/theory/scale/
|
|
183
|
-
function
|
|
234
|
+
// src/theory/scale/scales.ts
|
|
235
|
+
function majorScale(rootPc) {
|
|
184
236
|
return { rootPc: pitchClassOf(rootPc), modeMask12: MAJOR_MASK };
|
|
185
237
|
}
|
|
186
|
-
function
|
|
238
|
+
function minorScale(rootPc) {
|
|
187
239
|
return { rootPc: pitchClassOf(rootPc), modeMask12: NATURAL_MINOR_MASK };
|
|
188
240
|
}
|
|
189
|
-
function
|
|
241
|
+
function namedScale(name, rootPc) {
|
|
190
242
|
return { rootPc: pitchClassOf(rootPc), modeMask12: requireScaleMask(name) };
|
|
191
243
|
}
|
|
192
244
|
|
|
245
|
+
// src/theory/scale/name.ts
|
|
246
|
+
function scaleWordKey(text) {
|
|
247
|
+
return text.replace(/[^a-z0-9]/gi, "").toLowerCase();
|
|
248
|
+
}
|
|
249
|
+
var BUILT_IN_SCALE_NAMES = [
|
|
250
|
+
...Object.keys(NAMED_SCALES),
|
|
251
|
+
...Object.keys(WORLD_SCALES),
|
|
252
|
+
...Object.keys(SCALE_ALIASES)
|
|
253
|
+
];
|
|
254
|
+
var SCALE_BY_WORD = new Map(
|
|
255
|
+
BUILT_IN_SCALE_NAMES.map((name) => [scaleWordKey(name), name])
|
|
256
|
+
);
|
|
257
|
+
function scaleNameOfMask(mask) {
|
|
258
|
+
for (const name of Object.keys(NAMED_SCALES)) {
|
|
259
|
+
if (NAMED_SCALES[name] === mask) {
|
|
260
|
+
return name;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
for (const name of Object.keys(WORLD_SCALES)) {
|
|
264
|
+
if (WORLD_SCALES[name] === mask) {
|
|
265
|
+
return name;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return void 0;
|
|
269
|
+
}
|
|
270
|
+
function bareTonic(note) {
|
|
271
|
+
return { letter: note.letter, alter: note.alter };
|
|
272
|
+
}
|
|
273
|
+
function tryNamedScaleKey(text) {
|
|
274
|
+
const trimmed = text.trim();
|
|
275
|
+
const separator = trimmed.search(/\s/);
|
|
276
|
+
if (separator < 0) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
const name = SCALE_BY_WORD.get(scaleWordKey(trimmed.slice(separator)));
|
|
280
|
+
if (name === void 0) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
const tonic = tryParseNote(trimmed.slice(0, separator));
|
|
284
|
+
if (!tonic.ok) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
const scale = namedScale(name, noteToPitchClass(tonic.value));
|
|
288
|
+
return { scale, tonic: bareTonic(tonic.value), variant: variantOfMask(scale.modeMask12) };
|
|
289
|
+
}
|
|
290
|
+
function tryResolveKeyName(text, opts) {
|
|
291
|
+
const asked = assertOptions(opts, "opts");
|
|
292
|
+
if (typeof text === "string" && (asked.system === void 0 || asked.system === "english")) {
|
|
293
|
+
const named = tryNamedScaleKey(text);
|
|
294
|
+
if (named !== null) {
|
|
295
|
+
return { ok: true, value: named };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const parsed = tryParseKeyName(text, asked);
|
|
299
|
+
if (!parsed.ok) {
|
|
300
|
+
return parsed;
|
|
301
|
+
}
|
|
302
|
+
const tonic = bareTonic(parsed.value.tonic);
|
|
303
|
+
const rootPc = noteToPitchClass(tonic);
|
|
304
|
+
return parsed.value.mode === "major" ? { ok: true, value: { scale: majorScale(rootPc), tonic, variant: "major" } } : { ok: true, value: { scale: minorScale(rootPc), tonic, variant: "natural" } };
|
|
305
|
+
}
|
|
306
|
+
function resolveKeyName(text, opts) {
|
|
307
|
+
return unwrapParse(tryResolveKeyName(text, opts));
|
|
308
|
+
}
|
|
309
|
+
|
|
193
310
|
// src/theory/scale/coerce.ts
|
|
194
311
|
function normalizedKeyScale(scale) {
|
|
195
|
-
|
|
312
|
+
assertModeMask(scale.modeMask12, "key.modeMask12");
|
|
196
313
|
if ((scale.modeMask12 & 1) === 0) {
|
|
197
314
|
throw new InvalidInputError(
|
|
198
315
|
`key.modeMask12 must include its root; received ${scale.modeMask12}`
|
|
@@ -203,12 +320,10 @@ function normalizedKeyScale(scale) {
|
|
|
203
320
|
}
|
|
204
321
|
function toKeyScale(value) {
|
|
205
322
|
if (typeof value === "string") {
|
|
206
|
-
|
|
207
|
-
const rootPc = noteToPitchClass(name.tonic);
|
|
208
|
-
return name.mode === "major" ? majorKey(rootPc) : minorKey(rootPc);
|
|
323
|
+
return resolveKeyName(value).scale;
|
|
209
324
|
}
|
|
210
325
|
if (typeof value === "object" && value !== null) {
|
|
211
|
-
const data = "toJSON" in value && typeof value.toJSON === "function" ? value.toJSON().scale : value;
|
|
326
|
+
const data = "toJSON" in value && typeof value.toJSON === "function" ? value.toJSON().scale : "scale" in value ? value.scale : value;
|
|
212
327
|
if (typeof data === "object" && data !== null) {
|
|
213
328
|
return normalizedKeyScale(data);
|
|
214
329
|
}
|
|
@@ -224,10 +339,11 @@ function inScale(pitch, scale) {
|
|
|
224
339
|
return (scale.modeMask12 >> scaleOffset(pitch, scale) & 1) === 1;
|
|
225
340
|
}
|
|
226
341
|
function isScaleTone(pitch, key) {
|
|
227
|
-
return inScale(pitch, key);
|
|
342
|
+
return inScale(pitch, toKeyScale(key));
|
|
228
343
|
}
|
|
229
344
|
function nearestScaleTone(pitch, key) {
|
|
230
345
|
const scale = toKeyScale(key);
|
|
346
|
+
assertFiniteNumber(pitch, "pitch");
|
|
231
347
|
const base = clampToMidi(Math.round(pitch), "pitch");
|
|
232
348
|
for (let distance = 0; distance < 12; distance += 1) {
|
|
233
349
|
const lower = base - distance;
|
|
@@ -251,6 +367,10 @@ function scaleOffsets(key) {
|
|
|
251
367
|
return offsets;
|
|
252
368
|
}
|
|
253
369
|
function scaleLadderPosition(pitch, key) {
|
|
370
|
+
return ladderPosition(pitch, toKeyScale(key));
|
|
371
|
+
}
|
|
372
|
+
function ladderPosition(pitch, key) {
|
|
373
|
+
assertFiniteNumber(pitch, "pitch");
|
|
254
374
|
const offsets = scaleOffsets(key);
|
|
255
375
|
const size = offsets.length;
|
|
256
376
|
if (size === 0) {
|
|
@@ -268,6 +388,10 @@ function scaleLadderPosition(pitch, key) {
|
|
|
268
388
|
return { rung: octave * size + degree, offset: within - (offsets[degree] ?? 0) };
|
|
269
389
|
}
|
|
270
390
|
function scaleLadderPitch(rung, key) {
|
|
391
|
+
return ladderPitch(rung, toKeyScale(key));
|
|
392
|
+
}
|
|
393
|
+
function ladderPitch(rung, key) {
|
|
394
|
+
assertFiniteNumber(rung, "rung");
|
|
271
395
|
const offsets = scaleOffsets(key);
|
|
272
396
|
const size = offsets.length;
|
|
273
397
|
if (size === 0) {
|
|
@@ -278,12 +402,13 @@ function scaleLadderPitch(rung, key) {
|
|
|
278
402
|
return pitchClassOf(key.rootPc) + octave * 12 + (offsets[degree] ?? 0);
|
|
279
403
|
}
|
|
280
404
|
function shiftByScaleDegrees(pitch, degrees, key) {
|
|
281
|
-
const
|
|
405
|
+
const scale = toKeyScale(key);
|
|
406
|
+
const steps = Math.trunc(assertFiniteNumber(degrees, "degrees"));
|
|
282
407
|
if (steps === 0) {
|
|
283
408
|
return pitch;
|
|
284
409
|
}
|
|
285
|
-
const { rung, offset } =
|
|
286
|
-
return
|
|
410
|
+
const { rung, offset } = ladderPosition(pitch, scale);
|
|
411
|
+
return ladderPitch(rung + steps, scale) + offset;
|
|
287
412
|
}
|
|
288
413
|
function pitchToScaleDegree(pitch, key) {
|
|
289
414
|
const scale = toKeyScale(key);
|
|
@@ -300,18 +425,10 @@ function pitchToScaleDegree(pitch, key) {
|
|
|
300
425
|
return degree;
|
|
301
426
|
}
|
|
302
427
|
function diatonicPitchClasses(key) {
|
|
303
|
-
|
|
304
|
-
const root = pitchClassOf(scale.rootPc);
|
|
305
|
-
const pcs = [];
|
|
306
|
-
for (let n = 0; n < 12; n += 1) {
|
|
307
|
-
if ((scale.modeMask12 >> n & 1) === 1) {
|
|
308
|
-
pcs.push((root + n) % 12);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
return pcs.sort((a, b) => a - b);
|
|
428
|
+
return scaleTonesInDegreeOrder(key).sort((a, b) => a - b);
|
|
312
429
|
}
|
|
313
430
|
function scaleTonesInDegreeOrder(key) {
|
|
314
|
-
const scale = key;
|
|
431
|
+
const scale = toKeyScale(key);
|
|
315
432
|
const root = pitchClassOf(scale.rootPc);
|
|
316
433
|
const pcs = [];
|
|
317
434
|
for (let n = 0; n < 12; n += 1) {
|
|
@@ -348,22 +465,20 @@ var DIATONIC_MODE_OFFSETS = /* @__PURE__ */ new Map([
|
|
|
348
465
|
[rotateMask(MAJOR_MASK, 11), -5]
|
|
349
466
|
// locrian
|
|
350
467
|
]);
|
|
351
|
-
function hasMinorThird(modeMask12) {
|
|
352
|
-
return (modeMask12 >> 3 & 1) === 1 && (modeMask12 >> 4 & 1) === 0;
|
|
353
|
-
}
|
|
354
468
|
function modeOffset(key) {
|
|
355
469
|
const diatonic = DIATONIC_MODE_OFFSETS.get(key.modeMask12);
|
|
356
470
|
if (diatonic !== void 0) {
|
|
357
471
|
return diatonic;
|
|
358
472
|
}
|
|
359
|
-
return
|
|
473
|
+
return isMinorMask(key.modeMask12) ? MINOR_MODE_OFFSET : 0;
|
|
360
474
|
}
|
|
361
475
|
function isSignatureKey(key) {
|
|
362
|
-
|
|
476
|
+
const mask = maskOf(key);
|
|
477
|
+
return DIATONIC_MODE_OFFSETS.has(mask) || mask === HARMONIC_MINOR_MASK || mask === MELODIC_MINOR_MASK;
|
|
363
478
|
}
|
|
364
479
|
function keySignatureFifths(tonic, key) {
|
|
365
480
|
const note = toNoteData(tonic);
|
|
366
|
-
const scale = key;
|
|
481
|
+
const scale = toKeyScale(key);
|
|
367
482
|
const letter = diatonicLetterOf(note.letter);
|
|
368
483
|
assertInteger(note.alter, "tonic.alter");
|
|
369
484
|
assertInteger(scale.modeMask12, "key.modeMask12", 1, 4095);
|
|
@@ -376,33 +491,28 @@ function keyFromFifths(fifths, mode = "major") {
|
|
|
376
491
|
const letter = FIFTHS_LETTERS[(index % 7 + 7) % 7] ?? 0;
|
|
377
492
|
const alter = Math.floor(index / 7);
|
|
378
493
|
const rootPc = pitchClassOf(naturalPitchClassOf(letter) + alter);
|
|
494
|
+
const scale = which === "minor" ? minorScale(rootPc) : majorScale(rootPc);
|
|
379
495
|
return {
|
|
380
496
|
tonic: { letter, alter },
|
|
381
|
-
|
|
497
|
+
scale,
|
|
498
|
+
variant: variantOfMask(scale.modeMask12)
|
|
382
499
|
};
|
|
383
500
|
}
|
|
384
501
|
|
|
385
502
|
// src/theory/scale/relations.ts
|
|
386
503
|
var ENHARMONIC_FIFTHS = 12;
|
|
387
504
|
var MAX_CONVENTIONAL_FIFTHS = 7;
|
|
388
|
-
var DOUBLE_ACCIDENTAL = 2;
|
|
389
505
|
var MINOR_TONIC_FIFTHS = 3;
|
|
390
506
|
var FLATTEST_TONIC_FIFTHS = -MAX_CONVENTIONAL_FIFTHS;
|
|
391
507
|
var SHARPEST_TONIC_FIFTHS = MAX_CONVENTIONAL_FIFTHS + MINOR_TONIC_FIFTHS;
|
|
392
|
-
function isMinorMode(modeMask12) {
|
|
393
|
-
return (modeMask12 >> 3 & 1) === 1 && (modeMask12 >> 4 & 1) === 0;
|
|
394
|
-
}
|
|
395
|
-
function assertModeMask(key, name = "key") {
|
|
396
|
-
return assertInteger(key.modeMask12, `${name}.modeMask12`, 1, CHROMATIC_MASK);
|
|
397
|
-
}
|
|
398
508
|
function modeOf(key) {
|
|
399
|
-
return
|
|
509
|
+
return isMinorMask(maskOf(key)) ? "minor" : "major";
|
|
400
510
|
}
|
|
401
511
|
function oppositeMode(mode) {
|
|
402
512
|
return mode === "major" ? "minor" : "major";
|
|
403
513
|
}
|
|
404
514
|
function soundsLike(a, b) {
|
|
405
|
-
return pitchClassOf(a.rootPc) === pitchClassOf(b.rootPc) &&
|
|
515
|
+
return pitchClassOf(a.rootPc) === pitchClassOf(b.rootPc) && isMinorMask(a.modeMask12) === isMinorMask(b.modeMask12);
|
|
406
516
|
}
|
|
407
517
|
function spelledLike(a, b) {
|
|
408
518
|
return diatonicLetterOf(a.letter) === diatonicLetterOf(b.letter) && a.alter === b.alter;
|
|
@@ -413,7 +523,7 @@ function tonicCost(tonic, key) {
|
|
|
413
523
|
for (const note of spellScale(tonic, key)) {
|
|
414
524
|
const alter = Math.abs(note.alter);
|
|
415
525
|
accidentals += alter;
|
|
416
|
-
if (alter >=
|
|
526
|
+
if (alter >= MAX_NAME_ACCIDENTALS) {
|
|
417
527
|
doubles += 1;
|
|
418
528
|
}
|
|
419
529
|
}
|
|
@@ -429,7 +539,7 @@ function spellsBetter(a, b, signatureKey) {
|
|
|
429
539
|
return false;
|
|
430
540
|
}
|
|
431
541
|
function spelledKeyOf(key) {
|
|
432
|
-
const scale = key;
|
|
542
|
+
const scale = assertRecord(key, "key");
|
|
433
543
|
const rootPc = pitchClassOf(scale.rootPc);
|
|
434
544
|
const signatureKey = isSignatureKey(scale);
|
|
435
545
|
let tonic = keyFromFifths(0).tonic;
|
|
@@ -445,33 +555,35 @@ function spelledKeyOf(key) {
|
|
|
445
555
|
best = cost;
|
|
446
556
|
}
|
|
447
557
|
}
|
|
448
|
-
return { tonic, key };
|
|
558
|
+
return { tonic, scale: key, variant: variantOfMask(key.modeMask12) };
|
|
449
559
|
}
|
|
450
560
|
function relativeKeyOf(tonic, key) {
|
|
451
|
-
const
|
|
452
|
-
return keyFromFifths(
|
|
561
|
+
const mode = modeOf(toKeyScale(key));
|
|
562
|
+
return keyFromFifths(tonicFifths(toNoteData(tonic), mode), oppositeMode(mode));
|
|
453
563
|
}
|
|
454
564
|
function parallelKeyOf(tonic, key) {
|
|
455
565
|
const note = toNoteData(tonic);
|
|
456
566
|
const mode = oppositeMode(modeOf(toKeyScale(key)));
|
|
457
567
|
const rootPc = noteToPitchClass(note);
|
|
568
|
+
const scale = mode === "minor" ? minorScale(rootPc) : majorScale(rootPc);
|
|
458
569
|
return {
|
|
459
570
|
tonic: { letter: diatonicLetterOf(note.letter), alter: note.alter },
|
|
460
|
-
|
|
571
|
+
scale,
|
|
572
|
+
variant: variantOfMask(scale.modeMask12)
|
|
461
573
|
};
|
|
462
574
|
}
|
|
463
575
|
function tonicPosition(tonic) {
|
|
464
|
-
return keySignatureFifths(tonic,
|
|
576
|
+
return keySignatureFifths(tonic, majorScale(noteToPitchClass(tonic)));
|
|
465
577
|
}
|
|
466
578
|
function tonicFifths(tonic, mode) {
|
|
467
579
|
return tonicPosition(tonic) - (mode === "minor" ? MINOR_TONIC_FIFTHS : 0);
|
|
468
580
|
}
|
|
469
581
|
function isWrittenTonic(tonic, key) {
|
|
470
582
|
const position = tonicPosition(tonic);
|
|
471
|
-
if (
|
|
472
|
-
return
|
|
583
|
+
if (isSignatureKey(key)) {
|
|
584
|
+
return Math.abs(position) <= MAX_FIFTHS && Math.abs(keySignatureFifths(tonic, key)) <= MAX_CONVENTIONAL_FIFTHS;
|
|
473
585
|
}
|
|
474
|
-
return
|
|
586
|
+
return position >= FLATTEST_TONIC_FIFTHS && position <= SHARPEST_TONIC_FIFTHS && tonicCost(tonic, { rootPc: noteToPitchClass(tonic), modeMask12: key.modeMask12 }).doubles === 0;
|
|
475
587
|
}
|
|
476
588
|
function dominantKeyOf(tonic, key) {
|
|
477
589
|
const mode = modeOf(toKeyScale(key));
|
|
@@ -484,34 +596,35 @@ function subdominantKeyOf(tonic, key) {
|
|
|
484
596
|
function enharmonicKeyOf(tonic, key) {
|
|
485
597
|
const note = toNoteData(tonic);
|
|
486
598
|
const scale = toKeyScale(key);
|
|
487
|
-
|
|
599
|
+
maskOf(scale);
|
|
488
600
|
for (const position of [
|
|
489
601
|
tonicPosition(note) - ENHARMONIC_FIFTHS,
|
|
490
602
|
tonicPosition(note) + ENHARMONIC_FIFTHS
|
|
491
603
|
]) {
|
|
492
|
-
if (position
|
|
604
|
+
if (Math.abs(position) > MAX_FIFTHS) {
|
|
493
605
|
continue;
|
|
494
606
|
}
|
|
495
|
-
const
|
|
496
|
-
if (!isWrittenTonic(
|
|
607
|
+
const spelled2 = keyFromFifths(position).tonic;
|
|
608
|
+
if (!isWrittenTonic(spelled2, scale)) {
|
|
497
609
|
continue;
|
|
498
610
|
}
|
|
499
611
|
return {
|
|
500
|
-
tonic:
|
|
612
|
+
tonic: spelled2,
|
|
501
613
|
// The mask is the caller's; only the root is re-read, so a tonic that did
|
|
502
614
|
// not spell the mask's own root still comes back consistent.
|
|
503
|
-
|
|
615
|
+
scale: { rootPc: noteToPitchClass(spelled2), modeMask12: scale.modeMask12 },
|
|
616
|
+
variant: variantOfMask(scale.modeMask12)
|
|
504
617
|
};
|
|
505
618
|
}
|
|
506
619
|
return null;
|
|
507
620
|
}
|
|
508
621
|
function relativeOfDominantKeyOf(tonic, key) {
|
|
509
622
|
const dominant = dominantKeyOf(tonic, key);
|
|
510
|
-
return relativeKeyOf(dominant.tonic, dominant.
|
|
623
|
+
return relativeKeyOf(dominant.tonic, dominant.scale);
|
|
511
624
|
}
|
|
512
625
|
function relativeOfSubdominantKeyOf(tonic, key) {
|
|
513
626
|
const subdominant = subdominantKeyOf(tonic, key);
|
|
514
|
-
return relativeKeyOf(subdominant.tonic, subdominant.
|
|
627
|
+
return relativeKeyOf(subdominant.tonic, subdominant.scale);
|
|
515
628
|
}
|
|
516
629
|
var CLOSELY_RELATED = [
|
|
517
630
|
{ relation: "relative", of: relativeKeyOf },
|
|
@@ -527,19 +640,68 @@ function relatedKeysOf(tonic, key) {
|
|
|
527
640
|
return CLOSELY_RELATED.map(({ relation, of }) => ({ ...of(note, scale), relation }));
|
|
528
641
|
}
|
|
529
642
|
function keyRelationBetween(a, b) {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
643
|
+
const from = assertRecord(a, "a");
|
|
644
|
+
const to = assertRecord(b, "b");
|
|
645
|
+
maskOf(from.scale, "a.scale");
|
|
646
|
+
maskOf(to.scale, "b.scale");
|
|
647
|
+
if (soundsLike(from.scale, to.scale)) {
|
|
648
|
+
return spelledLike(from.tonic, to.tonic) ? "same" : "enharmonic";
|
|
534
649
|
}
|
|
535
650
|
for (const { relation, of } of CLOSELY_RELATED) {
|
|
536
|
-
if (soundsLike(of(
|
|
651
|
+
if (soundsLike(of(from.tonic, from.scale).scale, to.scale)) {
|
|
537
652
|
return relation;
|
|
538
653
|
}
|
|
539
654
|
}
|
|
540
655
|
return null;
|
|
541
656
|
}
|
|
542
657
|
|
|
658
|
+
// src/theory/scale/identity.ts
|
|
659
|
+
function carriesIdentity(value) {
|
|
660
|
+
return typeof value === "object" && value !== null;
|
|
661
|
+
}
|
|
662
|
+
function carriedTonic(tonic, scale) {
|
|
663
|
+
if (noteToPitchClass(tonic) !== scale.rootPc) {
|
|
664
|
+
throw new InvalidInputError(
|
|
665
|
+
`tonic ${formatNote(tonic)} does not match the scale root pitch class ${scale.rootPc}; pass a tonic that spells the scale root, or omit it to have one chosen`
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
return { letter: tonic.letter, alter: tonic.alter };
|
|
669
|
+
}
|
|
670
|
+
function carriedVariant(variant, scale) {
|
|
671
|
+
assertKeyVariant(variant, scale.modeMask12);
|
|
672
|
+
return variant;
|
|
673
|
+
}
|
|
674
|
+
function resolveKey(value) {
|
|
675
|
+
if (typeof value === "string") {
|
|
676
|
+
return resolveKeyName(value);
|
|
677
|
+
}
|
|
678
|
+
const scale = toKeyScale(value);
|
|
679
|
+
const carried = carriesIdentity(value) && "toJSON" in value && typeof value.toJSON === "function" ? value.toJSON() : value;
|
|
680
|
+
const tonic = carriesIdentity(carried) && carried.tonic !== void 0 ? carriedTonic(carried.tonic, scale) : spelledKeyOf(scale).tonic;
|
|
681
|
+
const variant = carriesIdentity(carried) && carried.variant !== void 0 ? carriedVariant(carried.variant, scale) : variantOfMask(scale.modeMask12);
|
|
682
|
+
return { scale, tonic, variant };
|
|
683
|
+
}
|
|
684
|
+
function scaleOf(key) {
|
|
685
|
+
const scale = assertRecord(key, "key").scale;
|
|
686
|
+
maskOf(scale, "key.scale");
|
|
687
|
+
return scale;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// src/theory/scale/key.ts
|
|
691
|
+
function spelled(scale) {
|
|
692
|
+
const { tonic, variant } = spelledKeyOf(scale);
|
|
693
|
+
return { ...scale, tonic, variant };
|
|
694
|
+
}
|
|
695
|
+
function majorKey(rootPc) {
|
|
696
|
+
return spelled(majorScale(rootPc));
|
|
697
|
+
}
|
|
698
|
+
function minorKey(rootPc) {
|
|
699
|
+
return spelled(minorScale(rootPc));
|
|
700
|
+
}
|
|
701
|
+
function scaleByName(name, rootPc) {
|
|
702
|
+
return spelled(namedScale(name, rootPc));
|
|
703
|
+
}
|
|
704
|
+
|
|
543
705
|
// src/theory/scale/system.ts
|
|
544
706
|
var SCALE_SYSTEMS = Object.freeze({
|
|
545
707
|
major: "common-practice",
|
|
@@ -607,7 +769,7 @@ function scaleSystemOf(scale) {
|
|
|
607
769
|
}
|
|
608
770
|
return SCALE_SYSTEMS[canonical];
|
|
609
771
|
}
|
|
610
|
-
return SYSTEM_BY_MASK.get(scale
|
|
772
|
+
return SYSTEM_BY_MASK.get(maskOf(scale, "scale"));
|
|
611
773
|
}
|
|
612
774
|
function supportsFunctionalHarmony(scale) {
|
|
613
775
|
return scaleSystemOf(scale) !== "non-functional";
|
|
@@ -653,6 +815,7 @@ var BASE_TONES = {
|
|
|
653
815
|
var SEVENTH_SEMITONES = { maj7: 11, min7: 10, dim7: 9 };
|
|
654
816
|
var DEGREE_SEMITONES = { 6: 9, 9: 14, 11: 17, 13: 21 };
|
|
655
817
|
var PERFECT_FIFTH = 7;
|
|
818
|
+
var FIFTH_KEY = "5";
|
|
656
819
|
var OMITTABLE_DEGREES = [1, 3, 5];
|
|
657
820
|
var BASE_QUALITY = {
|
|
658
821
|
maj: "maj",
|
|
@@ -676,114 +839,149 @@ function parts(base, spec = {}) {
|
|
|
676
839
|
}
|
|
677
840
|
return built;
|
|
678
841
|
}
|
|
679
|
-
var QUALITY_SPECS =
|
|
680
|
-
maj
|
|
681
|
-
min
|
|
682
|
-
dim
|
|
683
|
-
aug
|
|
684
|
-
majb5
|
|
685
|
-
maj7
|
|
686
|
-
min7
|
|
687
|
-
dom7
|
|
688
|
-
dim7
|
|
689
|
-
m7b5
|
|
690
|
-
minMaj7
|
|
691
|
-
minMaj9
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
[
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
"
|
|
719
|
-
"
|
|
720
|
-
"
|
|
721
|
-
"
|
|
842
|
+
var QUALITY_SPECS = [
|
|
843
|
+
["maj", parts("maj")],
|
|
844
|
+
["min", parts("min")],
|
|
845
|
+
["dim", parts("dim")],
|
|
846
|
+
["aug", parts("aug")],
|
|
847
|
+
["majb5", parts("maj", { alt: [[5, -1]] })],
|
|
848
|
+
["maj7", parts("maj", { seventh: "maj7" })],
|
|
849
|
+
["min7", parts("min", { seventh: "min7" })],
|
|
850
|
+
["dom7", parts("maj", { seventh: "min7" })],
|
|
851
|
+
["dim7", parts("dim", { seventh: "dim7" })],
|
|
852
|
+
["m7b5", parts("dim", { seventh: "min7" })],
|
|
853
|
+
["minMaj7", parts("min", { seventh: "maj7" })],
|
|
854
|
+
["minMaj9", parts("min", { seventh: "maj7", alt: [[9, 0]] })],
|
|
855
|
+
[
|
|
856
|
+
"minMaj11",
|
|
857
|
+
parts("min", {
|
|
858
|
+
seventh: "maj7",
|
|
859
|
+
alt: [
|
|
860
|
+
[9, 0],
|
|
861
|
+
[11, 0]
|
|
862
|
+
]
|
|
863
|
+
})
|
|
864
|
+
],
|
|
865
|
+
[
|
|
866
|
+
"minMaj13",
|
|
867
|
+
parts("min", {
|
|
868
|
+
seventh: "maj7",
|
|
869
|
+
alt: [
|
|
870
|
+
[9, 0],
|
|
871
|
+
[13, 0]
|
|
872
|
+
]
|
|
873
|
+
})
|
|
874
|
+
],
|
|
875
|
+
["aug7", parts("aug", { seventh: "min7" })],
|
|
876
|
+
["augMaj7", parts("aug", { seventh: "maj7" })],
|
|
877
|
+
["6", parts("maj", { add: [6] })],
|
|
878
|
+
["min6", parts("min", { add: [6] })],
|
|
879
|
+
["6/9", parts("maj", { add: [6, 9] })],
|
|
880
|
+
["sus2", parts("sus2")],
|
|
881
|
+
["sus4", parts("sus4")],
|
|
882
|
+
["add9", parts("maj", { add: [9] })],
|
|
883
|
+
["add11", parts("maj", { add: [11] })],
|
|
884
|
+
["maj9", parts("maj", { seventh: "maj7", alt: [[9, 0]] })],
|
|
885
|
+
["min9", parts("min", { seventh: "min7", alt: [[9, 0]] })],
|
|
886
|
+
["dom9", parts("maj", { seventh: "min7", alt: [[9, 0]] })],
|
|
887
|
+
["7b9", parts("maj", { seventh: "min7", alt: [[9, -1]] })],
|
|
888
|
+
["7#9", parts("maj", { seventh: "min7", alt: [[9, 1]] })],
|
|
889
|
+
["7#11", parts("maj", { seventh: "min7", alt: [[11, 1]] })],
|
|
890
|
+
["7b13", parts("maj", { seventh: "min7", alt: [[13, -1]] })],
|
|
722
891
|
// The eleventh chord as a chart voices it: the third gives way to the
|
|
723
892
|
// eleventh a semitone above it rather than sounding against it.
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
[
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
893
|
+
[
|
|
894
|
+
"11",
|
|
895
|
+
parts("maj", {
|
|
896
|
+
seventh: "min7",
|
|
897
|
+
alt: [
|
|
898
|
+
[9, 0],
|
|
899
|
+
[11, 0]
|
|
900
|
+
],
|
|
901
|
+
omit: [3]
|
|
902
|
+
})
|
|
903
|
+
],
|
|
904
|
+
[
|
|
905
|
+
"13",
|
|
906
|
+
parts("maj", {
|
|
907
|
+
seventh: "min7",
|
|
908
|
+
alt: [
|
|
909
|
+
[9, 0],
|
|
910
|
+
[13, 0]
|
|
911
|
+
]
|
|
912
|
+
})
|
|
913
|
+
],
|
|
914
|
+
["5", parts("power")],
|
|
915
|
+
["7sus4", parts("sus4", { seventh: "min7" })],
|
|
916
|
+
["7b5", parts("maj", { seventh: "min7", alt: [[5, -1]] })],
|
|
742
917
|
// The altered dominant as it is voiced from a lead sheet: a dominant seventh
|
|
743
918
|
// with a raised fifth and a raised ninth. The full seven-note altered stack
|
|
744
919
|
// has no seven-letter spelling, so it is not what this symbol denotes.
|
|
745
|
-
"7alt"
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
[
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
[
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
920
|
+
["7alt", parts("aug", { seventh: "min7", alt: [[9, 1]] })],
|
|
921
|
+
[
|
|
922
|
+
"13b9",
|
|
923
|
+
parts("maj", {
|
|
924
|
+
seventh: "min7",
|
|
925
|
+
alt: [
|
|
926
|
+
[9, -1],
|
|
927
|
+
[13, 0]
|
|
928
|
+
]
|
|
929
|
+
})
|
|
930
|
+
],
|
|
931
|
+
[
|
|
932
|
+
"maj13",
|
|
933
|
+
parts("maj", {
|
|
934
|
+
seventh: "maj7",
|
|
935
|
+
alt: [
|
|
936
|
+
[9, 0],
|
|
937
|
+
[13, 0]
|
|
938
|
+
]
|
|
939
|
+
})
|
|
940
|
+
],
|
|
941
|
+
["maj7#11", parts("maj", { seventh: "maj7", alt: [[11, 1]] })],
|
|
942
|
+
[
|
|
943
|
+
"min11",
|
|
944
|
+
parts("min", {
|
|
945
|
+
seventh: "min7",
|
|
946
|
+
alt: [
|
|
947
|
+
[9, 0],
|
|
948
|
+
[11, 0]
|
|
949
|
+
]
|
|
950
|
+
})
|
|
951
|
+
],
|
|
952
|
+
[
|
|
953
|
+
"min13",
|
|
954
|
+
parts("min", {
|
|
955
|
+
seventh: "min7",
|
|
956
|
+
alt: [
|
|
957
|
+
[9, 0],
|
|
958
|
+
[13, 0]
|
|
959
|
+
]
|
|
960
|
+
})
|
|
961
|
+
],
|
|
962
|
+
["minAdd9", parts("min", { add: [9] })],
|
|
963
|
+
["min6/9", parts("min", { add: [6, 9] })]
|
|
964
|
+
];
|
|
965
|
+
var QUALITY_ORDER = QUALITY_SPECS.map(([quality]) => quality);
|
|
966
|
+
var SPEC_BY_QUALITY = new Map(QUALITY_SPECS);
|
|
778
967
|
function normalizeChordSpec(spec) {
|
|
779
968
|
let base = spec.base;
|
|
780
969
|
const alterations = /* @__PURE__ */ new Map();
|
|
781
970
|
for (const alteration of spec.alterations ?? []) {
|
|
782
|
-
|
|
971
|
+
const { degree, alter } = alteration;
|
|
972
|
+
if (degree === 5) {
|
|
973
|
+
const current = alterations.get(FIFTH_KEY);
|
|
974
|
+
if (current !== void 0 && current.alter <= alter) {
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
alterations.set(FIFTH_KEY, { degree, alter });
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
alterations.set(`${degree}:${alter}`, { degree, alter });
|
|
783
981
|
}
|
|
784
982
|
const additions = new Set(spec.additions ?? []);
|
|
785
983
|
const omissions = new Set(spec.omissions ?? []);
|
|
786
|
-
const fifth = alterations.get(
|
|
984
|
+
const fifth = alterations.get(FIFTH_KEY)?.alter;
|
|
787
985
|
if (fifth !== void 0) {
|
|
788
986
|
if (base === "aug") {
|
|
789
987
|
base = "maj";
|
|
@@ -791,19 +989,19 @@ function normalizeChordSpec(spec) {
|
|
|
791
989
|
base = "min";
|
|
792
990
|
}
|
|
793
991
|
if (fifth === 0) {
|
|
794
|
-
alterations.delete(
|
|
992
|
+
alterations.delete(FIFTH_KEY);
|
|
795
993
|
} else if (base === "maj" && fifth === 1) {
|
|
796
994
|
base = "aug";
|
|
797
|
-
alterations.delete(
|
|
995
|
+
alterations.delete(FIFTH_KEY);
|
|
798
996
|
} else if (base === "min" && fifth === -1) {
|
|
799
997
|
base = "dim";
|
|
800
|
-
alterations.delete(
|
|
998
|
+
alterations.delete(FIFTH_KEY);
|
|
801
999
|
}
|
|
802
1000
|
}
|
|
803
1001
|
if (spec.seventh === void 0) {
|
|
804
|
-
for (const [degree, alter] of [...alterations]) {
|
|
1002
|
+
for (const [key, { degree, alter }] of [...alterations]) {
|
|
805
1003
|
if (degree !== 5 && alter === 0) {
|
|
806
|
-
alterations.delete(
|
|
1004
|
+
alterations.delete(key);
|
|
807
1005
|
additions.add(degree);
|
|
808
1006
|
}
|
|
809
1007
|
}
|
|
@@ -811,7 +1009,7 @@ function normalizeChordSpec(spec) {
|
|
|
811
1009
|
const normalized = {
|
|
812
1010
|
rootPc: spec.rootPc,
|
|
813
1011
|
base,
|
|
814
|
-
alterations: [...alterations].sort((a, b) => a
|
|
1012
|
+
alterations: [...alterations.values()].sort((a, b) => a.degree - b.degree || a.alter - b.alter),
|
|
815
1013
|
additions: [...additions].sort((a, b) => a - b),
|
|
816
1014
|
omissions: [...omissions].sort((a, b) => a - b)
|
|
817
1015
|
};
|
|
@@ -851,10 +1049,7 @@ function chordSpecIntervals(spec) {
|
|
|
851
1049
|
return specIntervals(assertChordSpec(spec));
|
|
852
1050
|
}
|
|
853
1051
|
var QUALITY_INTERVALS = Object.fromEntries(
|
|
854
|
-
|
|
855
|
-
quality,
|
|
856
|
-
specIntervals(normalizeChordSpec(QUALITY_SPECS[quality]))
|
|
857
|
-
])
|
|
1052
|
+
QUALITY_SPECS.map(([quality, spec]) => [quality, specIntervals(normalizeChordSpec(spec))])
|
|
858
1053
|
);
|
|
859
1054
|
function specKey(spec) {
|
|
860
1055
|
const normalized = normalizeChordSpec(spec);
|
|
@@ -867,16 +1062,14 @@ function specKey(spec) {
|
|
|
867
1062
|
].join("|");
|
|
868
1063
|
}
|
|
869
1064
|
var QUALITY_BY_KEY = new Map(
|
|
870
|
-
|
|
871
|
-
specKey(QUALITY_SPECS[quality]),
|
|
872
|
-
quality
|
|
873
|
-
])
|
|
1065
|
+
QUALITY_SPECS.map(([quality, spec]) => [specKey(spec), quality])
|
|
874
1066
|
);
|
|
875
1067
|
function chordSpecForQuality(quality, rootPc, bassPc) {
|
|
876
|
-
|
|
1068
|
+
const named = SPEC_BY_QUALITY.get(quality);
|
|
1069
|
+
if (named === void 0) {
|
|
877
1070
|
throw new InvalidInputError(`Unknown chord quality: ${String(quality)}`);
|
|
878
1071
|
}
|
|
879
|
-
const spec = normalizeChordSpec(
|
|
1072
|
+
const spec = normalizeChordSpec(named);
|
|
880
1073
|
spec.rootPc = rootPc;
|
|
881
1074
|
if (bassPc !== void 0) {
|
|
882
1075
|
spec.bassPc = bassPc;
|
|
@@ -892,10 +1085,10 @@ function chordSpecQuality(spec) {
|
|
|
892
1085
|
if (exact !== void 0) {
|
|
893
1086
|
return exact;
|
|
894
1087
|
}
|
|
895
|
-
const offsets = new Set(specIntervals(checked));
|
|
1088
|
+
const offsets = new Set(specIntervals({ ...checked, omissions: [] }));
|
|
896
1089
|
let nearest;
|
|
897
1090
|
let size = 0;
|
|
898
|
-
for (const quality of
|
|
1091
|
+
for (const quality of QUALITY_ORDER) {
|
|
899
1092
|
const template = QUALITY_INTERVALS[quality];
|
|
900
1093
|
if (template.length > size && template.every((offset) => offsets.has(offset))) {
|
|
901
1094
|
nearest = quality;
|
|
@@ -982,7 +1175,7 @@ function chordSpecFromIntervals(intervals) {
|
|
|
982
1175
|
function assertPart(value, allowed, name) {
|
|
983
1176
|
if (!allowed.includes(value)) {
|
|
984
1177
|
throw new InvalidInputError(
|
|
985
|
-
`${name} must be one of ${allowed.join(", ")}; received ${
|
|
1178
|
+
`${name} must be one of ${allowed.join(", ")}; received ${describeRejected(value)}`
|
|
986
1179
|
);
|
|
987
1180
|
}
|
|
988
1181
|
return value;
|
|
@@ -1034,13 +1227,23 @@ function assertChordSpec(spec) {
|
|
|
1034
1227
|
// src/theory/chord/index.ts
|
|
1035
1228
|
var MAX_CHORD_INTERVAL = 127;
|
|
1036
1229
|
function chordQualities() {
|
|
1037
|
-
return
|
|
1230
|
+
return [...QUALITY_ORDER];
|
|
1231
|
+
}
|
|
1232
|
+
function assertChordQuality(quality) {
|
|
1233
|
+
if (!Object.hasOwn(QUALITY_INTERVALS, quality)) {
|
|
1234
|
+
throw new InvalidInputError(`Unknown chord quality: ${String(quality)}`);
|
|
1235
|
+
}
|
|
1236
|
+
return quality;
|
|
1237
|
+
}
|
|
1238
|
+
function assertChord(chord, name = "chord") {
|
|
1239
|
+
const read = assertRecord(chord, name);
|
|
1240
|
+
assertFiniteNumber(read.rootPc, `${name}.rootPc`);
|
|
1241
|
+
assertChordQuality(read.quality);
|
|
1242
|
+
return read;
|
|
1038
1243
|
}
|
|
1039
1244
|
function chordFromDegree(degree, ext, key) {
|
|
1040
1245
|
assertDegree(degree, "chord degree");
|
|
1041
|
-
|
|
1042
|
-
throw new InvalidInputError(`Unknown chord quality: ${String(ext)}`);
|
|
1043
|
-
}
|
|
1246
|
+
assertChordQuality(ext);
|
|
1044
1247
|
const scale = toKeyScale(key);
|
|
1045
1248
|
const tones = scaleTonesInDegreeOrder(scale);
|
|
1046
1249
|
const length = tones.length;
|
|
@@ -1053,9 +1256,7 @@ function makeChord(rootPc, quality, bassPc) {
|
|
|
1053
1256
|
if (bassPc !== void 0) {
|
|
1054
1257
|
assertFiniteNumber(bassPc, "chord bassPc");
|
|
1055
1258
|
}
|
|
1056
|
-
|
|
1057
|
-
throw new InvalidInputError(`Unknown chord quality: ${String(quality)}`);
|
|
1058
|
-
}
|
|
1259
|
+
assertChordQuality(quality);
|
|
1059
1260
|
const chord = {
|
|
1060
1261
|
rootPc: pitchClassOf(rootPc),
|
|
1061
1262
|
quality,
|
|
@@ -1079,6 +1280,7 @@ function chordFromSpec(spec) {
|
|
|
1079
1280
|
return chord;
|
|
1080
1281
|
}
|
|
1081
1282
|
function chordSpecOf(chord) {
|
|
1283
|
+
assertRecord(chord, "chord");
|
|
1082
1284
|
assertFiniteNumber(chord.rootPc, "chord rootPc");
|
|
1083
1285
|
const rootPc = pitchClassOf(chord.rootPc);
|
|
1084
1286
|
const bassPc = chord.bassPc === void 0 ? void 0 : pitchClassOf(chord.bassPc);
|
|
@@ -1119,6 +1321,7 @@ function hasCustomIntervals(chord) {
|
|
|
1119
1321
|
return !matchesQualityIntervals(chord.quality, chord.intervals);
|
|
1120
1322
|
}
|
|
1121
1323
|
function chordFromSpan(span) {
|
|
1324
|
+
assertRecord(span, "chord span");
|
|
1122
1325
|
const chord = makeChord(span.rootPc, span.quality, span.bassPc);
|
|
1123
1326
|
if (span.intervals !== void 0) {
|
|
1124
1327
|
chord.intervals = copySpanIntervals(span.intervals);
|
|
@@ -1127,9 +1330,7 @@ function chordFromSpan(span) {
|
|
|
1127
1330
|
}
|
|
1128
1331
|
function spanFromChord(chord, startBeat) {
|
|
1129
1332
|
assertFiniteNumber(startBeat, "chord span startBeat");
|
|
1130
|
-
|
|
1131
|
-
throw new InvalidInputError(`Unknown chord quality: ${String(chord.quality)}`);
|
|
1132
|
-
}
|
|
1333
|
+
assertChord(chord);
|
|
1133
1334
|
const span = { rootPc: chord.rootPc, quality: chord.quality, startBeat };
|
|
1134
1335
|
if (chord.bassPc !== void 0) {
|
|
1135
1336
|
span.bassPc = chord.bassPc;
|
|
@@ -1140,6 +1341,7 @@ function spanFromChord(chord, startBeat) {
|
|
|
1140
1341
|
return span;
|
|
1141
1342
|
}
|
|
1142
1343
|
function transposeChord(chord, semitones) {
|
|
1344
|
+
assertChord(chord);
|
|
1143
1345
|
assertFiniteNumber(semitones, "semitones");
|
|
1144
1346
|
const steps = Math.round(semitones);
|
|
1145
1347
|
const moved = {
|
|
@@ -1171,59 +1373,145 @@ function chordToneSpellings(chord) {
|
|
|
1171
1373
|
}
|
|
1172
1374
|
return usable;
|
|
1173
1375
|
}
|
|
1174
|
-
function
|
|
1175
|
-
const
|
|
1376
|
+
function chordToneLetterOffsets(chord) {
|
|
1377
|
+
const hints = chordToneSpellings(chord);
|
|
1378
|
+
const rootHint = hints?.[chord.intervals.findIndex((interval) => pitchClassOf(interval) === 0)];
|
|
1379
|
+
if (hints === void 0 || rootHint === void 0) {
|
|
1380
|
+
return void 0;
|
|
1381
|
+
}
|
|
1382
|
+
return hints.map((hint) => diatonicLetterOf(hint.letter - rootHint.letter));
|
|
1383
|
+
}
|
|
1384
|
+
var ROLE_BY_LETTER_OFFSET = {
|
|
1385
|
+
0: "root",
|
|
1386
|
+
2: "third",
|
|
1387
|
+
4: "fifth",
|
|
1388
|
+
5: "sixth",
|
|
1389
|
+
6: "seventh"
|
|
1390
|
+
};
|
|
1391
|
+
function spelledToneRole(pitch, chord) {
|
|
1392
|
+
const offsets = chordToneLetterOffsets(chord);
|
|
1393
|
+
if (offsets === void 0) {
|
|
1394
|
+
return void 0;
|
|
1395
|
+
}
|
|
1396
|
+
const index = chord.intervals.findIndex(
|
|
1397
|
+
(interval) => pitchClassOf(chord.rootPc + interval) === pitchClassOf(pitch)
|
|
1398
|
+
);
|
|
1399
|
+
const offset = offsets[index];
|
|
1400
|
+
return offset === void 0 ? void 0 : ROLE_BY_LETTER_OFFSET[offset] ?? null;
|
|
1401
|
+
}
|
|
1402
|
+
function chordPcMask(chord, opts = {}) {
|
|
1403
|
+
assertRecord(chord, "chord");
|
|
1404
|
+
assertArray(chord.intervals, "chord intervals");
|
|
1405
|
+
let mask = 0;
|
|
1176
1406
|
for (const interval of chord.intervals) {
|
|
1177
|
-
|
|
1407
|
+
mask |= 1 << pitchClassOf(chord.rootPc + interval);
|
|
1178
1408
|
}
|
|
1179
1409
|
if (opts.includeBass !== false && chord.bassPc !== void 0) {
|
|
1180
|
-
|
|
1410
|
+
mask |= 1 << pitchClassOf(chord.bassPc);
|
|
1411
|
+
}
|
|
1412
|
+
return mask;
|
|
1413
|
+
}
|
|
1414
|
+
function hasPitchClass(mask, pc) {
|
|
1415
|
+
return (mask >> pc & 1) === 1;
|
|
1416
|
+
}
|
|
1417
|
+
function chordIntervalMask(chord) {
|
|
1418
|
+
let mask = 0;
|
|
1419
|
+
for (const interval of chord.intervals) {
|
|
1420
|
+
mask |= 1 << (interval % 12 + 12) % 12;
|
|
1421
|
+
}
|
|
1422
|
+
return mask;
|
|
1423
|
+
}
|
|
1424
|
+
function chordPitchClasses(chord, opts = {}) {
|
|
1425
|
+
const mask = chordPcMask(assertChord(chord), assertOptions(opts, "opts"));
|
|
1426
|
+
const pcs = [];
|
|
1427
|
+
for (let pc = 0; pc < 12; pc += 1) {
|
|
1428
|
+
if (hasPitchClass(mask, pc)) {
|
|
1429
|
+
pcs.push(pc);
|
|
1430
|
+
}
|
|
1181
1431
|
}
|
|
1182
|
-
return
|
|
1432
|
+
return pcs;
|
|
1183
1433
|
}
|
|
1184
1434
|
function intervalAboveRoot(pitch, chord) {
|
|
1185
|
-
return ((pitchClassOf(pitch) - pitchClassOf(chord.rootPc)) % 12 + 12) % 12;
|
|
1435
|
+
return ((pitchClassOf(pitch) - pitchClassOf(assertChord(chord).rootPc)) % 12 + 12) % 12;
|
|
1186
1436
|
}
|
|
1187
1437
|
function isChordMember(pitch, chord) {
|
|
1188
|
-
return chord === null ? false :
|
|
1189
|
-
}
|
|
1190
|
-
function chordToneOffsets(chord) {
|
|
1191
|
-
return new Set(chord.intervals.map((i) => (i % 12 + 12) % 12));
|
|
1438
|
+
return chord === null ? false : hasPitchClass(chordPcMask(chord), pitchClassOf(pitch));
|
|
1192
1439
|
}
|
|
1193
1440
|
function chordToneRole(pitch, chord) {
|
|
1441
|
+
assertRecord(chord, "chord");
|
|
1442
|
+
const spelled2 = spelledToneRole(pitch, chord);
|
|
1443
|
+
if (spelled2 !== void 0) {
|
|
1444
|
+
return spelled2;
|
|
1445
|
+
}
|
|
1194
1446
|
const interval = (pitchClassOf(pitch) - pitchClassOf(chord.rootPc) + 12) % 12;
|
|
1195
|
-
const tones =
|
|
1447
|
+
const tones = chordIntervalMask(chord);
|
|
1448
|
+
const sounds = (ic) => hasPitchClass(tones, ic);
|
|
1196
1449
|
if (interval === 0) {
|
|
1197
|
-
return "root";
|
|
1450
|
+
return sounds(0) ? "root" : null;
|
|
1198
1451
|
}
|
|
1199
1452
|
if (interval === 3 || interval === 4) {
|
|
1200
|
-
return
|
|
1453
|
+
return sounds(interval) && (interval !== 3 || !sounds(4)) ? "third" : null;
|
|
1201
1454
|
}
|
|
1202
1455
|
if (interval === 7) {
|
|
1203
|
-
return
|
|
1456
|
+
return sounds(7) ? "fifth" : null;
|
|
1204
1457
|
}
|
|
1205
1458
|
if (interval === 6) {
|
|
1206
|
-
return
|
|
1459
|
+
return sounds(6) && !sounds(7) ? "fifth" : null;
|
|
1207
1460
|
}
|
|
1208
1461
|
if (interval === 8) {
|
|
1209
|
-
return
|
|
1462
|
+
return sounds(8) && !sounds(7) ? "fifth" : null;
|
|
1210
1463
|
}
|
|
1211
1464
|
if (interval === 9) {
|
|
1212
|
-
const hasHigherSeventh =
|
|
1213
|
-
const isDiminishedSeventh =
|
|
1214
|
-
if (
|
|
1465
|
+
const hasHigherSeventh = sounds(10) || sounds(11);
|
|
1466
|
+
const isDiminishedSeventh = sounds(3) && sounds(6) && !hasHigherSeventh;
|
|
1467
|
+
if (sounds(9) && isDiminishedSeventh) {
|
|
1215
1468
|
return "seventh";
|
|
1216
1469
|
}
|
|
1217
|
-
if (
|
|
1470
|
+
if (sounds(9) && !hasHigherSeventh) {
|
|
1218
1471
|
return "sixth";
|
|
1219
1472
|
}
|
|
1220
1473
|
return null;
|
|
1221
1474
|
}
|
|
1222
1475
|
if (interval === 10 || interval === 11) {
|
|
1223
|
-
return
|
|
1476
|
+
return sounds(interval) ? "seventh" : null;
|
|
1224
1477
|
}
|
|
1225
1478
|
return null;
|
|
1226
1479
|
}
|
|
1480
|
+
var DEGREE_BY_ROLE = {
|
|
1481
|
+
root: 1,
|
|
1482
|
+
third: 3,
|
|
1483
|
+
fifth: 5,
|
|
1484
|
+
sixth: 6,
|
|
1485
|
+
seventh: 7
|
|
1486
|
+
};
|
|
1487
|
+
var TENSION_DEGREE = [1, 2, 2, 2, 3, 4, 4, 5, 6, 6, 7, 7];
|
|
1488
|
+
function degreeOfInterval(interval, chord) {
|
|
1489
|
+
const role = chordToneRole(pitchClassOf(chord.rootPc + interval), chord);
|
|
1490
|
+
if (role !== null) {
|
|
1491
|
+
return DEGREE_BY_ROLE[role];
|
|
1492
|
+
}
|
|
1493
|
+
return TENSION_DEGREE[(interval % 12 + 12) % 12];
|
|
1494
|
+
}
|
|
1495
|
+
function thirdSlotOf(chord) {
|
|
1496
|
+
const tones = chordIntervalMask(chord);
|
|
1497
|
+
if (hasPitchClass(tones, 4)) {
|
|
1498
|
+
return { kind: "third", interval: 4 };
|
|
1499
|
+
}
|
|
1500
|
+
if (hasPitchClass(tones, 3)) {
|
|
1501
|
+
return { kind: "third", interval: 3 };
|
|
1502
|
+
}
|
|
1503
|
+
const spec = chordSpecOf(chord);
|
|
1504
|
+
if (spec.base === "sus2") {
|
|
1505
|
+
return hasPitchClass(tones, 2) ? { kind: "suspension", interval: 2 } : { kind: "none" };
|
|
1506
|
+
}
|
|
1507
|
+
if (spec.base === "sus4" || spec.omissions.includes(3)) {
|
|
1508
|
+
return hasPitchClass(tones, 5) ? { kind: "suspension", interval: 5 } : { kind: "none" };
|
|
1509
|
+
}
|
|
1510
|
+
return { kind: "none" };
|
|
1511
|
+
}
|
|
1512
|
+
function displacedThirds(chord) {
|
|
1513
|
+
return thirdSlotOf(chord).kind === "suspension" ? [pitchClassOf(chord.rootPc + 3), pitchClassOf(chord.rootPc + 4)] : [];
|
|
1514
|
+
}
|
|
1227
1515
|
function classifyTriad(thirdIc, fifthIc) {
|
|
1228
1516
|
if (thirdIc === 4 && fifthIc === 8) {
|
|
1229
1517
|
return "aug";
|
|
@@ -1396,16 +1684,32 @@ function capChromaticAccidentals(preferred, alternative) {
|
|
|
1396
1684
|
return alternative !== void 0 && Math.abs(preferred.alter) - Math.abs(alternative.alter) >= 2 ? alternative : preferred;
|
|
1397
1685
|
}
|
|
1398
1686
|
var DIATONIC_LETTER_COUNT = 7;
|
|
1687
|
+
var MAX_JOINT_TONE_COUNT = DIATONIC_LETTER_COUNT + 1;
|
|
1688
|
+
var MAJOR_SEMITONES = [0, 2, 4, 5, 7, 9, 11];
|
|
1689
|
+
var IMPERFECT_DEGREES = /* @__PURE__ */ new Set([1, 2, 5, 6]);
|
|
1690
|
+
var DIATONIC_MODE_OFFSETS2 = MAJOR_SEMITONES.map(
|
|
1691
|
+
(tonic, mode) => MAJOR_SEMITONES.map((_, degree) => pitchClassOf((MAJOR_SEMITONES[(mode + degree) % 7] ?? 0) - tonic))
|
|
1692
|
+
);
|
|
1693
|
+
function degreeStrain(tonic, note) {
|
|
1694
|
+
const letterOffset = diatonicLetterOf(note.letter - tonic.letter);
|
|
1695
|
+
const offset = pitchClassOf(notePitchClass(note) - notePitchClass(tonic));
|
|
1696
|
+
let deviation = pitchClassOf(offset - (MAJOR_SEMITONES[letterOffset] ?? 0));
|
|
1697
|
+
if (deviation > 6) {
|
|
1698
|
+
deviation -= 12;
|
|
1699
|
+
}
|
|
1700
|
+
const strain = IMPERFECT_DEGREES.has(letterOffset) && deviation < 0 ? Math.abs(deviation + 1) : Math.abs(deviation);
|
|
1701
|
+
return Math.min(strain, 2);
|
|
1702
|
+
}
|
|
1399
1703
|
function isHeptatonic(key) {
|
|
1400
1704
|
return scaleTonesInDegreeOrder(key).length === DIATONIC_LETTER_COUNT;
|
|
1401
1705
|
}
|
|
1402
|
-
function
|
|
1706
|
+
function hasMinorThird(key) {
|
|
1403
1707
|
const tones = scaleTonesInDegreeOrder(key);
|
|
1404
1708
|
const root = pitchClassOf(key.rootPc);
|
|
1405
1709
|
return tones.includes(pitchClassOf(root + 3)) && !tones.includes(pitchClassOf(root + 4));
|
|
1406
1710
|
}
|
|
1407
1711
|
function leansFlat(tonic, key) {
|
|
1408
|
-
return tonic.alter < 0 ||
|
|
1712
|
+
return tonic.alter < 0 || hasMinorThird(key);
|
|
1409
1713
|
}
|
|
1410
1714
|
function gappedCandidates(tonic, offset, pc, preferFlat) {
|
|
1411
1715
|
const flat = letterFor(tonic, CHROMATIC_SPELLING_FLAT[offset], pc);
|
|
@@ -1417,22 +1721,64 @@ function gappedCandidates(tonic, offset, pc, preferFlat) {
|
|
|
1417
1721
|
const alternative = preferred === flat ? sharp : flat;
|
|
1418
1722
|
return alternative === void 0 || alternative.letter === preferred.letter ? [preferred] : [preferred, alternative];
|
|
1419
1723
|
}
|
|
1420
|
-
function
|
|
1724
|
+
function spellByParentMode(tonic, pcs, preferFlat) {
|
|
1725
|
+
const tonicPc = notePitchClass(tonic);
|
|
1726
|
+
let best;
|
|
1727
|
+
let bestAlter = 0;
|
|
1728
|
+
let bestSigned = 0;
|
|
1729
|
+
for (const offsets of DIATONIC_MODE_OFFSETS2) {
|
|
1730
|
+
const notes = [];
|
|
1731
|
+
let alter = 0;
|
|
1732
|
+
let signed = 0;
|
|
1733
|
+
for (const pc of pcs) {
|
|
1734
|
+
const degree = offsets.indexOf(pitchClassOf(pc - tonicPc));
|
|
1735
|
+
const note = letterFor(tonic, degree < 0 ? void 0 : degree, pc);
|
|
1736
|
+
if (note === void 0) {
|
|
1737
|
+
break;
|
|
1738
|
+
}
|
|
1739
|
+
notes.push(note);
|
|
1740
|
+
alter += Math.abs(note.alter);
|
|
1741
|
+
signed += note.alter;
|
|
1742
|
+
}
|
|
1743
|
+
if (notes.length !== pcs.length) {
|
|
1744
|
+
continue;
|
|
1745
|
+
}
|
|
1746
|
+
const evenlyLight = alter === bestAlter && (preferFlat ? signed < bestSigned : signed > bestSigned);
|
|
1747
|
+
const lighter = alter < bestAlter || evenlyLight;
|
|
1748
|
+
if (best === void 0 || lighter) {
|
|
1749
|
+
best = notes;
|
|
1750
|
+
bestAlter = alter;
|
|
1751
|
+
bestSigned = signed;
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
return best;
|
|
1755
|
+
}
|
|
1756
|
+
function assignDistinctLetters(candidates, tonic, duplicates) {
|
|
1757
|
+
const reserved = 1 << diatonicLetterOf(tonic.letter);
|
|
1421
1758
|
let best;
|
|
1759
|
+
let bestStrain = 0;
|
|
1422
1760
|
let bestAlter = 0;
|
|
1423
1761
|
let bestDeviations = 0;
|
|
1424
1762
|
for (let choice = 0; choice < 1 << candidates.length; choice += 1) {
|
|
1425
1763
|
const picked = [];
|
|
1426
1764
|
let letters = reserved;
|
|
1765
|
+
let spare = duplicates;
|
|
1766
|
+
let strain = 0;
|
|
1427
1767
|
let alter = 0;
|
|
1428
1768
|
let deviations = 0;
|
|
1429
1769
|
for (const [index, options] of candidates.entries()) {
|
|
1430
1770
|
const preferred = options[0];
|
|
1431
1771
|
const note = options[choice >> index & 1] ?? preferred;
|
|
1432
|
-
if (note === void 0
|
|
1772
|
+
if (note === void 0) {
|
|
1773
|
+
break;
|
|
1774
|
+
}
|
|
1775
|
+
const taken = (letters & 1 << note.letter) !== 0;
|
|
1776
|
+
if (taken && (spare === 0 || (reserved & 1 << note.letter) !== 0)) {
|
|
1433
1777
|
break;
|
|
1434
1778
|
}
|
|
1779
|
+
spare -= taken ? 1 : 0;
|
|
1435
1780
|
letters |= 1 << note.letter;
|
|
1781
|
+
strain += degreeStrain(tonic, note);
|
|
1436
1782
|
alter += Math.abs(note.alter);
|
|
1437
1783
|
deviations += note === preferred ? 0 : 1;
|
|
1438
1784
|
picked.push(note);
|
|
@@ -1440,8 +1786,10 @@ function assignDistinctLetters(candidates, reserved) {
|
|
|
1440
1786
|
if (picked.length !== candidates.length) {
|
|
1441
1787
|
continue;
|
|
1442
1788
|
}
|
|
1443
|
-
|
|
1789
|
+
const better = duplicates > 0 && strain !== bestStrain ? strain < bestStrain : alter !== bestAlter ? alter < bestAlter : deviations < bestDeviations;
|
|
1790
|
+
if (best === void 0 || better) {
|
|
1444
1791
|
best = picked;
|
|
1792
|
+
bestStrain = strain;
|
|
1445
1793
|
bestAlter = alter;
|
|
1446
1794
|
bestDeviations = deviations;
|
|
1447
1795
|
}
|
|
@@ -1449,18 +1797,33 @@ function assignDistinctLetters(candidates, reserved) {
|
|
|
1449
1797
|
return best;
|
|
1450
1798
|
}
|
|
1451
1799
|
function spellGappedScale(tonic, key) {
|
|
1800
|
+
const cacheKey = `${diatonicLetterOf(tonic.letter)}:${tonic.alter}:${key.rootPc}:${key.modeMask12}`;
|
|
1801
|
+
const cached = JOINT_SPELLINGS.get(cacheKey);
|
|
1802
|
+
if (cached !== void 0) {
|
|
1803
|
+
return cached.spellings;
|
|
1804
|
+
}
|
|
1805
|
+
const answer = readGappedScale(tonic, key);
|
|
1806
|
+
if (JOINT_SPELLINGS.size >= MAX_REMEMBERED_SCALES) {
|
|
1807
|
+
JOINT_SPELLINGS.clear();
|
|
1808
|
+
}
|
|
1809
|
+
JOINT_SPELLINGS.set(cacheKey, { spellings: answer });
|
|
1810
|
+
return answer;
|
|
1811
|
+
}
|
|
1812
|
+
var MAX_REMEMBERED_SCALES = 512;
|
|
1813
|
+
var JOINT_SPELLINGS = /* @__PURE__ */ new Map();
|
|
1814
|
+
function readGappedScale(tonic, key) {
|
|
1452
1815
|
const tones = scaleTonesInDegreeOrder(key);
|
|
1453
|
-
if (tones.length >
|
|
1816
|
+
if (tones.length > MAX_JOINT_TONE_COUNT) {
|
|
1454
1817
|
return void 0;
|
|
1455
1818
|
}
|
|
1456
1819
|
const tonicPc = pitchClassOf(naturalPitchClassOf(tonic.letter) + tonic.alter);
|
|
1457
1820
|
const tonicNote = { letter: diatonicLetterOf(tonic.letter), alter: tonic.alter };
|
|
1458
1821
|
const preferFlat = leansFlat(tonic, key);
|
|
1459
1822
|
const rest = tones.filter((pc) => pitchClassOf(pc - tonicPc) !== 0);
|
|
1460
|
-
const
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
);
|
|
1823
|
+
const candidates = rest.map((pc) => gappedCandidates(tonic, pitchClassOf(pc - tonicPc), pc, preferFlat));
|
|
1824
|
+
const chosen = spellByParentMode(tonicNote, rest, preferFlat) ?? // A scale that can hold a letter per tone does; only one that cannot writes
|
|
1825
|
+
// a letter twice, and then where the doubling falls is what is chosen.
|
|
1826
|
+
assignDistinctLetters(candidates, tonicNote, 0) ?? assignDistinctLetters(candidates, tonicNote, 1);
|
|
1464
1827
|
if (chosen === void 0) {
|
|
1465
1828
|
return void 0;
|
|
1466
1829
|
}
|
|
@@ -1498,7 +1861,7 @@ function spellByKey(pc, tonic, key) {
|
|
|
1498
1861
|
if (isScaleTone(pc, key)) {
|
|
1499
1862
|
const assigned = spellGappedScale(tonic, key)?.get(pitchClassOf(pc));
|
|
1500
1863
|
if (assigned !== void 0) {
|
|
1501
|
-
return assigned;
|
|
1864
|
+
return { letter: assigned.letter, alter: assigned.alter };
|
|
1502
1865
|
}
|
|
1503
1866
|
}
|
|
1504
1867
|
const [preferred] = gappedCandidates(tonic, offset, pc, leansFlat(tonic, key));
|
|
@@ -1523,13 +1886,39 @@ var CHORD_TONE_LETTER_STEPS = {
|
|
|
1523
1886
|
11: 6
|
|
1524
1887
|
// major seventh
|
|
1525
1888
|
};
|
|
1889
|
+
var LETTER_STEP_BY_ROLE = {
|
|
1890
|
+
root: 0,
|
|
1891
|
+
third: 2,
|
|
1892
|
+
fifth: 4,
|
|
1893
|
+
sixth: 5,
|
|
1894
|
+
seventh: 6
|
|
1895
|
+
};
|
|
1526
1896
|
var MAX_CONTEXT_ALTER = 2;
|
|
1527
|
-
function
|
|
1897
|
+
function chordToneLetterStep(pc, rootPc, chord) {
|
|
1898
|
+
if (chord !== void 0) {
|
|
1899
|
+
const offsets = chordToneLetterOffsets(chord);
|
|
1900
|
+
const index = chord.intervals.findIndex(
|
|
1901
|
+
(interval) => pitchClassOf(chord.rootPc + interval) === pitchClassOf(pc)
|
|
1902
|
+
);
|
|
1903
|
+
const offset = offsets?.[index];
|
|
1904
|
+
if (offset !== void 0) {
|
|
1905
|
+
return offset;
|
|
1906
|
+
}
|
|
1907
|
+
const role = chordToneRole(pc, chord);
|
|
1908
|
+
if (role !== null) {
|
|
1909
|
+
return LETTER_STEP_BY_ROLE[role];
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
return CHORD_TONE_LETTER_STEPS[pitchClassOf(pc - rootPc)];
|
|
1913
|
+
}
|
|
1914
|
+
function spellChordTone(pc, context, tonic, key) {
|
|
1915
|
+
const { chord } = context;
|
|
1916
|
+
const chordRoot = chord?.rootPc ?? context.chordRoot;
|
|
1528
1917
|
if (chordRoot === void 0) {
|
|
1529
1918
|
return void 0;
|
|
1530
1919
|
}
|
|
1531
|
-
const root = spellByKey(pitchClassOf(chordRoot), tonic, key);
|
|
1532
|
-
return letterFor(root,
|
|
1920
|
+
const root = chord === void 0 ? spellByKey(pitchClassOf(chordRoot), tonic, key) : chordRootSpelling(chord, tonic, key);
|
|
1921
|
+
return letterFor(root, chordToneLetterStep(pc, chordRoot, chord), pc);
|
|
1533
1922
|
}
|
|
1534
1923
|
function neighbourSpelling(neighbour, gap, letterStep, pc, tonic, key) {
|
|
1535
1924
|
if (neighbour === void 0 || pitchClassOf(neighbour - pc) !== pitchClassOf(gap)) {
|
|
@@ -1542,10 +1931,10 @@ function refineByContext(pc, tonic, key, context, keySpelling) {
|
|
|
1542
1931
|
if (pitchClassOf(pc) === tonicPc || isScaleTone(pc, key)) {
|
|
1543
1932
|
return keySpelling;
|
|
1544
1933
|
}
|
|
1545
|
-
const {
|
|
1934
|
+
const { previous, next } = context;
|
|
1546
1935
|
const candidates = [
|
|
1547
|
-
// The sounding chord names the pitch by
|
|
1548
|
-
|
|
1936
|
+
// The sounding chord names the pitch by the degree it writes it as.
|
|
1937
|
+
spellChordTone(pc, context, tonic, key),
|
|
1549
1938
|
// A semitone below what follows: the pitch is its leading tone, a letter down.
|
|
1550
1939
|
neighbourSpelling(next, 1, -1, pc, tonic, key),
|
|
1551
1940
|
// A semitone above what preceded: the pitch is that note inflected upward,
|
|
@@ -1569,6 +1958,7 @@ function spellPitchClass(pc, tonic, key, context) {
|
|
|
1569
1958
|
const note = toNoteData(tonic);
|
|
1570
1959
|
const scale = toKeyScale(key);
|
|
1571
1960
|
assertTonicOf(note, scale, "spellPitchClass");
|
|
1961
|
+
assertOptions(context, "context");
|
|
1572
1962
|
return spelledPitchClass(pc, note, scale, context);
|
|
1573
1963
|
}
|
|
1574
1964
|
function spelledPitchClass(pc, tonic, key, context) {
|
|
@@ -1577,15 +1967,18 @@ function spelledPitchClass(pc, tonic, key, context) {
|
|
|
1577
1967
|
}
|
|
1578
1968
|
function spellScale(tonic, key) {
|
|
1579
1969
|
const note = toNoteData(tonic);
|
|
1580
|
-
const scale = key;
|
|
1970
|
+
const scale = toKeyScale(key);
|
|
1581
1971
|
assertTonicOf(note, scale, "spellScale");
|
|
1582
1972
|
return scaleTonesInDegreeOrder(scale).map((pc) => spelledPitchClass(pc, note, scale));
|
|
1583
1973
|
}
|
|
1584
1974
|
function spellPitchClasses(pcs, tonic, key) {
|
|
1975
|
+
const classes = assertArray(pcs, "pcs");
|
|
1585
1976
|
const note = toNoteData(tonic);
|
|
1586
1977
|
const scale = toKeyScale(key);
|
|
1587
1978
|
assertTonicOf(note, scale, "spellPitchClasses");
|
|
1588
|
-
return
|
|
1979
|
+
return classes.map(
|
|
1980
|
+
(pc, index) => spelledPitchClass(assertPitchClass(pc, `pcs[${index}]`), note, scale)
|
|
1981
|
+
);
|
|
1589
1982
|
}
|
|
1590
1983
|
function chordLetterOffset(interval, chord) {
|
|
1591
1984
|
switch (interval) {
|
|
@@ -1599,11 +1992,11 @@ function chordLetterOffset(interval, chord) {
|
|
|
1599
1992
|
return 1;
|
|
1600
1993
|
case 3:
|
|
1601
1994
|
case 4:
|
|
1602
|
-
case 16:
|
|
1603
1995
|
return 2;
|
|
1604
1996
|
case 15:
|
|
1605
1997
|
return chord.intervals.some((i) => i === 3 || i === 4) ? 1 : 2;
|
|
1606
1998
|
case 5:
|
|
1999
|
+
case 16:
|
|
1607
2000
|
case 17:
|
|
1608
2001
|
case 18:
|
|
1609
2002
|
return 3;
|
|
@@ -1616,10 +2009,10 @@ function chordLetterOffset(interval, chord) {
|
|
|
1616
2009
|
return chord.quality === "dim7" ? 6 : 5;
|
|
1617
2010
|
case 20:
|
|
1618
2011
|
case 21:
|
|
2012
|
+
case 22:
|
|
1619
2013
|
return 5;
|
|
1620
2014
|
case 10:
|
|
1621
2015
|
case 11:
|
|
1622
|
-
case 22:
|
|
1623
2016
|
case 23:
|
|
1624
2017
|
return 6;
|
|
1625
2018
|
default: {
|
|
@@ -1630,22 +2023,14 @@ function chordLetterOffset(interval, chord) {
|
|
|
1630
2023
|
}
|
|
1631
2024
|
}
|
|
1632
2025
|
}
|
|
1633
|
-
function hintedLetterOffsets(chord) {
|
|
1634
|
-
const hints = chordToneSpellings(chord);
|
|
1635
|
-
const rootHint = hints?.[chord.intervals.findIndex((interval) => pitchClassOf(interval) === 0)];
|
|
1636
|
-
if (hints === void 0 || rootHint === void 0) {
|
|
1637
|
-
return void 0;
|
|
1638
|
-
}
|
|
1639
|
-
return hints.map((hint) => diatonicLetterOf(hint.letter - rootHint.letter));
|
|
1640
|
-
}
|
|
1641
2026
|
function spellChordFromRoot(chord, root) {
|
|
1642
|
-
return chordTonesFromRoot(chord, toNoteData(root));
|
|
2027
|
+
return chordTonesFromRoot(assertChord(chord), toNoteData(root));
|
|
1643
2028
|
}
|
|
1644
2029
|
function chordTonesFromRoot(chord, root) {
|
|
1645
2030
|
if (pitchClassOf(naturalPitchClassOf(root.letter) + root.alter) !== pitchClassOf(chord.rootPc)) {
|
|
1646
2031
|
throw new InvalidInputError("chord root spelling must match chord.rootPc");
|
|
1647
2032
|
}
|
|
1648
|
-
const hinted =
|
|
2033
|
+
const hinted = chordToneLetterOffsets(chord);
|
|
1649
2034
|
return chord.intervals.map((interval, index) => {
|
|
1650
2035
|
const letter = diatonicLetterOf(root.letter + (hinted?.[index] ?? chordLetterOffset(interval, chord)));
|
|
1651
2036
|
const pc = pitchClassOf(chord.rootPc + interval);
|
|
@@ -1653,6 +2038,7 @@ function chordTonesFromRoot(chord, root) {
|
|
|
1653
2038
|
});
|
|
1654
2039
|
}
|
|
1655
2040
|
function spellChord(chord, tonic, key) {
|
|
2041
|
+
assertChord(chord);
|
|
1656
2042
|
const note = toNoteData(tonic);
|
|
1657
2043
|
const scale = toKeyScale(key);
|
|
1658
2044
|
assertTonicOf(note, scale, "spellChord");
|
|
@@ -1669,16 +2055,26 @@ function spellPitch(pitch, tonic, key, context) {
|
|
|
1669
2055
|
const note = toNoteData(tonic);
|
|
1670
2056
|
const scale = toKeyScale(key);
|
|
1671
2057
|
assertTonicOf(note, scale, "spellPitch");
|
|
1672
|
-
|
|
1673
|
-
const
|
|
1674
|
-
const
|
|
1675
|
-
|
|
2058
|
+
assertOptions(context, "context");
|
|
2059
|
+
const rounded = Math.round(assertFiniteNumber(pitch, "pitch"));
|
|
2060
|
+
const spelled2 = spelledPitchClass(pitchClassOf(rounded), note, scale, context);
|
|
2061
|
+
const octave = (rounded - naturalPitchClassOf(spelled2.letter) - spelled2.alter) / 12 - 1;
|
|
2062
|
+
return { letter: spelled2.letter, alter: spelled2.alter, octave };
|
|
1676
2063
|
}
|
|
1677
2064
|
function noteNames(notes) {
|
|
2065
|
+
assertArray(notes, "notes");
|
|
1678
2066
|
return notes.map((note) => formatNote(toNoteData(note)));
|
|
1679
2067
|
}
|
|
1680
2068
|
|
|
1681
2069
|
// src/theory/counterpoint/internal.ts
|
|
2070
|
+
function assertQualityLabel(quality) {
|
|
2071
|
+
if (typeof quality !== "string" || !/^(P|M|m|A+|d+)$/.test(quality)) {
|
|
2072
|
+
throw new InvalidInputError(
|
|
2073
|
+
`interval.quality must be an interval quality; received ${describeRejected(quality)}`
|
|
2074
|
+
);
|
|
2075
|
+
}
|
|
2076
|
+
return quality;
|
|
2077
|
+
}
|
|
1682
2078
|
function pitchOf(value) {
|
|
1683
2079
|
return typeof value === "number" ? value : noteToMidi(value);
|
|
1684
2080
|
}
|
|
@@ -1686,8 +2082,10 @@ function simpleIntervalNumber(numberValue) {
|
|
|
1686
2082
|
return (Math.abs(numberValue) - 1) % 7 + 1;
|
|
1687
2083
|
}
|
|
1688
2084
|
function classifySpelledInterval(interval, twoVoice = true) {
|
|
1689
|
-
|
|
1690
|
-
const
|
|
2085
|
+
assertFlag(twoVoice, "twoVoice");
|
|
2086
|
+
const read = assertRecord(interval, "interval");
|
|
2087
|
+
const simple = simpleIntervalNumber(assertDegree(read.number, "interval.number"));
|
|
2088
|
+
const quality = assertQualityLabel(read.quality);
|
|
1691
2089
|
if (quality === "P") {
|
|
1692
2090
|
if (simple === 1 || simple === 5) {
|
|
1693
2091
|
return 0 /* PerfectConsonance */;
|
|
@@ -1738,23 +2136,26 @@ function motionBetween(from, to) {
|
|
|
1738
2136
|
return before === after ? "parallel" : "similar";
|
|
1739
2137
|
}
|
|
1740
2138
|
function voiceIndependence(lead, counter, opts) {
|
|
1741
|
-
|
|
2139
|
+
assertOptions(opts, "opts");
|
|
2140
|
+
const upper = assertArray(lead, "lead");
|
|
2141
|
+
const lower = assertArray(counter, "counter");
|
|
2142
|
+
if (upper.length !== lower.length) {
|
|
1742
2143
|
throw new InvalidInputError(
|
|
1743
|
-
`voiceIndependence needs the two lines aligned slot for slot; received ${
|
|
2144
|
+
`voiceIndependence needs the two lines aligned slot for slot; received ${upper.length} and ${lower.length}`
|
|
1744
2145
|
);
|
|
1745
2146
|
}
|
|
1746
2147
|
for (const [name, flags] of [
|
|
1747
2148
|
["leadAttacks", opts?.leadAttacks],
|
|
1748
2149
|
["counterAttacks", opts?.counterAttacks]
|
|
1749
2150
|
]) {
|
|
1750
|
-
if (flags !== void 0 && flags.length !==
|
|
2151
|
+
if (flags !== void 0 && flags.length !== upper.length) {
|
|
1751
2152
|
throw new InvalidInputError(
|
|
1752
|
-
`voiceIndependence needs ${name} aligned slot for slot with the lines; received ${flags.length} and ${
|
|
2153
|
+
`voiceIndependence needs ${name} aligned slot for slot with the lines; received ${flags.length} and ${upper.length}`
|
|
1753
2154
|
);
|
|
1754
2155
|
}
|
|
1755
2156
|
}
|
|
1756
|
-
const slots =
|
|
1757
|
-
const counterNote =
|
|
2157
|
+
const slots = upper.map((leadNote, index) => {
|
|
2158
|
+
const counterNote = lower[index] ?? null;
|
|
1758
2159
|
return {
|
|
1759
2160
|
lead: leadNote,
|
|
1760
2161
|
counter: counterNote,
|
|
@@ -1840,6 +2241,7 @@ function voiceIndependence(lead, counter, opts) {
|
|
|
1840
2241
|
}
|
|
1841
2242
|
|
|
1842
2243
|
// src/theory/counterpoint/index.ts
|
|
2244
|
+
var HIDDEN_PARALLEL_READINGS = Object.freeze(["fourPart", "twoVoice"]);
|
|
1843
2245
|
function createsVoiceCrossing(upper, lower) {
|
|
1844
2246
|
return pitchOf(upper) < pitchOf(lower);
|
|
1845
2247
|
}
|
|
@@ -1917,7 +2319,8 @@ function createsParallelUnison(aPrev, aCur, bPrev, bCur) {
|
|
|
1917
2319
|
}
|
|
1918
2320
|
return a1 === b1 && a0 === b0;
|
|
1919
2321
|
}
|
|
1920
|
-
function createsHiddenParallelPerfect(aPrev, aCur, bPrev, bCur) {
|
|
2322
|
+
function createsHiddenParallelPerfect(aPrev, aCur, bPrev, bCur, strictness = "fourPart") {
|
|
2323
|
+
assertOneOf(strictness, HIDDEN_PARALLEL_READINGS, "strictness");
|
|
1921
2324
|
const a0 = pitchOf(aPrev);
|
|
1922
2325
|
const a1 = pitchOf(aCur);
|
|
1923
2326
|
const b0 = pitchOf(bPrev);
|
|
@@ -1933,7 +2336,7 @@ function createsHiddenParallelPerfect(aPrev, aCur, bPrev, bCur) {
|
|
|
1933
2336
|
return false;
|
|
1934
2337
|
}
|
|
1935
2338
|
const upperMove = a1 >= b1 ? aMove : bMove;
|
|
1936
|
-
if (Math.abs(upperMove) <= 2) {
|
|
2339
|
+
if (strictness === "fourPart" && Math.abs(upperMove) <= 2) {
|
|
1937
2340
|
return false;
|
|
1938
2341
|
}
|
|
1939
2342
|
return true;
|
|
@@ -1948,7 +2351,12 @@ function createsBattuta(aPrev, aCur, bPrev, bCur) {
|
|
|
1948
2351
|
if (!bothVoicesMove(aMove, bMove) || similarMotion(aMove, bMove)) {
|
|
1949
2352
|
return false;
|
|
1950
2353
|
}
|
|
1951
|
-
|
|
2354
|
+
const nowClass = simpleClass(a1 - b1);
|
|
2355
|
+
if (nowClass !== 0) {
|
|
2356
|
+
return false;
|
|
2357
|
+
}
|
|
2358
|
+
const prevClass = simpleClass(a0 - b0);
|
|
2359
|
+
if (prevClass === nowClass) {
|
|
1952
2360
|
return false;
|
|
1953
2361
|
}
|
|
1954
2362
|
const upperMove = a1 >= b1 ? aMove : bMove;
|
|
@@ -1962,12 +2370,13 @@ function createsVoiceOverlap(upperPrev, upperCur, lowerPrev, lowerCur) {
|
|
|
1962
2370
|
return u1 < l0 || l1 > u0;
|
|
1963
2371
|
}
|
|
1964
2372
|
function exceedsSpacing(upper, lower, maxSemitones = 12) {
|
|
2373
|
+
assertFiniteNumber(maxSemitones, "maxSemitones");
|
|
1965
2374
|
return Math.abs(pitchOf(upper) - pitchOf(lower)) > maxSemitones;
|
|
1966
2375
|
}
|
|
1967
2376
|
function isLeadingToneResolution(prev, cur, key) {
|
|
1968
2377
|
const prevPitch = pitchOf(prev);
|
|
1969
2378
|
const curPitch = pitchOf(cur);
|
|
1970
|
-
const tonic = pitchClassOf(key.rootPc);
|
|
2379
|
+
const tonic = pitchClassOf(toKeyScale(key).rootPc);
|
|
1971
2380
|
const leading = (tonic + 11) % 12;
|
|
1972
2381
|
if (pitchClassOf(prevPitch) !== leading || pitchClassOf(curPitch) !== tonic) {
|
|
1973
2382
|
return false;
|
|
@@ -2009,7 +2418,18 @@ var PROFILE_WEIGHTS = Object.freeze({
|
|
|
2009
2418
|
var SAFETY_PROFILES = Object.keys(PROFILE_WEIGHTS);
|
|
2010
2419
|
function profileWeights(profile, overrides) {
|
|
2011
2420
|
const base = PROFILE_WEIGHTS[assertOneOf(profile, SAFETY_PROFILES, "safety profile")];
|
|
2012
|
-
|
|
2421
|
+
if (overrides === void 0) {
|
|
2422
|
+
return { ...base };
|
|
2423
|
+
}
|
|
2424
|
+
assertRecord(overrides, "overrides");
|
|
2425
|
+
const weights = { ...base };
|
|
2426
|
+
for (const [field, value] of Object.entries(overrides)) {
|
|
2427
|
+
if (value === void 0) {
|
|
2428
|
+
continue;
|
|
2429
|
+
}
|
|
2430
|
+
weights[field] = assertFiniteNumber(value, `weight ${field}`);
|
|
2431
|
+
}
|
|
2432
|
+
return weights;
|
|
2013
2433
|
}
|
|
2014
2434
|
var NoteSafety = /* @__PURE__ */ ((NoteSafety2) => {
|
|
2015
2435
|
NoteSafety2[NoteSafety2["Safe"] = 0] = "Safe";
|
|
@@ -2017,6 +2437,7 @@ var NoteSafety = /* @__PURE__ */ ((NoteSafety2) => {
|
|
|
2017
2437
|
NoteSafety2[NoteSafety2["Dissonant"] = 2] = "Dissonant";
|
|
2018
2438
|
return NoteSafety2;
|
|
2019
2439
|
})(NoteSafety || {});
|
|
2440
|
+
Object.freeze(NoteSafety);
|
|
2020
2441
|
var ReasonFlag = /* @__PURE__ */ ((ReasonFlag2) => {
|
|
2021
2442
|
ReasonFlag2[ReasonFlag2["ChordTone"] = 1] = "ChordTone";
|
|
2022
2443
|
ReasonFlag2[ReasonFlag2["Tension"] = 2] = "Tension";
|
|
@@ -2037,6 +2458,7 @@ var ReasonFlag = /* @__PURE__ */ ((ReasonFlag2) => {
|
|
|
2037
2458
|
ReasonFlag2[ReasonFlag2["MelodicTritone"] = 65536] = "MelodicTritone";
|
|
2038
2459
|
return ReasonFlag2;
|
|
2039
2460
|
})(ReasonFlag || {});
|
|
2461
|
+
Object.freeze(ReasonFlag);
|
|
2040
2462
|
var SUGGESTION_WINDOW = 12;
|
|
2041
2463
|
var MAX_SUGGESTIONS = 3;
|
|
2042
2464
|
var REASON_SEVERITY = [
|
|
@@ -2092,9 +2514,11 @@ function stepResolution(pitch, chord) {
|
|
|
2092
2514
|
function evaluateSafety(q, opts = {}) {
|
|
2093
2515
|
assertSafetyContext(q);
|
|
2094
2516
|
assertMidiPitch(q.candidatePitch, "candidatePitch");
|
|
2095
|
-
|
|
2517
|
+
const options = assertOptions(opts, "opts");
|
|
2518
|
+
return evaluateInternal(q, options.suggestions ?? true, options.partners ?? false);
|
|
2096
2519
|
}
|
|
2097
2520
|
function assertSafetyContext(q) {
|
|
2521
|
+
assertRecord(q, "safety query");
|
|
2098
2522
|
assertOneOf(q.profile, SAFETY_PROFILES, "safety profile");
|
|
2099
2523
|
if (q.prevPitch !== void 0) assertMidiPitch(q.prevPitch, "prevPitch");
|
|
2100
2524
|
if (q.vocalLow !== void 0) assertMidiPitch(q.vocalLow, "vocalLow");
|
|
@@ -2115,7 +2539,7 @@ function assertSafetyContext(q) {
|
|
|
2115
2539
|
}
|
|
2116
2540
|
}
|
|
2117
2541
|
}
|
|
2118
|
-
function evaluateInternal(q, collectSuggestions) {
|
|
2542
|
+
function evaluateInternal(q, collectSuggestions, collectPartners) {
|
|
2119
2543
|
let reasons = 0;
|
|
2120
2544
|
let level = 0 /* Safe */;
|
|
2121
2545
|
let resolveTo;
|
|
@@ -2186,41 +2610,58 @@ function evaluateInternal(q, collectSuggestions) {
|
|
|
2186
2610
|
reasons |= 512 /* MajorSeventh */;
|
|
2187
2611
|
}
|
|
2188
2612
|
}
|
|
2613
|
+
let bass = pitch;
|
|
2614
|
+
let top = pitch;
|
|
2615
|
+
for (const ov of q.otherVoices) {
|
|
2616
|
+
bass = Math.min(bass, ov.pitch);
|
|
2617
|
+
top = Math.max(top, ov.pitch);
|
|
2618
|
+
}
|
|
2619
|
+
const pairReasons = collectPartners ? new Array(q.otherVoices.length).fill(0) : void 0;
|
|
2620
|
+
const flagPair = (index, flag) => {
|
|
2621
|
+
reasons |= flag;
|
|
2622
|
+
if (pairReasons !== void 0) {
|
|
2623
|
+
pairReasons[index] = (pairReasons[index] ?? 0) | flag;
|
|
2624
|
+
}
|
|
2625
|
+
};
|
|
2189
2626
|
if (q.strongBeat) {
|
|
2190
|
-
const twoVoice = q.otherVoices.length === 1;
|
|
2191
2627
|
const prev = q.prevPitch;
|
|
2192
|
-
for (const ov of q.otherVoices) {
|
|
2193
|
-
|
|
2628
|
+
for (const [index, ov] of q.otherVoices.entries()) {
|
|
2629
|
+
const againstBass = pitch === bass || ov.pitch === bass;
|
|
2630
|
+
if (!createsVerticalDissonance(pitch, ov.pitch, againstBass)) {
|
|
2194
2631
|
continue;
|
|
2195
2632
|
}
|
|
2196
2633
|
if (chord && isStructuralChordDissonance(pitch, ov.pitch, chord)) {
|
|
2197
2634
|
continue;
|
|
2198
2635
|
}
|
|
2199
|
-
|
|
2636
|
+
if (chord && q.profile === "pop" && pitchClassOf(Math.abs(pitch - ov.pitch)) === 5 && isChordMember(pitch, chord) && isChordMember(ov.pitch, chord)) {
|
|
2637
|
+
continue;
|
|
2638
|
+
}
|
|
2639
|
+
flagPair(index, 1024 /* VerticalDissonance */);
|
|
2200
2640
|
raise(2 /* Dissonant */);
|
|
2201
|
-
if (prev !== void 0 && pitch === prev && ov.prevPitch !== void 0 && !createsVerticalDissonance(prev, ov.prevPitch,
|
|
2202
|
-
|
|
2641
|
+
if (prev !== void 0 && pitch === prev && ov.prevPitch !== void 0 && !createsVerticalDissonance(prev, ov.prevPitch, againstBass)) {
|
|
2642
|
+
flagPair(index, 16384 /* Suspension */);
|
|
2203
2643
|
}
|
|
2204
2644
|
}
|
|
2205
2645
|
}
|
|
2206
2646
|
if (q.prevPitch !== void 0) {
|
|
2207
2647
|
const prev = q.prevPitch;
|
|
2208
|
-
for (const ov of q.otherVoices) {
|
|
2648
|
+
for (const [index, ov] of q.otherVoices.entries()) {
|
|
2209
2649
|
if (ov.prevPitch === void 0) {
|
|
2210
2650
|
continue;
|
|
2211
2651
|
}
|
|
2212
2652
|
if (createsParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {
|
|
2213
|
-
|
|
2653
|
+
flagPair(index, 2048 /* ParallelPerfect */);
|
|
2214
2654
|
raise(parallelSeverity);
|
|
2215
2655
|
}
|
|
2216
|
-
|
|
2217
|
-
|
|
2656
|
+
const outerPair = pitch === top && ov.pitch === bass || pitch === bass && ov.pitch === top;
|
|
2657
|
+
if (outerPair && createsHiddenParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {
|
|
2658
|
+
flagPair(index, 4096 /* HiddenParallel */);
|
|
2218
2659
|
raise(parallelSeverity);
|
|
2219
2660
|
}
|
|
2220
2661
|
const crossedNow = pitch - ov.pitch;
|
|
2221
2662
|
const crossedPrev = prev - ov.prevPitch;
|
|
2222
2663
|
if (crossedNow !== 0 && crossedPrev !== 0 && crossedNow > 0 !== crossedPrev > 0) {
|
|
2223
|
-
|
|
2664
|
+
flagPair(index, 8192 /* VoiceCrossing */);
|
|
2224
2665
|
raise(parallelSeverity);
|
|
2225
2666
|
}
|
|
2226
2667
|
}
|
|
@@ -2236,6 +2677,17 @@ function evaluateInternal(q, collectSuggestions) {
|
|
|
2236
2677
|
result.suggestions = suggestions;
|
|
2237
2678
|
}
|
|
2238
2679
|
}
|
|
2680
|
+
if (pairReasons !== void 0) {
|
|
2681
|
+
const partners = [];
|
|
2682
|
+
pairReasons.forEach((pair, index) => {
|
|
2683
|
+
if (pair !== 0) {
|
|
2684
|
+
partners.push({ index, reasons: pair });
|
|
2685
|
+
}
|
|
2686
|
+
});
|
|
2687
|
+
if (partners.length > 0) {
|
|
2688
|
+
result.partners = partners;
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2239
2691
|
return result;
|
|
2240
2692
|
}
|
|
2241
2693
|
function findSafeNearby(q, candidate) {
|
|
@@ -2245,7 +2697,7 @@ function findSafeNearby(q, candidate) {
|
|
|
2245
2697
|
if (p < 0 || p > 127) {
|
|
2246
2698
|
continue;
|
|
2247
2699
|
}
|
|
2248
|
-
if (evaluateInternal({ ...q, candidatePitch: p }, false).safety === 0 /* Safe */) {
|
|
2700
|
+
if (evaluateInternal({ ...q, candidatePitch: p }, false, false).safety === 0 /* Safe */) {
|
|
2249
2701
|
out.push(p);
|
|
2250
2702
|
if (out.length >= MAX_SUGGESTIONS) {
|
|
2251
2703
|
return out;
|
|
@@ -2269,7 +2721,7 @@ var REASON_TEXT = [
|
|
|
2269
2721
|
[2048 /* ParallelPerfect */, "Parallel perfect interval with another voice"],
|
|
2270
2722
|
[4096 /* HiddenParallel */, "Hidden perfect interval by similar motion"],
|
|
2271
2723
|
[8192 /* VoiceCrossing */, "Crosses another voice"],
|
|
2272
|
-
[128 /* LargeLeap */, "
|
|
2724
|
+
[128 /* LargeLeap */, "Forbidden melodic leap \u2014 a seventh or wider than an octave"],
|
|
2273
2725
|
[32768 /* NeedsResolution */, "Dissonance awaiting a stepwise resolution"],
|
|
2274
2726
|
[2 /* Tension */, "Chord tension"],
|
|
2275
2727
|
[8 /* ScaleTone */, "Scale tone, not in the chord"],
|
|
@@ -2314,7 +2766,7 @@ function enumerateSafePitches(q, pitchLow, pitchHigh) {
|
|
|
2314
2766
|
const low = Math.max(pitchLow, q.vocalLow ?? pitchLow);
|
|
2315
2767
|
const high = Math.min(pitchHigh, q.vocalHigh ?? pitchHigh);
|
|
2316
2768
|
for (let pitch = high; pitch >= low; pitch -= 1) {
|
|
2317
|
-
const result = evaluateInternal({ ...q, candidatePitch: pitch }, false);
|
|
2769
|
+
const result = evaluateInternal({ ...q, candidatePitch: pitch }, false, false);
|
|
2318
2770
|
if (result.safety === 2 /* Dissonant */) {
|
|
2319
2771
|
continue;
|
|
2320
2772
|
}
|
|
@@ -2327,7 +2779,7 @@ function enumerateSafePitches(q, pitchLow, pitchHigh) {
|
|
|
2327
2779
|
return [...chordTones, ...others];
|
|
2328
2780
|
}
|
|
2329
2781
|
|
|
2330
|
-
// src/theory/symbol/
|
|
2782
|
+
// src/theory/symbol/grammar.ts
|
|
2331
2783
|
var ROOT_RE = /^[A-Ga-g](?:#{1,2}|b{1,2}|x)?/;
|
|
2332
2784
|
var BASS_RE = /^[A-Ga-g](?:#{1,2}|b{1,2}|x)?$/;
|
|
2333
2785
|
var DEFAULT_SYSTEM = "english";
|
|
@@ -2430,51 +2882,6 @@ var QUALITY_MAP = {
|
|
|
2430
2882
|
h: "m7b5",
|
|
2431
2883
|
h7: "m7b5"
|
|
2432
2884
|
};
|
|
2433
|
-
var CANONICAL_SUFFIX = {
|
|
2434
|
-
maj: "",
|
|
2435
|
-
min: "m",
|
|
2436
|
-
dim: "dim",
|
|
2437
|
-
aug: "aug",
|
|
2438
|
-
maj7: "maj7",
|
|
2439
|
-
min7: "m7",
|
|
2440
|
-
dom7: "7",
|
|
2441
|
-
dim7: "dim7",
|
|
2442
|
-
m7b5: "m7b5",
|
|
2443
|
-
minMaj7: "mMaj7",
|
|
2444
|
-
minMaj9: "mMaj9",
|
|
2445
|
-
minMaj11: "mMaj11",
|
|
2446
|
-
minMaj13: "mMaj13",
|
|
2447
|
-
aug7: "aug7",
|
|
2448
|
-
augMaj7: "augMaj7",
|
|
2449
|
-
majb5: "maj(b5)",
|
|
2450
|
-
"6": "6",
|
|
2451
|
-
min6: "m6",
|
|
2452
|
-
"6/9": "6/9",
|
|
2453
|
-
sus2: "sus2",
|
|
2454
|
-
sus4: "sus4",
|
|
2455
|
-
add9: "add9",
|
|
2456
|
-
add11: "add11",
|
|
2457
|
-
maj9: "maj9",
|
|
2458
|
-
min9: "m9",
|
|
2459
|
-
dom9: "9",
|
|
2460
|
-
"7b9": "7b9",
|
|
2461
|
-
"7#9": "7#9",
|
|
2462
|
-
"7#11": "7#11",
|
|
2463
|
-
"7b13": "7b13",
|
|
2464
|
-
"11": "11",
|
|
2465
|
-
"13": "13",
|
|
2466
|
-
"5": "5",
|
|
2467
|
-
"7sus4": "7sus4",
|
|
2468
|
-
"7b5": "7b5",
|
|
2469
|
-
"7alt": "7alt",
|
|
2470
|
-
"13b9": "13b9",
|
|
2471
|
-
maj13: "maj13",
|
|
2472
|
-
"maj7#11": "maj7#11",
|
|
2473
|
-
min11: "m11",
|
|
2474
|
-
min13: "m13",
|
|
2475
|
-
minAdd9: "madd9",
|
|
2476
|
-
"min6/9": "m6/9"
|
|
2477
|
-
};
|
|
2478
2885
|
function knownQuality(suffix) {
|
|
2479
2886
|
return Object.hasOwn(QUALITY_MAP, suffix) ? QUALITY_MAP[suffix] : void 0;
|
|
2480
2887
|
}
|
|
@@ -2486,6 +2893,7 @@ var BASE_TOKENS = [
|
|
|
2486
2893
|
["dim", "dim"],
|
|
2487
2894
|
["aug", "aug"],
|
|
2488
2895
|
["maj", "major"],
|
|
2896
|
+
["Maj", "major"],
|
|
2489
2897
|
["m", "min"],
|
|
2490
2898
|
["-", "min"],
|
|
2491
2899
|
["o", "dim"],
|
|
@@ -2495,6 +2903,10 @@ var BASE_TOKENS = [
|
|
|
2495
2903
|
["\u0394", "major"],
|
|
2496
2904
|
["^", "major"]
|
|
2497
2905
|
];
|
|
2906
|
+
var COMPOUND_TOKENS = [
|
|
2907
|
+
["\xF8", { base: "dim", seventh: "min7" }],
|
|
2908
|
+
["h", { base: "dim", seventh: "min7" }]
|
|
2909
|
+
];
|
|
2498
2910
|
var MAJOR_MARKERS = [
|
|
2499
2911
|
["maj", "word"],
|
|
2500
2912
|
["Maj", "word"],
|
|
@@ -2513,6 +2925,27 @@ var ADDED_DEGREES = {
|
|
|
2513
2925
|
};
|
|
2514
2926
|
var ALTERABLE = { "5": 5, "9": 9, "11": 11, "13": 13 };
|
|
2515
2927
|
var OMITTABLE = { "1": 1, "3": 3, "5": 5 };
|
|
2928
|
+
function alterationKey(degree, alter) {
|
|
2929
|
+
return `${degree}:${alter}`;
|
|
2930
|
+
}
|
|
2931
|
+
function writeAlteration(draft, degree, alter) {
|
|
2932
|
+
if (draft.implied.delete(degree)) {
|
|
2933
|
+
draft.alterations.delete(alterationKey(degree, 0));
|
|
2934
|
+
}
|
|
2935
|
+
draft.alterations.set(alterationKey(degree, alter), { degree, alter });
|
|
2936
|
+
}
|
|
2937
|
+
function implyAlteration(draft, degree) {
|
|
2938
|
+
draft.implied.add(degree);
|
|
2939
|
+
draft.alterations.set(alterationKey(degree, 0), { degree, alter: 0 });
|
|
2940
|
+
}
|
|
2941
|
+
function altersDegree(draft, degree) {
|
|
2942
|
+
for (const alteration of draft.alterations.values()) {
|
|
2943
|
+
if (alteration.degree === degree) {
|
|
2944
|
+
return true;
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
return false;
|
|
2948
|
+
}
|
|
2516
2949
|
function matchToken(text, at, table) {
|
|
2517
2950
|
let found;
|
|
2518
2951
|
for (const [token, value] of table) {
|
|
@@ -2536,15 +2969,18 @@ function stackTo(draft, top) {
|
|
|
2536
2969
|
draft.additions.add(6);
|
|
2537
2970
|
return;
|
|
2538
2971
|
}
|
|
2539
|
-
draft.seventh = draft.major ? "maj7" : draft.base === "dim" ? "dim7" : "min7";
|
|
2972
|
+
draft.seventh = draft.major ? "maj7" : draft.seventhOverBase ?? (draft.base === "dim" ? "dim7" : "min7");
|
|
2540
2973
|
if (top >= 9) {
|
|
2541
|
-
draft
|
|
2974
|
+
implyAlteration(draft, 9);
|
|
2542
2975
|
}
|
|
2543
2976
|
if (top === 11) {
|
|
2544
|
-
draft
|
|
2977
|
+
implyAlteration(draft, 11);
|
|
2978
|
+
if (!draft.major && draft.base === "maj") {
|
|
2979
|
+
draft.omissions.add(3);
|
|
2980
|
+
}
|
|
2545
2981
|
}
|
|
2546
2982
|
if (top === 13) {
|
|
2547
|
-
draft
|
|
2983
|
+
implyAlteration(draft, 13);
|
|
2548
2984
|
}
|
|
2549
2985
|
}
|
|
2550
2986
|
function applyFigure(draft, text, at) {
|
|
@@ -2560,7 +2996,7 @@ function applyFigure(draft, text, at) {
|
|
|
2560
2996
|
if (text.startsWith("alt", at)) {
|
|
2561
2997
|
draft.base = "aug";
|
|
2562
2998
|
draft.seventh = "min7";
|
|
2563
|
-
draft
|
|
2999
|
+
writeAlteration(draft, 9, 1);
|
|
2564
3000
|
return at + 3;
|
|
2565
3001
|
}
|
|
2566
3002
|
if (text.startsWith("add", at)) {
|
|
@@ -2592,13 +3028,37 @@ function applyFigure(draft, text, at) {
|
|
|
2592
3028
|
if (degree === void 0) {
|
|
2593
3029
|
return void 0;
|
|
2594
3030
|
}
|
|
2595
|
-
|
|
3031
|
+
if (degree.value === 5 && (accidental === void 0 || writesFifth(draft))) {
|
|
3032
|
+
return void 0;
|
|
3033
|
+
}
|
|
3034
|
+
writeAlteration(draft, degree.value, accidental?.value ?? 0);
|
|
2596
3035
|
return degree.next;
|
|
2597
3036
|
}
|
|
3037
|
+
function writesFifth(draft) {
|
|
3038
|
+
return draft.base === "dim" || draft.base === "aug" || altersDegree(draft, 5);
|
|
3039
|
+
}
|
|
2598
3040
|
function matchNumberToken(text, at, table) {
|
|
2599
3041
|
const entries = Object.entries(table).map(([token, value]) => [token, value]);
|
|
2600
3042
|
return matchToken(text, at, entries);
|
|
2601
3043
|
}
|
|
3044
|
+
function openings(core) {
|
|
3045
|
+
const found = [];
|
|
3046
|
+
for (const [token, value] of COMPOUND_TOKENS) {
|
|
3047
|
+
if (core.startsWith(token)) {
|
|
3048
|
+
found.push({ length: token.length, base: value.base, seventh: value.seventh });
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
for (const [token, value] of BASE_TOKENS) {
|
|
3052
|
+
if (!core.startsWith(token)) {
|
|
3053
|
+
continue;
|
|
3054
|
+
}
|
|
3055
|
+
found.push(
|
|
3056
|
+
value === "major" ? { length: token.length, major: true, glyph: token === "\u0394" || token === "^" } : { length: token.length, base: value }
|
|
3057
|
+
);
|
|
3058
|
+
}
|
|
3059
|
+
found.sort((a, b) => b.length - a.length);
|
|
3060
|
+
return [...found, void 0];
|
|
3061
|
+
}
|
|
2602
3062
|
function structuralSpec(text) {
|
|
2603
3063
|
let core = "";
|
|
2604
3064
|
const groups = [];
|
|
@@ -2626,24 +3086,37 @@ function structuralSpec(text) {
|
|
|
2626
3086
|
if (inside) {
|
|
2627
3087
|
return void 0;
|
|
2628
3088
|
}
|
|
3089
|
+
for (const opening of openings(core)) {
|
|
3090
|
+
const spec = readCore(core, groups, opening);
|
|
3091
|
+
if (spec !== void 0) {
|
|
3092
|
+
return spec;
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
return void 0;
|
|
3096
|
+
}
|
|
3097
|
+
function readCore(core, groups, opening) {
|
|
2629
3098
|
const draft = {
|
|
2630
3099
|
base: "maj",
|
|
2631
3100
|
alterations: /* @__PURE__ */ new Map(),
|
|
2632
3101
|
additions: /* @__PURE__ */ new Set(),
|
|
2633
3102
|
omissions: /* @__PURE__ */ new Set(),
|
|
3103
|
+
implied: /* @__PURE__ */ new Set(),
|
|
2634
3104
|
major: false
|
|
2635
3105
|
};
|
|
2636
3106
|
let at = 0;
|
|
2637
|
-
const opening = matchToken(core, at, BASE_TOKENS);
|
|
2638
3107
|
let markerGlyph = false;
|
|
2639
3108
|
let markerAfterBase = false;
|
|
2640
3109
|
if (opening !== void 0) {
|
|
2641
|
-
at = opening.
|
|
2642
|
-
if (opening.
|
|
3110
|
+
at = opening.length;
|
|
3111
|
+
if (opening.major === true) {
|
|
2643
3112
|
draft.major = true;
|
|
2644
|
-
markerGlyph =
|
|
2645
|
-
} else {
|
|
2646
|
-
draft.base = opening.
|
|
3113
|
+
markerGlyph = opening.glyph === true;
|
|
3114
|
+
} else if (opening.base !== void 0) {
|
|
3115
|
+
draft.base = opening.base;
|
|
3116
|
+
if (opening.seventh !== void 0) {
|
|
3117
|
+
draft.seventh = opening.seventh;
|
|
3118
|
+
draft.seventhOverBase = opening.seventh;
|
|
3119
|
+
}
|
|
2647
3120
|
}
|
|
2648
3121
|
}
|
|
2649
3122
|
if (!draft.major) {
|
|
@@ -2688,7 +3161,7 @@ function structuralSpec(text) {
|
|
|
2688
3161
|
rootPc: 0,
|
|
2689
3162
|
base: draft.base,
|
|
2690
3163
|
...draft.seventh === void 0 ? {} : { seventh: draft.seventh },
|
|
2691
|
-
alterations: [...draft.alterations
|
|
3164
|
+
alterations: [...draft.alterations.values()],
|
|
2692
3165
|
additions: [...draft.additions],
|
|
2693
3166
|
omissions: [...draft.omissions]
|
|
2694
3167
|
};
|
|
@@ -2715,7 +3188,8 @@ function rootSplits(text, system) {
|
|
|
2715
3188
|
}
|
|
2716
3189
|
assertNamingSystem(system);
|
|
2717
3190
|
const splits = [];
|
|
2718
|
-
|
|
3191
|
+
const longest = Math.min(text.length, maxNoteNameLength(system));
|
|
3192
|
+
for (let end = longest; end > 0; end -= 1) {
|
|
2719
3193
|
const root = bareNote(text.slice(0, end), system);
|
|
2720
3194
|
if (root !== void 0) {
|
|
2721
3195
|
splits.push({ root, rest: text.slice(end) });
|
|
@@ -2753,82 +3227,53 @@ function chordAfterRoot(root, rest, system) {
|
|
|
2753
3227
|
const spec = suffixSpec(rest);
|
|
2754
3228
|
return spec === void 0 ? void 0 : makeSpelledChord(root, spec);
|
|
2755
3229
|
}
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
}
|
|
2804
|
-
if (data.toneSpellings !== void 0) {
|
|
2805
|
-
copy.toneSpellings = data.toneSpellings;
|
|
2806
|
-
}
|
|
2807
|
-
return copy;
|
|
2808
|
-
}
|
|
2809
|
-
function toChordData(value) {
|
|
2810
|
-
if (typeof value === "string") {
|
|
2811
|
-
return parseChordSymbol(value);
|
|
2812
|
-
}
|
|
2813
|
-
if (typeof value === "object" && value !== null) {
|
|
2814
|
-
const data = "toJSON" in value && typeof value.toJSON === "function" ? value.toJSON() : value;
|
|
2815
|
-
return normalizedChord(data);
|
|
2816
|
-
}
|
|
2817
|
-
throw new InvalidInputError(
|
|
2818
|
-
`chord must be a chord symbol or chord data; received ${typeof value}`
|
|
2819
|
-
);
|
|
2820
|
-
}
|
|
2821
|
-
function symbolName(note, system) {
|
|
2822
|
-
const name = formatNote(note, { system });
|
|
2823
|
-
return system === "german" ? `${name.slice(0, 1).toUpperCase()}${name.slice(1)}` : name;
|
|
2824
|
-
}
|
|
2825
|
-
function pitchClassName(pc, hint, system, flats, inheritFlats) {
|
|
2826
|
-
if (flats === void 0 && hint !== void 0 && noteToPitchClass(hint) === pc) {
|
|
2827
|
-
return symbolName(hint, system);
|
|
2828
|
-
}
|
|
2829
|
-
const note = midiToNote(60 + pc, flats ?? inheritFlats ? "flat" : "sharp");
|
|
2830
|
-
return symbolName({ letter: note.letter, alter: note.alter }, system);
|
|
2831
|
-
}
|
|
3230
|
+
|
|
3231
|
+
// src/theory/symbol/suffix.ts
|
|
3232
|
+
var CANONICAL_SUFFIX = {
|
|
3233
|
+
maj: "",
|
|
3234
|
+
min: "m",
|
|
3235
|
+
dim: "dim",
|
|
3236
|
+
aug: "aug",
|
|
3237
|
+
maj7: "maj7",
|
|
3238
|
+
min7: "m7",
|
|
3239
|
+
dom7: "7",
|
|
3240
|
+
dim7: "dim7",
|
|
3241
|
+
m7b5: "m7b5",
|
|
3242
|
+
minMaj7: "mMaj7",
|
|
3243
|
+
minMaj9: "mMaj9",
|
|
3244
|
+
minMaj11: "mMaj11",
|
|
3245
|
+
minMaj13: "mMaj13",
|
|
3246
|
+
aug7: "aug7",
|
|
3247
|
+
augMaj7: "augMaj7",
|
|
3248
|
+
majb5: "maj(b5)",
|
|
3249
|
+
"6": "6",
|
|
3250
|
+
min6: "m6",
|
|
3251
|
+
"6/9": "6/9",
|
|
3252
|
+
sus2: "sus2",
|
|
3253
|
+
sus4: "sus4",
|
|
3254
|
+
add9: "add9",
|
|
3255
|
+
add11: "add11",
|
|
3256
|
+
maj9: "maj9",
|
|
3257
|
+
min9: "m9",
|
|
3258
|
+
dom9: "9",
|
|
3259
|
+
"7b9": "7b9",
|
|
3260
|
+
"7#9": "7#9",
|
|
3261
|
+
"7#11": "7#11",
|
|
3262
|
+
"7b13": "7b13",
|
|
3263
|
+
"11": "11",
|
|
3264
|
+
"13": "13",
|
|
3265
|
+
"5": "5",
|
|
3266
|
+
"7sus4": "7sus4",
|
|
3267
|
+
"7b5": "7b5",
|
|
3268
|
+
"7alt": "7alt",
|
|
3269
|
+
"13b9": "13b9",
|
|
3270
|
+
maj13: "maj13",
|
|
3271
|
+
"maj7#11": "maj7#11",
|
|
3272
|
+
min11: "m11",
|
|
3273
|
+
min13: "m13",
|
|
3274
|
+
minAdd9: "madd9",
|
|
3275
|
+
"min6/9": "m6/9"
|
|
3276
|
+
};
|
|
2832
3277
|
var CORE_CANDIDATES = chordQualities().map((quality) => [quality, chordSpecForQuality(quality, 0)]).filter(([, spec]) => spec.alterations.every(({ degree, alter }) => degree === 5 || alter === 0));
|
|
2833
3278
|
function carriesAlteration(spec, part) {
|
|
2834
3279
|
return spec.alterations.some(
|
|
@@ -2900,42 +3345,338 @@ function specSuffix(spec) {
|
|
|
2900
3345
|
const figures = figureText(spec, coreSpec);
|
|
2901
3346
|
return figures.length === 0 ? text : `${text}(${figures.join(",")})`;
|
|
2902
3347
|
}
|
|
3348
|
+
|
|
3349
|
+
// src/theory/symbol/spelling.ts
|
|
3350
|
+
function bareSpelling(note) {
|
|
3351
|
+
return { letter: note.letter, alter: note.alter };
|
|
3352
|
+
}
|
|
3353
|
+
function plainSpelling(pc, asFlat) {
|
|
3354
|
+
return bareSpelling(midiToNote(60 + pc, asFlat ? "flat" : "sharp"));
|
|
3355
|
+
}
|
|
3356
|
+
function symbolName(note, system) {
|
|
3357
|
+
const name = formatNote(note, { system });
|
|
3358
|
+
return system === "german" ? `${name.slice(0, 1).toUpperCase()}${name.slice(1)}` : name;
|
|
3359
|
+
}
|
|
3360
|
+
function isWrittenSpelling(spelling) {
|
|
3361
|
+
const written = midiToNote(
|
|
3362
|
+
60 + noteToPitchClass(spelling),
|
|
3363
|
+
spelling.alter < 0 ? "flat" : "sharp"
|
|
3364
|
+
);
|
|
3365
|
+
return written.letter === spelling.letter && written.alter === spelling.alter;
|
|
3366
|
+
}
|
|
3367
|
+
function pitchClassName(pc, hint, system, flats, inheritFlats) {
|
|
3368
|
+
if (flats === void 0 && hint !== void 0 && noteToPitchClass(hint) === pc) {
|
|
3369
|
+
return symbolName(hint, system);
|
|
3370
|
+
}
|
|
3371
|
+
return symbolName(plainSpelling(pc, flats ?? inheritFlats ?? false), system);
|
|
3372
|
+
}
|
|
3373
|
+
function onAccidentalSide(spelling, flats) {
|
|
3374
|
+
return flats === void 0 || spelling.alter === 0 || spelling.alter < 0 === flats;
|
|
3375
|
+
}
|
|
3376
|
+
function bySameStep(spelling, from, to) {
|
|
3377
|
+
return bareSpelling(transposeByInterval(spelling, spelledInterval(from, to)));
|
|
3378
|
+
}
|
|
3379
|
+
function spellingAbove(root, letters, pc) {
|
|
3380
|
+
const letter = ((root.letter + letters) % 7 + 7) % 7;
|
|
3381
|
+
const natural = noteToPitchClass({ letter, alter: 0 });
|
|
3382
|
+
return { letter, alter: ((pc - natural) % 12 + 18) % 12 - 6 };
|
|
3383
|
+
}
|
|
3384
|
+
function carriedBass(spelling, from, to, asFlat, isOwnSpelling) {
|
|
3385
|
+
const carried = bySameStep(spelling, from, to);
|
|
3386
|
+
return isOwnSpelling(carried) || isWrittenSpelling(carried) ? carried : plainSpelling(noteToPitchClass(carried), asFlat);
|
|
3387
|
+
}
|
|
3388
|
+
function hintFor(hint, pc) {
|
|
3389
|
+
return hint !== void 0 && noteToPitchClass(hint) === pc ? hint : void 0;
|
|
3390
|
+
}
|
|
3391
|
+
function rootSpellingFor(pc, hint, flats) {
|
|
3392
|
+
return hint !== void 0 && onAccidentalSide(hint, flats) ? hint : plainSpelling(pc, flats ?? false);
|
|
3393
|
+
}
|
|
3394
|
+
|
|
3395
|
+
// src/theory/symbol/index.ts
|
|
3396
|
+
function parseChordSymbol(text, opts) {
|
|
3397
|
+
return unwrapParse(tryParseChordSymbol(text, opts));
|
|
3398
|
+
}
|
|
3399
|
+
function tryParseChordSymbol(text, opts) {
|
|
3400
|
+
const asked = assertOptions(opts, "opts");
|
|
3401
|
+
try {
|
|
3402
|
+
if (typeof text !== "string") {
|
|
3403
|
+
throw new InvalidInputError(`chord symbol must be a string; received ${typeof text}`);
|
|
3404
|
+
}
|
|
3405
|
+
const system = asked.system ?? DEFAULT_SYSTEM;
|
|
3406
|
+
const trimmed = text.trim().replaceAll("\u266F", "#").replaceAll("\u266D", "b").replaceAll("\u{1D12A}", "x").replaceAll("\u{1D12B}", "bb").replaceAll("\u2206", "\u0394");
|
|
3407
|
+
const splits = rootSplits(trimmed, system);
|
|
3408
|
+
if (splits.length === 0) {
|
|
3409
|
+
throw new InvalidInputError(`Invalid chord symbol: ${describeRejected(text)}`);
|
|
3410
|
+
}
|
|
3411
|
+
for (const { root, rest } of splits) {
|
|
3412
|
+
const chord = chordAfterRoot(root, rest, system);
|
|
3413
|
+
if (chord !== void 0) {
|
|
3414
|
+
return { ok: true, value: chord };
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
throw new InvalidInputError(`Unrecognized chord quality: ${describeRejected(text)}`);
|
|
3418
|
+
} catch (error) {
|
|
3419
|
+
return parseFailure(error);
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
2903
3422
|
function formatChordSymbol(chord, opts) {
|
|
2904
3423
|
const data = toChordData(chord);
|
|
2905
3424
|
if (!Object.hasOwn(CANONICAL_SUFFIX, data.quality)) {
|
|
2906
3425
|
throw new InvalidInputError(`Unknown chord quality: ${String(data.quality)}`);
|
|
2907
3426
|
}
|
|
2908
|
-
const
|
|
3427
|
+
const asked = assertOptions(opts, "opts");
|
|
3428
|
+
const system = asked.system ?? DEFAULT_SYSTEM;
|
|
3429
|
+
const flats = asked.flats;
|
|
2909
3430
|
const rootPc = pitchClassOf(data.rootPc);
|
|
2910
|
-
const rootHint = data.rootSpelling;
|
|
2911
|
-
const
|
|
3431
|
+
const rootHint = hintFor(data.rootSpelling, rootPc);
|
|
3432
|
+
const rootSpelling = rootSpellingFor(rootPc, rootHint, flats);
|
|
2912
3433
|
const suffix = specSuffix(chordSpecOf(data));
|
|
2913
|
-
let symbol = `${
|
|
3434
|
+
let symbol = `${symbolName(rootSpelling, system)}${suffix}`;
|
|
2914
3435
|
if (data.bassPc !== void 0 && pitchClassOf(data.bassPc) !== rootPc) {
|
|
2915
|
-
const inheritFlats = rootHint !== void 0 && noteToPitchClass(rootHint) === rootPc ? rootHint.alter < 0 : void 0;
|
|
2916
3436
|
const bassPc = pitchClassOf(data.bassPc);
|
|
2917
|
-
|
|
3437
|
+
const bassHint = hintFor(data.bassSpelling, bassPc);
|
|
3438
|
+
const tonePcs = new Set(chordPitchClasses(data, { includeBass: false }));
|
|
3439
|
+
const ownSpelling = tonePcs.has(bassPc) ? spellingAbove(
|
|
3440
|
+
rootSpelling,
|
|
3441
|
+
(degreeOfInterval(pitchClassOf(bassPc - rootPc), data) ?? 1) - 1,
|
|
3442
|
+
bassPc
|
|
3443
|
+
) : void 0;
|
|
3444
|
+
const bassName = rootHint !== void 0 && bassHint !== void 0 ? symbolName(
|
|
3445
|
+
carriedBass(
|
|
3446
|
+
bassHint,
|
|
3447
|
+
rootHint,
|
|
3448
|
+
rootSpelling,
|
|
3449
|
+
flats ?? rootSpelling.alter < 0,
|
|
3450
|
+
(spelling) => ownSpelling !== void 0 && spelling.letter === ownSpelling.letter && spelling.alter === ownSpelling.alter
|
|
3451
|
+
),
|
|
3452
|
+
system
|
|
3453
|
+
) : pitchClassName(bassPc, bassHint, system, flats, rootSpelling.alter < 0);
|
|
3454
|
+
symbol += `/${bassName}`;
|
|
2918
3455
|
}
|
|
2919
3456
|
return symbol;
|
|
2920
3457
|
}
|
|
3458
|
+
function transposeChordSpellings(spellings, semitones, flats) {
|
|
3459
|
+
const byLetter = (spelling) => spelling === void 0 ? void 0 : bareSpelling(transposeNote(spelling, semitones));
|
|
3460
|
+
const movedRoot = byLetter(spellings.rootSpelling);
|
|
3461
|
+
const movedBass = byLetter(spellings.bassSpelling);
|
|
3462
|
+
const side = [movedRoot, spellings.rootSpelling, movedBass, spellings.bassSpelling].find(
|
|
3463
|
+
(spelling) => spelling !== void 0 && spelling.alter !== 0
|
|
3464
|
+
);
|
|
3465
|
+
const preferFlats = flats ?? (side !== void 0 && side.alter < 0);
|
|
3466
|
+
const octaveOnly = pitchClassOf(semitones) === 0;
|
|
3467
|
+
const plainName = (spelling, asFlat) => plainSpelling(noteToPitchClass(spelling), asFlat);
|
|
3468
|
+
const writable = (spelling) => octaveOnly || isWrittenSpelling(spelling) ? spelling : plainName(spelling, preferFlats);
|
|
3469
|
+
const root = spellings.rootSpelling;
|
|
3470
|
+
const bass = spellings.bassSpelling;
|
|
3471
|
+
const carriesBass = (candidate) => {
|
|
3472
|
+
if (root === void 0 || bass === void 0 || octaveOnly || !isWrittenSpelling(bass)) {
|
|
3473
|
+
return true;
|
|
3474
|
+
}
|
|
3475
|
+
const carried = bySameStep(bass, root, candidate);
|
|
3476
|
+
return isWrittenSpelling(carried) && onAccidentalSide(carried, flats);
|
|
3477
|
+
};
|
|
3478
|
+
const moved = {};
|
|
3479
|
+
if (movedRoot !== void 0) {
|
|
3480
|
+
const asked = flats !== void 0 && !octaveOnly && !isWrittenSpelling(movedRoot);
|
|
3481
|
+
const candidates = [
|
|
3482
|
+
...octaveOnly || isWrittenSpelling(movedRoot) ? [movedRoot] : [],
|
|
3483
|
+
plainName(movedRoot, preferFlats),
|
|
3484
|
+
...asked ? [movedRoot] : [],
|
|
3485
|
+
plainName(movedRoot, !preferFlats)
|
|
3486
|
+
];
|
|
3487
|
+
moved.rootSpelling = candidates.find(carriesBass) ?? plainName(movedRoot, preferFlats);
|
|
3488
|
+
}
|
|
3489
|
+
const step = root !== void 0 && moved.rootSpelling !== void 0 ? spelledInterval(root, moved.rootSpelling) : void 0;
|
|
3490
|
+
const byStep = (spelling) => bareSpelling(
|
|
3491
|
+
step === void 0 ? transposeNote(spelling, semitones) : transposeByInterval(spelling, step)
|
|
3492
|
+
);
|
|
3493
|
+
if (bass !== void 0) {
|
|
3494
|
+
moved.bassSpelling = writable(byStep(bass));
|
|
3495
|
+
}
|
|
3496
|
+
if (spellings.toneSpellings !== void 0) {
|
|
3497
|
+
moved.toneSpellings = spellings.toneSpellings.map(byStep);
|
|
3498
|
+
}
|
|
3499
|
+
return moved;
|
|
3500
|
+
}
|
|
2921
3501
|
function transposeChordSymbol(text, semitones, opts) {
|
|
2922
3502
|
assertFiniteSemitones(semitones);
|
|
2923
3503
|
const chord = parseChordSymbol(text, opts);
|
|
2924
3504
|
const transposed = { ...chord, rootPc: pitchClassOf(chord.rootPc + semitones) };
|
|
2925
|
-
if (chord.rootSpelling !== void 0) {
|
|
2926
|
-
const hint = transposeNote(chord.rootSpelling, semitones);
|
|
2927
|
-
transposed.rootSpelling = { letter: hint.letter, alter: hint.alter };
|
|
2928
|
-
}
|
|
2929
3505
|
if (chord.bassPc !== void 0) {
|
|
2930
3506
|
transposed.bassPc = pitchClassOf(chord.bassPc + semitones);
|
|
2931
3507
|
}
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
transposed.
|
|
3508
|
+
const spellings = transposeChordSpellings(chord, semitones, opts?.flats);
|
|
3509
|
+
if (spellings.rootSpelling !== void 0) {
|
|
3510
|
+
transposed.rootSpelling = spellings.rootSpelling;
|
|
3511
|
+
}
|
|
3512
|
+
if (spellings.bassSpelling !== void 0) {
|
|
3513
|
+
transposed.bassSpelling = spellings.bassSpelling;
|
|
3514
|
+
}
|
|
3515
|
+
if (spellings.toneSpellings !== void 0) {
|
|
3516
|
+
transposed.toneSpellings = spellings.toneSpellings;
|
|
2935
3517
|
}
|
|
2936
3518
|
return formatChordSymbol(transposed, opts);
|
|
2937
3519
|
}
|
|
2938
3520
|
|
|
3521
|
+
// src/theory/symbol/coerce.ts
|
|
3522
|
+
function normalizedChord(data) {
|
|
3523
|
+
if (!Array.isArray(data.intervals)) {
|
|
3524
|
+
throw new InvalidInputError("chord.intervals must be an array of semitone offsets");
|
|
3525
|
+
}
|
|
3526
|
+
const copy = {
|
|
3527
|
+
rootPc: pitchClassOf(assertFiniteNumber(data.rootPc, "chord.rootPc")),
|
|
3528
|
+
quality: data.quality,
|
|
3529
|
+
// Each offset is checked rather than copied blind: a chord holding a NaN
|
|
3530
|
+
// interval voices, spells and formats as a chord that looks real, and the
|
|
3531
|
+
// failure surfaces wherever the number is finally used.
|
|
3532
|
+
intervals: data.intervals.map(
|
|
3533
|
+
(interval, index) => assertFiniteNumber(interval, `chord.intervals[${index}]`)
|
|
3534
|
+
)
|
|
3535
|
+
};
|
|
3536
|
+
if (data.bassPc !== void 0) {
|
|
3537
|
+
copy.bassPc = pitchClassOf(assertFiniteNumber(data.bassPc, "chord.bassPc"));
|
|
3538
|
+
}
|
|
3539
|
+
if (data.rootSpelling !== void 0) {
|
|
3540
|
+
copy.rootSpelling = data.rootSpelling;
|
|
3541
|
+
}
|
|
3542
|
+
if (data.bassSpelling !== void 0) {
|
|
3543
|
+
copy.bassSpelling = data.bassSpelling;
|
|
3544
|
+
}
|
|
3545
|
+
if (data.toneSpellings !== void 0) {
|
|
3546
|
+
copy.toneSpellings = data.toneSpellings;
|
|
3547
|
+
}
|
|
3548
|
+
return copy;
|
|
3549
|
+
}
|
|
3550
|
+
function toChordData(value) {
|
|
3551
|
+
if (typeof value === "string") {
|
|
3552
|
+
return parseChordSymbol(value);
|
|
3553
|
+
}
|
|
3554
|
+
if (typeof value === "object" && value !== null) {
|
|
3555
|
+
const data = "toJSON" in value && typeof value.toJSON === "function" ? value.toJSON() : value;
|
|
3556
|
+
return normalizedChord(data);
|
|
3557
|
+
}
|
|
3558
|
+
throw new InvalidInputError(
|
|
3559
|
+
`chord must be a chord symbol or chord data; received ${typeof value}`
|
|
3560
|
+
);
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
// src/theory/tendency/index.ts
|
|
3564
|
+
function hasInterval(chord, semitones) {
|
|
3565
|
+
return chord.intervals.some((interval) => pitchClassOf(interval) === semitones);
|
|
3566
|
+
}
|
|
3567
|
+
function isDiatonicChord(chord, key) {
|
|
3568
|
+
return chordPitchClasses(chord).every((pc) => isScaleTone(pc, key));
|
|
3569
|
+
}
|
|
3570
|
+
function hasMajorThird(chord) {
|
|
3571
|
+
return hasInterval(chord, 4);
|
|
3572
|
+
}
|
|
3573
|
+
function soundsDominantSeventh(chord) {
|
|
3574
|
+
return hasMajorThird(chord) && hasInterval(chord, 10);
|
|
3575
|
+
}
|
|
3576
|
+
function soundsMajorTriad(chord) {
|
|
3577
|
+
return hasMajorThird(chord) && !hasInterval(chord, 10) && !hasInterval(chord, 11);
|
|
3578
|
+
}
|
|
3579
|
+
function hasDominantSonority(chord) {
|
|
3580
|
+
return soundsMajorTriad(chord) || soundsDominantSeventh(chord);
|
|
3581
|
+
}
|
|
3582
|
+
function isDominantChordOf(chord, key) {
|
|
3583
|
+
if (pitchClassOf(chord.rootPc - key.rootPc) !== 7) {
|
|
3584
|
+
return false;
|
|
3585
|
+
}
|
|
3586
|
+
if (hasMajorThird(chord)) {
|
|
3587
|
+
return true;
|
|
3588
|
+
}
|
|
3589
|
+
const suspended = !hasInterval(chord, 3) && (hasInterval(chord, 5) || hasInterval(chord, 2));
|
|
3590
|
+
return suspended && hasInterval(chord, 10);
|
|
3591
|
+
}
|
|
3592
|
+
function isNeapolitanChordOf(chord, key) {
|
|
3593
|
+
return pitchClassOf(chord.rootPc - key.rootPc) === 1 && soundsMajorTriad(chord) && !isDiatonicChord(chord, key);
|
|
3594
|
+
}
|
|
3595
|
+
var DOMINANT_DEGREE = 5;
|
|
3596
|
+
function heptatonicFrameOf(key) {
|
|
3597
|
+
return scaleTonesInDegreeOrder(key).length === 7 ? key : majorKey(pitchClassOf(key.rootPc));
|
|
3598
|
+
}
|
|
3599
|
+
function degreeTriad(tones, index) {
|
|
3600
|
+
const root = tones[index] ?? 0;
|
|
3601
|
+
return {
|
|
3602
|
+
third: pitchClassOf((tones[(index + 2) % tones.length] ?? 0) - root),
|
|
3603
|
+
fifth: pitchClassOf((tones[(index + 4) % tones.length] ?? 0) - root)
|
|
3604
|
+
};
|
|
3605
|
+
}
|
|
3606
|
+
function tonicizableDegrees(key) {
|
|
3607
|
+
const tones = scaleTonesInDegreeOrder(heptatonicFrameOf(key));
|
|
3608
|
+
const minor = isMinorMask(key.modeMask12);
|
|
3609
|
+
const out = [];
|
|
3610
|
+
for (let index = 0; index < tones.length; index += 1) {
|
|
3611
|
+
const rootPc = tones[index];
|
|
3612
|
+
if (rootPc === void 0) {
|
|
3613
|
+
continue;
|
|
3614
|
+
}
|
|
3615
|
+
const triad = degreeTriad(tones, index);
|
|
3616
|
+
if (triad.fifth !== 7) {
|
|
3617
|
+
continue;
|
|
3618
|
+
}
|
|
3619
|
+
const dominantDegree = minor && index + 1 === DOMINANT_DEGREE;
|
|
3620
|
+
out.push({ degreeNumber: index + 1, rootPc, lower: triad.third === 3 && !dominantDegree });
|
|
3621
|
+
}
|
|
3622
|
+
return out;
|
|
3623
|
+
}
|
|
3624
|
+
function appliedDominantTarget(rootPc, key) {
|
|
3625
|
+
const target = pitchClassOf(rootPc);
|
|
3626
|
+
return tonicizableDegrees(key).find(
|
|
3627
|
+
(degree) => degree.degreeNumber !== 1 && degree.rootPc === target
|
|
3628
|
+
) ?? null;
|
|
3629
|
+
}
|
|
3630
|
+
function leadingTonePcOf(key) {
|
|
3631
|
+
return pitchClassOf(key.rootPc - 1);
|
|
3632
|
+
}
|
|
3633
|
+
var LEADING_TONE_QUALITIES = /* @__PURE__ */ new Set(["dim", "dim7", "m7b5"]);
|
|
3634
|
+
function soundsDiminishedTriad(chord) {
|
|
3635
|
+
return hasInterval(chord, 3) && hasInterval(chord, 6) && !hasInterval(chord, 7);
|
|
3636
|
+
}
|
|
3637
|
+
function isLeadingToneChordOf(chord, key) {
|
|
3638
|
+
return pitchClassOf(chord.rootPc) === leadingTonePcOf(key) && soundsDiminishedTriad(chord);
|
|
3639
|
+
}
|
|
3640
|
+
function isFunctioningLeadingTone(chord, key) {
|
|
3641
|
+
const role = chordToneRole(leadingTonePcOf(key), chord);
|
|
3642
|
+
if (role === "third") {
|
|
3643
|
+
return pitchClassOf(chord.rootPc) === pitchClassOf(key.rootPc + 7);
|
|
3644
|
+
}
|
|
3645
|
+
if (role === "root") {
|
|
3646
|
+
return soundsDiminishedTriad(chord);
|
|
3647
|
+
}
|
|
3648
|
+
return false;
|
|
3649
|
+
}
|
|
3650
|
+
function seventhPcOf(chord) {
|
|
3651
|
+
for (const interval of chord.intervals) {
|
|
3652
|
+
const pc = pitchClassOf(chord.rootPc + interval);
|
|
3653
|
+
if (chordToneRole(pc, chord) === "seventh") {
|
|
3654
|
+
return pc;
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
return void 0;
|
|
3658
|
+
}
|
|
3659
|
+
function fifthPcOf(chord) {
|
|
3660
|
+
for (const interval of chord.intervals) {
|
|
3661
|
+
const pc = pitchClassOf(chord.rootPc + interval);
|
|
3662
|
+
if (chordToneRole(pc, chord) === "fifth") {
|
|
3663
|
+
return pc;
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
return void 0;
|
|
3667
|
+
}
|
|
3668
|
+
function isDescendingStep(interval) {
|
|
3669
|
+
return interval.number === 2 && (interval.semitones === -1 || interval.semitones === -2);
|
|
3670
|
+
}
|
|
3671
|
+
var THIRD_SEMITONES = [3, 4];
|
|
3672
|
+
function isFrustratedLeadingTone(fromPitch, toPitch, nextChord) {
|
|
3673
|
+
if (!THIRD_SEMITONES.includes(fromPitch - toPitch)) {
|
|
3674
|
+
return false;
|
|
3675
|
+
}
|
|
3676
|
+
const fifthPc = fifthPcOf(nextChord);
|
|
3677
|
+
return fifthPc !== void 0 && pitchClassOf(toPitch) === fifthPc;
|
|
3678
|
+
}
|
|
3679
|
+
|
|
2939
3680
|
export {
|
|
2940
3681
|
maskFromOffsets,
|
|
2941
3682
|
MAJOR_MASK,
|
|
@@ -2954,6 +3695,7 @@ export {
|
|
|
2954
3695
|
OCTATONIC_HALF_WHOLE_MASK,
|
|
2955
3696
|
OCTATONIC_WHOLE_HALF_MASK,
|
|
2956
3697
|
CHROMATIC_MASK,
|
|
3698
|
+
assertModeMask,
|
|
2957
3699
|
LYDIAN_DOMINANT_MASK,
|
|
2958
3700
|
MIXOLYDIAN_B13_MASK,
|
|
2959
3701
|
LOCRIAN_NATURAL2_MASK,
|
|
@@ -2972,9 +3714,11 @@ export {
|
|
|
2972
3714
|
resolveScaleName,
|
|
2973
3715
|
namedScaleMask,
|
|
2974
3716
|
requireScaleMask,
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
3717
|
+
isMinorMask,
|
|
3718
|
+
variantOfMask,
|
|
3719
|
+
assertKeyVariant,
|
|
3720
|
+
scaleNameOfMask,
|
|
3721
|
+
tryResolveKeyName,
|
|
2978
3722
|
toKeyScale,
|
|
2979
3723
|
isScaleTone,
|
|
2980
3724
|
nearestScaleTone,
|
|
@@ -2998,17 +3742,25 @@ export {
|
|
|
2998
3742
|
spelledKeyOf,
|
|
2999
3743
|
relativeKeyOf,
|
|
3000
3744
|
parallelKeyOf,
|
|
3745
|
+
isWrittenTonic,
|
|
3001
3746
|
dominantKeyOf,
|
|
3002
3747
|
subdominantKeyOf,
|
|
3003
3748
|
enharmonicKeyOf,
|
|
3004
3749
|
relatedKeysOf,
|
|
3005
3750
|
keyRelationBetween,
|
|
3751
|
+
resolveKey,
|
|
3752
|
+
scaleOf,
|
|
3753
|
+
majorKey,
|
|
3754
|
+
minorKey,
|
|
3755
|
+
scaleByName,
|
|
3006
3756
|
SCALE_SYSTEMS,
|
|
3007
3757
|
scaleSystemOf,
|
|
3008
3758
|
supportsFunctionalHarmony,
|
|
3009
3759
|
chordSpecIntervals,
|
|
3010
3760
|
chordSpecQuality,
|
|
3011
3761
|
chordQualities,
|
|
3762
|
+
assertChordQuality,
|
|
3763
|
+
assertChord,
|
|
3012
3764
|
chordFromDegree,
|
|
3013
3765
|
makeChord,
|
|
3014
3766
|
chordFromSpec,
|
|
@@ -3021,6 +3773,9 @@ export {
|
|
|
3021
3773
|
intervalAboveRoot,
|
|
3022
3774
|
isChordMember,
|
|
3023
3775
|
chordToneRole,
|
|
3776
|
+
degreeOfInterval,
|
|
3777
|
+
thirdSlotOf,
|
|
3778
|
+
displacedThirds,
|
|
3024
3779
|
diatonicTriad,
|
|
3025
3780
|
diatonicSeventh,
|
|
3026
3781
|
simpleIntervalNumber,
|
|
@@ -3044,10 +3799,28 @@ export {
|
|
|
3044
3799
|
ReasonFlag,
|
|
3045
3800
|
evaluateSafety,
|
|
3046
3801
|
enumerateSafePitches,
|
|
3802
|
+
toChordData,
|
|
3047
3803
|
parseChordSymbol,
|
|
3048
3804
|
tryParseChordSymbol,
|
|
3049
|
-
toChordData,
|
|
3050
3805
|
formatChordSymbol,
|
|
3051
|
-
|
|
3806
|
+
transposeChordSpellings,
|
|
3807
|
+
transposeChordSymbol,
|
|
3808
|
+
isDiatonicChord,
|
|
3809
|
+
hasMajorThird,
|
|
3810
|
+
soundsDominantSeventh,
|
|
3811
|
+
hasDominantSonority,
|
|
3812
|
+
isDominantChordOf,
|
|
3813
|
+
isNeapolitanChordOf,
|
|
3814
|
+
heptatonicFrameOf,
|
|
3815
|
+
tonicizableDegrees,
|
|
3816
|
+
appliedDominantTarget,
|
|
3817
|
+
leadingTonePcOf,
|
|
3818
|
+
LEADING_TONE_QUALITIES,
|
|
3819
|
+
isLeadingToneChordOf,
|
|
3820
|
+
isFunctioningLeadingTone,
|
|
3821
|
+
seventhPcOf,
|
|
3822
|
+
fifthPcOf,
|
|
3823
|
+
isDescendingStep,
|
|
3824
|
+
isFrustratedLeadingTone
|
|
3052
3825
|
};
|
|
3053
|
-
//# sourceMappingURL=chunk-
|
|
3826
|
+
//# sourceMappingURL=chunk-RFIBF2RR.js.map
|