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