@libraz/libcantus 0.9.4 → 1.0.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 +61 -261
- package/README_ja.md +115 -0
- package/dist/analyze/index.cjs +54 -7
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +213 -456
- package/dist/analyze/index.d.ts +213 -456
- package/dist/analyze/index.js +56 -9
- package/dist/chunk-3JXCW6DX.cjs +3220 -0
- package/dist/chunk-3JXCW6DX.cjs.map +1 -0
- package/dist/chunk-4A2RMUCC.js +7647 -0
- package/dist/chunk-4A2RMUCC.js.map +1 -0
- package/dist/chunk-6XPTBE7Y.cjs +49 -0
- package/dist/chunk-6XPTBE7Y.cjs.map +1 -0
- package/dist/chunk-AFM2ULJH.cjs +5121 -0
- package/dist/chunk-AFM2ULJH.cjs.map +1 -0
- package/dist/chunk-E3F4AHAH.cjs +690 -0
- package/dist/chunk-E3F4AHAH.cjs.map +1 -0
- package/dist/chunk-ELCTZUJJ.js +379 -0
- package/dist/chunk-ELCTZUJJ.js.map +1 -0
- package/dist/chunk-FVSI36UV.js +690 -0
- package/dist/chunk-FVSI36UV.js.map +1 -0
- package/dist/chunk-GEDTJHAC.cjs +1 -0
- package/dist/chunk-GEDTJHAC.cjs.map +1 -0
- package/dist/chunk-GOZG57ER.js +3220 -0
- package/dist/chunk-GOZG57ER.js.map +1 -0
- package/dist/chunk-H3VORETI.js +2243 -0
- package/dist/chunk-H3VORETI.js.map +1 -0
- package/dist/chunk-H42UHPYW.js +1577 -0
- package/dist/chunk-H42UHPYW.js.map +1 -0
- package/dist/chunk-HFPS7JFD.js +1 -0
- package/dist/chunk-INHVXP6Z.js +3053 -0
- package/dist/chunk-INHVXP6Z.js.map +1 -0
- package/dist/chunk-K4ZJUGF4.cjs +379 -0
- package/dist/chunk-K4ZJUGF4.cjs.map +1 -0
- package/dist/chunk-KNJEKLKV.js +49 -0
- package/dist/chunk-KNJEKLKV.js.map +1 -0
- package/dist/chunk-M4CWTJWJ.cjs +7647 -0
- package/dist/chunk-M4CWTJWJ.cjs.map +1 -0
- package/dist/chunk-MXO72PLY.cjs +211 -0
- package/dist/chunk-MXO72PLY.cjs.map +1 -0
- package/dist/chunk-NLEU4QSE.cjs +360 -0
- package/dist/chunk-NLEU4QSE.cjs.map +1 -0
- package/dist/chunk-OE6WK2Y2.cjs +1577 -0
- package/dist/chunk-OE6WK2Y2.cjs.map +1 -0
- package/dist/chunk-Q2WGKJNN.cjs +1064 -0
- package/dist/chunk-Q2WGKJNN.cjs.map +1 -0
- package/dist/chunk-QYQQ5J6E.js +1 -0
- package/dist/chunk-QYQQ5J6E.js.map +1 -0
- package/dist/chunk-RHIJ3S4I.cjs +1 -0
- package/dist/chunk-RHIJ3S4I.cjs.map +1 -0
- package/dist/chunk-TTCLJX6D.js +211 -0
- package/dist/chunk-TTCLJX6D.js.map +1 -0
- package/dist/chunk-UADYAYAT.cjs +3053 -0
- package/dist/chunk-UADYAYAT.cjs.map +1 -0
- package/dist/chunk-VOHZB45S.js +5121 -0
- package/dist/chunk-VOHZB45S.js.map +1 -0
- package/dist/chunk-VTI6FOLB.js +1064 -0
- package/dist/chunk-VTI6FOLB.js.map +1 -0
- package/dist/chunk-WWURE4XT.cjs +2243 -0
- package/dist/chunk-WWURE4XT.cjs.map +1 -0
- package/dist/chunk-YH3BIKHH.js +360 -0
- package/dist/chunk-YH3BIKHH.js.map +1 -0
- package/dist/core/index.cjs +103 -6
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +96 -345
- package/dist/core/index.d.ts +96 -345
- package/dist/core/index.js +104 -7
- package/dist/generate/index.cjs +89 -10
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +437 -1095
- package/dist/generate/index.d.ts +437 -1095
- package/dist/generate/index.js +94 -15
- package/dist/index-9wcMhG73.d.ts +1095 -0
- package/dist/{index-D6b6na69.d.cts → index-B4o55NDr.d.cts} +13 -9
- package/dist/index-B9MNZboQ.d.cts +770 -0
- package/dist/index-B9MNZboQ.d.ts +770 -0
- package/dist/index-BGFW-8P2.d.cts +2307 -0
- package/dist/index-BO6xE9YU.d.cts +1804 -0
- package/dist/index-BkGmlyI3.d.cts +470 -0
- package/dist/index-CCNJRG-P.d.cts +1095 -0
- package/dist/index-CGpRv5sl.d.cts +643 -0
- package/dist/index-C_8o--VE.d.cts +1242 -0
- package/dist/index-CgHqxdps.d.ts +2307 -0
- package/dist/index-Chbst3Jx.d.ts +1343 -0
- package/dist/index-Cj49F892.d.ts +470 -0
- package/dist/{index-tSQh-xwY.d.ts → index-CunYKLiu.d.ts} +13 -9
- package/dist/index-D57QRbVn.d.ts +1804 -0
- package/dist/index-DInQqT8Q.d.cts +66 -0
- package/dist/index-DInQqT8Q.d.ts +66 -0
- package/dist/index-Dzr13q3d.d.cts +1343 -0
- package/dist/index-G1uwGGWQ.d.ts +643 -0
- package/dist/index-szBv3496.d.ts +1242 -0
- package/dist/index.cjs +358 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -15
- package/dist/index.d.ts +17 -15
- package/dist/index.js +407 -64
- package/dist/masks-DYbE485J.d.cts +445 -0
- package/dist/masks-DYbE485J.d.ts +445 -0
- package/dist/model/index.cjs +39 -7
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +4685 -243
- package/dist/model/index.d.ts +4685 -243
- package/dist/model/index.js +40 -8
- package/dist/positional-Cptlj-xs.d.ts +287 -0
- package/dist/positional-DvH_sd8u.d.cts +287 -0
- package/dist/theory/index.cjs +101 -7
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +513 -173
- package/dist/theory/index.d.ts +513 -173
- package/dist/theory/index.js +118 -24
- package/package.json +16 -11
- package/dist/chunk-36N73KZ3.cjs +0 -246
- package/dist/chunk-36N73KZ3.cjs.map +0 -1
- package/dist/chunk-4HLLZPXT.cjs +0 -3077
- package/dist/chunk-4HLLZPXT.cjs.map +0 -1
- package/dist/chunk-7MGZ7D62.cjs +0 -498
- package/dist/chunk-7MGZ7D62.cjs.map +0 -1
- package/dist/chunk-7NKDPF74.js +0 -3077
- package/dist/chunk-7NKDPF74.js.map +0 -1
- package/dist/chunk-CAN76NCU.cjs +0 -679
- package/dist/chunk-CAN76NCU.cjs.map +0 -1
- package/dist/chunk-D2FAJBYL.js +0 -479
- package/dist/chunk-D2FAJBYL.js.map +0 -1
- package/dist/chunk-EHVEBWM5.js +0 -1449
- package/dist/chunk-EHVEBWM5.js.map +0 -1
- package/dist/chunk-FCANXBAT.cjs +0 -30
- package/dist/chunk-FCANXBAT.cjs.map +0 -1
- package/dist/chunk-FNDWXRRI.cjs +0 -479
- package/dist/chunk-FNDWXRRI.cjs.map +0 -1
- package/dist/chunk-GQSGS2N4.js +0 -173
- package/dist/chunk-GQSGS2N4.js.map +0 -1
- package/dist/chunk-HKGYCQ34.js +0 -53
- package/dist/chunk-HKGYCQ34.js.map +0 -1
- package/dist/chunk-I24MV65U.js +0 -106
- package/dist/chunk-I24MV65U.js.map +0 -1
- package/dist/chunk-KMHOICIY.js +0 -679
- package/dist/chunk-KMHOICIY.js.map +0 -1
- package/dist/chunk-LW53AHQU.cjs +0 -1117
- package/dist/chunk-LW53AHQU.cjs.map +0 -1
- package/dist/chunk-N6IHQUOX.js +0 -1
- package/dist/chunk-NKIBQ6PT.js +0 -30
- package/dist/chunk-NKIBQ6PT.js.map +0 -1
- package/dist/chunk-NP4NA2FB.cjs +0 -106
- package/dist/chunk-NP4NA2FB.cjs.map +0 -1
- package/dist/chunk-NVE34YGZ.cjs +0 -1449
- package/dist/chunk-NVE34YGZ.cjs.map +0 -1
- package/dist/chunk-NYYWMV2C.js +0 -327
- package/dist/chunk-NYYWMV2C.js.map +0 -1
- package/dist/chunk-Q2XUP227.js +0 -845
- package/dist/chunk-Q2XUP227.js.map +0 -1
- package/dist/chunk-Q6OP4U43.cjs +0 -327
- package/dist/chunk-Q6OP4U43.cjs.map +0 -1
- package/dist/chunk-RC6GBMCD.cjs +0 -845
- package/dist/chunk-RC6GBMCD.cjs.map +0 -1
- package/dist/chunk-RK35RIN7.js +0 -1117
- package/dist/chunk-RK35RIN7.js.map +0 -1
- package/dist/chunk-RUMU3UA6.cjs +0 -53
- package/dist/chunk-RUMU3UA6.cjs.map +0 -1
- package/dist/chunk-T64C7ZD7.cjs +0 -1
- package/dist/chunk-T64C7ZD7.cjs.map +0 -1
- package/dist/chunk-XEBG3G53.js +0 -498
- package/dist/chunk-XEBG3G53.js.map +0 -1
- package/dist/chunk-YLEMGEFH.js +0 -246
- package/dist/chunk-YLEMGEFH.js.map +0 -1
- package/dist/chunk-ZEJR4TKL.cjs +0 -173
- package/dist/chunk-ZEJR4TKL.cjs.map +0 -1
- package/dist/function-BtiZb9i9.d.ts +0 -208
- package/dist/function-DNugAOrt.d.cts +0 -208
- package/dist/index-6VR8-kit.d.ts +0 -143
- package/dist/index-70p168VM.d.ts +0 -192
- package/dist/index-Be74mcon.d.cts +0 -152
- package/dist/index-BtXT6UQ0.d.cts +0 -192
- package/dist/index-C5UBbQ4G.d.ts +0 -152
- package/dist/index-C83mZI1P.d.ts +0 -242
- package/dist/index-DXd9JikN.d.cts +0 -246
- package/dist/index-DXd9JikN.d.ts +0 -246
- package/dist/index-OG65ocOr.d.cts +0 -272
- package/dist/index-OG65ocOr.d.ts +0 -272
- package/dist/index-tJhkmQgE.d.cts +0 -143
- package/dist/index-y_ak9_Hi.d.cts +0 -242
- package/dist/styled-DIRkk9Uz.d.ts +0 -506
- package/dist/styled-DJbkJQfj.d.cts +0 -506
- package/dist/types-BN6_Q9Kl.d.cts +0 -39
- package/dist/types-BN6_Q9Kl.d.ts +0 -39
- /package/dist/{chunk-N6IHQUOX.js.map → chunk-HFPS7JFD.js.map} +0 -0
|
@@ -0,0 +1,3053 @@
|
|
|
1
|
+
import {
|
|
2
|
+
InvalidInputError,
|
|
3
|
+
assertDegree,
|
|
4
|
+
assertFiniteNumber,
|
|
5
|
+
assertFiniteSemitones,
|
|
6
|
+
assertGenerationBudget,
|
|
7
|
+
assertInteger,
|
|
8
|
+
assertMidiPitch,
|
|
9
|
+
assertOneOf,
|
|
10
|
+
clampToMidi,
|
|
11
|
+
diatonicLetterOf,
|
|
12
|
+
formatNote,
|
|
13
|
+
isConsonantInterval,
|
|
14
|
+
midiToNote,
|
|
15
|
+
naturalPitchClassOf,
|
|
16
|
+
noteToMidi,
|
|
17
|
+
noteToPitchClass,
|
|
18
|
+
parseFailure,
|
|
19
|
+
parseKeyName,
|
|
20
|
+
parseNote,
|
|
21
|
+
pitchClassOf,
|
|
22
|
+
spelledInterval,
|
|
23
|
+
toNoteData,
|
|
24
|
+
transposeNote,
|
|
25
|
+
tryParseNote,
|
|
26
|
+
unwrapParse
|
|
27
|
+
} from "./chunk-VTI6FOLB.js";
|
|
28
|
+
|
|
29
|
+
// src/theory/scale/masks.ts
|
|
30
|
+
function maskFromOffsets(offsets) {
|
|
31
|
+
let mask = 1;
|
|
32
|
+
for (const [index, offset] of offsets.entries()) {
|
|
33
|
+
assertInteger(offset, `offsets[${index}]`);
|
|
34
|
+
mask |= 1 << (offset % 12 + 12) % 12;
|
|
35
|
+
}
|
|
36
|
+
return mask;
|
|
37
|
+
}
|
|
38
|
+
var MAJOR_MASK = 2741;
|
|
39
|
+
var NATURAL_MINOR_MASK = 1453;
|
|
40
|
+
var HARMONIC_MINOR_MASK = maskFromOffsets([0, 2, 3, 5, 7, 8, 11]);
|
|
41
|
+
var MELODIC_MINOR_MASK = maskFromOffsets([0, 2, 3, 5, 7, 9, 11]);
|
|
42
|
+
var DORIAN_MASK = maskFromOffsets([0, 2, 3, 5, 7, 9, 10]);
|
|
43
|
+
var PHRYGIAN_MASK = maskFromOffsets([0, 1, 3, 5, 7, 8, 10]);
|
|
44
|
+
var LYDIAN_MASK = maskFromOffsets([0, 2, 4, 6, 7, 9, 11]);
|
|
45
|
+
var MIXOLYDIAN_MASK = maskFromOffsets([0, 2, 4, 5, 7, 9, 10]);
|
|
46
|
+
var LOCRIAN_MASK = maskFromOffsets([0, 1, 3, 5, 6, 8, 10]);
|
|
47
|
+
var MAJOR_PENTATONIC_MASK = maskFromOffsets([0, 2, 4, 7, 9]);
|
|
48
|
+
var MINOR_PENTATONIC_MASK = maskFromOffsets([0, 3, 5, 7, 10]);
|
|
49
|
+
var BLUES_MASK = maskFromOffsets([0, 3, 5, 6, 7, 10]);
|
|
50
|
+
var WHOLE_TONE_MASK = maskFromOffsets([0, 2, 4, 6, 8, 10]);
|
|
51
|
+
var OCTATONIC_HALF_WHOLE_MASK = maskFromOffsets([0, 1, 3, 4, 6, 7, 9, 10]);
|
|
52
|
+
var OCTATONIC_WHOLE_HALF_MASK = maskFromOffsets([0, 2, 3, 5, 6, 8, 9, 11]);
|
|
53
|
+
var CHROMATIC_MASK = 4095;
|
|
54
|
+
var LYDIAN_DOMINANT_MASK = maskFromOffsets([0, 2, 4, 6, 7, 9, 10]);
|
|
55
|
+
var MIXOLYDIAN_B13_MASK = maskFromOffsets([0, 2, 4, 5, 7, 8, 10]);
|
|
56
|
+
var LOCRIAN_NATURAL2_MASK = maskFromOffsets([0, 2, 3, 5, 6, 8, 10]);
|
|
57
|
+
var ALTERED_MASK = maskFromOffsets([0, 1, 3, 4, 6, 8, 10]);
|
|
58
|
+
var PHRYGIAN_DOMINANT_MASK = maskFromOffsets([0, 1, 4, 5, 7, 8, 10]);
|
|
59
|
+
var MIYAKO_BUSHI_MASK = maskFromOffsets([0, 1, 5, 7, 8]);
|
|
60
|
+
var RITSU_MASK = maskFromOffsets([0, 2, 5, 7, 9]);
|
|
61
|
+
var RYUKYU_MASK = maskFromOffsets([0, 4, 5, 7, 11]);
|
|
62
|
+
var DOUBLE_HARMONIC_MASK = maskFromOffsets([0, 1, 4, 5, 7, 8, 11]);
|
|
63
|
+
var TODI_MASK = maskFromOffsets([0, 1, 3, 6, 7, 8, 11]);
|
|
64
|
+
var MARWA_MASK = maskFromOffsets([0, 1, 4, 6, 7, 9, 11]);
|
|
65
|
+
var PURVI_MASK = maskFromOffsets([0, 1, 4, 6, 7, 8, 11]);
|
|
66
|
+
var NAMED_SCALES = Object.freeze({
|
|
67
|
+
major: MAJOR_MASK,
|
|
68
|
+
ionian: MAJOR_MASK,
|
|
69
|
+
naturalMinor: NATURAL_MINOR_MASK,
|
|
70
|
+
aeolian: NATURAL_MINOR_MASK,
|
|
71
|
+
harmonicMinor: HARMONIC_MINOR_MASK,
|
|
72
|
+
melodicMinor: MELODIC_MINOR_MASK,
|
|
73
|
+
dorian: DORIAN_MASK,
|
|
74
|
+
phrygian: PHRYGIAN_MASK,
|
|
75
|
+
lydian: LYDIAN_MASK,
|
|
76
|
+
mixolydian: MIXOLYDIAN_MASK,
|
|
77
|
+
locrian: LOCRIAN_MASK,
|
|
78
|
+
lydianDominant: LYDIAN_DOMINANT_MASK,
|
|
79
|
+
mixolydianB13: MIXOLYDIAN_B13_MASK,
|
|
80
|
+
locrianNatural2: LOCRIAN_NATURAL2_MASK,
|
|
81
|
+
altered: ALTERED_MASK,
|
|
82
|
+
phrygianDominant: PHRYGIAN_DOMINANT_MASK,
|
|
83
|
+
majorPentatonic: MAJOR_PENTATONIC_MASK,
|
|
84
|
+
minorPentatonic: MINOR_PENTATONIC_MASK,
|
|
85
|
+
blues: BLUES_MASK,
|
|
86
|
+
wholeTone: WHOLE_TONE_MASK,
|
|
87
|
+
octatonicHalfWhole: OCTATONIC_HALF_WHOLE_MASK,
|
|
88
|
+
octatonicWholeHalf: OCTATONIC_WHOLE_HALF_MASK,
|
|
89
|
+
chromatic: CHROMATIC_MASK
|
|
90
|
+
});
|
|
91
|
+
var WORLD_SCALES = Object.freeze({
|
|
92
|
+
/** Miyako-bushi (都節), the hemitonic pentatonic of Japanese art music. */
|
|
93
|
+
miyakoBushi: MIYAKO_BUSHI_MASK,
|
|
94
|
+
/** Ritsu (律), the anhemitonic pentatonic of Japanese court music and chant. */
|
|
95
|
+
ritsu: RITSU_MASK,
|
|
96
|
+
/**
|
|
97
|
+
* Min'yō (民謡), the pentatonic of Japanese folk song, called inaka-bushi
|
|
98
|
+
* (田舎節) opposite miyako-bushi. The same five pitch classes as the minor
|
|
99
|
+
* pentatonic.
|
|
100
|
+
*/
|
|
101
|
+
minyo: MINOR_PENTATONIC_MASK,
|
|
102
|
+
/** Ryūkyū (琉球), the pentatonic of Okinawan music. */
|
|
103
|
+
ryukyu: RYUKYU_MASK,
|
|
104
|
+
/** Maqam Ajam of Arabic practice; the same pitch classes as the major scale. */
|
|
105
|
+
ajam: MAJOR_MASK,
|
|
106
|
+
/**
|
|
107
|
+
* Maqam Nahawand of Arabic practice; the same pitch classes as the natural
|
|
108
|
+
* minor, though practice raises the seventh degree at a cadence.
|
|
109
|
+
*/
|
|
110
|
+
nahawand: NATURAL_MINOR_MASK,
|
|
111
|
+
/** Maqam Kurd of Arabic practice; the same pitch classes as the phrygian mode. */
|
|
112
|
+
kurd: PHRYGIAN_MASK,
|
|
113
|
+
/**
|
|
114
|
+
* Maqam Hijaz of Arabic practice; the same pitch classes as the phrygian
|
|
115
|
+
* dominant. Played intonation stretches the augmented second beyond its
|
|
116
|
+
* tempered size, which is the sound the mask cannot carry.
|
|
117
|
+
*/
|
|
118
|
+
hijaz: PHRYGIAN_DOMINANT_MASK,
|
|
119
|
+
/** Maqam Hijazkar of Arabic practice: Hijaz over Hijaz, with two augmented seconds. */
|
|
120
|
+
hijazkar: DOUBLE_HARMONIC_MASK,
|
|
121
|
+
/** Thaat Bilaval of Hindustani classical music; the same pitch classes as the major scale. */
|
|
122
|
+
bilaval: MAJOR_MASK,
|
|
123
|
+
/** Thaat Khamaj; the same pitch classes as the mixolydian mode. */
|
|
124
|
+
khamaj: MIXOLYDIAN_MASK,
|
|
125
|
+
/** Thaat Kafi; the same pitch classes as the dorian mode. */
|
|
126
|
+
kafi: DORIAN_MASK,
|
|
127
|
+
/** Thaat Asavari; the same pitch classes as the natural minor. */
|
|
128
|
+
asavari: NATURAL_MINOR_MASK,
|
|
129
|
+
/** Thaat Bhairavi; the same pitch classes as the phrygian mode. */
|
|
130
|
+
bhairavi: PHRYGIAN_MASK,
|
|
131
|
+
/** Thaat Bhairav, with komal Re and komal Dha against a natural third and seventh. */
|
|
132
|
+
bhairav: DOUBLE_HARMONIC_MASK,
|
|
133
|
+
/** Thaat Kalyan; the same pitch classes as the lydian mode. */
|
|
134
|
+
kalyan: LYDIAN_MASK,
|
|
135
|
+
/** Thaat Marwa, with komal Re and tivra Ma. */
|
|
136
|
+
marwa: MARWA_MASK,
|
|
137
|
+
/** Thaat Purvi, Marwa's komal Dha counterpart. */
|
|
138
|
+
purvi: PURVI_MASK,
|
|
139
|
+
/** Thaat Todi, with komal Re, komal Ga, komal Dha and tivra Ma. */
|
|
140
|
+
todi: TODI_MASK
|
|
141
|
+
});
|
|
142
|
+
var SCALE_ALIASES = Object.freeze({
|
|
143
|
+
/** Inaka-bushi (田舎節), the name the in/yō pairing gives the min'yō scale. */
|
|
144
|
+
inakaBushi: "minyo",
|
|
145
|
+
/** The Okinawan scale, the usual English name for the ryūkyū scale. */
|
|
146
|
+
okinawan: "ryukyu",
|
|
147
|
+
/** Turkish spelling of Hijaz. */
|
|
148
|
+
hicaz: "hijaz",
|
|
149
|
+
/** The English-language scale-list name for the Hijazkar/Bhairav set. */
|
|
150
|
+
doubleHarmonic: "hijazkar",
|
|
151
|
+
/** Transliteration variant of Bilaval. */
|
|
152
|
+
bilawal: "bilaval",
|
|
153
|
+
/** Transliteration variant of Marwa. */
|
|
154
|
+
marva: "marwa",
|
|
155
|
+
/** Transliteration variant of Purvi. */
|
|
156
|
+
poorvi: "purvi"
|
|
157
|
+
});
|
|
158
|
+
function resolveScaleName(name) {
|
|
159
|
+
if (Object.hasOwn(NAMED_SCALES, name)) {
|
|
160
|
+
return name;
|
|
161
|
+
}
|
|
162
|
+
if (Object.hasOwn(WORLD_SCALES, name)) {
|
|
163
|
+
return name;
|
|
164
|
+
}
|
|
165
|
+
return Object.hasOwn(SCALE_ALIASES, name) ? SCALE_ALIASES[name] : void 0;
|
|
166
|
+
}
|
|
167
|
+
function namedScaleMask(name) {
|
|
168
|
+
const canonical = resolveScaleName(name);
|
|
169
|
+
if (canonical === void 0) {
|
|
170
|
+
return void 0;
|
|
171
|
+
}
|
|
172
|
+
return Object.hasOwn(NAMED_SCALES, canonical) ? NAMED_SCALES[canonical] : WORLD_SCALES[canonical];
|
|
173
|
+
}
|
|
174
|
+
function requireScaleMask(name, label = "scale") {
|
|
175
|
+
const mask = namedScaleMask(name);
|
|
176
|
+
if (mask === void 0) {
|
|
177
|
+
throw new InvalidInputError(`Unknown ${label}: ${String(name)}`);
|
|
178
|
+
}
|
|
179
|
+
return mask;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/theory/scale/key.ts
|
|
183
|
+
function majorKey(rootPc) {
|
|
184
|
+
return { rootPc: pitchClassOf(rootPc), modeMask12: MAJOR_MASK };
|
|
185
|
+
}
|
|
186
|
+
function minorKey(rootPc) {
|
|
187
|
+
return { rootPc: pitchClassOf(rootPc), modeMask12: NATURAL_MINOR_MASK };
|
|
188
|
+
}
|
|
189
|
+
function scaleByName(name, rootPc) {
|
|
190
|
+
return { rootPc: pitchClassOf(rootPc), modeMask12: requireScaleMask(name) };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/theory/scale/coerce.ts
|
|
194
|
+
function normalizedKeyScale(scale) {
|
|
195
|
+
assertInteger(scale.modeMask12, "key.modeMask12", 1, CHROMATIC_MASK);
|
|
196
|
+
if ((scale.modeMask12 & 1) === 0) {
|
|
197
|
+
throw new InvalidInputError(
|
|
198
|
+
`key.modeMask12 must include its root; received ${scale.modeMask12}`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
assertInteger(scale.rootPc, "key.rootPc");
|
|
202
|
+
return { rootPc: pitchClassOf(scale.rootPc), modeMask12: scale.modeMask12 };
|
|
203
|
+
}
|
|
204
|
+
function toKeyScale(value) {
|
|
205
|
+
if (typeof value === "string") {
|
|
206
|
+
const name = parseKeyName(value);
|
|
207
|
+
const rootPc = noteToPitchClass(name.tonic);
|
|
208
|
+
return name.mode === "major" ? majorKey(rootPc) : minorKey(rootPc);
|
|
209
|
+
}
|
|
210
|
+
if (typeof value === "object" && value !== null) {
|
|
211
|
+
const data = "toJSON" in value && typeof value.toJSON === "function" ? value.toJSON().scale : value;
|
|
212
|
+
if (typeof data === "object" && data !== null) {
|
|
213
|
+
return normalizedKeyScale(data);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
throw new InvalidInputError(`key must be a key name or a key/scale; received ${typeof value}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/theory/scale/degrees.ts
|
|
220
|
+
function scaleOffset(pitch, key) {
|
|
221
|
+
return (pitchClassOf(pitch) - pitchClassOf(key.rootPc) + 12) % 12;
|
|
222
|
+
}
|
|
223
|
+
function inScale(pitch, scale) {
|
|
224
|
+
return (scale.modeMask12 >> scaleOffset(pitch, scale) & 1) === 1;
|
|
225
|
+
}
|
|
226
|
+
function isScaleTone(pitch, key) {
|
|
227
|
+
return inScale(pitch, key);
|
|
228
|
+
}
|
|
229
|
+
function nearestScaleTone(pitch, key) {
|
|
230
|
+
const scale = toKeyScale(key);
|
|
231
|
+
const base = clampToMidi(Math.round(pitch), "pitch");
|
|
232
|
+
for (let distance = 0; distance < 12; distance += 1) {
|
|
233
|
+
const lower = base - distance;
|
|
234
|
+
if (lower >= 0 && inScale(lower, scale)) {
|
|
235
|
+
return lower;
|
|
236
|
+
}
|
|
237
|
+
const higher = base + distance;
|
|
238
|
+
if (higher <= 127 && inScale(higher, scale)) {
|
|
239
|
+
return higher;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return base;
|
|
243
|
+
}
|
|
244
|
+
function scaleOffsets(key) {
|
|
245
|
+
const offsets = [];
|
|
246
|
+
for (let n = 0; n < 12; n += 1) {
|
|
247
|
+
if ((key.modeMask12 >> n & 1) === 1) {
|
|
248
|
+
offsets.push(n);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return offsets;
|
|
252
|
+
}
|
|
253
|
+
function scaleLadderPosition(pitch, key) {
|
|
254
|
+
const offsets = scaleOffsets(key);
|
|
255
|
+
const size = offsets.length;
|
|
256
|
+
if (size === 0) {
|
|
257
|
+
return { rung: 0, offset: 0 };
|
|
258
|
+
}
|
|
259
|
+
const relative = pitch - pitchClassOf(key.rootPc);
|
|
260
|
+
const octave = Math.floor(relative / 12);
|
|
261
|
+
const within = relative - octave * 12;
|
|
262
|
+
let degree = 0;
|
|
263
|
+
for (let n = 0; n < size; n += 1) {
|
|
264
|
+
if ((offsets[n] ?? 0) <= within) {
|
|
265
|
+
degree = n;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return { rung: octave * size + degree, offset: within - (offsets[degree] ?? 0) };
|
|
269
|
+
}
|
|
270
|
+
function scaleLadderPitch(rung, key) {
|
|
271
|
+
const offsets = scaleOffsets(key);
|
|
272
|
+
const size = offsets.length;
|
|
273
|
+
if (size === 0) {
|
|
274
|
+
return pitchClassOf(key.rootPc);
|
|
275
|
+
}
|
|
276
|
+
const octave = Math.floor(rung / size);
|
|
277
|
+
const degree = rung - octave * size;
|
|
278
|
+
return pitchClassOf(key.rootPc) + octave * 12 + (offsets[degree] ?? 0);
|
|
279
|
+
}
|
|
280
|
+
function shiftByScaleDegrees(pitch, degrees, key) {
|
|
281
|
+
const steps = Math.trunc(degrees);
|
|
282
|
+
if (steps === 0) {
|
|
283
|
+
return pitch;
|
|
284
|
+
}
|
|
285
|
+
const { rung, offset } = scaleLadderPosition(pitch, key);
|
|
286
|
+
return scaleLadderPitch(rung + steps, key) + offset;
|
|
287
|
+
}
|
|
288
|
+
function pitchToScaleDegree(pitch, key) {
|
|
289
|
+
const scale = toKeyScale(key);
|
|
290
|
+
const offset = scaleOffset(pitch, scale);
|
|
291
|
+
if ((scale.modeMask12 >> offset & 1) === 0) {
|
|
292
|
+
return -1;
|
|
293
|
+
}
|
|
294
|
+
let degree = 1;
|
|
295
|
+
for (let n = 0; n < offset; n += 1) {
|
|
296
|
+
if ((scale.modeMask12 >> n & 1) === 1) {
|
|
297
|
+
degree += 1;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return degree;
|
|
301
|
+
}
|
|
302
|
+
function diatonicPitchClasses(key) {
|
|
303
|
+
const scale = key;
|
|
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);
|
|
312
|
+
}
|
|
313
|
+
function scaleTonesInDegreeOrder(key) {
|
|
314
|
+
const scale = key;
|
|
315
|
+
const root = pitchClassOf(scale.rootPc);
|
|
316
|
+
const pcs = [];
|
|
317
|
+
for (let n = 0; n < 12; n += 1) {
|
|
318
|
+
if ((scale.modeMask12 >> n & 1) === 1) {
|
|
319
|
+
pcs.push((root + n) % 12);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return pcs;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/theory/scale/signature.ts
|
|
326
|
+
var LETTER_FIFTHS = [0, 2, 4, -1, 1, 3, 5];
|
|
327
|
+
var FIFTHS_LETTERS = [3, 0, 4, 1, 5, 2, 6];
|
|
328
|
+
var FIFTHS_PER_ALTERATION = 7;
|
|
329
|
+
var MINOR_MODE_OFFSET = -3;
|
|
330
|
+
var MAX_FIFTHS = 12;
|
|
331
|
+
function rotateMask(mask, semitones) {
|
|
332
|
+
const steps = (semitones % 12 + 12) % 12;
|
|
333
|
+
return (mask >> steps | mask << 12 - steps) & 4095;
|
|
334
|
+
}
|
|
335
|
+
var DIATONIC_MODE_OFFSETS = /* @__PURE__ */ new Map([
|
|
336
|
+
[rotateMask(MAJOR_MASK, 5), 1],
|
|
337
|
+
// lydian
|
|
338
|
+
[MAJOR_MASK, 0],
|
|
339
|
+
// ionian / major
|
|
340
|
+
[rotateMask(MAJOR_MASK, 7), -1],
|
|
341
|
+
// mixolydian
|
|
342
|
+
[rotateMask(MAJOR_MASK, 2), -2],
|
|
343
|
+
// dorian
|
|
344
|
+
[rotateMask(MAJOR_MASK, 9), MINOR_MODE_OFFSET],
|
|
345
|
+
// aeolian / natural minor
|
|
346
|
+
[rotateMask(MAJOR_MASK, 4), -4],
|
|
347
|
+
// phrygian
|
|
348
|
+
[rotateMask(MAJOR_MASK, 11), -5]
|
|
349
|
+
// locrian
|
|
350
|
+
]);
|
|
351
|
+
function hasMinorThird(modeMask12) {
|
|
352
|
+
return (modeMask12 >> 3 & 1) === 1 && (modeMask12 >> 4 & 1) === 0;
|
|
353
|
+
}
|
|
354
|
+
function modeOffset(key) {
|
|
355
|
+
const diatonic = DIATONIC_MODE_OFFSETS.get(key.modeMask12);
|
|
356
|
+
if (diatonic !== void 0) {
|
|
357
|
+
return diatonic;
|
|
358
|
+
}
|
|
359
|
+
return hasMinorThird(key.modeMask12) ? MINOR_MODE_OFFSET : 0;
|
|
360
|
+
}
|
|
361
|
+
function isSignatureKey(key) {
|
|
362
|
+
return DIATONIC_MODE_OFFSETS.has(key.modeMask12) || key.modeMask12 === HARMONIC_MINOR_MASK || key.modeMask12 === MELODIC_MINOR_MASK;
|
|
363
|
+
}
|
|
364
|
+
function keySignatureFifths(tonic, key) {
|
|
365
|
+
const note = toNoteData(tonic);
|
|
366
|
+
const scale = key;
|
|
367
|
+
const letter = diatonicLetterOf(note.letter);
|
|
368
|
+
assertInteger(note.alter, "tonic.alter");
|
|
369
|
+
assertInteger(scale.modeMask12, "key.modeMask12", 1, 4095);
|
|
370
|
+
return (LETTER_FIFTHS[letter] ?? 0) + FIFTHS_PER_ALTERATION * note.alter + modeOffset(scale);
|
|
371
|
+
}
|
|
372
|
+
function keyFromFifths(fifths, mode = "major") {
|
|
373
|
+
assertInteger(fifths, "fifths", -MAX_FIFTHS, MAX_FIFTHS);
|
|
374
|
+
const which = assertOneOf(mode, ["major", "minor"], "mode");
|
|
375
|
+
const index = fifths + 1 + (which === "minor" ? -MINOR_MODE_OFFSET : 0);
|
|
376
|
+
const letter = FIFTHS_LETTERS[(index % 7 + 7) % 7] ?? 0;
|
|
377
|
+
const alter = Math.floor(index / 7);
|
|
378
|
+
const rootPc = pitchClassOf(naturalPitchClassOf(letter) + alter);
|
|
379
|
+
return {
|
|
380
|
+
tonic: { letter, alter },
|
|
381
|
+
key: which === "minor" ? minorKey(rootPc) : majorKey(rootPc)
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/theory/scale/relations.ts
|
|
386
|
+
var ENHARMONIC_FIFTHS = 12;
|
|
387
|
+
var MAX_CONVENTIONAL_FIFTHS = 7;
|
|
388
|
+
var DOUBLE_ACCIDENTAL = 2;
|
|
389
|
+
var MINOR_TONIC_FIFTHS = 3;
|
|
390
|
+
var FLATTEST_TONIC_FIFTHS = -MAX_CONVENTIONAL_FIFTHS;
|
|
391
|
+
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
|
+
function modeOf(key) {
|
|
399
|
+
return isMinorMode(assertModeMask(key)) ? "minor" : "major";
|
|
400
|
+
}
|
|
401
|
+
function oppositeMode(mode) {
|
|
402
|
+
return mode === "major" ? "minor" : "major";
|
|
403
|
+
}
|
|
404
|
+
function soundsLike(a, b) {
|
|
405
|
+
return pitchClassOf(a.rootPc) === pitchClassOf(b.rootPc) && isMinorMode(a.modeMask12) === isMinorMode(b.modeMask12);
|
|
406
|
+
}
|
|
407
|
+
function spelledLike(a, b) {
|
|
408
|
+
return diatonicLetterOf(a.letter) === diatonicLetterOf(b.letter) && a.alter === b.alter;
|
|
409
|
+
}
|
|
410
|
+
function tonicCost(tonic, key) {
|
|
411
|
+
let doubles = 0;
|
|
412
|
+
let accidentals = 0;
|
|
413
|
+
for (const note of spellScale(tonic, key)) {
|
|
414
|
+
const alter = Math.abs(note.alter);
|
|
415
|
+
accidentals += alter;
|
|
416
|
+
if (alter >= DOUBLE_ACCIDENTAL) {
|
|
417
|
+
doubles += 1;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return { doubles, accidentals, fifths: Math.abs(keySignatureFifths(tonic, key)) };
|
|
421
|
+
}
|
|
422
|
+
function spellsBetter(a, b, signatureKey) {
|
|
423
|
+
const order = signatureKey ? ["fifths", "doubles", "accidentals"] : ["doubles", "accidentals", "fifths"];
|
|
424
|
+
for (const term of order) {
|
|
425
|
+
if (a[term] !== b[term]) {
|
|
426
|
+
return a[term] < b[term];
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
function spelledKeyOf(key) {
|
|
432
|
+
const scale = key;
|
|
433
|
+
const rootPc = pitchClassOf(scale.rootPc);
|
|
434
|
+
const signatureKey = isSignatureKey(scale);
|
|
435
|
+
let tonic = keyFromFifths(0).tonic;
|
|
436
|
+
let best;
|
|
437
|
+
for (let fifths = FLATTEST_TONIC_FIFTHS; fifths <= SHARPEST_TONIC_FIFTHS; fifths += 1) {
|
|
438
|
+
const candidate = keyFromFifths(fifths).tonic;
|
|
439
|
+
if (noteToPitchClass(candidate) !== rootPc) {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
const cost = tonicCost(candidate, scale);
|
|
443
|
+
if (best === void 0 || spellsBetter(cost, best, signatureKey)) {
|
|
444
|
+
tonic = candidate;
|
|
445
|
+
best = cost;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return { tonic, key };
|
|
449
|
+
}
|
|
450
|
+
function relativeKeyOf(tonic, key) {
|
|
451
|
+
const scale = toKeyScale(key);
|
|
452
|
+
return keyFromFifths(keySignatureFifths(tonic, scale), oppositeMode(modeOf(scale)));
|
|
453
|
+
}
|
|
454
|
+
function parallelKeyOf(tonic, key) {
|
|
455
|
+
const note = toNoteData(tonic);
|
|
456
|
+
const mode = oppositeMode(modeOf(toKeyScale(key)));
|
|
457
|
+
const rootPc = noteToPitchClass(note);
|
|
458
|
+
return {
|
|
459
|
+
tonic: { letter: diatonicLetterOf(note.letter), alter: note.alter },
|
|
460
|
+
key: mode === "minor" ? minorKey(rootPc) : majorKey(rootPc)
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
function tonicPosition(tonic) {
|
|
464
|
+
return keySignatureFifths(tonic, majorKey(noteToPitchClass(tonic)));
|
|
465
|
+
}
|
|
466
|
+
function tonicFifths(tonic, mode) {
|
|
467
|
+
return tonicPosition(tonic) - (mode === "minor" ? MINOR_TONIC_FIFTHS : 0);
|
|
468
|
+
}
|
|
469
|
+
function isWrittenTonic(tonic, key) {
|
|
470
|
+
const position = tonicPosition(tonic);
|
|
471
|
+
if (position < FLATTEST_TONIC_FIFTHS || position > SHARPEST_TONIC_FIFTHS) {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
return isSignatureKey(key) ? Math.abs(keySignatureFifths(tonic, key)) <= MAX_CONVENTIONAL_FIFTHS : tonicCost(tonic, key).doubles === 0;
|
|
475
|
+
}
|
|
476
|
+
function dominantKeyOf(tonic, key) {
|
|
477
|
+
const mode = modeOf(toKeyScale(key));
|
|
478
|
+
return keyFromFifths(tonicFifths(toNoteData(tonic), mode) + 1, mode);
|
|
479
|
+
}
|
|
480
|
+
function subdominantKeyOf(tonic, key) {
|
|
481
|
+
const mode = modeOf(toKeyScale(key));
|
|
482
|
+
return keyFromFifths(tonicFifths(toNoteData(tonic), mode) - 1, mode);
|
|
483
|
+
}
|
|
484
|
+
function enharmonicKeyOf(tonic, key) {
|
|
485
|
+
const note = toNoteData(tonic);
|
|
486
|
+
const scale = toKeyScale(key);
|
|
487
|
+
assertModeMask(scale);
|
|
488
|
+
for (const position of [
|
|
489
|
+
tonicPosition(note) - ENHARMONIC_FIFTHS,
|
|
490
|
+
tonicPosition(note) + ENHARMONIC_FIFTHS
|
|
491
|
+
]) {
|
|
492
|
+
if (position < FLATTEST_TONIC_FIFTHS || position > SHARPEST_TONIC_FIFTHS) {
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
const spelled = keyFromFifths(position).tonic;
|
|
496
|
+
if (!isWrittenTonic(spelled, scale)) {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
return {
|
|
500
|
+
tonic: spelled,
|
|
501
|
+
// The mask is the caller's; only the root is re-read, so a tonic that did
|
|
502
|
+
// not spell the mask's own root still comes back consistent.
|
|
503
|
+
key: { rootPc: noteToPitchClass(spelled), modeMask12: scale.modeMask12 }
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
function relativeOfDominantKeyOf(tonic, key) {
|
|
509
|
+
const dominant = dominantKeyOf(tonic, key);
|
|
510
|
+
return relativeKeyOf(dominant.tonic, dominant.key);
|
|
511
|
+
}
|
|
512
|
+
function relativeOfSubdominantKeyOf(tonic, key) {
|
|
513
|
+
const subdominant = subdominantKeyOf(tonic, key);
|
|
514
|
+
return relativeKeyOf(subdominant.tonic, subdominant.key);
|
|
515
|
+
}
|
|
516
|
+
var CLOSELY_RELATED = [
|
|
517
|
+
{ relation: "relative", of: relativeKeyOf },
|
|
518
|
+
{ relation: "parallel", of: parallelKeyOf },
|
|
519
|
+
{ relation: "dominant", of: dominantKeyOf },
|
|
520
|
+
{ relation: "subdominant", of: subdominantKeyOf },
|
|
521
|
+
{ relation: "relativeOfDominant", of: relativeOfDominantKeyOf },
|
|
522
|
+
{ relation: "relativeOfSubdominant", of: relativeOfSubdominantKeyOf }
|
|
523
|
+
];
|
|
524
|
+
function relatedKeysOf(tonic, key) {
|
|
525
|
+
const note = toNoteData(tonic);
|
|
526
|
+
const scale = toKeyScale(key);
|
|
527
|
+
return CLOSELY_RELATED.map(({ relation, of }) => ({ ...of(note, scale), relation }));
|
|
528
|
+
}
|
|
529
|
+
function keyRelationBetween(a, b) {
|
|
530
|
+
assertModeMask(a.key, "a.key");
|
|
531
|
+
assertModeMask(b.key, "b.key");
|
|
532
|
+
if (soundsLike(a.key, b.key)) {
|
|
533
|
+
return spelledLike(a.tonic, b.tonic) ? "same" : "enharmonic";
|
|
534
|
+
}
|
|
535
|
+
for (const { relation, of } of CLOSELY_RELATED) {
|
|
536
|
+
if (soundsLike(of(a.tonic, a.key).key, b.key)) {
|
|
537
|
+
return relation;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/theory/scale/system.ts
|
|
544
|
+
var SCALE_SYSTEMS = Object.freeze({
|
|
545
|
+
major: "common-practice",
|
|
546
|
+
ionian: "common-practice",
|
|
547
|
+
naturalMinor: "common-practice",
|
|
548
|
+
aeolian: "common-practice",
|
|
549
|
+
harmonicMinor: "common-practice",
|
|
550
|
+
melodicMinor: "common-practice",
|
|
551
|
+
dorian: "modal",
|
|
552
|
+
phrygian: "modal",
|
|
553
|
+
lydian: "modal",
|
|
554
|
+
mixolydian: "modal",
|
|
555
|
+
locrian: "modal",
|
|
556
|
+
lydianDominant: "modal",
|
|
557
|
+
mixolydianB13: "modal",
|
|
558
|
+
locrianNatural2: "modal",
|
|
559
|
+
altered: "modal",
|
|
560
|
+
phrygianDominant: "modal",
|
|
561
|
+
majorPentatonic: "non-functional",
|
|
562
|
+
minorPentatonic: "non-functional",
|
|
563
|
+
blues: "non-functional",
|
|
564
|
+
wholeTone: "non-functional",
|
|
565
|
+
octatonicHalfWhole: "non-functional",
|
|
566
|
+
octatonicWholeHalf: "non-functional",
|
|
567
|
+
chromatic: "non-functional",
|
|
568
|
+
miyakoBushi: "non-functional",
|
|
569
|
+
ritsu: "non-functional",
|
|
570
|
+
minyo: "non-functional",
|
|
571
|
+
ryukyu: "non-functional",
|
|
572
|
+
ajam: "non-functional",
|
|
573
|
+
nahawand: "non-functional",
|
|
574
|
+
kurd: "non-functional",
|
|
575
|
+
hijaz: "non-functional",
|
|
576
|
+
hijazkar: "non-functional",
|
|
577
|
+
bilaval: "non-functional",
|
|
578
|
+
khamaj: "non-functional",
|
|
579
|
+
kafi: "non-functional",
|
|
580
|
+
asavari: "non-functional",
|
|
581
|
+
bhairavi: "non-functional",
|
|
582
|
+
bhairav: "non-functional",
|
|
583
|
+
kalyan: "non-functional",
|
|
584
|
+
marwa: "non-functional",
|
|
585
|
+
purvi: "non-functional",
|
|
586
|
+
todi: "non-functional"
|
|
587
|
+
});
|
|
588
|
+
var SYSTEM_BY_MASK = (() => {
|
|
589
|
+
const byMask = /* @__PURE__ */ new Map();
|
|
590
|
+
const names = [
|
|
591
|
+
...Object.keys(NAMED_SCALES),
|
|
592
|
+
...Object.keys(WORLD_SCALES)
|
|
593
|
+
];
|
|
594
|
+
for (const name of names) {
|
|
595
|
+
const mask = Object.hasOwn(NAMED_SCALES, name) ? NAMED_SCALES[name] : WORLD_SCALES[name];
|
|
596
|
+
if (!byMask.has(mask)) {
|
|
597
|
+
byMask.set(mask, SCALE_SYSTEMS[name]);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return byMask;
|
|
601
|
+
})();
|
|
602
|
+
function scaleSystemOf(scale) {
|
|
603
|
+
if (typeof scale === "string") {
|
|
604
|
+
const canonical = resolveScaleName(scale);
|
|
605
|
+
if (canonical === void 0) {
|
|
606
|
+
throw new InvalidInputError(`Unknown scale: ${scale}`);
|
|
607
|
+
}
|
|
608
|
+
return SCALE_SYSTEMS[canonical];
|
|
609
|
+
}
|
|
610
|
+
return SYSTEM_BY_MASK.get(scale.modeMask12);
|
|
611
|
+
}
|
|
612
|
+
function supportsFunctionalHarmony(scale) {
|
|
613
|
+
return scaleSystemOf(scale) !== "non-functional";
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// src/theory/chord/spec.ts
|
|
617
|
+
var BASE_TONES = {
|
|
618
|
+
maj: [
|
|
619
|
+
[1, 0],
|
|
620
|
+
[3, 4],
|
|
621
|
+
[5, 7]
|
|
622
|
+
],
|
|
623
|
+
min: [
|
|
624
|
+
[1, 0],
|
|
625
|
+
[3, 3],
|
|
626
|
+
[5, 7]
|
|
627
|
+
],
|
|
628
|
+
dim: [
|
|
629
|
+
[1, 0],
|
|
630
|
+
[3, 3],
|
|
631
|
+
[5, 6]
|
|
632
|
+
],
|
|
633
|
+
aug: [
|
|
634
|
+
[1, 0],
|
|
635
|
+
[3, 4],
|
|
636
|
+
[5, 8]
|
|
637
|
+
],
|
|
638
|
+
sus2: [
|
|
639
|
+
[1, 0],
|
|
640
|
+
[3, 2],
|
|
641
|
+
[5, 7]
|
|
642
|
+
],
|
|
643
|
+
sus4: [
|
|
644
|
+
[1, 0],
|
|
645
|
+
[3, 5],
|
|
646
|
+
[5, 7]
|
|
647
|
+
],
|
|
648
|
+
power: [
|
|
649
|
+
[1, 0],
|
|
650
|
+
[5, 7]
|
|
651
|
+
]
|
|
652
|
+
};
|
|
653
|
+
var SEVENTH_SEMITONES = { maj7: 11, min7: 10, dim7: 9 };
|
|
654
|
+
var DEGREE_SEMITONES = { 6: 9, 9: 14, 11: 17, 13: 21 };
|
|
655
|
+
var PERFECT_FIFTH = 7;
|
|
656
|
+
var OMITTABLE_DEGREES = [1, 3, 5];
|
|
657
|
+
var BASE_QUALITY = {
|
|
658
|
+
maj: "maj",
|
|
659
|
+
min: "min",
|
|
660
|
+
dim: "dim",
|
|
661
|
+
aug: "aug",
|
|
662
|
+
sus2: "sus2",
|
|
663
|
+
sus4: "sus4",
|
|
664
|
+
power: "5"
|
|
665
|
+
};
|
|
666
|
+
function parts(base, spec = {}) {
|
|
667
|
+
const built = {
|
|
668
|
+
rootPc: 0,
|
|
669
|
+
base,
|
|
670
|
+
alterations: (spec.alt ?? []).map(([degree, alter]) => ({ degree, alter })),
|
|
671
|
+
additions: [...spec.add ?? []],
|
|
672
|
+
omissions: [...spec.omit ?? []]
|
|
673
|
+
};
|
|
674
|
+
if (spec.seventh !== void 0) {
|
|
675
|
+
built.seventh = spec.seventh;
|
|
676
|
+
}
|
|
677
|
+
return built;
|
|
678
|
+
}
|
|
679
|
+
var QUALITY_SPECS = {
|
|
680
|
+
maj: parts("maj"),
|
|
681
|
+
min: parts("min"),
|
|
682
|
+
dim: parts("dim"),
|
|
683
|
+
aug: parts("aug"),
|
|
684
|
+
majb5: parts("maj", { alt: [[5, -1]] }),
|
|
685
|
+
maj7: parts("maj", { seventh: "maj7" }),
|
|
686
|
+
min7: parts("min", { seventh: "min7" }),
|
|
687
|
+
dom7: parts("maj", { seventh: "min7" }),
|
|
688
|
+
dim7: parts("dim", { seventh: "dim7" }),
|
|
689
|
+
m7b5: parts("dim", { seventh: "min7" }),
|
|
690
|
+
minMaj7: parts("min", { seventh: "maj7" }),
|
|
691
|
+
minMaj9: parts("min", { seventh: "maj7", alt: [[9, 0]] }),
|
|
692
|
+
minMaj11: parts("min", {
|
|
693
|
+
seventh: "maj7",
|
|
694
|
+
alt: [
|
|
695
|
+
[9, 0],
|
|
696
|
+
[11, 0]
|
|
697
|
+
]
|
|
698
|
+
}),
|
|
699
|
+
minMaj13: parts("min", {
|
|
700
|
+
seventh: "maj7",
|
|
701
|
+
alt: [
|
|
702
|
+
[9, 0],
|
|
703
|
+
[13, 0]
|
|
704
|
+
]
|
|
705
|
+
}),
|
|
706
|
+
aug7: parts("aug", { seventh: "min7" }),
|
|
707
|
+
augMaj7: parts("aug", { seventh: "maj7" }),
|
|
708
|
+
"6": parts("maj", { add: [6] }),
|
|
709
|
+
min6: parts("min", { add: [6] }),
|
|
710
|
+
"6/9": parts("maj", { add: [6, 9] }),
|
|
711
|
+
sus2: parts("sus2"),
|
|
712
|
+
sus4: parts("sus4"),
|
|
713
|
+
add9: parts("maj", { add: [9] }),
|
|
714
|
+
add11: parts("maj", { add: [11] }),
|
|
715
|
+
maj9: parts("maj", { seventh: "maj7", alt: [[9, 0]] }),
|
|
716
|
+
min9: parts("min", { seventh: "min7", alt: [[9, 0]] }),
|
|
717
|
+
dom9: parts("maj", { seventh: "min7", alt: [[9, 0]] }),
|
|
718
|
+
"7b9": parts("maj", { seventh: "min7", alt: [[9, -1]] }),
|
|
719
|
+
"7#9": parts("maj", { seventh: "min7", alt: [[9, 1]] }),
|
|
720
|
+
"7#11": parts("maj", { seventh: "min7", alt: [[11, 1]] }),
|
|
721
|
+
"7b13": parts("maj", { seventh: "min7", alt: [[13, -1]] }),
|
|
722
|
+
// The eleventh chord as a chart voices it: the third gives way to the
|
|
723
|
+
// eleventh a semitone above it rather than sounding against it.
|
|
724
|
+
"11": parts("maj", {
|
|
725
|
+
seventh: "min7",
|
|
726
|
+
alt: [
|
|
727
|
+
[9, 0],
|
|
728
|
+
[11, 0]
|
|
729
|
+
],
|
|
730
|
+
omit: [3]
|
|
731
|
+
}),
|
|
732
|
+
"13": parts("maj", {
|
|
733
|
+
seventh: "min7",
|
|
734
|
+
alt: [
|
|
735
|
+
[9, 0],
|
|
736
|
+
[13, 0]
|
|
737
|
+
]
|
|
738
|
+
}),
|
|
739
|
+
"5": parts("power"),
|
|
740
|
+
"7sus4": parts("sus4", { seventh: "min7" }),
|
|
741
|
+
"7b5": parts("maj", { seventh: "min7", alt: [[5, -1]] }),
|
|
742
|
+
// The altered dominant as it is voiced from a lead sheet: a dominant seventh
|
|
743
|
+
// with a raised fifth and a raised ninth. The full seven-note altered stack
|
|
744
|
+
// has no seven-letter spelling, so it is not what this symbol denotes.
|
|
745
|
+
"7alt": parts("aug", { seventh: "min7", alt: [[9, 1]] }),
|
|
746
|
+
"13b9": parts("maj", {
|
|
747
|
+
seventh: "min7",
|
|
748
|
+
alt: [
|
|
749
|
+
[9, -1],
|
|
750
|
+
[13, 0]
|
|
751
|
+
]
|
|
752
|
+
}),
|
|
753
|
+
maj13: parts("maj", {
|
|
754
|
+
seventh: "maj7",
|
|
755
|
+
alt: [
|
|
756
|
+
[9, 0],
|
|
757
|
+
[13, 0]
|
|
758
|
+
]
|
|
759
|
+
}),
|
|
760
|
+
"maj7#11": parts("maj", { seventh: "maj7", alt: [[11, 1]] }),
|
|
761
|
+
min11: parts("min", {
|
|
762
|
+
seventh: "min7",
|
|
763
|
+
alt: [
|
|
764
|
+
[9, 0],
|
|
765
|
+
[11, 0]
|
|
766
|
+
]
|
|
767
|
+
}),
|
|
768
|
+
min13: parts("min", {
|
|
769
|
+
seventh: "min7",
|
|
770
|
+
alt: [
|
|
771
|
+
[9, 0],
|
|
772
|
+
[13, 0]
|
|
773
|
+
]
|
|
774
|
+
}),
|
|
775
|
+
minAdd9: parts("min", { add: [9] }),
|
|
776
|
+
"min6/9": parts("min", { add: [6, 9] })
|
|
777
|
+
};
|
|
778
|
+
function normalizeChordSpec(spec) {
|
|
779
|
+
let base = spec.base;
|
|
780
|
+
const alterations = /* @__PURE__ */ new Map();
|
|
781
|
+
for (const alteration of spec.alterations ?? []) {
|
|
782
|
+
alterations.set(alteration.degree, alteration.alter);
|
|
783
|
+
}
|
|
784
|
+
const additions = new Set(spec.additions ?? []);
|
|
785
|
+
const omissions = new Set(spec.omissions ?? []);
|
|
786
|
+
const fifth = alterations.get(5);
|
|
787
|
+
if (fifth !== void 0) {
|
|
788
|
+
if (base === "aug") {
|
|
789
|
+
base = "maj";
|
|
790
|
+
} else if (base === "dim") {
|
|
791
|
+
base = "min";
|
|
792
|
+
}
|
|
793
|
+
if (fifth === 0) {
|
|
794
|
+
alterations.delete(5);
|
|
795
|
+
} else if (base === "maj" && fifth === 1) {
|
|
796
|
+
base = "aug";
|
|
797
|
+
alterations.delete(5);
|
|
798
|
+
} else if (base === "min" && fifth === -1) {
|
|
799
|
+
base = "dim";
|
|
800
|
+
alterations.delete(5);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (spec.seventh === void 0) {
|
|
804
|
+
for (const [degree, alter] of [...alterations]) {
|
|
805
|
+
if (degree !== 5 && alter === 0) {
|
|
806
|
+
alterations.delete(degree);
|
|
807
|
+
additions.add(degree);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
const normalized = {
|
|
812
|
+
rootPc: spec.rootPc,
|
|
813
|
+
base,
|
|
814
|
+
alterations: [...alterations].sort((a, b) => a[0] - b[0]).map(([degree, alter]) => ({ degree, alter })),
|
|
815
|
+
additions: [...additions].sort((a, b) => a - b),
|
|
816
|
+
omissions: [...omissions].sort((a, b) => a - b)
|
|
817
|
+
};
|
|
818
|
+
if (spec.seventh !== void 0) {
|
|
819
|
+
normalized.seventh = spec.seventh;
|
|
820
|
+
}
|
|
821
|
+
if (spec.bassPc !== void 0) {
|
|
822
|
+
normalized.bassPc = spec.bassPc;
|
|
823
|
+
}
|
|
824
|
+
return normalized;
|
|
825
|
+
}
|
|
826
|
+
function specIntervals(normalized) {
|
|
827
|
+
const tones = new Map(BASE_TONES[normalized.base]);
|
|
828
|
+
for (const { degree, alter } of normalized.alterations) {
|
|
829
|
+
if (degree === 5 && tones.has(5)) {
|
|
830
|
+
tones.set(5, PERFECT_FIFTH + alter);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
for (const degree of normalized.omissions) {
|
|
834
|
+
tones.delete(degree);
|
|
835
|
+
}
|
|
836
|
+
const offsets = new Set(tones.values());
|
|
837
|
+
if (normalized.seventh !== void 0) {
|
|
838
|
+
offsets.add(SEVENTH_SEMITONES[normalized.seventh]);
|
|
839
|
+
}
|
|
840
|
+
for (const { degree, alter } of normalized.alterations) {
|
|
841
|
+
if (degree !== 5) {
|
|
842
|
+
offsets.add((DEGREE_SEMITONES[degree] ?? 0) + alter);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
for (const degree of normalized.additions) {
|
|
846
|
+
offsets.add(DEGREE_SEMITONES[degree] ?? 0);
|
|
847
|
+
}
|
|
848
|
+
return [...offsets].sort((a, b) => a - b);
|
|
849
|
+
}
|
|
850
|
+
function chordSpecIntervals(spec) {
|
|
851
|
+
return specIntervals(assertChordSpec(spec));
|
|
852
|
+
}
|
|
853
|
+
var QUALITY_INTERVALS = Object.fromEntries(
|
|
854
|
+
Object.keys(QUALITY_SPECS).map((quality) => [
|
|
855
|
+
quality,
|
|
856
|
+
specIntervals(normalizeChordSpec(QUALITY_SPECS[quality]))
|
|
857
|
+
])
|
|
858
|
+
);
|
|
859
|
+
function specKey(spec) {
|
|
860
|
+
const normalized = normalizeChordSpec(spec);
|
|
861
|
+
return [
|
|
862
|
+
normalized.base,
|
|
863
|
+
normalized.seventh ?? "",
|
|
864
|
+
normalized.alterations.map(({ degree, alter }) => `${alter}:${degree}`).join("."),
|
|
865
|
+
normalized.additions.join("."),
|
|
866
|
+
normalized.omissions.join(".")
|
|
867
|
+
].join("|");
|
|
868
|
+
}
|
|
869
|
+
var QUALITY_BY_KEY = new Map(
|
|
870
|
+
Object.keys(QUALITY_SPECS).map((quality) => [
|
|
871
|
+
specKey(QUALITY_SPECS[quality]),
|
|
872
|
+
quality
|
|
873
|
+
])
|
|
874
|
+
);
|
|
875
|
+
function chordSpecForQuality(quality, rootPc, bassPc) {
|
|
876
|
+
if (!Object.hasOwn(QUALITY_SPECS, quality)) {
|
|
877
|
+
throw new InvalidInputError(`Unknown chord quality: ${String(quality)}`);
|
|
878
|
+
}
|
|
879
|
+
const spec = normalizeChordSpec(QUALITY_SPECS[quality]);
|
|
880
|
+
spec.rootPc = rootPc;
|
|
881
|
+
if (bassPc !== void 0) {
|
|
882
|
+
spec.bassPc = bassPc;
|
|
883
|
+
}
|
|
884
|
+
return spec;
|
|
885
|
+
}
|
|
886
|
+
function exactChordSpecQuality(spec) {
|
|
887
|
+
return QUALITY_BY_KEY.get(specKey(spec));
|
|
888
|
+
}
|
|
889
|
+
function chordSpecQuality(spec) {
|
|
890
|
+
const checked = assertChordSpec(spec);
|
|
891
|
+
const exact = exactChordSpecQuality(checked);
|
|
892
|
+
if (exact !== void 0) {
|
|
893
|
+
return exact;
|
|
894
|
+
}
|
|
895
|
+
const offsets = new Set(specIntervals(checked));
|
|
896
|
+
let nearest;
|
|
897
|
+
let size = 0;
|
|
898
|
+
for (const quality of Object.keys(QUALITY_INTERVALS)) {
|
|
899
|
+
const template = QUALITY_INTERVALS[quality];
|
|
900
|
+
if (template.length > size && template.every((offset) => offsets.has(offset))) {
|
|
901
|
+
nearest = quality;
|
|
902
|
+
size = template.length;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return nearest ?? BASE_QUALITY[checked.base];
|
|
906
|
+
}
|
|
907
|
+
var EXTENSION_BY_SEMITONE = /* @__PURE__ */ new Map([
|
|
908
|
+
[13, { degree: 9, alter: -1 }],
|
|
909
|
+
[14, { degree: 9, alter: 0 }],
|
|
910
|
+
[15, { degree: 9, alter: 1 }],
|
|
911
|
+
[16, { degree: 11, alter: -1 }],
|
|
912
|
+
[17, { degree: 11, alter: 0 }],
|
|
913
|
+
[18, { degree: 11, alter: 1 }],
|
|
914
|
+
[20, { degree: 13, alter: -1 }],
|
|
915
|
+
[21, { degree: 13, alter: 0 }],
|
|
916
|
+
[22, { degree: 13, alter: 1 }]
|
|
917
|
+
]);
|
|
918
|
+
function sameOffsets(a, b) {
|
|
919
|
+
return a.length === b.length && a.every((offset, index) => offset === b[index]);
|
|
920
|
+
}
|
|
921
|
+
function chordSpecFromIntervals(intervals) {
|
|
922
|
+
const offsets = [...new Set(intervals)].sort((a, b) => a - b);
|
|
923
|
+
const has = (offset) => offsets.includes(offset);
|
|
924
|
+
const alterations = [];
|
|
925
|
+
const additions = [];
|
|
926
|
+
const omissions = [];
|
|
927
|
+
if (!has(0)) {
|
|
928
|
+
omissions.push(1);
|
|
929
|
+
}
|
|
930
|
+
const third = has(4) ? 4 : has(3) ? 3 : has(5) ? 5 : has(2) ? 2 : void 0;
|
|
931
|
+
const fifth = has(7) ? 7 : has(6) ? 6 : has(8) ? 8 : void 0;
|
|
932
|
+
let base = "maj";
|
|
933
|
+
if (third === 3) {
|
|
934
|
+
base = "min";
|
|
935
|
+
} else if (third === 5) {
|
|
936
|
+
base = "sus4";
|
|
937
|
+
} else if (third === 2) {
|
|
938
|
+
base = "sus2";
|
|
939
|
+
} else if (third === void 0) {
|
|
940
|
+
if (fifth === 7 && offsets.length === 2 && has(0)) {
|
|
941
|
+
base = "power";
|
|
942
|
+
} else {
|
|
943
|
+
omissions.push(3);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
if (fifth === void 0) {
|
|
947
|
+
omissions.push(5);
|
|
948
|
+
} else if (fifth !== 7) {
|
|
949
|
+
if (base === "maj" && fifth === 8) {
|
|
950
|
+
base = "aug";
|
|
951
|
+
} else if (base === "min" && fifth === 6) {
|
|
952
|
+
base = "dim";
|
|
953
|
+
} else {
|
|
954
|
+
alterations.push({ degree: 5, alter: fifth === 6 ? -1 : 1 });
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
let seventh = has(11) ? "maj7" : has(10) ? "min7" : void 0;
|
|
958
|
+
if (has(9)) {
|
|
959
|
+
if (seventh === void 0 && base === "dim") {
|
|
960
|
+
seventh = "dim7";
|
|
961
|
+
} else {
|
|
962
|
+
additions.push(6);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
for (const offset of offsets) {
|
|
966
|
+
if (offset <= 12) {
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
const extension = EXTENSION_BY_SEMITONE.get(offset);
|
|
970
|
+
if (extension === void 0) {
|
|
971
|
+
return void 0;
|
|
972
|
+
}
|
|
973
|
+
alterations.push(extension);
|
|
974
|
+
}
|
|
975
|
+
const spec = { rootPc: 0, base, alterations, additions, omissions };
|
|
976
|
+
if (seventh !== void 0) {
|
|
977
|
+
spec.seventh = seventh;
|
|
978
|
+
}
|
|
979
|
+
const normalized = normalizeChordSpec(spec);
|
|
980
|
+
return sameOffsets(specIntervals(normalized), offsets) ? normalized : void 0;
|
|
981
|
+
}
|
|
982
|
+
function assertPart(value, allowed, name) {
|
|
983
|
+
if (!allowed.includes(value)) {
|
|
984
|
+
throw new InvalidInputError(
|
|
985
|
+
`${name} must be one of ${allowed.join(", ")}; received ${JSON.stringify(value)}`
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
return value;
|
|
989
|
+
}
|
|
990
|
+
function assertDegreeIn(value, allowed, name) {
|
|
991
|
+
if (!allowed.includes(value)) {
|
|
992
|
+
throw new InvalidInputError(
|
|
993
|
+
`${name} must be one of ${allowed.join(", ")}; received ${String(value)}`
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
return value;
|
|
997
|
+
}
|
|
998
|
+
function assertDegreeList(value, allowed, name) {
|
|
999
|
+
if (!Array.isArray(value)) {
|
|
1000
|
+
throw new InvalidInputError(`${name} must be an array; received ${typeof value}`);
|
|
1001
|
+
}
|
|
1002
|
+
for (const degree of value) {
|
|
1003
|
+
assertDegreeIn(degree, allowed, name);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
function assertChordSpec(spec) {
|
|
1007
|
+
if (spec === null || typeof spec !== "object") {
|
|
1008
|
+
throw new InvalidInputError(`chord spec must be an object; received ${typeof spec}`);
|
|
1009
|
+
}
|
|
1010
|
+
assertFiniteNumber(spec.rootPc, "chord spec rootPc");
|
|
1011
|
+
if (spec.bassPc !== void 0) {
|
|
1012
|
+
assertFiniteNumber(spec.bassPc, "chord spec bassPc");
|
|
1013
|
+
}
|
|
1014
|
+
assertPart(spec.base, Object.keys(BASE_TONES), "chord spec base");
|
|
1015
|
+
if (spec.seventh !== void 0) {
|
|
1016
|
+
assertPart(
|
|
1017
|
+
spec.seventh,
|
|
1018
|
+
Object.keys(SEVENTH_SEMITONES),
|
|
1019
|
+
"chord spec seventh"
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
if (!Array.isArray(spec.alterations)) {
|
|
1023
|
+
throw new InvalidInputError("chord spec alterations must be an array");
|
|
1024
|
+
}
|
|
1025
|
+
for (const alteration of spec.alterations) {
|
|
1026
|
+
assertDegreeIn(alteration?.degree, [5, 9, 11, 13], "chord spec alteration degree");
|
|
1027
|
+
assertDegreeIn(alteration.alter, [-1, 0, 1], "chord spec alteration");
|
|
1028
|
+
}
|
|
1029
|
+
assertDegreeList(spec.additions, [6, 9, 11, 13], "chord spec additions");
|
|
1030
|
+
assertDegreeList(spec.omissions, OMITTABLE_DEGREES, "chord spec omissions");
|
|
1031
|
+
return normalizeChordSpec(spec);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// src/theory/chord/index.ts
|
|
1035
|
+
var MAX_CHORD_INTERVAL = 127;
|
|
1036
|
+
function chordQualities() {
|
|
1037
|
+
return Object.keys(QUALITY_INTERVALS);
|
|
1038
|
+
}
|
|
1039
|
+
function chordFromDegree(degree, ext, key) {
|
|
1040
|
+
assertDegree(degree, "chord degree");
|
|
1041
|
+
if (!Object.hasOwn(QUALITY_INTERVALS, ext)) {
|
|
1042
|
+
throw new InvalidInputError(`Unknown chord quality: ${String(ext)}`);
|
|
1043
|
+
}
|
|
1044
|
+
const scale = toKeyScale(key);
|
|
1045
|
+
const tones = scaleTonesInDegreeOrder(scale);
|
|
1046
|
+
const length = tones.length;
|
|
1047
|
+
const index = length > 0 ? (degree - 1) % length : 0;
|
|
1048
|
+
const rootPc = tones[index] ?? pitchClassOf(scale.rootPc);
|
|
1049
|
+
return { rootPc, quality: ext, intervals: [...QUALITY_INTERVALS[ext]] };
|
|
1050
|
+
}
|
|
1051
|
+
function makeChord(rootPc, quality, bassPc) {
|
|
1052
|
+
assertFiniteNumber(rootPc, "chord rootPc");
|
|
1053
|
+
if (bassPc !== void 0) {
|
|
1054
|
+
assertFiniteNumber(bassPc, "chord bassPc");
|
|
1055
|
+
}
|
|
1056
|
+
if (!Object.hasOwn(QUALITY_INTERVALS, quality)) {
|
|
1057
|
+
throw new InvalidInputError(`Unknown chord quality: ${String(quality)}`);
|
|
1058
|
+
}
|
|
1059
|
+
const chord = {
|
|
1060
|
+
rootPc: pitchClassOf(rootPc),
|
|
1061
|
+
quality,
|
|
1062
|
+
intervals: [...QUALITY_INTERVALS[quality] ?? []]
|
|
1063
|
+
};
|
|
1064
|
+
if (bassPc !== void 0) {
|
|
1065
|
+
chord.bassPc = pitchClassOf(bassPc);
|
|
1066
|
+
}
|
|
1067
|
+
return chord;
|
|
1068
|
+
}
|
|
1069
|
+
function chordFromSpec(spec) {
|
|
1070
|
+
const checked = assertChordSpec(spec);
|
|
1071
|
+
const chord = {
|
|
1072
|
+
rootPc: pitchClassOf(checked.rootPc),
|
|
1073
|
+
quality: chordSpecQuality(checked),
|
|
1074
|
+
intervals: chordSpecIntervals(checked)
|
|
1075
|
+
};
|
|
1076
|
+
if (checked.bassPc !== void 0) {
|
|
1077
|
+
chord.bassPc = pitchClassOf(checked.bassPc);
|
|
1078
|
+
}
|
|
1079
|
+
return chord;
|
|
1080
|
+
}
|
|
1081
|
+
function chordSpecOf(chord) {
|
|
1082
|
+
assertFiniteNumber(chord.rootPc, "chord rootPc");
|
|
1083
|
+
const rootPc = pitchClassOf(chord.rootPc);
|
|
1084
|
+
const bassPc = chord.bassPc === void 0 ? void 0 : pitchClassOf(chord.bassPc);
|
|
1085
|
+
const intervals = chord.intervals ?? [];
|
|
1086
|
+
const read = intervals.length === 0 ? void 0 : chordSpecFromIntervals(intervals);
|
|
1087
|
+
if (read === void 0) {
|
|
1088
|
+
return chordSpecForQuality(chord.quality, rootPc, bassPc);
|
|
1089
|
+
}
|
|
1090
|
+
read.rootPc = rootPc;
|
|
1091
|
+
if (bassPc !== void 0) {
|
|
1092
|
+
read.bassPc = bassPc;
|
|
1093
|
+
}
|
|
1094
|
+
return read;
|
|
1095
|
+
}
|
|
1096
|
+
function copySpanIntervals(intervals) {
|
|
1097
|
+
const name = "chord span intervals";
|
|
1098
|
+
if (!Array.isArray(intervals)) {
|
|
1099
|
+
throw new InvalidInputError(`${name} must be an array; received ${typeof intervals}`);
|
|
1100
|
+
}
|
|
1101
|
+
if (intervals.length === 0) {
|
|
1102
|
+
throw new InvalidInputError(`${name} must name at least one interval`);
|
|
1103
|
+
}
|
|
1104
|
+
const copy = [];
|
|
1105
|
+
for (let index = 0; index < intervals.length; index += 1) {
|
|
1106
|
+
const interval = intervals[index];
|
|
1107
|
+
if (interval === void 0) {
|
|
1108
|
+
throw new InvalidInputError(
|
|
1109
|
+
`${name}[${index}] must be a semitone offset; received undefined`
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
copy.push(
|
|
1113
|
+
assertInteger(interval, `${name}[${index}]`, -MAX_CHORD_INTERVAL, MAX_CHORD_INTERVAL)
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
return copy;
|
|
1117
|
+
}
|
|
1118
|
+
function hasCustomIntervals(chord) {
|
|
1119
|
+
return !matchesQualityIntervals(chord.quality, chord.intervals);
|
|
1120
|
+
}
|
|
1121
|
+
function chordFromSpan(span) {
|
|
1122
|
+
const chord = makeChord(span.rootPc, span.quality, span.bassPc);
|
|
1123
|
+
if (span.intervals !== void 0) {
|
|
1124
|
+
chord.intervals = copySpanIntervals(span.intervals);
|
|
1125
|
+
}
|
|
1126
|
+
return chord;
|
|
1127
|
+
}
|
|
1128
|
+
function spanFromChord(chord, startBeat) {
|
|
1129
|
+
assertFiniteNumber(startBeat, "chord span startBeat");
|
|
1130
|
+
if (!Object.hasOwn(QUALITY_INTERVALS, chord.quality)) {
|
|
1131
|
+
throw new InvalidInputError(`Unknown chord quality: ${String(chord.quality)}`);
|
|
1132
|
+
}
|
|
1133
|
+
const span = { rootPc: chord.rootPc, quality: chord.quality, startBeat };
|
|
1134
|
+
if (chord.bassPc !== void 0) {
|
|
1135
|
+
span.bassPc = chord.bassPc;
|
|
1136
|
+
}
|
|
1137
|
+
if (hasCustomIntervals(chord)) {
|
|
1138
|
+
span.intervals = [...chord.intervals];
|
|
1139
|
+
}
|
|
1140
|
+
return span;
|
|
1141
|
+
}
|
|
1142
|
+
function transposeChord(chord, semitones) {
|
|
1143
|
+
assertFiniteNumber(semitones, "semitones");
|
|
1144
|
+
const steps = Math.round(semitones);
|
|
1145
|
+
const moved = {
|
|
1146
|
+
rootPc: pitchClassOf(chord.rootPc + steps),
|
|
1147
|
+
quality: chord.quality,
|
|
1148
|
+
intervals: [...chord.intervals]
|
|
1149
|
+
};
|
|
1150
|
+
if (chord.bassPc !== void 0) {
|
|
1151
|
+
moved.bassPc = pitchClassOf(chord.bassPc + steps);
|
|
1152
|
+
}
|
|
1153
|
+
return moved;
|
|
1154
|
+
}
|
|
1155
|
+
function chordToneSpellings(chord) {
|
|
1156
|
+
const hints = chord.toneSpellings;
|
|
1157
|
+
if (hints === void 0 || hints.length !== chord.intervals.length) {
|
|
1158
|
+
return void 0;
|
|
1159
|
+
}
|
|
1160
|
+
const usable = [];
|
|
1161
|
+
for (let index = 0; index < hints.length; index += 1) {
|
|
1162
|
+
const hint = hints[index];
|
|
1163
|
+
const interval = chord.intervals[index];
|
|
1164
|
+
if (hint === void 0 || interval === void 0) {
|
|
1165
|
+
return void 0;
|
|
1166
|
+
}
|
|
1167
|
+
if (noteToPitchClass(hint) !== pitchClassOf(chord.rootPc + interval)) {
|
|
1168
|
+
return void 0;
|
|
1169
|
+
}
|
|
1170
|
+
usable.push({ letter: diatonicLetterOf(hint.letter), alter: hint.alter });
|
|
1171
|
+
}
|
|
1172
|
+
return usable;
|
|
1173
|
+
}
|
|
1174
|
+
function chordPitchClasses(chord, opts = {}) {
|
|
1175
|
+
const set = /* @__PURE__ */ new Set();
|
|
1176
|
+
for (const interval of chord.intervals) {
|
|
1177
|
+
set.add(pitchClassOf(chord.rootPc + interval));
|
|
1178
|
+
}
|
|
1179
|
+
if (opts.includeBass !== false && chord.bassPc !== void 0) {
|
|
1180
|
+
set.add(pitchClassOf(chord.bassPc));
|
|
1181
|
+
}
|
|
1182
|
+
return [...set].sort((a, b) => a - b);
|
|
1183
|
+
}
|
|
1184
|
+
function intervalAboveRoot(pitch, chord) {
|
|
1185
|
+
return ((pitchClassOf(pitch) - pitchClassOf(chord.rootPc)) % 12 + 12) % 12;
|
|
1186
|
+
}
|
|
1187
|
+
function isChordMember(pitch, chord) {
|
|
1188
|
+
return chord === null ? false : chordPitchClasses(chord).includes(pitchClassOf(pitch));
|
|
1189
|
+
}
|
|
1190
|
+
function chordToneOffsets(chord) {
|
|
1191
|
+
return new Set(chord.intervals.map((i) => (i % 12 + 12) % 12));
|
|
1192
|
+
}
|
|
1193
|
+
function chordToneRole(pitch, chord) {
|
|
1194
|
+
const interval = (pitchClassOf(pitch) - pitchClassOf(chord.rootPc) + 12) % 12;
|
|
1195
|
+
const tones = chordToneOffsets(chord);
|
|
1196
|
+
if (interval === 0) {
|
|
1197
|
+
return "root";
|
|
1198
|
+
}
|
|
1199
|
+
if (interval === 3 || interval === 4) {
|
|
1200
|
+
return tones.has(interval) && (interval !== 3 || !tones.has(4)) ? "third" : null;
|
|
1201
|
+
}
|
|
1202
|
+
if (interval === 7) {
|
|
1203
|
+
return tones.has(7) ? "fifth" : null;
|
|
1204
|
+
}
|
|
1205
|
+
if (interval === 6) {
|
|
1206
|
+
return tones.has(6) && !tones.has(7) ? "fifth" : null;
|
|
1207
|
+
}
|
|
1208
|
+
if (interval === 8) {
|
|
1209
|
+
return tones.has(8) && !tones.has(7) ? "fifth" : null;
|
|
1210
|
+
}
|
|
1211
|
+
if (interval === 9) {
|
|
1212
|
+
const hasHigherSeventh = tones.has(10) || tones.has(11);
|
|
1213
|
+
const isDiminishedSeventh = tones.has(3) && tones.has(6) && !hasHigherSeventh;
|
|
1214
|
+
if (tones.has(9) && isDiminishedSeventh) {
|
|
1215
|
+
return "seventh";
|
|
1216
|
+
}
|
|
1217
|
+
if (tones.has(9) && !hasHigherSeventh) {
|
|
1218
|
+
return "sixth";
|
|
1219
|
+
}
|
|
1220
|
+
return null;
|
|
1221
|
+
}
|
|
1222
|
+
if (interval === 10 || interval === 11) {
|
|
1223
|
+
return tones.has(interval) ? "seventh" : null;
|
|
1224
|
+
}
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1227
|
+
function classifyTriad(thirdIc, fifthIc) {
|
|
1228
|
+
if (thirdIc === 4 && fifthIc === 8) {
|
|
1229
|
+
return "aug";
|
|
1230
|
+
}
|
|
1231
|
+
if (thirdIc === 4 && fifthIc === 6) {
|
|
1232
|
+
return "majb5";
|
|
1233
|
+
}
|
|
1234
|
+
if (thirdIc === 3 && fifthIc === 6) {
|
|
1235
|
+
return "dim";
|
|
1236
|
+
}
|
|
1237
|
+
if (thirdIc === 3) {
|
|
1238
|
+
return "min";
|
|
1239
|
+
}
|
|
1240
|
+
return "maj";
|
|
1241
|
+
}
|
|
1242
|
+
function classifySeventh(thirdIc, fifthIc, seventhIc) {
|
|
1243
|
+
if (thirdIc === 3 && fifthIc === 6 && seventhIc === 9) {
|
|
1244
|
+
return "dim7";
|
|
1245
|
+
}
|
|
1246
|
+
if (thirdIc === 3 && fifthIc === 6 && seventhIc === 10) {
|
|
1247
|
+
return "m7b5";
|
|
1248
|
+
}
|
|
1249
|
+
if (thirdIc === 3 && seventhIc === 11) {
|
|
1250
|
+
return "minMaj7";
|
|
1251
|
+
}
|
|
1252
|
+
if (thirdIc === 3) {
|
|
1253
|
+
return "min7";
|
|
1254
|
+
}
|
|
1255
|
+
if (fifthIc === 8 && seventhIc === 10) {
|
|
1256
|
+
return "aug7";
|
|
1257
|
+
}
|
|
1258
|
+
if (fifthIc === 8 && seventhIc === 11) {
|
|
1259
|
+
return "augMaj7";
|
|
1260
|
+
}
|
|
1261
|
+
if (fifthIc === 6 && seventhIc === 10) {
|
|
1262
|
+
return "7b5";
|
|
1263
|
+
}
|
|
1264
|
+
if (seventhIc === 11) {
|
|
1265
|
+
return "maj7";
|
|
1266
|
+
}
|
|
1267
|
+
return "dom7";
|
|
1268
|
+
}
|
|
1269
|
+
function matchesQualityIntervals(quality, intervals) {
|
|
1270
|
+
const template = QUALITY_INTERVALS[quality];
|
|
1271
|
+
return template.length === intervals.length && template.every((interval, index) => interval === intervals[index]);
|
|
1272
|
+
}
|
|
1273
|
+
function stackThirds(degree, key, size) {
|
|
1274
|
+
assertDegree(degree, "chord degree");
|
|
1275
|
+
const tones = scaleTonesInDegreeOrder(key);
|
|
1276
|
+
const length = tones.length;
|
|
1277
|
+
if (length !== 7) {
|
|
1278
|
+
throw new InvalidInputError(
|
|
1279
|
+
`diatonic chord stacking requires a heptatonic scale (received ${length} tones)`
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
const idx = (step) => (degree - 1 + step) % length;
|
|
1283
|
+
const rootPc = tones[idx(0)] ?? pitchClassOf(key.rootPc);
|
|
1284
|
+
const offsets = [0, 2, 4, 6].slice(0, size).map((step) => {
|
|
1285
|
+
const pc = tones[idx(step)] ?? rootPc;
|
|
1286
|
+
return ((pc - rootPc) % 12 + 12) % 12;
|
|
1287
|
+
});
|
|
1288
|
+
const thirdIc = offsets[1] ?? 4;
|
|
1289
|
+
const fifthIc = offsets[2] ?? 7;
|
|
1290
|
+
const quality = size === 3 ? classifyTriad(thirdIc, fifthIc) : classifySeventh(thirdIc, fifthIc, offsets[3] ?? 11);
|
|
1291
|
+
if (!matchesQualityIntervals(quality, offsets)) {
|
|
1292
|
+
throw new InvalidInputError(
|
|
1293
|
+
`scale thirds at degree ${degree} produce unsupported intervals [${offsets.join(", ")}]`
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
return { rootPc, quality, intervals: offsets };
|
|
1297
|
+
}
|
|
1298
|
+
function diatonicTriad(degree, key) {
|
|
1299
|
+
return stackThirds(degree, toKeyScale(key), 3);
|
|
1300
|
+
}
|
|
1301
|
+
function diatonicSeventh(degree, key) {
|
|
1302
|
+
return stackThirds(degree, toKeyScale(key), 4);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// src/theory/spelling/index.ts
|
|
1306
|
+
var CHROMATIC_SPELLING = {
|
|
1307
|
+
1: 1,
|
|
1308
|
+
// b2
|
|
1309
|
+
2: 1,
|
|
1310
|
+
// M2
|
|
1311
|
+
3: 2,
|
|
1312
|
+
// b3
|
|
1313
|
+
4: 2,
|
|
1314
|
+
// M3
|
|
1315
|
+
5: 3,
|
|
1316
|
+
// P4
|
|
1317
|
+
6: 3,
|
|
1318
|
+
// #4
|
|
1319
|
+
7: 4,
|
|
1320
|
+
// P5
|
|
1321
|
+
8: 5,
|
|
1322
|
+
// b6
|
|
1323
|
+
9: 5,
|
|
1324
|
+
// #6 (raised sixth of a minor key)
|
|
1325
|
+
10: 6,
|
|
1326
|
+
// b7
|
|
1327
|
+
11: 6
|
|
1328
|
+
// #7 (raised leading tone of a minor key)
|
|
1329
|
+
};
|
|
1330
|
+
var CHROMATIC_SPELLING_FLAT = {
|
|
1331
|
+
...CHROMATIC_SPELLING,
|
|
1332
|
+
6: 4
|
|
1333
|
+
// b5
|
|
1334
|
+
};
|
|
1335
|
+
var CHROMATIC_SPELLING_SHARP = {
|
|
1336
|
+
1: 0,
|
|
1337
|
+
// #1
|
|
1338
|
+
2: 1,
|
|
1339
|
+
// M2
|
|
1340
|
+
3: 1,
|
|
1341
|
+
// #2
|
|
1342
|
+
4: 2,
|
|
1343
|
+
// M3
|
|
1344
|
+
5: 3,
|
|
1345
|
+
// P4
|
|
1346
|
+
6: 3,
|
|
1347
|
+
// #4
|
|
1348
|
+
7: 4,
|
|
1349
|
+
// P5
|
|
1350
|
+
8: 4,
|
|
1351
|
+
// #5
|
|
1352
|
+
9: 5,
|
|
1353
|
+
// M6
|
|
1354
|
+
10: 5,
|
|
1355
|
+
// #6
|
|
1356
|
+
11: 6
|
|
1357
|
+
// M7
|
|
1358
|
+
};
|
|
1359
|
+
function notePitchClass(note) {
|
|
1360
|
+
return pitchClassOf(naturalPitchClassOf(note.letter) + note.alter);
|
|
1361
|
+
}
|
|
1362
|
+
function assertTonicOf(tonic, key, source) {
|
|
1363
|
+
const tonicPc = notePitchClass(tonic);
|
|
1364
|
+
const rootPc = pitchClassOf(key.rootPc);
|
|
1365
|
+
if (tonicPc !== rootPc) {
|
|
1366
|
+
const name = formatNote({ letter: diatonicLetterOf(tonic.letter), alter: tonic.alter });
|
|
1367
|
+
throw new InvalidInputError(
|
|
1368
|
+
`${source}: tonic ${name} sounds pitch class ${tonicPc}, not the key root ${rootPc}; pass a tonic that spells the key root, or take one from spelledKeyOf`
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
function alterFor(letter, pc) {
|
|
1373
|
+
let d = pitchClassOf(pc - naturalPitchClassOf(letter));
|
|
1374
|
+
if (d > 6) {
|
|
1375
|
+
d -= 12;
|
|
1376
|
+
}
|
|
1377
|
+
return d;
|
|
1378
|
+
}
|
|
1379
|
+
function letterFor(tonic, degreeOffset, pc) {
|
|
1380
|
+
if (degreeOffset === void 0) {
|
|
1381
|
+
return void 0;
|
|
1382
|
+
}
|
|
1383
|
+
const letter = diatonicLetterOf(tonic.letter + degreeOffset);
|
|
1384
|
+
return { letter, alter: alterFor(letter, pc) };
|
|
1385
|
+
}
|
|
1386
|
+
function pickSpelling(flat, sharp, preferFlat) {
|
|
1387
|
+
if (flat === void 0 || sharp === void 0) {
|
|
1388
|
+
return flat ?? sharp;
|
|
1389
|
+
}
|
|
1390
|
+
if (Math.abs(flat.alter) !== Math.abs(sharp.alter)) {
|
|
1391
|
+
return Math.abs(flat.alter) < Math.abs(sharp.alter) ? flat : sharp;
|
|
1392
|
+
}
|
|
1393
|
+
return preferFlat ? flat : sharp;
|
|
1394
|
+
}
|
|
1395
|
+
function capChromaticAccidentals(preferred, alternative) {
|
|
1396
|
+
return alternative !== void 0 && Math.abs(preferred.alter) - Math.abs(alternative.alter) >= 2 ? alternative : preferred;
|
|
1397
|
+
}
|
|
1398
|
+
var DIATONIC_LETTER_COUNT = 7;
|
|
1399
|
+
function isHeptatonic(key) {
|
|
1400
|
+
return scaleTonesInDegreeOrder(key).length === DIATONIC_LETTER_COUNT;
|
|
1401
|
+
}
|
|
1402
|
+
function hasMinorThird2(key) {
|
|
1403
|
+
const tones = scaleTonesInDegreeOrder(key);
|
|
1404
|
+
const root = pitchClassOf(key.rootPc);
|
|
1405
|
+
return tones.includes(pitchClassOf(root + 3)) && !tones.includes(pitchClassOf(root + 4));
|
|
1406
|
+
}
|
|
1407
|
+
function leansFlat(tonic, key) {
|
|
1408
|
+
return tonic.alter < 0 || hasMinorThird2(key);
|
|
1409
|
+
}
|
|
1410
|
+
function gappedCandidates(tonic, offset, pc, preferFlat) {
|
|
1411
|
+
const flat = letterFor(tonic, CHROMATIC_SPELLING_FLAT[offset], pc);
|
|
1412
|
+
const sharp = letterFor(tonic, CHROMATIC_SPELLING_SHARP[offset], pc);
|
|
1413
|
+
const preferred = pickSpelling(flat, sharp, preferFlat);
|
|
1414
|
+
if (preferred === void 0) {
|
|
1415
|
+
return [];
|
|
1416
|
+
}
|
|
1417
|
+
const alternative = preferred === flat ? sharp : flat;
|
|
1418
|
+
return alternative === void 0 || alternative.letter === preferred.letter ? [preferred] : [preferred, alternative];
|
|
1419
|
+
}
|
|
1420
|
+
function assignDistinctLetters(candidates, reserved) {
|
|
1421
|
+
let best;
|
|
1422
|
+
let bestAlter = 0;
|
|
1423
|
+
let bestDeviations = 0;
|
|
1424
|
+
for (let choice = 0; choice < 1 << candidates.length; choice += 1) {
|
|
1425
|
+
const picked = [];
|
|
1426
|
+
let letters = reserved;
|
|
1427
|
+
let alter = 0;
|
|
1428
|
+
let deviations = 0;
|
|
1429
|
+
for (const [index, options] of candidates.entries()) {
|
|
1430
|
+
const preferred = options[0];
|
|
1431
|
+
const note = options[choice >> index & 1] ?? preferred;
|
|
1432
|
+
if (note === void 0 || (letters & 1 << note.letter) !== 0) {
|
|
1433
|
+
break;
|
|
1434
|
+
}
|
|
1435
|
+
letters |= 1 << note.letter;
|
|
1436
|
+
alter += Math.abs(note.alter);
|
|
1437
|
+
deviations += note === preferred ? 0 : 1;
|
|
1438
|
+
picked.push(note);
|
|
1439
|
+
}
|
|
1440
|
+
if (picked.length !== candidates.length) {
|
|
1441
|
+
continue;
|
|
1442
|
+
}
|
|
1443
|
+
if (best === void 0 || alter < bestAlter || alter === bestAlter && deviations < bestDeviations) {
|
|
1444
|
+
best = picked;
|
|
1445
|
+
bestAlter = alter;
|
|
1446
|
+
bestDeviations = deviations;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
return best;
|
|
1450
|
+
}
|
|
1451
|
+
function spellGappedScale(tonic, key) {
|
|
1452
|
+
const tones = scaleTonesInDegreeOrder(key);
|
|
1453
|
+
if (tones.length > DIATONIC_LETTER_COUNT) {
|
|
1454
|
+
return void 0;
|
|
1455
|
+
}
|
|
1456
|
+
const tonicPc = pitchClassOf(naturalPitchClassOf(tonic.letter) + tonic.alter);
|
|
1457
|
+
const tonicNote = { letter: diatonicLetterOf(tonic.letter), alter: tonic.alter };
|
|
1458
|
+
const preferFlat = leansFlat(tonic, key);
|
|
1459
|
+
const rest = tones.filter((pc) => pitchClassOf(pc - tonicPc) !== 0);
|
|
1460
|
+
const chosen = assignDistinctLetters(
|
|
1461
|
+
rest.map((pc) => gappedCandidates(tonic, pitchClassOf(pc - tonicPc), pc, preferFlat)),
|
|
1462
|
+
1 << tonicNote.letter
|
|
1463
|
+
);
|
|
1464
|
+
if (chosen === void 0) {
|
|
1465
|
+
return void 0;
|
|
1466
|
+
}
|
|
1467
|
+
const spellings = /* @__PURE__ */ new Map([[tonicPc, tonicNote]]);
|
|
1468
|
+
for (const [index, pc] of rest.entries()) {
|
|
1469
|
+
const note = chosen[index];
|
|
1470
|
+
if (note !== void 0) {
|
|
1471
|
+
spellings.set(pitchClassOf(pc), note);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
return spellings;
|
|
1475
|
+
}
|
|
1476
|
+
function spellByKey(pc, tonic, key) {
|
|
1477
|
+
const tonicPc = pitchClassOf(naturalPitchClassOf(tonic.letter) + tonic.alter);
|
|
1478
|
+
const offset = pitchClassOf(pc - tonicPc);
|
|
1479
|
+
if (offset === 0) {
|
|
1480
|
+
return { letter: diatonicLetterOf(tonic.letter), alter: tonic.alter };
|
|
1481
|
+
}
|
|
1482
|
+
const heptatonic = isHeptatonic(key);
|
|
1483
|
+
if (heptatonic) {
|
|
1484
|
+
const degree = scaleTonesInDegreeOrder(key).indexOf(pitchClassOf(pc));
|
|
1485
|
+
if (degree >= 0) {
|
|
1486
|
+
const letter = diatonicLetterOf(tonic.letter + degree);
|
|
1487
|
+
return { letter, alter: alterFor(letter, pc) };
|
|
1488
|
+
}
|
|
1489
|
+
const degreeOffset = CHROMATIC_SPELLING[offset];
|
|
1490
|
+
const preferred = letterFor(tonic, degreeOffset, pc);
|
|
1491
|
+
if (preferred !== void 0) {
|
|
1492
|
+
const flat = letterFor(tonic, CHROMATIC_SPELLING_FLAT[offset], pc);
|
|
1493
|
+
const sharp = letterFor(tonic, CHROMATIC_SPELLING_SHARP[offset], pc);
|
|
1494
|
+
const alternative = flat?.letter === preferred.letter ? sharp : sharp?.letter === preferred.letter ? flat : void 0;
|
|
1495
|
+
return capChromaticAccidentals(preferred, alternative);
|
|
1496
|
+
}
|
|
1497
|
+
} else {
|
|
1498
|
+
if (isScaleTone(pc, key)) {
|
|
1499
|
+
const assigned = spellGappedScale(tonic, key)?.get(pitchClassOf(pc));
|
|
1500
|
+
if (assigned !== void 0) {
|
|
1501
|
+
return assigned;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
const [preferred] = gappedCandidates(tonic, offset, pc, leansFlat(tonic, key));
|
|
1505
|
+
if (preferred !== void 0) {
|
|
1506
|
+
return preferred;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
const belowLetter = diatonicLetterOf([0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6][pitchClassOf(pc)] ?? 0);
|
|
1510
|
+
return { letter: belowLetter, alter: alterFor(belowLetter, pc) };
|
|
1511
|
+
}
|
|
1512
|
+
var CHORD_TONE_LETTER_STEPS = {
|
|
1513
|
+
0: 0,
|
|
1514
|
+
// root
|
|
1515
|
+
3: 2,
|
|
1516
|
+
// minor third
|
|
1517
|
+
4: 2,
|
|
1518
|
+
// major third
|
|
1519
|
+
7: 4,
|
|
1520
|
+
// perfect fifth
|
|
1521
|
+
10: 6,
|
|
1522
|
+
// minor seventh
|
|
1523
|
+
11: 6
|
|
1524
|
+
// major seventh
|
|
1525
|
+
};
|
|
1526
|
+
var MAX_CONTEXT_ALTER = 2;
|
|
1527
|
+
function chordToneSpelling(pc, chordRoot, tonic, key) {
|
|
1528
|
+
if (chordRoot === void 0) {
|
|
1529
|
+
return void 0;
|
|
1530
|
+
}
|
|
1531
|
+
const root = spellByKey(pitchClassOf(chordRoot), tonic, key);
|
|
1532
|
+
return letterFor(root, CHORD_TONE_LETTER_STEPS[pitchClassOf(pc - chordRoot)], pc);
|
|
1533
|
+
}
|
|
1534
|
+
function neighbourSpelling(neighbour, gap, letterStep, pc, tonic, key) {
|
|
1535
|
+
if (neighbour === void 0 || pitchClassOf(neighbour - pc) !== pitchClassOf(gap)) {
|
|
1536
|
+
return void 0;
|
|
1537
|
+
}
|
|
1538
|
+
return letterFor(spellByKey(pitchClassOf(neighbour), tonic, key), letterStep, pc);
|
|
1539
|
+
}
|
|
1540
|
+
function refineByContext(pc, tonic, key, context, keySpelling) {
|
|
1541
|
+
const tonicPc = pitchClassOf(naturalPitchClassOf(tonic.letter) + tonic.alter);
|
|
1542
|
+
if (pitchClassOf(pc) === tonicPc || isScaleTone(pc, key)) {
|
|
1543
|
+
return keySpelling;
|
|
1544
|
+
}
|
|
1545
|
+
const { chordRoot, previous, next } = context;
|
|
1546
|
+
const candidates = [
|
|
1547
|
+
// The sounding chord names the pitch by its interval above the root.
|
|
1548
|
+
chordToneSpelling(pc, chordRoot, tonic, key),
|
|
1549
|
+
// A semitone below what follows: the pitch is its leading tone, a letter down.
|
|
1550
|
+
neighbourSpelling(next, 1, -1, pc, tonic, key),
|
|
1551
|
+
// A semitone above what preceded: the pitch is that note inflected upward,
|
|
1552
|
+
// so it keeps its letter and a chromatic ascent spells with sharps.
|
|
1553
|
+
neighbourSpelling(previous, -1, 0, pc, tonic, key),
|
|
1554
|
+
// A semitone above what follows: the pitch leans on it from a letter up,
|
|
1555
|
+
// so a falling line spells with flats.
|
|
1556
|
+
neighbourSpelling(next, -1, 1, pc, tonic, key),
|
|
1557
|
+
// Failing that, a semitone below what preceded: that note inflected downward,
|
|
1558
|
+
// again on its own letter.
|
|
1559
|
+
neighbourSpelling(previous, 1, 0, pc, tonic, key)
|
|
1560
|
+
];
|
|
1561
|
+
for (const candidate of candidates) {
|
|
1562
|
+
if (candidate !== void 0 && Math.abs(candidate.alter) <= MAX_CONTEXT_ALTER) {
|
|
1563
|
+
return candidate;
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
return keySpelling;
|
|
1567
|
+
}
|
|
1568
|
+
function spellPitchClass(pc, tonic, key, context) {
|
|
1569
|
+
const note = toNoteData(tonic);
|
|
1570
|
+
const scale = toKeyScale(key);
|
|
1571
|
+
assertTonicOf(note, scale, "spellPitchClass");
|
|
1572
|
+
return spelledPitchClass(pc, note, scale, context);
|
|
1573
|
+
}
|
|
1574
|
+
function spelledPitchClass(pc, tonic, key, context) {
|
|
1575
|
+
const keySpelling = spellByKey(pc, tonic, key);
|
|
1576
|
+
return context === void 0 ? keySpelling : refineByContext(pc, tonic, key, context, keySpelling);
|
|
1577
|
+
}
|
|
1578
|
+
function spellScale(tonic, key) {
|
|
1579
|
+
const note = toNoteData(tonic);
|
|
1580
|
+
const scale = key;
|
|
1581
|
+
assertTonicOf(note, scale, "spellScale");
|
|
1582
|
+
return scaleTonesInDegreeOrder(scale).map((pc) => spelledPitchClass(pc, note, scale));
|
|
1583
|
+
}
|
|
1584
|
+
function spellPitchClasses(pcs, tonic, key) {
|
|
1585
|
+
const note = toNoteData(tonic);
|
|
1586
|
+
const scale = toKeyScale(key);
|
|
1587
|
+
assertTonicOf(note, scale, "spellPitchClasses");
|
|
1588
|
+
return pcs.map((pc) => spelledPitchClass(pc, note, scale));
|
|
1589
|
+
}
|
|
1590
|
+
function chordLetterOffset(interval, chord) {
|
|
1591
|
+
switch (interval) {
|
|
1592
|
+
case 0:
|
|
1593
|
+
case 12:
|
|
1594
|
+
return 0;
|
|
1595
|
+
case 1:
|
|
1596
|
+
case 2:
|
|
1597
|
+
case 13:
|
|
1598
|
+
case 14:
|
|
1599
|
+
return 1;
|
|
1600
|
+
case 3:
|
|
1601
|
+
case 4:
|
|
1602
|
+
case 16:
|
|
1603
|
+
return 2;
|
|
1604
|
+
case 15:
|
|
1605
|
+
return chord.intervals.some((i) => i === 3 || i === 4) ? 1 : 2;
|
|
1606
|
+
case 5:
|
|
1607
|
+
case 17:
|
|
1608
|
+
case 18:
|
|
1609
|
+
return 3;
|
|
1610
|
+
case 6:
|
|
1611
|
+
case 7:
|
|
1612
|
+
case 8:
|
|
1613
|
+
case 19:
|
|
1614
|
+
return 4;
|
|
1615
|
+
case 9:
|
|
1616
|
+
return chord.quality === "dim7" ? 6 : 5;
|
|
1617
|
+
case 20:
|
|
1618
|
+
case 21:
|
|
1619
|
+
return 5;
|
|
1620
|
+
case 10:
|
|
1621
|
+
case 11:
|
|
1622
|
+
case 22:
|
|
1623
|
+
case 23:
|
|
1624
|
+
return 6;
|
|
1625
|
+
default: {
|
|
1626
|
+
const octaves = Math.floor(Math.max(0, interval) / 12);
|
|
1627
|
+
const simple = pitchClassOf(interval);
|
|
1628
|
+
const simpleOffset = [0, 1, 1, 2, 2, 3, 4, 4, 4, 5, 6, 6][simple] ?? 0;
|
|
1629
|
+
return octaves * 7 + simpleOffset;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
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
|
+
function spellChordFromRoot(chord, root) {
|
|
1642
|
+
return chordTonesFromRoot(chord, toNoteData(root));
|
|
1643
|
+
}
|
|
1644
|
+
function chordTonesFromRoot(chord, root) {
|
|
1645
|
+
if (pitchClassOf(naturalPitchClassOf(root.letter) + root.alter) !== pitchClassOf(chord.rootPc)) {
|
|
1646
|
+
throw new InvalidInputError("chord root spelling must match chord.rootPc");
|
|
1647
|
+
}
|
|
1648
|
+
const hinted = hintedLetterOffsets(chord);
|
|
1649
|
+
return chord.intervals.map((interval, index) => {
|
|
1650
|
+
const letter = diatonicLetterOf(root.letter + (hinted?.[index] ?? chordLetterOffset(interval, chord)));
|
|
1651
|
+
const pc = pitchClassOf(chord.rootPc + interval);
|
|
1652
|
+
return { letter, alter: alterFor(letter, pc) };
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
function spellChord(chord, tonic, key) {
|
|
1656
|
+
const note = toNoteData(tonic);
|
|
1657
|
+
const scale = toKeyScale(key);
|
|
1658
|
+
assertTonicOf(note, scale, "spellChord");
|
|
1659
|
+
return chordTonesFromRoot(chord, chordRootSpelling(chord, note, scale));
|
|
1660
|
+
}
|
|
1661
|
+
function chordRootSpelling(chord, tonic, key) {
|
|
1662
|
+
const hint = chord.rootSpelling;
|
|
1663
|
+
if (hint !== void 0 && pitchClassOf(naturalPitchClassOf(hint.letter) + hint.alter) === pitchClassOf(chord.rootPc)) {
|
|
1664
|
+
return { letter: diatonicLetterOf(hint.letter), alter: hint.alter };
|
|
1665
|
+
}
|
|
1666
|
+
return spelledPitchClass(chord.rootPc, tonic, key);
|
|
1667
|
+
}
|
|
1668
|
+
function spellPitch(pitch, tonic, key, context) {
|
|
1669
|
+
const note = toNoteData(tonic);
|
|
1670
|
+
const scale = toKeyScale(key);
|
|
1671
|
+
assertTonicOf(note, scale, "spellPitch");
|
|
1672
|
+
const rounded = Math.round(pitch);
|
|
1673
|
+
const spelled = spelledPitchClass(pitchClassOf(rounded), note, scale, context);
|
|
1674
|
+
const octave = (rounded - naturalPitchClassOf(spelled.letter) - spelled.alter) / 12 - 1;
|
|
1675
|
+
return { letter: spelled.letter, alter: spelled.alter, octave };
|
|
1676
|
+
}
|
|
1677
|
+
function noteNames(notes) {
|
|
1678
|
+
return notes.map((note) => formatNote(toNoteData(note)));
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// src/theory/counterpoint/internal.ts
|
|
1682
|
+
function pitchOf(value) {
|
|
1683
|
+
return typeof value === "number" ? value : noteToMidi(value);
|
|
1684
|
+
}
|
|
1685
|
+
function simpleIntervalNumber(numberValue) {
|
|
1686
|
+
return (Math.abs(numberValue) - 1) % 7 + 1;
|
|
1687
|
+
}
|
|
1688
|
+
function classifySpelledInterval(interval, twoVoice = true) {
|
|
1689
|
+
const simple = simpleIntervalNumber(interval.number);
|
|
1690
|
+
const quality = interval.quality;
|
|
1691
|
+
if (quality === "P") {
|
|
1692
|
+
if (simple === 1 || simple === 5) {
|
|
1693
|
+
return 0 /* PerfectConsonance */;
|
|
1694
|
+
}
|
|
1695
|
+
if (simple === 4) {
|
|
1696
|
+
return twoVoice ? 2 /* Dissonance */ : 1 /* ImperfectConsonance */;
|
|
1697
|
+
}
|
|
1698
|
+
return 2 /* Dissonance */;
|
|
1699
|
+
}
|
|
1700
|
+
if ((quality === "M" || quality === "m") && (simple === 3 || simple === 6)) {
|
|
1701
|
+
return 1 /* ImperfectConsonance */;
|
|
1702
|
+
}
|
|
1703
|
+
return 2 /* Dissonance */;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// src/theory/counterpoint/independence.ts
|
|
1707
|
+
function bothSound(slot) {
|
|
1708
|
+
return slot.lead !== null && slot.counter !== null;
|
|
1709
|
+
}
|
|
1710
|
+
function attacksAt(index, notes, flags) {
|
|
1711
|
+
if (flags !== void 0) {
|
|
1712
|
+
return flags[index] === true;
|
|
1713
|
+
}
|
|
1714
|
+
const current = notes[index];
|
|
1715
|
+
if (current === void 0 || current === null) {
|
|
1716
|
+
return false;
|
|
1717
|
+
}
|
|
1718
|
+
const previous = index > 0 ? notes[index - 1] : void 0;
|
|
1719
|
+
if (previous === void 0 || previous === null) {
|
|
1720
|
+
return true;
|
|
1721
|
+
}
|
|
1722
|
+
return previous.letter !== current.letter || previous.alter !== current.alter || previous.octave !== current.octave;
|
|
1723
|
+
}
|
|
1724
|
+
function motionBetween(from, to) {
|
|
1725
|
+
const leadMove = pitchOf(to.lead) - pitchOf(from.lead);
|
|
1726
|
+
const counterMove = pitchOf(to.counter) - pitchOf(from.counter);
|
|
1727
|
+
if (leadMove === 0 && counterMove === 0) {
|
|
1728
|
+
return null;
|
|
1729
|
+
}
|
|
1730
|
+
if (leadMove === 0 || counterMove === 0) {
|
|
1731
|
+
return "oblique";
|
|
1732
|
+
}
|
|
1733
|
+
if (leadMove > 0 !== counterMove > 0) {
|
|
1734
|
+
return "contrary";
|
|
1735
|
+
}
|
|
1736
|
+
const before = simpleIntervalNumber(spelledInterval(from.counter, from.lead).number);
|
|
1737
|
+
const after = simpleIntervalNumber(spelledInterval(to.counter, to.lead).number);
|
|
1738
|
+
return before === after ? "parallel" : "similar";
|
|
1739
|
+
}
|
|
1740
|
+
function voiceIndependence(lead, counter, opts) {
|
|
1741
|
+
if (lead.length !== counter.length) {
|
|
1742
|
+
throw new InvalidInputError(
|
|
1743
|
+
`voiceIndependence needs the two lines aligned slot for slot; received ${lead.length} and ${counter.length}`
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
for (const [name, flags] of [
|
|
1747
|
+
["leadAttacks", opts?.leadAttacks],
|
|
1748
|
+
["counterAttacks", opts?.counterAttacks]
|
|
1749
|
+
]) {
|
|
1750
|
+
if (flags !== void 0 && flags.length !== lead.length) {
|
|
1751
|
+
throw new InvalidInputError(
|
|
1752
|
+
`voiceIndependence needs ${name} aligned slot for slot with the lines; received ${flags.length} and ${lead.length}`
|
|
1753
|
+
);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
const slots = lead.map((leadNote, index) => {
|
|
1757
|
+
const counterNote = counter[index] ?? null;
|
|
1758
|
+
return {
|
|
1759
|
+
lead: leadNote,
|
|
1760
|
+
counter: counterNote,
|
|
1761
|
+
leadPitch: leadNote === null ? null : pitchOf(leadNote),
|
|
1762
|
+
counterPitch: counterNote === null ? null : pitchOf(counterNote)
|
|
1763
|
+
};
|
|
1764
|
+
});
|
|
1765
|
+
const counts = { contrary: 0, oblique: 0, similar: 0, parallel: 0 };
|
|
1766
|
+
let moves = 0;
|
|
1767
|
+
for (let index = 1; index < slots.length; index += 1) {
|
|
1768
|
+
const from = slots[index - 1];
|
|
1769
|
+
const to = slots[index];
|
|
1770
|
+
if (from === void 0 || to === void 0 || !bothSound(from) || !bothSound(to)) {
|
|
1771
|
+
continue;
|
|
1772
|
+
}
|
|
1773
|
+
const motion = motionBetween(from, to);
|
|
1774
|
+
if (motion === null) {
|
|
1775
|
+
continue;
|
|
1776
|
+
}
|
|
1777
|
+
counts[motion] += 1;
|
|
1778
|
+
moves += 1;
|
|
1779
|
+
}
|
|
1780
|
+
let filled = 0;
|
|
1781
|
+
let gaps = 0;
|
|
1782
|
+
for (let index = 0; index < slots.length; index += 1) {
|
|
1783
|
+
const leadHolds = !attacksAt(index, lead, opts?.leadAttacks);
|
|
1784
|
+
if (!leadHolds) {
|
|
1785
|
+
continue;
|
|
1786
|
+
}
|
|
1787
|
+
gaps += 1;
|
|
1788
|
+
if (attacksAt(index, counter, opts?.counterAttacks)) {
|
|
1789
|
+
filled += 1;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
let sounding = 0;
|
|
1793
|
+
let totalGap = 0;
|
|
1794
|
+
let minGap = Number.POSITIVE_INFINITY;
|
|
1795
|
+
let orderSum = 0;
|
|
1796
|
+
let perfectRun = 0;
|
|
1797
|
+
let longestPerfectRun = 0;
|
|
1798
|
+
for (const slot of slots) {
|
|
1799
|
+
if (slot.leadPitch === null || slot.counterPitch === null) {
|
|
1800
|
+
perfectRun = 0;
|
|
1801
|
+
continue;
|
|
1802
|
+
}
|
|
1803
|
+
sounding += 1;
|
|
1804
|
+
const gap = slot.leadPitch - slot.counterPitch;
|
|
1805
|
+
totalGap += Math.abs(gap);
|
|
1806
|
+
minGap = Math.min(minGap, Math.abs(gap));
|
|
1807
|
+
orderSum += Math.sign(gap);
|
|
1808
|
+
const interval = spelledInterval(slot.counter, slot.lead);
|
|
1809
|
+
const perfect = classifySpelledInterval(interval) === 0 /* PerfectConsonance */ || (opts?.countFourths ?? false) && interval.quality === "P" && simpleIntervalNumber(interval.number) === 4;
|
|
1810
|
+
perfectRun = perfect ? perfectRun + 1 : 0;
|
|
1811
|
+
longestPerfectRun = Math.max(longestPerfectRun, perfectRun);
|
|
1812
|
+
}
|
|
1813
|
+
const leadAbove = orderSum >= 0;
|
|
1814
|
+
let crossings = 0;
|
|
1815
|
+
for (const slot of slots) {
|
|
1816
|
+
if (slot.leadPitch === null || slot.counterPitch === null) {
|
|
1817
|
+
continue;
|
|
1818
|
+
}
|
|
1819
|
+
const gap = slot.leadPitch - slot.counterPitch;
|
|
1820
|
+
if (gap !== 0 && gap > 0 !== leadAbove) {
|
|
1821
|
+
crossings += 1;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
return {
|
|
1825
|
+
motion: {
|
|
1826
|
+
contrary: moves === 0 ? 0 : counts.contrary / moves,
|
|
1827
|
+
oblique: moves === 0 ? 0 : counts.oblique / moves,
|
|
1828
|
+
similar: moves === 0 ? 0 : counts.similar / moves,
|
|
1829
|
+
parallel: moves === 0 ? 0 : counts.parallel / moves
|
|
1830
|
+
},
|
|
1831
|
+
rhythmicComplementarity: gaps === 0 ? 0 : filled / gaps,
|
|
1832
|
+
separation: {
|
|
1833
|
+
mean: sounding === 0 ? 0 : totalGap / sounding,
|
|
1834
|
+
min: sounding === 0 ? 0 : minGap
|
|
1835
|
+
},
|
|
1836
|
+
crossings,
|
|
1837
|
+
longestPerfectRun,
|
|
1838
|
+
sounding
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
// src/theory/counterpoint/index.ts
|
|
1843
|
+
function createsVoiceCrossing(upper, lower) {
|
|
1844
|
+
return pitchOf(upper) < pitchOf(lower);
|
|
1845
|
+
}
|
|
1846
|
+
function createsVerticalDissonance(a, b, twoVoice) {
|
|
1847
|
+
if (typeof a !== "number" && typeof b !== "number") {
|
|
1848
|
+
return classifySpelledInterval(spelledInterval(a, b), twoVoice) === 2 /* Dissonance */;
|
|
1849
|
+
}
|
|
1850
|
+
return !isConsonantInterval(pitchOf(a) - pitchOf(b), twoVoice);
|
|
1851
|
+
}
|
|
1852
|
+
function isForbiddenMelodicLeap(prev, cur) {
|
|
1853
|
+
if (typeof prev !== "number" && typeof cur !== "number") {
|
|
1854
|
+
const interval = spelledInterval(prev, cur);
|
|
1855
|
+
if (Math.abs(interval.semitones) > 12 || interval.number > 8) {
|
|
1856
|
+
return true;
|
|
1857
|
+
}
|
|
1858
|
+
if (simpleIntervalNumber(interval.number) === 7) {
|
|
1859
|
+
return true;
|
|
1860
|
+
}
|
|
1861
|
+
return isAlteredInterval(interval);
|
|
1862
|
+
}
|
|
1863
|
+
const semis = Math.abs(pitchOf(cur) - pitchOf(prev));
|
|
1864
|
+
return semis === 6 || semis === 10 || semis === 11 || semis > 12;
|
|
1865
|
+
}
|
|
1866
|
+
function isAlteredInterval(interval) {
|
|
1867
|
+
return interval.number >= 2 && (interval.quality.startsWith("A") || interval.quality[0] === "d");
|
|
1868
|
+
}
|
|
1869
|
+
function isAugmentedMelodicInterval(prev, cur) {
|
|
1870
|
+
if (typeof prev === "number" || typeof cur === "number") {
|
|
1871
|
+
return false;
|
|
1872
|
+
}
|
|
1873
|
+
const interval = spelledInterval(prev, cur);
|
|
1874
|
+
return interval.number >= 2 && interval.quality.startsWith("A");
|
|
1875
|
+
}
|
|
1876
|
+
function simpleClass(semitones) {
|
|
1877
|
+
return pitchClassOf(Math.abs(semitones));
|
|
1878
|
+
}
|
|
1879
|
+
function isPerfectClass(cls) {
|
|
1880
|
+
return cls === 0 || cls === 7;
|
|
1881
|
+
}
|
|
1882
|
+
function similarMotion(aMove, bMove) {
|
|
1883
|
+
return aMove !== 0 && bMove !== 0 && aMove > 0 === bMove > 0;
|
|
1884
|
+
}
|
|
1885
|
+
function bothVoicesMove(aMove, bMove) {
|
|
1886
|
+
return aMove !== 0 && bMove !== 0;
|
|
1887
|
+
}
|
|
1888
|
+
function createsParallelPerfect(aPrev, aCur, bPrev, bCur) {
|
|
1889
|
+
const a0 = pitchOf(aPrev);
|
|
1890
|
+
const a1 = pitchOf(aCur);
|
|
1891
|
+
const b0 = pitchOf(bPrev);
|
|
1892
|
+
const b1 = pitchOf(bCur);
|
|
1893
|
+
if (!bothVoicesMove(a1 - a0, b1 - b0)) {
|
|
1894
|
+
return false;
|
|
1895
|
+
}
|
|
1896
|
+
const nowClass = simpleClass(a1 - b1);
|
|
1897
|
+
const prevClass = simpleClass(a0 - b0);
|
|
1898
|
+
return isPerfectClass(nowClass) && nowClass === prevClass;
|
|
1899
|
+
}
|
|
1900
|
+
function createsParallelOctave(aPrev, aCur, bPrev, bCur) {
|
|
1901
|
+
const a0 = pitchOf(aPrev);
|
|
1902
|
+
const a1 = pitchOf(aCur);
|
|
1903
|
+
const b0 = pitchOf(bPrev);
|
|
1904
|
+
const b1 = pitchOf(bCur);
|
|
1905
|
+
if (!similarMotion(a1 - a0, b1 - b0)) {
|
|
1906
|
+
return false;
|
|
1907
|
+
}
|
|
1908
|
+
return simpleClass(a1 - b1) === 0 && simpleClass(a0 - b0) === 0;
|
|
1909
|
+
}
|
|
1910
|
+
function createsParallelUnison(aPrev, aCur, bPrev, bCur) {
|
|
1911
|
+
const a0 = pitchOf(aPrev);
|
|
1912
|
+
const a1 = pitchOf(aCur);
|
|
1913
|
+
const b0 = pitchOf(bPrev);
|
|
1914
|
+
const b1 = pitchOf(bCur);
|
|
1915
|
+
if (a1 === a0 || b1 === b0) {
|
|
1916
|
+
return false;
|
|
1917
|
+
}
|
|
1918
|
+
return a1 === b1 && a0 === b0;
|
|
1919
|
+
}
|
|
1920
|
+
function createsHiddenParallelPerfect(aPrev, aCur, bPrev, bCur) {
|
|
1921
|
+
const a0 = pitchOf(aPrev);
|
|
1922
|
+
const a1 = pitchOf(aCur);
|
|
1923
|
+
const b0 = pitchOf(bPrev);
|
|
1924
|
+
const b1 = pitchOf(bCur);
|
|
1925
|
+
const aMove = a1 - a0;
|
|
1926
|
+
const bMove = b1 - b0;
|
|
1927
|
+
if (!similarMotion(aMove, bMove)) {
|
|
1928
|
+
return false;
|
|
1929
|
+
}
|
|
1930
|
+
const nowClass = simpleClass(a1 - b1);
|
|
1931
|
+
const prevClass = simpleClass(a0 - b0);
|
|
1932
|
+
if (!isPerfectClass(nowClass) || prevClass === nowClass) {
|
|
1933
|
+
return false;
|
|
1934
|
+
}
|
|
1935
|
+
const upperMove = a1 >= b1 ? aMove : bMove;
|
|
1936
|
+
if (Math.abs(upperMove) <= 2) {
|
|
1937
|
+
return false;
|
|
1938
|
+
}
|
|
1939
|
+
return true;
|
|
1940
|
+
}
|
|
1941
|
+
function createsBattuta(aPrev, aCur, bPrev, bCur) {
|
|
1942
|
+
const a0 = pitchOf(aPrev);
|
|
1943
|
+
const a1 = pitchOf(aCur);
|
|
1944
|
+
const b0 = pitchOf(bPrev);
|
|
1945
|
+
const b1 = pitchOf(bCur);
|
|
1946
|
+
const aMove = a1 - a0;
|
|
1947
|
+
const bMove = b1 - b0;
|
|
1948
|
+
if (!bothVoicesMove(aMove, bMove) || similarMotion(aMove, bMove)) {
|
|
1949
|
+
return false;
|
|
1950
|
+
}
|
|
1951
|
+
if (simpleClass(a1 - b1) !== 0) {
|
|
1952
|
+
return false;
|
|
1953
|
+
}
|
|
1954
|
+
const upperMove = a1 >= b1 ? aMove : bMove;
|
|
1955
|
+
return upperMove < -2;
|
|
1956
|
+
}
|
|
1957
|
+
function createsVoiceOverlap(upperPrev, upperCur, lowerPrev, lowerCur) {
|
|
1958
|
+
const u0 = pitchOf(upperPrev);
|
|
1959
|
+
const u1 = pitchOf(upperCur);
|
|
1960
|
+
const l0 = pitchOf(lowerPrev);
|
|
1961
|
+
const l1 = pitchOf(lowerCur);
|
|
1962
|
+
return u1 < l0 || l1 > u0;
|
|
1963
|
+
}
|
|
1964
|
+
function exceedsSpacing(upper, lower, maxSemitones = 12) {
|
|
1965
|
+
return Math.abs(pitchOf(upper) - pitchOf(lower)) > maxSemitones;
|
|
1966
|
+
}
|
|
1967
|
+
function isLeadingToneResolution(prev, cur, key) {
|
|
1968
|
+
const prevPitch = pitchOf(prev);
|
|
1969
|
+
const curPitch = pitchOf(cur);
|
|
1970
|
+
const tonic = pitchClassOf(key.rootPc);
|
|
1971
|
+
const leading = (tonic + 11) % 12;
|
|
1972
|
+
if (pitchClassOf(prevPitch) !== leading || pitchClassOf(curPitch) !== tonic) {
|
|
1973
|
+
return false;
|
|
1974
|
+
}
|
|
1975
|
+
return curPitch > prevPitch && curPitch - prevPitch <= 2;
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// src/theory/safety/index.ts
|
|
1979
|
+
var PROFILE_WEIGHTS = Object.freeze({
|
|
1980
|
+
strict: Object.freeze({
|
|
1981
|
+
imperfectConsonance: 2,
|
|
1982
|
+
perfectConsonance: 0.5,
|
|
1983
|
+
dissonance: -2,
|
|
1984
|
+
chordTone: 1,
|
|
1985
|
+
contraryMotion: 2,
|
|
1986
|
+
obliqueMotion: 1,
|
|
1987
|
+
similarMotion: -1,
|
|
1988
|
+
parallelImperfect: 0,
|
|
1989
|
+
stepwise: 1,
|
|
1990
|
+
melodicDistance: -0.3,
|
|
1991
|
+
forbiddenLeap: -6,
|
|
1992
|
+
registerDrift: -0.02
|
|
1993
|
+
}),
|
|
1994
|
+
pop: Object.freeze({
|
|
1995
|
+
imperfectConsonance: 2.5,
|
|
1996
|
+
perfectConsonance: 1,
|
|
1997
|
+
dissonance: -2,
|
|
1998
|
+
chordTone: 1.5,
|
|
1999
|
+
contraryMotion: 1.5,
|
|
2000
|
+
obliqueMotion: 1.25,
|
|
2001
|
+
similarMotion: 0,
|
|
2002
|
+
parallelImperfect: 2,
|
|
2003
|
+
stepwise: 0.75,
|
|
2004
|
+
melodicDistance: -0.2,
|
|
2005
|
+
forbiddenLeap: -6,
|
|
2006
|
+
registerDrift: -0.02
|
|
2007
|
+
})
|
|
2008
|
+
});
|
|
2009
|
+
var SAFETY_PROFILES = Object.keys(PROFILE_WEIGHTS);
|
|
2010
|
+
function profileWeights(profile, overrides) {
|
|
2011
|
+
const base = PROFILE_WEIGHTS[assertOneOf(profile, SAFETY_PROFILES, "safety profile")];
|
|
2012
|
+
return { ...base, ...overrides };
|
|
2013
|
+
}
|
|
2014
|
+
var NoteSafety = /* @__PURE__ */ ((NoteSafety2) => {
|
|
2015
|
+
NoteSafety2[NoteSafety2["Safe"] = 0] = "Safe";
|
|
2016
|
+
NoteSafety2[NoteSafety2["Warning"] = 1] = "Warning";
|
|
2017
|
+
NoteSafety2[NoteSafety2["Dissonant"] = 2] = "Dissonant";
|
|
2018
|
+
return NoteSafety2;
|
|
2019
|
+
})(NoteSafety || {});
|
|
2020
|
+
var ReasonFlag = /* @__PURE__ */ ((ReasonFlag2) => {
|
|
2021
|
+
ReasonFlag2[ReasonFlag2["ChordTone"] = 1] = "ChordTone";
|
|
2022
|
+
ReasonFlag2[ReasonFlag2["Tension"] = 2] = "Tension";
|
|
2023
|
+
ReasonFlag2[ReasonFlag2["AvoidNote"] = 4] = "AvoidNote";
|
|
2024
|
+
ReasonFlag2[ReasonFlag2["ScaleTone"] = 8] = "ScaleTone";
|
|
2025
|
+
ReasonFlag2[ReasonFlag2["NonScale"] = 16] = "NonScale";
|
|
2026
|
+
ReasonFlag2[ReasonFlag2["OutOfRange"] = 32] = "OutOfRange";
|
|
2027
|
+
ReasonFlag2[ReasonFlag2["Tritone"] = 64] = "Tritone";
|
|
2028
|
+
ReasonFlag2[ReasonFlag2["LargeLeap"] = 128] = "LargeLeap";
|
|
2029
|
+
ReasonFlag2[ReasonFlag2["MinorSecond"] = 256] = "MinorSecond";
|
|
2030
|
+
ReasonFlag2[ReasonFlag2["MajorSeventh"] = 512] = "MajorSeventh";
|
|
2031
|
+
ReasonFlag2[ReasonFlag2["VerticalDissonance"] = 1024] = "VerticalDissonance";
|
|
2032
|
+
ReasonFlag2[ReasonFlag2["ParallelPerfect"] = 2048] = "ParallelPerfect";
|
|
2033
|
+
ReasonFlag2[ReasonFlag2["HiddenParallel"] = 4096] = "HiddenParallel";
|
|
2034
|
+
ReasonFlag2[ReasonFlag2["VoiceCrossing"] = 8192] = "VoiceCrossing";
|
|
2035
|
+
ReasonFlag2[ReasonFlag2["Suspension"] = 16384] = "Suspension";
|
|
2036
|
+
ReasonFlag2[ReasonFlag2["NeedsResolution"] = 32768] = "NeedsResolution";
|
|
2037
|
+
ReasonFlag2[ReasonFlag2["MelodicTritone"] = 65536] = "MelodicTritone";
|
|
2038
|
+
return ReasonFlag2;
|
|
2039
|
+
})(ReasonFlag || {});
|
|
2040
|
+
var SUGGESTION_WINDOW = 12;
|
|
2041
|
+
var MAX_SUGGESTIONS = 3;
|
|
2042
|
+
var REASON_SEVERITY = [
|
|
2043
|
+
[2 /* Tension */, 1 /* Warning */, 1 /* Warning */],
|
|
2044
|
+
[4 /* AvoidNote */, 1 /* Warning */, 2 /* Dissonant */],
|
|
2045
|
+
[8 /* ScaleTone */, 1 /* Warning */, 1 /* Warning */],
|
|
2046
|
+
[16 /* NonScale */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
2047
|
+
[32 /* OutOfRange */, 1 /* Warning */, 1 /* Warning */],
|
|
2048
|
+
[64 /* Tritone */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
2049
|
+
[65536 /* MelodicTritone */, 1 /* Warning */, 2 /* Dissonant */],
|
|
2050
|
+
[128 /* LargeLeap */, 1 /* Warning */, 2 /* Dissonant */],
|
|
2051
|
+
[1024 /* VerticalDissonance */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
2052
|
+
[2048 /* ParallelPerfect */, 1 /* Warning */, 2 /* Dissonant */],
|
|
2053
|
+
[4096 /* HiddenParallel */, 1 /* Warning */, 2 /* Dissonant */],
|
|
2054
|
+
[8192 /* VoiceCrossing */, 1 /* Warning */, 2 /* Dissonant */],
|
|
2055
|
+
[16384 /* Suspension */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
2056
|
+
[32768 /* NeedsResolution */, 1 /* Warning */, 2 /* Dissonant */]
|
|
2057
|
+
];
|
|
2058
|
+
function minimumSafetyForReasons(reasons, profile) {
|
|
2059
|
+
let minimum = 0 /* Safe */;
|
|
2060
|
+
const severityIndex = profile === "strict" ? 2 : 1;
|
|
2061
|
+
for (const entry of REASON_SEVERITY) {
|
|
2062
|
+
if (reasons & entry[0]) {
|
|
2063
|
+
minimum = Math.max(minimum, entry[severityIndex]);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
return minimum;
|
|
2067
|
+
}
|
|
2068
|
+
function avoidsFourth(chord) {
|
|
2069
|
+
return chord.intervals.includes(4) && !chord.intervals.includes(5);
|
|
2070
|
+
}
|
|
2071
|
+
function avoidsMajorSeventh(chord) {
|
|
2072
|
+
return chord.intervals.includes(4) && chord.intervals.includes(10);
|
|
2073
|
+
}
|
|
2074
|
+
function isStructuralChordDissonance(a, b, chord) {
|
|
2075
|
+
const rawClass = pitchClassOf(Math.abs(a - b));
|
|
2076
|
+
const aOffset = intervalAboveRoot(a, chord);
|
|
2077
|
+
const bOffset = intervalAboveRoot(b, chord);
|
|
2078
|
+
const rootSeventh = aOffset === 0 && (bOffset === 10 || bOffset === 11) || bOffset === 0 && (aOffset === 10 || aOffset === 11);
|
|
2079
|
+
if (rawClass !== 6 && !rootSeventh) {
|
|
2080
|
+
return false;
|
|
2081
|
+
}
|
|
2082
|
+
return isChordMember(a, chord) && isChordMember(b, chord);
|
|
2083
|
+
}
|
|
2084
|
+
function stepResolution(pitch, chord) {
|
|
2085
|
+
for (let delta = 1; delta <= 2; delta += 1) {
|
|
2086
|
+
if (isChordMember(pitch - delta, chord)) {
|
|
2087
|
+
return pitch - delta;
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
return void 0;
|
|
2091
|
+
}
|
|
2092
|
+
function evaluateSafety(q, opts = {}) {
|
|
2093
|
+
assertSafetyContext(q);
|
|
2094
|
+
assertMidiPitch(q.candidatePitch, "candidatePitch");
|
|
2095
|
+
return evaluateInternal(q, opts.suggestions ?? true);
|
|
2096
|
+
}
|
|
2097
|
+
function assertSafetyContext(q) {
|
|
2098
|
+
assertOneOf(q.profile, SAFETY_PROFILES, "safety profile");
|
|
2099
|
+
if (q.prevPitch !== void 0) assertMidiPitch(q.prevPitch, "prevPitch");
|
|
2100
|
+
if (q.vocalLow !== void 0) assertMidiPitch(q.vocalLow, "vocalLow");
|
|
2101
|
+
if (q.vocalHigh !== void 0) assertMidiPitch(q.vocalHigh, "vocalHigh");
|
|
2102
|
+
if (q.vocalLow !== void 0 && q.vocalHigh !== void 0 && q.vocalLow > q.vocalHigh) {
|
|
2103
|
+
throw new InvalidInputError("vocalLow must not exceed vocalHigh");
|
|
2104
|
+
}
|
|
2105
|
+
if (!Array.isArray(q.otherVoices)) {
|
|
2106
|
+
throw new InvalidInputError(`otherVoices must be an array; received ${typeof q.otherVoices}`);
|
|
2107
|
+
}
|
|
2108
|
+
assertGenerationBudget(q.otherVoices.length, "other voices");
|
|
2109
|
+
for (let index = 0; index < q.otherVoices.length; index += 1) {
|
|
2110
|
+
const voice = q.otherVoices[index];
|
|
2111
|
+
if (!voice) continue;
|
|
2112
|
+
assertMidiPitch(voice.pitch, `otherVoices[${index}].pitch`);
|
|
2113
|
+
if (voice.prevPitch !== void 0) {
|
|
2114
|
+
assertMidiPitch(voice.prevPitch, `otherVoices[${index}].prevPitch`);
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
function evaluateInternal(q, collectSuggestions) {
|
|
2119
|
+
let reasons = 0;
|
|
2120
|
+
let level = 0 /* Safe */;
|
|
2121
|
+
let resolveTo;
|
|
2122
|
+
const raise = (l) => {
|
|
2123
|
+
if (l > level) {
|
|
2124
|
+
level = l;
|
|
2125
|
+
}
|
|
2126
|
+
};
|
|
2127
|
+
const avoidSeverity = q.profile === "strict" ? 2 /* Dissonant */ : 1 /* Warning */;
|
|
2128
|
+
const parallelSeverity = avoidSeverity;
|
|
2129
|
+
const pitch = q.candidatePitch;
|
|
2130
|
+
if (q.vocalLow !== void 0 && pitch < q.vocalLow || q.vocalHigh !== void 0 && pitch > q.vocalHigh) {
|
|
2131
|
+
reasons |= 32 /* OutOfRange */;
|
|
2132
|
+
raise(1 /* Warning */);
|
|
2133
|
+
}
|
|
2134
|
+
const inScale2 = isScaleTone(pitch, q.key);
|
|
2135
|
+
const chord = q.chord;
|
|
2136
|
+
if (!inScale2 && (chord === null || !isChordMember(pitch, chord))) {
|
|
2137
|
+
reasons |= 16 /* NonScale */;
|
|
2138
|
+
raise(2 /* Dissonant */);
|
|
2139
|
+
}
|
|
2140
|
+
if (chord && isChordMember(pitch, chord)) {
|
|
2141
|
+
reasons |= 1 /* ChordTone */;
|
|
2142
|
+
} else if (chord) {
|
|
2143
|
+
const ic = intervalAboveRoot(pitch, chord);
|
|
2144
|
+
const isTension = ic === 2 || ic === 5 || ic === 9;
|
|
2145
|
+
const avoid = ic === 5 && avoidsFourth(chord) || ic === 11 && avoidsMajorSeventh(chord);
|
|
2146
|
+
if (avoid) {
|
|
2147
|
+
reasons |= 4 /* AvoidNote */;
|
|
2148
|
+
if (isTension) {
|
|
2149
|
+
reasons |= 2 /* Tension */;
|
|
2150
|
+
}
|
|
2151
|
+
reasons |= 32768 /* NeedsResolution */;
|
|
2152
|
+
resolveTo = stepResolution(pitch, chord);
|
|
2153
|
+
raise(avoidSeverity);
|
|
2154
|
+
} else if (isTension && inScale2) {
|
|
2155
|
+
reasons |= 2 /* Tension */;
|
|
2156
|
+
raise(1 /* Warning */);
|
|
2157
|
+
} else if (inScale2) {
|
|
2158
|
+
reasons |= 8 /* ScaleTone */;
|
|
2159
|
+
raise(1 /* Warning */);
|
|
2160
|
+
}
|
|
2161
|
+
} else if (inScale2) {
|
|
2162
|
+
reasons |= 8 /* ScaleTone */;
|
|
2163
|
+
raise(1 /* Warning */);
|
|
2164
|
+
}
|
|
2165
|
+
if (chord && !isChordMember(pitch, chord)) {
|
|
2166
|
+
const ic = intervalAboveRoot(pitch, chord);
|
|
2167
|
+
const toneOffsets = chord.intervals.map((i) => (i % 12 + 12) % 12);
|
|
2168
|
+
if (toneOffsets.some((t) => Math.abs(ic - t) === 6)) {
|
|
2169
|
+
reasons |= 64 /* Tritone */;
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
if (q.prevPitch !== void 0) {
|
|
2173
|
+
const pc = Math.abs(pitch - q.prevPitch) % 12;
|
|
2174
|
+
if (pc === 6) {
|
|
2175
|
+
reasons |= 65536 /* MelodicTritone */;
|
|
2176
|
+
raise(q.profile === "strict" ? 2 /* Dissonant */ : 1 /* Warning */);
|
|
2177
|
+
}
|
|
2178
|
+
if (isForbiddenMelodicLeap(q.prevPitch, pitch)) {
|
|
2179
|
+
reasons |= 128 /* LargeLeap */;
|
|
2180
|
+
raise(parallelSeverity);
|
|
2181
|
+
}
|
|
2182
|
+
if (pc === 1) {
|
|
2183
|
+
reasons |= 256 /* MinorSecond */;
|
|
2184
|
+
}
|
|
2185
|
+
if (pc === 11) {
|
|
2186
|
+
reasons |= 512 /* MajorSeventh */;
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
if (q.strongBeat) {
|
|
2190
|
+
const twoVoice = q.otherVoices.length === 1;
|
|
2191
|
+
const prev = q.prevPitch;
|
|
2192
|
+
for (const ov of q.otherVoices) {
|
|
2193
|
+
if (!createsVerticalDissonance(pitch, ov.pitch, twoVoice)) {
|
|
2194
|
+
continue;
|
|
2195
|
+
}
|
|
2196
|
+
if (chord && isStructuralChordDissonance(pitch, ov.pitch, chord)) {
|
|
2197
|
+
continue;
|
|
2198
|
+
}
|
|
2199
|
+
reasons |= 1024 /* VerticalDissonance */;
|
|
2200
|
+
raise(2 /* Dissonant */);
|
|
2201
|
+
if (prev !== void 0 && pitch === prev && ov.prevPitch !== void 0 && !createsVerticalDissonance(prev, ov.prevPitch, twoVoice)) {
|
|
2202
|
+
reasons |= 16384 /* Suspension */;
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
if (q.prevPitch !== void 0) {
|
|
2207
|
+
const prev = q.prevPitch;
|
|
2208
|
+
for (const ov of q.otherVoices) {
|
|
2209
|
+
if (ov.prevPitch === void 0) {
|
|
2210
|
+
continue;
|
|
2211
|
+
}
|
|
2212
|
+
if (createsParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {
|
|
2213
|
+
reasons |= 2048 /* ParallelPerfect */;
|
|
2214
|
+
raise(parallelSeverity);
|
|
2215
|
+
}
|
|
2216
|
+
if (createsHiddenParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {
|
|
2217
|
+
reasons |= 4096 /* HiddenParallel */;
|
|
2218
|
+
raise(parallelSeverity);
|
|
2219
|
+
}
|
|
2220
|
+
const crossedNow = pitch - ov.pitch;
|
|
2221
|
+
const crossedPrev = prev - ov.prevPitch;
|
|
2222
|
+
if (crossedNow !== 0 && crossedPrev !== 0 && crossedNow > 0 !== crossedPrev > 0) {
|
|
2223
|
+
reasons |= 8192 /* VoiceCrossing */;
|
|
2224
|
+
raise(parallelSeverity);
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
raise(minimumSafetyForReasons(reasons, q.profile));
|
|
2229
|
+
const result = { safety: level, reasons, rationale: describe(reasons, q) };
|
|
2230
|
+
if (resolveTo !== void 0) {
|
|
2231
|
+
result.resolveTo = resolveTo;
|
|
2232
|
+
}
|
|
2233
|
+
if (collectSuggestions && level !== 0 /* Safe */) {
|
|
2234
|
+
const suggestions = findSafeNearby(q, pitch);
|
|
2235
|
+
if (suggestions.length > 0) {
|
|
2236
|
+
result.suggestions = suggestions;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
return result;
|
|
2240
|
+
}
|
|
2241
|
+
function findSafeNearby(q, candidate) {
|
|
2242
|
+
const out = [];
|
|
2243
|
+
for (let d = 1; d <= SUGGESTION_WINDOW; d += 1) {
|
|
2244
|
+
for (const p of [candidate - d, candidate + d]) {
|
|
2245
|
+
if (p < 0 || p > 127) {
|
|
2246
|
+
continue;
|
|
2247
|
+
}
|
|
2248
|
+
if (evaluateInternal({ ...q, candidatePitch: p }, false).safety === 0 /* Safe */) {
|
|
2249
|
+
out.push(p);
|
|
2250
|
+
if (out.length >= MAX_SUGGESTIONS) {
|
|
2251
|
+
return out;
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
return out;
|
|
2257
|
+
}
|
|
2258
|
+
var REASON_TEXT = [
|
|
2259
|
+
[32 /* OutOfRange */, "Outside the target vocal range"],
|
|
2260
|
+
[
|
|
2261
|
+
16384 /* Suspension */,
|
|
2262
|
+
"Prepared suspension \u2014 a held consonance now dissonant, awaiting resolution"
|
|
2263
|
+
],
|
|
2264
|
+
[1024 /* VerticalDissonance */, "Dissonant against a sounding voice on a strong beat"],
|
|
2265
|
+
[64 /* Tritone */, "Tritone against the sounding harmony"],
|
|
2266
|
+
[65536 /* MelodicTritone */, "Melodic tritone leap"],
|
|
2267
|
+
[4 /* AvoidNote */, "Avoid note \u2014 clashes with a chord tone a semitone away"],
|
|
2268
|
+
[16 /* NonScale */, "Outside the key"],
|
|
2269
|
+
[2048 /* ParallelPerfect */, "Parallel perfect interval with another voice"],
|
|
2270
|
+
[4096 /* HiddenParallel */, "Hidden perfect interval by similar motion"],
|
|
2271
|
+
[8192 /* VoiceCrossing */, "Crosses another voice"],
|
|
2272
|
+
[128 /* LargeLeap */, "Leap wider than the profile allows"],
|
|
2273
|
+
[32768 /* NeedsResolution */, "Dissonance awaiting a stepwise resolution"],
|
|
2274
|
+
[2 /* Tension */, "Chord tension"],
|
|
2275
|
+
[8 /* ScaleTone */, "Scale tone, not in the chord"],
|
|
2276
|
+
[256 /* MinorSecond */, "Melodic semitone"],
|
|
2277
|
+
[512 /* MajorSeventh */, "Melodic major seventh"],
|
|
2278
|
+
[1 /* ChordTone */, "Chord tone"]
|
|
2279
|
+
];
|
|
2280
|
+
function severityOfReason(flag, profile) {
|
|
2281
|
+
const entry = REASON_SEVERITY.find(([candidate]) => candidate === flag);
|
|
2282
|
+
return entry === void 0 ? 0 /* Safe */ : entry[profile === "strict" ? 2 : 1];
|
|
2283
|
+
}
|
|
2284
|
+
function describe(reasons, q) {
|
|
2285
|
+
let best;
|
|
2286
|
+
let bestSeverity = -1;
|
|
2287
|
+
for (const [flag, text] of REASON_TEXT) {
|
|
2288
|
+
if (!(reasons & flag)) {
|
|
2289
|
+
continue;
|
|
2290
|
+
}
|
|
2291
|
+
const severity = severityOfReason(flag, q.profile);
|
|
2292
|
+
if (severity > bestSeverity) {
|
|
2293
|
+
bestSeverity = severity;
|
|
2294
|
+
best = text;
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
if (best !== void 0) {
|
|
2298
|
+
return best;
|
|
2299
|
+
}
|
|
2300
|
+
return q.chord ? "Placeable" : "No chord context";
|
|
2301
|
+
}
|
|
2302
|
+
function enumerateSafePitches(q, pitchLow, pitchHigh) {
|
|
2303
|
+
assertSafetyContext(q);
|
|
2304
|
+
const chordTones = [];
|
|
2305
|
+
const others = [];
|
|
2306
|
+
assertMidiPitch(pitchLow, "pitchLow");
|
|
2307
|
+
assertMidiPitch(pitchHigh, "pitchHigh");
|
|
2308
|
+
if (pitchLow > pitchHigh) {
|
|
2309
|
+
throw new InvalidInputError(
|
|
2310
|
+
`pitchLow must not exceed pitchHigh; received ${pitchLow} > ${pitchHigh}`
|
|
2311
|
+
);
|
|
2312
|
+
}
|
|
2313
|
+
assertGenerationBudget(pitchHigh - pitchLow + 1, "safe pitch candidates");
|
|
2314
|
+
const low = Math.max(pitchLow, q.vocalLow ?? pitchLow);
|
|
2315
|
+
const high = Math.min(pitchHigh, q.vocalHigh ?? pitchHigh);
|
|
2316
|
+
for (let pitch = high; pitch >= low; pitch -= 1) {
|
|
2317
|
+
const result = evaluateInternal({ ...q, candidatePitch: pitch }, false);
|
|
2318
|
+
if (result.safety === 2 /* Dissonant */) {
|
|
2319
|
+
continue;
|
|
2320
|
+
}
|
|
2321
|
+
if (result.reasons & 1 /* ChordTone */) {
|
|
2322
|
+
chordTones.push(pitch);
|
|
2323
|
+
} else {
|
|
2324
|
+
others.push(pitch);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
return [...chordTones, ...others];
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
// src/theory/symbol/index.ts
|
|
2331
|
+
var ROOT_RE = /^[A-Ga-g](?:#{1,2}|b{1,2}|x)?/;
|
|
2332
|
+
var BASS_RE = /^[A-Ga-g](?:#{1,2}|b{1,2}|x)?$/;
|
|
2333
|
+
var DEFAULT_SYSTEM = "english";
|
|
2334
|
+
var SYSTEM_PROBE = { letter: 0, alter: 0 };
|
|
2335
|
+
var QUALITY_MAP = {
|
|
2336
|
+
"": "maj",
|
|
2337
|
+
maj: "maj",
|
|
2338
|
+
m: "min",
|
|
2339
|
+
min: "min",
|
|
2340
|
+
"-": "min",
|
|
2341
|
+
dim: "dim",
|
|
2342
|
+
"\xB0": "dim",
|
|
2343
|
+
aug: "aug",
|
|
2344
|
+
"+": "aug",
|
|
2345
|
+
maj7: "maj7",
|
|
2346
|
+
M7: "maj7",
|
|
2347
|
+
\u0394: "maj7",
|
|
2348
|
+
\u03947: "maj7",
|
|
2349
|
+
m7: "min7",
|
|
2350
|
+
min7: "min7",
|
|
2351
|
+
"-7": "min7",
|
|
2352
|
+
"7": "dom7",
|
|
2353
|
+
dim7: "dim7",
|
|
2354
|
+
"\xB07": "dim7",
|
|
2355
|
+
m7b5: "m7b5",
|
|
2356
|
+
\u00F8: "m7b5",
|
|
2357
|
+
\u00F87: "m7b5",
|
|
2358
|
+
min7b5: "m7b5",
|
|
2359
|
+
mMaj7: "minMaj7",
|
|
2360
|
+
mM7: "minMaj7",
|
|
2361
|
+
minMaj7: "minMaj7",
|
|
2362
|
+
mMaj9: "minMaj9",
|
|
2363
|
+
mM9: "minMaj9",
|
|
2364
|
+
minMaj9: "minMaj9",
|
|
2365
|
+
mMaj11: "minMaj11",
|
|
2366
|
+
mM11: "minMaj11",
|
|
2367
|
+
minMaj11: "minMaj11",
|
|
2368
|
+
mMaj13: "minMaj13",
|
|
2369
|
+
mM13: "minMaj13",
|
|
2370
|
+
minMaj13: "minMaj13",
|
|
2371
|
+
aug7: "aug7",
|
|
2372
|
+
"+7": "aug7",
|
|
2373
|
+
"7#5": "aug7",
|
|
2374
|
+
augMaj7: "augMaj7",
|
|
2375
|
+
augM7: "augMaj7",
|
|
2376
|
+
"+maj7": "augMaj7",
|
|
2377
|
+
"+M7": "augMaj7",
|
|
2378
|
+
majb5: "majb5",
|
|
2379
|
+
"maj(b5)": "majb5",
|
|
2380
|
+
"(b5)": "majb5",
|
|
2381
|
+
"6": "6",
|
|
2382
|
+
m6: "min6",
|
|
2383
|
+
min6: "min6",
|
|
2384
|
+
"6/9": "6/9",
|
|
2385
|
+
"69": "6/9",
|
|
2386
|
+
sus2: "sus2",
|
|
2387
|
+
sus: "sus4",
|
|
2388
|
+
sus4: "sus4",
|
|
2389
|
+
add9: "add9",
|
|
2390
|
+
add11: "add11",
|
|
2391
|
+
maj9: "maj9",
|
|
2392
|
+
M9: "maj9",
|
|
2393
|
+
m9: "min9",
|
|
2394
|
+
min9: "min9",
|
|
2395
|
+
"9": "dom9",
|
|
2396
|
+
"7b9": "7b9",
|
|
2397
|
+
"7#9": "7#9",
|
|
2398
|
+
"7#11": "7#11",
|
|
2399
|
+
"7b13": "7b13",
|
|
2400
|
+
"11": "11",
|
|
2401
|
+
"13": "13",
|
|
2402
|
+
"5": "5",
|
|
2403
|
+
"7sus4": "7sus4",
|
|
2404
|
+
"7sus": "7sus4",
|
|
2405
|
+
"9sus4": "11",
|
|
2406
|
+
"9sus": "11",
|
|
2407
|
+
"7b5": "7b5",
|
|
2408
|
+
"7alt": "7alt",
|
|
2409
|
+
alt: "7alt",
|
|
2410
|
+
"13b9": "13b9",
|
|
2411
|
+
maj13: "maj13",
|
|
2412
|
+
M13: "maj13",
|
|
2413
|
+
"maj7#11": "maj7#11",
|
|
2414
|
+
"M7#11": "maj7#11",
|
|
2415
|
+
m11: "min11",
|
|
2416
|
+
min11: "min11",
|
|
2417
|
+
m13: "min13",
|
|
2418
|
+
min13: "min13",
|
|
2419
|
+
madd9: "minAdd9",
|
|
2420
|
+
"min(add9)": "minAdd9",
|
|
2421
|
+
"m(add9)": "minAdd9",
|
|
2422
|
+
"6add9": "6/9",
|
|
2423
|
+
"m6/9": "min6/9",
|
|
2424
|
+
m69: "min6/9",
|
|
2425
|
+
"min6/9": "min6/9",
|
|
2426
|
+
// ASCII stand-ins for the degree and half-diminished glyphs, which most
|
|
2427
|
+
// lead sheets and chord-chart text files use instead of the typographic ones.
|
|
2428
|
+
o: "dim",
|
|
2429
|
+
o7: "dim7",
|
|
2430
|
+
h: "m7b5",
|
|
2431
|
+
h7: "m7b5"
|
|
2432
|
+
};
|
|
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
|
+
function knownQuality(suffix) {
|
|
2479
|
+
return Object.hasOwn(QUALITY_MAP, suffix) ? QUALITY_MAP[suffix] : void 0;
|
|
2480
|
+
}
|
|
2481
|
+
var BASE_TOKENS = [
|
|
2482
|
+
["sus2", "sus2"],
|
|
2483
|
+
["sus4", "sus4"],
|
|
2484
|
+
["sus", "sus4"],
|
|
2485
|
+
["min", "min"],
|
|
2486
|
+
["dim", "dim"],
|
|
2487
|
+
["aug", "aug"],
|
|
2488
|
+
["maj", "major"],
|
|
2489
|
+
["m", "min"],
|
|
2490
|
+
["-", "min"],
|
|
2491
|
+
["o", "dim"],
|
|
2492
|
+
["\xB0", "dim"],
|
|
2493
|
+
["+", "aug"],
|
|
2494
|
+
["M", "major"],
|
|
2495
|
+
["\u0394", "major"],
|
|
2496
|
+
["^", "major"]
|
|
2497
|
+
];
|
|
2498
|
+
var MAJOR_MARKERS = [
|
|
2499
|
+
["maj", "word"],
|
|
2500
|
+
["Maj", "word"],
|
|
2501
|
+
["M", "word"],
|
|
2502
|
+
["\u0394", "glyph"],
|
|
2503
|
+
["^", "glyph"]
|
|
2504
|
+
];
|
|
2505
|
+
var EXTENSION_NUMBERS = [13, 11, 9, 7, 6];
|
|
2506
|
+
var ADDED_DEGREES = {
|
|
2507
|
+
"2": 9,
|
|
2508
|
+
"4": 11,
|
|
2509
|
+
"6": 6,
|
|
2510
|
+
"9": 9,
|
|
2511
|
+
"11": 11,
|
|
2512
|
+
"13": 13
|
|
2513
|
+
};
|
|
2514
|
+
var ALTERABLE = { "5": 5, "9": 9, "11": 11, "13": 13 };
|
|
2515
|
+
var OMITTABLE = { "1": 1, "3": 3, "5": 5 };
|
|
2516
|
+
function matchToken(text, at, table) {
|
|
2517
|
+
let found;
|
|
2518
|
+
for (const [token, value] of table) {
|
|
2519
|
+
if (text.startsWith(token, at) && (found === void 0 || at + token.length > found.next)) {
|
|
2520
|
+
found = { value, next: at + token.length };
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
return found;
|
|
2524
|
+
}
|
|
2525
|
+
function matchNumber(text, at) {
|
|
2526
|
+
for (const value of EXTENSION_NUMBERS) {
|
|
2527
|
+
const token = String(value);
|
|
2528
|
+
if (text.startsWith(token, at)) {
|
|
2529
|
+
return { value, next: at + token.length };
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
return void 0;
|
|
2533
|
+
}
|
|
2534
|
+
function stackTo(draft, top) {
|
|
2535
|
+
if (top === 6) {
|
|
2536
|
+
draft.additions.add(6);
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
draft.seventh = draft.major ? "maj7" : draft.base === "dim" ? "dim7" : "min7";
|
|
2540
|
+
if (top >= 9) {
|
|
2541
|
+
draft.alterations.set(9, 0);
|
|
2542
|
+
}
|
|
2543
|
+
if (top === 11) {
|
|
2544
|
+
draft.alterations.set(11, 0);
|
|
2545
|
+
}
|
|
2546
|
+
if (top === 13) {
|
|
2547
|
+
draft.alterations.set(13, 0);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
function applyFigure(draft, text, at) {
|
|
2551
|
+
const base = matchToken(text, at, [
|
|
2552
|
+
["sus2", "sus2"],
|
|
2553
|
+
["sus4", "sus4"],
|
|
2554
|
+
["sus", "sus4"]
|
|
2555
|
+
]);
|
|
2556
|
+
if (base !== void 0) {
|
|
2557
|
+
draft.base = base.value;
|
|
2558
|
+
return base.next;
|
|
2559
|
+
}
|
|
2560
|
+
if (text.startsWith("alt", at)) {
|
|
2561
|
+
draft.base = "aug";
|
|
2562
|
+
draft.seventh = "min7";
|
|
2563
|
+
draft.alterations.set(9, 1);
|
|
2564
|
+
return at + 3;
|
|
2565
|
+
}
|
|
2566
|
+
if (text.startsWith("add", at)) {
|
|
2567
|
+
const degree2 = matchNumberToken(text, at + 3, ADDED_DEGREES);
|
|
2568
|
+
if (degree2 === void 0) {
|
|
2569
|
+
return void 0;
|
|
2570
|
+
}
|
|
2571
|
+
draft.additions.add(degree2.value);
|
|
2572
|
+
return degree2.next;
|
|
2573
|
+
}
|
|
2574
|
+
const omitted = matchToken(text, at, [
|
|
2575
|
+
["omit", 4],
|
|
2576
|
+
["no", 2]
|
|
2577
|
+
]);
|
|
2578
|
+
if (omitted !== void 0) {
|
|
2579
|
+
const degree2 = matchNumberToken(text, omitted.next, OMITTABLE);
|
|
2580
|
+
if (degree2 === void 0) {
|
|
2581
|
+
return void 0;
|
|
2582
|
+
}
|
|
2583
|
+
draft.omissions.add(degree2.value);
|
|
2584
|
+
return degree2.next;
|
|
2585
|
+
}
|
|
2586
|
+
const accidental = matchToken(text, at, [
|
|
2587
|
+
["b", -1],
|
|
2588
|
+
["#", 1]
|
|
2589
|
+
]);
|
|
2590
|
+
const from = accidental?.next ?? at;
|
|
2591
|
+
const degree = matchNumberToken(text, from, ALTERABLE);
|
|
2592
|
+
if (degree === void 0) {
|
|
2593
|
+
return void 0;
|
|
2594
|
+
}
|
|
2595
|
+
draft.alterations.set(degree.value, accidental?.value ?? 0);
|
|
2596
|
+
return degree.next;
|
|
2597
|
+
}
|
|
2598
|
+
function matchNumberToken(text, at, table) {
|
|
2599
|
+
const entries = Object.entries(table).map(([token, value]) => [token, value]);
|
|
2600
|
+
return matchToken(text, at, entries);
|
|
2601
|
+
}
|
|
2602
|
+
function structuralSpec(text) {
|
|
2603
|
+
let core = "";
|
|
2604
|
+
const groups = [];
|
|
2605
|
+
let inside = false;
|
|
2606
|
+
let buffer = "";
|
|
2607
|
+
for (const char of text) {
|
|
2608
|
+
if (char === "(") {
|
|
2609
|
+
if (inside) {
|
|
2610
|
+
return void 0;
|
|
2611
|
+
}
|
|
2612
|
+
inside = true;
|
|
2613
|
+
buffer = "";
|
|
2614
|
+
} else if (char === ")") {
|
|
2615
|
+
if (!inside) {
|
|
2616
|
+
return void 0;
|
|
2617
|
+
}
|
|
2618
|
+
inside = false;
|
|
2619
|
+
groups.push(buffer);
|
|
2620
|
+
} else if (inside) {
|
|
2621
|
+
buffer += char;
|
|
2622
|
+
} else {
|
|
2623
|
+
core += char;
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
if (inside) {
|
|
2627
|
+
return void 0;
|
|
2628
|
+
}
|
|
2629
|
+
const draft = {
|
|
2630
|
+
base: "maj",
|
|
2631
|
+
alterations: /* @__PURE__ */ new Map(),
|
|
2632
|
+
additions: /* @__PURE__ */ new Set(),
|
|
2633
|
+
omissions: /* @__PURE__ */ new Set(),
|
|
2634
|
+
major: false
|
|
2635
|
+
};
|
|
2636
|
+
let at = 0;
|
|
2637
|
+
const opening = matchToken(core, at, BASE_TOKENS);
|
|
2638
|
+
let markerGlyph = false;
|
|
2639
|
+
let markerAfterBase = false;
|
|
2640
|
+
if (opening !== void 0) {
|
|
2641
|
+
at = opening.next;
|
|
2642
|
+
if (opening.value === "major") {
|
|
2643
|
+
draft.major = true;
|
|
2644
|
+
markerGlyph = core.startsWith("\u0394") || core.startsWith("^");
|
|
2645
|
+
} else {
|
|
2646
|
+
draft.base = opening.value;
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
if (!draft.major) {
|
|
2650
|
+
const marker = matchToken(core, at, MAJOR_MARKERS);
|
|
2651
|
+
if (marker !== void 0) {
|
|
2652
|
+
draft.major = true;
|
|
2653
|
+
markerGlyph = marker.value === "glyph";
|
|
2654
|
+
markerAfterBase = opening !== void 0;
|
|
2655
|
+
at = marker.next;
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
const number = matchNumber(core, at);
|
|
2659
|
+
if (number !== void 0) {
|
|
2660
|
+
at = number.next;
|
|
2661
|
+
stackTo(draft, number.value);
|
|
2662
|
+
} else if (draft.major && (markerGlyph || markerAfterBase)) {
|
|
2663
|
+
draft.seventh = "maj7";
|
|
2664
|
+
}
|
|
2665
|
+
while (at < core.length) {
|
|
2666
|
+
if (core[at] === "/" && at > 0 && at + 1 < core.length) {
|
|
2667
|
+
at += 1;
|
|
2668
|
+
continue;
|
|
2669
|
+
}
|
|
2670
|
+
const next = applyFigure(draft, core, at);
|
|
2671
|
+
if (next === void 0 || next === at) {
|
|
2672
|
+
return void 0;
|
|
2673
|
+
}
|
|
2674
|
+
at = next;
|
|
2675
|
+
}
|
|
2676
|
+
for (const group of groups) {
|
|
2677
|
+
for (const figure of group.split(/[,/\s]+/)) {
|
|
2678
|
+
if (figure.length === 0) {
|
|
2679
|
+
continue;
|
|
2680
|
+
}
|
|
2681
|
+
const next = applyFigure(draft, figure, 0);
|
|
2682
|
+
if (next !== figure.length) {
|
|
2683
|
+
return void 0;
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
return {
|
|
2688
|
+
rootPc: 0,
|
|
2689
|
+
base: draft.base,
|
|
2690
|
+
...draft.seventh === void 0 ? {} : { seventh: draft.seventh },
|
|
2691
|
+
alterations: [...draft.alterations].map(([degree, alter]) => ({ degree, alter })),
|
|
2692
|
+
additions: [...draft.additions],
|
|
2693
|
+
omissions: [...draft.omissions]
|
|
2694
|
+
};
|
|
2695
|
+
}
|
|
2696
|
+
function suffixSpec(suffix) {
|
|
2697
|
+
const quality = knownQuality(suffix);
|
|
2698
|
+
return quality === void 0 ? structuralSpec(suffix) : chordSpecForQuality(quality, 0);
|
|
2699
|
+
}
|
|
2700
|
+
function assertNamingSystem(system) {
|
|
2701
|
+
formatNote(SYSTEM_PROBE, { system });
|
|
2702
|
+
}
|
|
2703
|
+
function bareNote(text, system) {
|
|
2704
|
+
const read = tryParseNote(text, { system });
|
|
2705
|
+
if (!read.ok) {
|
|
2706
|
+
return void 0;
|
|
2707
|
+
}
|
|
2708
|
+
return read.value.octave === void 0 ? read.value : void 0;
|
|
2709
|
+
}
|
|
2710
|
+
function rootSplits(text, system) {
|
|
2711
|
+
if (system === "english") {
|
|
2712
|
+
const match = text.match(ROOT_RE);
|
|
2713
|
+
const token = match?.[0];
|
|
2714
|
+
return token === void 0 ? [] : [{ root: parseNote(token, { system }), rest: text.slice(token.length) }];
|
|
2715
|
+
}
|
|
2716
|
+
assertNamingSystem(system);
|
|
2717
|
+
const splits = [];
|
|
2718
|
+
for (let end = text.length; end > 0; end -= 1) {
|
|
2719
|
+
const root = bareNote(text.slice(0, end), system);
|
|
2720
|
+
if (root !== void 0) {
|
|
2721
|
+
splits.push({ root, rest: text.slice(end) });
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
return splits;
|
|
2725
|
+
}
|
|
2726
|
+
function bassNote(text, system) {
|
|
2727
|
+
if (system === "english") {
|
|
2728
|
+
return BASS_RE.test(text) ? parseNote(text, { system }) : void 0;
|
|
2729
|
+
}
|
|
2730
|
+
return bareNote(text, system);
|
|
2731
|
+
}
|
|
2732
|
+
function makeSpelledChord(root, spec, bass) {
|
|
2733
|
+
const rooted = { ...spec, rootPc: noteToPitchClass(root) };
|
|
2734
|
+
if (bass !== void 0) {
|
|
2735
|
+
rooted.bassPc = noteToPitchClass(bass);
|
|
2736
|
+
}
|
|
2737
|
+
const chord = chordFromSpec(rooted);
|
|
2738
|
+
chord.rootSpelling = { letter: root.letter, alter: root.alter };
|
|
2739
|
+
if (bass !== void 0) {
|
|
2740
|
+
chord.bassSpelling = { letter: bass.letter, alter: bass.alter };
|
|
2741
|
+
}
|
|
2742
|
+
return chord;
|
|
2743
|
+
}
|
|
2744
|
+
function chordAfterRoot(root, rest, system) {
|
|
2745
|
+
const slashIndex = rest.lastIndexOf("/");
|
|
2746
|
+
if (slashIndex !== -1) {
|
|
2747
|
+
const slashSpec = suffixSpec(rest.slice(0, slashIndex));
|
|
2748
|
+
const bass = slashSpec === void 0 ? void 0 : bassNote(rest.slice(slashIndex + 1), system);
|
|
2749
|
+
if (slashSpec !== void 0 && bass !== void 0) {
|
|
2750
|
+
return makeSpelledChord(root, slashSpec, bass);
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
const spec = suffixSpec(rest);
|
|
2754
|
+
return spec === void 0 ? void 0 : makeSpelledChord(root, spec);
|
|
2755
|
+
}
|
|
2756
|
+
function parseChordSymbol(text, opts) {
|
|
2757
|
+
return unwrapParse(tryParseChordSymbol(text, opts));
|
|
2758
|
+
}
|
|
2759
|
+
function tryParseChordSymbol(text, opts) {
|
|
2760
|
+
try {
|
|
2761
|
+
if (typeof text !== "string") {
|
|
2762
|
+
throw new InvalidInputError(`chord symbol must be a string; received ${typeof text}`);
|
|
2763
|
+
}
|
|
2764
|
+
const system = opts?.system ?? DEFAULT_SYSTEM;
|
|
2765
|
+
const trimmed = text.trim().replaceAll("\u266F", "#").replaceAll("\u266D", "b").replaceAll("\u{1D12A}", "x").replaceAll("\u{1D12B}", "bb").replaceAll("\u2206", "\u0394");
|
|
2766
|
+
const splits = rootSplits(trimmed, system);
|
|
2767
|
+
if (splits.length === 0) {
|
|
2768
|
+
throw new InvalidInputError(`Invalid chord symbol: ${text}`);
|
|
2769
|
+
}
|
|
2770
|
+
for (const { root, rest } of splits) {
|
|
2771
|
+
const chord = chordAfterRoot(root, rest, system);
|
|
2772
|
+
if (chord !== void 0) {
|
|
2773
|
+
return { ok: true, value: chord };
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
throw new InvalidInputError(`Unrecognized chord quality: ${text}`);
|
|
2777
|
+
} catch (error) {
|
|
2778
|
+
return parseFailure(error);
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
function normalizedChord(data) {
|
|
2782
|
+
if (!Array.isArray(data.intervals)) {
|
|
2783
|
+
throw new InvalidInputError("chord.intervals must be an array of semitone offsets");
|
|
2784
|
+
}
|
|
2785
|
+
const copy = {
|
|
2786
|
+
rootPc: pitchClassOf(assertFiniteNumber(data.rootPc, "chord.rootPc")),
|
|
2787
|
+
quality: data.quality,
|
|
2788
|
+
// Each offset is checked rather than copied blind: a chord holding a NaN
|
|
2789
|
+
// interval voices, spells and formats as a chord that looks real, and the
|
|
2790
|
+
// failure surfaces wherever the number is finally used.
|
|
2791
|
+
intervals: data.intervals.map(
|
|
2792
|
+
(interval, index) => assertFiniteNumber(interval, `chord.intervals[${index}]`)
|
|
2793
|
+
)
|
|
2794
|
+
};
|
|
2795
|
+
if (data.bassPc !== void 0) {
|
|
2796
|
+
copy.bassPc = pitchClassOf(assertFiniteNumber(data.bassPc, "chord.bassPc"));
|
|
2797
|
+
}
|
|
2798
|
+
if (data.rootSpelling !== void 0) {
|
|
2799
|
+
copy.rootSpelling = data.rootSpelling;
|
|
2800
|
+
}
|
|
2801
|
+
if (data.bassSpelling !== void 0) {
|
|
2802
|
+
copy.bassSpelling = data.bassSpelling;
|
|
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
|
+
}
|
|
2832
|
+
var CORE_CANDIDATES = chordQualities().map((quality) => [quality, chordSpecForQuality(quality, 0)]).filter(([, spec]) => spec.alterations.every(({ degree, alter }) => degree === 5 || alter === 0));
|
|
2833
|
+
function carriesAlteration(spec, part) {
|
|
2834
|
+
return spec.alterations.some(
|
|
2835
|
+
({ degree, alter }) => degree === part.degree && alter === part.alter
|
|
2836
|
+
);
|
|
2837
|
+
}
|
|
2838
|
+
function coreQuality(spec) {
|
|
2839
|
+
let core;
|
|
2840
|
+
let named = -1;
|
|
2841
|
+
for (const [quality, candidate] of CORE_CANDIDATES) {
|
|
2842
|
+
const size = candidate.alterations.length + candidate.additions.length;
|
|
2843
|
+
if (size <= named || candidate.base !== spec.base || candidate.seventh !== spec.seventh || !candidate.alterations.every((part) => carriesAlteration(spec, part)) || !candidate.additions.every((degree) => spec.additions.includes(degree)) || !candidate.omissions.every((degree) => spec.omissions.includes(degree))) {
|
|
2844
|
+
continue;
|
|
2845
|
+
}
|
|
2846
|
+
core = quality;
|
|
2847
|
+
named = size;
|
|
2848
|
+
}
|
|
2849
|
+
return core;
|
|
2850
|
+
}
|
|
2851
|
+
var BASE_SUFFIX = {
|
|
2852
|
+
maj: "",
|
|
2853
|
+
min: "m",
|
|
2854
|
+
dim: "dim",
|
|
2855
|
+
aug: "aug",
|
|
2856
|
+
sus2: "sus2",
|
|
2857
|
+
sus4: "sus4",
|
|
2858
|
+
power: "5"
|
|
2859
|
+
};
|
|
2860
|
+
var SEVENTH_SUFFIX = { maj7: "maj7", min7: "7", dim7: "dim7" };
|
|
2861
|
+
var MAJOR_SEVENTH_AFTER_BASE = "Maj7";
|
|
2862
|
+
function builtCoreSuffix(spec) {
|
|
2863
|
+
const base = BASE_SUFFIX[spec.base];
|
|
2864
|
+
if (spec.seventh === void 0) {
|
|
2865
|
+
return base;
|
|
2866
|
+
}
|
|
2867
|
+
const seventh = SEVENTH_SUFFIX[spec.seventh];
|
|
2868
|
+
if (spec.base === "sus2" || spec.base === "sus4") {
|
|
2869
|
+
return `${seventh}${base}`;
|
|
2870
|
+
}
|
|
2871
|
+
if (spec.base === "maj") {
|
|
2872
|
+
return seventh;
|
|
2873
|
+
}
|
|
2874
|
+
return `${base}${spec.seventh === "maj7" ? MAJOR_SEVENTH_AFTER_BASE : seventh}`;
|
|
2875
|
+
}
|
|
2876
|
+
function figureText(spec, core) {
|
|
2877
|
+
const figures = [];
|
|
2878
|
+
for (const alteration of spec.alterations) {
|
|
2879
|
+
if (core !== void 0 && carriesAlteration(core, alteration)) {
|
|
2880
|
+
continue;
|
|
2881
|
+
}
|
|
2882
|
+
const { degree, alter } = alteration;
|
|
2883
|
+
figures.push({ degree, text: `${alter < 0 ? "b" : alter > 0 ? "#" : ""}${degree}` });
|
|
2884
|
+
}
|
|
2885
|
+
for (const degree of spec.additions) {
|
|
2886
|
+
if (core?.additions.includes(degree) !== true) {
|
|
2887
|
+
figures.push({ degree, text: `add${degree}` });
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
return figures.sort((a, b) => a.degree - b.degree).map(({ text }) => text);
|
|
2891
|
+
}
|
|
2892
|
+
function specSuffix(spec) {
|
|
2893
|
+
const named = exactChordSpecQuality(spec);
|
|
2894
|
+
if (named !== void 0) {
|
|
2895
|
+
return CANONICAL_SUFFIX[named];
|
|
2896
|
+
}
|
|
2897
|
+
const core = coreQuality(spec);
|
|
2898
|
+
const coreSpec = core === void 0 ? void 0 : chordSpecForQuality(core, 0);
|
|
2899
|
+
const text = core === void 0 ? builtCoreSuffix(spec) : CANONICAL_SUFFIX[core];
|
|
2900
|
+
const figures = figureText(spec, coreSpec);
|
|
2901
|
+
return figures.length === 0 ? text : `${text}(${figures.join(",")})`;
|
|
2902
|
+
}
|
|
2903
|
+
function formatChordSymbol(chord, opts) {
|
|
2904
|
+
const data = toChordData(chord);
|
|
2905
|
+
if (!Object.hasOwn(CANONICAL_SUFFIX, data.quality)) {
|
|
2906
|
+
throw new InvalidInputError(`Unknown chord quality: ${String(data.quality)}`);
|
|
2907
|
+
}
|
|
2908
|
+
const system = opts?.system ?? DEFAULT_SYSTEM;
|
|
2909
|
+
const rootPc = pitchClassOf(data.rootPc);
|
|
2910
|
+
const rootHint = data.rootSpelling;
|
|
2911
|
+
const rootName = pitchClassName(rootPc, rootHint, system, opts?.flats);
|
|
2912
|
+
const suffix = specSuffix(chordSpecOf(data));
|
|
2913
|
+
let symbol = `${rootName}${suffix}`;
|
|
2914
|
+
if (data.bassPc !== void 0 && pitchClassOf(data.bassPc) !== rootPc) {
|
|
2915
|
+
const inheritFlats = rootHint !== void 0 && noteToPitchClass(rootHint) === rootPc ? rootHint.alter < 0 : void 0;
|
|
2916
|
+
const bassPc = pitchClassOf(data.bassPc);
|
|
2917
|
+
symbol += `/${pitchClassName(bassPc, data.bassSpelling, system, opts?.flats, inheritFlats)}`;
|
|
2918
|
+
}
|
|
2919
|
+
return symbol;
|
|
2920
|
+
}
|
|
2921
|
+
function transposeChordSymbol(text, semitones, opts) {
|
|
2922
|
+
assertFiniteSemitones(semitones);
|
|
2923
|
+
const chord = parseChordSymbol(text, opts);
|
|
2924
|
+
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
|
+
if (chord.bassPc !== void 0) {
|
|
2930
|
+
transposed.bassPc = pitchClassOf(chord.bassPc + semitones);
|
|
2931
|
+
}
|
|
2932
|
+
if (chord.bassSpelling !== void 0) {
|
|
2933
|
+
const hint = transposeNote(chord.bassSpelling, semitones);
|
|
2934
|
+
transposed.bassSpelling = { letter: hint.letter, alter: hint.alter };
|
|
2935
|
+
}
|
|
2936
|
+
return formatChordSymbol(transposed, opts);
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
export {
|
|
2940
|
+
maskFromOffsets,
|
|
2941
|
+
MAJOR_MASK,
|
|
2942
|
+
NATURAL_MINOR_MASK,
|
|
2943
|
+
HARMONIC_MINOR_MASK,
|
|
2944
|
+
MELODIC_MINOR_MASK,
|
|
2945
|
+
DORIAN_MASK,
|
|
2946
|
+
PHRYGIAN_MASK,
|
|
2947
|
+
LYDIAN_MASK,
|
|
2948
|
+
MIXOLYDIAN_MASK,
|
|
2949
|
+
LOCRIAN_MASK,
|
|
2950
|
+
MAJOR_PENTATONIC_MASK,
|
|
2951
|
+
MINOR_PENTATONIC_MASK,
|
|
2952
|
+
BLUES_MASK,
|
|
2953
|
+
WHOLE_TONE_MASK,
|
|
2954
|
+
OCTATONIC_HALF_WHOLE_MASK,
|
|
2955
|
+
OCTATONIC_WHOLE_HALF_MASK,
|
|
2956
|
+
CHROMATIC_MASK,
|
|
2957
|
+
LYDIAN_DOMINANT_MASK,
|
|
2958
|
+
MIXOLYDIAN_B13_MASK,
|
|
2959
|
+
LOCRIAN_NATURAL2_MASK,
|
|
2960
|
+
ALTERED_MASK,
|
|
2961
|
+
PHRYGIAN_DOMINANT_MASK,
|
|
2962
|
+
MIYAKO_BUSHI_MASK,
|
|
2963
|
+
RITSU_MASK,
|
|
2964
|
+
RYUKYU_MASK,
|
|
2965
|
+
DOUBLE_HARMONIC_MASK,
|
|
2966
|
+
TODI_MASK,
|
|
2967
|
+
MARWA_MASK,
|
|
2968
|
+
PURVI_MASK,
|
|
2969
|
+
NAMED_SCALES,
|
|
2970
|
+
WORLD_SCALES,
|
|
2971
|
+
SCALE_ALIASES,
|
|
2972
|
+
resolveScaleName,
|
|
2973
|
+
namedScaleMask,
|
|
2974
|
+
requireScaleMask,
|
|
2975
|
+
majorKey,
|
|
2976
|
+
minorKey,
|
|
2977
|
+
scaleByName,
|
|
2978
|
+
toKeyScale,
|
|
2979
|
+
isScaleTone,
|
|
2980
|
+
nearestScaleTone,
|
|
2981
|
+
scaleLadderPosition,
|
|
2982
|
+
scaleLadderPitch,
|
|
2983
|
+
shiftByScaleDegrees,
|
|
2984
|
+
pitchToScaleDegree,
|
|
2985
|
+
diatonicPitchClasses,
|
|
2986
|
+
scaleTonesInDegreeOrder,
|
|
2987
|
+
assertTonicOf,
|
|
2988
|
+
spellPitchClass,
|
|
2989
|
+
spellScale,
|
|
2990
|
+
spellPitchClasses,
|
|
2991
|
+
spellChordFromRoot,
|
|
2992
|
+
spellChord,
|
|
2993
|
+
spellPitch,
|
|
2994
|
+
noteNames,
|
|
2995
|
+
isSignatureKey,
|
|
2996
|
+
keySignatureFifths,
|
|
2997
|
+
keyFromFifths,
|
|
2998
|
+
spelledKeyOf,
|
|
2999
|
+
relativeKeyOf,
|
|
3000
|
+
parallelKeyOf,
|
|
3001
|
+
dominantKeyOf,
|
|
3002
|
+
subdominantKeyOf,
|
|
3003
|
+
enharmonicKeyOf,
|
|
3004
|
+
relatedKeysOf,
|
|
3005
|
+
keyRelationBetween,
|
|
3006
|
+
SCALE_SYSTEMS,
|
|
3007
|
+
scaleSystemOf,
|
|
3008
|
+
supportsFunctionalHarmony,
|
|
3009
|
+
chordSpecIntervals,
|
|
3010
|
+
chordSpecQuality,
|
|
3011
|
+
chordQualities,
|
|
3012
|
+
chordFromDegree,
|
|
3013
|
+
makeChord,
|
|
3014
|
+
chordFromSpec,
|
|
3015
|
+
chordSpecOf,
|
|
3016
|
+
chordFromSpan,
|
|
3017
|
+
spanFromChord,
|
|
3018
|
+
transposeChord,
|
|
3019
|
+
chordToneSpellings,
|
|
3020
|
+
chordPitchClasses,
|
|
3021
|
+
intervalAboveRoot,
|
|
3022
|
+
isChordMember,
|
|
3023
|
+
chordToneRole,
|
|
3024
|
+
diatonicTriad,
|
|
3025
|
+
diatonicSeventh,
|
|
3026
|
+
simpleIntervalNumber,
|
|
3027
|
+
classifySpelledInterval,
|
|
3028
|
+
voiceIndependence,
|
|
3029
|
+
createsVoiceCrossing,
|
|
3030
|
+
createsVerticalDissonance,
|
|
3031
|
+
isForbiddenMelodicLeap,
|
|
3032
|
+
isAugmentedMelodicInterval,
|
|
3033
|
+
createsParallelPerfect,
|
|
3034
|
+
createsParallelOctave,
|
|
3035
|
+
createsParallelUnison,
|
|
3036
|
+
createsHiddenParallelPerfect,
|
|
3037
|
+
createsBattuta,
|
|
3038
|
+
createsVoiceOverlap,
|
|
3039
|
+
exceedsSpacing,
|
|
3040
|
+
isLeadingToneResolution,
|
|
3041
|
+
PROFILE_WEIGHTS,
|
|
3042
|
+
profileWeights,
|
|
3043
|
+
NoteSafety,
|
|
3044
|
+
ReasonFlag,
|
|
3045
|
+
evaluateSafety,
|
|
3046
|
+
enumerateSafePitches,
|
|
3047
|
+
parseChordSymbol,
|
|
3048
|
+
tryParseChordSymbol,
|
|
3049
|
+
toChordData,
|
|
3050
|
+
formatChordSymbol,
|
|
3051
|
+
transposeChordSymbol
|
|
3052
|
+
};
|
|
3053
|
+
//# sourceMappingURL=chunk-INHVXP6Z.js.map
|