@libraz/libcantus 0.9.3 → 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 +2 -2
- package/dist/analyze/index.cjs +39 -1947
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +197 -37
- package/dist/analyze/index.d.ts +197 -37
- 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-4MHA432G.js → chunk-7NKDPF74.js} +403 -180
- 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-MIWO425M.js → chunk-D2FAJBYL.js} +101 -34
- package/dist/chunk-D2FAJBYL.js.map +1 -0
- package/dist/{chunk-SGU7SPLK.js → chunk-EHVEBWM5.js} +1056 -531
- 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-IT2O4XFE.js → chunk-GQSGS2N4.js} +43 -24
- package/dist/chunk-GQSGS2N4.js.map +1 -0
- package/dist/{chunk-62QZKCJQ.js → chunk-HKGYCQ34.js} +10 -6
- package/dist/chunk-HKGYCQ34.js.map +1 -0
- package/dist/{chunk-AKLYPWIN.js → chunk-I24MV65U.js} +29 -6
- package/dist/chunk-I24MV65U.js.map +1 -0
- package/dist/{chunk-CHMXELDS.js → chunk-KMHOICIY.js} +311 -181
- 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-2ZWMSJH6.js → chunk-NKIBQ6PT.js} +2 -2
- 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-EH6EM37U.js → chunk-NYYWMV2C.js} +61 -49
- package/dist/chunk-NYYWMV2C.js.map +1 -0
- package/dist/{chunk-GOAW4VYL.js → chunk-Q2XUP227.js} +243 -117
- 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-C5XYKUVG.js → chunk-RK35RIN7.js} +370 -157
- 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-I5R7ZBJS.js → chunk-YLEMGEFH.js} +103 -59
- 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 -647
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +301 -39
- package/dist/core/index.d.ts +301 -39
- package/dist/core/index.js +54 -16
- package/dist/{function-DYt4iKsn.d.cts → function-BtiZb9i9.d.ts} +28 -7
- package/dist/{function-DVH4Eusr.d.ts → function-DNugAOrt.d.cts} +28 -7
- package/dist/generate/index.cjs +28 -4085
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +370 -62
- package/dist/generate/index.d.ts +370 -62
- package/dist/generate/index.js +23 -9
- package/dist/{index-CxqBQtH1.d.cts → index-6VR8-kit.d.ts} +19 -4
- package/dist/index-70p168VM.d.ts +192 -0
- package/dist/{index-CpOsP1y9.d.cts → index-Be74mcon.d.cts} +18 -17
- package/dist/index-BtXT6UQ0.d.cts +192 -0
- package/dist/{index-DxvARIP2.d.ts → index-C5UBbQ4G.d.ts} +18 -17
- package/dist/{index-Bt4tOUD9.d.ts → index-C83mZI1P.d.ts} +93 -8
- package/dist/{index-Bg5dYMmZ.d.ts → 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-Jc0Ryi79.d.ts → index-tJhkmQgE.d.cts} +19 -4
- package/dist/{index-Dikst44v.d.cts → index-tSQh-xwY.d.ts} +14 -11
- package/dist/{index-BNlGP86A.d.cts → index-y_ak9_Hi.d.cts} +93 -8
- package/dist/index.cjs +419 -7383
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -14
- package/dist/index.d.ts +14 -14
- package/dist/index.js +111 -27
- package/dist/model/index.cjs +8 -2562
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +433 -75
- package/dist/model/index.d.ts +433 -75
- 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 -1822
- 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 +30 -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 +24 -6
- package/dist/chunk-2ZWMSJH6.js.map +0 -1
- package/dist/chunk-4MHA432G.js.map +0 -1
- package/dist/chunk-62QZKCJQ.js.map +0 -1
- package/dist/chunk-AKLYPWIN.js.map +0 -1
- package/dist/chunk-C5XYKUVG.js.map +0 -1
- package/dist/chunk-CHMXELDS.js.map +0 -1
- package/dist/chunk-DYWFNC4A.js +0 -152
- package/dist/chunk-DYWFNC4A.js.map +0 -1
- package/dist/chunk-EH6EM37U.js.map +0 -1
- package/dist/chunk-FBQK3ZQN.js +0 -131
- package/dist/chunk-FBQK3ZQN.js.map +0 -1
- package/dist/chunk-GOAW4VYL.js.map +0 -1
- package/dist/chunk-I5R7ZBJS.js.map +0 -1
- package/dist/chunk-IT2O4XFE.js.map +0 -1
- package/dist/chunk-MIWO425M.js.map +0 -1
- package/dist/chunk-SGU7SPLK.js.map +0 -1
- package/dist/index-CGMZNCx6.d.cts +0 -140
- package/dist/index-CGMZNCx6.d.ts +0 -140
- package/dist/index-CnZCo29u.d.cts +0 -108
- package/dist/index-JnF4v9bx.d.ts +0 -108
- package/dist/styled-DZ7GjShb.d.ts +0 -282
- package/dist/styled-ZTbdaVFM.d.cts +0 -282
- package/src/analyze/arrange/index.ts +0 -21
- package/src/analyze/arrange/internal.ts +0 -127
- package/src/analyze/arrange/tension.ts +0 -199
- package/src/analyze/arrange/tracks.ts +0 -312
- package/src/analyze/detect/index.ts +0 -272
- 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 -402
- package/src/analyze/index.ts +0 -49
- package/src/analyze/timeline/index.ts +0 -451
- package/src/analyze/voice/index.ts +0 -339
- package/src/core/event-index/index.ts +0 -104
- package/src/core/index.ts +0 -61
- package/src/core/interval/index.ts +0 -81
- package/src/core/meter/index.ts +0 -302
- package/src/core/pitch/index.ts +0 -311
- package/src/core/random/index.ts +0 -76
- package/src/core/tuning/index.ts +0 -186
- package/src/core/types.ts +0 -29
- package/src/core/validation/index.ts +0 -119
- package/src/generate/bass/index.ts +0 -373
- package/src/generate/countermelody/index.ts +0 -470
- package/src/generate/drums/beat.ts +0 -266
- package/src/generate/drums/euclid.ts +0 -65
- 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 -361
- 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 -351
- package/src/generate/harmonize/index.ts +0 -451
- package/src/generate/index.ts +0 -58
- package/src/generate/motif/index.ts +0 -422
- package/src/generate/progression/index.ts +0 -362
- package/src/generate/reharmony/index.ts +0 -323
- package/src/generate/rhythm/index.ts +0 -175
- package/src/index.ts +0 -13
- package/src/model/chord.ts +0 -442
- 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 -375
- package/src/theory/chordscale/index.ts +0 -374
- package/src/theory/counterpoint/index.ts +0 -230
- package/src/theory/harmony/index.ts +0 -77
- package/src/theory/index.ts +0 -112
- package/src/theory/safety/index.ts +0 -488
- 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 -249
- 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 -216
- package/src/theory/voicing/styled.ts +0 -199
- package/tsconfig.json +0 -23
|
@@ -1,32 +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";
|
|
19
13
|
import {
|
|
14
|
+
InvalidInputError,
|
|
15
|
+
NoSolutionError,
|
|
20
16
|
assertFiniteNumber,
|
|
21
17
|
assertGenerationBudget,
|
|
22
18
|
assertInteger,
|
|
23
|
-
assertPositiveInt
|
|
24
|
-
|
|
19
|
+
assertPositiveInt,
|
|
20
|
+
diatonicLetterOf,
|
|
21
|
+
formatNote,
|
|
22
|
+
midiToNote,
|
|
23
|
+
naturalPitchClassOf,
|
|
24
|
+
noteToPitchClass,
|
|
25
|
+
parseNote,
|
|
26
|
+
pitchClassOf
|
|
27
|
+
} from "./chunk-XEBG3G53.js";
|
|
25
28
|
|
|
26
29
|
// src/theory/chordscale/index.ts
|
|
27
|
-
function pitchClass(value) {
|
|
28
|
-
return (Math.trunc(value) % 12 + 12) % 12;
|
|
29
|
-
}
|
|
30
30
|
function popcount12(mask) {
|
|
31
31
|
let count = 0;
|
|
32
32
|
for (let n = 0; n < 12; n += 1) {
|
|
@@ -35,7 +35,7 @@ function popcount12(mask) {
|
|
|
35
35
|
return count;
|
|
36
36
|
}
|
|
37
37
|
function maskHasPitchClass(mask, scaleRootPc, pc) {
|
|
38
|
-
const offset =
|
|
38
|
+
const offset = pitchClassOf(pc - scaleRootPc);
|
|
39
39
|
return (mask >> offset & 1) === 1;
|
|
40
40
|
}
|
|
41
41
|
function scaleMatchesChord(chordPcs, scaleMask, scaleRootPc) {
|
|
@@ -48,16 +48,31 @@ function scaleMatchesChord(chordPcs, scaleMask, scaleRootPc) {
|
|
|
48
48
|
}
|
|
49
49
|
var ALIASED_SCALE_NAMES = /* @__PURE__ */ new Set(["major", "naturalMinor"]);
|
|
50
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
|
+
}
|
|
51
66
|
function chordScales(chord) {
|
|
52
67
|
const chordPcs = chordPitchClasses(chord);
|
|
53
|
-
const rootPc =
|
|
68
|
+
const rootPc = pitchClassOf(chord.rootPc);
|
|
54
69
|
const ranked = [];
|
|
55
70
|
const seenMasks = /* @__PURE__ */ new Set();
|
|
56
71
|
for (const name of Object.keys(NAMED_SCALES)) {
|
|
57
72
|
if (name === "chromatic" || ALIASED_SCALE_NAMES.has(name)) {
|
|
58
73
|
continue;
|
|
59
74
|
}
|
|
60
|
-
const mask =
|
|
75
|
+
const mask = namedScaleMask(name);
|
|
61
76
|
if (mask === void 0 || seenMasks.has(mask)) {
|
|
62
77
|
continue;
|
|
63
78
|
}
|
|
@@ -84,16 +99,18 @@ function chordScales(chord) {
|
|
|
84
99
|
if (a.size !== b.size) {
|
|
85
100
|
return a.size - b.size;
|
|
86
101
|
}
|
|
102
|
+
const rankA = conventionalRank(a.name);
|
|
103
|
+
const rankB = conventionalRank(b.name);
|
|
104
|
+
if (rankA !== rankB) {
|
|
105
|
+
return rankA - rankB;
|
|
106
|
+
}
|
|
87
107
|
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
88
108
|
});
|
|
89
109
|
return ranked.map((entry) => ({ name: entry.name, rootPc }));
|
|
90
110
|
}
|
|
91
111
|
function avoidNotes(chord, scaleName) {
|
|
92
|
-
const mask =
|
|
93
|
-
|
|
94
|
-
return [];
|
|
95
|
-
}
|
|
96
|
-
const rootPc = pitchClass(chord.rootPc);
|
|
112
|
+
const mask = requireScaleMask(scaleName);
|
|
113
|
+
const rootPc = pitchClassOf(chord.rootPc);
|
|
97
114
|
const chordPcs = chordPitchClasses(chord);
|
|
98
115
|
if (!scaleMatchesChord(chordPcs, mask, rootPc)) {
|
|
99
116
|
return [];
|
|
@@ -104,18 +121,15 @@ function avoidNotes(chord, scaleName) {
|
|
|
104
121
|
if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc)) {
|
|
105
122
|
continue;
|
|
106
123
|
}
|
|
107
|
-
if (chordSet.has(
|
|
124
|
+
if (chordSet.has(pitchClassOf(pc - 1))) {
|
|
108
125
|
avoid.push(pc);
|
|
109
126
|
}
|
|
110
127
|
}
|
|
111
128
|
return avoid.sort((a, b) => a - b);
|
|
112
129
|
}
|
|
113
130
|
function availableTensions(chord, scaleName) {
|
|
114
|
-
const mask =
|
|
115
|
-
|
|
116
|
-
return [];
|
|
117
|
-
}
|
|
118
|
-
const rootPc = pitchClass(chord.rootPc);
|
|
131
|
+
const mask = requireScaleMask(scaleName);
|
|
132
|
+
const rootPc = pitchClassOf(chord.rootPc);
|
|
119
133
|
const chordPcs = chordPitchClasses(chord);
|
|
120
134
|
if (!scaleMatchesChord(chordPcs, mask, rootPc)) {
|
|
121
135
|
return [];
|
|
@@ -143,14 +157,14 @@ function chordScaleReport(chord, limit) {
|
|
|
143
157
|
}
|
|
144
158
|
var RANK_PENALTY = 0.01;
|
|
145
159
|
function scalePitchClassSet(name, rootPc) {
|
|
146
|
-
const mask =
|
|
160
|
+
const mask = namedScaleMask(name);
|
|
147
161
|
const pcs = /* @__PURE__ */ new Set();
|
|
148
162
|
if (mask === void 0) {
|
|
149
163
|
return pcs;
|
|
150
164
|
}
|
|
151
165
|
for (let n = 0; n < 12; n += 1) {
|
|
152
166
|
if ((mask >> n & 1) === 1) {
|
|
153
|
-
pcs.add(
|
|
167
|
+
pcs.add(pitchClassOf(rootPc + n));
|
|
154
168
|
}
|
|
155
169
|
}
|
|
156
170
|
return pcs;
|
|
@@ -228,64 +242,117 @@ function scalesForChanges(chords) {
|
|
|
228
242
|
}
|
|
229
243
|
|
|
230
244
|
// src/theory/spelling/index.ts
|
|
231
|
-
var LETTER_SEMITONES = [0, 2, 4, 5, 7, 9, 11];
|
|
232
245
|
var CHROMATIC_SPELLING = {
|
|
233
|
-
1:
|
|
246
|
+
1: 1,
|
|
234
247
|
// b2
|
|
235
|
-
|
|
248
|
+
2: 1,
|
|
249
|
+
// M2
|
|
250
|
+
3: 2,
|
|
236
251
|
// b3
|
|
237
|
-
|
|
252
|
+
4: 2,
|
|
253
|
+
// M3
|
|
254
|
+
5: 3,
|
|
255
|
+
// P4
|
|
256
|
+
6: 3,
|
|
238
257
|
// #4
|
|
239
|
-
|
|
258
|
+
7: 4,
|
|
259
|
+
// P5
|
|
260
|
+
8: 5,
|
|
240
261
|
// b6
|
|
241
|
-
9:
|
|
262
|
+
9: 5,
|
|
242
263
|
// #6 (raised sixth of a minor key)
|
|
243
|
-
10:
|
|
264
|
+
10: 6,
|
|
244
265
|
// b7
|
|
245
|
-
11:
|
|
266
|
+
11: 6
|
|
246
267
|
// #7 (raised leading tone of a minor key)
|
|
247
268
|
};
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
+
};
|
|
257
298
|
function alterFor(letter, pc) {
|
|
258
|
-
let d =
|
|
299
|
+
let d = pitchClassOf(pc - naturalPitchClassOf(letter));
|
|
259
300
|
if (d > 6) {
|
|
260
301
|
d -= 12;
|
|
261
302
|
}
|
|
262
303
|
return d;
|
|
263
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
|
+
}
|
|
264
321
|
function isHeptatonic(key) {
|
|
265
322
|
return scaleTonesInDegreeOrder(key).length === 7;
|
|
266
323
|
}
|
|
267
|
-
function
|
|
268
|
-
const tonicPc = mod12(naturalPc(tonic.letter) + tonic.alter);
|
|
269
|
-
const offset = mod12(pc - tonicPc);
|
|
324
|
+
function hasMinorThird(key) {
|
|
270
325
|
const tones = scaleTonesInDegreeOrder(key);
|
|
271
|
-
|
|
272
|
-
|
|
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));
|
|
273
338
|
if (degree >= 0) {
|
|
274
|
-
const letter =
|
|
339
|
+
const letter = diatonicLetterOf(tonic.letter + degree);
|
|
275
340
|
return { letter, alter: alterFor(letter, pc) };
|
|
276
341
|
}
|
|
277
|
-
const
|
|
278
|
-
if (
|
|
279
|
-
const letter =
|
|
342
|
+
const degreeOffset = CHROMATIC_SPELLING[offset];
|
|
343
|
+
if (degreeOffset !== void 0) {
|
|
344
|
+
const letter = diatonicLetterOf(tonic.letter + degreeOffset);
|
|
280
345
|
return { letter, alter: alterFor(letter, pc) };
|
|
281
346
|
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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;
|
|
286
353
|
}
|
|
287
354
|
}
|
|
288
|
-
const belowLetter =
|
|
355
|
+
const belowLetter = diatonicLetterOf([0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6][pitchClassOf(pc)] ?? 0);
|
|
289
356
|
return { letter: belowLetter, alter: alterFor(belowLetter, pc) };
|
|
290
357
|
}
|
|
291
358
|
function spellScale(tonic, key) {
|
|
@@ -306,9 +373,10 @@ function chordLetterOffset(interval, chord) {
|
|
|
306
373
|
return 1;
|
|
307
374
|
case 3:
|
|
308
375
|
case 4:
|
|
309
|
-
case 15:
|
|
310
376
|
case 16:
|
|
311
377
|
return 2;
|
|
378
|
+
case 15:
|
|
379
|
+
return chord.intervals.some((i) => i === 3 || i === 4) ? 1 : 2;
|
|
312
380
|
case 5:
|
|
313
381
|
case 17:
|
|
314
382
|
case 18:
|
|
@@ -330,7 +398,7 @@ function chordLetterOffset(interval, chord) {
|
|
|
330
398
|
return 6;
|
|
331
399
|
default: {
|
|
332
400
|
const octaves = Math.floor(Math.max(0, interval) / 12);
|
|
333
|
-
const simple =
|
|
401
|
+
const simple = pitchClassOf(interval);
|
|
334
402
|
const simpleOffset = [0, 1, 1, 2, 2, 3, 4, 4, 4, 5, 6, 6][simple] ?? 0;
|
|
335
403
|
return octaves * 7 + simpleOffset;
|
|
336
404
|
}
|
|
@@ -338,14 +406,26 @@ function chordLetterOffset(interval, chord) {
|
|
|
338
406
|
}
|
|
339
407
|
function spellChordFromRoot(chord, root) {
|
|
340
408
|
return chord.intervals.map((interval) => {
|
|
341
|
-
const letter =
|
|
342
|
-
const pc =
|
|
409
|
+
const letter = diatonicLetterOf(root.letter + chordLetterOffset(interval, chord));
|
|
410
|
+
const pc = pitchClassOf(chord.rootPc + interval);
|
|
343
411
|
return { letter, alter: alterFor(letter, pc) };
|
|
344
412
|
});
|
|
345
413
|
}
|
|
346
414
|
function spellChord(chord, tonic, key) {
|
|
347
|
-
|
|
348
|
-
|
|
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 };
|
|
349
429
|
}
|
|
350
430
|
function noteNames(notes) {
|
|
351
431
|
return notes.map(formatNote);
|
|
@@ -411,7 +491,36 @@ var QUALITY_MAP = {
|
|
|
411
491
|
"7b13": "7b13",
|
|
412
492
|
"11": "11",
|
|
413
493
|
"13": "13",
|
|
414
|
-
"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"
|
|
415
524
|
};
|
|
416
525
|
var CANONICAL_SUFFIX = {
|
|
417
526
|
maj: "",
|
|
@@ -443,10 +552,20 @@ var CANONICAL_SUFFIX = {
|
|
|
443
552
|
"7b13": "7b13",
|
|
444
553
|
"11": "11",
|
|
445
554
|
"13": "13",
|
|
446
|
-
"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"
|
|
447
566
|
};
|
|
448
|
-
function
|
|
449
|
-
return
|
|
567
|
+
function knownQuality(suffix) {
|
|
568
|
+
return Object.hasOwn(QUALITY_MAP, suffix) ? QUALITY_MAP[suffix] : void 0;
|
|
450
569
|
}
|
|
451
570
|
function makeSpelledChord(root, quality, bass) {
|
|
452
571
|
const chord = makeChord(
|
|
@@ -464,7 +583,7 @@ function parseChordSymbol(text) {
|
|
|
464
583
|
const trimmed = text.trim();
|
|
465
584
|
const rootMatch = ROOT_RE.exec(trimmed);
|
|
466
585
|
if (!rootMatch) {
|
|
467
|
-
throw new
|
|
586
|
+
throw new InvalidInputError(`Invalid chord symbol: ${text}`);
|
|
468
587
|
}
|
|
469
588
|
const rootToken = rootMatch[0];
|
|
470
589
|
const rootNote = parseNote(rootToken);
|
|
@@ -473,39 +592,48 @@ function parseChordSymbol(text) {
|
|
|
473
592
|
if (slashIndex !== -1) {
|
|
474
593
|
const before = rest.slice(0, slashIndex);
|
|
475
594
|
const after = rest.slice(slashIndex + 1);
|
|
476
|
-
const beforeQuality =
|
|
595
|
+
const beforeQuality = knownQuality(before);
|
|
477
596
|
if (beforeQuality !== void 0 && BASS_RE.test(after)) {
|
|
478
597
|
return makeSpelledChord(rootNote, beforeQuality, parseNote(after));
|
|
479
598
|
}
|
|
480
599
|
}
|
|
481
|
-
const quality =
|
|
600
|
+
const quality = knownQuality(rest);
|
|
482
601
|
if (quality === void 0) {
|
|
483
|
-
throw new
|
|
602
|
+
throw new InvalidInputError(`Unrecognized chord quality: ${text}`);
|
|
484
603
|
}
|
|
485
604
|
return makeSpelledChord(rootNote, quality);
|
|
486
605
|
}
|
|
487
|
-
function pitchClassName(pc, hint, flats) {
|
|
606
|
+
function pitchClassName(pc, hint, flats, inheritFlats) {
|
|
488
607
|
if (flats === void 0 && hint !== void 0 && noteToPitchClass(hint) === pc) {
|
|
489
608
|
return formatNote(hint);
|
|
490
609
|
}
|
|
491
|
-
const note = midiToNote(60 + pc, flats ? "flat" : "sharp");
|
|
610
|
+
const note = midiToNote(60 + pc, flats ?? inheritFlats ? "flat" : "sharp");
|
|
492
611
|
return formatNote({ letter: note.letter, alter: note.alter });
|
|
493
612
|
}
|
|
494
613
|
function formatChordSymbol(chord, opts) {
|
|
495
|
-
|
|
496
|
-
|
|
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);
|
|
497
624
|
const suffix = CANONICAL_SUFFIX[chord.quality];
|
|
498
625
|
let symbol = `${rootName}${suffix}`;
|
|
499
|
-
if (chord.bassPc !== void 0 &&
|
|
500
|
-
|
|
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)}`;
|
|
501
629
|
}
|
|
502
630
|
return symbol;
|
|
503
631
|
}
|
|
504
632
|
function transposeChordSymbol(text, semitones, opts) {
|
|
505
633
|
const chord = parseChordSymbol(text);
|
|
506
|
-
const transposed = { ...chord, rootPc:
|
|
634
|
+
const transposed = { ...chord, rootPc: pitchClassOf(chord.rootPc + semitones) };
|
|
507
635
|
if (chord.bassPc !== void 0) {
|
|
508
|
-
transposed.bassPc =
|
|
636
|
+
transposed.bassPc = pitchClassOf(chord.bassPc + semitones);
|
|
509
637
|
}
|
|
510
638
|
return formatChordSymbol(transposed, opts);
|
|
511
639
|
}
|
|
@@ -514,15 +642,16 @@ function transposeChordSymbol(text, semitones, opts) {
|
|
|
514
642
|
var DEFAULT_MAX_SPACING = 12;
|
|
515
643
|
var VIOLATION_PENALTY = 1e3;
|
|
516
644
|
var MISSING_TONE_PENALTY = 500;
|
|
645
|
+
var MISSING_FIFTH_PENALTY = 20;
|
|
517
646
|
var POOR_DOUBLING_PENALTY = 4;
|
|
647
|
+
var LEADING_TONE_DOUBLING_PENALTY = 200;
|
|
648
|
+
var RESOLUTION_PENALTY = 40;
|
|
518
649
|
var MAX_CANDIDATES = 4e3;
|
|
519
|
-
|
|
520
|
-
return (Math.trunc(value) % 12 + 12) % 12;
|
|
521
|
-
}
|
|
650
|
+
var MAX_SEARCH_NODES = MAX_CANDIDATES * 16;
|
|
522
651
|
function pitchesForPc(pc, range) {
|
|
523
652
|
const result = [];
|
|
524
653
|
for (let midi = Math.ceil(range.min); midi <= range.max; midi += 1) {
|
|
525
|
-
if (
|
|
654
|
+
if (pitchClassOf(midi) === pc) {
|
|
526
655
|
result.push(midi);
|
|
527
656
|
}
|
|
528
657
|
}
|
|
@@ -535,65 +664,107 @@ function pitchesForPc(pc, range) {
|
|
|
535
664
|
}
|
|
536
665
|
function enumerateVoicings(chord, ranges, maxSpacing) {
|
|
537
666
|
const chordPcs = chordPitchClasses(chord);
|
|
538
|
-
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
|
+
}
|
|
539
676
|
const results = [];
|
|
540
677
|
const current = [];
|
|
678
|
+
let nodes = 0;
|
|
541
679
|
const build = (voice) => {
|
|
542
|
-
|
|
680
|
+
nodes += 1;
|
|
681
|
+
if (results.length >= MAX_CANDIDATES || nodes > MAX_SEARCH_NODES) {
|
|
543
682
|
return;
|
|
544
683
|
}
|
|
545
684
|
if (voice === ranges.length) {
|
|
546
685
|
results.push([...current]);
|
|
547
686
|
return;
|
|
548
687
|
}
|
|
549
|
-
const range = ranges[voice];
|
|
550
|
-
if (range === void 0) {
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
const pcs = voice === 0 ? [bassPc] : chordPcs;
|
|
554
688
|
const prev = current[voice - 1];
|
|
555
|
-
for (const
|
|
556
|
-
|
|
557
|
-
if (
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
continue;
|
|
564
|
-
}
|
|
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;
|
|
565
697
|
}
|
|
566
|
-
current.push(pitch);
|
|
567
|
-
build(voice + 1);
|
|
568
|
-
current.pop();
|
|
569
698
|
}
|
|
699
|
+
current.push(pitch);
|
|
700
|
+
build(voice + 1);
|
|
701
|
+
current.pop();
|
|
570
702
|
}
|
|
571
703
|
};
|
|
572
704
|
build(0);
|
|
573
705
|
return results;
|
|
574
706
|
}
|
|
575
|
-
function structuralPenalty(pitches, chord) {
|
|
707
|
+
function structuralPenalty(pitches, chord, key) {
|
|
576
708
|
const counts = /* @__PURE__ */ new Map();
|
|
577
709
|
for (const pitch of pitches) {
|
|
578
|
-
const pc =
|
|
710
|
+
const pc = pitchClassOf(pitch);
|
|
579
711
|
counts.set(pc, (counts.get(pc) ?? 0) + 1);
|
|
580
712
|
}
|
|
581
713
|
let penalty = 0;
|
|
582
714
|
for (const pc of chordPitchClasses(chord)) {
|
|
583
715
|
if (!counts.has(pc)) {
|
|
584
|
-
penalty += MISSING_TONE_PENALTY;
|
|
716
|
+
penalty += chordToneRole(pc, chord) === "fifth" ? MISSING_FIFTH_PENALTY : MISSING_TONE_PENALTY;
|
|
585
717
|
}
|
|
586
718
|
}
|
|
587
|
-
const rootPc =
|
|
588
|
-
const
|
|
719
|
+
const rootPc = pitchClassOf(chord.rootPc);
|
|
720
|
+
const leadingTonePc = key === void 0 ? void 0 : pitchClassOf(key.rootPc - 1);
|
|
589
721
|
for (const [pc, count] of counts) {
|
|
590
|
-
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") {
|
|
591
730
|
penalty += (count - 1) * POOR_DOUBLING_PENALTY;
|
|
592
731
|
}
|
|
593
732
|
}
|
|
594
733
|
return penalty;
|
|
595
734
|
}
|
|
596
|
-
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) {
|
|
597
768
|
let count = 0;
|
|
598
769
|
for (let lower = 0; lower < cur.length; lower += 1) {
|
|
599
770
|
for (let upper = lower + 1; upper < cur.length; upper += 1) {
|
|
@@ -607,16 +778,10 @@ function violationCount(prev, cur, maxSpacing) {
|
|
|
607
778
|
if (createsParallelPerfect(prevUpper, curUpper, prevLower, curLower)) {
|
|
608
779
|
count += 1;
|
|
609
780
|
}
|
|
610
|
-
if (createsVoiceCrossing(curUpper, curLower)) {
|
|
611
|
-
count += 1;
|
|
612
|
-
}
|
|
613
781
|
if (upper === lower + 1) {
|
|
614
782
|
if (createsVoiceOverlap(prevUpper, curUpper, prevLower, curLower)) {
|
|
615
783
|
count += 1;
|
|
616
784
|
}
|
|
617
|
-
if (lower > 0 && exceedsSpacing(curUpper, curLower, maxSpacing)) {
|
|
618
|
-
count += 1;
|
|
619
|
-
}
|
|
620
785
|
}
|
|
621
786
|
}
|
|
622
787
|
}
|
|
@@ -650,48 +815,57 @@ function voiceLeadingCost(from, to) {
|
|
|
650
815
|
return total;
|
|
651
816
|
}
|
|
652
817
|
function nextVoicing(current, chord, opts) {
|
|
653
|
-
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) => {
|
|
654
828
|
const centre = Math.min(127, Math.max(0, pitch));
|
|
655
829
|
return { min: Math.max(0, centre - 12), max: Math.min(127, centre + 12) };
|
|
656
|
-
});
|
|
657
|
-
const maxSpacing = opts
|
|
830
|
+
}) : resolveRanges(opts);
|
|
831
|
+
const maxSpacing = resolveMaxSpacing(opts);
|
|
658
832
|
const candidates = enumerateVoicings(chord, ranges, maxSpacing);
|
|
659
833
|
let best;
|
|
660
834
|
let bestScore = Number.POSITIVE_INFINITY;
|
|
661
835
|
for (const candidate of candidates) {
|
|
662
|
-
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);
|
|
663
837
|
if (score < bestScore) {
|
|
664
838
|
bestScore = score;
|
|
665
839
|
best = candidate;
|
|
666
840
|
}
|
|
667
841
|
}
|
|
668
842
|
if (best === void 0) {
|
|
669
|
-
throw new
|
|
843
|
+
throw new NoSolutionError("no voicing satisfies the given ranges");
|
|
670
844
|
}
|
|
671
845
|
return best;
|
|
672
846
|
}
|
|
673
847
|
|
|
674
848
|
// src/theory/voicing/satb.ts
|
|
675
|
-
var SATB_RANGES = [
|
|
676
|
-
{ min: 40, max: 60 },
|
|
677
|
-
{ min: 48, max: 67 },
|
|
678
|
-
{ min: 55, max: 74 },
|
|
679
|
-
{ min: 60, max: 79 }
|
|
680
|
-
];
|
|
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
|
+
]);
|
|
681
855
|
var DERIVED_LOW = 40;
|
|
682
856
|
var DERIVED_HIGH = 79;
|
|
683
857
|
var DERIVED_SPAN = 19;
|
|
684
858
|
function resolveRanges(opts) {
|
|
685
859
|
if (opts?.ranges !== void 0) {
|
|
686
860
|
if (opts.ranges.length === 0) {
|
|
687
|
-
throw new
|
|
861
|
+
throw new InvalidInputError("ranges must contain at least one voice range");
|
|
688
862
|
}
|
|
689
863
|
assertGenerationBudget(opts.ranges.length, "voice ranges", 128);
|
|
690
864
|
return opts.ranges.map((range, index) => {
|
|
691
865
|
assertFiniteNumber(range.min, `ranges[${index}].min`);
|
|
692
866
|
assertFiniteNumber(range.max, `ranges[${index}].max`);
|
|
693
867
|
if (range.min > range.max) {
|
|
694
|
-
throw new
|
|
868
|
+
throw new InvalidInputError(`ranges[${index}].min must not exceed max`);
|
|
695
869
|
}
|
|
696
870
|
assertGenerationBudget(
|
|
697
871
|
Math.floor(range.max) - Math.ceil(range.min) + 1,
|
|
@@ -718,16 +892,22 @@ function resolveRanges(opts) {
|
|
|
718
892
|
}
|
|
719
893
|
return ranges;
|
|
720
894
|
}
|
|
721
|
-
function
|
|
722
|
-
const ranges = resolveRanges(opts);
|
|
895
|
+
function resolveMaxSpacing(opts) {
|
|
723
896
|
const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
|
|
724
897
|
assertFiniteNumber(maxSpacing, "maxSpacing");
|
|
725
|
-
if (maxSpacing < 0)
|
|
898
|
+
if (maxSpacing < 0) {
|
|
899
|
+
throw new InvalidInputError("maxSpacing must be non-negative");
|
|
900
|
+
}
|
|
901
|
+
return maxSpacing;
|
|
902
|
+
}
|
|
903
|
+
function voiceChord(chord, opts) {
|
|
904
|
+
const ranges = resolveRanges(opts);
|
|
905
|
+
const maxSpacing = resolveMaxSpacing(opts);
|
|
726
906
|
const candidates = enumerateVoicings(chord, ranges, maxSpacing);
|
|
727
907
|
let best;
|
|
728
908
|
let bestScore = Number.POSITIVE_INFINITY;
|
|
729
909
|
for (const candidate of candidates) {
|
|
730
|
-
let score = structuralPenalty(candidate, chord);
|
|
910
|
+
let score = structuralPenalty(candidate, chord, opts?.key);
|
|
731
911
|
for (let i = 0; i < candidate.length; i += 1) {
|
|
732
912
|
const pitch = candidate[i];
|
|
733
913
|
const range = ranges[i];
|
|
@@ -742,39 +922,63 @@ function voiceChord(chord, opts) {
|
|
|
742
922
|
}
|
|
743
923
|
}
|
|
744
924
|
if (best === void 0) {
|
|
745
|
-
throw new
|
|
925
|
+
throw new NoSolutionError(
|
|
926
|
+
`no voicing satisfies the given ranges for ${formatChordSymbol(chord)}`
|
|
927
|
+
);
|
|
746
928
|
}
|
|
747
929
|
return best;
|
|
748
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
|
+
}
|
|
749
947
|
function voiceProgression(chords, opts) {
|
|
750
|
-
assertGenerationBudget(chords.length
|
|
948
|
+
assertGenerationBudget(chords.length, "voiced progression chords", opts?.budget);
|
|
751
949
|
const ranges = resolveRanges(opts);
|
|
752
|
-
const maxSpacing = opts
|
|
753
|
-
assertFiniteNumber(maxSpacing, "maxSpacing");
|
|
754
|
-
if (maxSpacing < 0) throw new RangeError("maxSpacing must be non-negative");
|
|
950
|
+
const maxSpacing = resolveMaxSpacing(opts);
|
|
755
951
|
const result = [];
|
|
756
952
|
let prev;
|
|
757
|
-
|
|
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
|
+
}
|
|
758
959
|
if (prev === void 0) {
|
|
759
|
-
prev = voiceChord(chord, opts);
|
|
960
|
+
prev = locate(index, chord, () => voiceChord(chord, opts));
|
|
961
|
+
prevChord = chord;
|
|
760
962
|
result.push(prev);
|
|
761
963
|
continue;
|
|
762
964
|
}
|
|
763
|
-
const candidates = enumerateVoicings(chord, ranges, maxSpacing);
|
|
965
|
+
const candidates = locate(index, chord, () => enumerateVoicings(chord, ranges, maxSpacing));
|
|
764
966
|
let best;
|
|
765
967
|
let bestScore = Number.POSITIVE_INFINITY;
|
|
766
968
|
for (const candidate of candidates) {
|
|
767
|
-
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;
|
|
768
971
|
if (score < bestScore) {
|
|
769
972
|
bestScore = score;
|
|
770
973
|
best = candidate;
|
|
771
974
|
}
|
|
772
975
|
}
|
|
773
976
|
if (best === void 0) {
|
|
774
|
-
throw
|
|
977
|
+
throw noSolutionAt(index, chord);
|
|
775
978
|
}
|
|
776
979
|
result.push(best);
|
|
777
980
|
prev = best;
|
|
981
|
+
prevChord = chord;
|
|
778
982
|
}
|
|
779
983
|
return result;
|
|
780
984
|
}
|
|
@@ -793,7 +997,7 @@ function dedupePcs(pcs) {
|
|
|
793
997
|
return result;
|
|
794
998
|
}
|
|
795
999
|
function lowestPitchAtOrAbove(pc, floor) {
|
|
796
|
-
return floor + ((pc -
|
|
1000
|
+
return floor + ((pc - pitchClassOf(floor)) % 12 + 12) % 12;
|
|
797
1001
|
}
|
|
798
1002
|
function nearestPc(target, pcs) {
|
|
799
1003
|
let best = pcs[0] ?? target;
|
|
@@ -815,10 +1019,10 @@ function voiceChordStyled(chord, opts) {
|
|
|
815
1019
|
assertFiniteNumber(opts.topNote, "styled voicing topNote");
|
|
816
1020
|
}
|
|
817
1021
|
const base = 12 * (octave + 1);
|
|
818
|
-
const rootPc =
|
|
819
|
-
const bassPc =
|
|
1022
|
+
const rootPc = pitchClassOf(chord.rootPc);
|
|
1023
|
+
const bassPc = pitchClassOf(chord.bassPc ?? chord.rootPc);
|
|
820
1024
|
const omitRoot = style === "rootless" || opts?.rootless === true;
|
|
821
|
-
let order = dedupePcs(chord.intervals.map((interval) =>
|
|
1025
|
+
let order = dedupePcs(chord.intervals.map((interval) => pitchClassOf(chord.rootPc + interval)));
|
|
822
1026
|
const bassIndex = order.indexOf(bassPc);
|
|
823
1027
|
if (bassIndex > 0) {
|
|
824
1028
|
order = [...order.slice(bassIndex), ...order.slice(0, bassIndex)];
|
|
@@ -840,7 +1044,7 @@ function voiceChordStyled(chord, opts) {
|
|
|
840
1044
|
return [];
|
|
841
1045
|
}
|
|
842
1046
|
if (opts?.topNote !== void 0) {
|
|
843
|
-
const target = order.includes(
|
|
1047
|
+
const target = order.includes(pitchClassOf(opts.topNote)) ? pitchClassOf(opts.topNote) : nearestPc(pitchClassOf(opts.topNote), order);
|
|
844
1048
|
const targetIndex = order.indexOf(target);
|
|
845
1049
|
order = [...order.slice(targetIndex + 1), ...order.slice(0, targetIndex + 1)];
|
|
846
1050
|
}
|
|
@@ -850,7 +1054,7 @@ function voiceChordStyled(chord, opts) {
|
|
|
850
1054
|
if (prev === void 0) {
|
|
851
1055
|
prev = lowestPitchAtOrAbove(pc, base);
|
|
852
1056
|
} else {
|
|
853
|
-
let delta = ((pc -
|
|
1057
|
+
let delta = ((pc - pitchClassOf(prev)) % 12 + 12) % 12;
|
|
854
1058
|
if (delta === 0) {
|
|
855
1059
|
delta = 12;
|
|
856
1060
|
}
|
|
@@ -864,7 +1068,7 @@ function voiceChordStyled(chord, opts) {
|
|
|
864
1068
|
stack[stack.length - 3] = (stack[stack.length - 3] ?? 0) - 12;
|
|
865
1069
|
}
|
|
866
1070
|
if (chord.bassPc !== void 0) {
|
|
867
|
-
const bassIndexInStack = stack.findIndex((pitch) =>
|
|
1071
|
+
const bassIndexInStack = stack.findIndex((pitch) => pitchClassOf(pitch) === bassPc);
|
|
868
1072
|
if (bassIndexInStack >= 0) {
|
|
869
1073
|
const otherLowest = Math.min(
|
|
870
1074
|
...stack.filter((_pitch, index) => index !== bassIndexInStack),
|
|
@@ -875,7 +1079,15 @@ function voiceChordStyled(chord, opts) {
|
|
|
875
1079
|
}
|
|
876
1080
|
}
|
|
877
1081
|
}
|
|
878
|
-
|
|
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;
|
|
879
1091
|
}
|
|
880
1092
|
|
|
881
1093
|
export {
|
|
@@ -890,6 +1102,7 @@ export {
|
|
|
890
1102
|
spellPitchClasses,
|
|
891
1103
|
spellChordFromRoot,
|
|
892
1104
|
spellChord,
|
|
1105
|
+
spellPitch,
|
|
893
1106
|
noteNames,
|
|
894
1107
|
parseChordSymbol,
|
|
895
1108
|
formatChordSymbol,
|
|
@@ -901,4 +1114,4 @@ export {
|
|
|
901
1114
|
nextVoicing,
|
|
902
1115
|
voiceChordStyled
|
|
903
1116
|
};
|
|
904
|
-
//# sourceMappingURL=chunk-
|
|
1117
|
+
//# sourceMappingURL=chunk-RK35RIN7.js.map
|