@libraz/libcantus 0.9.0 → 0.9.2
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 +25 -2
- package/dist/analyze/index.cjs +1697 -0
- package/dist/analyze/index.cjs.map +1 -0
- package/dist/analyze/index.d.cts +429 -0
- package/dist/analyze/index.d.ts +429 -0
- package/dist/analyze/index.js +51 -0
- package/dist/analyze/index.js.map +1 -0
- package/dist/chunk-5HTNMQW4.js +653 -0
- package/dist/chunk-5HTNMQW4.js.map +1 -0
- package/dist/chunk-7Y7WF64D.js +410 -0
- package/dist/chunk-7Y7WF64D.js.map +1 -0
- package/dist/chunk-A4532ZFR.js +259 -0
- package/dist/chunk-A4532ZFR.js.map +1 -0
- package/dist/chunk-BHUT2S5G.js +22 -0
- package/dist/chunk-BHUT2S5G.js.map +1 -0
- package/dist/chunk-CCBLVAPK.js +42 -0
- package/dist/chunk-CCBLVAPK.js.map +1 -0
- package/dist/chunk-COKTX2G5.js +907 -0
- package/dist/chunk-COKTX2G5.js.map +1 -0
- package/dist/chunk-DYWFNC4A.js +152 -0
- package/dist/chunk-DYWFNC4A.js.map +1 -0
- package/dist/chunk-GQKN5KDT.js +159 -0
- package/dist/chunk-GQKN5KDT.js.map +1 -0
- package/dist/chunk-LN7ZZMD2.js +530 -0
- package/dist/chunk-LN7ZZMD2.js.map +1 -0
- package/dist/chunk-MSWFZF7J.js +118 -0
- package/dist/chunk-MSWFZF7J.js.map +1 -0
- package/dist/chunk-R3S2LUBH.js +2722 -0
- package/dist/chunk-R3S2LUBH.js.map +1 -0
- package/dist/chunk-SDM3VJJ5.js +41 -0
- package/dist/chunk-SDM3VJJ5.js.map +1 -0
- package/dist/chunk-XTC3X6MS.js +816 -0
- package/dist/chunk-XTC3X6MS.js.map +1 -0
- package/dist/chunk-ZXW5YIWK.js +55 -0
- package/dist/chunk-ZXW5YIWK.js.map +1 -0
- package/dist/core/index.cjs +432 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +210 -0
- package/dist/core/index.d.ts +210 -0
- package/dist/core/index.js +73 -0
- package/dist/core/index.js.map +1 -0
- package/dist/function-2bJW1Zp_.d.ts +187 -0
- package/dist/function-r2sh-jOO.d.cts +187 -0
- package/dist/generate/index.cjs +3787 -0
- package/dist/generate/index.cjs.map +1 -0
- package/dist/generate/index.d.cts +980 -0
- package/dist/generate/index.d.ts +980 -0
- package/dist/generate/index.js +52 -0
- package/dist/generate/index.js.map +1 -0
- package/dist/index-BMPxwjwV.d.ts +151 -0
- package/dist/index-BOBXBNwP.d.cts +157 -0
- package/dist/index-BdeQ1r_p.d.cts +56 -0
- package/dist/index-BreoJRw0.d.ts +127 -0
- package/dist/index-CGMZNCx6.d.cts +140 -0
- package/dist/index-CGMZNCx6.d.ts +140 -0
- package/dist/index-CM5W4rcm.d.ts +157 -0
- package/dist/index-CP3wAfEH.d.ts +56 -0
- package/dist/index-Cl7mhOFM.d.cts +164 -0
- package/dist/index-Cl7mhOFM.d.ts +164 -0
- package/dist/index-DZ1M21do.d.cts +127 -0
- package/dist/index-D_ZTUNKp.d.cts +151 -0
- package/dist/index.cjs +5277 -4893
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -2621
- package/dist/index.d.ts +14 -2621
- package/dist/index.js +170 -6152
- package/dist/index.js.map +1 -1
- package/dist/model/index.cjs +2435 -0
- package/dist/model/index.cjs.map +1 -0
- package/dist/model/index.d.cts +630 -0
- package/dist/model/index.d.ts +630 -0
- package/dist/model/index.js +21 -0
- package/dist/model/index.js.map +1 -0
- package/dist/styled-BjXtOFNk.d.cts +282 -0
- package/dist/styled-D7BAx3S3.d.ts +282 -0
- package/dist/theory/index.cjs +1677 -0
- package/dist/theory/index.cjs.map +1 -0
- package/dist/theory/index.d.cts +535 -0
- package/dist/theory/index.d.ts +535 -0
- package/dist/theory/index.js +149 -0
- package/dist/theory/index.js.map +1 -0
- package/dist/types-MGP29r3t.d.cts +30 -0
- package/dist/types-MGP29r3t.d.ts +30 -0
- package/package.json +31 -1
- package/src/analyze/arrange/index.ts +21 -0
- package/src/analyze/arrange/internal.ts +125 -0
- package/src/analyze/arrange/tension.ts +187 -0
- package/src/analyze/arrange/tracks.ts +279 -0
- package/src/{detect → analyze/detect}/index.ts +82 -23
- package/src/analyze/functional/borrowed.ts +84 -0
- package/src/analyze/functional/cadence.ts +66 -0
- package/src/analyze/functional/function.ts +184 -0
- package/src/analyze/functional/index.ts +23 -0
- package/src/analyze/functional/internal.ts +29 -0
- package/src/analyze/functional/roman.ts +393 -0
- package/src/analyze/index.ts +49 -0
- package/src/{timeline → analyze/timeline}/index.ts +87 -24
- package/src/{analysis → analyze/voice}/index.ts +81 -12
- package/src/core/index.ts +47 -0
- package/src/{interval → core/interval}/index.ts +15 -1
- package/src/{meter → core/meter}/index.ts +108 -5
- package/src/{pitch → core/pitch}/index.ts +58 -3
- package/src/{random → core/random}/index.ts +13 -1
- package/src/{tuning → core/tuning}/index.ts +28 -1
- package/src/{bass → generate/bass}/index.ts +66 -17
- package/src/{countermelody → generate/countermelody}/index.ts +54 -24
- package/src/{drums → generate/drums}/fills.ts +31 -20
- package/src/{drums → generate/drums}/index.ts +127 -9
- package/src/{drums → generate/drums}/internal.ts +7 -1
- package/src/{drums → generate/drums}/rng.ts +2 -2
- package/src/{drums → generate/drums}/swing.ts +7 -2
- package/src/{groove → generate/groove}/index.ts +118 -22
- package/src/{harmonize → generate/harmonize}/index.ts +96 -23
- package/src/generate/index.ts +59 -0
- package/src/{motif → generate/motif}/index.ts +94 -17
- package/src/{progression → generate/progression}/index.ts +61 -27
- package/src/{reharmony → generate/reharmony}/index.ts +46 -15
- package/src/{rhythm → generate/rhythm}/index.ts +47 -13
- package/src/index.ts +130 -116
- package/src/model/chord.ts +423 -0
- package/src/model/index.ts +5 -947
- package/src/model/interval.ts +82 -0
- package/src/model/key.ts +266 -0
- package/src/model/note.ts +176 -0
- package/src/model/progression.ts +161 -0
- package/src/model/shared.ts +12 -0
- package/src/{chord → theory/chord}/index.ts +85 -4
- package/src/{chordscale → theory/chordscale}/index.ts +67 -9
- package/src/{counterpoint → theory/counterpoint}/index.ts +23 -2
- package/src/{harmony → theory/harmony}/index.ts +19 -2
- package/src/theory/index.ts +107 -0
- package/src/{safety → theory/safety}/index.ts +59 -12
- package/src/theory/scale/degrees.ts +123 -0
- package/src/theory/scale/index.ts +28 -0
- package/src/theory/scale/key.ts +56 -0
- package/src/theory/scale/masks.ts +153 -0
- package/src/{spelling → theory/spelling}/index.ts +21 -3
- package/src/{symbol → theory/symbol}/index.ts +91 -36
- package/src/theory/voicing/index.ts +5 -0
- package/src/theory/voicing/internal.ts +168 -0
- package/src/theory/voicing/leading.ts +112 -0
- package/src/theory/voicing/satb.ts +195 -0
- package/src/theory/voicing/styled.ts +176 -0
- package/src/arrange/index.ts +0 -409
- package/src/functional/index.ts +0 -602
- package/src/scale/index.ts +0 -215
- package/src/voicing/index.ts +0 -523
- /package/src/{types.ts → core/types.ts} +0 -0
- /package/src/{drums → generate/drums}/beat.ts +0 -0
- /package/src/{drums → generate/drums}/euclid.ts +0 -0
- /package/src/{drums → generate/drums}/ghost.ts +0 -0
- /package/src/{drums → generate/drums}/hihat.ts +0 -0
- /package/src/{drums → generate/drums}/hit.ts +0 -0
- /package/src/{drums → generate/drums}/kick.ts +0 -0
- /package/src/{drums → generate/drums}/percussion.ts +0 -0
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatNote,
|
|
3
|
+
midiToNote,
|
|
4
|
+
noteToPitchClass,
|
|
5
|
+
parseNote
|
|
6
|
+
} from "./chunk-DYWFNC4A.js";
|
|
7
|
+
import {
|
|
8
|
+
NAMED_SCALES,
|
|
9
|
+
chordPitchClasses,
|
|
10
|
+
chordToneRole,
|
|
11
|
+
createsHiddenParallelPerfect,
|
|
12
|
+
createsParallelPerfect,
|
|
13
|
+
createsVoiceCrossing,
|
|
14
|
+
createsVoiceOverlap,
|
|
15
|
+
exceedsSpacing,
|
|
16
|
+
makeChord,
|
|
17
|
+
scaleTonesInDegreeOrder
|
|
18
|
+
} from "./chunk-7Y7WF64D.js";
|
|
19
|
+
|
|
20
|
+
// src/theory/chordscale/index.ts
|
|
21
|
+
function pitchClass(value) {
|
|
22
|
+
return (Math.trunc(value) % 12 + 12) % 12;
|
|
23
|
+
}
|
|
24
|
+
function popcount12(mask) {
|
|
25
|
+
let count = 0;
|
|
26
|
+
for (let n = 0; n < 12; n += 1) {
|
|
27
|
+
count += mask >> n & 1;
|
|
28
|
+
}
|
|
29
|
+
return count;
|
|
30
|
+
}
|
|
31
|
+
function maskHasPitchClass(mask, scaleRootPc, pc) {
|
|
32
|
+
const offset = pitchClass(pc - scaleRootPc);
|
|
33
|
+
return (mask >> offset & 1) === 1;
|
|
34
|
+
}
|
|
35
|
+
function scaleMatchesChord(chordPcs, scaleMask, scaleRootPc) {
|
|
36
|
+
for (const pc of chordPcs) {
|
|
37
|
+
if (!maskHasPitchClass(scaleMask, scaleRootPc, pc)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
var ALIASED_SCALE_NAMES = /* @__PURE__ */ new Set(["major", "naturalMinor"]);
|
|
44
|
+
var HEPTATONIC_SIZE = 7;
|
|
45
|
+
function chordScales(chord) {
|
|
46
|
+
const chordPcs = chordPitchClasses(chord);
|
|
47
|
+
const rootPc = pitchClass(chord.rootPc);
|
|
48
|
+
const ranked = [];
|
|
49
|
+
const seenMasks = /* @__PURE__ */ new Set();
|
|
50
|
+
for (const name of Object.keys(NAMED_SCALES)) {
|
|
51
|
+
if (name === "chromatic" || ALIASED_SCALE_NAMES.has(name)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const mask = NAMED_SCALES[name];
|
|
55
|
+
if (mask === void 0 || seenMasks.has(mask)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
seenMasks.add(mask);
|
|
59
|
+
if (scaleMatchesChord(chordPcs, mask, rootPc)) {
|
|
60
|
+
const size = popcount12(mask);
|
|
61
|
+
ranked.push({ name, extra: size - chordPcs.length, size });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (ranked.length === 0) {
|
|
65
|
+
if (NAMED_SCALES.chromatic !== void 0) {
|
|
66
|
+
return [{ name: "chromatic", rootPc }];
|
|
67
|
+
}
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
const preferHeptatonic = chordPcs.length <= 3;
|
|
71
|
+
ranked.sort((a, b) => {
|
|
72
|
+
if (preferHeptatonic && a.size === HEPTATONIC_SIZE !== (b.size === HEPTATONIC_SIZE)) {
|
|
73
|
+
return a.size === HEPTATONIC_SIZE ? -1 : 1;
|
|
74
|
+
}
|
|
75
|
+
if (a.extra !== b.extra) {
|
|
76
|
+
return a.extra - b.extra;
|
|
77
|
+
}
|
|
78
|
+
if (a.size !== b.size) {
|
|
79
|
+
return a.size - b.size;
|
|
80
|
+
}
|
|
81
|
+
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
82
|
+
});
|
|
83
|
+
return ranked.map((entry) => ({ name: entry.name, rootPc }));
|
|
84
|
+
}
|
|
85
|
+
function avoidNotes(chord, scaleName) {
|
|
86
|
+
const mask = NAMED_SCALES[scaleName];
|
|
87
|
+
if (mask === void 0) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
const rootPc = pitchClass(chord.rootPc);
|
|
91
|
+
const chordPcs = chordPitchClasses(chord);
|
|
92
|
+
if (!scaleMatchesChord(chordPcs, mask, rootPc)) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
const chordSet = new Set(chordPcs);
|
|
96
|
+
const avoid = [];
|
|
97
|
+
for (let pc = 0; pc < 12; pc += 1) {
|
|
98
|
+
if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc)) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (chordSet.has(pitchClass(pc - 1))) {
|
|
102
|
+
avoid.push(pc);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return avoid.sort((a, b) => a - b);
|
|
106
|
+
}
|
|
107
|
+
function availableTensions(chord, scaleName) {
|
|
108
|
+
const mask = NAMED_SCALES[scaleName];
|
|
109
|
+
if (mask === void 0) {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
const rootPc = pitchClass(chord.rootPc);
|
|
113
|
+
const chordPcs = chordPitchClasses(chord);
|
|
114
|
+
if (!scaleMatchesChord(chordPcs, mask, rootPc)) {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
const chordSet = new Set(chordPcs);
|
|
118
|
+
const avoidSet = new Set(avoidNotes(chord, scaleName));
|
|
119
|
+
const tensions = [];
|
|
120
|
+
for (let pc = 0; pc < 12; pc += 1) {
|
|
121
|
+
if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc) || avoidSet.has(pc)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
tensions.push(pc);
|
|
125
|
+
}
|
|
126
|
+
return tensions.sort((a, b) => a - b);
|
|
127
|
+
}
|
|
128
|
+
function chordScaleReport(chord, limit) {
|
|
129
|
+
const matches = chordScales(chord);
|
|
130
|
+
const chosen = limit === void 0 ? matches : matches.slice(0, Math.max(0, limit));
|
|
131
|
+
return chosen.map((match) => ({
|
|
132
|
+
name: match.name,
|
|
133
|
+
rootPc: match.rootPc,
|
|
134
|
+
avoid: avoidNotes(chord, match.name),
|
|
135
|
+
tensions: availableTensions(chord, match.name)
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
var RANK_PENALTY = 0.01;
|
|
139
|
+
function scalePitchClassSet(name, rootPc) {
|
|
140
|
+
const mask = NAMED_SCALES[name];
|
|
141
|
+
const pcs = /* @__PURE__ */ new Set();
|
|
142
|
+
if (mask === void 0) {
|
|
143
|
+
return pcs;
|
|
144
|
+
}
|
|
145
|
+
for (let n = 0; n < 12; n += 1) {
|
|
146
|
+
if ((mask >> n & 1) === 1) {
|
|
147
|
+
pcs.add(pitchClass(rootPc + n));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return pcs;
|
|
151
|
+
}
|
|
152
|
+
function symmetricDifferenceSize(a, b) {
|
|
153
|
+
let count = 0;
|
|
154
|
+
for (const pc of a) {
|
|
155
|
+
if (!b.has(pc)) {
|
|
156
|
+
count += 1;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
for (const pc of b) {
|
|
160
|
+
if (!a.has(pc)) {
|
|
161
|
+
count += 1;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return count;
|
|
165
|
+
}
|
|
166
|
+
function scalesForChanges(chords) {
|
|
167
|
+
if (chords.length === 0) {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
const candidateLists = chords.map((chord) => chordScales(chord));
|
|
171
|
+
const pcSets = candidateLists.map(
|
|
172
|
+
(candidates) => candidates.map((match) => scalePitchClassSet(match.name, match.rootPc))
|
|
173
|
+
);
|
|
174
|
+
const first = candidateLists[0] ?? [];
|
|
175
|
+
const dp = [first.map((_, j) => j * RANK_PENALTY)];
|
|
176
|
+
const back = [first.map(() => -1)];
|
|
177
|
+
for (let i = 1; i < candidateLists.length; i += 1) {
|
|
178
|
+
const candidates = candidateLists[i] ?? [];
|
|
179
|
+
const prevCosts = dp[i - 1] ?? [];
|
|
180
|
+
const prevSets = pcSets[i - 1] ?? [];
|
|
181
|
+
const curSets = pcSets[i] ?? [];
|
|
182
|
+
const stageCosts = [];
|
|
183
|
+
const stageBack = [];
|
|
184
|
+
for (let j = 0; j < candidates.length; j += 1) {
|
|
185
|
+
let bestCost = Number.POSITIVE_INFINITY;
|
|
186
|
+
let bestPrev = -1;
|
|
187
|
+
for (let k = 0; k < prevCosts.length; k += 1) {
|
|
188
|
+
const transition = symmetricDifferenceSize(
|
|
189
|
+
prevSets[k] ?? /* @__PURE__ */ new Set(),
|
|
190
|
+
curSets[j] ?? /* @__PURE__ */ new Set()
|
|
191
|
+
);
|
|
192
|
+
const cost = (prevCosts[k] ?? 0) + transition;
|
|
193
|
+
if (cost < bestCost) {
|
|
194
|
+
bestCost = cost;
|
|
195
|
+
bestPrev = k;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
stageCosts.push(bestCost + j * RANK_PENALTY);
|
|
199
|
+
stageBack.push(bestPrev);
|
|
200
|
+
}
|
|
201
|
+
dp.push(stageCosts);
|
|
202
|
+
back.push(stageBack);
|
|
203
|
+
}
|
|
204
|
+
const lastCosts = dp[dp.length - 1] ?? [];
|
|
205
|
+
let bestFinal = 0;
|
|
206
|
+
for (let j = 1; j < lastCosts.length; j += 1) {
|
|
207
|
+
if ((lastCosts[j] ?? Number.POSITIVE_INFINITY) < (lastCosts[bestFinal] ?? 0)) {
|
|
208
|
+
bestFinal = j;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const chosenIndices = new Array(candidateLists.length);
|
|
212
|
+
let current = bestFinal;
|
|
213
|
+
for (let i = candidateLists.length - 1; i >= 0; i -= 1) {
|
|
214
|
+
chosenIndices[i] = current;
|
|
215
|
+
current = back[i]?.[current] ?? -1;
|
|
216
|
+
}
|
|
217
|
+
return chords.map((chord, i) => {
|
|
218
|
+
const candidates = candidateLists[i] ?? [];
|
|
219
|
+
const scale = candidates[chosenIndices[i] ?? 0] ?? { name: "chromatic", rootPc: chord.rootPc };
|
|
220
|
+
return { chord, scale };
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/theory/spelling/index.ts
|
|
225
|
+
var LETTER_SEMITONES = [0, 2, 4, 5, 7, 9, 11];
|
|
226
|
+
var CHROMATIC_SPELLING = {
|
|
227
|
+
1: { degreeOffset: 1, alter: -1 },
|
|
228
|
+
// b2
|
|
229
|
+
3: { degreeOffset: 2, alter: -1 },
|
|
230
|
+
// b3
|
|
231
|
+
6: { degreeOffset: 3, alter: 1 },
|
|
232
|
+
// #4
|
|
233
|
+
8: { degreeOffset: 5, alter: -1 },
|
|
234
|
+
// b6
|
|
235
|
+
9: { degreeOffset: 5, alter: 1 },
|
|
236
|
+
// #6 (raised sixth of a minor key)
|
|
237
|
+
10: { degreeOffset: 6, alter: -1 },
|
|
238
|
+
// b7
|
|
239
|
+
11: { degreeOffset: 6, alter: 1 }
|
|
240
|
+
// #7 (raised leading tone of a minor key)
|
|
241
|
+
};
|
|
242
|
+
function mod12(n) {
|
|
243
|
+
return (n % 12 + 12) % 12;
|
|
244
|
+
}
|
|
245
|
+
function mod7(n) {
|
|
246
|
+
return (n % 7 + 7) % 7;
|
|
247
|
+
}
|
|
248
|
+
function naturalPc(letter) {
|
|
249
|
+
return LETTER_SEMITONES[mod7(letter)] ?? 0;
|
|
250
|
+
}
|
|
251
|
+
function alterFor(letter, pc) {
|
|
252
|
+
let d = mod12(pc - naturalPc(letter));
|
|
253
|
+
if (d > 6) {
|
|
254
|
+
d -= 12;
|
|
255
|
+
}
|
|
256
|
+
return d;
|
|
257
|
+
}
|
|
258
|
+
function isHeptatonic(key) {
|
|
259
|
+
return scaleTonesInDegreeOrder(key).length === 7;
|
|
260
|
+
}
|
|
261
|
+
function spellPitchClass(pc, tonic, key) {
|
|
262
|
+
const tonicPc = mod12(naturalPc(tonic.letter) + tonic.alter);
|
|
263
|
+
const offset = mod12(pc - tonicPc);
|
|
264
|
+
const tones = scaleTonesInDegreeOrder(key);
|
|
265
|
+
if (isHeptatonic(key)) {
|
|
266
|
+
const degree = tones.indexOf(mod12(pc));
|
|
267
|
+
if (degree >= 0) {
|
|
268
|
+
const letter = mod7(tonic.letter + degree);
|
|
269
|
+
return { letter, alter: alterFor(letter, pc) };
|
|
270
|
+
}
|
|
271
|
+
const chromatic = CHROMATIC_SPELLING[offset];
|
|
272
|
+
if (chromatic) {
|
|
273
|
+
const letter = mod7(tonic.letter + chromatic.degreeOffset);
|
|
274
|
+
return { letter, alter: alterFor(letter, pc) };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
for (let letter = 0; letter < 7; letter += 1) {
|
|
278
|
+
if (naturalPc(letter) === mod12(pc)) {
|
|
279
|
+
return { letter, alter: 0 };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const belowLetter = mod7([0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6][mod12(pc)] ?? 0);
|
|
283
|
+
return { letter: belowLetter, alter: alterFor(belowLetter, pc) };
|
|
284
|
+
}
|
|
285
|
+
function spellScale(tonic, key) {
|
|
286
|
+
return scaleTonesInDegreeOrder(key).map((pc) => spellPitchClass(pc, tonic, key));
|
|
287
|
+
}
|
|
288
|
+
function spellPitchClasses(pcs, tonic, key) {
|
|
289
|
+
return pcs.map((pc) => spellPitchClass(pc, tonic, key));
|
|
290
|
+
}
|
|
291
|
+
function spellChord(chord, tonic, key) {
|
|
292
|
+
return chord.intervals.map(
|
|
293
|
+
(interval) => spellPitchClass(mod12(chord.rootPc + interval), tonic, key)
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
function noteNames(notes) {
|
|
297
|
+
return notes.map(formatNote);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/theory/symbol/index.ts
|
|
301
|
+
var ROOT_RE = /^[A-Ga-g](?:#{1,2}|b{1,2})?/;
|
|
302
|
+
var BASS_RE = /^[A-Ga-g](?:#{1,2}|b{1,2})?$/;
|
|
303
|
+
var QUALITY_MAP = {
|
|
304
|
+
"": "maj",
|
|
305
|
+
m: "min",
|
|
306
|
+
min: "min",
|
|
307
|
+
"-": "min",
|
|
308
|
+
dim: "dim",
|
|
309
|
+
"\xB0": "dim",
|
|
310
|
+
aug: "aug",
|
|
311
|
+
"+": "aug",
|
|
312
|
+
maj7: "maj7",
|
|
313
|
+
M7: "maj7",
|
|
314
|
+
\u0394: "maj7",
|
|
315
|
+
\u03947: "maj7",
|
|
316
|
+
m7: "min7",
|
|
317
|
+
min7: "min7",
|
|
318
|
+
"-7": "min7",
|
|
319
|
+
"7": "dom7",
|
|
320
|
+
dim7: "dim7",
|
|
321
|
+
"\xB07": "dim7",
|
|
322
|
+
m7b5: "m7b5",
|
|
323
|
+
\u00F8: "m7b5",
|
|
324
|
+
\u00F87: "m7b5",
|
|
325
|
+
min7b5: "m7b5",
|
|
326
|
+
mMaj7: "minMaj7",
|
|
327
|
+
mM7: "minMaj7",
|
|
328
|
+
minMaj7: "minMaj7",
|
|
329
|
+
aug7: "aug7",
|
|
330
|
+
"+7": "aug7",
|
|
331
|
+
"7#5": "aug7",
|
|
332
|
+
augMaj7: "augMaj7",
|
|
333
|
+
augM7: "augMaj7",
|
|
334
|
+
"+maj7": "augMaj7",
|
|
335
|
+
"+M7": "augMaj7",
|
|
336
|
+
majb5: "majb5",
|
|
337
|
+
"maj(b5)": "majb5",
|
|
338
|
+
"(b5)": "majb5",
|
|
339
|
+
"6": "6",
|
|
340
|
+
m6: "min6",
|
|
341
|
+
min6: "min6",
|
|
342
|
+
"6/9": "6/9",
|
|
343
|
+
"69": "6/9",
|
|
344
|
+
sus2: "sus2",
|
|
345
|
+
sus: "sus4",
|
|
346
|
+
sus4: "sus4",
|
|
347
|
+
add9: "add9",
|
|
348
|
+
add11: "add11",
|
|
349
|
+
maj9: "maj9",
|
|
350
|
+
M9: "maj9",
|
|
351
|
+
m9: "min9",
|
|
352
|
+
min9: "min9",
|
|
353
|
+
"9": "dom9",
|
|
354
|
+
"7b9": "7b9",
|
|
355
|
+
"7#9": "7#9",
|
|
356
|
+
"7#11": "7#11",
|
|
357
|
+
"7b13": "7b13",
|
|
358
|
+
"11": "11",
|
|
359
|
+
"13": "13",
|
|
360
|
+
"5": "5"
|
|
361
|
+
};
|
|
362
|
+
var CANONICAL_SUFFIX = {
|
|
363
|
+
maj: "",
|
|
364
|
+
min: "m",
|
|
365
|
+
dim: "dim",
|
|
366
|
+
aug: "aug",
|
|
367
|
+
maj7: "maj7",
|
|
368
|
+
min7: "m7",
|
|
369
|
+
dom7: "7",
|
|
370
|
+
dim7: "dim7",
|
|
371
|
+
m7b5: "m7b5",
|
|
372
|
+
minMaj7: "mMaj7",
|
|
373
|
+
aug7: "aug7",
|
|
374
|
+
augMaj7: "augMaj7",
|
|
375
|
+
majb5: "maj(b5)",
|
|
376
|
+
"6": "6",
|
|
377
|
+
min6: "m6",
|
|
378
|
+
"6/9": "6/9",
|
|
379
|
+
sus2: "sus2",
|
|
380
|
+
sus4: "sus4",
|
|
381
|
+
add9: "add9",
|
|
382
|
+
add11: "add11",
|
|
383
|
+
maj9: "maj9",
|
|
384
|
+
min9: "m9",
|
|
385
|
+
dom9: "9",
|
|
386
|
+
"7b9": "7b9",
|
|
387
|
+
"7#9": "7#9",
|
|
388
|
+
"7#11": "7#11",
|
|
389
|
+
"7b13": "7b13",
|
|
390
|
+
"11": "11",
|
|
391
|
+
"13": "13",
|
|
392
|
+
"5": "5"
|
|
393
|
+
};
|
|
394
|
+
function pitchClass2(value) {
|
|
395
|
+
return (Math.trunc(value) % 12 + 12) % 12;
|
|
396
|
+
}
|
|
397
|
+
function makeSpelledChord(root, quality, bass) {
|
|
398
|
+
const chord = makeChord(
|
|
399
|
+
noteToPitchClass(root),
|
|
400
|
+
quality,
|
|
401
|
+
bass === void 0 ? void 0 : noteToPitchClass(bass)
|
|
402
|
+
);
|
|
403
|
+
chord.rootSpelling = { letter: root.letter, alter: root.alter };
|
|
404
|
+
if (bass !== void 0) {
|
|
405
|
+
chord.bassSpelling = { letter: bass.letter, alter: bass.alter };
|
|
406
|
+
}
|
|
407
|
+
return chord;
|
|
408
|
+
}
|
|
409
|
+
function parseChordSymbol(text) {
|
|
410
|
+
const trimmed = text.trim();
|
|
411
|
+
const rootMatch = ROOT_RE.exec(trimmed);
|
|
412
|
+
if (!rootMatch) {
|
|
413
|
+
throw new Error(`Invalid chord symbol: ${text}`);
|
|
414
|
+
}
|
|
415
|
+
const rootToken = rootMatch[0];
|
|
416
|
+
const rootNote = parseNote(rootToken);
|
|
417
|
+
const rest = trimmed.slice(rootToken.length);
|
|
418
|
+
const slashIndex = rest.lastIndexOf("/");
|
|
419
|
+
if (slashIndex !== -1) {
|
|
420
|
+
const before = rest.slice(0, slashIndex);
|
|
421
|
+
const after = rest.slice(slashIndex + 1);
|
|
422
|
+
const beforeQuality = QUALITY_MAP[before];
|
|
423
|
+
if (beforeQuality !== void 0 && BASS_RE.test(after)) {
|
|
424
|
+
return makeSpelledChord(rootNote, beforeQuality, parseNote(after));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const quality = QUALITY_MAP[rest];
|
|
428
|
+
if (quality === void 0) {
|
|
429
|
+
throw new Error(`Unrecognized chord quality: ${text}`);
|
|
430
|
+
}
|
|
431
|
+
return makeSpelledChord(rootNote, quality);
|
|
432
|
+
}
|
|
433
|
+
function pitchClassName(pc, hint, flats) {
|
|
434
|
+
if (flats === void 0 && hint !== void 0 && noteToPitchClass(hint) === pc) {
|
|
435
|
+
return formatNote(hint);
|
|
436
|
+
}
|
|
437
|
+
const note = midiToNote(60 + pc, flats ? "flat" : "sharp");
|
|
438
|
+
return formatNote({ letter: note.letter, alter: note.alter });
|
|
439
|
+
}
|
|
440
|
+
function formatChordSymbol(chord, opts) {
|
|
441
|
+
const rootPc = pitchClass2(chord.rootPc);
|
|
442
|
+
const rootName = pitchClassName(rootPc, chord.rootSpelling, opts?.flats);
|
|
443
|
+
const suffix = CANONICAL_SUFFIX[chord.quality];
|
|
444
|
+
let symbol = `${rootName}${suffix}`;
|
|
445
|
+
if (chord.bassPc !== void 0 && pitchClass2(chord.bassPc) !== rootPc) {
|
|
446
|
+
symbol += `/${pitchClassName(pitchClass2(chord.bassPc), chord.bassSpelling, opts?.flats)}`;
|
|
447
|
+
}
|
|
448
|
+
return symbol;
|
|
449
|
+
}
|
|
450
|
+
function transposeChordSymbol(text, semitones, opts) {
|
|
451
|
+
const chord = parseChordSymbol(text);
|
|
452
|
+
const transposed = { ...chord, rootPc: pitchClass2(chord.rootPc + semitones) };
|
|
453
|
+
if (chord.bassPc !== void 0) {
|
|
454
|
+
transposed.bassPc = pitchClass2(chord.bassPc + semitones);
|
|
455
|
+
}
|
|
456
|
+
return formatChordSymbol(transposed, opts);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// src/theory/voicing/internal.ts
|
|
460
|
+
var DEFAULT_MAX_SPACING = 12;
|
|
461
|
+
var VIOLATION_PENALTY = 1e3;
|
|
462
|
+
var MISSING_TONE_PENALTY = 500;
|
|
463
|
+
var POOR_DOUBLING_PENALTY = 4;
|
|
464
|
+
var MAX_CANDIDATES = 4e3;
|
|
465
|
+
function pitchClass3(value) {
|
|
466
|
+
return (Math.trunc(value) % 12 + 12) % 12;
|
|
467
|
+
}
|
|
468
|
+
function pitchesForPc(pc, range) {
|
|
469
|
+
const result = [];
|
|
470
|
+
for (let midi = Math.ceil(range.min); midi <= range.max; midi += 1) {
|
|
471
|
+
if (pitchClass3(midi) === pc) {
|
|
472
|
+
result.push(midi);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const center = (range.min + range.max) / 2;
|
|
476
|
+
return result.sort((a, b) => {
|
|
477
|
+
const da = Math.abs(a - center);
|
|
478
|
+
const db = Math.abs(b - center);
|
|
479
|
+
return da === db ? a - b : da - db;
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
function enumerateVoicings(chord, ranges, maxSpacing) {
|
|
483
|
+
const chordPcs = chordPitchClasses(chord);
|
|
484
|
+
const bassPc = pitchClass3(chord.bassPc ?? chord.rootPc);
|
|
485
|
+
const results = [];
|
|
486
|
+
const current = [];
|
|
487
|
+
const build = (voice) => {
|
|
488
|
+
if (results.length >= MAX_CANDIDATES) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (voice === ranges.length) {
|
|
492
|
+
results.push([...current]);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const range = ranges[voice];
|
|
496
|
+
if (range === void 0) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
const pcs = voice === 0 ? [bassPc] : chordPcs;
|
|
500
|
+
const prev = current[voice - 1];
|
|
501
|
+
for (const pc of pcs) {
|
|
502
|
+
for (const pitch of pitchesForPc(pc, range)) {
|
|
503
|
+
if (prev !== void 0) {
|
|
504
|
+
if (pitch < prev) {
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
const spacingLimit = voice === 1 ? maxSpacing + 12 : maxSpacing;
|
|
508
|
+
if (pitch - prev > spacingLimit) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
current.push(pitch);
|
|
513
|
+
build(voice + 1);
|
|
514
|
+
current.pop();
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
build(0);
|
|
519
|
+
return results;
|
|
520
|
+
}
|
|
521
|
+
function structuralPenalty(pitches, chord) {
|
|
522
|
+
const counts = /* @__PURE__ */ new Map();
|
|
523
|
+
for (const pitch of pitches) {
|
|
524
|
+
const pc = pitchClass3(pitch);
|
|
525
|
+
counts.set(pc, (counts.get(pc) ?? 0) + 1);
|
|
526
|
+
}
|
|
527
|
+
let penalty = 0;
|
|
528
|
+
for (const pc of chordPitchClasses(chord)) {
|
|
529
|
+
if (!counts.has(pc)) {
|
|
530
|
+
penalty += MISSING_TONE_PENALTY;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const rootPc = pitchClass3(chord.rootPc);
|
|
534
|
+
const fifthPc = pitchClass3(chord.rootPc + 7);
|
|
535
|
+
for (const [pc, count] of counts) {
|
|
536
|
+
if (count > 1 && pc !== rootPc && pc !== fifthPc) {
|
|
537
|
+
penalty += (count - 1) * POOR_DOUBLING_PENALTY;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return penalty;
|
|
541
|
+
}
|
|
542
|
+
function violationCount(prev, cur, maxSpacing) {
|
|
543
|
+
let count = 0;
|
|
544
|
+
for (let lower = 0; lower < cur.length; lower += 1) {
|
|
545
|
+
for (let upper = lower + 1; upper < cur.length; upper += 1) {
|
|
546
|
+
const prevLower = prev[lower];
|
|
547
|
+
const prevUpper = prev[upper];
|
|
548
|
+
const curLower = cur[lower];
|
|
549
|
+
const curUpper = cur[upper];
|
|
550
|
+
if (prevLower === void 0 || prevUpper === void 0 || curLower === void 0 || curUpper === void 0) {
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (createsParallelPerfect(prevUpper, curUpper, prevLower, curLower)) {
|
|
554
|
+
count += 1;
|
|
555
|
+
}
|
|
556
|
+
if (createsVoiceCrossing(curUpper, curLower)) {
|
|
557
|
+
count += 1;
|
|
558
|
+
}
|
|
559
|
+
if (upper === lower + 1) {
|
|
560
|
+
if (createsVoiceOverlap(prevUpper, curUpper, prevLower, curLower)) {
|
|
561
|
+
count += 1;
|
|
562
|
+
}
|
|
563
|
+
if (lower > 0 && exceedsSpacing(curUpper, curLower, maxSpacing)) {
|
|
564
|
+
count += 1;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return count;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/theory/voicing/leading.ts
|
|
573
|
+
var HIDDEN_PERFECT_PENALTY = 6;
|
|
574
|
+
function voiceLeadingCost(from, to) {
|
|
575
|
+
if (from.length !== to.length) {
|
|
576
|
+
return Number.POSITIVE_INFINITY;
|
|
577
|
+
}
|
|
578
|
+
let total = 0;
|
|
579
|
+
for (let i = 0; i < from.length; i += 1) {
|
|
580
|
+
const a = from[i];
|
|
581
|
+
const b = to[i];
|
|
582
|
+
if (a === void 0 || b === void 0) {
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
total += Math.abs(b - a);
|
|
586
|
+
}
|
|
587
|
+
if (from.length >= 2) {
|
|
588
|
+
const bassPrev = from[0];
|
|
589
|
+
const bassCur = to[0];
|
|
590
|
+
const sopPrev = from[from.length - 1];
|
|
591
|
+
const sopCur = to[to.length - 1];
|
|
592
|
+
if (bassPrev !== void 0 && bassCur !== void 0 && sopPrev !== void 0 && sopCur !== void 0 && createsHiddenParallelPerfect(bassPrev, bassCur, sopPrev, sopCur)) {
|
|
593
|
+
total += HIDDEN_PERFECT_PENALTY;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return total;
|
|
597
|
+
}
|
|
598
|
+
function nextVoicing(current, chord, opts) {
|
|
599
|
+
const ranges = opts?.ranges !== void 0 || opts?.voices !== void 0 ? resolveRanges(opts) : current.map((pitch) => {
|
|
600
|
+
const centre = Math.min(127, Math.max(0, pitch));
|
|
601
|
+
return { min: Math.max(0, centre - 12), max: Math.min(127, centre + 12) };
|
|
602
|
+
});
|
|
603
|
+
const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
|
|
604
|
+
const candidates = enumerateVoicings(chord, ranges, maxSpacing);
|
|
605
|
+
let best;
|
|
606
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
607
|
+
for (const candidate of candidates) {
|
|
608
|
+
const score = structuralPenalty(candidate, chord) + voiceLeadingCost(current, candidate) + VIOLATION_PENALTY * violationCount(current, candidate, maxSpacing);
|
|
609
|
+
if (score < bestScore) {
|
|
610
|
+
bestScore = score;
|
|
611
|
+
best = candidate;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (best === void 0) {
|
|
615
|
+
throw new Error("no voicing satisfies the given ranges");
|
|
616
|
+
}
|
|
617
|
+
return best;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// src/theory/voicing/satb.ts
|
|
621
|
+
var SATB_RANGES = [
|
|
622
|
+
{ min: 40, max: 60 },
|
|
623
|
+
{ min: 48, max: 67 },
|
|
624
|
+
{ min: 55, max: 74 },
|
|
625
|
+
{ min: 60, max: 79 }
|
|
626
|
+
];
|
|
627
|
+
var DERIVED_LOW = 40;
|
|
628
|
+
var DERIVED_HIGH = 79;
|
|
629
|
+
var DERIVED_SPAN = 19;
|
|
630
|
+
function resolveRanges(opts) {
|
|
631
|
+
if (opts?.ranges !== void 0) {
|
|
632
|
+
if (opts.ranges.length === 0) {
|
|
633
|
+
throw new Error("ranges must contain at least one voice range");
|
|
634
|
+
}
|
|
635
|
+
return opts.ranges.map((range) => ({ ...range }));
|
|
636
|
+
}
|
|
637
|
+
const voices = opts?.voices ?? 4;
|
|
638
|
+
if (voices < 1) {
|
|
639
|
+
throw new Error("voices must be at least 1");
|
|
640
|
+
}
|
|
641
|
+
if (voices === 4) {
|
|
642
|
+
return SATB_RANGES.map((range) => ({ ...range }));
|
|
643
|
+
}
|
|
644
|
+
if (voices === 1) {
|
|
645
|
+
return [{ min: DERIVED_LOW, max: DERIVED_HIGH }];
|
|
646
|
+
}
|
|
647
|
+
const ranges = [];
|
|
648
|
+
for (let i = 0; i < voices; i += 1) {
|
|
649
|
+
const min = Math.round(
|
|
650
|
+
DERIVED_LOW + i * (DERIVED_HIGH - DERIVED_SPAN - DERIVED_LOW) / (voices - 1)
|
|
651
|
+
);
|
|
652
|
+
ranges.push({ min, max: min + DERIVED_SPAN });
|
|
653
|
+
}
|
|
654
|
+
return ranges;
|
|
655
|
+
}
|
|
656
|
+
function voiceChord(chord, opts) {
|
|
657
|
+
const ranges = resolveRanges(opts);
|
|
658
|
+
const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
|
|
659
|
+
const candidates = enumerateVoicings(chord, ranges, maxSpacing);
|
|
660
|
+
let best;
|
|
661
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
662
|
+
for (const candidate of candidates) {
|
|
663
|
+
let score = structuralPenalty(candidate, chord);
|
|
664
|
+
for (let i = 0; i < candidate.length; i += 1) {
|
|
665
|
+
const pitch = candidate[i];
|
|
666
|
+
const range = ranges[i];
|
|
667
|
+
if (pitch === void 0 || range === void 0) {
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
score += Math.abs(pitch - (range.min + range.max) / 2);
|
|
671
|
+
}
|
|
672
|
+
if (score < bestScore) {
|
|
673
|
+
bestScore = score;
|
|
674
|
+
best = candidate;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (best === void 0) {
|
|
678
|
+
throw new Error("no voicing satisfies the given ranges");
|
|
679
|
+
}
|
|
680
|
+
return best;
|
|
681
|
+
}
|
|
682
|
+
function voiceProgression(chords, opts) {
|
|
683
|
+
const ranges = resolveRanges(opts);
|
|
684
|
+
const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
|
|
685
|
+
const result = [];
|
|
686
|
+
let prev;
|
|
687
|
+
for (const chord of chords) {
|
|
688
|
+
if (prev === void 0) {
|
|
689
|
+
prev = voiceChord(chord, opts);
|
|
690
|
+
result.push(prev);
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
const candidates = enumerateVoicings(chord, ranges, maxSpacing);
|
|
694
|
+
let best;
|
|
695
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
696
|
+
for (const candidate of candidates) {
|
|
697
|
+
const score = structuralPenalty(candidate, chord) + voiceLeadingCost(prev, candidate) + VIOLATION_PENALTY * violationCount(prev, candidate, maxSpacing);
|
|
698
|
+
if (score < bestScore) {
|
|
699
|
+
bestScore = score;
|
|
700
|
+
best = candidate;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (best === void 0) {
|
|
704
|
+
throw new Error("no voicing satisfies the given ranges");
|
|
705
|
+
}
|
|
706
|
+
result.push(best);
|
|
707
|
+
prev = best;
|
|
708
|
+
}
|
|
709
|
+
return result;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// src/theory/voicing/styled.ts
|
|
713
|
+
var DEFAULT_STYLE_OCTAVE = 4;
|
|
714
|
+
function dedupePcs(pcs) {
|
|
715
|
+
const seen = /* @__PURE__ */ new Set();
|
|
716
|
+
const result = [];
|
|
717
|
+
for (const pc of pcs) {
|
|
718
|
+
if (!seen.has(pc)) {
|
|
719
|
+
seen.add(pc);
|
|
720
|
+
result.push(pc);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return result;
|
|
724
|
+
}
|
|
725
|
+
function lowestPitchAtOrAbove(pc, floor) {
|
|
726
|
+
return floor + ((pc - pitchClass3(floor)) % 12 + 12) % 12;
|
|
727
|
+
}
|
|
728
|
+
function nearestPc(target, pcs) {
|
|
729
|
+
let best = pcs[0] ?? target;
|
|
730
|
+
let bestDist = 12;
|
|
731
|
+
for (const pc of pcs) {
|
|
732
|
+
const dist = Math.min(((pc - target) % 12 + 12) % 12, ((target - pc) % 12 + 12) % 12);
|
|
733
|
+
if (dist < bestDist) {
|
|
734
|
+
bestDist = dist;
|
|
735
|
+
best = pc;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return best;
|
|
739
|
+
}
|
|
740
|
+
function voiceChordStyled(chord, opts) {
|
|
741
|
+
const style = opts?.style ?? "close";
|
|
742
|
+
const base = 12 * ((opts?.octave ?? DEFAULT_STYLE_OCTAVE) + 1);
|
|
743
|
+
const rootPc = pitchClass3(chord.rootPc);
|
|
744
|
+
const bassPc = pitchClass3(chord.bassPc ?? chord.rootPc);
|
|
745
|
+
const omitRoot = style === "rootless" || opts?.rootless === true;
|
|
746
|
+
let order = dedupePcs(chord.intervals.map((interval) => pitchClass3(chord.rootPc + interval)));
|
|
747
|
+
const bassIndex = order.indexOf(bassPc);
|
|
748
|
+
if (bassIndex > 0) {
|
|
749
|
+
order = [...order.slice(bassIndex), ...order.slice(0, bassIndex)];
|
|
750
|
+
} else if (bassIndex < 0) {
|
|
751
|
+
order = [bassPc, ...order];
|
|
752
|
+
}
|
|
753
|
+
if (style === "shell") {
|
|
754
|
+
const roles = order.map((pc) => chordToneRole(pc, chord));
|
|
755
|
+
const guide = roles.includes("seventh") ? "seventh" : roles.includes("sixth") ? "sixth" : "fifth";
|
|
756
|
+
order = order.filter((_pc, i) => {
|
|
757
|
+
const role = roles[i];
|
|
758
|
+
return role === "root" || role === "third" || role === guide;
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
if (omitRoot) {
|
|
762
|
+
order = order.filter((pc) => pc !== rootPc);
|
|
763
|
+
}
|
|
764
|
+
if (order.length === 0) {
|
|
765
|
+
return [];
|
|
766
|
+
}
|
|
767
|
+
if (opts?.topNote !== void 0) {
|
|
768
|
+
const target = order.includes(pitchClass3(opts.topNote)) ? pitchClass3(opts.topNote) : nearestPc(pitchClass3(opts.topNote), order);
|
|
769
|
+
const targetIndex = order.indexOf(target);
|
|
770
|
+
order = [...order.slice(targetIndex + 1), ...order.slice(0, targetIndex + 1)];
|
|
771
|
+
}
|
|
772
|
+
const stack = [];
|
|
773
|
+
let prev;
|
|
774
|
+
for (const pc of order) {
|
|
775
|
+
if (prev === void 0) {
|
|
776
|
+
prev = lowestPitchAtOrAbove(pc, base);
|
|
777
|
+
} else {
|
|
778
|
+
let delta = ((pc - pitchClass3(prev)) % 12 + 12) % 12;
|
|
779
|
+
if (delta === 0) {
|
|
780
|
+
delta = 12;
|
|
781
|
+
}
|
|
782
|
+
prev += delta;
|
|
783
|
+
}
|
|
784
|
+
stack.push(prev);
|
|
785
|
+
}
|
|
786
|
+
if (style === "drop2" && stack.length >= 2) {
|
|
787
|
+
stack[stack.length - 2] = (stack[stack.length - 2] ?? 0) - 12;
|
|
788
|
+
} else if (style === "drop3" && stack.length >= 3) {
|
|
789
|
+
stack[stack.length - 3] = (stack[stack.length - 3] ?? 0) - 12;
|
|
790
|
+
}
|
|
791
|
+
return stack.sort((a, b) => a - b);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
export {
|
|
795
|
+
scaleMatchesChord,
|
|
796
|
+
chordScales,
|
|
797
|
+
avoidNotes,
|
|
798
|
+
availableTensions,
|
|
799
|
+
chordScaleReport,
|
|
800
|
+
scalesForChanges,
|
|
801
|
+
spellPitchClass,
|
|
802
|
+
spellScale,
|
|
803
|
+
spellPitchClasses,
|
|
804
|
+
spellChord,
|
|
805
|
+
noteNames,
|
|
806
|
+
parseChordSymbol,
|
|
807
|
+
formatChordSymbol,
|
|
808
|
+
transposeChordSymbol,
|
|
809
|
+
SATB_RANGES,
|
|
810
|
+
voiceChord,
|
|
811
|
+
voiceProgression,
|
|
812
|
+
voiceLeadingCost,
|
|
813
|
+
nextVoicing,
|
|
814
|
+
voiceChordStyled
|
|
815
|
+
};
|
|
816
|
+
//# sourceMappingURL=chunk-XTC3X6MS.js.map
|