@libraz/libcantus 0.9.2 → 0.9.4
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 +6 -5
- package/dist/analyze/index.cjs +41 -1676
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +194 -122
- package/dist/analyze/index.d.ts +194 -122
- package/dist/analyze/index.js +19 -8
- package/dist/chunk-36N73KZ3.cjs +246 -0
- package/dist/chunk-36N73KZ3.cjs.map +1 -0
- package/dist/chunk-4HLLZPXT.cjs +3077 -0
- package/dist/chunk-4HLLZPXT.cjs.map +1 -0
- package/dist/chunk-7MGZ7D62.cjs +498 -0
- package/dist/chunk-7MGZ7D62.cjs.map +1 -0
- package/dist/{chunk-R3S2LUBH.js → chunk-7NKDPF74.js} +660 -305
- package/dist/chunk-7NKDPF74.js.map +1 -0
- package/dist/chunk-CAN76NCU.cjs +679 -0
- package/dist/chunk-CAN76NCU.cjs.map +1 -0
- package/dist/{chunk-7Y7WF64D.js → chunk-D2FAJBYL.js} +104 -35
- package/dist/chunk-D2FAJBYL.js.map +1 -0
- package/dist/{chunk-COKTX2G5.js → chunk-EHVEBWM5.js} +1052 -510
- package/dist/chunk-EHVEBWM5.js.map +1 -0
- package/dist/chunk-FCANXBAT.cjs +30 -0
- package/dist/chunk-FCANXBAT.cjs.map +1 -0
- package/dist/chunk-FNDWXRRI.cjs +479 -0
- package/dist/chunk-FNDWXRRI.cjs.map +1 -0
- package/dist/{chunk-GQKN5KDT.js → chunk-GQSGS2N4.js} +43 -29
- package/dist/chunk-GQSGS2N4.js.map +1 -0
- package/dist/chunk-HKGYCQ34.js +53 -0
- package/dist/chunk-HKGYCQ34.js.map +1 -0
- package/dist/chunk-I24MV65U.js +106 -0
- package/dist/chunk-I24MV65U.js.map +1 -0
- package/dist/{chunk-LN7ZZMD2.js → chunk-KMHOICIY.js} +322 -173
- package/dist/chunk-KMHOICIY.js.map +1 -0
- package/dist/chunk-LW53AHQU.cjs +1117 -0
- package/dist/chunk-LW53AHQU.cjs.map +1 -0
- package/dist/chunk-N6IHQUOX.js +1 -0
- package/dist/chunk-N6IHQUOX.js.map +1 -0
- package/dist/chunk-NKIBQ6PT.js +30 -0
- package/dist/chunk-NKIBQ6PT.js.map +1 -0
- package/dist/chunk-NP4NA2FB.cjs +106 -0
- package/dist/chunk-NP4NA2FB.cjs.map +1 -0
- package/dist/chunk-NVE34YGZ.cjs +1449 -0
- package/dist/chunk-NVE34YGZ.cjs.map +1 -0
- package/dist/{chunk-A4532ZFR.js → chunk-NYYWMV2C.js} +115 -47
- package/dist/chunk-NYYWMV2C.js.map +1 -0
- package/dist/{chunk-5HTNMQW4.js → chunk-Q2XUP227.js} +310 -118
- package/dist/chunk-Q2XUP227.js.map +1 -0
- package/dist/chunk-Q6OP4U43.cjs +327 -0
- package/dist/chunk-Q6OP4U43.cjs.map +1 -0
- package/dist/chunk-RC6GBMCD.cjs +845 -0
- package/dist/chunk-RC6GBMCD.cjs.map +1 -0
- package/dist/{chunk-XTC3X6MS.js → chunk-RK35RIN7.js} +453 -152
- package/dist/chunk-RK35RIN7.js.map +1 -0
- package/dist/chunk-RUMU3UA6.cjs +53 -0
- package/dist/chunk-RUMU3UA6.cjs.map +1 -0
- package/dist/chunk-T64C7ZD7.cjs +1 -0
- package/dist/chunk-T64C7ZD7.cjs.map +1 -0
- package/dist/chunk-XEBG3G53.js +498 -0
- package/dist/chunk-XEBG3G53.js.map +1 -0
- package/dist/chunk-YLEMGEFH.js +246 -0
- package/dist/chunk-YLEMGEFH.js.map +1 -0
- package/dist/chunk-ZEJR4TKL.cjs +173 -0
- package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
- package/dist/core/index.cjs +130 -431
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +331 -17
- package/dist/core/index.d.ts +331 -17
- package/dist/core/index.js +71 -13
- package/dist/{function-2bJW1Zp_.d.ts → function-BtiZb9i9.d.ts} +28 -7
- package/dist/{function-r2sh-jOO.d.cts → function-DNugAOrt.d.cts} +28 -7
- package/dist/generate/index.cjs +30 -3751
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +373 -59
- package/dist/generate/index.d.ts +373 -59
- package/dist/generate/index.js +23 -9
- package/dist/{index-DZ1M21do.d.cts → index-6VR8-kit.d.ts} +20 -4
- package/dist/index-70p168VM.d.ts +192 -0
- package/dist/{index-D_ZTUNKp.d.cts → index-Be74mcon.d.cts} +18 -17
- package/dist/index-BtXT6UQ0.d.cts +192 -0
- package/dist/{index-BMPxwjwV.d.ts → index-C5UBbQ4G.d.ts} +18 -17
- package/dist/{index-BOBXBNwP.d.cts → index-C83mZI1P.d.ts} +93 -8
- package/dist/{index-BdeQ1r_p.d.cts → index-D6b6na69.d.cts} +14 -11
- package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
- package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
- package/dist/index-OG65ocOr.d.cts +272 -0
- package/dist/index-OG65ocOr.d.ts +272 -0
- package/dist/{index-BreoJRw0.d.ts → index-tJhkmQgE.d.cts} +20 -4
- package/dist/{index-CP3wAfEH.d.ts → index-tSQh-xwY.d.ts} +14 -11
- package/dist/{index-CM5W4rcm.d.ts → index-y_ak9_Hi.d.cts} +93 -8
- package/dist/index.cjs +419 -6848
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -13
- package/dist/index.d.ts +14 -13
- package/dist/index.js +132 -27
- package/dist/model/index.cjs +8 -2423
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +433 -74
- package/dist/model/index.d.ts +433 -74
- package/dist/model/index.js +6 -7
- package/dist/styled-DIRkk9Uz.d.ts +506 -0
- package/dist/styled-DJbkJQfj.d.cts +506 -0
- package/dist/theory/index.cjs +172 -1676
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +151 -233
- package/dist/theory/index.d.ts +151 -233
- package/dist/theory/index.js +31 -7
- package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
- package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
- package/package.json +73 -24
- package/dist/chunk-5HTNMQW4.js.map +0 -1
- package/dist/chunk-7Y7WF64D.js.map +0 -1
- package/dist/chunk-A4532ZFR.js.map +0 -1
- package/dist/chunk-BHUT2S5G.js +0 -22
- package/dist/chunk-BHUT2S5G.js.map +0 -1
- package/dist/chunk-CCBLVAPK.js +0 -42
- package/dist/chunk-CCBLVAPK.js.map +0 -1
- package/dist/chunk-COKTX2G5.js.map +0 -1
- package/dist/chunk-DYWFNC4A.js +0 -152
- package/dist/chunk-DYWFNC4A.js.map +0 -1
- package/dist/chunk-GQKN5KDT.js.map +0 -1
- package/dist/chunk-LN7ZZMD2.js.map +0 -1
- package/dist/chunk-MSWFZF7J.js +0 -118
- package/dist/chunk-MSWFZF7J.js.map +0 -1
- package/dist/chunk-R3S2LUBH.js.map +0 -1
- package/dist/chunk-SDM3VJJ5.js +0 -41
- package/dist/chunk-SDM3VJJ5.js.map +0 -1
- package/dist/chunk-XTC3X6MS.js.map +0 -1
- package/dist/chunk-ZXW5YIWK.js +0 -55
- package/dist/chunk-ZXW5YIWK.js.map +0 -1
- package/dist/index-CGMZNCx6.d.cts +0 -140
- package/dist/index-CGMZNCx6.d.ts +0 -140
- package/dist/styled-BjXtOFNk.d.cts +0 -282
- package/dist/styled-D7BAx3S3.d.ts +0 -282
- package/src/analyze/arrange/index.ts +0 -21
- package/src/analyze/arrange/internal.ts +0 -125
- package/src/analyze/arrange/tension.ts +0 -187
- package/src/analyze/arrange/tracks.ts +0 -279
- package/src/analyze/detect/index.ts +0 -235
- package/src/analyze/functional/borrowed.ts +0 -84
- package/src/analyze/functional/cadence.ts +0 -66
- package/src/analyze/functional/function.ts +0 -184
- package/src/analyze/functional/index.ts +0 -23
- package/src/analyze/functional/internal.ts +0 -29
- package/src/analyze/functional/roman.ts +0 -393
- package/src/analyze/index.ts +0 -49
- package/src/analyze/timeline/index.ts +0 -415
- package/src/analyze/voice/index.ts +0 -330
- package/src/core/index.ts +0 -47
- package/src/core/interval/index.ts +0 -81
- package/src/core/meter/index.ts +0 -282
- package/src/core/pitch/index.ts +0 -311
- package/src/core/random/index.ts +0 -53
- package/src/core/tuning/index.ts +0 -156
- package/src/core/types.ts +0 -29
- package/src/generate/bass/index.ts +0 -350
- package/src/generate/countermelody/index.ts +0 -379
- package/src/generate/drums/beat.ts +0 -266
- package/src/generate/drums/euclid.ts +0 -58
- package/src/generate/drums/fills.ts +0 -356
- package/src/generate/drums/ghost.ts +0 -119
- package/src/generate/drums/hihat.ts +0 -285
- package/src/generate/drums/hit.ts +0 -31
- package/src/generate/drums/index.ts +0 -329
- package/src/generate/drums/internal.ts +0 -257
- package/src/generate/drums/kick.ts +0 -160
- package/src/generate/drums/percussion.ts +0 -121
- package/src/generate/drums/rng.ts +0 -7
- package/src/generate/drums/swing.ts +0 -71
- package/src/generate/groove/index.ts +0 -326
- package/src/generate/harmonize/index.ts +0 -440
- package/src/generate/index.ts +0 -59
- package/src/generate/motif/index.ts +0 -398
- package/src/generate/progression/index.ts +0 -370
- package/src/generate/reharmony/index.ts +0 -330
- package/src/generate/rhythm/index.ts +0 -165
- package/src/index.ts +0 -247
- package/src/model/chord.ts +0 -423
- package/src/model/index.ts +0 -15
- package/src/model/interval.ts +0 -82
- package/src/model/key.ts +0 -266
- package/src/model/note.ts +0 -176
- package/src/model/progression.ts +0 -161
- package/src/model/shared.ts +0 -12
- package/src/theory/chord/index.ts +0 -373
- package/src/theory/chordscale/index.ts +0 -374
- package/src/theory/counterpoint/index.ts +0 -230
- package/src/theory/harmony/index.ts +0 -92
- package/src/theory/index.ts +0 -107
- package/src/theory/safety/index.ts +0 -419
- package/src/theory/scale/degrees.ts +0 -123
- package/src/theory/scale/index.ts +0 -28
- package/src/theory/scale/key.ts +0 -56
- package/src/theory/scale/masks.ts +0 -153
- package/src/theory/spelling/index.ts +0 -182
- package/src/theory/symbol/index.ts +0 -268
- package/src/theory/voicing/index.ts +0 -5
- package/src/theory/voicing/internal.ts +0 -168
- package/src/theory/voicing/leading.ts +0 -112
- package/src/theory/voicing/satb.ts +0 -195
- package/src/theory/voicing/styled.ts +0 -176
- package/tsconfig.json +0 -23
|
@@ -1,26 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
formatNote,
|
|
3
|
-
midiToNote,
|
|
4
|
-
noteToPitchClass,
|
|
5
|
-
parseNote
|
|
6
|
-
} from "./chunk-DYWFNC4A.js";
|
|
7
1
|
import {
|
|
8
2
|
NAMED_SCALES,
|
|
9
3
|
chordPitchClasses,
|
|
10
4
|
chordToneRole,
|
|
11
5
|
createsHiddenParallelPerfect,
|
|
12
6
|
createsParallelPerfect,
|
|
13
|
-
createsVoiceCrossing,
|
|
14
7
|
createsVoiceOverlap,
|
|
15
|
-
exceedsSpacing,
|
|
16
8
|
makeChord,
|
|
9
|
+
namedScaleMask,
|
|
10
|
+
requireScaleMask,
|
|
17
11
|
scaleTonesInDegreeOrder
|
|
18
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-D2FAJBYL.js";
|
|
13
|
+
import {
|
|
14
|
+
InvalidInputError,
|
|
15
|
+
NoSolutionError,
|
|
16
|
+
assertFiniteNumber,
|
|
17
|
+
assertGenerationBudget,
|
|
18
|
+
assertInteger,
|
|
19
|
+
assertPositiveInt,
|
|
20
|
+
diatonicLetterOf,
|
|
21
|
+
formatNote,
|
|
22
|
+
midiToNote,
|
|
23
|
+
naturalPitchClassOf,
|
|
24
|
+
noteToPitchClass,
|
|
25
|
+
parseNote,
|
|
26
|
+
pitchClassOf
|
|
27
|
+
} from "./chunk-XEBG3G53.js";
|
|
19
28
|
|
|
20
29
|
// src/theory/chordscale/index.ts
|
|
21
|
-
function pitchClass(value) {
|
|
22
|
-
return (Math.trunc(value) % 12 + 12) % 12;
|
|
23
|
-
}
|
|
24
30
|
function popcount12(mask) {
|
|
25
31
|
let count = 0;
|
|
26
32
|
for (let n = 0; n < 12; n += 1) {
|
|
@@ -29,7 +35,7 @@ function popcount12(mask) {
|
|
|
29
35
|
return count;
|
|
30
36
|
}
|
|
31
37
|
function maskHasPitchClass(mask, scaleRootPc, pc) {
|
|
32
|
-
const offset =
|
|
38
|
+
const offset = pitchClassOf(pc - scaleRootPc);
|
|
33
39
|
return (mask >> offset & 1) === 1;
|
|
34
40
|
}
|
|
35
41
|
function scaleMatchesChord(chordPcs, scaleMask, scaleRootPc) {
|
|
@@ -42,16 +48,31 @@ function scaleMatchesChord(chordPcs, scaleMask, scaleRootPc) {
|
|
|
42
48
|
}
|
|
43
49
|
var ALIASED_SCALE_NAMES = /* @__PURE__ */ new Set(["major", "naturalMinor"]);
|
|
44
50
|
var HEPTATONIC_SIZE = 7;
|
|
51
|
+
var SCALE_PREFERENCE = [
|
|
52
|
+
"ionian",
|
|
53
|
+
"major",
|
|
54
|
+
"mixolydian",
|
|
55
|
+
"dorian",
|
|
56
|
+
"lydian",
|
|
57
|
+
"aeolian",
|
|
58
|
+
"naturalMinor",
|
|
59
|
+
"phrygian",
|
|
60
|
+
"locrian"
|
|
61
|
+
];
|
|
62
|
+
function conventionalRank(name) {
|
|
63
|
+
const index = SCALE_PREFERENCE.indexOf(name);
|
|
64
|
+
return index < 0 ? SCALE_PREFERENCE.length : index;
|
|
65
|
+
}
|
|
45
66
|
function chordScales(chord) {
|
|
46
67
|
const chordPcs = chordPitchClasses(chord);
|
|
47
|
-
const rootPc =
|
|
68
|
+
const rootPc = pitchClassOf(chord.rootPc);
|
|
48
69
|
const ranked = [];
|
|
49
70
|
const seenMasks = /* @__PURE__ */ new Set();
|
|
50
71
|
for (const name of Object.keys(NAMED_SCALES)) {
|
|
51
72
|
if (name === "chromatic" || ALIASED_SCALE_NAMES.has(name)) {
|
|
52
73
|
continue;
|
|
53
74
|
}
|
|
54
|
-
const mask =
|
|
75
|
+
const mask = namedScaleMask(name);
|
|
55
76
|
if (mask === void 0 || seenMasks.has(mask)) {
|
|
56
77
|
continue;
|
|
57
78
|
}
|
|
@@ -78,16 +99,18 @@ function chordScales(chord) {
|
|
|
78
99
|
if (a.size !== b.size) {
|
|
79
100
|
return a.size - b.size;
|
|
80
101
|
}
|
|
102
|
+
const rankA = conventionalRank(a.name);
|
|
103
|
+
const rankB = conventionalRank(b.name);
|
|
104
|
+
if (rankA !== rankB) {
|
|
105
|
+
return rankA - rankB;
|
|
106
|
+
}
|
|
81
107
|
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
82
108
|
});
|
|
83
109
|
return ranked.map((entry) => ({ name: entry.name, rootPc }));
|
|
84
110
|
}
|
|
85
111
|
function avoidNotes(chord, scaleName) {
|
|
86
|
-
const mask =
|
|
87
|
-
|
|
88
|
-
return [];
|
|
89
|
-
}
|
|
90
|
-
const rootPc = pitchClass(chord.rootPc);
|
|
112
|
+
const mask = requireScaleMask(scaleName);
|
|
113
|
+
const rootPc = pitchClassOf(chord.rootPc);
|
|
91
114
|
const chordPcs = chordPitchClasses(chord);
|
|
92
115
|
if (!scaleMatchesChord(chordPcs, mask, rootPc)) {
|
|
93
116
|
return [];
|
|
@@ -98,18 +121,15 @@ function avoidNotes(chord, scaleName) {
|
|
|
98
121
|
if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc)) {
|
|
99
122
|
continue;
|
|
100
123
|
}
|
|
101
|
-
if (chordSet.has(
|
|
124
|
+
if (chordSet.has(pitchClassOf(pc - 1))) {
|
|
102
125
|
avoid.push(pc);
|
|
103
126
|
}
|
|
104
127
|
}
|
|
105
128
|
return avoid.sort((a, b) => a - b);
|
|
106
129
|
}
|
|
107
130
|
function availableTensions(chord, scaleName) {
|
|
108
|
-
const mask =
|
|
109
|
-
|
|
110
|
-
return [];
|
|
111
|
-
}
|
|
112
|
-
const rootPc = pitchClass(chord.rootPc);
|
|
131
|
+
const mask = requireScaleMask(scaleName);
|
|
132
|
+
const rootPc = pitchClassOf(chord.rootPc);
|
|
113
133
|
const chordPcs = chordPitchClasses(chord);
|
|
114
134
|
if (!scaleMatchesChord(chordPcs, mask, rootPc)) {
|
|
115
135
|
return [];
|
|
@@ -137,14 +157,14 @@ function chordScaleReport(chord, limit) {
|
|
|
137
157
|
}
|
|
138
158
|
var RANK_PENALTY = 0.01;
|
|
139
159
|
function scalePitchClassSet(name, rootPc) {
|
|
140
|
-
const mask =
|
|
160
|
+
const mask = namedScaleMask(name);
|
|
141
161
|
const pcs = /* @__PURE__ */ new Set();
|
|
142
162
|
if (mask === void 0) {
|
|
143
163
|
return pcs;
|
|
144
164
|
}
|
|
145
165
|
for (let n = 0; n < 12; n += 1) {
|
|
146
166
|
if ((mask >> n & 1) === 1) {
|
|
147
|
-
pcs.add(
|
|
167
|
+
pcs.add(pitchClassOf(rootPc + n));
|
|
148
168
|
}
|
|
149
169
|
}
|
|
150
170
|
return pcs;
|
|
@@ -222,64 +242,117 @@ function scalesForChanges(chords) {
|
|
|
222
242
|
}
|
|
223
243
|
|
|
224
244
|
// src/theory/spelling/index.ts
|
|
225
|
-
var LETTER_SEMITONES = [0, 2, 4, 5, 7, 9, 11];
|
|
226
245
|
var CHROMATIC_SPELLING = {
|
|
227
|
-
1:
|
|
246
|
+
1: 1,
|
|
228
247
|
// b2
|
|
229
|
-
|
|
248
|
+
2: 1,
|
|
249
|
+
// M2
|
|
250
|
+
3: 2,
|
|
230
251
|
// b3
|
|
231
|
-
|
|
252
|
+
4: 2,
|
|
253
|
+
// M3
|
|
254
|
+
5: 3,
|
|
255
|
+
// P4
|
|
256
|
+
6: 3,
|
|
232
257
|
// #4
|
|
233
|
-
|
|
258
|
+
7: 4,
|
|
259
|
+
// P5
|
|
260
|
+
8: 5,
|
|
234
261
|
// b6
|
|
235
|
-
9:
|
|
262
|
+
9: 5,
|
|
236
263
|
// #6 (raised sixth of a minor key)
|
|
237
|
-
10:
|
|
264
|
+
10: 6,
|
|
238
265
|
// b7
|
|
239
|
-
11:
|
|
266
|
+
11: 6
|
|
240
267
|
// #7 (raised leading tone of a minor key)
|
|
241
268
|
};
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
269
|
+
var CHROMATIC_SPELLING_FLAT = {
|
|
270
|
+
...CHROMATIC_SPELLING,
|
|
271
|
+
6: 4
|
|
272
|
+
// b5
|
|
273
|
+
};
|
|
274
|
+
var CHROMATIC_SPELLING_SHARP = {
|
|
275
|
+
1: 0,
|
|
276
|
+
// #1
|
|
277
|
+
2: 1,
|
|
278
|
+
// M2
|
|
279
|
+
3: 1,
|
|
280
|
+
// #2
|
|
281
|
+
4: 2,
|
|
282
|
+
// M3
|
|
283
|
+
5: 3,
|
|
284
|
+
// P4
|
|
285
|
+
6: 3,
|
|
286
|
+
// #4
|
|
287
|
+
7: 4,
|
|
288
|
+
// P5
|
|
289
|
+
8: 4,
|
|
290
|
+
// #5
|
|
291
|
+
9: 5,
|
|
292
|
+
// M6
|
|
293
|
+
10: 5,
|
|
294
|
+
// #6
|
|
295
|
+
11: 6
|
|
296
|
+
// M7
|
|
297
|
+
};
|
|
251
298
|
function alterFor(letter, pc) {
|
|
252
|
-
let d =
|
|
299
|
+
let d = pitchClassOf(pc - naturalPitchClassOf(letter));
|
|
253
300
|
if (d > 6) {
|
|
254
301
|
d -= 12;
|
|
255
302
|
}
|
|
256
303
|
return d;
|
|
257
304
|
}
|
|
305
|
+
function letterFor(tonic, degreeOffset, pc) {
|
|
306
|
+
if (degreeOffset === void 0) {
|
|
307
|
+
return void 0;
|
|
308
|
+
}
|
|
309
|
+
const letter = diatonicLetterOf(tonic.letter + degreeOffset);
|
|
310
|
+
return { letter, alter: alterFor(letter, pc) };
|
|
311
|
+
}
|
|
312
|
+
function pickSpelling(flat, sharp, preferFlat) {
|
|
313
|
+
if (flat === void 0 || sharp === void 0) {
|
|
314
|
+
return flat ?? sharp;
|
|
315
|
+
}
|
|
316
|
+
if (Math.abs(flat.alter) !== Math.abs(sharp.alter)) {
|
|
317
|
+
return Math.abs(flat.alter) < Math.abs(sharp.alter) ? flat : sharp;
|
|
318
|
+
}
|
|
319
|
+
return preferFlat ? flat : sharp;
|
|
320
|
+
}
|
|
258
321
|
function isHeptatonic(key) {
|
|
259
322
|
return scaleTonesInDegreeOrder(key).length === 7;
|
|
260
323
|
}
|
|
261
|
-
function
|
|
262
|
-
const tonicPc = mod12(naturalPc(tonic.letter) + tonic.alter);
|
|
263
|
-
const offset = mod12(pc - tonicPc);
|
|
324
|
+
function hasMinorThird(key) {
|
|
264
325
|
const tones = scaleTonesInDegreeOrder(key);
|
|
265
|
-
|
|
266
|
-
|
|
326
|
+
const root = pitchClassOf(key.rootPc);
|
|
327
|
+
return tones.includes(pitchClassOf(root + 3)) && !tones.includes(pitchClassOf(root + 4));
|
|
328
|
+
}
|
|
329
|
+
function spellPitchClass(pc, tonic, key) {
|
|
330
|
+
const tonicPc = pitchClassOf(naturalPitchClassOf(tonic.letter) + tonic.alter);
|
|
331
|
+
const offset = pitchClassOf(pc - tonicPc);
|
|
332
|
+
if (offset === 0) {
|
|
333
|
+
return { letter: diatonicLetterOf(tonic.letter), alter: tonic.alter };
|
|
334
|
+
}
|
|
335
|
+
const heptatonic = isHeptatonic(key);
|
|
336
|
+
if (heptatonic) {
|
|
337
|
+
const degree = scaleTonesInDegreeOrder(key).indexOf(pitchClassOf(pc));
|
|
267
338
|
if (degree >= 0) {
|
|
268
|
-
const letter =
|
|
339
|
+
const letter = diatonicLetterOf(tonic.letter + degree);
|
|
269
340
|
return { letter, alter: alterFor(letter, pc) };
|
|
270
341
|
}
|
|
271
|
-
const
|
|
272
|
-
if (
|
|
273
|
-
const letter =
|
|
342
|
+
const degreeOffset = CHROMATIC_SPELLING[offset];
|
|
343
|
+
if (degreeOffset !== void 0) {
|
|
344
|
+
const letter = diatonicLetterOf(tonic.letter + degreeOffset);
|
|
274
345
|
return { letter, alter: alterFor(letter, pc) };
|
|
275
346
|
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
347
|
+
} else {
|
|
348
|
+
const flat = letterFor(tonic, CHROMATIC_SPELLING_FLAT[offset], pc);
|
|
349
|
+
const sharp = letterFor(tonic, CHROMATIC_SPELLING_SHARP[offset], pc);
|
|
350
|
+
const best = pickSpelling(flat, sharp, tonic.alter < 0 || hasMinorThird(key));
|
|
351
|
+
if (best !== void 0) {
|
|
352
|
+
return best;
|
|
280
353
|
}
|
|
281
354
|
}
|
|
282
|
-
const belowLetter =
|
|
355
|
+
const belowLetter = diatonicLetterOf([0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6][pitchClassOf(pc)] ?? 0);
|
|
283
356
|
return { letter: belowLetter, alter: alterFor(belowLetter, pc) };
|
|
284
357
|
}
|
|
285
358
|
function spellScale(tonic, key) {
|
|
@@ -288,10 +361,71 @@ function spellScale(tonic, key) {
|
|
|
288
361
|
function spellPitchClasses(pcs, tonic, key) {
|
|
289
362
|
return pcs.map((pc) => spellPitchClass(pc, tonic, key));
|
|
290
363
|
}
|
|
364
|
+
function chordLetterOffset(interval, chord) {
|
|
365
|
+
switch (interval) {
|
|
366
|
+
case 0:
|
|
367
|
+
case 12:
|
|
368
|
+
return 0;
|
|
369
|
+
case 1:
|
|
370
|
+
case 2:
|
|
371
|
+
case 13:
|
|
372
|
+
case 14:
|
|
373
|
+
return 1;
|
|
374
|
+
case 3:
|
|
375
|
+
case 4:
|
|
376
|
+
case 16:
|
|
377
|
+
return 2;
|
|
378
|
+
case 15:
|
|
379
|
+
return chord.intervals.some((i) => i === 3 || i === 4) ? 1 : 2;
|
|
380
|
+
case 5:
|
|
381
|
+
case 17:
|
|
382
|
+
case 18:
|
|
383
|
+
return 3;
|
|
384
|
+
case 6:
|
|
385
|
+
case 7:
|
|
386
|
+
case 8:
|
|
387
|
+
case 19:
|
|
388
|
+
return 4;
|
|
389
|
+
case 9:
|
|
390
|
+
return chord.quality === "dim7" ? 6 : 5;
|
|
391
|
+
case 20:
|
|
392
|
+
case 21:
|
|
393
|
+
return 5;
|
|
394
|
+
case 10:
|
|
395
|
+
case 11:
|
|
396
|
+
case 22:
|
|
397
|
+
case 23:
|
|
398
|
+
return 6;
|
|
399
|
+
default: {
|
|
400
|
+
const octaves = Math.floor(Math.max(0, interval) / 12);
|
|
401
|
+
const simple = pitchClassOf(interval);
|
|
402
|
+
const simpleOffset = [0, 1, 1, 2, 2, 3, 4, 4, 4, 5, 6, 6][simple] ?? 0;
|
|
403
|
+
return octaves * 7 + simpleOffset;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function spellChordFromRoot(chord, root) {
|
|
408
|
+
return chord.intervals.map((interval) => {
|
|
409
|
+
const letter = diatonicLetterOf(root.letter + chordLetterOffset(interval, chord));
|
|
410
|
+
const pc = pitchClassOf(chord.rootPc + interval);
|
|
411
|
+
return { letter, alter: alterFor(letter, pc) };
|
|
412
|
+
});
|
|
413
|
+
}
|
|
291
414
|
function spellChord(chord, tonic, key) {
|
|
292
|
-
return chord
|
|
293
|
-
|
|
294
|
-
|
|
415
|
+
return spellChordFromRoot(chord, chordRootSpelling(chord, tonic, key));
|
|
416
|
+
}
|
|
417
|
+
function chordRootSpelling(chord, tonic, key) {
|
|
418
|
+
const hint = chord.rootSpelling;
|
|
419
|
+
if (hint !== void 0 && pitchClassOf(naturalPitchClassOf(hint.letter) + hint.alter) === pitchClassOf(chord.rootPc)) {
|
|
420
|
+
return { letter: diatonicLetterOf(hint.letter), alter: hint.alter };
|
|
421
|
+
}
|
|
422
|
+
return spellPitchClass(chord.rootPc, tonic, key);
|
|
423
|
+
}
|
|
424
|
+
function spellPitch(pitch, tonic, key) {
|
|
425
|
+
const rounded = Math.round(pitch);
|
|
426
|
+
const spelled = spellPitchClass(pitchClassOf(rounded), tonic, key);
|
|
427
|
+
const octave = (rounded - naturalPitchClassOf(spelled.letter) - spelled.alter) / 12 - 1;
|
|
428
|
+
return { letter: spelled.letter, alter: spelled.alter, octave };
|
|
295
429
|
}
|
|
296
430
|
function noteNames(notes) {
|
|
297
431
|
return notes.map(formatNote);
|
|
@@ -357,7 +491,36 @@ var QUALITY_MAP = {
|
|
|
357
491
|
"7b13": "7b13",
|
|
358
492
|
"11": "11",
|
|
359
493
|
"13": "13",
|
|
360
|
-
"5": "5"
|
|
494
|
+
"5": "5",
|
|
495
|
+
"7sus4": "7sus4",
|
|
496
|
+
"7sus": "7sus4",
|
|
497
|
+
"9sus4": "11",
|
|
498
|
+
"9sus": "11",
|
|
499
|
+
"7b5": "7b5",
|
|
500
|
+
"7alt": "7alt",
|
|
501
|
+
alt: "7alt",
|
|
502
|
+
"13b9": "13b9",
|
|
503
|
+
maj13: "maj13",
|
|
504
|
+
M13: "maj13",
|
|
505
|
+
"maj7#11": "maj7#11",
|
|
506
|
+
"M7#11": "maj7#11",
|
|
507
|
+
m11: "min11",
|
|
508
|
+
min11: "min11",
|
|
509
|
+
m13: "min13",
|
|
510
|
+
min13: "min13",
|
|
511
|
+
madd9: "minAdd9",
|
|
512
|
+
"min(add9)": "minAdd9",
|
|
513
|
+
"m(add9)": "minAdd9",
|
|
514
|
+
"6add9": "6/9",
|
|
515
|
+
"m6/9": "min6/9",
|
|
516
|
+
m69: "min6/9",
|
|
517
|
+
"min6/9": "min6/9",
|
|
518
|
+
// ASCII stand-ins for the degree and half-diminished glyphs, which most
|
|
519
|
+
// lead sheets and chord-chart text files use instead of the typographic ones.
|
|
520
|
+
o: "dim",
|
|
521
|
+
o7: "dim7",
|
|
522
|
+
h: "m7b5",
|
|
523
|
+
h7: "m7b5"
|
|
361
524
|
};
|
|
362
525
|
var CANONICAL_SUFFIX = {
|
|
363
526
|
maj: "",
|
|
@@ -389,10 +552,20 @@ var CANONICAL_SUFFIX = {
|
|
|
389
552
|
"7b13": "7b13",
|
|
390
553
|
"11": "11",
|
|
391
554
|
"13": "13",
|
|
392
|
-
"5": "5"
|
|
555
|
+
"5": "5",
|
|
556
|
+
"7sus4": "7sus4",
|
|
557
|
+
"7b5": "7b5",
|
|
558
|
+
"7alt": "7alt",
|
|
559
|
+
"13b9": "13b9",
|
|
560
|
+
maj13: "maj13",
|
|
561
|
+
"maj7#11": "maj7#11",
|
|
562
|
+
min11: "m11",
|
|
563
|
+
min13: "m13",
|
|
564
|
+
minAdd9: "madd9",
|
|
565
|
+
"min6/9": "m6/9"
|
|
393
566
|
};
|
|
394
|
-
function
|
|
395
|
-
return
|
|
567
|
+
function knownQuality(suffix) {
|
|
568
|
+
return Object.hasOwn(QUALITY_MAP, suffix) ? QUALITY_MAP[suffix] : void 0;
|
|
396
569
|
}
|
|
397
570
|
function makeSpelledChord(root, quality, bass) {
|
|
398
571
|
const chord = makeChord(
|
|
@@ -410,7 +583,7 @@ function parseChordSymbol(text) {
|
|
|
410
583
|
const trimmed = text.trim();
|
|
411
584
|
const rootMatch = ROOT_RE.exec(trimmed);
|
|
412
585
|
if (!rootMatch) {
|
|
413
|
-
throw new
|
|
586
|
+
throw new InvalidInputError(`Invalid chord symbol: ${text}`);
|
|
414
587
|
}
|
|
415
588
|
const rootToken = rootMatch[0];
|
|
416
589
|
const rootNote = parseNote(rootToken);
|
|
@@ -419,39 +592,48 @@ function parseChordSymbol(text) {
|
|
|
419
592
|
if (slashIndex !== -1) {
|
|
420
593
|
const before = rest.slice(0, slashIndex);
|
|
421
594
|
const after = rest.slice(slashIndex + 1);
|
|
422
|
-
const beforeQuality =
|
|
595
|
+
const beforeQuality = knownQuality(before);
|
|
423
596
|
if (beforeQuality !== void 0 && BASS_RE.test(after)) {
|
|
424
597
|
return makeSpelledChord(rootNote, beforeQuality, parseNote(after));
|
|
425
598
|
}
|
|
426
599
|
}
|
|
427
|
-
const quality =
|
|
600
|
+
const quality = knownQuality(rest);
|
|
428
601
|
if (quality === void 0) {
|
|
429
|
-
throw new
|
|
602
|
+
throw new InvalidInputError(`Unrecognized chord quality: ${text}`);
|
|
430
603
|
}
|
|
431
604
|
return makeSpelledChord(rootNote, quality);
|
|
432
605
|
}
|
|
433
|
-
function pitchClassName(pc, hint, flats) {
|
|
606
|
+
function pitchClassName(pc, hint, flats, inheritFlats) {
|
|
434
607
|
if (flats === void 0 && hint !== void 0 && noteToPitchClass(hint) === pc) {
|
|
435
608
|
return formatNote(hint);
|
|
436
609
|
}
|
|
437
|
-
const note = midiToNote(60 + pc, flats ? "flat" : "sharp");
|
|
610
|
+
const note = midiToNote(60 + pc, flats ?? inheritFlats ? "flat" : "sharp");
|
|
438
611
|
return formatNote({ letter: note.letter, alter: note.alter });
|
|
439
612
|
}
|
|
440
613
|
function formatChordSymbol(chord, opts) {
|
|
441
|
-
|
|
442
|
-
|
|
614
|
+
assertFiniteNumber(chord.rootPc, "chord rootPc");
|
|
615
|
+
if (chord.bassPc !== void 0) {
|
|
616
|
+
assertFiniteNumber(chord.bassPc, "chord bassPc");
|
|
617
|
+
}
|
|
618
|
+
if (!Object.hasOwn(CANONICAL_SUFFIX, chord.quality)) {
|
|
619
|
+
throw new InvalidInputError(`Unknown chord quality: ${String(chord.quality)}`);
|
|
620
|
+
}
|
|
621
|
+
const rootPc = pitchClassOf(chord.rootPc);
|
|
622
|
+
const rootHint = chord.rootSpelling;
|
|
623
|
+
const rootName = pitchClassName(rootPc, rootHint, opts?.flats);
|
|
443
624
|
const suffix = CANONICAL_SUFFIX[chord.quality];
|
|
444
625
|
let symbol = `${rootName}${suffix}`;
|
|
445
|
-
if (chord.bassPc !== void 0 &&
|
|
446
|
-
|
|
626
|
+
if (chord.bassPc !== void 0 && pitchClassOf(chord.bassPc) !== rootPc) {
|
|
627
|
+
const inheritFlats = rootHint !== void 0 && noteToPitchClass(rootHint) === rootPc ? rootHint.alter < 0 : void 0;
|
|
628
|
+
symbol += `/${pitchClassName(pitchClassOf(chord.bassPc), chord.bassSpelling, opts?.flats, inheritFlats)}`;
|
|
447
629
|
}
|
|
448
630
|
return symbol;
|
|
449
631
|
}
|
|
450
632
|
function transposeChordSymbol(text, semitones, opts) {
|
|
451
633
|
const chord = parseChordSymbol(text);
|
|
452
|
-
const transposed = { ...chord, rootPc:
|
|
634
|
+
const transposed = { ...chord, rootPc: pitchClassOf(chord.rootPc + semitones) };
|
|
453
635
|
if (chord.bassPc !== void 0) {
|
|
454
|
-
transposed.bassPc =
|
|
636
|
+
transposed.bassPc = pitchClassOf(chord.bassPc + semitones);
|
|
455
637
|
}
|
|
456
638
|
return formatChordSymbol(transposed, opts);
|
|
457
639
|
}
|
|
@@ -460,15 +642,16 @@ function transposeChordSymbol(text, semitones, opts) {
|
|
|
460
642
|
var DEFAULT_MAX_SPACING = 12;
|
|
461
643
|
var VIOLATION_PENALTY = 1e3;
|
|
462
644
|
var MISSING_TONE_PENALTY = 500;
|
|
645
|
+
var MISSING_FIFTH_PENALTY = 20;
|
|
463
646
|
var POOR_DOUBLING_PENALTY = 4;
|
|
647
|
+
var LEADING_TONE_DOUBLING_PENALTY = 200;
|
|
648
|
+
var RESOLUTION_PENALTY = 40;
|
|
464
649
|
var MAX_CANDIDATES = 4e3;
|
|
465
|
-
|
|
466
|
-
return (Math.trunc(value) % 12 + 12) % 12;
|
|
467
|
-
}
|
|
650
|
+
var MAX_SEARCH_NODES = MAX_CANDIDATES * 16;
|
|
468
651
|
function pitchesForPc(pc, range) {
|
|
469
652
|
const result = [];
|
|
470
653
|
for (let midi = Math.ceil(range.min); midi <= range.max; midi += 1) {
|
|
471
|
-
if (
|
|
654
|
+
if (pitchClassOf(midi) === pc) {
|
|
472
655
|
result.push(midi);
|
|
473
656
|
}
|
|
474
657
|
}
|
|
@@ -481,65 +664,107 @@ function pitchesForPc(pc, range) {
|
|
|
481
664
|
}
|
|
482
665
|
function enumerateVoicings(chord, ranges, maxSpacing) {
|
|
483
666
|
const chordPcs = chordPitchClasses(chord);
|
|
484
|
-
const bassPc =
|
|
667
|
+
const bassPc = pitchClassOf(chord.bassPc ?? chord.rootPc);
|
|
668
|
+
const byVoice = ranges.map(
|
|
669
|
+
(range, voice) => (voice === 0 ? [bassPc] : chordPcs).flatMap((pc) => pitchesForPc(pc, range))
|
|
670
|
+
);
|
|
671
|
+
for (let voice = 0; voice < byVoice.length; voice += 1) {
|
|
672
|
+
if ((byVoice[voice] ?? []).length === 0) {
|
|
673
|
+
throw new NoSolutionError("no voicing satisfies the given ranges");
|
|
674
|
+
}
|
|
675
|
+
}
|
|
485
676
|
const results = [];
|
|
486
677
|
const current = [];
|
|
678
|
+
let nodes = 0;
|
|
487
679
|
const build = (voice) => {
|
|
488
|
-
|
|
680
|
+
nodes += 1;
|
|
681
|
+
if (results.length >= MAX_CANDIDATES || nodes > MAX_SEARCH_NODES) {
|
|
489
682
|
return;
|
|
490
683
|
}
|
|
491
684
|
if (voice === ranges.length) {
|
|
492
685
|
results.push([...current]);
|
|
493
686
|
return;
|
|
494
687
|
}
|
|
495
|
-
const range = ranges[voice];
|
|
496
|
-
if (range === void 0) {
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
const pcs = voice === 0 ? [bassPc] : chordPcs;
|
|
500
688
|
const prev = current[voice - 1];
|
|
501
|
-
for (const
|
|
502
|
-
|
|
503
|
-
if (
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
continue;
|
|
510
|
-
}
|
|
689
|
+
for (const pitch of byVoice[voice] ?? []) {
|
|
690
|
+
if (prev !== void 0) {
|
|
691
|
+
if (pitch < prev) {
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
const spacingLimit = voice === 1 ? maxSpacing + 12 : maxSpacing;
|
|
695
|
+
if (pitch - prev > spacingLimit) {
|
|
696
|
+
continue;
|
|
511
697
|
}
|
|
512
|
-
current.push(pitch);
|
|
513
|
-
build(voice + 1);
|
|
514
|
-
current.pop();
|
|
515
698
|
}
|
|
699
|
+
current.push(pitch);
|
|
700
|
+
build(voice + 1);
|
|
701
|
+
current.pop();
|
|
516
702
|
}
|
|
517
703
|
};
|
|
518
704
|
build(0);
|
|
519
705
|
return results;
|
|
520
706
|
}
|
|
521
|
-
function structuralPenalty(pitches, chord) {
|
|
707
|
+
function structuralPenalty(pitches, chord, key) {
|
|
522
708
|
const counts = /* @__PURE__ */ new Map();
|
|
523
709
|
for (const pitch of pitches) {
|
|
524
|
-
const pc =
|
|
710
|
+
const pc = pitchClassOf(pitch);
|
|
525
711
|
counts.set(pc, (counts.get(pc) ?? 0) + 1);
|
|
526
712
|
}
|
|
527
713
|
let penalty = 0;
|
|
528
714
|
for (const pc of chordPitchClasses(chord)) {
|
|
529
715
|
if (!counts.has(pc)) {
|
|
530
|
-
penalty += MISSING_TONE_PENALTY;
|
|
716
|
+
penalty += chordToneRole(pc, chord) === "fifth" ? MISSING_FIFTH_PENALTY : MISSING_TONE_PENALTY;
|
|
531
717
|
}
|
|
532
718
|
}
|
|
533
|
-
const rootPc =
|
|
534
|
-
const
|
|
719
|
+
const rootPc = pitchClassOf(chord.rootPc);
|
|
720
|
+
const leadingTonePc = key === void 0 ? void 0 : pitchClassOf(key.rootPc - 1);
|
|
535
721
|
for (const [pc, count] of counts) {
|
|
536
|
-
if (count
|
|
722
|
+
if (count <= 1) {
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
if (pc === leadingTonePc) {
|
|
726
|
+
penalty += (count - 1) * LEADING_TONE_DOUBLING_PENALTY;
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
if (pc !== rootPc && chordToneRole(pc, chord) !== "fifth") {
|
|
537
730
|
penalty += (count - 1) * POOR_DOUBLING_PENALTY;
|
|
538
731
|
}
|
|
539
732
|
}
|
|
540
733
|
return penalty;
|
|
541
734
|
}
|
|
542
|
-
function
|
|
735
|
+
function seventhPcOf(chord) {
|
|
736
|
+
for (const interval of chord.intervals) {
|
|
737
|
+
const pc = pitchClassOf(chord.rootPc + interval);
|
|
738
|
+
if (chordToneRole(pc, chord) === "seventh") {
|
|
739
|
+
return pc;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return void 0;
|
|
743
|
+
}
|
|
744
|
+
function resolutionViolations(prev, cur, prevChord, nextChord, key) {
|
|
745
|
+
const seventhPc = seventhPcOf(prevChord);
|
|
746
|
+
const leadingTonePc = key === void 0 ? void 0 : pitchClassOf(key.rootPc - 1);
|
|
747
|
+
const tonicPc = key === void 0 ? void 0 : pitchClassOf(key.rootPc);
|
|
748
|
+
const nextPcs = new Set(chordPitchClasses(nextChord));
|
|
749
|
+
let count = 0;
|
|
750
|
+
for (let voice = 0; voice < prev.length && voice < cur.length; voice += 1) {
|
|
751
|
+
const from = prev[voice];
|
|
752
|
+
const to = cur[voice];
|
|
753
|
+
if (from === void 0 || to === void 0) {
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
const fromPc = pitchClassOf(from);
|
|
757
|
+
const motion = to - from;
|
|
758
|
+
if (fromPc === seventhPc && !nextPcs.has(fromPc) && motion !== -1 && motion !== -2) {
|
|
759
|
+
count += 1;
|
|
760
|
+
}
|
|
761
|
+
if (fromPc === leadingTonePc && tonicPc !== void 0 && nextPcs.has(tonicPc) && !nextPcs.has(fromPc) && motion !== 1) {
|
|
762
|
+
count += 1;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return count;
|
|
766
|
+
}
|
|
767
|
+
function violationCount(prev, cur) {
|
|
543
768
|
let count = 0;
|
|
544
769
|
for (let lower = 0; lower < cur.length; lower += 1) {
|
|
545
770
|
for (let upper = lower + 1; upper < cur.length; upper += 1) {
|
|
@@ -553,16 +778,10 @@ function violationCount(prev, cur, maxSpacing) {
|
|
|
553
778
|
if (createsParallelPerfect(prevUpper, curUpper, prevLower, curLower)) {
|
|
554
779
|
count += 1;
|
|
555
780
|
}
|
|
556
|
-
if (createsVoiceCrossing(curUpper, curLower)) {
|
|
557
|
-
count += 1;
|
|
558
|
-
}
|
|
559
781
|
if (upper === lower + 1) {
|
|
560
782
|
if (createsVoiceOverlap(prevUpper, curUpper, prevLower, curLower)) {
|
|
561
783
|
count += 1;
|
|
562
784
|
}
|
|
563
|
-
if (lower > 0 && exceedsSpacing(curUpper, curLower, maxSpacing)) {
|
|
564
|
-
count += 1;
|
|
565
|
-
}
|
|
566
785
|
}
|
|
567
786
|
}
|
|
568
787
|
}
|
|
@@ -596,48 +815,68 @@ function voiceLeadingCost(from, to) {
|
|
|
596
815
|
return total;
|
|
597
816
|
}
|
|
598
817
|
function nextVoicing(current, chord, opts) {
|
|
599
|
-
const
|
|
818
|
+
const derived = opts?.ranges === void 0 && opts?.voices === void 0;
|
|
819
|
+
if (derived) {
|
|
820
|
+
if (current.length === 0) {
|
|
821
|
+
throw new InvalidInputError("current must contain at least one pitch");
|
|
822
|
+
}
|
|
823
|
+
for (let index = 0; index < current.length; index += 1) {
|
|
824
|
+
assertFiniteNumber(current[index] ?? Number.NaN, `current[${index}]`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
const ranges = derived ? current.map((pitch) => {
|
|
600
828
|
const centre = Math.min(127, Math.max(0, pitch));
|
|
601
829
|
return { min: Math.max(0, centre - 12), max: Math.min(127, centre + 12) };
|
|
602
|
-
});
|
|
603
|
-
const maxSpacing = opts
|
|
830
|
+
}) : resolveRanges(opts);
|
|
831
|
+
const maxSpacing = resolveMaxSpacing(opts);
|
|
604
832
|
const candidates = enumerateVoicings(chord, ranges, maxSpacing);
|
|
605
833
|
let best;
|
|
606
834
|
let bestScore = Number.POSITIVE_INFINITY;
|
|
607
835
|
for (const candidate of candidates) {
|
|
608
|
-
const score = structuralPenalty(candidate, chord) + voiceLeadingCost(current, candidate) + VIOLATION_PENALTY * violationCount(current, candidate
|
|
836
|
+
const score = structuralPenalty(candidate, chord, opts?.key) + voiceLeadingCost(current, candidate) + VIOLATION_PENALTY * violationCount(current, candidate);
|
|
609
837
|
if (score < bestScore) {
|
|
610
838
|
bestScore = score;
|
|
611
839
|
best = candidate;
|
|
612
840
|
}
|
|
613
841
|
}
|
|
614
842
|
if (best === void 0) {
|
|
615
|
-
throw new
|
|
843
|
+
throw new NoSolutionError("no voicing satisfies the given ranges");
|
|
616
844
|
}
|
|
617
845
|
return best;
|
|
618
846
|
}
|
|
619
847
|
|
|
620
848
|
// 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
|
-
];
|
|
849
|
+
var SATB_RANGES = Object.freeze([
|
|
850
|
+
Object.freeze({ min: 40, max: 60 }),
|
|
851
|
+
Object.freeze({ min: 48, max: 67 }),
|
|
852
|
+
Object.freeze({ min: 55, max: 74 }),
|
|
853
|
+
Object.freeze({ min: 60, max: 79 })
|
|
854
|
+
]);
|
|
627
855
|
var DERIVED_LOW = 40;
|
|
628
856
|
var DERIVED_HIGH = 79;
|
|
629
857
|
var DERIVED_SPAN = 19;
|
|
630
858
|
function resolveRanges(opts) {
|
|
631
859
|
if (opts?.ranges !== void 0) {
|
|
632
860
|
if (opts.ranges.length === 0) {
|
|
633
|
-
throw new
|
|
861
|
+
throw new InvalidInputError("ranges must contain at least one voice range");
|
|
634
862
|
}
|
|
635
|
-
|
|
863
|
+
assertGenerationBudget(opts.ranges.length, "voice ranges", 128);
|
|
864
|
+
return opts.ranges.map((range, index) => {
|
|
865
|
+
assertFiniteNumber(range.min, `ranges[${index}].min`);
|
|
866
|
+
assertFiniteNumber(range.max, `ranges[${index}].max`);
|
|
867
|
+
if (range.min > range.max) {
|
|
868
|
+
throw new InvalidInputError(`ranges[${index}].min must not exceed max`);
|
|
869
|
+
}
|
|
870
|
+
assertGenerationBudget(
|
|
871
|
+
Math.floor(range.max) - Math.ceil(range.min) + 1,
|
|
872
|
+
`ranges[${index}] span`,
|
|
873
|
+
4096
|
|
874
|
+
);
|
|
875
|
+
return { ...range };
|
|
876
|
+
});
|
|
636
877
|
}
|
|
637
878
|
const voices = opts?.voices ?? 4;
|
|
638
|
-
|
|
639
|
-
throw new Error("voices must be at least 1");
|
|
640
|
-
}
|
|
879
|
+
assertPositiveInt(voices, "voices", 128);
|
|
641
880
|
if (voices === 4) {
|
|
642
881
|
return SATB_RANGES.map((range) => ({ ...range }));
|
|
643
882
|
}
|
|
@@ -653,14 +892,22 @@ function resolveRanges(opts) {
|
|
|
653
892
|
}
|
|
654
893
|
return ranges;
|
|
655
894
|
}
|
|
895
|
+
function resolveMaxSpacing(opts) {
|
|
896
|
+
const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
|
|
897
|
+
assertFiniteNumber(maxSpacing, "maxSpacing");
|
|
898
|
+
if (maxSpacing < 0) {
|
|
899
|
+
throw new InvalidInputError("maxSpacing must be non-negative");
|
|
900
|
+
}
|
|
901
|
+
return maxSpacing;
|
|
902
|
+
}
|
|
656
903
|
function voiceChord(chord, opts) {
|
|
657
904
|
const ranges = resolveRanges(opts);
|
|
658
|
-
const maxSpacing = opts
|
|
905
|
+
const maxSpacing = resolveMaxSpacing(opts);
|
|
659
906
|
const candidates = enumerateVoicings(chord, ranges, maxSpacing);
|
|
660
907
|
let best;
|
|
661
908
|
let bestScore = Number.POSITIVE_INFINITY;
|
|
662
909
|
for (const candidate of candidates) {
|
|
663
|
-
let score = structuralPenalty(candidate, chord);
|
|
910
|
+
let score = structuralPenalty(candidate, chord, opts?.key);
|
|
664
911
|
for (let i = 0; i < candidate.length; i += 1) {
|
|
665
912
|
const pitch = candidate[i];
|
|
666
913
|
const range = ranges[i];
|
|
@@ -675,36 +922,63 @@ function voiceChord(chord, opts) {
|
|
|
675
922
|
}
|
|
676
923
|
}
|
|
677
924
|
if (best === void 0) {
|
|
678
|
-
throw new
|
|
925
|
+
throw new NoSolutionError(
|
|
926
|
+
`no voicing satisfies the given ranges for ${formatChordSymbol(chord)}`
|
|
927
|
+
);
|
|
679
928
|
}
|
|
680
929
|
return best;
|
|
681
930
|
}
|
|
931
|
+
function noSolutionAt(index, chord) {
|
|
932
|
+
return new NoSolutionError(
|
|
933
|
+
`no voicing satisfies the given ranges for ${formatChordSymbol(chord)} at index ${index}`,
|
|
934
|
+
{ at: index }
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
function locate(index, chord, work) {
|
|
938
|
+
try {
|
|
939
|
+
return work();
|
|
940
|
+
} catch (error) {
|
|
941
|
+
if (error instanceof NoSolutionError) {
|
|
942
|
+
throw noSolutionAt(index, chord);
|
|
943
|
+
}
|
|
944
|
+
throw error;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
682
947
|
function voiceProgression(chords, opts) {
|
|
948
|
+
assertGenerationBudget(chords.length, "voiced progression chords", opts?.budget);
|
|
683
949
|
const ranges = resolveRanges(opts);
|
|
684
|
-
const maxSpacing = opts
|
|
950
|
+
const maxSpacing = resolveMaxSpacing(opts);
|
|
685
951
|
const result = [];
|
|
686
952
|
let prev;
|
|
687
|
-
|
|
953
|
+
let prevChord;
|
|
954
|
+
for (let index = 0; index < chords.length; index += 1) {
|
|
955
|
+
const chord = chords[index];
|
|
956
|
+
if (chord === void 0) {
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
688
959
|
if (prev === void 0) {
|
|
689
|
-
prev = voiceChord(chord, opts);
|
|
960
|
+
prev = locate(index, chord, () => voiceChord(chord, opts));
|
|
961
|
+
prevChord = chord;
|
|
690
962
|
result.push(prev);
|
|
691
963
|
continue;
|
|
692
964
|
}
|
|
693
|
-
const candidates = enumerateVoicings(chord, ranges, maxSpacing);
|
|
965
|
+
const candidates = locate(index, chord, () => enumerateVoicings(chord, ranges, maxSpacing));
|
|
694
966
|
let best;
|
|
695
967
|
let bestScore = Number.POSITIVE_INFINITY;
|
|
696
968
|
for (const candidate of candidates) {
|
|
697
|
-
const
|
|
969
|
+
const unresolved = prevChord === void 0 ? 0 : resolutionViolations(prev, candidate, prevChord, chord, opts?.key);
|
|
970
|
+
const score = structuralPenalty(candidate, chord, opts?.key) + voiceLeadingCost(prev, candidate) + VIOLATION_PENALTY * violationCount(prev, candidate) + RESOLUTION_PENALTY * unresolved;
|
|
698
971
|
if (score < bestScore) {
|
|
699
972
|
bestScore = score;
|
|
700
973
|
best = candidate;
|
|
701
974
|
}
|
|
702
975
|
}
|
|
703
976
|
if (best === void 0) {
|
|
704
|
-
throw
|
|
977
|
+
throw noSolutionAt(index, chord);
|
|
705
978
|
}
|
|
706
979
|
result.push(best);
|
|
707
980
|
prev = best;
|
|
981
|
+
prevChord = chord;
|
|
708
982
|
}
|
|
709
983
|
return result;
|
|
710
984
|
}
|
|
@@ -723,7 +997,7 @@ function dedupePcs(pcs) {
|
|
|
723
997
|
return result;
|
|
724
998
|
}
|
|
725
999
|
function lowestPitchAtOrAbove(pc, floor) {
|
|
726
|
-
return floor + ((pc -
|
|
1000
|
+
return floor + ((pc - pitchClassOf(floor)) % 12 + 12) % 12;
|
|
727
1001
|
}
|
|
728
1002
|
function nearestPc(target, pcs) {
|
|
729
1003
|
let best = pcs[0] ?? target;
|
|
@@ -739,11 +1013,16 @@ function nearestPc(target, pcs) {
|
|
|
739
1013
|
}
|
|
740
1014
|
function voiceChordStyled(chord, opts) {
|
|
741
1015
|
const style = opts?.style ?? "close";
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
|
|
1016
|
+
const octave = opts?.octave ?? DEFAULT_STYLE_OCTAVE;
|
|
1017
|
+
assertInteger(octave, "styled voicing octave", -10, 10);
|
|
1018
|
+
if (opts?.topNote !== void 0) {
|
|
1019
|
+
assertFiniteNumber(opts.topNote, "styled voicing topNote");
|
|
1020
|
+
}
|
|
1021
|
+
const base = 12 * (octave + 1);
|
|
1022
|
+
const rootPc = pitchClassOf(chord.rootPc);
|
|
1023
|
+
const bassPc = pitchClassOf(chord.bassPc ?? chord.rootPc);
|
|
745
1024
|
const omitRoot = style === "rootless" || opts?.rootless === true;
|
|
746
|
-
let order = dedupePcs(chord.intervals.map((interval) =>
|
|
1025
|
+
let order = dedupePcs(chord.intervals.map((interval) => pitchClassOf(chord.rootPc + interval)));
|
|
747
1026
|
const bassIndex = order.indexOf(bassPc);
|
|
748
1027
|
if (bassIndex > 0) {
|
|
749
1028
|
order = [...order.slice(bassIndex), ...order.slice(0, bassIndex)];
|
|
@@ -755,17 +1034,17 @@ function voiceChordStyled(chord, opts) {
|
|
|
755
1034
|
const guide = roles.includes("seventh") ? "seventh" : roles.includes("sixth") ? "sixth" : "fifth";
|
|
756
1035
|
order = order.filter((_pc, i) => {
|
|
757
1036
|
const role = roles[i];
|
|
758
|
-
return role === "root" || role === "third" || role === guide;
|
|
1037
|
+
return _pc === bassPc || role === "root" || role === "third" || role === guide;
|
|
759
1038
|
});
|
|
760
1039
|
}
|
|
761
1040
|
if (omitRoot) {
|
|
762
|
-
order = order.filter((pc) => pc !== rootPc);
|
|
1041
|
+
order = order.filter((pc) => pc !== rootPc || chord.bassPc !== void 0 && pc === bassPc);
|
|
763
1042
|
}
|
|
764
1043
|
if (order.length === 0) {
|
|
765
1044
|
return [];
|
|
766
1045
|
}
|
|
767
1046
|
if (opts?.topNote !== void 0) {
|
|
768
|
-
const target = order.includes(
|
|
1047
|
+
const target = order.includes(pitchClassOf(opts.topNote)) ? pitchClassOf(opts.topNote) : nearestPc(pitchClassOf(opts.topNote), order);
|
|
769
1048
|
const targetIndex = order.indexOf(target);
|
|
770
1049
|
order = [...order.slice(targetIndex + 1), ...order.slice(0, targetIndex + 1)];
|
|
771
1050
|
}
|
|
@@ -775,7 +1054,7 @@ function voiceChordStyled(chord, opts) {
|
|
|
775
1054
|
if (prev === void 0) {
|
|
776
1055
|
prev = lowestPitchAtOrAbove(pc, base);
|
|
777
1056
|
} else {
|
|
778
|
-
let delta = ((pc -
|
|
1057
|
+
let delta = ((pc - pitchClassOf(prev)) % 12 + 12) % 12;
|
|
779
1058
|
if (delta === 0) {
|
|
780
1059
|
delta = 12;
|
|
781
1060
|
}
|
|
@@ -788,7 +1067,27 @@ function voiceChordStyled(chord, opts) {
|
|
|
788
1067
|
} else if (style === "drop3" && stack.length >= 3) {
|
|
789
1068
|
stack[stack.length - 3] = (stack[stack.length - 3] ?? 0) - 12;
|
|
790
1069
|
}
|
|
791
|
-
|
|
1070
|
+
if (chord.bassPc !== void 0) {
|
|
1071
|
+
const bassIndexInStack = stack.findIndex((pitch) => pitchClassOf(pitch) === bassPc);
|
|
1072
|
+
if (bassIndexInStack >= 0) {
|
|
1073
|
+
const otherLowest = Math.min(
|
|
1074
|
+
...stack.filter((_pitch, index) => index !== bassIndexInStack),
|
|
1075
|
+
Number.POSITIVE_INFINITY
|
|
1076
|
+
);
|
|
1077
|
+
while ((stack[bassIndexInStack] ?? 0) >= otherLowest) {
|
|
1078
|
+
stack[bassIndexInStack] = (stack[bassIndexInStack] ?? 0) - 12;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
const voicing = stack.sort((a, b) => a - b);
|
|
1083
|
+
const lowest = voicing[0] ?? 0;
|
|
1084
|
+
const highest = voicing[voicing.length - 1] ?? 0;
|
|
1085
|
+
if (lowest < 0 || highest > 127) {
|
|
1086
|
+
throw new InvalidInputError(
|
|
1087
|
+
`styled voicing at octave ${octave} spans MIDI ${lowest}..${highest}, outside 0..127`
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
return voicing;
|
|
792
1091
|
}
|
|
793
1092
|
|
|
794
1093
|
export {
|
|
@@ -801,7 +1100,9 @@ export {
|
|
|
801
1100
|
spellPitchClass,
|
|
802
1101
|
spellScale,
|
|
803
1102
|
spellPitchClasses,
|
|
1103
|
+
spellChordFromRoot,
|
|
804
1104
|
spellChord,
|
|
1105
|
+
spellPitch,
|
|
805
1106
|
noteNames,
|
|
806
1107
|
parseChordSymbol,
|
|
807
1108
|
formatChordSymbol,
|
|
@@ -813,4 +1114,4 @@ export {
|
|
|
813
1114
|
nextVoicing,
|
|
814
1115
|
voiceChordStyled
|
|
815
1116
|
};
|
|
816
|
-
//# sourceMappingURL=chunk-
|
|
1117
|
+
//# sourceMappingURL=chunk-RK35RIN7.js.map
|