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