@libraz/libcantus 0.9.2 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/dist/analyze/index.cjs +41 -1676
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +194 -122
- package/dist/analyze/index.d.ts +194 -122
- package/dist/analyze/index.js +19 -8
- package/dist/chunk-36N73KZ3.cjs +246 -0
- package/dist/chunk-36N73KZ3.cjs.map +1 -0
- package/dist/chunk-4HLLZPXT.cjs +3077 -0
- package/dist/chunk-4HLLZPXT.cjs.map +1 -0
- package/dist/chunk-7MGZ7D62.cjs +498 -0
- package/dist/chunk-7MGZ7D62.cjs.map +1 -0
- package/dist/{chunk-R3S2LUBH.js → chunk-7NKDPF74.js} +660 -305
- package/dist/chunk-7NKDPF74.js.map +1 -0
- package/dist/chunk-CAN76NCU.cjs +679 -0
- package/dist/chunk-CAN76NCU.cjs.map +1 -0
- package/dist/{chunk-7Y7WF64D.js → chunk-D2FAJBYL.js} +104 -35
- package/dist/chunk-D2FAJBYL.js.map +1 -0
- package/dist/{chunk-COKTX2G5.js → chunk-EHVEBWM5.js} +1052 -510
- package/dist/chunk-EHVEBWM5.js.map +1 -0
- package/dist/chunk-FCANXBAT.cjs +30 -0
- package/dist/chunk-FCANXBAT.cjs.map +1 -0
- package/dist/chunk-FNDWXRRI.cjs +479 -0
- package/dist/chunk-FNDWXRRI.cjs.map +1 -0
- package/dist/{chunk-GQKN5KDT.js → chunk-GQSGS2N4.js} +43 -29
- package/dist/chunk-GQSGS2N4.js.map +1 -0
- package/dist/chunk-HKGYCQ34.js +53 -0
- package/dist/chunk-HKGYCQ34.js.map +1 -0
- package/dist/chunk-I24MV65U.js +106 -0
- package/dist/chunk-I24MV65U.js.map +1 -0
- package/dist/{chunk-LN7ZZMD2.js → chunk-KMHOICIY.js} +322 -173
- package/dist/chunk-KMHOICIY.js.map +1 -0
- package/dist/chunk-LW53AHQU.cjs +1117 -0
- package/dist/chunk-LW53AHQU.cjs.map +1 -0
- package/dist/chunk-N6IHQUOX.js +1 -0
- package/dist/chunk-N6IHQUOX.js.map +1 -0
- package/dist/chunk-NKIBQ6PT.js +30 -0
- package/dist/chunk-NKIBQ6PT.js.map +1 -0
- package/dist/chunk-NP4NA2FB.cjs +106 -0
- package/dist/chunk-NP4NA2FB.cjs.map +1 -0
- package/dist/chunk-NVE34YGZ.cjs +1449 -0
- package/dist/chunk-NVE34YGZ.cjs.map +1 -0
- package/dist/{chunk-A4532ZFR.js → chunk-NYYWMV2C.js} +115 -47
- package/dist/chunk-NYYWMV2C.js.map +1 -0
- package/dist/{chunk-5HTNMQW4.js → chunk-Q2XUP227.js} +310 -118
- package/dist/chunk-Q2XUP227.js.map +1 -0
- package/dist/chunk-Q6OP4U43.cjs +327 -0
- package/dist/chunk-Q6OP4U43.cjs.map +1 -0
- package/dist/chunk-RC6GBMCD.cjs +845 -0
- package/dist/chunk-RC6GBMCD.cjs.map +1 -0
- package/dist/{chunk-XTC3X6MS.js → chunk-RK35RIN7.js} +453 -152
- package/dist/chunk-RK35RIN7.js.map +1 -0
- package/dist/chunk-RUMU3UA6.cjs +53 -0
- package/dist/chunk-RUMU3UA6.cjs.map +1 -0
- package/dist/chunk-T64C7ZD7.cjs +1 -0
- package/dist/chunk-T64C7ZD7.cjs.map +1 -0
- package/dist/chunk-XEBG3G53.js +498 -0
- package/dist/chunk-XEBG3G53.js.map +1 -0
- package/dist/chunk-YLEMGEFH.js +246 -0
- package/dist/chunk-YLEMGEFH.js.map +1 -0
- package/dist/chunk-ZEJR4TKL.cjs +173 -0
- package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
- package/dist/core/index.cjs +130 -431
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +331 -17
- package/dist/core/index.d.ts +331 -17
- package/dist/core/index.js +71 -13
- package/dist/{function-2bJW1Zp_.d.ts → function-BtiZb9i9.d.ts} +28 -7
- package/dist/{function-r2sh-jOO.d.cts → function-DNugAOrt.d.cts} +28 -7
- package/dist/generate/index.cjs +30 -3751
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +373 -59
- package/dist/generate/index.d.ts +373 -59
- package/dist/generate/index.js +23 -9
- package/dist/{index-DZ1M21do.d.cts → index-6VR8-kit.d.ts} +20 -4
- package/dist/index-70p168VM.d.ts +192 -0
- package/dist/{index-D_ZTUNKp.d.cts → index-Be74mcon.d.cts} +18 -17
- package/dist/index-BtXT6UQ0.d.cts +192 -0
- package/dist/{index-BMPxwjwV.d.ts → index-C5UBbQ4G.d.ts} +18 -17
- package/dist/{index-BOBXBNwP.d.cts → index-C83mZI1P.d.ts} +93 -8
- package/dist/{index-BdeQ1r_p.d.cts → index-D6b6na69.d.cts} +14 -11
- package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
- package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
- package/dist/index-OG65ocOr.d.cts +272 -0
- package/dist/index-OG65ocOr.d.ts +272 -0
- package/dist/{index-BreoJRw0.d.ts → index-tJhkmQgE.d.cts} +20 -4
- package/dist/{index-CP3wAfEH.d.ts → index-tSQh-xwY.d.ts} +14 -11
- package/dist/{index-CM5W4rcm.d.ts → index-y_ak9_Hi.d.cts} +93 -8
- package/dist/index.cjs +419 -6848
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -13
- package/dist/index.d.ts +14 -13
- package/dist/index.js +132 -27
- package/dist/model/index.cjs +8 -2423
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +433 -74
- package/dist/model/index.d.ts +433 -74
- package/dist/model/index.js +6 -7
- package/dist/styled-DIRkk9Uz.d.ts +506 -0
- package/dist/styled-DJbkJQfj.d.cts +506 -0
- package/dist/theory/index.cjs +172 -1676
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +151 -233
- package/dist/theory/index.d.ts +151 -233
- package/dist/theory/index.js +31 -7
- package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
- package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
- package/package.json +73 -24
- package/dist/chunk-5HTNMQW4.js.map +0 -1
- package/dist/chunk-7Y7WF64D.js.map +0 -1
- package/dist/chunk-A4532ZFR.js.map +0 -1
- package/dist/chunk-BHUT2S5G.js +0 -22
- package/dist/chunk-BHUT2S5G.js.map +0 -1
- package/dist/chunk-CCBLVAPK.js +0 -42
- package/dist/chunk-CCBLVAPK.js.map +0 -1
- package/dist/chunk-COKTX2G5.js.map +0 -1
- package/dist/chunk-DYWFNC4A.js +0 -152
- package/dist/chunk-DYWFNC4A.js.map +0 -1
- package/dist/chunk-GQKN5KDT.js.map +0 -1
- package/dist/chunk-LN7ZZMD2.js.map +0 -1
- package/dist/chunk-MSWFZF7J.js +0 -118
- package/dist/chunk-MSWFZF7J.js.map +0 -1
- package/dist/chunk-R3S2LUBH.js.map +0 -1
- package/dist/chunk-SDM3VJJ5.js +0 -41
- package/dist/chunk-SDM3VJJ5.js.map +0 -1
- package/dist/chunk-XTC3X6MS.js.map +0 -1
- package/dist/chunk-ZXW5YIWK.js +0 -55
- package/dist/chunk-ZXW5YIWK.js.map +0 -1
- package/dist/index-CGMZNCx6.d.cts +0 -140
- package/dist/index-CGMZNCx6.d.ts +0 -140
- package/dist/styled-BjXtOFNk.d.cts +0 -282
- package/dist/styled-D7BAx3S3.d.ts +0 -282
- package/src/analyze/arrange/index.ts +0 -21
- package/src/analyze/arrange/internal.ts +0 -125
- package/src/analyze/arrange/tension.ts +0 -187
- package/src/analyze/arrange/tracks.ts +0 -279
- package/src/analyze/detect/index.ts +0 -235
- package/src/analyze/functional/borrowed.ts +0 -84
- package/src/analyze/functional/cadence.ts +0 -66
- package/src/analyze/functional/function.ts +0 -184
- package/src/analyze/functional/index.ts +0 -23
- package/src/analyze/functional/internal.ts +0 -29
- package/src/analyze/functional/roman.ts +0 -393
- package/src/analyze/index.ts +0 -49
- package/src/analyze/timeline/index.ts +0 -415
- package/src/analyze/voice/index.ts +0 -330
- package/src/core/index.ts +0 -47
- package/src/core/interval/index.ts +0 -81
- package/src/core/meter/index.ts +0 -282
- package/src/core/pitch/index.ts +0 -311
- package/src/core/random/index.ts +0 -53
- package/src/core/tuning/index.ts +0 -156
- package/src/core/types.ts +0 -29
- package/src/generate/bass/index.ts +0 -350
- package/src/generate/countermelody/index.ts +0 -379
- package/src/generate/drums/beat.ts +0 -266
- package/src/generate/drums/euclid.ts +0 -58
- package/src/generate/drums/fills.ts +0 -356
- package/src/generate/drums/ghost.ts +0 -119
- package/src/generate/drums/hihat.ts +0 -285
- package/src/generate/drums/hit.ts +0 -31
- package/src/generate/drums/index.ts +0 -329
- package/src/generate/drums/internal.ts +0 -257
- package/src/generate/drums/kick.ts +0 -160
- package/src/generate/drums/percussion.ts +0 -121
- package/src/generate/drums/rng.ts +0 -7
- package/src/generate/drums/swing.ts +0 -71
- package/src/generate/groove/index.ts +0 -326
- package/src/generate/harmonize/index.ts +0 -440
- package/src/generate/index.ts +0 -59
- package/src/generate/motif/index.ts +0 -398
- package/src/generate/progression/index.ts +0 -370
- package/src/generate/reharmony/index.ts +0 -330
- package/src/generate/rhythm/index.ts +0 -165
- package/src/index.ts +0 -247
- package/src/model/chord.ts +0 -423
- package/src/model/index.ts +0 -15
- package/src/model/interval.ts +0 -82
- package/src/model/key.ts +0 -266
- package/src/model/note.ts +0 -176
- package/src/model/progression.ts +0 -161
- package/src/model/shared.ts +0 -12
- package/src/theory/chord/index.ts +0 -373
- package/src/theory/chordscale/index.ts +0 -374
- package/src/theory/counterpoint/index.ts +0 -230
- package/src/theory/harmony/index.ts +0 -92
- package/src/theory/index.ts +0 -107
- package/src/theory/safety/index.ts +0 -419
- package/src/theory/scale/degrees.ts +0 -123
- package/src/theory/scale/index.ts +0 -28
- package/src/theory/scale/key.ts +0 -56
- package/src/theory/scale/masks.ts +0 -153
- package/src/theory/spelling/index.ts +0 -182
- package/src/theory/symbol/index.ts +0 -268
- package/src/theory/voicing/index.ts +0 -5
- package/src/theory/voicing/internal.ts +0 -168
- package/src/theory/voicing/leading.ts +0 -112
- package/src/theory/voicing/satb.ts +0 -195
- package/src/theory/voicing/styled.ts +0 -176
- package/tsconfig.json +0 -23
package/dist/model/index.cjs
CHANGED
|
@@ -1,2435 +1,20 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
19
2
|
|
|
20
|
-
// src/model/index.ts
|
|
21
|
-
var model_exports = {};
|
|
22
|
-
__export(model_exports, {
|
|
23
|
-
Chord: () => Chord,
|
|
24
|
-
Interval: () => Interval,
|
|
25
|
-
Key: () => Key,
|
|
26
|
-
Note: () => Note,
|
|
27
|
-
Progression: () => Progression
|
|
28
|
-
});
|
|
29
|
-
module.exports = __toCommonJS(model_exports);
|
|
30
3
|
|
|
31
|
-
// src/theory/scale/degrees.ts
|
|
32
|
-
function pitchClass(pitch) {
|
|
33
|
-
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
34
|
-
}
|
|
35
|
-
function scaleOffset(pitch, key) {
|
|
36
|
-
return (pitchClass(pitch) - pitchClass(key.rootPc) + 12) % 12;
|
|
37
|
-
}
|
|
38
|
-
function isScaleTone(pitch, key) {
|
|
39
|
-
return (key.modeMask12 >> scaleOffset(pitch, key) & 1) === 1;
|
|
40
|
-
}
|
|
41
|
-
function scaleTonesInDegreeOrder(key) {
|
|
42
|
-
const root = pitchClass(key.rootPc);
|
|
43
|
-
const pcs = [];
|
|
44
|
-
for (let n = 0; n < 12; n += 1) {
|
|
45
|
-
if ((key.modeMask12 >> n & 1) === 1) {
|
|
46
|
-
pcs.push((root + n) % 12);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return pcs;
|
|
50
|
-
}
|
|
51
4
|
|
|
52
|
-
// src/theory/scale/masks.ts
|
|
53
|
-
function maskFromOffsets(offsets) {
|
|
54
|
-
let mask = 1;
|
|
55
|
-
for (const offset of offsets) {
|
|
56
|
-
mask |= 1 << (offset % 12 + 12) % 12;
|
|
57
|
-
}
|
|
58
|
-
return mask;
|
|
59
|
-
}
|
|
60
|
-
var MAJOR_MASK = 2741;
|
|
61
|
-
var NATURAL_MINOR_MASK = 1453;
|
|
62
|
-
var HARMONIC_MINOR_MASK = maskFromOffsets([0, 2, 3, 5, 7, 8, 11]);
|
|
63
|
-
var MELODIC_MINOR_MASK = maskFromOffsets([0, 2, 3, 5, 7, 9, 11]);
|
|
64
|
-
var DORIAN_MASK = maskFromOffsets([0, 2, 3, 5, 7, 9, 10]);
|
|
65
|
-
var PHRYGIAN_MASK = maskFromOffsets([0, 1, 3, 5, 7, 8, 10]);
|
|
66
|
-
var LYDIAN_MASK = maskFromOffsets([0, 2, 4, 6, 7, 9, 11]);
|
|
67
|
-
var MIXOLYDIAN_MASK = maskFromOffsets([0, 2, 4, 5, 7, 9, 10]);
|
|
68
|
-
var LOCRIAN_MASK = maskFromOffsets([0, 1, 3, 5, 6, 8, 10]);
|
|
69
|
-
var MAJOR_PENTATONIC_MASK = maskFromOffsets([0, 2, 4, 7, 9]);
|
|
70
|
-
var MINOR_PENTATONIC_MASK = maskFromOffsets([0, 3, 5, 7, 10]);
|
|
71
|
-
var BLUES_MASK = maskFromOffsets([0, 3, 5, 6, 7, 10]);
|
|
72
|
-
var WHOLE_TONE_MASK = maskFromOffsets([0, 2, 4, 6, 8, 10]);
|
|
73
|
-
var OCTATONIC_HALF_WHOLE_MASK = maskFromOffsets([0, 1, 3, 4, 6, 7, 9, 10]);
|
|
74
|
-
var OCTATONIC_WHOLE_HALF_MASK = maskFromOffsets([0, 2, 3, 5, 6, 8, 9, 11]);
|
|
75
|
-
var CHROMATIC_MASK = 4095;
|
|
76
|
-
var NAMED_SCALES = {
|
|
77
|
-
major: MAJOR_MASK,
|
|
78
|
-
ionian: MAJOR_MASK,
|
|
79
|
-
naturalMinor: NATURAL_MINOR_MASK,
|
|
80
|
-
aeolian: NATURAL_MINOR_MASK,
|
|
81
|
-
harmonicMinor: HARMONIC_MINOR_MASK,
|
|
82
|
-
melodicMinor: MELODIC_MINOR_MASK,
|
|
83
|
-
dorian: DORIAN_MASK,
|
|
84
|
-
phrygian: PHRYGIAN_MASK,
|
|
85
|
-
lydian: LYDIAN_MASK,
|
|
86
|
-
mixolydian: MIXOLYDIAN_MASK,
|
|
87
|
-
locrian: LOCRIAN_MASK,
|
|
88
|
-
majorPentatonic: MAJOR_PENTATONIC_MASK,
|
|
89
|
-
minorPentatonic: MINOR_PENTATONIC_MASK,
|
|
90
|
-
blues: BLUES_MASK,
|
|
91
|
-
wholeTone: WHOLE_TONE_MASK,
|
|
92
|
-
octatonicHalfWhole: OCTATONIC_HALF_WHOLE_MASK,
|
|
93
|
-
octatonicWholeHalf: OCTATONIC_WHOLE_HALF_MASK,
|
|
94
|
-
chromatic: CHROMATIC_MASK
|
|
95
|
-
};
|
|
96
5
|
|
|
97
|
-
// src/theory/scale/key.ts
|
|
98
|
-
function majorKey(rootPc) {
|
|
99
|
-
return { rootPc: (rootPc % 12 + 12) % 12, modeMask12: MAJOR_MASK };
|
|
100
|
-
}
|
|
101
|
-
function minorKey(rootPc) {
|
|
102
|
-
return { rootPc: (rootPc % 12 + 12) % 12, modeMask12: NATURAL_MINOR_MASK };
|
|
103
|
-
}
|
|
104
|
-
function scaleByName(name, rootPc) {
|
|
105
|
-
const mask = NAMED_SCALES[name];
|
|
106
|
-
if (mask === void 0) {
|
|
107
|
-
throw new Error(`Unknown scale: ${name}`);
|
|
108
|
-
}
|
|
109
|
-
return { rootPc: (rootPc % 12 + 12) % 12, modeMask12: mask };
|
|
110
|
-
}
|
|
111
6
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
majb5: [0, 4, 6],
|
|
119
|
-
maj7: [0, 4, 7, 11],
|
|
120
|
-
min7: [0, 3, 7, 10],
|
|
121
|
-
dom7: [0, 4, 7, 10],
|
|
122
|
-
dim7: [0, 3, 6, 9],
|
|
123
|
-
m7b5: [0, 3, 6, 10],
|
|
124
|
-
minMaj7: [0, 3, 7, 11],
|
|
125
|
-
aug7: [0, 4, 8, 10],
|
|
126
|
-
augMaj7: [0, 4, 8, 11],
|
|
127
|
-
"6": [0, 4, 7, 9],
|
|
128
|
-
min6: [0, 3, 7, 9],
|
|
129
|
-
"6/9": [0, 4, 7, 9, 14],
|
|
130
|
-
sus2: [0, 2, 7],
|
|
131
|
-
sus4: [0, 5, 7],
|
|
132
|
-
add9: [0, 4, 7, 14],
|
|
133
|
-
add11: [0, 4, 7, 17],
|
|
134
|
-
maj9: [0, 4, 7, 11, 14],
|
|
135
|
-
min9: [0, 3, 7, 10, 14],
|
|
136
|
-
dom9: [0, 4, 7, 10, 14],
|
|
137
|
-
"7b9": [0, 4, 7, 10, 13],
|
|
138
|
-
"7#9": [0, 4, 7, 10, 15],
|
|
139
|
-
"7#11": [0, 4, 7, 10, 18],
|
|
140
|
-
"7b13": [0, 4, 7, 10, 20],
|
|
141
|
-
"11": [0, 7, 10, 14, 17],
|
|
142
|
-
"13": [0, 4, 7, 10, 14, 21],
|
|
143
|
-
"5": [0, 7]
|
|
144
|
-
};
|
|
145
|
-
function pitchClass2(value) {
|
|
146
|
-
return (Math.trunc(value) % 12 + 12) % 12;
|
|
147
|
-
}
|
|
148
|
-
function chordQualities() {
|
|
149
|
-
return Object.keys(QUALITY_INTERVALS);
|
|
150
|
-
}
|
|
151
|
-
function chordFromDegree(degree, ext, key) {
|
|
152
|
-
const tones = scaleTonesInDegreeOrder(key);
|
|
153
|
-
const length = tones.length;
|
|
154
|
-
const index = length > 0 ? (degree % length + length) % length : 0;
|
|
155
|
-
const rootPc = tones[index] ?? pitchClass2(key.rootPc);
|
|
156
|
-
return { rootPc, quality: ext, intervals: [...QUALITY_INTERVALS[ext]] };
|
|
157
|
-
}
|
|
158
|
-
function makeChord(rootPc, quality, bassPc) {
|
|
159
|
-
const chord = {
|
|
160
|
-
rootPc: pitchClass2(rootPc),
|
|
161
|
-
quality,
|
|
162
|
-
intervals: [...QUALITY_INTERVALS[quality]]
|
|
163
|
-
};
|
|
164
|
-
if (bassPc !== void 0) {
|
|
165
|
-
chord.bassPc = pitchClass2(bassPc);
|
|
166
|
-
}
|
|
167
|
-
return chord;
|
|
168
|
-
}
|
|
169
|
-
function chordPitchClasses(chord) {
|
|
170
|
-
const set = /* @__PURE__ */ new Set();
|
|
171
|
-
for (const interval of chord.intervals) {
|
|
172
|
-
set.add(pitchClass2(chord.rootPc + interval));
|
|
173
|
-
}
|
|
174
|
-
return [...set].sort((a, b) => a - b);
|
|
175
|
-
}
|
|
176
|
-
function chordToneOffsets(chord) {
|
|
177
|
-
return new Set(chord.intervals.map((i) => (i % 12 + 12) % 12));
|
|
178
|
-
}
|
|
179
|
-
function chordToneRole(pitch, chord) {
|
|
180
|
-
const interval = (pitchClass2(pitch) - pitchClass2(chord.rootPc) + 12) % 12;
|
|
181
|
-
const tones = chordToneOffsets(chord);
|
|
182
|
-
if (interval === 0) {
|
|
183
|
-
return "root";
|
|
184
|
-
}
|
|
185
|
-
if (interval === 3 || interval === 4) {
|
|
186
|
-
return tones.has(interval) ? "third" : null;
|
|
187
|
-
}
|
|
188
|
-
if (interval === 7) {
|
|
189
|
-
return tones.has(7) ? "fifth" : null;
|
|
190
|
-
}
|
|
191
|
-
if (interval === 6) {
|
|
192
|
-
return tones.has(6) && !tones.has(7) ? "fifth" : null;
|
|
193
|
-
}
|
|
194
|
-
if (interval === 8) {
|
|
195
|
-
return tones.has(8) && !tones.has(7) ? "fifth" : null;
|
|
196
|
-
}
|
|
197
|
-
if (interval === 9) {
|
|
198
|
-
const hasHigherSeventh = tones.has(10) || tones.has(11);
|
|
199
|
-
const isDiminishedSeventh = tones.has(3) && tones.has(6) && !hasHigherSeventh;
|
|
200
|
-
if (isDiminishedSeventh) {
|
|
201
|
-
return "seventh";
|
|
202
|
-
}
|
|
203
|
-
if (tones.has(9) && !hasHigherSeventh) {
|
|
204
|
-
return "sixth";
|
|
205
|
-
}
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
|
-
if (interval === 10 || interval === 11) {
|
|
209
|
-
return tones.has(interval) ? "seventh" : null;
|
|
210
|
-
}
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
213
|
-
function classifyTriad(thirdIc, fifthIc) {
|
|
214
|
-
if (thirdIc === 4 && fifthIc === 8) {
|
|
215
|
-
return "aug";
|
|
216
|
-
}
|
|
217
|
-
if (thirdIc === 4 && fifthIc === 6) {
|
|
218
|
-
return "majb5";
|
|
219
|
-
}
|
|
220
|
-
if (thirdIc === 3 && fifthIc === 6) {
|
|
221
|
-
return "dim";
|
|
222
|
-
}
|
|
223
|
-
if (thirdIc === 3) {
|
|
224
|
-
return "min";
|
|
225
|
-
}
|
|
226
|
-
return "maj";
|
|
227
|
-
}
|
|
228
|
-
function classifySeventh(thirdIc, fifthIc, seventhIc) {
|
|
229
|
-
if (thirdIc === 3 && fifthIc === 6 && seventhIc === 9) {
|
|
230
|
-
return "dim7";
|
|
231
|
-
}
|
|
232
|
-
if (thirdIc === 3 && fifthIc === 6 && seventhIc === 10) {
|
|
233
|
-
return "m7b5";
|
|
234
|
-
}
|
|
235
|
-
if (thirdIc === 3 && seventhIc === 11) {
|
|
236
|
-
return "minMaj7";
|
|
237
|
-
}
|
|
238
|
-
if (thirdIc === 3) {
|
|
239
|
-
return "min7";
|
|
240
|
-
}
|
|
241
|
-
if (fifthIc === 8 && seventhIc === 10) {
|
|
242
|
-
return "aug7";
|
|
243
|
-
}
|
|
244
|
-
if (fifthIc === 8 && seventhIc === 11) {
|
|
245
|
-
return "augMaj7";
|
|
246
|
-
}
|
|
247
|
-
if (seventhIc === 11) {
|
|
248
|
-
return "maj7";
|
|
249
|
-
}
|
|
250
|
-
return "dom7";
|
|
251
|
-
}
|
|
252
|
-
function stackThirds(degree, key, size) {
|
|
253
|
-
const tones = scaleTonesInDegreeOrder(key);
|
|
254
|
-
const length = tones.length;
|
|
255
|
-
if (length === 0) {
|
|
256
|
-
return { rootPc: pitchClass2(key.rootPc), quality: "maj", intervals: [0, 4, 7] };
|
|
257
|
-
}
|
|
258
|
-
const idx = (step) => ((degree + step) % length + length) % length;
|
|
259
|
-
const rootPc = tones[idx(0)] ?? pitchClass2(key.rootPc);
|
|
260
|
-
const offsets = [0, 2, 4, 6].slice(0, size).map((step) => {
|
|
261
|
-
const pc = tones[idx(step)] ?? rootPc;
|
|
262
|
-
return ((pc - rootPc) % 12 + 12) % 12;
|
|
263
|
-
});
|
|
264
|
-
const thirdIc = offsets[1] ?? 4;
|
|
265
|
-
const fifthIc = offsets[2] ?? 7;
|
|
266
|
-
const quality = size === 3 ? classifyTriad(thirdIc, fifthIc) : classifySeventh(thirdIc, fifthIc, offsets[3] ?? 11);
|
|
267
|
-
return { rootPc, quality, intervals: offsets };
|
|
268
|
-
}
|
|
269
|
-
function diatonicTriad(degree, key) {
|
|
270
|
-
return stackThirds(degree, key, 3);
|
|
271
|
-
}
|
|
272
|
-
function diatonicSeventh(degree, key) {
|
|
273
|
-
return stackThirds(degree, key, 4);
|
|
274
|
-
}
|
|
7
|
+
var _chunkNVE34YGZcjs = require('../chunk-NVE34YGZ.cjs');
|
|
8
|
+
require('../chunk-ZEJR4TKL.cjs');
|
|
9
|
+
require('../chunk-CAN76NCU.cjs');
|
|
10
|
+
require('../chunk-LW53AHQU.cjs');
|
|
11
|
+
require('../chunk-FNDWXRRI.cjs');
|
|
12
|
+
require('../chunk-7MGZ7D62.cjs');
|
|
275
13
|
|
|
276
|
-
// src/analyze/detect/index.ts
|
|
277
|
-
function pitchClass3(value) {
|
|
278
|
-
return (Math.trunc(value) % 12 + 12) % 12;
|
|
279
|
-
}
|
|
280
|
-
function uniquePitchClasses(pitches) {
|
|
281
|
-
return [...new Set(pitches.map(pitchClass3))].sort((a, b) => a - b);
|
|
282
|
-
}
|
|
283
|
-
function detectChord(pitches) {
|
|
284
|
-
const input = uniquePitchClasses(pitches);
|
|
285
|
-
if (input.length === 0) {
|
|
286
|
-
return [];
|
|
287
|
-
}
|
|
288
|
-
const bassPc = pitchClass3(Math.min(...pitches));
|
|
289
|
-
const inputSet = new Set(input);
|
|
290
|
-
const matches = [];
|
|
291
|
-
const qualities = chordQualities();
|
|
292
|
-
for (const rootPc of input) {
|
|
293
|
-
for (const quality of qualities) {
|
|
294
|
-
const chord = makeChord(rootPc, quality);
|
|
295
|
-
const tones = chordPitchClasses(chord);
|
|
296
|
-
const toneSet = new Set(tones);
|
|
297
|
-
const missingPcs = tones.filter((pc) => !inputSet.has(pc));
|
|
298
|
-
const fifthPc = pitchClass3(rootPc + 7);
|
|
299
|
-
if (missingPcs.length > 1 || missingPcs.length === 1 && missingPcs[0] !== fifthPc) {
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
const extraPcs = input.filter((pc) => !toneSet.has(pc));
|
|
303
|
-
const bassIndex = chord.intervals.findIndex((iv) => pitchClass3(rootPc + iv) === bassPc);
|
|
304
|
-
const inversion = bassIndex > 0 ? bassIndex : 0;
|
|
305
|
-
const match = {
|
|
306
|
-
rootPc,
|
|
307
|
-
quality,
|
|
308
|
-
missingPcs,
|
|
309
|
-
extraPcs,
|
|
310
|
-
exact: extraPcs.length === 0 && missingPcs.length === 0,
|
|
311
|
-
inversion
|
|
312
|
-
};
|
|
313
|
-
if (inversion > 0) {
|
|
314
|
-
match.bassPc = bassPc;
|
|
315
|
-
}
|
|
316
|
-
matches.push(match);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
matches.sort((a, b) => {
|
|
320
|
-
if (a.extraPcs.length !== b.extraPcs.length) {
|
|
321
|
-
return a.extraPcs.length - b.extraPcs.length;
|
|
322
|
-
}
|
|
323
|
-
if (a.missingPcs.length !== b.missingPcs.length) {
|
|
324
|
-
return a.missingPcs.length - b.missingPcs.length;
|
|
325
|
-
}
|
|
326
|
-
if (a.inversion === 0 !== (b.inversion === 0)) {
|
|
327
|
-
return a.inversion === 0 ? -1 : 1;
|
|
328
|
-
}
|
|
329
|
-
const aSize = chordPitchClasses(makeChord(a.rootPc, a.quality)).length;
|
|
330
|
-
const bSize = chordPitchClasses(makeChord(b.rootPc, b.quality)).length;
|
|
331
|
-
return bSize - aSize;
|
|
332
|
-
});
|
|
333
|
-
return matches;
|
|
334
|
-
}
|
|
335
|
-
function detectChordBest(pitches) {
|
|
336
|
-
const best = detectChord(pitches)[0];
|
|
337
|
-
if (!best) {
|
|
338
|
-
return null;
|
|
339
|
-
}
|
|
340
|
-
return makeChord(best.rootPc, best.quality, best.bassPc);
|
|
341
|
-
}
|
|
342
14
|
|
|
343
|
-
// src/analyze/functional/internal.ts
|
|
344
|
-
function mod12(n) {
|
|
345
|
-
return (n % 12 + 12) % 12;
|
|
346
|
-
}
|
|
347
|
-
function degreeRootPc(degreeNumber, key) {
|
|
348
|
-
const tones = scaleTonesInDegreeOrder(key);
|
|
349
|
-
if (tones.length === 0) {
|
|
350
|
-
return mod12(key.rootPc);
|
|
351
|
-
}
|
|
352
|
-
return tones[(degreeNumber - 1) % tones.length] ?? mod12(key.rootPc);
|
|
353
|
-
}
|
|
354
|
-
function isNeapolitan(chord, key) {
|
|
355
|
-
return mod12(chord.rootPc - key.rootPc) === 1 && chord.quality === "maj";
|
|
356
|
-
}
|
|
357
15
|
|
|
358
|
-
// src/analyze/functional/roman.ts
|
|
359
|
-
var ROMAN = ["I", "II", "III", "IV", "V", "VI", "VII"];
|
|
360
|
-
var ROMAN_TO_DEGREE = {
|
|
361
|
-
I: 1,
|
|
362
|
-
II: 2,
|
|
363
|
-
III: 3,
|
|
364
|
-
IV: 4,
|
|
365
|
-
V: 5,
|
|
366
|
-
VI: 6,
|
|
367
|
-
VII: 7
|
|
368
|
-
};
|
|
369
|
-
var OFFSET_SPELLING = [
|
|
370
|
-
[1, ""],
|
|
371
|
-
[2, "b"],
|
|
372
|
-
[2, ""],
|
|
373
|
-
[3, "b"],
|
|
374
|
-
[3, ""],
|
|
375
|
-
[4, ""],
|
|
376
|
-
[4, "#"],
|
|
377
|
-
[5, ""],
|
|
378
|
-
[6, "b"],
|
|
379
|
-
[6, ""],
|
|
380
|
-
[7, "b"],
|
|
381
|
-
[7, ""]
|
|
382
|
-
];
|
|
383
|
-
function seventhQuality(base, halfDim) {
|
|
384
|
-
if (halfDim) {
|
|
385
|
-
return "m7b5";
|
|
386
|
-
}
|
|
387
|
-
if (base === "dim") {
|
|
388
|
-
return "dim7";
|
|
389
|
-
}
|
|
390
|
-
if (base === "aug") {
|
|
391
|
-
return "aug7";
|
|
392
|
-
}
|
|
393
|
-
return base === "maj" ? "dom7" : "min7";
|
|
394
|
-
}
|
|
395
|
-
var INVERSION_FIGURES = /* @__PURE__ */ new Set(["6", "64", "65", "43", "42", "2", "7"]);
|
|
396
|
-
var ROMAN_STYLE = {
|
|
397
|
-
maj: { lower: false, suffix: "" },
|
|
398
|
-
min: { lower: true, suffix: "" },
|
|
399
|
-
dim: { lower: true, suffix: "o" },
|
|
400
|
-
aug: { lower: false, suffix: "+" },
|
|
401
|
-
maj7: { lower: false, suffix: "maj7" },
|
|
402
|
-
min7: { lower: true, suffix: "7" },
|
|
403
|
-
dom7: { lower: false, suffix: "7" },
|
|
404
|
-
dim7: { lower: true, suffix: "o7" },
|
|
405
|
-
m7b5: { lower: true, suffix: "\xF87" },
|
|
406
|
-
minMaj7: { lower: true, suffix: "maj7" },
|
|
407
|
-
aug7: { lower: false, suffix: "+7" },
|
|
408
|
-
augMaj7: { lower: false, suffix: "+maj7" },
|
|
409
|
-
majb5: { lower: false, suffix: "b5" },
|
|
410
|
-
"6": { lower: false, suffix: "add6" },
|
|
411
|
-
min6: { lower: true, suffix: "add6" },
|
|
412
|
-
"6/9": { lower: false, suffix: "69" },
|
|
413
|
-
sus2: { lower: false, suffix: "sus2" },
|
|
414
|
-
sus4: { lower: false, suffix: "sus4" },
|
|
415
|
-
add9: { lower: false, suffix: "add9" },
|
|
416
|
-
add11: { lower: false, suffix: "add11" },
|
|
417
|
-
maj9: { lower: false, suffix: "maj9" },
|
|
418
|
-
min9: { lower: true, suffix: "9" },
|
|
419
|
-
dom9: { lower: false, suffix: "9" },
|
|
420
|
-
"7b9": { lower: false, suffix: "7b9" },
|
|
421
|
-
"7#9": { lower: false, suffix: "7#9" },
|
|
422
|
-
"7#11": { lower: false, suffix: "7#11" },
|
|
423
|
-
"7b13": { lower: false, suffix: "7b13" },
|
|
424
|
-
"11": { lower: false, suffix: "11" },
|
|
425
|
-
"13": { lower: false, suffix: "13" },
|
|
426
|
-
"5": { lower: false, suffix: "5" }
|
|
427
|
-
};
|
|
428
|
-
var SUFFIX_QUALITY = new Map(
|
|
429
|
-
Object.entries(ROMAN_STYLE).map(
|
|
430
|
-
([quality, style]) => [`${style.lower ? "l" : "u"}:${style.suffix}`, quality]
|
|
431
|
-
)
|
|
432
|
-
);
|
|
433
|
-
function parseInversion(figures) {
|
|
434
|
-
if (figures === "65") {
|
|
435
|
-
return { inversion: 1, seventh: true };
|
|
436
|
-
}
|
|
437
|
-
if (figures === "43") {
|
|
438
|
-
return { inversion: 2, seventh: true };
|
|
439
|
-
}
|
|
440
|
-
if (figures === "42" || figures === "2") {
|
|
441
|
-
return { inversion: 3, seventh: true };
|
|
442
|
-
}
|
|
443
|
-
if (figures === "7") {
|
|
444
|
-
return { inversion: 0, seventh: true };
|
|
445
|
-
}
|
|
446
|
-
if (figures === "64") {
|
|
447
|
-
return { inversion: 2, seventh: false };
|
|
448
|
-
}
|
|
449
|
-
if (figures === "6") {
|
|
450
|
-
return { inversion: 1, seventh: false };
|
|
451
|
-
}
|
|
452
|
-
return { inversion: 0, seventh: false };
|
|
453
|
-
}
|
|
454
|
-
function parseSimpleRoman(text, key) {
|
|
455
|
-
const match = /^([b#]?)([iIvV]+)(.*)$/.exec(text.trim());
|
|
456
|
-
if (!match) {
|
|
457
|
-
throw new Error(`Invalid Roman numeral: ${text}`);
|
|
458
|
-
}
|
|
459
|
-
const accidental = match[1] === "b" ? -1 : match[1] === "#" ? 1 : 0;
|
|
460
|
-
const glyph = (match[2] ?? "").toUpperCase();
|
|
461
|
-
const degreeNumber = ROMAN_TO_DEGREE[glyph];
|
|
462
|
-
if (degreeNumber === void 0) {
|
|
463
|
-
throw new Error(`Invalid Roman numeral: ${text}`);
|
|
464
|
-
}
|
|
465
|
-
const isUpper = (match[2] ?? "")[0] === (match[2] ?? "")[0]?.toUpperCase();
|
|
466
|
-
const suffix = match[3] ?? "";
|
|
467
|
-
const rootPc = mod12(degreeRootPc(degreeNumber, key) + accidental);
|
|
468
|
-
const canonical = SUFFIX_QUALITY.get(`${isUpper ? "u" : "l"}:${suffix}`);
|
|
469
|
-
if (canonical !== void 0) {
|
|
470
|
-
return { rootPc, quality: canonical, inversion: 0 };
|
|
471
|
-
}
|
|
472
|
-
const isDim = /o|°|dim/.test(suffix);
|
|
473
|
-
const isAug = /\+|aug/.test(suffix);
|
|
474
|
-
const isHalfDim = /ø/.test(suffix);
|
|
475
|
-
const explicitMaj7 = /maj7|M7/.test(suffix);
|
|
476
|
-
const figures = suffix.replace(/maj7|M7/g, "").replace(/[^0-9]/g, "");
|
|
477
|
-
if (figures !== "" && !INVERSION_FIGURES.has(figures)) {
|
|
478
|
-
throw new Error(`Unsupported figured-bass or extension "${figures}" in Roman numeral: ${text}`);
|
|
479
|
-
}
|
|
480
|
-
const { inversion, seventh } = parseInversion(figures);
|
|
481
|
-
const hasSeventh = explicitMaj7 || seventh || isHalfDim;
|
|
482
|
-
let base;
|
|
483
|
-
if (isDim || isHalfDim) {
|
|
484
|
-
base = "dim";
|
|
485
|
-
} else if (isAug) {
|
|
486
|
-
base = "aug";
|
|
487
|
-
} else {
|
|
488
|
-
base = isUpper ? "maj" : "min";
|
|
489
|
-
}
|
|
490
|
-
let quality;
|
|
491
|
-
if (explicitMaj7) {
|
|
492
|
-
quality = base === "min" ? "minMaj7" : base === "aug" ? "augMaj7" : "maj7";
|
|
493
|
-
} else if (hasSeventh) {
|
|
494
|
-
quality = seventhQuality(base, isHalfDim);
|
|
495
|
-
} else if (base === "dim") {
|
|
496
|
-
quality = "dim";
|
|
497
|
-
} else if (base === "aug") {
|
|
498
|
-
quality = "aug";
|
|
499
|
-
} else {
|
|
500
|
-
quality = base;
|
|
501
|
-
}
|
|
502
|
-
return { rootPc, quality, inversion };
|
|
503
|
-
}
|
|
504
|
-
function chordFromParsed(parsed) {
|
|
505
|
-
const chord = makeChord(parsed.rootPc, parsed.quality);
|
|
506
|
-
if (parsed.inversion > 0 && parsed.inversion < chord.intervals.length) {
|
|
507
|
-
chord.bassPc = mod12(parsed.rootPc + (chord.intervals[parsed.inversion] ?? 0));
|
|
508
|
-
}
|
|
509
|
-
return chord;
|
|
510
|
-
}
|
|
511
|
-
function romanToChord(text, key) {
|
|
512
|
-
const trimmed = text.trim();
|
|
513
|
-
const slash = trimmed.indexOf("/");
|
|
514
|
-
if (slash >= 0) {
|
|
515
|
-
const applied = trimmed.slice(0, slash);
|
|
516
|
-
const target = trimmed.slice(slash + 1);
|
|
517
|
-
const targetRoot = parseSimpleRoman(target, key).rootPc;
|
|
518
|
-
const localKey = majorKey(targetRoot);
|
|
519
|
-
return chordFromParsed(parseSimpleRoman(applied, localKey));
|
|
520
|
-
}
|
|
521
|
-
return chordFromParsed(parseSimpleRoman(trimmed, key));
|
|
522
|
-
}
|
|
523
|
-
function romanStyle(quality) {
|
|
524
|
-
return ROMAN_STYLE[quality];
|
|
525
|
-
}
|
|
526
|
-
function romanSpelling(rootPc, key) {
|
|
527
|
-
const tones = scaleTonesInDegreeOrder(key);
|
|
528
|
-
const diatonic = tones.indexOf(mod12(rootPc));
|
|
529
|
-
if (diatonic >= 0) {
|
|
530
|
-
return { degreeNumber: diatonic + 1, accidental: "" };
|
|
531
|
-
}
|
|
532
|
-
for (let i = 0; i < tones.length; i += 1) {
|
|
533
|
-
const degreeNumber2 = i + 1;
|
|
534
|
-
if (degreeNumber2 === 1 || degreeNumber2 === 5) {
|
|
535
|
-
continue;
|
|
536
|
-
}
|
|
537
|
-
if (mod12((tones[i] ?? 0) - 1) === mod12(rootPc)) {
|
|
538
|
-
return { degreeNumber: degreeNumber2, accidental: "b" };
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
for (let i = 0; i < tones.length; i += 1) {
|
|
542
|
-
if (mod12((tones[i] ?? 0) + 1) === mod12(rootPc)) {
|
|
543
|
-
return { degreeNumber: i + 1, accidental: "#" };
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
const [degreeNumber, accidental] = OFFSET_SPELLING[mod12(rootPc - key.rootPc)] ?? [1, ""];
|
|
547
|
-
return { degreeNumber, accidental };
|
|
548
|
-
}
|
|
549
|
-
function baseMarker(quality) {
|
|
550
|
-
if (quality === "dim" || quality === "dim7") {
|
|
551
|
-
return "o";
|
|
552
|
-
}
|
|
553
|
-
if (quality === "m7b5") {
|
|
554
|
-
return "\xF8";
|
|
555
|
-
}
|
|
556
|
-
if (quality === "aug" || quality === "aug7") {
|
|
557
|
-
return "+";
|
|
558
|
-
}
|
|
559
|
-
if (quality === "augMaj7") {
|
|
560
|
-
return "+maj7";
|
|
561
|
-
}
|
|
562
|
-
if (quality === "maj7" || quality === "minMaj7") {
|
|
563
|
-
return "maj7";
|
|
564
|
-
}
|
|
565
|
-
return "";
|
|
566
|
-
}
|
|
567
|
-
var TRIAD_FIGURES = { 1: "6", 2: "64" };
|
|
568
|
-
var SEVENTH_FIGURES = { 1: "65", 2: "43", 3: "42" };
|
|
569
|
-
var TRIAD_FIGURE_QUALITIES = /* @__PURE__ */ new Set(["maj", "min", "dim", "aug"]);
|
|
570
|
-
var SEVENTH_FIGURE_QUALITIES = /* @__PURE__ */ new Set([
|
|
571
|
-
"maj7",
|
|
572
|
-
"min7",
|
|
573
|
-
"dom7",
|
|
574
|
-
"dim7",
|
|
575
|
-
"m7b5",
|
|
576
|
-
"minMaj7",
|
|
577
|
-
"aug7",
|
|
578
|
-
"augMaj7"
|
|
579
|
-
]);
|
|
580
|
-
function chordToRoman(chord, key) {
|
|
581
|
-
const { degreeNumber, accidental } = romanSpelling(chord.rootPc, key);
|
|
582
|
-
const { lower, suffix } = romanStyle(chord.quality);
|
|
583
|
-
const numeral = ROMAN[degreeNumber - 1] ?? "I";
|
|
584
|
-
const cased = lower ? numeral.toLowerCase() : numeral;
|
|
585
|
-
let inversion = 0;
|
|
586
|
-
if (chord.bassPc !== void 0) {
|
|
587
|
-
const idx = chord.intervals.findIndex((iv) => mod12(chord.rootPc + iv) === chord.bassPc);
|
|
588
|
-
if (idx > 0) {
|
|
589
|
-
inversion = idx;
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
if (inversion > 0) {
|
|
593
|
-
const figure = SEVENTH_FIGURE_QUALITIES.has(chord.quality) ? SEVENTH_FIGURES[inversion] : TRIAD_FIGURE_QUALITIES.has(chord.quality) ? TRIAD_FIGURES[inversion] : void 0;
|
|
594
|
-
if (figure !== void 0) {
|
|
595
|
-
return `${accidental}${cased}${baseMarker(chord.quality)}${figure}`;
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
return `${accidental}${cased}${suffix}`;
|
|
599
|
-
}
|
|
600
16
|
|
|
601
|
-
// src/analyze/functional/function.ts
|
|
602
|
-
var FUNCTION_BY_OFFSET = [
|
|
603
|
-
"tonic",
|
|
604
|
-
// 0 I
|
|
605
|
-
"subdominant",
|
|
606
|
-
// 1 bII (Neapolitan)
|
|
607
|
-
"subdominant",
|
|
608
|
-
// 2 ii
|
|
609
|
-
"tonic",
|
|
610
|
-
// 3 bIII
|
|
611
|
-
"tonic",
|
|
612
|
-
// 4 iii
|
|
613
|
-
"subdominant",
|
|
614
|
-
// 5 IV
|
|
615
|
-
"dominant",
|
|
616
|
-
// 6 #IV / bV
|
|
617
|
-
"dominant",
|
|
618
|
-
// 7 V
|
|
619
|
-
"subdominant",
|
|
620
|
-
// 8 bVI
|
|
621
|
-
"tonic",
|
|
622
|
-
// 9 vi
|
|
623
|
-
"subdominant",
|
|
624
|
-
// 10 bVII
|
|
625
|
-
"dominant"
|
|
626
|
-
// 11 vii
|
|
627
|
-
];
|
|
628
|
-
function isMinorKey(key) {
|
|
629
|
-
const hasMinorThird = key.modeMask12 >> 3 & 1;
|
|
630
|
-
const hasMajorThird2 = key.modeMask12 >> 4 & 1;
|
|
631
|
-
return Boolean(hasMinorThird) && !hasMajorThird2;
|
|
632
|
-
}
|
|
633
|
-
function functionOf(chord, key) {
|
|
634
|
-
return FUNCTION_BY_OFFSET[mod12(chord.rootPc - key.rootPc)] ?? "tonic";
|
|
635
|
-
}
|
|
636
|
-
function isDiatonic(chord, key) {
|
|
637
|
-
return chordPitchClasses(chord).every((pc) => isScaleTone(pc, key));
|
|
638
|
-
}
|
|
639
|
-
function parallelKey(key) {
|
|
640
|
-
return {
|
|
641
|
-
rootPc: mod12(key.rootPc),
|
|
642
|
-
modeMask12: isMinorKey(key) ? MAJOR_MASK : NATURAL_MINOR_MASK
|
|
643
|
-
};
|
|
644
|
-
}
|
|
645
|
-
function isDiminishedQuality(quality) {
|
|
646
|
-
return quality === "dim" || quality === "dim7" || quality === "m7b5";
|
|
647
|
-
}
|
|
648
|
-
function hasMajorThird(chord) {
|
|
649
|
-
return chord.intervals.some((interval) => mod12(interval) === 4);
|
|
650
|
-
}
|
|
651
|
-
function qualityAwareFunction(chord, key) {
|
|
652
|
-
const offset = mod12(chord.rootPc - key.rootPc);
|
|
653
|
-
if (isDiminishedQuality(chord.quality) && (offset === 11 || offset === 6)) {
|
|
654
|
-
return "dominant";
|
|
655
|
-
}
|
|
656
|
-
if (isNeapolitan(chord, key)) {
|
|
657
|
-
return "subdominant";
|
|
658
|
-
}
|
|
659
|
-
if (!isMinorKey(key) && hasMajorThird(chord) && (offset === 8 || offset === 10)) {
|
|
660
|
-
return "subdominant";
|
|
661
|
-
}
|
|
662
|
-
return functionOf(chord, key);
|
|
663
|
-
}
|
|
664
|
-
function analyzeChord(chord, key) {
|
|
665
|
-
const source = borrowedSource(chord, key);
|
|
666
|
-
return {
|
|
667
|
-
function: qualityAwareFunction(chord, key),
|
|
668
|
-
borrowed: source !== null,
|
|
669
|
-
source,
|
|
670
|
-
roman: chordToRoman(chord, key)
|
|
671
|
-
};
|
|
672
|
-
}
|
|
673
17
|
|
|
674
|
-
// src/analyze/functional/borrowed.ts
|
|
675
|
-
function isHarmonicMinorAlteration(chord, key) {
|
|
676
|
-
return isMinorKey(key) && isDiatonic(chord, { rootPc: mod12(key.rootPc), modeMask12: HARMONIC_MINOR_MASK });
|
|
677
|
-
}
|
|
678
|
-
function isBorrowedChord(chord, key) {
|
|
679
|
-
if (isDiatonic(chord, key)) {
|
|
680
|
-
return false;
|
|
681
|
-
}
|
|
682
|
-
if (isHarmonicMinorAlteration(chord, key)) {
|
|
683
|
-
return false;
|
|
684
|
-
}
|
|
685
|
-
return isDiatonic(chord, parallelKey(key));
|
|
686
|
-
}
|
|
687
|
-
function borrowedSource(chord, key) {
|
|
688
|
-
if (isDiatonic(chord, key)) {
|
|
689
|
-
return null;
|
|
690
|
-
}
|
|
691
|
-
if (isNeapolitan(chord, key)) {
|
|
692
|
-
return "neapolitan";
|
|
693
|
-
}
|
|
694
|
-
if (isBorrowedChord(chord, key)) {
|
|
695
|
-
return isMinorKey(key) ? "parallel-major" : "parallel-minor";
|
|
696
|
-
}
|
|
697
|
-
return null;
|
|
698
|
-
}
|
|
699
18
|
|
|
700
|
-
|
|
701
|
-
function detectCadence(from, to, key) {
|
|
702
|
-
const tonic = mod12(key.rootPc);
|
|
703
|
-
const fromOffset = mod12(from.rootPc - tonic);
|
|
704
|
-
const toOffset = mod12(to.rootPc - tonic);
|
|
705
|
-
const deceptiveTargets = isMinorKey(key) ? [8] : [8, 9];
|
|
706
|
-
if (fromOffset === 7 && toOffset === 0) {
|
|
707
|
-
return "authentic";
|
|
708
|
-
}
|
|
709
|
-
if (fromOffset === 5 && toOffset === 0) {
|
|
710
|
-
return "plagal";
|
|
711
|
-
}
|
|
712
|
-
if (fromOffset === 7 && deceptiveTargets.includes(toOffset)) {
|
|
713
|
-
return "deceptive";
|
|
714
|
-
}
|
|
715
|
-
if (toOffset === 7 && fromOffset !== 7) {
|
|
716
|
-
return "half";
|
|
717
|
-
}
|
|
718
|
-
return null;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
// src/generate/reharmony/index.ts
|
|
722
|
-
function mod122(n) {
|
|
723
|
-
return (n % 12 + 12) % 12;
|
|
724
|
-
}
|
|
725
|
-
function negativeHarmonyMirror(chord, key) {
|
|
726
|
-
const tonic = mod122(key.rootPc);
|
|
727
|
-
const mirror = (p) => mod122(2 * tonic + 7 - p);
|
|
728
|
-
const pcs = chordPitchClasses(chord).map(mirror);
|
|
729
|
-
const bassPc = chord.bassPc !== void 0 ? mirror(chord.bassPc) : void 0;
|
|
730
|
-
const pitches = bassPc !== void 0 ? [bassPc - 12, ...pcs] : pcs;
|
|
731
|
-
const detected = detectChordBest(pitches);
|
|
732
|
-
if (detected) {
|
|
733
|
-
if (bassPc === void 0 && detected.bassPc !== void 0) {
|
|
734
|
-
return makeChord(detected.rootPc, detected.quality);
|
|
735
|
-
}
|
|
736
|
-
return detected;
|
|
737
|
-
}
|
|
738
|
-
const quality = chord.quality;
|
|
739
|
-
return makeChord(mirror(chord.rootPc), quality, bassPc);
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
// src/theory/chordscale/index.ts
|
|
743
|
-
function pitchClass4(value) {
|
|
744
|
-
return (Math.trunc(value) % 12 + 12) % 12;
|
|
745
|
-
}
|
|
746
|
-
function popcount12(mask) {
|
|
747
|
-
let count = 0;
|
|
748
|
-
for (let n = 0; n < 12; n += 1) {
|
|
749
|
-
count += mask >> n & 1;
|
|
750
|
-
}
|
|
751
|
-
return count;
|
|
752
|
-
}
|
|
753
|
-
function maskHasPitchClass(mask, scaleRootPc, pc) {
|
|
754
|
-
const offset = pitchClass4(pc - scaleRootPc);
|
|
755
|
-
return (mask >> offset & 1) === 1;
|
|
756
|
-
}
|
|
757
|
-
function scaleMatchesChord(chordPcs, scaleMask, scaleRootPc) {
|
|
758
|
-
for (const pc of chordPcs) {
|
|
759
|
-
if (!maskHasPitchClass(scaleMask, scaleRootPc, pc)) {
|
|
760
|
-
return false;
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
return true;
|
|
764
|
-
}
|
|
765
|
-
var ALIASED_SCALE_NAMES = /* @__PURE__ */ new Set(["major", "naturalMinor"]);
|
|
766
|
-
var HEPTATONIC_SIZE = 7;
|
|
767
|
-
function chordScales(chord) {
|
|
768
|
-
const chordPcs = chordPitchClasses(chord);
|
|
769
|
-
const rootPc = pitchClass4(chord.rootPc);
|
|
770
|
-
const ranked = [];
|
|
771
|
-
const seenMasks = /* @__PURE__ */ new Set();
|
|
772
|
-
for (const name of Object.keys(NAMED_SCALES)) {
|
|
773
|
-
if (name === "chromatic" || ALIASED_SCALE_NAMES.has(name)) {
|
|
774
|
-
continue;
|
|
775
|
-
}
|
|
776
|
-
const mask = NAMED_SCALES[name];
|
|
777
|
-
if (mask === void 0 || seenMasks.has(mask)) {
|
|
778
|
-
continue;
|
|
779
|
-
}
|
|
780
|
-
seenMasks.add(mask);
|
|
781
|
-
if (scaleMatchesChord(chordPcs, mask, rootPc)) {
|
|
782
|
-
const size = popcount12(mask);
|
|
783
|
-
ranked.push({ name, extra: size - chordPcs.length, size });
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
if (ranked.length === 0) {
|
|
787
|
-
if (NAMED_SCALES.chromatic !== void 0) {
|
|
788
|
-
return [{ name: "chromatic", rootPc }];
|
|
789
|
-
}
|
|
790
|
-
return [];
|
|
791
|
-
}
|
|
792
|
-
const preferHeptatonic = chordPcs.length <= 3;
|
|
793
|
-
ranked.sort((a, b) => {
|
|
794
|
-
if (preferHeptatonic && a.size === HEPTATONIC_SIZE !== (b.size === HEPTATONIC_SIZE)) {
|
|
795
|
-
return a.size === HEPTATONIC_SIZE ? -1 : 1;
|
|
796
|
-
}
|
|
797
|
-
if (a.extra !== b.extra) {
|
|
798
|
-
return a.extra - b.extra;
|
|
799
|
-
}
|
|
800
|
-
if (a.size !== b.size) {
|
|
801
|
-
return a.size - b.size;
|
|
802
|
-
}
|
|
803
|
-
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
804
|
-
});
|
|
805
|
-
return ranked.map((entry) => ({ name: entry.name, rootPc }));
|
|
806
|
-
}
|
|
807
|
-
function avoidNotes(chord, scaleName) {
|
|
808
|
-
const mask = NAMED_SCALES[scaleName];
|
|
809
|
-
if (mask === void 0) {
|
|
810
|
-
return [];
|
|
811
|
-
}
|
|
812
|
-
const rootPc = pitchClass4(chord.rootPc);
|
|
813
|
-
const chordPcs = chordPitchClasses(chord);
|
|
814
|
-
if (!scaleMatchesChord(chordPcs, mask, rootPc)) {
|
|
815
|
-
return [];
|
|
816
|
-
}
|
|
817
|
-
const chordSet = new Set(chordPcs);
|
|
818
|
-
const avoid = [];
|
|
819
|
-
for (let pc = 0; pc < 12; pc += 1) {
|
|
820
|
-
if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc)) {
|
|
821
|
-
continue;
|
|
822
|
-
}
|
|
823
|
-
if (chordSet.has(pitchClass4(pc - 1))) {
|
|
824
|
-
avoid.push(pc);
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
return avoid.sort((a, b) => a - b);
|
|
828
|
-
}
|
|
829
|
-
function availableTensions(chord, scaleName) {
|
|
830
|
-
const mask = NAMED_SCALES[scaleName];
|
|
831
|
-
if (mask === void 0) {
|
|
832
|
-
return [];
|
|
833
|
-
}
|
|
834
|
-
const rootPc = pitchClass4(chord.rootPc);
|
|
835
|
-
const chordPcs = chordPitchClasses(chord);
|
|
836
|
-
if (!scaleMatchesChord(chordPcs, mask, rootPc)) {
|
|
837
|
-
return [];
|
|
838
|
-
}
|
|
839
|
-
const chordSet = new Set(chordPcs);
|
|
840
|
-
const avoidSet = new Set(avoidNotes(chord, scaleName));
|
|
841
|
-
const tensions = [];
|
|
842
|
-
for (let pc = 0; pc < 12; pc += 1) {
|
|
843
|
-
if (!maskHasPitchClass(mask, rootPc, pc) || chordSet.has(pc) || avoidSet.has(pc)) {
|
|
844
|
-
continue;
|
|
845
|
-
}
|
|
846
|
-
tensions.push(pc);
|
|
847
|
-
}
|
|
848
|
-
return tensions.sort((a, b) => a - b);
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
// src/core/pitch/index.ts
|
|
852
|
-
var LETTER_SEMITONES = [0, 2, 4, 5, 7, 9, 11];
|
|
853
|
-
var LETTER_NAMES = ["C", "D", "E", "F", "G", "A", "B"];
|
|
854
|
-
function mod123(n) {
|
|
855
|
-
return (n % 12 + 12) % 12;
|
|
856
|
-
}
|
|
857
|
-
function mod7(n) {
|
|
858
|
-
return (n % 7 + 7) % 7;
|
|
859
|
-
}
|
|
860
|
-
function parseNote(text) {
|
|
861
|
-
const match = /^([A-Ga-g])([#x]*|b*)(-?\d+)?$/.exec(text.trim());
|
|
862
|
-
if (!match) {
|
|
863
|
-
throw new Error(`Invalid note: ${text}`);
|
|
864
|
-
}
|
|
865
|
-
const letterGlyph = (match[1] ?? "").toUpperCase();
|
|
866
|
-
const letter = LETTER_NAMES.indexOf(letterGlyph);
|
|
867
|
-
let alter = 0;
|
|
868
|
-
for (const ch of match[2] ?? "") {
|
|
869
|
-
if (ch === "#") {
|
|
870
|
-
alter += 1;
|
|
871
|
-
} else if (ch === "x") {
|
|
872
|
-
alter += 2;
|
|
873
|
-
} else if (ch === "b") {
|
|
874
|
-
alter -= 1;
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
const note = { letter, alter };
|
|
878
|
-
if (match[3] !== void 0) {
|
|
879
|
-
note.octave = Number.parseInt(match[3], 10);
|
|
880
|
-
}
|
|
881
|
-
return note;
|
|
882
|
-
}
|
|
883
|
-
function formatAlter(alter) {
|
|
884
|
-
if (alter > 0) {
|
|
885
|
-
return "#".repeat(alter);
|
|
886
|
-
}
|
|
887
|
-
if (alter < 0) {
|
|
888
|
-
return "b".repeat(-alter);
|
|
889
|
-
}
|
|
890
|
-
return "";
|
|
891
|
-
}
|
|
892
|
-
function formatNote(note) {
|
|
893
|
-
const glyph = LETTER_NAMES[mod7(note.letter)] ?? "C";
|
|
894
|
-
const octave = note.octave === void 0 ? "" : String(note.octave);
|
|
895
|
-
return `${glyph}${formatAlter(note.alter)}${octave}`;
|
|
896
|
-
}
|
|
897
|
-
function noteToPitchClass(note) {
|
|
898
|
-
const natural = LETTER_SEMITONES[mod7(note.letter)] ?? 0;
|
|
899
|
-
return mod123(natural + note.alter);
|
|
900
|
-
}
|
|
901
|
-
function noteToMidi(note) {
|
|
902
|
-
if (note.octave === void 0) {
|
|
903
|
-
throw new Error("noteToMidi requires an octave");
|
|
904
|
-
}
|
|
905
|
-
const natural = LETTER_SEMITONES[mod7(note.letter)] ?? 0;
|
|
906
|
-
return (note.octave + 1) * 12 + natural + note.alter;
|
|
907
|
-
}
|
|
908
|
-
var SHARP_SPELLING = [
|
|
909
|
-
[0, 0],
|
|
910
|
-
[0, 1],
|
|
911
|
-
[1, 0],
|
|
912
|
-
[1, 1],
|
|
913
|
-
[2, 0],
|
|
914
|
-
[3, 0],
|
|
915
|
-
[3, 1],
|
|
916
|
-
[4, 0],
|
|
917
|
-
[4, 1],
|
|
918
|
-
[5, 0],
|
|
919
|
-
[5, 1],
|
|
920
|
-
[6, 0]
|
|
921
|
-
];
|
|
922
|
-
var FLAT_SPELLING = [
|
|
923
|
-
[0, 0],
|
|
924
|
-
[1, -1],
|
|
925
|
-
[1, 0],
|
|
926
|
-
[2, -1],
|
|
927
|
-
[2, 0],
|
|
928
|
-
[3, 0],
|
|
929
|
-
[4, -1],
|
|
930
|
-
[4, 0],
|
|
931
|
-
[5, -1],
|
|
932
|
-
[5, 0],
|
|
933
|
-
[6, -1],
|
|
934
|
-
[6, 0]
|
|
935
|
-
];
|
|
936
|
-
function midiToNote(midi, spelling = "sharp") {
|
|
937
|
-
const rounded = Math.round(midi);
|
|
938
|
-
const pc = mod123(rounded);
|
|
939
|
-
const octave = Math.floor(rounded / 12) - 1;
|
|
940
|
-
const table = spelling === "sharp" ? SHARP_SPELLING : FLAT_SPELLING;
|
|
941
|
-
const entry = table[pc] ?? [0, 0];
|
|
942
|
-
return { letter: entry[0], alter: entry[1], octave };
|
|
943
|
-
}
|
|
944
|
-
var SIMPLE_REFERENCE = [0, 0, 2, 4, 5, 7, 9, 11];
|
|
945
|
-
function isPerfectNumber(simpleNumber) {
|
|
946
|
-
return simpleNumber === 1 || simpleNumber === 4 || simpleNumber === 5;
|
|
947
|
-
}
|
|
948
|
-
function qualityFromSpan(numberValue, semitones) {
|
|
949
|
-
const octaves = Math.floor((numberValue - 1) / 7);
|
|
950
|
-
const simple = numberValue - 7 * octaves;
|
|
951
|
-
const reference = (SIMPLE_REFERENCE[simple] ?? 0) + 12 * octaves;
|
|
952
|
-
const delta = semitones - reference;
|
|
953
|
-
if (isPerfectNumber(simple)) {
|
|
954
|
-
if (delta === 0) {
|
|
955
|
-
return "P";
|
|
956
|
-
}
|
|
957
|
-
return delta > 0 ? "A".repeat(delta) : "d".repeat(-delta);
|
|
958
|
-
}
|
|
959
|
-
if (delta === 0) {
|
|
960
|
-
return "M";
|
|
961
|
-
}
|
|
962
|
-
if (delta === -1) {
|
|
963
|
-
return "m";
|
|
964
|
-
}
|
|
965
|
-
return delta > 0 ? "A".repeat(delta) : "d".repeat(-delta - 1);
|
|
966
|
-
}
|
|
967
|
-
function diatonicIndex(note) {
|
|
968
|
-
return mod7(note.letter) + 7 * (note.octave ?? 0);
|
|
969
|
-
}
|
|
970
|
-
function spelledInterval(a, b) {
|
|
971
|
-
const octaved = a.octave !== void 0 && b.octave !== void 0;
|
|
972
|
-
let letterSteps;
|
|
973
|
-
let semitones;
|
|
974
|
-
if (octaved) {
|
|
975
|
-
letterSteps = diatonicIndex(b) - diatonicIndex(a);
|
|
976
|
-
semitones = noteToMidi(b) - noteToMidi(a);
|
|
977
|
-
} else {
|
|
978
|
-
letterSteps = mod7(b.letter - a.letter);
|
|
979
|
-
let rawSemis = mod123(noteToPitchClass(b) - noteToPitchClass(a));
|
|
980
|
-
const reference = SIMPLE_REFERENCE[letterSteps + 1] ?? 0;
|
|
981
|
-
if (reference - rawSemis > 6) {
|
|
982
|
-
rawSemis += 12;
|
|
983
|
-
} else if (rawSemis - reference > 6) {
|
|
984
|
-
rawSemis -= 12;
|
|
985
|
-
}
|
|
986
|
-
semitones = rawSemis;
|
|
987
|
-
}
|
|
988
|
-
const absSteps = Math.abs(letterSteps);
|
|
989
|
-
const number = absSteps + 1;
|
|
990
|
-
const quality = qualityFromSpan(number, Math.abs(semitones));
|
|
991
|
-
return { number, quality, semitones };
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
// src/theory/spelling/index.ts
|
|
995
|
-
var LETTER_SEMITONES2 = [0, 2, 4, 5, 7, 9, 11];
|
|
996
|
-
var CHROMATIC_SPELLING = {
|
|
997
|
-
1: { degreeOffset: 1, alter: -1 },
|
|
998
|
-
// b2
|
|
999
|
-
3: { degreeOffset: 2, alter: -1 },
|
|
1000
|
-
// b3
|
|
1001
|
-
6: { degreeOffset: 3, alter: 1 },
|
|
1002
|
-
// #4
|
|
1003
|
-
8: { degreeOffset: 5, alter: -1 },
|
|
1004
|
-
// b6
|
|
1005
|
-
9: { degreeOffset: 5, alter: 1 },
|
|
1006
|
-
// #6 (raised sixth of a minor key)
|
|
1007
|
-
10: { degreeOffset: 6, alter: -1 },
|
|
1008
|
-
// b7
|
|
1009
|
-
11: { degreeOffset: 6, alter: 1 }
|
|
1010
|
-
// #7 (raised leading tone of a minor key)
|
|
1011
|
-
};
|
|
1012
|
-
function mod124(n) {
|
|
1013
|
-
return (n % 12 + 12) % 12;
|
|
1014
|
-
}
|
|
1015
|
-
function mod72(n) {
|
|
1016
|
-
return (n % 7 + 7) % 7;
|
|
1017
|
-
}
|
|
1018
|
-
function naturalPc(letter) {
|
|
1019
|
-
return LETTER_SEMITONES2[mod72(letter)] ?? 0;
|
|
1020
|
-
}
|
|
1021
|
-
function alterFor(letter, pc) {
|
|
1022
|
-
let d = mod124(pc - naturalPc(letter));
|
|
1023
|
-
if (d > 6) {
|
|
1024
|
-
d -= 12;
|
|
1025
|
-
}
|
|
1026
|
-
return d;
|
|
1027
|
-
}
|
|
1028
|
-
function isHeptatonic(key) {
|
|
1029
|
-
return scaleTonesInDegreeOrder(key).length === 7;
|
|
1030
|
-
}
|
|
1031
|
-
function spellPitchClass(pc, tonic, key) {
|
|
1032
|
-
const tonicPc = mod124(naturalPc(tonic.letter) + tonic.alter);
|
|
1033
|
-
const offset = mod124(pc - tonicPc);
|
|
1034
|
-
const tones = scaleTonesInDegreeOrder(key);
|
|
1035
|
-
if (isHeptatonic(key)) {
|
|
1036
|
-
const degree = tones.indexOf(mod124(pc));
|
|
1037
|
-
if (degree >= 0) {
|
|
1038
|
-
const letter = mod72(tonic.letter + degree);
|
|
1039
|
-
return { letter, alter: alterFor(letter, pc) };
|
|
1040
|
-
}
|
|
1041
|
-
const chromatic = CHROMATIC_SPELLING[offset];
|
|
1042
|
-
if (chromatic) {
|
|
1043
|
-
const letter = mod72(tonic.letter + chromatic.degreeOffset);
|
|
1044
|
-
return { letter, alter: alterFor(letter, pc) };
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
for (let letter = 0; letter < 7; letter += 1) {
|
|
1048
|
-
if (naturalPc(letter) === mod124(pc)) {
|
|
1049
|
-
return { letter, alter: 0 };
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
const belowLetter = mod72([0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6][mod124(pc)] ?? 0);
|
|
1053
|
-
return { letter: belowLetter, alter: alterFor(belowLetter, pc) };
|
|
1054
|
-
}
|
|
1055
|
-
function spellScale(tonic, key) {
|
|
1056
|
-
return scaleTonesInDegreeOrder(key).map((pc) => spellPitchClass(pc, tonic, key));
|
|
1057
|
-
}
|
|
1058
|
-
function spellChord(chord, tonic, key) {
|
|
1059
|
-
return chord.intervals.map(
|
|
1060
|
-
(interval) => spellPitchClass(mod124(chord.rootPc + interval), tonic, key)
|
|
1061
|
-
);
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
// src/theory/symbol/index.ts
|
|
1065
|
-
var ROOT_RE = /^[A-Ga-g](?:#{1,2}|b{1,2})?/;
|
|
1066
|
-
var BASS_RE = /^[A-Ga-g](?:#{1,2}|b{1,2})?$/;
|
|
1067
|
-
var QUALITY_MAP = {
|
|
1068
|
-
"": "maj",
|
|
1069
|
-
m: "min",
|
|
1070
|
-
min: "min",
|
|
1071
|
-
"-": "min",
|
|
1072
|
-
dim: "dim",
|
|
1073
|
-
"\xB0": "dim",
|
|
1074
|
-
aug: "aug",
|
|
1075
|
-
"+": "aug",
|
|
1076
|
-
maj7: "maj7",
|
|
1077
|
-
M7: "maj7",
|
|
1078
|
-
\u0394: "maj7",
|
|
1079
|
-
\u03947: "maj7",
|
|
1080
|
-
m7: "min7",
|
|
1081
|
-
min7: "min7",
|
|
1082
|
-
"-7": "min7",
|
|
1083
|
-
"7": "dom7",
|
|
1084
|
-
dim7: "dim7",
|
|
1085
|
-
"\xB07": "dim7",
|
|
1086
|
-
m7b5: "m7b5",
|
|
1087
|
-
\u00F8: "m7b5",
|
|
1088
|
-
\u00F87: "m7b5",
|
|
1089
|
-
min7b5: "m7b5",
|
|
1090
|
-
mMaj7: "minMaj7",
|
|
1091
|
-
mM7: "minMaj7",
|
|
1092
|
-
minMaj7: "minMaj7",
|
|
1093
|
-
aug7: "aug7",
|
|
1094
|
-
"+7": "aug7",
|
|
1095
|
-
"7#5": "aug7",
|
|
1096
|
-
augMaj7: "augMaj7",
|
|
1097
|
-
augM7: "augMaj7",
|
|
1098
|
-
"+maj7": "augMaj7",
|
|
1099
|
-
"+M7": "augMaj7",
|
|
1100
|
-
majb5: "majb5",
|
|
1101
|
-
"maj(b5)": "majb5",
|
|
1102
|
-
"(b5)": "majb5",
|
|
1103
|
-
"6": "6",
|
|
1104
|
-
m6: "min6",
|
|
1105
|
-
min6: "min6",
|
|
1106
|
-
"6/9": "6/9",
|
|
1107
|
-
"69": "6/9",
|
|
1108
|
-
sus2: "sus2",
|
|
1109
|
-
sus: "sus4",
|
|
1110
|
-
sus4: "sus4",
|
|
1111
|
-
add9: "add9",
|
|
1112
|
-
add11: "add11",
|
|
1113
|
-
maj9: "maj9",
|
|
1114
|
-
M9: "maj9",
|
|
1115
|
-
m9: "min9",
|
|
1116
|
-
min9: "min9",
|
|
1117
|
-
"9": "dom9",
|
|
1118
|
-
"7b9": "7b9",
|
|
1119
|
-
"7#9": "7#9",
|
|
1120
|
-
"7#11": "7#11",
|
|
1121
|
-
"7b13": "7b13",
|
|
1122
|
-
"11": "11",
|
|
1123
|
-
"13": "13",
|
|
1124
|
-
"5": "5"
|
|
1125
|
-
};
|
|
1126
|
-
var CANONICAL_SUFFIX = {
|
|
1127
|
-
maj: "",
|
|
1128
|
-
min: "m",
|
|
1129
|
-
dim: "dim",
|
|
1130
|
-
aug: "aug",
|
|
1131
|
-
maj7: "maj7",
|
|
1132
|
-
min7: "m7",
|
|
1133
|
-
dom7: "7",
|
|
1134
|
-
dim7: "dim7",
|
|
1135
|
-
m7b5: "m7b5",
|
|
1136
|
-
minMaj7: "mMaj7",
|
|
1137
|
-
aug7: "aug7",
|
|
1138
|
-
augMaj7: "augMaj7",
|
|
1139
|
-
majb5: "maj(b5)",
|
|
1140
|
-
"6": "6",
|
|
1141
|
-
min6: "m6",
|
|
1142
|
-
"6/9": "6/9",
|
|
1143
|
-
sus2: "sus2",
|
|
1144
|
-
sus4: "sus4",
|
|
1145
|
-
add9: "add9",
|
|
1146
|
-
add11: "add11",
|
|
1147
|
-
maj9: "maj9",
|
|
1148
|
-
min9: "m9",
|
|
1149
|
-
dom9: "9",
|
|
1150
|
-
"7b9": "7b9",
|
|
1151
|
-
"7#9": "7#9",
|
|
1152
|
-
"7#11": "7#11",
|
|
1153
|
-
"7b13": "7b13",
|
|
1154
|
-
"11": "11",
|
|
1155
|
-
"13": "13",
|
|
1156
|
-
"5": "5"
|
|
1157
|
-
};
|
|
1158
|
-
function pitchClass5(value) {
|
|
1159
|
-
return (Math.trunc(value) % 12 + 12) % 12;
|
|
1160
|
-
}
|
|
1161
|
-
function makeSpelledChord(root, quality, bass) {
|
|
1162
|
-
const chord = makeChord(
|
|
1163
|
-
noteToPitchClass(root),
|
|
1164
|
-
quality,
|
|
1165
|
-
bass === void 0 ? void 0 : noteToPitchClass(bass)
|
|
1166
|
-
);
|
|
1167
|
-
chord.rootSpelling = { letter: root.letter, alter: root.alter };
|
|
1168
|
-
if (bass !== void 0) {
|
|
1169
|
-
chord.bassSpelling = { letter: bass.letter, alter: bass.alter };
|
|
1170
|
-
}
|
|
1171
|
-
return chord;
|
|
1172
|
-
}
|
|
1173
|
-
function parseChordSymbol(text) {
|
|
1174
|
-
const trimmed = text.trim();
|
|
1175
|
-
const rootMatch = ROOT_RE.exec(trimmed);
|
|
1176
|
-
if (!rootMatch) {
|
|
1177
|
-
throw new Error(`Invalid chord symbol: ${text}`);
|
|
1178
|
-
}
|
|
1179
|
-
const rootToken = rootMatch[0];
|
|
1180
|
-
const rootNote = parseNote(rootToken);
|
|
1181
|
-
const rest = trimmed.slice(rootToken.length);
|
|
1182
|
-
const slashIndex = rest.lastIndexOf("/");
|
|
1183
|
-
if (slashIndex !== -1) {
|
|
1184
|
-
const before = rest.slice(0, slashIndex);
|
|
1185
|
-
const after = rest.slice(slashIndex + 1);
|
|
1186
|
-
const beforeQuality = QUALITY_MAP[before];
|
|
1187
|
-
if (beforeQuality !== void 0 && BASS_RE.test(after)) {
|
|
1188
|
-
return makeSpelledChord(rootNote, beforeQuality, parseNote(after));
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
const quality = QUALITY_MAP[rest];
|
|
1192
|
-
if (quality === void 0) {
|
|
1193
|
-
throw new Error(`Unrecognized chord quality: ${text}`);
|
|
1194
|
-
}
|
|
1195
|
-
return makeSpelledChord(rootNote, quality);
|
|
1196
|
-
}
|
|
1197
|
-
function pitchClassName(pc, hint, flats) {
|
|
1198
|
-
if (flats === void 0 && hint !== void 0 && noteToPitchClass(hint) === pc) {
|
|
1199
|
-
return formatNote(hint);
|
|
1200
|
-
}
|
|
1201
|
-
const note = midiToNote(60 + pc, flats ? "flat" : "sharp");
|
|
1202
|
-
return formatNote({ letter: note.letter, alter: note.alter });
|
|
1203
|
-
}
|
|
1204
|
-
function formatChordSymbol(chord, opts) {
|
|
1205
|
-
const rootPc = pitchClass5(chord.rootPc);
|
|
1206
|
-
const rootName = pitchClassName(rootPc, chord.rootSpelling, opts?.flats);
|
|
1207
|
-
const suffix = CANONICAL_SUFFIX[chord.quality];
|
|
1208
|
-
let symbol = `${rootName}${suffix}`;
|
|
1209
|
-
if (chord.bassPc !== void 0 && pitchClass5(chord.bassPc) !== rootPc) {
|
|
1210
|
-
symbol += `/${pitchClassName(pitchClass5(chord.bassPc), chord.bassSpelling, opts?.flats)}`;
|
|
1211
|
-
}
|
|
1212
|
-
return symbol;
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
// src/theory/counterpoint/index.ts
|
|
1216
|
-
function createsVoiceCrossing(upperPitch, lowerPitch) {
|
|
1217
|
-
return upperPitch < lowerPitch;
|
|
1218
|
-
}
|
|
1219
|
-
function simpleClass(semitones) {
|
|
1220
|
-
return Math.abs(semitones) % 12;
|
|
1221
|
-
}
|
|
1222
|
-
function isPerfectClass(cls) {
|
|
1223
|
-
return cls === 0 || cls === 7;
|
|
1224
|
-
}
|
|
1225
|
-
function similarMotion(aMove, bMove) {
|
|
1226
|
-
return aMove !== 0 && bMove !== 0 && aMove > 0 === bMove > 0;
|
|
1227
|
-
}
|
|
1228
|
-
function bothVoicesMove(aMove, bMove) {
|
|
1229
|
-
return aMove !== 0 && bMove !== 0;
|
|
1230
|
-
}
|
|
1231
|
-
function createsParallelPerfect(aPrev, aCur, bPrev, bCur) {
|
|
1232
|
-
if (!bothVoicesMove(aCur - aPrev, bCur - bPrev)) {
|
|
1233
|
-
return false;
|
|
1234
|
-
}
|
|
1235
|
-
const nowClass = simpleClass(aCur - bCur);
|
|
1236
|
-
const prevClass = simpleClass(aPrev - bPrev);
|
|
1237
|
-
return isPerfectClass(nowClass) && nowClass === prevClass;
|
|
1238
|
-
}
|
|
1239
|
-
function createsHiddenParallelPerfect(aPrev, aCur, bPrev, bCur) {
|
|
1240
|
-
const aMove = aCur - aPrev;
|
|
1241
|
-
const bMove = bCur - bPrev;
|
|
1242
|
-
if (!similarMotion(aMove, bMove)) {
|
|
1243
|
-
return false;
|
|
1244
|
-
}
|
|
1245
|
-
const nowClass = simpleClass(aCur - bCur);
|
|
1246
|
-
const prevClass = simpleClass(aPrev - bPrev);
|
|
1247
|
-
if (!isPerfectClass(nowClass) || prevClass === nowClass) {
|
|
1248
|
-
return false;
|
|
1249
|
-
}
|
|
1250
|
-
const upperMove = aCur >= bCur ? aMove : bMove;
|
|
1251
|
-
if (Math.abs(upperMove) <= 2) {
|
|
1252
|
-
return false;
|
|
1253
|
-
}
|
|
1254
|
-
return true;
|
|
1255
|
-
}
|
|
1256
|
-
function createsVoiceOverlap(upperPrev, upperCur, lowerPrev, lowerCur) {
|
|
1257
|
-
return upperCur < lowerPrev || lowerCur > upperPrev;
|
|
1258
|
-
}
|
|
1259
|
-
function exceedsSpacing(upperPitch, lowerPitch, maxSemitones = 12) {
|
|
1260
|
-
return Math.abs(upperPitch - lowerPitch) > maxSemitones;
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
// src/theory/voicing/internal.ts
|
|
1264
|
-
var DEFAULT_MAX_SPACING = 12;
|
|
1265
|
-
var VIOLATION_PENALTY = 1e3;
|
|
1266
|
-
var MISSING_TONE_PENALTY = 500;
|
|
1267
|
-
var POOR_DOUBLING_PENALTY = 4;
|
|
1268
|
-
var MAX_CANDIDATES = 4e3;
|
|
1269
|
-
function pitchClass6(value) {
|
|
1270
|
-
return (Math.trunc(value) % 12 + 12) % 12;
|
|
1271
|
-
}
|
|
1272
|
-
function pitchesForPc(pc, range) {
|
|
1273
|
-
const result = [];
|
|
1274
|
-
for (let midi = Math.ceil(range.min); midi <= range.max; midi += 1) {
|
|
1275
|
-
if (pitchClass6(midi) === pc) {
|
|
1276
|
-
result.push(midi);
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
const center = (range.min + range.max) / 2;
|
|
1280
|
-
return result.sort((a, b) => {
|
|
1281
|
-
const da = Math.abs(a - center);
|
|
1282
|
-
const db = Math.abs(b - center);
|
|
1283
|
-
return da === db ? a - b : da - db;
|
|
1284
|
-
});
|
|
1285
|
-
}
|
|
1286
|
-
function enumerateVoicings(chord, ranges, maxSpacing) {
|
|
1287
|
-
const chordPcs = chordPitchClasses(chord);
|
|
1288
|
-
const bassPc = pitchClass6(chord.bassPc ?? chord.rootPc);
|
|
1289
|
-
const results = [];
|
|
1290
|
-
const current = [];
|
|
1291
|
-
const build = (voice) => {
|
|
1292
|
-
if (results.length >= MAX_CANDIDATES) {
|
|
1293
|
-
return;
|
|
1294
|
-
}
|
|
1295
|
-
if (voice === ranges.length) {
|
|
1296
|
-
results.push([...current]);
|
|
1297
|
-
return;
|
|
1298
|
-
}
|
|
1299
|
-
const range = ranges[voice];
|
|
1300
|
-
if (range === void 0) {
|
|
1301
|
-
return;
|
|
1302
|
-
}
|
|
1303
|
-
const pcs = voice === 0 ? [bassPc] : chordPcs;
|
|
1304
|
-
const prev = current[voice - 1];
|
|
1305
|
-
for (const pc of pcs) {
|
|
1306
|
-
for (const pitch of pitchesForPc(pc, range)) {
|
|
1307
|
-
if (prev !== void 0) {
|
|
1308
|
-
if (pitch < prev) {
|
|
1309
|
-
continue;
|
|
1310
|
-
}
|
|
1311
|
-
const spacingLimit = voice === 1 ? maxSpacing + 12 : maxSpacing;
|
|
1312
|
-
if (pitch - prev > spacingLimit) {
|
|
1313
|
-
continue;
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
current.push(pitch);
|
|
1317
|
-
build(voice + 1);
|
|
1318
|
-
current.pop();
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
};
|
|
1322
|
-
build(0);
|
|
1323
|
-
return results;
|
|
1324
|
-
}
|
|
1325
|
-
function structuralPenalty(pitches, chord) {
|
|
1326
|
-
const counts = /* @__PURE__ */ new Map();
|
|
1327
|
-
for (const pitch of pitches) {
|
|
1328
|
-
const pc = pitchClass6(pitch);
|
|
1329
|
-
counts.set(pc, (counts.get(pc) ?? 0) + 1);
|
|
1330
|
-
}
|
|
1331
|
-
let penalty = 0;
|
|
1332
|
-
for (const pc of chordPitchClasses(chord)) {
|
|
1333
|
-
if (!counts.has(pc)) {
|
|
1334
|
-
penalty += MISSING_TONE_PENALTY;
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
const rootPc = pitchClass6(chord.rootPc);
|
|
1338
|
-
const fifthPc = pitchClass6(chord.rootPc + 7);
|
|
1339
|
-
for (const [pc, count] of counts) {
|
|
1340
|
-
if (count > 1 && pc !== rootPc && pc !== fifthPc) {
|
|
1341
|
-
penalty += (count - 1) * POOR_DOUBLING_PENALTY;
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
return penalty;
|
|
1345
|
-
}
|
|
1346
|
-
function violationCount(prev, cur, maxSpacing) {
|
|
1347
|
-
let count = 0;
|
|
1348
|
-
for (let lower = 0; lower < cur.length; lower += 1) {
|
|
1349
|
-
for (let upper = lower + 1; upper < cur.length; upper += 1) {
|
|
1350
|
-
const prevLower = prev[lower];
|
|
1351
|
-
const prevUpper = prev[upper];
|
|
1352
|
-
const curLower = cur[lower];
|
|
1353
|
-
const curUpper = cur[upper];
|
|
1354
|
-
if (prevLower === void 0 || prevUpper === void 0 || curLower === void 0 || curUpper === void 0) {
|
|
1355
|
-
continue;
|
|
1356
|
-
}
|
|
1357
|
-
if (createsParallelPerfect(prevUpper, curUpper, prevLower, curLower)) {
|
|
1358
|
-
count += 1;
|
|
1359
|
-
}
|
|
1360
|
-
if (createsVoiceCrossing(curUpper, curLower)) {
|
|
1361
|
-
count += 1;
|
|
1362
|
-
}
|
|
1363
|
-
if (upper === lower + 1) {
|
|
1364
|
-
if (createsVoiceOverlap(prevUpper, curUpper, prevLower, curLower)) {
|
|
1365
|
-
count += 1;
|
|
1366
|
-
}
|
|
1367
|
-
if (lower > 0 && exceedsSpacing(curUpper, curLower, maxSpacing)) {
|
|
1368
|
-
count += 1;
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
return count;
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
// src/theory/voicing/satb.ts
|
|
1377
|
-
var SATB_RANGES = [
|
|
1378
|
-
{ min: 40, max: 60 },
|
|
1379
|
-
{ min: 48, max: 67 },
|
|
1380
|
-
{ min: 55, max: 74 },
|
|
1381
|
-
{ min: 60, max: 79 }
|
|
1382
|
-
];
|
|
1383
|
-
var DERIVED_LOW = 40;
|
|
1384
|
-
var DERIVED_HIGH = 79;
|
|
1385
|
-
var DERIVED_SPAN = 19;
|
|
1386
|
-
function resolveRanges(opts) {
|
|
1387
|
-
if (opts?.ranges !== void 0) {
|
|
1388
|
-
if (opts.ranges.length === 0) {
|
|
1389
|
-
throw new Error("ranges must contain at least one voice range");
|
|
1390
|
-
}
|
|
1391
|
-
return opts.ranges.map((range) => ({ ...range }));
|
|
1392
|
-
}
|
|
1393
|
-
const voices = opts?.voices ?? 4;
|
|
1394
|
-
if (voices < 1) {
|
|
1395
|
-
throw new Error("voices must be at least 1");
|
|
1396
|
-
}
|
|
1397
|
-
if (voices === 4) {
|
|
1398
|
-
return SATB_RANGES.map((range) => ({ ...range }));
|
|
1399
|
-
}
|
|
1400
|
-
if (voices === 1) {
|
|
1401
|
-
return [{ min: DERIVED_LOW, max: DERIVED_HIGH }];
|
|
1402
|
-
}
|
|
1403
|
-
const ranges = [];
|
|
1404
|
-
for (let i = 0; i < voices; i += 1) {
|
|
1405
|
-
const min = Math.round(
|
|
1406
|
-
DERIVED_LOW + i * (DERIVED_HIGH - DERIVED_SPAN - DERIVED_LOW) / (voices - 1)
|
|
1407
|
-
);
|
|
1408
|
-
ranges.push({ min, max: min + DERIVED_SPAN });
|
|
1409
|
-
}
|
|
1410
|
-
return ranges;
|
|
1411
|
-
}
|
|
1412
|
-
function voiceChord(chord, opts) {
|
|
1413
|
-
const ranges = resolveRanges(opts);
|
|
1414
|
-
const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
|
|
1415
|
-
const candidates = enumerateVoicings(chord, ranges, maxSpacing);
|
|
1416
|
-
let best;
|
|
1417
|
-
let bestScore = Number.POSITIVE_INFINITY;
|
|
1418
|
-
for (const candidate of candidates) {
|
|
1419
|
-
let score = structuralPenalty(candidate, chord);
|
|
1420
|
-
for (let i = 0; i < candidate.length; i += 1) {
|
|
1421
|
-
const pitch = candidate[i];
|
|
1422
|
-
const range = ranges[i];
|
|
1423
|
-
if (pitch === void 0 || range === void 0) {
|
|
1424
|
-
continue;
|
|
1425
|
-
}
|
|
1426
|
-
score += Math.abs(pitch - (range.min + range.max) / 2);
|
|
1427
|
-
}
|
|
1428
|
-
if (score < bestScore) {
|
|
1429
|
-
bestScore = score;
|
|
1430
|
-
best = candidate;
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
if (best === void 0) {
|
|
1434
|
-
throw new Error("no voicing satisfies the given ranges");
|
|
1435
|
-
}
|
|
1436
|
-
return best;
|
|
1437
|
-
}
|
|
1438
|
-
function voiceProgression(chords, opts) {
|
|
1439
|
-
const ranges = resolveRanges(opts);
|
|
1440
|
-
const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
|
|
1441
|
-
const result = [];
|
|
1442
|
-
let prev;
|
|
1443
|
-
for (const chord of chords) {
|
|
1444
|
-
if (prev === void 0) {
|
|
1445
|
-
prev = voiceChord(chord, opts);
|
|
1446
|
-
result.push(prev);
|
|
1447
|
-
continue;
|
|
1448
|
-
}
|
|
1449
|
-
const candidates = enumerateVoicings(chord, ranges, maxSpacing);
|
|
1450
|
-
let best;
|
|
1451
|
-
let bestScore = Number.POSITIVE_INFINITY;
|
|
1452
|
-
for (const candidate of candidates) {
|
|
1453
|
-
const score = structuralPenalty(candidate, chord) + voiceLeadingCost(prev, candidate) + VIOLATION_PENALTY * violationCount(prev, candidate, maxSpacing);
|
|
1454
|
-
if (score < bestScore) {
|
|
1455
|
-
bestScore = score;
|
|
1456
|
-
best = candidate;
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
if (best === void 0) {
|
|
1460
|
-
throw new Error("no voicing satisfies the given ranges");
|
|
1461
|
-
}
|
|
1462
|
-
result.push(best);
|
|
1463
|
-
prev = best;
|
|
1464
|
-
}
|
|
1465
|
-
return result;
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
// src/theory/voicing/leading.ts
|
|
1469
|
-
var HIDDEN_PERFECT_PENALTY = 6;
|
|
1470
|
-
function voiceLeadingCost(from, to) {
|
|
1471
|
-
if (from.length !== to.length) {
|
|
1472
|
-
return Number.POSITIVE_INFINITY;
|
|
1473
|
-
}
|
|
1474
|
-
let total = 0;
|
|
1475
|
-
for (let i = 0; i < from.length; i += 1) {
|
|
1476
|
-
const a = from[i];
|
|
1477
|
-
const b = to[i];
|
|
1478
|
-
if (a === void 0 || b === void 0) {
|
|
1479
|
-
continue;
|
|
1480
|
-
}
|
|
1481
|
-
total += Math.abs(b - a);
|
|
1482
|
-
}
|
|
1483
|
-
if (from.length >= 2) {
|
|
1484
|
-
const bassPrev = from[0];
|
|
1485
|
-
const bassCur = to[0];
|
|
1486
|
-
const sopPrev = from[from.length - 1];
|
|
1487
|
-
const sopCur = to[to.length - 1];
|
|
1488
|
-
if (bassPrev !== void 0 && bassCur !== void 0 && sopPrev !== void 0 && sopCur !== void 0 && createsHiddenParallelPerfect(bassPrev, bassCur, sopPrev, sopCur)) {
|
|
1489
|
-
total += HIDDEN_PERFECT_PENALTY;
|
|
1490
|
-
}
|
|
1491
|
-
}
|
|
1492
|
-
return total;
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
// src/theory/voicing/styled.ts
|
|
1496
|
-
var DEFAULT_STYLE_OCTAVE = 4;
|
|
1497
|
-
function dedupePcs(pcs) {
|
|
1498
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1499
|
-
const result = [];
|
|
1500
|
-
for (const pc of pcs) {
|
|
1501
|
-
if (!seen.has(pc)) {
|
|
1502
|
-
seen.add(pc);
|
|
1503
|
-
result.push(pc);
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
return result;
|
|
1507
|
-
}
|
|
1508
|
-
function lowestPitchAtOrAbove(pc, floor) {
|
|
1509
|
-
return floor + ((pc - pitchClass6(floor)) % 12 + 12) % 12;
|
|
1510
|
-
}
|
|
1511
|
-
function nearestPc(target, pcs) {
|
|
1512
|
-
let best = pcs[0] ?? target;
|
|
1513
|
-
let bestDist = 12;
|
|
1514
|
-
for (const pc of pcs) {
|
|
1515
|
-
const dist = Math.min(((pc - target) % 12 + 12) % 12, ((target - pc) % 12 + 12) % 12);
|
|
1516
|
-
if (dist < bestDist) {
|
|
1517
|
-
bestDist = dist;
|
|
1518
|
-
best = pc;
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
return best;
|
|
1522
|
-
}
|
|
1523
|
-
function voiceChordStyled(chord, opts) {
|
|
1524
|
-
const style = opts?.style ?? "close";
|
|
1525
|
-
const base = 12 * ((opts?.octave ?? DEFAULT_STYLE_OCTAVE) + 1);
|
|
1526
|
-
const rootPc = pitchClass6(chord.rootPc);
|
|
1527
|
-
const bassPc = pitchClass6(chord.bassPc ?? chord.rootPc);
|
|
1528
|
-
const omitRoot = style === "rootless" || opts?.rootless === true;
|
|
1529
|
-
let order = dedupePcs(chord.intervals.map((interval) => pitchClass6(chord.rootPc + interval)));
|
|
1530
|
-
const bassIndex = order.indexOf(bassPc);
|
|
1531
|
-
if (bassIndex > 0) {
|
|
1532
|
-
order = [...order.slice(bassIndex), ...order.slice(0, bassIndex)];
|
|
1533
|
-
} else if (bassIndex < 0) {
|
|
1534
|
-
order = [bassPc, ...order];
|
|
1535
|
-
}
|
|
1536
|
-
if (style === "shell") {
|
|
1537
|
-
const roles = order.map((pc) => chordToneRole(pc, chord));
|
|
1538
|
-
const guide = roles.includes("seventh") ? "seventh" : roles.includes("sixth") ? "sixth" : "fifth";
|
|
1539
|
-
order = order.filter((_pc, i) => {
|
|
1540
|
-
const role = roles[i];
|
|
1541
|
-
return role === "root" || role === "third" || role === guide;
|
|
1542
|
-
});
|
|
1543
|
-
}
|
|
1544
|
-
if (omitRoot) {
|
|
1545
|
-
order = order.filter((pc) => pc !== rootPc);
|
|
1546
|
-
}
|
|
1547
|
-
if (order.length === 0) {
|
|
1548
|
-
return [];
|
|
1549
|
-
}
|
|
1550
|
-
if (opts?.topNote !== void 0) {
|
|
1551
|
-
const target = order.includes(pitchClass6(opts.topNote)) ? pitchClass6(opts.topNote) : nearestPc(pitchClass6(opts.topNote), order);
|
|
1552
|
-
const targetIndex = order.indexOf(target);
|
|
1553
|
-
order = [...order.slice(targetIndex + 1), ...order.slice(0, targetIndex + 1)];
|
|
1554
|
-
}
|
|
1555
|
-
const stack = [];
|
|
1556
|
-
let prev;
|
|
1557
|
-
for (const pc of order) {
|
|
1558
|
-
if (prev === void 0) {
|
|
1559
|
-
prev = lowestPitchAtOrAbove(pc, base);
|
|
1560
|
-
} else {
|
|
1561
|
-
let delta = ((pc - pitchClass6(prev)) % 12 + 12) % 12;
|
|
1562
|
-
if (delta === 0) {
|
|
1563
|
-
delta = 12;
|
|
1564
|
-
}
|
|
1565
|
-
prev += delta;
|
|
1566
|
-
}
|
|
1567
|
-
stack.push(prev);
|
|
1568
|
-
}
|
|
1569
|
-
if (style === "drop2" && stack.length >= 2) {
|
|
1570
|
-
stack[stack.length - 2] = (stack[stack.length - 2] ?? 0) - 12;
|
|
1571
|
-
} else if (style === "drop3" && stack.length >= 3) {
|
|
1572
|
-
stack[stack.length - 3] = (stack[stack.length - 3] ?? 0) - 12;
|
|
1573
|
-
}
|
|
1574
|
-
return stack.sort((a, b) => a - b);
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
// src/model/shared.ts
|
|
1578
|
-
function mod125(n) {
|
|
1579
|
-
return (n % 12 + 12) % 12;
|
|
1580
|
-
}
|
|
1581
|
-
function spellPitchClassBare(pc, spelling) {
|
|
1582
|
-
const spelled = midiToNote(60 + mod125(pc), spelling);
|
|
1583
|
-
return { letter: spelled.letter, alter: spelled.alter };
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
// src/model/note.ts
|
|
1587
|
-
function copyNote(data) {
|
|
1588
|
-
const copy = { letter: data.letter, alter: data.alter };
|
|
1589
|
-
if (data.octave !== void 0) {
|
|
1590
|
-
copy.octave = data.octave;
|
|
1591
|
-
}
|
|
1592
|
-
return copy;
|
|
1593
|
-
}
|
|
1594
|
-
var Note = class _Note {
|
|
1595
|
-
#data;
|
|
1596
|
-
/**
|
|
1597
|
-
* Wrap a plain note object.
|
|
1598
|
-
*
|
|
1599
|
-
* @param data The spelled note; it is copied, never retained or mutated.
|
|
1600
|
-
*/
|
|
1601
|
-
constructor(data) {
|
|
1602
|
-
this.#data = copyNote(data);
|
|
1603
|
-
}
|
|
1604
|
-
/**
|
|
1605
|
-
* Parse scientific pitch notation (e.g. `'C#4'`, `'Bb'`, `'F##3'`).
|
|
1606
|
-
*
|
|
1607
|
-
* @param name The note text.
|
|
1608
|
-
* @returns The parsed note.
|
|
1609
|
-
* @throws If the text is not a valid note.
|
|
1610
|
-
*/
|
|
1611
|
-
static of(name) {
|
|
1612
|
-
return new _Note(parseNote(name));
|
|
1613
|
-
}
|
|
1614
|
-
/**
|
|
1615
|
-
* Name a MIDI number (middle C = C4 = 60) as a note.
|
|
1616
|
-
*
|
|
1617
|
-
* @param midi The MIDI number.
|
|
1618
|
-
* @param spelling Whether to prefer sharps or flats for black keys.
|
|
1619
|
-
* @returns The spelled note, with octave.
|
|
1620
|
-
*/
|
|
1621
|
-
static fromMidi(midi, spelling = "sharp") {
|
|
1622
|
-
return new _Note(midiToNote(midi, spelling));
|
|
1623
|
-
}
|
|
1624
|
-
/**
|
|
1625
|
-
* Wrap an existing plain note object.
|
|
1626
|
-
*
|
|
1627
|
-
* @param data The plain note.
|
|
1628
|
-
* @returns The wrapped note.
|
|
1629
|
-
*/
|
|
1630
|
-
static fromData(data) {
|
|
1631
|
-
return new _Note(data);
|
|
1632
|
-
}
|
|
1633
|
-
/** The note rendered as scientific pitch notation, e.g. `'G4'` or `'Bb'`. */
|
|
1634
|
-
get name() {
|
|
1635
|
-
return formatNote(this.#data);
|
|
1636
|
-
}
|
|
1637
|
-
/** The pitch class (0..11), ignoring octave. */
|
|
1638
|
-
get pitchClass() {
|
|
1639
|
-
return noteToPitchClass(this.#data);
|
|
1640
|
-
}
|
|
1641
|
-
/**
|
|
1642
|
-
* The MIDI number (middle C = C4 = 60).
|
|
1643
|
-
*
|
|
1644
|
-
* @throws If the note has no octave and therefore no fixed pitch.
|
|
1645
|
-
*/
|
|
1646
|
-
get midi() {
|
|
1647
|
-
if (this.#data.octave === void 0) {
|
|
1648
|
-
throw new Error(`note ${this.name} has no octave, so it has no MIDI number`);
|
|
1649
|
-
}
|
|
1650
|
-
return noteToMidi(this.#data);
|
|
1651
|
-
}
|
|
1652
|
-
/** The diatonic letter number: 0..6 for C..B. */
|
|
1653
|
-
get letter() {
|
|
1654
|
-
return this.#data.letter;
|
|
1655
|
-
}
|
|
1656
|
-
/** The chromatic alteration in semitones: -1 flat, 0 natural, +1 sharp, ... */
|
|
1657
|
-
get alter() {
|
|
1658
|
-
return this.#data.alter;
|
|
1659
|
-
}
|
|
1660
|
-
/** The octave (scientific pitch notation), or undefined for a bare pitch class. */
|
|
1661
|
-
get octave() {
|
|
1662
|
-
return this.#data.octave;
|
|
1663
|
-
}
|
|
1664
|
-
/** A copy of the underlying plain note object. */
|
|
1665
|
-
get data() {
|
|
1666
|
-
return copyNote(this.#data);
|
|
1667
|
-
}
|
|
1668
|
-
/**
|
|
1669
|
-
* Transpose by a signed number of semitones.
|
|
1670
|
-
*
|
|
1671
|
-
* When the note carries an octave the transposition happens in MIDI space and
|
|
1672
|
-
* the result is spelled with a sharp preference. An octave-less note stays
|
|
1673
|
-
* octave-less: only its pitch class is moved. Transposing by zero is the
|
|
1674
|
-
* identity: the original spelling is preserved (no enharmonic respelling).
|
|
1675
|
-
*
|
|
1676
|
-
* @param semitones The signed semitone offset.
|
|
1677
|
-
* @returns The transposed note.
|
|
1678
|
-
*/
|
|
1679
|
-
transpose(semitones) {
|
|
1680
|
-
if (semitones === 0) {
|
|
1681
|
-
return new _Note(this.#data);
|
|
1682
|
-
}
|
|
1683
|
-
if (this.#data.octave !== void 0) {
|
|
1684
|
-
return _Note.fromMidi(this.midi + semitones);
|
|
1685
|
-
}
|
|
1686
|
-
return new _Note(spellPitchClassBare(this.pitchClass + semitones, "sharp"));
|
|
1687
|
-
}
|
|
1688
|
-
/**
|
|
1689
|
-
* The spelled interval from this note to another.
|
|
1690
|
-
*
|
|
1691
|
-
* @param other The second note.
|
|
1692
|
-
* @returns The diatonic number, quality, and semitone span.
|
|
1693
|
-
*/
|
|
1694
|
-
intervalTo(other) {
|
|
1695
|
-
return spelledInterval(this.#data, other.#data);
|
|
1696
|
-
}
|
|
1697
|
-
/**
|
|
1698
|
-
* Whether another note has the same letter, alteration, and octave.
|
|
1699
|
-
*
|
|
1700
|
-
* @param other The note to compare.
|
|
1701
|
-
* @returns True if the spellings are identical.
|
|
1702
|
-
*/
|
|
1703
|
-
equals(other) {
|
|
1704
|
-
return this.#data.letter === other.#data.letter && this.#data.alter === other.#data.alter && this.#data.octave === other.#data.octave;
|
|
1705
|
-
}
|
|
1706
|
-
/**
|
|
1707
|
-
* The plain note data, for JSON serialization.
|
|
1708
|
-
*
|
|
1709
|
-
* Private class fields do not serialize, so an explicit `toJSON` keeps
|
|
1710
|
-
* `JSON.stringify(note)` from collapsing to `{}`.
|
|
1711
|
-
*
|
|
1712
|
-
* @returns A copy of the underlying plain note object.
|
|
1713
|
-
*/
|
|
1714
|
-
toJSON() {
|
|
1715
|
-
return this.data;
|
|
1716
|
-
}
|
|
1717
|
-
};
|
|
1718
|
-
|
|
1719
|
-
// src/model/progression.ts
|
|
1720
|
-
var Progression = class _Progression {
|
|
1721
|
-
#chords;
|
|
1722
|
-
#key;
|
|
1723
|
-
/**
|
|
1724
|
-
* Wrap a chord sequence.
|
|
1725
|
-
*
|
|
1726
|
-
* @param chords The chords in order; the array is copied.
|
|
1727
|
-
* @param key Optional key context for analysis methods.
|
|
1728
|
-
*/
|
|
1729
|
-
constructor(chords, key) {
|
|
1730
|
-
this.#chords = [...chords];
|
|
1731
|
-
this.#key = key;
|
|
1732
|
-
}
|
|
1733
|
-
/** A copy of the chord sequence. */
|
|
1734
|
-
get chords() {
|
|
1735
|
-
return [...this.#chords];
|
|
1736
|
-
}
|
|
1737
|
-
/** The carried key context, if any. */
|
|
1738
|
-
get key() {
|
|
1739
|
-
return this.#key;
|
|
1740
|
-
}
|
|
1741
|
-
/** The number of chords. */
|
|
1742
|
-
get length() {
|
|
1743
|
-
return this.#chords.length;
|
|
1744
|
-
}
|
|
1745
|
-
/**
|
|
1746
|
-
* A copy of this progression with a chord appended.
|
|
1747
|
-
*
|
|
1748
|
-
* @param chord The chord to append.
|
|
1749
|
-
* @returns The new progression.
|
|
1750
|
-
*/
|
|
1751
|
-
add(chord) {
|
|
1752
|
-
return new _Progression([...this.#chords, chord], this.#key);
|
|
1753
|
-
}
|
|
1754
|
-
/**
|
|
1755
|
-
* A copy of this progression carrying the given key context.
|
|
1756
|
-
*
|
|
1757
|
-
* @param key The key context to attach.
|
|
1758
|
-
* @returns The new progression.
|
|
1759
|
-
*/
|
|
1760
|
-
withKey(key) {
|
|
1761
|
-
return new _Progression(this.#chords, key);
|
|
1762
|
-
}
|
|
1763
|
-
/**
|
|
1764
|
-
* Voice the progression with smooth voice leading.
|
|
1765
|
-
*
|
|
1766
|
-
* @param opts Voicing options; defaults to four SATB voices.
|
|
1767
|
-
* @returns One ascending voicing (MIDI pitches) per chord.
|
|
1768
|
-
* @throws If any chord admits no voicing within the given ranges.
|
|
1769
|
-
*/
|
|
1770
|
-
voice(opts) {
|
|
1771
|
-
return voiceProgression(
|
|
1772
|
-
this.#chords.map((chord) => chord.data),
|
|
1773
|
-
opts
|
|
1774
|
-
);
|
|
1775
|
-
}
|
|
1776
|
-
/**
|
|
1777
|
-
* The Roman numeral of each chord in a key.
|
|
1778
|
-
*
|
|
1779
|
-
* @param key Key to analyze in; falls back to the carried context.
|
|
1780
|
-
* @returns One numeral per chord.
|
|
1781
|
-
* @throws If no key is given and none is carried.
|
|
1782
|
-
*/
|
|
1783
|
-
roman(key) {
|
|
1784
|
-
const resolved = this.#resolveKey(key);
|
|
1785
|
-
return this.#chords.map((chord) => chord.roman(resolved));
|
|
1786
|
-
}
|
|
1787
|
-
/**
|
|
1788
|
-
* The harmonic function of each chord in a key.
|
|
1789
|
-
*
|
|
1790
|
-
* @param key Key to analyze in; falls back to the carried context.
|
|
1791
|
-
* @returns One function per chord.
|
|
1792
|
-
* @throws If no key is given and none is carried.
|
|
1793
|
-
*/
|
|
1794
|
-
functions(key) {
|
|
1795
|
-
const resolved = this.#resolveKey(key);
|
|
1796
|
-
return this.#chords.map((chord) => chord.function(resolved));
|
|
1797
|
-
}
|
|
1798
|
-
/**
|
|
1799
|
-
* Analyze every chord and classify the closing cadence.
|
|
1800
|
-
*
|
|
1801
|
-
* The cadence is detected on the final chord pair and is null when the
|
|
1802
|
-
* progression has fewer than two chords.
|
|
1803
|
-
*
|
|
1804
|
-
* @param key Key to analyze in; falls back to the carried context.
|
|
1805
|
-
* @returns Per-chord analyses and the closing cadence.
|
|
1806
|
-
* @throws If no key is given and none is carried.
|
|
1807
|
-
*/
|
|
1808
|
-
analyze(key) {
|
|
1809
|
-
const resolved = this.#resolveKey(key);
|
|
1810
|
-
const chords = this.#chords.map((chord) => chord.analyze(resolved));
|
|
1811
|
-
const from = this.#chords[this.#chords.length - 2];
|
|
1812
|
-
const to = this.#chords[this.#chords.length - 1];
|
|
1813
|
-
const cadence = from !== void 0 && to !== void 0 ? detectCadence(from.data, to.data, resolved.scale) : null;
|
|
1814
|
-
return { chords, cadence };
|
|
1815
|
-
}
|
|
1816
|
-
/**
|
|
1817
|
-
* The plain progression data, for JSON serialization.
|
|
1818
|
-
*
|
|
1819
|
-
* Private class fields do not serialize, so an explicit `toJSON` keeps
|
|
1820
|
-
* `JSON.stringify(progression)` from collapsing to `{}`. The chords are
|
|
1821
|
-
* emitted as plain data and the carried key, when present, as its own data.
|
|
1822
|
-
*
|
|
1823
|
-
* @returns The chord data sequence and the carried key, if any.
|
|
1824
|
-
*/
|
|
1825
|
-
toJSON() {
|
|
1826
|
-
return {
|
|
1827
|
-
chords: this.#chords.map((chord) => chord.data),
|
|
1828
|
-
key: this.#key?.toJSON()
|
|
1829
|
-
};
|
|
1830
|
-
}
|
|
1831
|
-
/** Resolve the key for an analysis method: explicit first, then carried. */
|
|
1832
|
-
#resolveKey(key) {
|
|
1833
|
-
const resolved = key ?? this.#key;
|
|
1834
|
-
if (resolved === void 0) {
|
|
1835
|
-
throw new Error("progression has no key context; pass a Key or attach one with withKey()");
|
|
1836
|
-
}
|
|
1837
|
-
return resolved;
|
|
1838
|
-
}
|
|
1839
|
-
};
|
|
1840
|
-
|
|
1841
|
-
// src/model/chord.ts
|
|
1842
|
-
function copyChord(data) {
|
|
1843
|
-
const copy = {
|
|
1844
|
-
rootPc: data.rootPc,
|
|
1845
|
-
quality: data.quality,
|
|
1846
|
-
intervals: [...data.intervals]
|
|
1847
|
-
};
|
|
1848
|
-
if (data.bassPc !== void 0) {
|
|
1849
|
-
copy.bassPc = data.bassPc;
|
|
1850
|
-
}
|
|
1851
|
-
if (data.rootSpelling !== void 0) {
|
|
1852
|
-
copy.rootSpelling = { letter: data.rootSpelling.letter, alter: data.rootSpelling.alter };
|
|
1853
|
-
}
|
|
1854
|
-
if (data.bassSpelling !== void 0) {
|
|
1855
|
-
copy.bassSpelling = { letter: data.bassSpelling.letter, alter: data.bassSpelling.alter };
|
|
1856
|
-
}
|
|
1857
|
-
return copy;
|
|
1858
|
-
}
|
|
1859
|
-
var Chord = class _Chord {
|
|
1860
|
-
#data;
|
|
1861
|
-
#key;
|
|
1862
|
-
/**
|
|
1863
|
-
* Wrap a plain chord object.
|
|
1864
|
-
*
|
|
1865
|
-
* @param data The chord; it is copied, never retained or mutated.
|
|
1866
|
-
* @param key Optional key context for analysis methods.
|
|
1867
|
-
*/
|
|
1868
|
-
constructor(data, key) {
|
|
1869
|
-
this.#data = copyChord(data);
|
|
1870
|
-
this.#key = key;
|
|
1871
|
-
}
|
|
1872
|
-
/**
|
|
1873
|
-
* Build a chord from a root and quality.
|
|
1874
|
-
*
|
|
1875
|
-
* @param root Root as a note name (e.g. `'Eb'`) or a pitch class.
|
|
1876
|
-
* @param quality The chord quality.
|
|
1877
|
-
* @param bass Optional slash-chord bass pitch class.
|
|
1878
|
-
* @returns The chord (without key context).
|
|
1879
|
-
*/
|
|
1880
|
-
static of(root, quality, bass) {
|
|
1881
|
-
const rootPc = typeof root === "string" ? Note.of(root).pitchClass : root;
|
|
1882
|
-
return new _Chord(makeChord(rootPc, quality, bass));
|
|
1883
|
-
}
|
|
1884
|
-
/**
|
|
1885
|
-
* Wrap an existing plain chord object.
|
|
1886
|
-
*
|
|
1887
|
-
* @param data The plain chord.
|
|
1888
|
-
* @returns The wrapped chord (without key context).
|
|
1889
|
-
*/
|
|
1890
|
-
static from(data) {
|
|
1891
|
-
return new _Chord(data);
|
|
1892
|
-
}
|
|
1893
|
-
/**
|
|
1894
|
-
* Parse a lead-sheet chord symbol (e.g. `'Cmaj7'`, `'F#m7b5'`, `'C/G'`).
|
|
1895
|
-
*
|
|
1896
|
-
* @param symbol The chord symbol.
|
|
1897
|
-
* @returns The chord (without key context).
|
|
1898
|
-
* @throws If the root or quality is not recognized.
|
|
1899
|
-
*/
|
|
1900
|
-
static parse(symbol) {
|
|
1901
|
-
return new _Chord(parseChordSymbol(symbol));
|
|
1902
|
-
}
|
|
1903
|
-
/**
|
|
1904
|
-
* Identify the chords matching a set of pitches, best interpretation first.
|
|
1905
|
-
*
|
|
1906
|
-
* @param pitches MIDI pitches or bare pitch classes.
|
|
1907
|
-
* @returns Ranked chord interpretations (may be empty).
|
|
1908
|
-
*/
|
|
1909
|
-
static detect(pitches) {
|
|
1910
|
-
return detectChord(pitches).map(
|
|
1911
|
-
(match) => new _Chord(makeChord(match.rootPc, match.quality, match.bassPc))
|
|
1912
|
-
);
|
|
1913
|
-
}
|
|
1914
|
-
/**
|
|
1915
|
-
* The single best chord interpretation of a pitch set.
|
|
1916
|
-
*
|
|
1917
|
-
* @param pitches MIDI pitches or bare pitch classes.
|
|
1918
|
-
* @returns The top-ranked chord, or null when nothing matches.
|
|
1919
|
-
*/
|
|
1920
|
-
static detectBest(pitches) {
|
|
1921
|
-
const best = detectChordBest(pitches);
|
|
1922
|
-
return best === null ? null : new _Chord(best);
|
|
1923
|
-
}
|
|
1924
|
-
/** The root pitch class (0..11). */
|
|
1925
|
-
get rootPc() {
|
|
1926
|
-
return this.#data.rootPc;
|
|
1927
|
-
}
|
|
1928
|
-
/** The chord quality. */
|
|
1929
|
-
get quality() {
|
|
1930
|
-
return this.#data.quality;
|
|
1931
|
-
}
|
|
1932
|
-
/** A copy of the semitone offsets above the root. */
|
|
1933
|
-
get intervals() {
|
|
1934
|
-
return [...this.#data.intervals];
|
|
1935
|
-
}
|
|
1936
|
-
/** The slash-chord bass pitch class, or undefined in root position. */
|
|
1937
|
-
get bassPc() {
|
|
1938
|
-
return this.#data.bassPc;
|
|
1939
|
-
}
|
|
1940
|
-
/** A copy of the underlying plain chord object. */
|
|
1941
|
-
get data() {
|
|
1942
|
-
return copyChord(this.#data);
|
|
1943
|
-
}
|
|
1944
|
-
/** The carried key context, if any. */
|
|
1945
|
-
get key() {
|
|
1946
|
-
return this.#key;
|
|
1947
|
-
}
|
|
1948
|
-
/**
|
|
1949
|
-
* A copy of this chord carrying the given key context.
|
|
1950
|
-
*
|
|
1951
|
-
* @param key The key context to attach.
|
|
1952
|
-
* @returns The new chord.
|
|
1953
|
-
*/
|
|
1954
|
-
withKey(key) {
|
|
1955
|
-
return new _Chord(this.#data, key);
|
|
1956
|
-
}
|
|
1957
|
-
/**
|
|
1958
|
-
* The chord's sorted, deduplicated pitch classes.
|
|
1959
|
-
*
|
|
1960
|
-
* @returns Pitch classes ascending in [0, 11].
|
|
1961
|
-
*/
|
|
1962
|
-
pitchClasses() {
|
|
1963
|
-
return chordPitchClasses(this.#data);
|
|
1964
|
-
}
|
|
1965
|
-
/**
|
|
1966
|
-
* The chord's Roman numeral in a key.
|
|
1967
|
-
*
|
|
1968
|
-
* @param key Key to analyze in; falls back to the carried context.
|
|
1969
|
-
* @returns The Roman numeral string.
|
|
1970
|
-
* @throws If no key is given and none is carried.
|
|
1971
|
-
*/
|
|
1972
|
-
roman(key) {
|
|
1973
|
-
return chordToRoman(this.#data, this.#resolveKey(key).scale);
|
|
1974
|
-
}
|
|
1975
|
-
/**
|
|
1976
|
-
* The chord's harmonic function (tonic / subdominant / dominant) in a key.
|
|
1977
|
-
*
|
|
1978
|
-
* @param key Key to analyze in; falls back to the carried context.
|
|
1979
|
-
* @returns The harmonic function.
|
|
1980
|
-
* @throws If no key is given and none is carried.
|
|
1981
|
-
*/
|
|
1982
|
-
function(key) {
|
|
1983
|
-
return functionOf(this.#data, this.#resolveKey(key).scale);
|
|
1984
|
-
}
|
|
1985
|
-
/**
|
|
1986
|
-
* Full functional analysis: function, borrowing, and Roman numeral.
|
|
1987
|
-
*
|
|
1988
|
-
* @param key Key to analyze in; falls back to the carried context.
|
|
1989
|
-
* @returns The chord analysis.
|
|
1990
|
-
* @throws If no key is given and none is carried.
|
|
1991
|
-
*/
|
|
1992
|
-
analyze(key) {
|
|
1993
|
-
return analyzeChord(this.#data, this.#resolveKey(key).scale);
|
|
1994
|
-
}
|
|
1995
|
-
/**
|
|
1996
|
-
* Whether the chord is borrowed from the parallel mode (modal interchange).
|
|
1997
|
-
*
|
|
1998
|
-
* @param key Key to analyze in; falls back to the carried context.
|
|
1999
|
-
* @returns True if the chord is borrowed.
|
|
2000
|
-
* @throws If no key is given and none is carried.
|
|
2001
|
-
*/
|
|
2002
|
-
isBorrowed(key) {
|
|
2003
|
-
return isBorrowedChord(this.#data, this.#resolveKey(key).scale);
|
|
2004
|
-
}
|
|
2005
|
-
/**
|
|
2006
|
-
* The origin of a non-diatonic chord (parallel mode or Neapolitan), or null.
|
|
2007
|
-
*
|
|
2008
|
-
* @param key Key to analyze in; falls back to the carried context.
|
|
2009
|
-
* @returns The borrowing source, or null.
|
|
2010
|
-
* @throws If no key is given and none is carried.
|
|
2011
|
-
*/
|
|
2012
|
-
borrowedSource(key) {
|
|
2013
|
-
return borrowedSource(this.#data, this.#resolveKey(key).scale);
|
|
2014
|
-
}
|
|
2015
|
-
/**
|
|
2016
|
-
* The chord rendered as a lead-sheet symbol (e.g. `'Cmaj7'`, `'F#m7'`, `'C/G'`).
|
|
2017
|
-
*
|
|
2018
|
-
* @param opts Set `flats: true` to spell the root/bass with flats.
|
|
2019
|
-
* @returns The chord symbol.
|
|
2020
|
-
*/
|
|
2021
|
-
symbol(opts) {
|
|
2022
|
-
return formatChordSymbol(this.#data, opts);
|
|
2023
|
-
}
|
|
2024
|
-
/**
|
|
2025
|
-
* Realize the chord as one MIDI pitch per voice, ascending.
|
|
2026
|
-
*
|
|
2027
|
-
* @param opts Voicing options; defaults to four SATB voices.
|
|
2028
|
-
* @returns MIDI pitches, ascending, one per voice.
|
|
2029
|
-
* @throws If no voicing fits the given ranges.
|
|
2030
|
-
*/
|
|
2031
|
-
voice(opts) {
|
|
2032
|
-
return voiceChord(this.#data, opts);
|
|
2033
|
-
}
|
|
2034
|
-
/**
|
|
2035
|
-
* Realize the chord as a single styled voicing (`close`, `drop2`, `drop3`,
|
|
2036
|
-
* `shell`, or `rootless`), optionally constraining the top voice.
|
|
2037
|
-
*
|
|
2038
|
-
* @param opts Styled-voicing options; defaults to a close-position voicing.
|
|
2039
|
-
* @returns MIDI pitches, ascending.
|
|
2040
|
-
*/
|
|
2041
|
-
styledVoicing(opts) {
|
|
2042
|
-
return voiceChordStyled(this.#data, opts);
|
|
2043
|
-
}
|
|
2044
|
-
/**
|
|
2045
|
-
* The negative-harmony mirror of the chord about the key's tonic–dominant
|
|
2046
|
-
* axis (major becomes minor and vice versa).
|
|
2047
|
-
*
|
|
2048
|
-
* @param key Key providing the reflection axis; falls back to the carried
|
|
2049
|
-
* context.
|
|
2050
|
-
* @returns The mirrored chord, keeping any key context.
|
|
2051
|
-
* @throws If no key is given and none is carried.
|
|
2052
|
-
*/
|
|
2053
|
-
negativeHarmony(key) {
|
|
2054
|
-
const resolved = this.#resolveKey(key);
|
|
2055
|
-
return new _Chord(negativeHarmonyMirror(this.#data, resolved.scale), key ?? this.#key);
|
|
2056
|
-
}
|
|
2057
|
-
/**
|
|
2058
|
-
* The n-th inversion: a copy whose bass is the chord tone `n` steps above the
|
|
2059
|
-
* root in the interval template (`invert(1)` puts the third in the bass).
|
|
2060
|
-
* `n` wraps around the template length; negative values count backwards.
|
|
2061
|
-
*
|
|
2062
|
-
* `invert(0)` (and any `n` that wraps to it) is root position, so it carries
|
|
2063
|
-
* no slash bass and equals the original chord.
|
|
2064
|
-
*
|
|
2065
|
-
* @param n The inversion number.
|
|
2066
|
-
* @returns The inverted chord, keeping any key context.
|
|
2067
|
-
* @throws If the chord has no intervals.
|
|
2068
|
-
*/
|
|
2069
|
-
invert(n) {
|
|
2070
|
-
const intervals = this.#data.intervals;
|
|
2071
|
-
const length = intervals.length;
|
|
2072
|
-
if (length === 0) {
|
|
2073
|
-
throw new Error("cannot invert a chord with no intervals");
|
|
2074
|
-
}
|
|
2075
|
-
const index = (n % length + length) % length;
|
|
2076
|
-
const data = copyChord(this.#data);
|
|
2077
|
-
if (index === 0) {
|
|
2078
|
-
delete data.bassPc;
|
|
2079
|
-
} else {
|
|
2080
|
-
data.bassPc = mod125(this.#data.rootPc + (intervals[index] ?? 0));
|
|
2081
|
-
}
|
|
2082
|
-
return new _Chord(data, this.#key);
|
|
2083
|
-
}
|
|
2084
|
-
/**
|
|
2085
|
-
* The named scales that fit over this chord, best fit first, rooted on the
|
|
2086
|
-
* chord root.
|
|
2087
|
-
*
|
|
2088
|
-
* @returns The matching scales.
|
|
2089
|
-
*/
|
|
2090
|
-
scales() {
|
|
2091
|
-
return chordScales(this.#data);
|
|
2092
|
-
}
|
|
2093
|
-
/**
|
|
2094
|
-
* The available tensions (usable non-chord, non-avoid scale tones) of a
|
|
2095
|
-
* scale over this chord.
|
|
2096
|
-
*
|
|
2097
|
-
* @param scaleName A named scale, rooted on the chord root.
|
|
2098
|
-
* @returns Tension pitch classes, ascending in [0, 11].
|
|
2099
|
-
*/
|
|
2100
|
-
tensions(scaleName) {
|
|
2101
|
-
return availableTensions(this.#data, scaleName);
|
|
2102
|
-
}
|
|
2103
|
-
/**
|
|
2104
|
-
* The avoid notes (scale tones a semitone above a chord tone) of a scale
|
|
2105
|
-
* over this chord.
|
|
2106
|
-
*
|
|
2107
|
-
* @param scaleName A named scale, rooted on the chord root.
|
|
2108
|
-
* @returns Avoid-note pitch classes, ascending in [0, 11].
|
|
2109
|
-
*/
|
|
2110
|
-
avoidNotes(scaleName) {
|
|
2111
|
-
return avoidNotes(this.#data, scaleName);
|
|
2112
|
-
}
|
|
2113
|
-
/**
|
|
2114
|
-
* Spell the chord tones with letter names, root first, in the key's spelling.
|
|
2115
|
-
*
|
|
2116
|
-
* @param key Key providing the spelled tonic; falls back to the carried
|
|
2117
|
-
* context.
|
|
2118
|
-
* @returns Spelled octave-less notes in the chord's own (tertian) order.
|
|
2119
|
-
* @throws If no key is given and none is carried.
|
|
2120
|
-
*/
|
|
2121
|
-
spell(key) {
|
|
2122
|
-
const resolved = this.#resolveKey(key);
|
|
2123
|
-
return spellChord(this.#data, resolved.tonic.data, resolved.scale).map(
|
|
2124
|
-
(note) => new Note(note)
|
|
2125
|
-
);
|
|
2126
|
-
}
|
|
2127
|
-
/**
|
|
2128
|
-
* Start a progression with this chord followed by others, carrying this
|
|
2129
|
-
* chord's key context (if any).
|
|
2130
|
-
*
|
|
2131
|
-
* @param others The chords following this one.
|
|
2132
|
-
* @returns The progression.
|
|
2133
|
-
*/
|
|
2134
|
-
progressionTo(...others) {
|
|
2135
|
-
return new Progression([this, ...others], this.#key);
|
|
2136
|
-
}
|
|
2137
|
-
/**
|
|
2138
|
-
* Whether another chord has the same root, quality, intervals, and bass.
|
|
2139
|
-
* Key context is not compared.
|
|
2140
|
-
*
|
|
2141
|
-
* @param other The chord to compare.
|
|
2142
|
-
* @returns True if the chord data is identical.
|
|
2143
|
-
*/
|
|
2144
|
-
equals(other) {
|
|
2145
|
-
const a = this.#data;
|
|
2146
|
-
const b = other.#data;
|
|
2147
|
-
return a.rootPc === b.rootPc && a.quality === b.quality && a.bassPc === b.bassPc && a.intervals.length === b.intervals.length && a.intervals.every((interval, i) => interval === b.intervals[i]);
|
|
2148
|
-
}
|
|
2149
|
-
/**
|
|
2150
|
-
* The plain chord data, for JSON serialization.
|
|
2151
|
-
*
|
|
2152
|
-
* @returns A copy of the underlying plain chord object.
|
|
2153
|
-
*/
|
|
2154
|
-
toJSON() {
|
|
2155
|
-
return this.data;
|
|
2156
|
-
}
|
|
2157
|
-
/** Resolve the key for an analysis method: explicit first, then carried. */
|
|
2158
|
-
#resolveKey(key) {
|
|
2159
|
-
const resolved = key ?? this.#key;
|
|
2160
|
-
if (resolved === void 0) {
|
|
2161
|
-
throw new Error("chord has no key context; pass a Key or attach one with withKey()");
|
|
2162
|
-
}
|
|
2163
|
-
return resolved;
|
|
2164
|
-
}
|
|
2165
|
-
};
|
|
2166
|
-
|
|
2167
|
-
// src/model/interval.ts
|
|
2168
|
-
var Interval = class _Interval {
|
|
2169
|
-
#number;
|
|
2170
|
-
#quality;
|
|
2171
|
-
#semitones;
|
|
2172
|
-
constructor(numberValue, quality, semitones) {
|
|
2173
|
-
this.#number = numberValue;
|
|
2174
|
-
this.#quality = quality;
|
|
2175
|
-
this.#semitones = semitones;
|
|
2176
|
-
}
|
|
2177
|
-
/**
|
|
2178
|
-
* The spelled interval between two notes.
|
|
2179
|
-
*
|
|
2180
|
-
* @param a The first note.
|
|
2181
|
-
* @param b The second note.
|
|
2182
|
-
* @returns The interval from `a` to `b`.
|
|
2183
|
-
*/
|
|
2184
|
-
static between(a, b) {
|
|
2185
|
-
const spelled = a.intervalTo(b);
|
|
2186
|
-
return new _Interval(spelled.number, spelled.quality, spelled.semitones);
|
|
2187
|
-
}
|
|
2188
|
-
/**
|
|
2189
|
-
* Build an interval from explicit components.
|
|
2190
|
-
*
|
|
2191
|
-
* @param numberValue Diatonic size: 1 = unison, 2 = second, ... 8 = octave.
|
|
2192
|
-
* @param quality Quality label: `'P'`, `'M'`, `'m'`, or repeated `'A'`/`'d'`.
|
|
2193
|
-
* @param semitones Signed semitone span.
|
|
2194
|
-
* @returns The interval.
|
|
2195
|
-
*/
|
|
2196
|
-
static of(numberValue, quality, semitones) {
|
|
2197
|
-
return new _Interval(numberValue, quality, semitones);
|
|
2198
|
-
}
|
|
2199
|
-
/** Diatonic size: 1 = unison, 2 = second, ... 8 = octave, and beyond. */
|
|
2200
|
-
get number() {
|
|
2201
|
-
return this.#number;
|
|
2202
|
-
}
|
|
2203
|
-
/** Quality label: `'P'`, `'M'`, `'m'`, or repeated `'A'`/`'d'`. */
|
|
2204
|
-
get quality() {
|
|
2205
|
-
return this.#quality;
|
|
2206
|
-
}
|
|
2207
|
-
/** Signed semitone span. */
|
|
2208
|
-
get semitones() {
|
|
2209
|
-
return this.#semitones;
|
|
2210
|
-
}
|
|
2211
|
-
/** A readable label composed of quality and number, e.g. `'P5'` or `'M3'`. */
|
|
2212
|
-
get name() {
|
|
2213
|
-
return `${this.#quality}${this.#number}`;
|
|
2214
|
-
}
|
|
2215
|
-
/**
|
|
2216
|
-
* The plain interval data, for JSON serialization.
|
|
2217
|
-
*
|
|
2218
|
-
* Private class fields do not serialize, so an explicit `toJSON` keeps
|
|
2219
|
-
* `JSON.stringify(interval)` from collapsing to `{}`.
|
|
2220
|
-
*
|
|
2221
|
-
* @returns The diatonic number, quality, and semitone span.
|
|
2222
|
-
*/
|
|
2223
|
-
toJSON() {
|
|
2224
|
-
return { number: this.#number, quality: this.#quality, semitones: this.#semitones };
|
|
2225
|
-
}
|
|
2226
|
-
};
|
|
2227
|
-
|
|
2228
|
-
// src/model/key.ts
|
|
2229
|
-
function resolveTonic(root, spelling) {
|
|
2230
|
-
return typeof root === "string" ? Note.of(root) : new Note(spellPitchClassBare(root, spelling));
|
|
2231
|
-
}
|
|
2232
|
-
function accidentalLoad(tonic, scale) {
|
|
2233
|
-
return spellScale(tonic, scale).reduce((sum, note) => sum + Math.abs(note.alter), 0);
|
|
2234
|
-
}
|
|
2235
|
-
function bestTonicForScale(rootPc, scale) {
|
|
2236
|
-
const sharp = spellPitchClassBare(rootPc, "sharp");
|
|
2237
|
-
const flat = spellPitchClassBare(rootPc, "flat");
|
|
2238
|
-
if (sharp.letter === flat.letter && sharp.alter === flat.alter) {
|
|
2239
|
-
return new Note(sharp);
|
|
2240
|
-
}
|
|
2241
|
-
return accidentalLoad(flat, scale) <= accidentalLoad(sharp, scale) ? new Note(flat) : new Note(sharp);
|
|
2242
|
-
}
|
|
2243
|
-
var Key = class _Key {
|
|
2244
|
-
#scale;
|
|
2245
|
-
#tonic;
|
|
2246
|
-
/**
|
|
2247
|
-
* Wrap a key/scale and its spelled tonic.
|
|
2248
|
-
*
|
|
2249
|
-
* @param scale The key/scale; its root is normalized to a pitch class.
|
|
2250
|
-
* @param tonic The spelled tonic anchoring letter-name spelling.
|
|
2251
|
-
*/
|
|
2252
|
-
constructor(scale, tonic) {
|
|
2253
|
-
this.#scale = { rootPc: mod125(scale.rootPc), modeMask12: scale.modeMask12 };
|
|
2254
|
-
this.#tonic = tonic;
|
|
2255
|
-
}
|
|
2256
|
-
/**
|
|
2257
|
-
* A major key.
|
|
2258
|
-
*
|
|
2259
|
-
* @param root Tonic as a note name (e.g. `'Eb'`) or a pitch class; a numeric
|
|
2260
|
-
* root is spelled with whichever accidental side yields the fewest
|
|
2261
|
-
* accidentals across the scale.
|
|
2262
|
-
* @returns The major key.
|
|
2263
|
-
*/
|
|
2264
|
-
static major(root) {
|
|
2265
|
-
if (typeof root === "string") {
|
|
2266
|
-
const tonic = Note.of(root);
|
|
2267
|
-
return new _Key(majorKey(tonic.pitchClass), tonic);
|
|
2268
|
-
}
|
|
2269
|
-
const scale = majorKey(root);
|
|
2270
|
-
return new _Key(scale, bestTonicForScale(root, scale));
|
|
2271
|
-
}
|
|
2272
|
-
/**
|
|
2273
|
-
* A natural-minor key.
|
|
2274
|
-
*
|
|
2275
|
-
* @param root Tonic as a note name or a pitch class; a numeric root is
|
|
2276
|
-
* spelled with whichever accidental side yields the fewest accidentals
|
|
2277
|
-
* across the scale.
|
|
2278
|
-
* @returns The minor key.
|
|
2279
|
-
*/
|
|
2280
|
-
static minor(root) {
|
|
2281
|
-
if (typeof root === "string") {
|
|
2282
|
-
const tonic = Note.of(root);
|
|
2283
|
-
return new _Key(minorKey(tonic.pitchClass), tonic);
|
|
2284
|
-
}
|
|
2285
|
-
const scale = minorKey(root);
|
|
2286
|
-
return new _Key(scale, bestTonicForScale(root, scale));
|
|
2287
|
-
}
|
|
2288
|
-
/**
|
|
2289
|
-
* A key on a named scale (e.g. `'dorian'`, `'harmonicMinor'`).
|
|
2290
|
-
*
|
|
2291
|
-
* @param name The scale name, a key of the scale module's named-scale table.
|
|
2292
|
-
* @param root Tonic as a note name or a pitch class.
|
|
2293
|
-
* @returns The key.
|
|
2294
|
-
* @throws If the name is not a known scale.
|
|
2295
|
-
*/
|
|
2296
|
-
static named(name, root) {
|
|
2297
|
-
const tonic = resolveTonic(root, "sharp");
|
|
2298
|
-
return new _Key(scaleByName(name, tonic.pitchClass), tonic);
|
|
2299
|
-
}
|
|
2300
|
-
/**
|
|
2301
|
-
* Wrap an existing `KeyScale`, synthesizing a spelled tonic when none is
|
|
2302
|
-
* given (flat-preferred for minor-third scales, sharp-preferred otherwise).
|
|
2303
|
-
*
|
|
2304
|
-
* @param scale The key/scale to wrap.
|
|
2305
|
-
* @param tonic Optional spelled tonic.
|
|
2306
|
-
* @returns The key.
|
|
2307
|
-
*/
|
|
2308
|
-
static of(scale, tonic) {
|
|
2309
|
-
const spelled = tonic ?? new Note(spellPitchClassBare(scale.rootPc, isMinorKey(scale) ? "flat" : "sharp"));
|
|
2310
|
-
return new _Key(scale, spelled);
|
|
2311
|
-
}
|
|
2312
|
-
/** A copy of the underlying plain `KeyScale`. */
|
|
2313
|
-
get scale() {
|
|
2314
|
-
return { rootPc: this.#scale.rootPc, modeMask12: this.#scale.modeMask12 };
|
|
2315
|
-
}
|
|
2316
|
-
/** The spelled tonic. */
|
|
2317
|
-
get tonic() {
|
|
2318
|
-
return this.#tonic;
|
|
2319
|
-
}
|
|
2320
|
-
/** The tonic pitch class (0..11). */
|
|
2321
|
-
get rootPc() {
|
|
2322
|
-
return this.#scale.rootPc;
|
|
2323
|
-
}
|
|
2324
|
-
/** Whether the scale has a minor third and no major third. */
|
|
2325
|
-
get isMinor() {
|
|
2326
|
-
return isMinorKey(this.#scale);
|
|
2327
|
-
}
|
|
2328
|
-
/**
|
|
2329
|
-
* The scale's pitch classes in ascending scale-degree order (degree 0 first).
|
|
2330
|
-
*
|
|
2331
|
-
* @returns One pitch class per scale degree.
|
|
2332
|
-
*/
|
|
2333
|
-
pitchClasses() {
|
|
2334
|
-
return scaleTonesInDegreeOrder(this.#scale);
|
|
2335
|
-
}
|
|
2336
|
-
/**
|
|
2337
|
-
* The spelled scale, one note per degree (e.g. C D E F G A B for C major).
|
|
2338
|
-
*
|
|
2339
|
-
* @returns Spelled octave-less notes in scale-degree order.
|
|
2340
|
-
*/
|
|
2341
|
-
notes() {
|
|
2342
|
-
return spellScale(this.#tonic.data, this.#scale).map((note) => new Note(note));
|
|
2343
|
-
}
|
|
2344
|
-
/**
|
|
2345
|
-
* Alias of {@link Key.notes}.
|
|
2346
|
-
*
|
|
2347
|
-
* @returns Spelled octave-less notes in scale-degree order.
|
|
2348
|
-
*/
|
|
2349
|
-
spell() {
|
|
2350
|
-
return this.notes();
|
|
2351
|
-
}
|
|
2352
|
-
/**
|
|
2353
|
-
* The spelled scale as letter-name strings.
|
|
2354
|
-
*
|
|
2355
|
-
* @returns One name per scale degree.
|
|
2356
|
-
*/
|
|
2357
|
-
noteNames() {
|
|
2358
|
-
return this.notes().map((note) => note.name);
|
|
2359
|
-
}
|
|
2360
|
-
/**
|
|
2361
|
-
* Build a chord on a scale degree, carrying this key as context.
|
|
2362
|
-
*
|
|
2363
|
-
* With an explicit quality the quality's interval template is attached to the
|
|
2364
|
-
* degree's diatonic root; without one the scale-correct diatonic triad is
|
|
2365
|
-
* stacked (e.g. a diminished triad on the leading tone of a major key).
|
|
2366
|
-
*
|
|
2367
|
-
* @param degree 0-based scale degree of the chord root.
|
|
2368
|
-
* @param quality Optional chord quality.
|
|
2369
|
-
* @returns The chord, with this key attached.
|
|
2370
|
-
*/
|
|
2371
|
-
chord(degree, quality) {
|
|
2372
|
-
const data = quality === void 0 ? diatonicTriad(degree, this.#scale) : chordFromDegree(degree, quality, this.#scale);
|
|
2373
|
-
return new Chord(data, this);
|
|
2374
|
-
}
|
|
2375
|
-
/**
|
|
2376
|
-
* The diatonic triad on a scale degree, carrying this key as context.
|
|
2377
|
-
*
|
|
2378
|
-
* @param degree 0-based scale degree of the chord root.
|
|
2379
|
-
* @returns The triad, with this key attached.
|
|
2380
|
-
*/
|
|
2381
|
-
diatonicTriad(degree) {
|
|
2382
|
-
return new Chord(diatonicTriad(degree, this.#scale), this);
|
|
2383
|
-
}
|
|
2384
|
-
/**
|
|
2385
|
-
* The diatonic seventh chord on a scale degree, carrying this key as context.
|
|
2386
|
-
*
|
|
2387
|
-
* @param degree 0-based scale degree of the chord root.
|
|
2388
|
-
* @returns The seventh chord, with this key attached.
|
|
2389
|
-
*/
|
|
2390
|
-
diatonicSeventh(degree) {
|
|
2391
|
-
return new Chord(diatonicSeventh(degree, this.#scale), this);
|
|
2392
|
-
}
|
|
2393
|
-
/**
|
|
2394
|
-
* Build the chord denoted by a Roman numeral in this key (including applied
|
|
2395
|
-
* chords such as `'V7/V'`), carrying this key as context.
|
|
2396
|
-
*
|
|
2397
|
-
* @param text The Roman numeral.
|
|
2398
|
-
* @returns The chord, with this key attached.
|
|
2399
|
-
* @throws If the numeral is not valid.
|
|
2400
|
-
*/
|
|
2401
|
-
roman(text) {
|
|
2402
|
-
return new Chord(romanToChord(text, this.#scale), this);
|
|
2403
|
-
}
|
|
2404
|
-
/**
|
|
2405
|
-
* Whether a pitch belongs to the scale.
|
|
2406
|
-
*
|
|
2407
|
-
* @param x A MIDI pitch, bare pitch class, or note.
|
|
2408
|
-
* @returns True if the pitch class is a scale tone.
|
|
2409
|
-
*/
|
|
2410
|
-
contains(x) {
|
|
2411
|
-
return isScaleTone(typeof x === "number" ? x : x.pitchClass, this.#scale);
|
|
2412
|
-
}
|
|
2413
|
-
/**
|
|
2414
|
-
* The plain key data, for JSON serialization.
|
|
2415
|
-
*
|
|
2416
|
-
* Private class fields do not serialize, so an explicit `toJSON` keeps
|
|
2417
|
-
* `JSON.stringify(key)` from collapsing to `{}`. The result pairs the
|
|
2418
|
-
* `KeyScale` with the spelled tonic, enough to reconstruct the key via
|
|
2419
|
-
* {@link Key.of}.
|
|
2420
|
-
*
|
|
2421
|
-
* @returns The key/scale and its spelled tonic.
|
|
2422
|
-
*/
|
|
2423
|
-
toJSON() {
|
|
2424
|
-
return { scale: this.scale, tonic: this.#tonic.data };
|
|
2425
|
-
}
|
|
2426
|
-
};
|
|
2427
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
2428
|
-
0 && (module.exports = {
|
|
2429
|
-
Chord,
|
|
2430
|
-
Interval,
|
|
2431
|
-
Key,
|
|
2432
|
-
Note,
|
|
2433
|
-
Progression
|
|
2434
|
-
});
|
|
19
|
+
exports.Chord = _chunkNVE34YGZcjs.Chord; exports.Interval = _chunkNVE34YGZcjs.Interval; exports.Key = _chunkNVE34YGZcjs.Key; exports.Note = _chunkNVE34YGZcjs.Note; exports.Progression = _chunkNVE34YGZcjs.Progression;
|
|
2435
20
|
//# sourceMappingURL=index.cjs.map
|