@libraz/libcantus 0.9.2 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/dist/analyze/index.cjs +41 -1676
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +194 -122
- package/dist/analyze/index.d.ts +194 -122
- package/dist/analyze/index.js +19 -8
- package/dist/chunk-36N73KZ3.cjs +246 -0
- package/dist/chunk-36N73KZ3.cjs.map +1 -0
- package/dist/chunk-4HLLZPXT.cjs +3077 -0
- package/dist/chunk-4HLLZPXT.cjs.map +1 -0
- package/dist/chunk-7MGZ7D62.cjs +498 -0
- package/dist/chunk-7MGZ7D62.cjs.map +1 -0
- package/dist/{chunk-R3S2LUBH.js → chunk-7NKDPF74.js} +660 -305
- package/dist/chunk-7NKDPF74.js.map +1 -0
- package/dist/chunk-CAN76NCU.cjs +679 -0
- package/dist/chunk-CAN76NCU.cjs.map +1 -0
- package/dist/{chunk-7Y7WF64D.js → chunk-D2FAJBYL.js} +104 -35
- package/dist/chunk-D2FAJBYL.js.map +1 -0
- package/dist/{chunk-COKTX2G5.js → chunk-EHVEBWM5.js} +1052 -510
- package/dist/chunk-EHVEBWM5.js.map +1 -0
- package/dist/chunk-FCANXBAT.cjs +30 -0
- package/dist/chunk-FCANXBAT.cjs.map +1 -0
- package/dist/chunk-FNDWXRRI.cjs +479 -0
- package/dist/chunk-FNDWXRRI.cjs.map +1 -0
- package/dist/{chunk-GQKN5KDT.js → chunk-GQSGS2N4.js} +43 -29
- package/dist/chunk-GQSGS2N4.js.map +1 -0
- package/dist/chunk-HKGYCQ34.js +53 -0
- package/dist/chunk-HKGYCQ34.js.map +1 -0
- package/dist/chunk-I24MV65U.js +106 -0
- package/dist/chunk-I24MV65U.js.map +1 -0
- package/dist/{chunk-LN7ZZMD2.js → chunk-KMHOICIY.js} +322 -173
- package/dist/chunk-KMHOICIY.js.map +1 -0
- package/dist/chunk-LW53AHQU.cjs +1117 -0
- package/dist/chunk-LW53AHQU.cjs.map +1 -0
- package/dist/chunk-N6IHQUOX.js +1 -0
- package/dist/chunk-N6IHQUOX.js.map +1 -0
- package/dist/chunk-NKIBQ6PT.js +30 -0
- package/dist/chunk-NKIBQ6PT.js.map +1 -0
- package/dist/chunk-NP4NA2FB.cjs +106 -0
- package/dist/chunk-NP4NA2FB.cjs.map +1 -0
- package/dist/chunk-NVE34YGZ.cjs +1449 -0
- package/dist/chunk-NVE34YGZ.cjs.map +1 -0
- package/dist/{chunk-A4532ZFR.js → chunk-NYYWMV2C.js} +115 -47
- package/dist/chunk-NYYWMV2C.js.map +1 -0
- package/dist/{chunk-5HTNMQW4.js → chunk-Q2XUP227.js} +310 -118
- package/dist/chunk-Q2XUP227.js.map +1 -0
- package/dist/chunk-Q6OP4U43.cjs +327 -0
- package/dist/chunk-Q6OP4U43.cjs.map +1 -0
- package/dist/chunk-RC6GBMCD.cjs +845 -0
- package/dist/chunk-RC6GBMCD.cjs.map +1 -0
- package/dist/{chunk-XTC3X6MS.js → chunk-RK35RIN7.js} +453 -152
- package/dist/chunk-RK35RIN7.js.map +1 -0
- package/dist/chunk-RUMU3UA6.cjs +53 -0
- package/dist/chunk-RUMU3UA6.cjs.map +1 -0
- package/dist/chunk-T64C7ZD7.cjs +1 -0
- package/dist/chunk-T64C7ZD7.cjs.map +1 -0
- package/dist/chunk-XEBG3G53.js +498 -0
- package/dist/chunk-XEBG3G53.js.map +1 -0
- package/dist/chunk-YLEMGEFH.js +246 -0
- package/dist/chunk-YLEMGEFH.js.map +1 -0
- package/dist/chunk-ZEJR4TKL.cjs +173 -0
- package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
- package/dist/core/index.cjs +130 -431
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +331 -17
- package/dist/core/index.d.ts +331 -17
- package/dist/core/index.js +71 -13
- package/dist/{function-2bJW1Zp_.d.ts → function-BtiZb9i9.d.ts} +28 -7
- package/dist/{function-r2sh-jOO.d.cts → function-DNugAOrt.d.cts} +28 -7
- package/dist/generate/index.cjs +30 -3751
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +373 -59
- package/dist/generate/index.d.ts +373 -59
- package/dist/generate/index.js +23 -9
- package/dist/{index-DZ1M21do.d.cts → index-6VR8-kit.d.ts} +20 -4
- package/dist/index-70p168VM.d.ts +192 -0
- package/dist/{index-D_ZTUNKp.d.cts → index-Be74mcon.d.cts} +18 -17
- package/dist/index-BtXT6UQ0.d.cts +192 -0
- package/dist/{index-BMPxwjwV.d.ts → index-C5UBbQ4G.d.ts} +18 -17
- package/dist/{index-BOBXBNwP.d.cts → index-C83mZI1P.d.ts} +93 -8
- package/dist/{index-BdeQ1r_p.d.cts → index-D6b6na69.d.cts} +14 -11
- package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
- package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
- package/dist/index-OG65ocOr.d.cts +272 -0
- package/dist/index-OG65ocOr.d.ts +272 -0
- package/dist/{index-BreoJRw0.d.ts → index-tJhkmQgE.d.cts} +20 -4
- package/dist/{index-CP3wAfEH.d.ts → index-tSQh-xwY.d.ts} +14 -11
- package/dist/{index-CM5W4rcm.d.ts → index-y_ak9_Hi.d.cts} +93 -8
- package/dist/index.cjs +419 -6848
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -13
- package/dist/index.d.ts +14 -13
- package/dist/index.js +132 -27
- package/dist/model/index.cjs +8 -2423
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +433 -74
- package/dist/model/index.d.ts +433 -74
- package/dist/model/index.js +6 -7
- package/dist/styled-DIRkk9Uz.d.ts +506 -0
- package/dist/styled-DJbkJQfj.d.cts +506 -0
- package/dist/theory/index.cjs +172 -1676
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +151 -233
- package/dist/theory/index.d.ts +151 -233
- package/dist/theory/index.js +31 -7
- package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
- package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
- package/package.json +73 -24
- package/dist/chunk-5HTNMQW4.js.map +0 -1
- package/dist/chunk-7Y7WF64D.js.map +0 -1
- package/dist/chunk-A4532ZFR.js.map +0 -1
- package/dist/chunk-BHUT2S5G.js +0 -22
- package/dist/chunk-BHUT2S5G.js.map +0 -1
- package/dist/chunk-CCBLVAPK.js +0 -42
- package/dist/chunk-CCBLVAPK.js.map +0 -1
- package/dist/chunk-COKTX2G5.js.map +0 -1
- package/dist/chunk-DYWFNC4A.js +0 -152
- package/dist/chunk-DYWFNC4A.js.map +0 -1
- package/dist/chunk-GQKN5KDT.js.map +0 -1
- package/dist/chunk-LN7ZZMD2.js.map +0 -1
- package/dist/chunk-MSWFZF7J.js +0 -118
- package/dist/chunk-MSWFZF7J.js.map +0 -1
- package/dist/chunk-R3S2LUBH.js.map +0 -1
- package/dist/chunk-SDM3VJJ5.js +0 -41
- package/dist/chunk-SDM3VJJ5.js.map +0 -1
- package/dist/chunk-XTC3X6MS.js.map +0 -1
- package/dist/chunk-ZXW5YIWK.js +0 -55
- package/dist/chunk-ZXW5YIWK.js.map +0 -1
- package/dist/index-CGMZNCx6.d.cts +0 -140
- package/dist/index-CGMZNCx6.d.ts +0 -140
- package/dist/styled-BjXtOFNk.d.cts +0 -282
- package/dist/styled-D7BAx3S3.d.ts +0 -282
- package/src/analyze/arrange/index.ts +0 -21
- package/src/analyze/arrange/internal.ts +0 -125
- package/src/analyze/arrange/tension.ts +0 -187
- package/src/analyze/arrange/tracks.ts +0 -279
- package/src/analyze/detect/index.ts +0 -235
- package/src/analyze/functional/borrowed.ts +0 -84
- package/src/analyze/functional/cadence.ts +0 -66
- package/src/analyze/functional/function.ts +0 -184
- package/src/analyze/functional/index.ts +0 -23
- package/src/analyze/functional/internal.ts +0 -29
- package/src/analyze/functional/roman.ts +0 -393
- package/src/analyze/index.ts +0 -49
- package/src/analyze/timeline/index.ts +0 -415
- package/src/analyze/voice/index.ts +0 -330
- package/src/core/index.ts +0 -47
- package/src/core/interval/index.ts +0 -81
- package/src/core/meter/index.ts +0 -282
- package/src/core/pitch/index.ts +0 -311
- package/src/core/random/index.ts +0 -53
- package/src/core/tuning/index.ts +0 -156
- package/src/core/types.ts +0 -29
- package/src/generate/bass/index.ts +0 -350
- package/src/generate/countermelody/index.ts +0 -379
- package/src/generate/drums/beat.ts +0 -266
- package/src/generate/drums/euclid.ts +0 -58
- package/src/generate/drums/fills.ts +0 -356
- package/src/generate/drums/ghost.ts +0 -119
- package/src/generate/drums/hihat.ts +0 -285
- package/src/generate/drums/hit.ts +0 -31
- package/src/generate/drums/index.ts +0 -329
- package/src/generate/drums/internal.ts +0 -257
- package/src/generate/drums/kick.ts +0 -160
- package/src/generate/drums/percussion.ts +0 -121
- package/src/generate/drums/rng.ts +0 -7
- package/src/generate/drums/swing.ts +0 -71
- package/src/generate/groove/index.ts +0 -326
- package/src/generate/harmonize/index.ts +0 -440
- package/src/generate/index.ts +0 -59
- package/src/generate/motif/index.ts +0 -398
- package/src/generate/progression/index.ts +0 -370
- package/src/generate/reharmony/index.ts +0 -330
- package/src/generate/rhythm/index.ts +0 -165
- package/src/index.ts +0 -247
- package/src/model/chord.ts +0 -423
- package/src/model/index.ts +0 -15
- package/src/model/interval.ts +0 -82
- package/src/model/key.ts +0 -266
- package/src/model/note.ts +0 -176
- package/src/model/progression.ts +0 -161
- package/src/model/shared.ts +0 -12
- package/src/theory/chord/index.ts +0 -373
- package/src/theory/chordscale/index.ts +0 -374
- package/src/theory/counterpoint/index.ts +0 -230
- package/src/theory/harmony/index.ts +0 -92
- package/src/theory/index.ts +0 -107
- package/src/theory/safety/index.ts +0 -419
- package/src/theory/scale/degrees.ts +0 -123
- package/src/theory/scale/index.ts +0 -28
- package/src/theory/scale/key.ts +0 -56
- package/src/theory/scale/masks.ts +0 -153
- package/src/theory/spelling/index.ts +0 -182
- package/src/theory/symbol/index.ts +0 -268
- package/src/theory/voicing/index.ts +0 -5
- package/src/theory/voicing/internal.ts +0 -168
- package/src/theory/voicing/leading.ts +0 -112
- package/src/theory/voicing/satb.ts +0 -195
- package/src/theory/voicing/styled.ts +0 -176
- package/tsconfig.json +0 -23
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
negativeHarmonyMirror
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-GQSGS2N4.js";
|
|
4
4
|
import {
|
|
5
5
|
analyzeChord,
|
|
6
6
|
borrowedSource,
|
|
@@ -8,11 +8,13 @@ import {
|
|
|
8
8
|
detectCadence,
|
|
9
9
|
detectChord,
|
|
10
10
|
detectChordBest,
|
|
11
|
+
detectKey,
|
|
12
|
+
detectKeyBest,
|
|
11
13
|
functionOf,
|
|
12
14
|
isBorrowedChord,
|
|
13
15
|
isMinorKey,
|
|
14
16
|
romanToChord
|
|
15
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-KMHOICIY.js";
|
|
16
18
|
import {
|
|
17
19
|
availableTensions,
|
|
18
20
|
avoidNotes,
|
|
@@ -20,19 +22,13 @@ import {
|
|
|
20
22
|
formatChordSymbol,
|
|
21
23
|
parseChordSymbol,
|
|
22
24
|
spellChord,
|
|
25
|
+
spellChordFromRoot,
|
|
26
|
+
spellPitchClass,
|
|
23
27
|
spellScale,
|
|
24
28
|
voiceChord,
|
|
25
29
|
voiceChordStyled,
|
|
26
30
|
voiceProgression
|
|
27
|
-
} from "./chunk-
|
|
28
|
-
import {
|
|
29
|
-
formatNote,
|
|
30
|
-
midiToNote,
|
|
31
|
-
noteToMidi,
|
|
32
|
-
noteToPitchClass,
|
|
33
|
-
parseNote,
|
|
34
|
-
spelledInterval
|
|
35
|
-
} from "./chunk-DYWFNC4A.js";
|
|
31
|
+
} from "./chunk-RK35RIN7.js";
|
|
36
32
|
import {
|
|
37
33
|
chordFromDegree,
|
|
38
34
|
chordPitchClasses,
|
|
@@ -43,20 +39,189 @@ import {
|
|
|
43
39
|
makeChord,
|
|
44
40
|
minorKey,
|
|
45
41
|
scaleByName,
|
|
46
|
-
scaleTonesInDegreeOrder
|
|
47
|
-
|
|
42
|
+
scaleTonesInDegreeOrder,
|
|
43
|
+
transposeChord
|
|
44
|
+
} from "./chunk-D2FAJBYL.js";
|
|
45
|
+
import {
|
|
46
|
+
InvalidInputError,
|
|
47
|
+
assertFiniteNumber,
|
|
48
|
+
formatNote,
|
|
49
|
+
intervalSemitones,
|
|
50
|
+
isConsonantInterval,
|
|
51
|
+
midiToNote,
|
|
52
|
+
noteToMidi,
|
|
53
|
+
noteToPitchClass,
|
|
54
|
+
parseInterval,
|
|
55
|
+
parseNote,
|
|
56
|
+
pitchClassOf,
|
|
57
|
+
spelledInterval,
|
|
58
|
+
transposeByInterval,
|
|
59
|
+
transposeNote
|
|
60
|
+
} from "./chunk-XEBG3G53.js";
|
|
48
61
|
|
|
49
|
-
// src/model/
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
// src/model/interval.ts
|
|
63
|
+
var INVERSION_SUM = 9;
|
|
64
|
+
function invertQuality(quality) {
|
|
65
|
+
if (quality === "P") {
|
|
66
|
+
return "P";
|
|
67
|
+
}
|
|
68
|
+
if (quality === "M") {
|
|
69
|
+
return "m";
|
|
70
|
+
}
|
|
71
|
+
if (quality === "m") {
|
|
72
|
+
return "M";
|
|
73
|
+
}
|
|
74
|
+
return quality.startsWith("A") ? `d${"d".repeat(quality.length - 1)}` : `A${"A".repeat(quality.length - 1)}`;
|
|
56
75
|
}
|
|
76
|
+
var Interval = class _Interval {
|
|
77
|
+
#number;
|
|
78
|
+
#quality;
|
|
79
|
+
#semitones;
|
|
80
|
+
constructor(numberValue, quality, semitones) {
|
|
81
|
+
this.#number = numberValue;
|
|
82
|
+
this.#quality = quality;
|
|
83
|
+
this.#semitones = semitones;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The spelled interval between two notes.
|
|
87
|
+
*
|
|
88
|
+
* @param a The first note.
|
|
89
|
+
* @param b The second note.
|
|
90
|
+
* @returns The interval from `a` to `b`.
|
|
91
|
+
*/
|
|
92
|
+
static between(a, b) {
|
|
93
|
+
return a.intervalTo(b);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Build an interval from explicit components.
|
|
97
|
+
*
|
|
98
|
+
* @param numberValue Diatonic size: 1 = unison, 2 = second, ... 8 = octave.
|
|
99
|
+
* @param quality Quality label: `'P'`, `'M'`, `'m'`, or repeated `'A'`/`'d'`.
|
|
100
|
+
* @param semitones Signed semitone span; its magnitude must be the span the
|
|
101
|
+
* number and quality describe.
|
|
102
|
+
* @returns The interval.
|
|
103
|
+
* @throws If the three components do not describe the same interval — a
|
|
104
|
+
* `P5` spanning 8 semitones is not a value any other method can produce.
|
|
105
|
+
*/
|
|
106
|
+
static of(numberValue, quality, semitones) {
|
|
107
|
+
const expected = intervalSemitones(numberValue, quality);
|
|
108
|
+
if (Math.abs(semitones) !== expected) {
|
|
109
|
+
throw new InvalidInputError(
|
|
110
|
+
`${quality}${numberValue} spans ${expected} semitones; received ${semitones}`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return new _Interval(numberValue, quality, semitones);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Wrap a plain spelled interval, as returned by the pitch module.
|
|
117
|
+
*
|
|
118
|
+
* @param data The plain interval.
|
|
119
|
+
* @returns The wrapped interval.
|
|
120
|
+
*/
|
|
121
|
+
static fromData(data) {
|
|
122
|
+
return new _Interval(data.number, data.quality, data.semitones);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Rebuild an interval from its {@link Interval.toJSON} output.
|
|
126
|
+
*
|
|
127
|
+
* @param data The serialized interval.
|
|
128
|
+
* @returns The wrapped interval.
|
|
129
|
+
*/
|
|
130
|
+
static fromJSON(data) {
|
|
131
|
+
return _Interval.fromData(data);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Parse an interval name such as `'P5'`, `'m3'`, or `'AA4'`.
|
|
135
|
+
*
|
|
136
|
+
* @param name The interval name.
|
|
137
|
+
* @returns The ascending interval of that name.
|
|
138
|
+
* @throws If the name is not a quality label followed by a diatonic number.
|
|
139
|
+
*/
|
|
140
|
+
static parse(name) {
|
|
141
|
+
return _Interval.fromData(parseInterval(name));
|
|
142
|
+
}
|
|
143
|
+
/** Diatonic size: 1 = unison, 2 = second, ... 8 = octave, and beyond. */
|
|
144
|
+
get number() {
|
|
145
|
+
return this.#number;
|
|
146
|
+
}
|
|
147
|
+
/** Quality label: `'P'`, `'M'`, `'m'`, or repeated `'A'`/`'d'`. */
|
|
148
|
+
get quality() {
|
|
149
|
+
return this.#quality;
|
|
150
|
+
}
|
|
151
|
+
/** Signed semitone span. */
|
|
152
|
+
get semitones() {
|
|
153
|
+
return this.#semitones;
|
|
154
|
+
}
|
|
155
|
+
/** A readable label composed of quality and number, e.g. `'P5'` or `'M3'`. */
|
|
156
|
+
get name() {
|
|
157
|
+
return `${this.#quality}${this.#number}`;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The interval's inversion: the complement that completes the octave.
|
|
161
|
+
*
|
|
162
|
+
* A compound interval is reduced to its simple form first, and the result is
|
|
163
|
+
* always ascending — an inversion answers "what is left of the octave",
|
|
164
|
+
* which has no direction of its own.
|
|
165
|
+
*
|
|
166
|
+
* @returns The inverted interval, e.g. `M3` becomes `m6`.
|
|
167
|
+
*/
|
|
168
|
+
invert() {
|
|
169
|
+
const simple = (this.#number - 1) % 7 + 1;
|
|
170
|
+
const numberValue = INVERSION_SUM - simple;
|
|
171
|
+
const quality = invertQuality(this.#quality);
|
|
172
|
+
return _Interval.of(numberValue, quality, intervalSemitones(numberValue, quality));
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Whether the interval is consonant.
|
|
176
|
+
*
|
|
177
|
+
* @param twoVoice When true, the perfect fourth counts as a dissonance,
|
|
178
|
+
* matching two-voice counterpoint.
|
|
179
|
+
* @returns True if the interval is consonant in that context.
|
|
180
|
+
*/
|
|
181
|
+
isConsonant(twoVoice = true) {
|
|
182
|
+
return isConsonantInterval(this.#semitones, twoVoice);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Whether another interval is spelled identically.
|
|
186
|
+
*
|
|
187
|
+
* Enharmonic equivalents are not equal: an augmented second and a minor
|
|
188
|
+
* third span the same distance but are different intervals.
|
|
189
|
+
*
|
|
190
|
+
* @param other The interval to compare with.
|
|
191
|
+
* @returns True when number, quality, and span all match.
|
|
192
|
+
*/
|
|
193
|
+
equals(other) {
|
|
194
|
+
return this.#number === other.number && this.#quality === other.quality && this.#semitones === other.semitones;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* The plain interval data, for JSON serialization.
|
|
198
|
+
*
|
|
199
|
+
* Private class fields do not serialize, so an explicit `toJSON` keeps
|
|
200
|
+
* `JSON.stringify(interval)` from collapsing to `{}`.
|
|
201
|
+
*
|
|
202
|
+
* @returns The diatonic number, quality, and semitone span.
|
|
203
|
+
*/
|
|
204
|
+
toJSON() {
|
|
205
|
+
return { number: this.#number, quality: this.#quality, semitones: this.#semitones };
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* The interval's name, so a template literal or a log line reads as the
|
|
209
|
+
* interval.
|
|
210
|
+
*
|
|
211
|
+
* @returns The name, e.g. `'M3'`.
|
|
212
|
+
*/
|
|
213
|
+
toString() {
|
|
214
|
+
return this.name;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
57
217
|
|
|
58
218
|
// src/model/note.ts
|
|
59
219
|
function copyNote(data) {
|
|
220
|
+
if (!Number.isInteger(data.letter) || data.letter < 0 || data.letter > 6) {
|
|
221
|
+
throw new InvalidInputError(
|
|
222
|
+
`note letter must be an integer in [0, 6]; received ${data.letter}`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
60
225
|
const copy = { letter: data.letter, alter: data.alter };
|
|
61
226
|
if (data.octave !== void 0) {
|
|
62
227
|
copy.octave = data.octave;
|
|
@@ -69,6 +234,7 @@ var Note = class _Note {
|
|
|
69
234
|
* Wrap a plain note object.
|
|
70
235
|
*
|
|
71
236
|
* @param data The spelled note; it is copied, never retained or mutated.
|
|
237
|
+
* @throws If `letter` is not an integer in 0..6.
|
|
72
238
|
*/
|
|
73
239
|
constructor(data) {
|
|
74
240
|
this.#data = copyNote(data);
|
|
@@ -98,6 +264,7 @@ var Note = class _Note {
|
|
|
98
264
|
*
|
|
99
265
|
* @param data The plain note.
|
|
100
266
|
* @returns The wrapped note.
|
|
267
|
+
* @throws If `letter` is not an integer in 0..6.
|
|
101
268
|
*/
|
|
102
269
|
static fromData(data) {
|
|
103
270
|
return new _Note(data);
|
|
@@ -117,7 +284,7 @@ var Note = class _Note {
|
|
|
117
284
|
*/
|
|
118
285
|
get midi() {
|
|
119
286
|
if (this.#data.octave === void 0) {
|
|
120
|
-
throw new
|
|
287
|
+
throw new InvalidInputError(`note ${this.name} has no octave, so it has no MIDI number`);
|
|
121
288
|
}
|
|
122
289
|
return noteToMidi(this.#data);
|
|
123
290
|
}
|
|
@@ -138,33 +305,67 @@ var Note = class _Note {
|
|
|
138
305
|
return copyNote(this.#data);
|
|
139
306
|
}
|
|
140
307
|
/**
|
|
141
|
-
* Transpose by a signed number of semitones.
|
|
308
|
+
* Transpose by a signed number of semitones, keeping the spelling.
|
|
142
309
|
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
310
|
+
* The letter moves by the diatonic distance of the conventional interval for
|
|
311
|
+
* that many semitones, so `Ab4` up a major second is `Bb4` rather than `A#4`
|
|
312
|
+
* and a flat key stays on the flat side. An octave-less note stays
|
|
145
313
|
* octave-less: only its pitch class is moved. Transposing by zero is the
|
|
146
314
|
* identity: the original spelling is preserved (no enharmonic respelling).
|
|
147
315
|
*
|
|
148
316
|
* @param semitones The signed semitone offset.
|
|
317
|
+
* @param opts `spelling` forces the result onto the sharp or flat side
|
|
318
|
+
* instead of following this note's letter.
|
|
149
319
|
* @returns The transposed note.
|
|
320
|
+
* @example
|
|
321
|
+
* ```ts
|
|
322
|
+
* import { Note } from '@libraz/libcantus';
|
|
323
|
+
* Note.of('Ab4').transpose(2).name; // 'Bb4'
|
|
324
|
+
* Note.of('Ab4').transpose(2, { spelling: 'sharp' }).name; // 'A#4'
|
|
325
|
+
* ```
|
|
150
326
|
*/
|
|
151
|
-
transpose(semitones) {
|
|
152
|
-
if (semitones === 0) {
|
|
327
|
+
transpose(semitones, opts) {
|
|
328
|
+
if (semitones === 0 && opts?.spelling === void 0) {
|
|
153
329
|
return new _Note(this.#data);
|
|
154
330
|
}
|
|
155
|
-
|
|
156
|
-
return _Note.fromMidi(this.midi + semitones);
|
|
157
|
-
}
|
|
158
|
-
return new _Note(spellPitchClassBare(this.pitchClass + semitones, "sharp"));
|
|
331
|
+
return new _Note(transposeNote(this.#data, semitones, opts));
|
|
159
332
|
}
|
|
160
333
|
/**
|
|
161
334
|
* The spelled interval from this note to another.
|
|
162
335
|
*
|
|
163
336
|
* @param other The second note.
|
|
164
|
-
* @returns The
|
|
337
|
+
* @returns The interval, which {@link Interval.toJSON} unwraps to plain data.
|
|
165
338
|
*/
|
|
166
339
|
intervalTo(other) {
|
|
167
|
-
return spelledInterval(this.#data, other
|
|
340
|
+
return Interval.fromData(spelledInterval(this.#data, other.data));
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Transpose by a spelled interval, keeping the spelling the interval names.
|
|
344
|
+
*
|
|
345
|
+
* Unlike {@link Note.transpose}, which picks a letter from the semitone
|
|
346
|
+
* count, the interval's diatonic number decides the letter: C up an
|
|
347
|
+
* augmented second is D#, not Eb.
|
|
348
|
+
*
|
|
349
|
+
* @param interval The interval to apply; a descending interval moves down.
|
|
350
|
+
* @returns The transposed note.
|
|
351
|
+
* @example
|
|
352
|
+
* ```ts
|
|
353
|
+
* import { Interval, Note } from '@libraz/libcantus';
|
|
354
|
+
* Note.of('C4').transposeBy(Interval.parse('A2')).name; // 'D#4'
|
|
355
|
+
* ```
|
|
356
|
+
*/
|
|
357
|
+
transposeBy(interval) {
|
|
358
|
+
return new _Note(transposeByInterval(this.#data, interval.toJSON()));
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Rebuild a note from its {@link Note.toJSON} output.
|
|
362
|
+
*
|
|
363
|
+
* @param data The serialized note.
|
|
364
|
+
* @returns The wrapped note.
|
|
365
|
+
* @throws If `letter` is not an integer in 0..6.
|
|
366
|
+
*/
|
|
367
|
+
static fromJSON(data) {
|
|
368
|
+
return new _Note(data);
|
|
168
369
|
}
|
|
169
370
|
/**
|
|
170
371
|
* Whether another note has the same letter, alteration, and octave.
|
|
@@ -173,7 +374,8 @@ var Note = class _Note {
|
|
|
173
374
|
* @returns True if the spellings are identical.
|
|
174
375
|
*/
|
|
175
376
|
equals(other) {
|
|
176
|
-
|
|
377
|
+
const b = other.data;
|
|
378
|
+
return this.#data.letter === b.letter && this.#data.alter === b.alter && this.#data.octave === b.octave;
|
|
177
379
|
}
|
|
178
380
|
/**
|
|
179
381
|
* The plain note data, for JSON serialization.
|
|
@@ -186,722 +388,1062 @@ var Note = class _Note {
|
|
|
186
388
|
toJSON() {
|
|
187
389
|
return this.data;
|
|
188
390
|
}
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
// src/model/progression.ts
|
|
192
|
-
var Progression = class _Progression {
|
|
193
|
-
#chords;
|
|
194
|
-
#key;
|
|
195
391
|
/**
|
|
196
|
-
*
|
|
392
|
+
* The note's name, so a template literal or a log line reads as the note.
|
|
197
393
|
*
|
|
198
|
-
* @
|
|
199
|
-
* @param key Optional key context for analysis methods.
|
|
394
|
+
* @returns The spelled name, e.g. `'Bb3'`.
|
|
200
395
|
*/
|
|
201
|
-
|
|
202
|
-
this
|
|
203
|
-
this.#key = key;
|
|
204
|
-
}
|
|
205
|
-
/** A copy of the chord sequence. */
|
|
206
|
-
get chords() {
|
|
207
|
-
return [...this.#chords];
|
|
396
|
+
toString() {
|
|
397
|
+
return this.name;
|
|
208
398
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
// src/model/shared.ts
|
|
402
|
+
function spellPitchClassBare(pc, spelling) {
|
|
403
|
+
const spelled = midiToNote(60 + pitchClassOf(pc), spelling);
|
|
404
|
+
return { letter: spelled.letter, alter: spelled.alter };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/model/key.ts
|
|
408
|
+
function accidentalLoad(tonic, scale) {
|
|
409
|
+
return spellScale(tonic, scale).reduce((sum, note) => sum + Math.abs(note.alter), 0);
|
|
410
|
+
}
|
|
411
|
+
function bestTonicForScale(rootPc, scale) {
|
|
412
|
+
const sharp = spellPitchClassBare(rootPc, "sharp");
|
|
413
|
+
const flat = spellPitchClassBare(rootPc, "flat");
|
|
414
|
+
if (sharp.letter === flat.letter && sharp.alter === flat.alter) {
|
|
415
|
+
return new Note(sharp);
|
|
212
416
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
417
|
+
return accidentalLoad(flat, scale) <= accidentalLoad(sharp, scale) ? new Note(flat) : new Note(sharp);
|
|
418
|
+
}
|
|
419
|
+
var Key = class _Key {
|
|
420
|
+
#scale;
|
|
421
|
+
#tonic;
|
|
422
|
+
/**
|
|
423
|
+
* Wrap a key/scale and its spelled tonic.
|
|
424
|
+
*
|
|
425
|
+
* @param scale The key/scale; its root is normalized to a pitch class.
|
|
426
|
+
* @param tonic The spelled tonic anchoring letter-name spelling.
|
|
427
|
+
*/
|
|
428
|
+
constructor(scale, tonic) {
|
|
429
|
+
const rootPc = pitchClassOf(scale.rootPc);
|
|
430
|
+
if (tonic.pitchClass !== rootPc) {
|
|
431
|
+
throw new InvalidInputError(
|
|
432
|
+
`tonic ${tonic.name} does not match the scale root pitch class ${rootPc}; pass a tonic that spells the scale root, or omit it to have one chosen`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
this.#scale = { rootPc, modeMask12: scale.modeMask12 };
|
|
436
|
+
this.#tonic = tonic;
|
|
216
437
|
}
|
|
217
438
|
/**
|
|
218
|
-
* A
|
|
439
|
+
* A major key.
|
|
219
440
|
*
|
|
220
|
-
* @param
|
|
221
|
-
*
|
|
441
|
+
* @param root Tonic as a note name (e.g. `'Eb'`) or a pitch class; a numeric
|
|
442
|
+
* root is spelled with whichever accidental side yields the fewest
|
|
443
|
+
* accidentals across the scale.
|
|
444
|
+
* @returns The major key.
|
|
222
445
|
*/
|
|
223
|
-
|
|
224
|
-
|
|
446
|
+
static major(root) {
|
|
447
|
+
if (typeof root === "string") {
|
|
448
|
+
const tonic = Note.of(root);
|
|
449
|
+
return new _Key(majorKey(tonic.pitchClass), tonic);
|
|
450
|
+
}
|
|
451
|
+
const scale = majorKey(root);
|
|
452
|
+
return new _Key(scale, bestTonicForScale(root, scale));
|
|
225
453
|
}
|
|
226
454
|
/**
|
|
227
|
-
* A
|
|
455
|
+
* A natural-minor key.
|
|
228
456
|
*
|
|
229
|
-
* @param
|
|
230
|
-
*
|
|
457
|
+
* @param root Tonic as a note name or a pitch class; a numeric root is
|
|
458
|
+
* spelled with whichever accidental side yields the fewest accidentals
|
|
459
|
+
* across the scale.
|
|
460
|
+
* @returns The minor key.
|
|
231
461
|
*/
|
|
232
|
-
|
|
233
|
-
|
|
462
|
+
static minor(root) {
|
|
463
|
+
if (typeof root === "string") {
|
|
464
|
+
const tonic = Note.of(root);
|
|
465
|
+
return new _Key(minorKey(tonic.pitchClass), tonic);
|
|
466
|
+
}
|
|
467
|
+
const scale = minorKey(root);
|
|
468
|
+
return new _Key(scale, bestTonicForScale(root, scale));
|
|
234
469
|
}
|
|
235
470
|
/**
|
|
236
|
-
*
|
|
471
|
+
* A key on a named scale (e.g. `'dorian'`, `'harmonicMinor'`).
|
|
237
472
|
*
|
|
238
|
-
* @param
|
|
239
|
-
* @
|
|
240
|
-
*
|
|
473
|
+
* @param name The scale name, a key of the scale module's named-scale table.
|
|
474
|
+
* @param root Tonic as a note name or a pitch class; a numeric root is
|
|
475
|
+
* spelled with whichever accidental side yields the fewest accidentals
|
|
476
|
+
* across the scale, exactly as {@link Key.major} does.
|
|
477
|
+
* @returns The key.
|
|
478
|
+
* @throws If the name is not a known scale.
|
|
241
479
|
*/
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
480
|
+
static named(name, root) {
|
|
481
|
+
if (typeof root === "string") {
|
|
482
|
+
const tonic = Note.of(root);
|
|
483
|
+
return new _Key(scaleByName(name, tonic.pitchClass), tonic);
|
|
484
|
+
}
|
|
485
|
+
const scale = scaleByName(name, pitchClassOf(root));
|
|
486
|
+
return new _Key(scale, bestTonicForScale(pitchClassOf(root), scale));
|
|
247
487
|
}
|
|
248
488
|
/**
|
|
249
|
-
*
|
|
489
|
+
* Wrap an existing `KeyScale`, synthesizing a spelled tonic when none is
|
|
490
|
+
* given.
|
|
250
491
|
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
492
|
+
* The synthesized tonic is chosen the same way as for a numeric root
|
|
493
|
+
* elsewhere: whichever accidental side spells the scale with the fewest
|
|
494
|
+
* accidentals. This is what keeps `Key.of(detectKey(...).scale)` from handing
|
|
495
|
+
* every downstream chord a double-sharp spelling.
|
|
496
|
+
*
|
|
497
|
+
* @param scale The key/scale to wrap.
|
|
498
|
+
* @param tonic Optional spelled tonic; must spell the scale's root pitch class.
|
|
499
|
+
* @returns The key.
|
|
500
|
+
* @throws If the given tonic is not the scale's root pitch class.
|
|
254
501
|
*/
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
return this.#chords.map((chord) => chord.roman(resolved));
|
|
502
|
+
static of(scale, tonic) {
|
|
503
|
+
return new _Key(scale, tonic ?? bestTonicForScale(pitchClassOf(scale.rootPc), scale));
|
|
258
504
|
}
|
|
259
505
|
/**
|
|
260
|
-
*
|
|
506
|
+
* Rebuild a key from its {@link Key.toJSON} output.
|
|
261
507
|
*
|
|
262
|
-
* @param
|
|
263
|
-
* @returns
|
|
264
|
-
* @throws If no key is given and none is carried.
|
|
508
|
+
* @param data The serialized key and tonic.
|
|
509
|
+
* @returns The key.
|
|
265
510
|
*/
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
return this.#chords.map((chord) => chord.function(resolved));
|
|
511
|
+
static fromJSON(data) {
|
|
512
|
+
return new _Key(data.scale, new Note(data.tonic));
|
|
269
513
|
}
|
|
270
514
|
/**
|
|
271
|
-
*
|
|
515
|
+
* Identify the keys a set of pitches fits, best interpretation first.
|
|
272
516
|
*
|
|
273
|
-
* The
|
|
274
|
-
* progression has fewer than two chords.
|
|
517
|
+
* The counterpart of {@link Chord.detect}.
|
|
275
518
|
*
|
|
276
|
-
* @param
|
|
277
|
-
* @
|
|
278
|
-
* @
|
|
519
|
+
* @param pitches MIDI pitches or bare pitch classes.
|
|
520
|
+
* @param opts How to weigh the input; see {@link DetectKeyOptions}.
|
|
521
|
+
* @returns Ranked keys (may be empty).
|
|
279
522
|
*/
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const chords = this.#chords.map((chord) => chord.analyze(resolved));
|
|
283
|
-
const from = this.#chords[this.#chords.length - 2];
|
|
284
|
-
const to = this.#chords[this.#chords.length - 1];
|
|
285
|
-
const cadence = from !== void 0 && to !== void 0 ? detectCadence(from.data, to.data, resolved.scale) : null;
|
|
286
|
-
return { chords, cadence };
|
|
523
|
+
static detect(pitches, opts) {
|
|
524
|
+
return detectKey(pitches, opts).map((match) => _Key.of(match.key));
|
|
287
525
|
}
|
|
288
526
|
/**
|
|
289
|
-
* The
|
|
527
|
+
* The single best key interpretation of a pitch set.
|
|
290
528
|
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
529
|
+
* @param pitches MIDI pitches or bare pitch classes.
|
|
530
|
+
* @param opts How to weigh the input; see {@link DetectKeyOptions}.
|
|
531
|
+
* @returns The top-ranked key, or null when nothing matches.
|
|
532
|
+
* @example
|
|
533
|
+
* ```ts
|
|
534
|
+
* import { Key } from '@libraz/libcantus';
|
|
535
|
+
* Key.detectBest([0, 2, 4, 5, 7, 9, 11])?.toString(); // 'C major'
|
|
536
|
+
* ```
|
|
537
|
+
*/
|
|
538
|
+
static detectBest(pitches, opts) {
|
|
539
|
+
const best = detectKeyBest(pitches, opts);
|
|
540
|
+
return best === null ? null : _Key.of(best.key);
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Whether another key has the same tonic pitch class and mode.
|
|
294
544
|
*
|
|
295
|
-
*
|
|
545
|
+
* The spelled tonic is not compared: C# major and Db major are the same key
|
|
546
|
+
* written two ways.
|
|
547
|
+
*
|
|
548
|
+
* @param other The key to compare.
|
|
549
|
+
* @returns True when tonic and mode mask both match.
|
|
296
550
|
*/
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
key: this.#key?.toJSON()
|
|
301
|
-
};
|
|
551
|
+
equals(other) {
|
|
552
|
+
const scale = other.scale;
|
|
553
|
+
return this.#scale.rootPc === scale.rootPc && this.#scale.modeMask12 === scale.modeMask12;
|
|
302
554
|
}
|
|
303
|
-
/**
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (resolved === void 0) {
|
|
307
|
-
throw new Error("progression has no key context; pass a Key or attach one with withKey()");
|
|
308
|
-
}
|
|
309
|
-
return resolved;
|
|
555
|
+
/** A copy of the underlying plain `KeyScale`. */
|
|
556
|
+
get scale() {
|
|
557
|
+
return { rootPc: this.#scale.rootPc, modeMask12: this.#scale.modeMask12 };
|
|
310
558
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
function copyChord(data) {
|
|
315
|
-
const copy = {
|
|
316
|
-
rootPc: data.rootPc,
|
|
317
|
-
quality: data.quality,
|
|
318
|
-
intervals: [...data.intervals]
|
|
319
|
-
};
|
|
320
|
-
if (data.bassPc !== void 0) {
|
|
321
|
-
copy.bassPc = data.bassPc;
|
|
559
|
+
/** The spelled tonic. */
|
|
560
|
+
get tonic() {
|
|
561
|
+
return this.#tonic;
|
|
322
562
|
}
|
|
323
|
-
|
|
324
|
-
|
|
563
|
+
/** The tonic pitch class (0..11). */
|
|
564
|
+
get rootPc() {
|
|
565
|
+
return this.#scale.rootPc;
|
|
325
566
|
}
|
|
326
|
-
|
|
327
|
-
|
|
567
|
+
/** Whether the scale has a minor third and no major third. */
|
|
568
|
+
get isMinor() {
|
|
569
|
+
return isMinorKey(this.#scale);
|
|
328
570
|
}
|
|
329
|
-
return copy;
|
|
330
|
-
}
|
|
331
|
-
var Chord = class _Chord {
|
|
332
|
-
#data;
|
|
333
|
-
#key;
|
|
334
571
|
/**
|
|
335
|
-
*
|
|
572
|
+
* The scale's pitch classes in ascending scale-degree order (degree 0 first).
|
|
336
573
|
*
|
|
337
|
-
* @
|
|
338
|
-
* @param key Optional key context for analysis methods.
|
|
574
|
+
* @returns One pitch class per scale degree.
|
|
339
575
|
*/
|
|
340
|
-
|
|
341
|
-
this.#
|
|
342
|
-
this.#key = key;
|
|
576
|
+
pitchClasses() {
|
|
577
|
+
return scaleTonesInDegreeOrder(this.#scale);
|
|
343
578
|
}
|
|
344
579
|
/**
|
|
345
|
-
*
|
|
580
|
+
* Transpose the key by a number of semitones.
|
|
346
581
|
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
582
|
+
* The mode mask is unchanged, so the scale keeps its shape; only the tonic
|
|
583
|
+
* moves. The new tonic is spelled the way that key is conventionally written
|
|
584
|
+
* — D major, not C## major.
|
|
585
|
+
*
|
|
586
|
+
* @param semitones The signed semitone offset.
|
|
587
|
+
* @returns The transposed key.
|
|
588
|
+
* @example
|
|
589
|
+
* ```ts
|
|
590
|
+
* import { Key } from '@libraz/libcantus';
|
|
591
|
+
* Key.major('C').transpose(2).toString(); // 'D major'
|
|
592
|
+
* ```
|
|
351
593
|
*/
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
594
|
+
transpose(semitones) {
|
|
595
|
+
assertFiniteNumber(semitones, "semitones");
|
|
596
|
+
const rootPc = pitchClassOf(this.#scale.rootPc + Math.round(semitones));
|
|
597
|
+
return _Key.of({ rootPc, modeMask12: this.#scale.modeMask12 });
|
|
355
598
|
}
|
|
356
599
|
/**
|
|
357
|
-
*
|
|
600
|
+
* The spelled scale, one note per degree (e.g. C D E F G A B for C major).
|
|
358
601
|
*
|
|
359
|
-
* @
|
|
360
|
-
* @returns The wrapped chord (without key context).
|
|
602
|
+
* @returns Spelled octave-less notes in scale-degree order.
|
|
361
603
|
*/
|
|
362
|
-
|
|
363
|
-
return new
|
|
604
|
+
notes() {
|
|
605
|
+
return spellScale(this.#tonic.data, this.#scale).map((note) => new Note(note));
|
|
364
606
|
}
|
|
365
607
|
/**
|
|
366
|
-
*
|
|
608
|
+
* Alias of {@link Key.notes}.
|
|
367
609
|
*
|
|
368
|
-
* @
|
|
369
|
-
* @returns The chord (without key context).
|
|
370
|
-
* @throws If the root or quality is not recognized.
|
|
610
|
+
* @returns Spelled octave-less notes in scale-degree order.
|
|
371
611
|
*/
|
|
372
|
-
|
|
373
|
-
return
|
|
612
|
+
spell() {
|
|
613
|
+
return this.notes();
|
|
374
614
|
}
|
|
375
615
|
/**
|
|
376
|
-
*
|
|
616
|
+
* The spelled scale as letter-name strings.
|
|
377
617
|
*
|
|
378
|
-
* @
|
|
379
|
-
* @returns Ranked chord interpretations (may be empty).
|
|
618
|
+
* @returns One name per scale degree.
|
|
380
619
|
*/
|
|
381
|
-
|
|
382
|
-
return
|
|
383
|
-
(match) => new _Chord(makeChord(match.rootPc, match.quality, match.bassPc))
|
|
384
|
-
);
|
|
620
|
+
noteNames() {
|
|
621
|
+
return this.notes().map((note) => note.name);
|
|
385
622
|
}
|
|
386
623
|
/**
|
|
387
|
-
*
|
|
624
|
+
* Build a chord on a scale degree, carrying this key as context.
|
|
388
625
|
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
626
|
+
* With an explicit quality the quality's interval template is attached to the
|
|
627
|
+
* degree's diatonic root; without one the scale-correct diatonic triad is
|
|
628
|
+
* stacked (e.g. a diminished triad on the leading tone of a major key).
|
|
629
|
+
*
|
|
630
|
+
* @param degree 0-based scale degree of the chord root.
|
|
631
|
+
* @param quality Optional chord quality.
|
|
632
|
+
* @returns The chord, with this key attached.
|
|
633
|
+
* @throws Without a `quality`, if this key's scale is not heptatonic —
|
|
634
|
+
* stacking thirds needs seven degrees, so the pentatonic, blues,
|
|
635
|
+
* whole-tone, octatonic and chromatic scales have no diatonic triad. Pass
|
|
636
|
+
* an explicit `quality` for those.
|
|
391
637
|
*/
|
|
392
|
-
|
|
393
|
-
const
|
|
394
|
-
return
|
|
395
|
-
}
|
|
396
|
-
/** The root pitch class (0..11). */
|
|
397
|
-
get rootPc() {
|
|
398
|
-
return this.#data.rootPc;
|
|
399
|
-
}
|
|
400
|
-
/** The chord quality. */
|
|
401
|
-
get quality() {
|
|
402
|
-
return this.#data.quality;
|
|
403
|
-
}
|
|
404
|
-
/** A copy of the semitone offsets above the root. */
|
|
405
|
-
get intervals() {
|
|
406
|
-
return [...this.#data.intervals];
|
|
407
|
-
}
|
|
408
|
-
/** The slash-chord bass pitch class, or undefined in root position. */
|
|
409
|
-
get bassPc() {
|
|
410
|
-
return this.#data.bassPc;
|
|
411
|
-
}
|
|
412
|
-
/** A copy of the underlying plain chord object. */
|
|
413
|
-
get data() {
|
|
414
|
-
return copyChord(this.#data);
|
|
415
|
-
}
|
|
416
|
-
/** The carried key context, if any. */
|
|
417
|
-
get key() {
|
|
418
|
-
return this.#key;
|
|
638
|
+
chord(degree, quality) {
|
|
639
|
+
const data = quality === void 0 ? diatonicTriad(degree, this.#scale) : chordFromDegree(degree, quality, this.#scale);
|
|
640
|
+
return new Chord(data, this);
|
|
419
641
|
}
|
|
420
642
|
/**
|
|
421
|
-
*
|
|
643
|
+
* The diatonic triad on a scale degree, carrying this key as context.
|
|
422
644
|
*
|
|
423
|
-
* @param
|
|
424
|
-
* @returns The
|
|
645
|
+
* @param degree 0-based scale degree of the chord root.
|
|
646
|
+
* @returns The triad, with this key attached.
|
|
647
|
+
* @throws If this key's scale is not heptatonic; stacking thirds needs seven
|
|
648
|
+
* degrees. Use {@link Key.chord} with an explicit quality instead.
|
|
425
649
|
*/
|
|
426
|
-
|
|
427
|
-
return new
|
|
650
|
+
diatonicTriad(degree) {
|
|
651
|
+
return new Chord(diatonicTriad(degree, this.#scale), this);
|
|
428
652
|
}
|
|
429
653
|
/**
|
|
430
|
-
* The chord
|
|
654
|
+
* The diatonic seventh chord on a scale degree, carrying this key as context.
|
|
431
655
|
*
|
|
432
|
-
* @
|
|
656
|
+
* @param degree 0-based scale degree of the chord root.
|
|
657
|
+
* @returns The seventh chord, with this key attached.
|
|
658
|
+
* @throws If this key's scale is not heptatonic; stacking thirds needs seven
|
|
659
|
+
* degrees. Use {@link Key.chord} with an explicit quality instead.
|
|
433
660
|
*/
|
|
434
|
-
|
|
435
|
-
return
|
|
661
|
+
diatonicSeventh(degree) {
|
|
662
|
+
return new Chord(diatonicSeventh(degree, this.#scale), this);
|
|
436
663
|
}
|
|
437
664
|
/**
|
|
438
|
-
*
|
|
665
|
+
* Build the chord denoted by a Roman numeral in this key (including applied
|
|
666
|
+
* chords such as `'V7/V'`), carrying this key as context.
|
|
439
667
|
*
|
|
440
|
-
* @param
|
|
441
|
-
* @returns The
|
|
442
|
-
* @throws If
|
|
668
|
+
* @param text The Roman numeral.
|
|
669
|
+
* @returns The chord, with this key attached.
|
|
670
|
+
* @throws If the numeral is not valid.
|
|
443
671
|
*/
|
|
444
|
-
roman(
|
|
445
|
-
return
|
|
672
|
+
roman(text) {
|
|
673
|
+
return new Chord(romanToChord(text, this.#scale), this);
|
|
446
674
|
}
|
|
447
675
|
/**
|
|
448
|
-
*
|
|
676
|
+
* Whether a pitch belongs to the scale.
|
|
449
677
|
*
|
|
450
|
-
* @param
|
|
451
|
-
* @returns
|
|
452
|
-
* @throws If no key is given and none is carried.
|
|
678
|
+
* @param x A MIDI pitch, bare pitch class, or note.
|
|
679
|
+
* @returns True if the pitch class is a scale tone.
|
|
453
680
|
*/
|
|
454
|
-
|
|
455
|
-
return
|
|
681
|
+
contains(x) {
|
|
682
|
+
return isScaleTone(typeof x === "number" ? x : x.pitchClass, this.#scale);
|
|
456
683
|
}
|
|
457
684
|
/**
|
|
458
|
-
*
|
|
685
|
+
* The plain key data, for JSON serialization.
|
|
459
686
|
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
*
|
|
687
|
+
* Private class fields do not serialize, so an explicit `toJSON` keeps
|
|
688
|
+
* `JSON.stringify(key)` from collapsing to `{}`. The result pairs the
|
|
689
|
+
* `KeyScale` with the spelled tonic, enough to reconstruct the key via
|
|
690
|
+
* {@link Key.of}.
|
|
691
|
+
*
|
|
692
|
+
* @returns The key/scale and its spelled tonic.
|
|
463
693
|
*/
|
|
464
|
-
|
|
465
|
-
return
|
|
694
|
+
toJSON() {
|
|
695
|
+
return { scale: this.scale, tonic: this.#tonic.data };
|
|
466
696
|
}
|
|
467
697
|
/**
|
|
468
|
-
*
|
|
698
|
+
* The key's tonic and mode, so a template literal or a log line reads as the
|
|
699
|
+
* key. Only the major/minor distinction is named, since a mask does not carry
|
|
700
|
+
* a scale name.
|
|
469
701
|
*
|
|
470
|
-
* @
|
|
471
|
-
* @returns True if the chord is borrowed.
|
|
472
|
-
* @throws If no key is given and none is carried.
|
|
702
|
+
* @returns The name, e.g. `'C major'` or `'A minor'`.
|
|
473
703
|
*/
|
|
474
|
-
|
|
475
|
-
return
|
|
704
|
+
toString() {
|
|
705
|
+
return `${this.#tonic.name} ${this.isMinor ? "minor" : "major"}`;
|
|
476
706
|
}
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
// src/model/progression.ts
|
|
710
|
+
var Progression = class _Progression {
|
|
711
|
+
#chords;
|
|
712
|
+
#key;
|
|
477
713
|
/**
|
|
478
|
-
*
|
|
714
|
+
* Wrap a chord sequence.
|
|
479
715
|
*
|
|
480
|
-
* @param
|
|
481
|
-
* @
|
|
482
|
-
* @throws If no key is given and none is carried.
|
|
716
|
+
* @param chords The chords in order; the array is copied.
|
|
717
|
+
* @param key Optional key context for analysis methods.
|
|
483
718
|
*/
|
|
484
|
-
|
|
485
|
-
|
|
719
|
+
constructor(chords, key) {
|
|
720
|
+
this.#chords = Object.freeze([...chords]);
|
|
721
|
+
this.#key = key;
|
|
486
722
|
}
|
|
487
723
|
/**
|
|
488
|
-
*
|
|
724
|
+
* Build a progression from the {@link ChordSpan} records the generators
|
|
725
|
+
* return, keeping their order.
|
|
489
726
|
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
727
|
+
* The spans' `startBeat`, `degree`, and `secondaryDominant` are analysis
|
|
728
|
+
* annotations that a chord sequence does not carry; only the harmony crosses
|
|
729
|
+
* over. Keep the spans themselves if the timing matters.
|
|
730
|
+
*
|
|
731
|
+
* @param spans The chord spans, in order.
|
|
732
|
+
* @param key Optional key context for the analysis methods.
|
|
733
|
+
* @returns The progression.
|
|
734
|
+
* @example
|
|
735
|
+
* ```ts
|
|
736
|
+
* import { generateProgression, Key, Progression } from '@libraz/libcantus';
|
|
737
|
+
* const key = Key.major('C');
|
|
738
|
+
* const spans = generateProgression({ key: key.scale, style: 'dance', bars: 4 });
|
|
739
|
+
* Progression.fromSpans(spans, key).roman();
|
|
740
|
+
* ```
|
|
492
741
|
*/
|
|
493
|
-
|
|
494
|
-
|
|
742
|
+
static fromSpans(spans, key) {
|
|
743
|
+
const chords = spans.map((span) => {
|
|
744
|
+
const chord = Chord.from(makeChord(span.rootPc, span.quality, span.bassPc));
|
|
745
|
+
return key === void 0 ? chord : chord.withKey(key);
|
|
746
|
+
});
|
|
747
|
+
return key === void 0 ? new _Progression(chords) : new _Progression(chords, key);
|
|
495
748
|
}
|
|
496
749
|
/**
|
|
497
|
-
*
|
|
750
|
+
* Rebuild a progression from its {@link Progression.toJSON} output.
|
|
498
751
|
*
|
|
499
|
-
* @param
|
|
500
|
-
* @returns
|
|
501
|
-
* @throws If no voicing fits the given ranges.
|
|
752
|
+
* @param data The serialized chords and key.
|
|
753
|
+
* @returns The progression.
|
|
502
754
|
*/
|
|
503
|
-
|
|
504
|
-
|
|
755
|
+
static fromJSON(data) {
|
|
756
|
+
const key = data.key === void 0 ? void 0 : Key.fromJSON(data.key);
|
|
757
|
+
const chords = data.chords.map((chord) => Chord.fromJSON(chord));
|
|
758
|
+
return key === void 0 ? new _Progression(chords) : new _Progression(chords, key);
|
|
505
759
|
}
|
|
506
760
|
/**
|
|
507
|
-
*
|
|
508
|
-
* `shell`, or `rootless`), optionally constraining the top voice.
|
|
761
|
+
* The chord sequence.
|
|
509
762
|
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
763
|
+
* The array is the progression's own and is frozen rather than copied, so
|
|
764
|
+
* reading it in a loop stays linear.
|
|
512
765
|
*/
|
|
513
|
-
|
|
514
|
-
return
|
|
766
|
+
get chords() {
|
|
767
|
+
return this.#chords;
|
|
768
|
+
}
|
|
769
|
+
/** The carried key context, if any. */
|
|
770
|
+
get key() {
|
|
771
|
+
return this.#key;
|
|
772
|
+
}
|
|
773
|
+
/** The number of chords. */
|
|
774
|
+
get length() {
|
|
775
|
+
return this.#chords.length;
|
|
515
776
|
}
|
|
516
777
|
/**
|
|
517
|
-
* The
|
|
518
|
-
* axis (major becomes minor and vice versa).
|
|
778
|
+
* The chord at an index.
|
|
519
779
|
*
|
|
520
|
-
* @param
|
|
521
|
-
*
|
|
522
|
-
* @returns The mirrored chord, keeping any key context.
|
|
523
|
-
* @throws If no key is given and none is carried.
|
|
780
|
+
* @param index 0-based position; a negative index counts from the end.
|
|
781
|
+
* @returns The chord, or undefined when the index is out of range.
|
|
524
782
|
*/
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
783
|
+
at(index) {
|
|
784
|
+
return this.#chords.at(index);
|
|
785
|
+
}
|
|
786
|
+
/** Iterate the chords in order, so a progression works with `for...of`. */
|
|
787
|
+
[Symbol.iterator]() {
|
|
788
|
+
return this.#chords[Symbol.iterator]();
|
|
528
789
|
}
|
|
529
790
|
/**
|
|
530
|
-
*
|
|
531
|
-
* root in the interval template (`invert(1)` puts the third in the bass).
|
|
532
|
-
* `n` wraps around the template length; negative values count backwards.
|
|
791
|
+
* Whether another progression holds the same chords in the same order.
|
|
533
792
|
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
793
|
+
* The key context is not compared: it is an analysis lens, not part of the
|
|
794
|
+
* harmony. Chord equality follows {@link Chord.equals}.
|
|
536
795
|
*
|
|
537
|
-
* @param
|
|
538
|
-
* @returns
|
|
539
|
-
* @throws If the chord has no intervals.
|
|
796
|
+
* @param other The progression to compare.
|
|
797
|
+
* @returns True when the chord sequences match.
|
|
540
798
|
*/
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
if (length === 0) {
|
|
545
|
-
throw new Error("cannot invert a chord with no intervals");
|
|
546
|
-
}
|
|
547
|
-
const index = (n % length + length) % length;
|
|
548
|
-
const data = copyChord(this.#data);
|
|
549
|
-
if (index === 0) {
|
|
550
|
-
delete data.bassPc;
|
|
551
|
-
} else {
|
|
552
|
-
data.bassPc = mod12(this.#data.rootPc + (intervals[index] ?? 0));
|
|
799
|
+
equals(other) {
|
|
800
|
+
if (this.#chords.length !== other.length) {
|
|
801
|
+
return false;
|
|
553
802
|
}
|
|
554
|
-
return
|
|
803
|
+
return this.#chords.every((chord, index) => {
|
|
804
|
+
const theirs = other.at(index);
|
|
805
|
+
return theirs !== void 0 && chord.equals(theirs);
|
|
806
|
+
});
|
|
555
807
|
}
|
|
556
808
|
/**
|
|
557
|
-
*
|
|
558
|
-
* chord root.
|
|
809
|
+
* A copy of this progression with a chord appended.
|
|
559
810
|
*
|
|
560
|
-
* @
|
|
811
|
+
* @param chord The chord to append.
|
|
812
|
+
* @returns The new progression.
|
|
561
813
|
*/
|
|
562
|
-
|
|
563
|
-
return
|
|
814
|
+
add(chord) {
|
|
815
|
+
return new _Progression([...this.#chords, chord], this.#key);
|
|
564
816
|
}
|
|
565
817
|
/**
|
|
566
|
-
*
|
|
567
|
-
* scale over this chord.
|
|
818
|
+
* A copy of this progression carrying the given key context.
|
|
568
819
|
*
|
|
569
|
-
* @param
|
|
570
|
-
* @returns
|
|
820
|
+
* @param key The key context to attach.
|
|
821
|
+
* @returns The new progression.
|
|
571
822
|
*/
|
|
572
|
-
|
|
573
|
-
return
|
|
823
|
+
withKey(key) {
|
|
824
|
+
return new _Progression(this.#chords, key);
|
|
574
825
|
}
|
|
575
826
|
/**
|
|
576
|
-
*
|
|
577
|
-
* over this chord.
|
|
827
|
+
* Voice the progression with smooth voice leading.
|
|
578
828
|
*
|
|
579
|
-
* @param
|
|
580
|
-
* @returns
|
|
829
|
+
* @param opts Voicing options; defaults to four SATB voices.
|
|
830
|
+
* @returns One ascending voicing (MIDI pitches) per chord.
|
|
831
|
+
* @throws If any chord admits no voicing within the given ranges.
|
|
581
832
|
*/
|
|
582
|
-
|
|
583
|
-
return
|
|
833
|
+
voice(opts) {
|
|
834
|
+
return voiceProgression(
|
|
835
|
+
this.#chords.map((chord) => chord.data),
|
|
836
|
+
opts
|
|
837
|
+
);
|
|
584
838
|
}
|
|
585
839
|
/**
|
|
586
|
-
*
|
|
840
|
+
* The Roman numeral of each chord in a key.
|
|
587
841
|
*
|
|
588
|
-
* @param key Key
|
|
589
|
-
*
|
|
590
|
-
* @returns Spelled octave-less notes in the chord's own (tertian) order.
|
|
842
|
+
* @param key Key to analyze in; falls back to the carried context.
|
|
843
|
+
* @returns One numeral per chord.
|
|
591
844
|
* @throws If no key is given and none is carried.
|
|
592
845
|
*/
|
|
593
|
-
|
|
846
|
+
roman(key) {
|
|
594
847
|
const resolved = this.#resolveKey(key);
|
|
595
|
-
return
|
|
596
|
-
(note) => new Note(note)
|
|
597
|
-
);
|
|
848
|
+
return this.#chords.map((chord) => chord.roman(resolved));
|
|
598
849
|
}
|
|
599
850
|
/**
|
|
600
|
-
*
|
|
601
|
-
* chord's key context (if any).
|
|
851
|
+
* The harmonic function of each chord in a key.
|
|
602
852
|
*
|
|
603
|
-
* @param
|
|
604
|
-
* @returns
|
|
853
|
+
* @param key Key to analyze in; falls back to the carried context.
|
|
854
|
+
* @returns One function per chord.
|
|
855
|
+
* @throws If no key is given and none is carried.
|
|
605
856
|
*/
|
|
606
|
-
|
|
607
|
-
|
|
857
|
+
functions(key) {
|
|
858
|
+
const resolved = this.#resolveKey(key);
|
|
859
|
+
return this.#chords.map((chord) => chord.function(resolved));
|
|
608
860
|
}
|
|
609
861
|
/**
|
|
610
|
-
*
|
|
611
|
-
* Key context is not compared.
|
|
862
|
+
* Analyze every chord and classify the closing cadence.
|
|
612
863
|
*
|
|
613
|
-
*
|
|
614
|
-
*
|
|
864
|
+
* The cadence is detected on the final chord pair and is null when the
|
|
865
|
+
* progression has fewer than two chords.
|
|
866
|
+
*
|
|
867
|
+
* @param key Key to analyze in; falls back to the carried context.
|
|
868
|
+
* @returns Per-chord analyses and the closing cadence.
|
|
869
|
+
* @throws If no key is given and none is carried.
|
|
615
870
|
*/
|
|
616
|
-
|
|
617
|
-
const
|
|
618
|
-
const
|
|
619
|
-
|
|
871
|
+
analyze(key) {
|
|
872
|
+
const resolved = this.#resolveKey(key);
|
|
873
|
+
const chords = this.#chords.map((chord) => chord.analyze(resolved));
|
|
874
|
+
const from = this.#chords[this.#chords.length - 2];
|
|
875
|
+
const to = this.#chords[this.#chords.length - 1];
|
|
876
|
+
const cadence = from !== void 0 && to !== void 0 ? detectCadence(from.data, to.data, resolved.scale) : null;
|
|
877
|
+
return { chords, cadence };
|
|
620
878
|
}
|
|
621
879
|
/**
|
|
622
|
-
* The plain
|
|
880
|
+
* The plain progression data, for JSON serialization.
|
|
623
881
|
*
|
|
624
|
-
*
|
|
882
|
+
* Private class fields do not serialize, so an explicit `toJSON` keeps
|
|
883
|
+
* `JSON.stringify(progression)` from collapsing to `{}`. The chords are
|
|
884
|
+
* emitted as plain data and the carried key, when present, as its own data.
|
|
885
|
+
*
|
|
886
|
+
* @returns The chord data sequence and the carried key, if any.
|
|
887
|
+
*/
|
|
888
|
+
/**
|
|
889
|
+
* Transpose every chord by a number of semitones.
|
|
890
|
+
*
|
|
891
|
+
* A carried key moves with the chords, so the progression keeps its degrees
|
|
892
|
+
* and functions in the new key.
|
|
893
|
+
*
|
|
894
|
+
* @param semitones The signed semitone offset.
|
|
895
|
+
* @returns The transposed progression.
|
|
896
|
+
* @example
|
|
897
|
+
* ```ts
|
|
898
|
+
* import { Chord, Progression } from '@libraz/libcantus';
|
|
899
|
+
* new Progression([Chord.parse('C'), Chord.parse('G')]).transpose(2).toString(); // 'D A'
|
|
900
|
+
* ```
|
|
901
|
+
*/
|
|
902
|
+
transpose(semitones) {
|
|
903
|
+
const key = this.#key?.transpose(semitones);
|
|
904
|
+
const chords = this.#chords.map((chord) => {
|
|
905
|
+
const moved = chord.transpose(semitones);
|
|
906
|
+
return key !== void 0 && moved.key === void 0 ? moved.withKey(key) : moved;
|
|
907
|
+
});
|
|
908
|
+
return key === void 0 ? new _Progression(chords) : new _Progression(chords, key);
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* The chord symbols separated by spaces, so a template literal or a log line
|
|
912
|
+
* reads as the progression.
|
|
913
|
+
*
|
|
914
|
+
* @returns The symbols in order, e.g. `'C Am F G'`.
|
|
625
915
|
*/
|
|
916
|
+
toString() {
|
|
917
|
+
return this.#chords.map((chord) => chord.symbol()).join(" ");
|
|
918
|
+
}
|
|
626
919
|
toJSON() {
|
|
627
|
-
return
|
|
920
|
+
return {
|
|
921
|
+
chords: this.#chords.map((chord) => chord.data),
|
|
922
|
+
key: this.#key?.toJSON()
|
|
923
|
+
};
|
|
628
924
|
}
|
|
629
925
|
/** Resolve the key for an analysis method: explicit first, then carried. */
|
|
630
926
|
#resolveKey(key) {
|
|
631
927
|
const resolved = key ?? this.#key;
|
|
632
928
|
if (resolved === void 0) {
|
|
633
|
-
throw new
|
|
929
|
+
throw new InvalidInputError(
|
|
930
|
+
"progression has no key context; pass a Key or attach one with withKey()"
|
|
931
|
+
);
|
|
634
932
|
}
|
|
635
933
|
return resolved;
|
|
636
934
|
}
|
|
637
935
|
};
|
|
638
936
|
|
|
639
|
-
// src/model/
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
937
|
+
// src/model/chord.ts
|
|
938
|
+
function hintMatches(hint, pc) {
|
|
939
|
+
return hint !== void 0 && pc !== void 0 && pitchClassOf(noteToPitchClass(hint)) === pitchClassOf(pc);
|
|
940
|
+
}
|
|
941
|
+
function copyChord(data) {
|
|
942
|
+
const copy = {
|
|
943
|
+
rootPc: data.rootPc,
|
|
944
|
+
quality: data.quality,
|
|
945
|
+
intervals: [...data.intervals]
|
|
946
|
+
};
|
|
947
|
+
if (data.bassPc !== void 0) {
|
|
948
|
+
copy.bassPc = data.bassPc;
|
|
949
|
+
}
|
|
950
|
+
if (hintMatches(data.rootSpelling, data.rootPc) && data.rootSpelling !== void 0) {
|
|
951
|
+
copy.rootSpelling = { letter: data.rootSpelling.letter, alter: data.rootSpelling.alter };
|
|
952
|
+
}
|
|
953
|
+
if (hintMatches(data.bassSpelling, data.bassPc) && data.bassSpelling !== void 0) {
|
|
954
|
+
copy.bassSpelling = { letter: data.bassSpelling.letter, alter: data.bassSpelling.alter };
|
|
955
|
+
}
|
|
956
|
+
return copy;
|
|
957
|
+
}
|
|
958
|
+
function deriveBassSpelling(chord, key) {
|
|
959
|
+
const bassPc = chord.bassPc;
|
|
960
|
+
if (bassPc === void 0) {
|
|
961
|
+
return void 0;
|
|
962
|
+
}
|
|
963
|
+
const root = chord.rootSpelling;
|
|
964
|
+
if (root !== void 0) {
|
|
965
|
+
const index = chord.intervals.findIndex((i) => pitchClassOf(chord.rootPc + i) === bassPc);
|
|
966
|
+
const tone = index >= 0 ? spellChordFromRoot(chord, root)[index] : void 0;
|
|
967
|
+
if (tone !== void 0) {
|
|
968
|
+
return { letter: tone.letter, alter: tone.alter };
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
if (key === void 0) {
|
|
972
|
+
return void 0;
|
|
648
973
|
}
|
|
974
|
+
const spelled = spellPitchClass(bassPc, key.tonic.data, key.scale);
|
|
975
|
+
return { letter: spelled.letter, alter: spelled.alter };
|
|
976
|
+
}
|
|
977
|
+
var Chord = class _Chord {
|
|
978
|
+
/** Exactly what the caller supplied: any spelling here is the caller's own. */
|
|
979
|
+
#given;
|
|
980
|
+
#key;
|
|
649
981
|
/**
|
|
650
|
-
*
|
|
982
|
+
* Wrap a plain chord object.
|
|
651
983
|
*
|
|
652
|
-
* @param
|
|
653
|
-
* @param
|
|
654
|
-
* @returns The interval from `a` to `b`.
|
|
984
|
+
* @param data The chord; it is copied, never retained or mutated.
|
|
985
|
+
* @param key Optional key context for analysis methods.
|
|
655
986
|
*/
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
987
|
+
constructor(data, key) {
|
|
988
|
+
this.#given = copyChord(data);
|
|
989
|
+
this.#key = key;
|
|
659
990
|
}
|
|
660
991
|
/**
|
|
661
|
-
*
|
|
992
|
+
* The chord data with any missing spelling hint filled in from the key.
|
|
662
993
|
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
665
|
-
*
|
|
666
|
-
* @returns The interval.
|
|
994
|
+
* Derived spellings are computed on read rather than baked in at construction,
|
|
995
|
+
* so re-attaching a different key re-spells the chord instead of carrying the
|
|
996
|
+
* first key's letters forever. A spelling the caller supplied always wins.
|
|
667
997
|
*/
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
return this.#semitones;
|
|
682
|
-
}
|
|
683
|
-
/** A readable label composed of quality and number, e.g. `'P5'` or `'M3'`. */
|
|
684
|
-
get name() {
|
|
685
|
-
return `${this.#quality}${this.#number}`;
|
|
998
|
+
get #data() {
|
|
999
|
+
const out = copyChord(this.#given);
|
|
1000
|
+
const key = this.#key;
|
|
1001
|
+
if (out.rootSpelling === void 0 && key !== void 0) {
|
|
1002
|
+
out.rootSpelling = spellPitchClass(out.rootPc, key.tonic.data, key.scale);
|
|
1003
|
+
}
|
|
1004
|
+
if (out.bassSpelling === void 0) {
|
|
1005
|
+
const bass = deriveBassSpelling(out, key);
|
|
1006
|
+
if (bass !== void 0) {
|
|
1007
|
+
out.bassSpelling = bass;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
return out;
|
|
686
1011
|
}
|
|
687
1012
|
/**
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
* Private class fields do not serialize, so an explicit `toJSON` keeps
|
|
691
|
-
* `JSON.stringify(interval)` from collapsing to `{}`.
|
|
1013
|
+
* Build a chord from a root and quality.
|
|
692
1014
|
*
|
|
693
|
-
* @
|
|
1015
|
+
* @param root Root as a note name (e.g. `'Eb'`) or a pitch class.
|
|
1016
|
+
* @param quality The chord quality.
|
|
1017
|
+
* @param bass Optional slash-chord bass, as a note name (e.g. `'Bb'`) or a
|
|
1018
|
+
* pitch class. A named bass keeps its own spelling.
|
|
1019
|
+
* @returns The chord (without key context).
|
|
694
1020
|
*/
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
return new Note(sharp);
|
|
1021
|
+
static of(root, quality, bass) {
|
|
1022
|
+
const bassNote = typeof bass === "string" ? Note.of(bass) : void 0;
|
|
1023
|
+
const bassPc = bassNote !== void 0 ? bassNote.pitchClass : bass;
|
|
1024
|
+
const rootNote = typeof root === "string" ? Note.of(root) : void 0;
|
|
1025
|
+
const data = makeChord(
|
|
1026
|
+
rootNote !== void 0 ? rootNote.pitchClass : root,
|
|
1027
|
+
quality,
|
|
1028
|
+
bassPc
|
|
1029
|
+
);
|
|
1030
|
+
if (rootNote !== void 0) {
|
|
1031
|
+
data.rootSpelling = { letter: rootNote.letter, alter: rootNote.alter };
|
|
1032
|
+
}
|
|
1033
|
+
if (bassNote !== void 0) {
|
|
1034
|
+
data.bassSpelling = { letter: bassNote.letter, alter: bassNote.alter };
|
|
1035
|
+
}
|
|
1036
|
+
return new _Chord(data);
|
|
712
1037
|
}
|
|
713
|
-
return accidentalLoad(flat, scale) <= accidentalLoad(sharp, scale) ? new Note(flat) : new Note(sharp);
|
|
714
|
-
}
|
|
715
|
-
var Key = class _Key {
|
|
716
|
-
#scale;
|
|
717
|
-
#tonic;
|
|
718
1038
|
/**
|
|
719
|
-
* Wrap
|
|
1039
|
+
* Wrap an existing plain chord object.
|
|
720
1040
|
*
|
|
721
|
-
* @param
|
|
722
|
-
* @
|
|
1041
|
+
* @param data The plain chord.
|
|
1042
|
+
* @returns The wrapped chord (without key context).
|
|
723
1043
|
*/
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
this.#tonic = tonic;
|
|
1044
|
+
static from(data) {
|
|
1045
|
+
return new _Chord(data);
|
|
727
1046
|
}
|
|
728
1047
|
/**
|
|
729
|
-
*
|
|
1048
|
+
* Alias of {@link Chord.from}, matching the `fromData` factory on the other
|
|
1049
|
+
* classes.
|
|
730
1050
|
*
|
|
731
|
-
* @param
|
|
732
|
-
*
|
|
733
|
-
* accidentals across the scale.
|
|
734
|
-
* @returns The major key.
|
|
1051
|
+
* @param data The plain chord.
|
|
1052
|
+
* @returns The wrapped chord (without key context).
|
|
735
1053
|
*/
|
|
736
|
-
static
|
|
737
|
-
|
|
738
|
-
const tonic = Note.of(root);
|
|
739
|
-
return new _Key(majorKey(tonic.pitchClass), tonic);
|
|
740
|
-
}
|
|
741
|
-
const scale = majorKey(root);
|
|
742
|
-
return new _Key(scale, bestTonicForScale(root, scale));
|
|
1054
|
+
static fromData(data) {
|
|
1055
|
+
return new _Chord(data);
|
|
743
1056
|
}
|
|
744
1057
|
/**
|
|
745
|
-
*
|
|
1058
|
+
* Rebuild a chord from its {@link Chord.toJSON} output.
|
|
746
1059
|
*
|
|
747
|
-
*
|
|
748
|
-
*
|
|
749
|
-
*
|
|
750
|
-
* @
|
|
1060
|
+
* The key context is not serialized, so the result carries none; re-attach
|
|
1061
|
+
* one with {@link Chord.withKey}.
|
|
1062
|
+
*
|
|
1063
|
+
* @param data The serialized chord.
|
|
1064
|
+
* @returns The wrapped chord.
|
|
751
1065
|
*/
|
|
752
|
-
static
|
|
753
|
-
|
|
754
|
-
const tonic = Note.of(root);
|
|
755
|
-
return new _Key(minorKey(tonic.pitchClass), tonic);
|
|
756
|
-
}
|
|
757
|
-
const scale = minorKey(root);
|
|
758
|
-
return new _Key(scale, bestTonicForScale(root, scale));
|
|
1066
|
+
static fromJSON(data) {
|
|
1067
|
+
return new _Chord(data);
|
|
759
1068
|
}
|
|
760
1069
|
/**
|
|
761
|
-
*
|
|
1070
|
+
* Parse a lead-sheet chord symbol (e.g. `'Cmaj7'`, `'F#m7b5'`, `'C/G'`).
|
|
762
1071
|
*
|
|
763
|
-
* @param
|
|
764
|
-
* @
|
|
765
|
-
* @
|
|
766
|
-
* @throws If the name is not a known scale.
|
|
1072
|
+
* @param symbol The chord symbol.
|
|
1073
|
+
* @returns The chord (without key context).
|
|
1074
|
+
* @throws If the root or quality is not recognized.
|
|
767
1075
|
*/
|
|
768
|
-
static
|
|
769
|
-
|
|
770
|
-
return new _Key(scaleByName(name, tonic.pitchClass), tonic);
|
|
1076
|
+
static parse(symbol) {
|
|
1077
|
+
return new _Chord(parseChordSymbol(symbol));
|
|
771
1078
|
}
|
|
772
1079
|
/**
|
|
773
|
-
*
|
|
774
|
-
* given (flat-preferred for minor-third scales, sharp-preferred otherwise).
|
|
1080
|
+
* Identify the chords matching a set of pitches, best interpretation first.
|
|
775
1081
|
*
|
|
776
|
-
* @param
|
|
777
|
-
* @param
|
|
778
|
-
* @returns
|
|
1082
|
+
* @param pitches MIDI pitches or bare pitch classes.
|
|
1083
|
+
* @param opts How to interpret the input; see {@link DetectChordOptions}.
|
|
1084
|
+
* @returns Ranked chord interpretations (may be empty).
|
|
779
1085
|
*/
|
|
780
|
-
static
|
|
781
|
-
|
|
782
|
-
|
|
1086
|
+
static detect(pitches, opts) {
|
|
1087
|
+
return detectChord(pitches, opts).map(
|
|
1088
|
+
(match) => new _Chord(makeChord(match.rootPc, match.quality, match.bassPc))
|
|
1089
|
+
);
|
|
783
1090
|
}
|
|
784
|
-
/**
|
|
785
|
-
|
|
786
|
-
|
|
1091
|
+
/**
|
|
1092
|
+
* Identify the chords matching a set of pitches, keeping each match's
|
|
1093
|
+
* recognition metadata beside the chord.
|
|
1094
|
+
*
|
|
1095
|
+
* {@link Chord.detect} discards the confidence signals a recognition UI needs
|
|
1096
|
+
* — whether the set matched exactly, which chord tones were missing, which
|
|
1097
|
+
* input notes were foreign, and which inversion the bass implies.
|
|
1098
|
+
*
|
|
1099
|
+
* @param pitches MIDI pitches or bare pitch classes.
|
|
1100
|
+
* @param opts How to interpret the input; see {@link DetectChordOptions}.
|
|
1101
|
+
* @returns Ranked interpretations, each with its chord and its match record.
|
|
1102
|
+
* @example
|
|
1103
|
+
* ```ts
|
|
1104
|
+
* import { Chord } from '@libraz/libcantus';
|
|
1105
|
+
* const [best] = Chord.detectMatches([60, 64, 67]);
|
|
1106
|
+
* best?.match.exact; // true
|
|
1107
|
+
* ```
|
|
1108
|
+
*/
|
|
1109
|
+
static detectMatches(pitches, opts) {
|
|
1110
|
+
return detectChord(pitches, opts).map((match) => ({
|
|
1111
|
+
chord: new _Chord(makeChord(match.rootPc, match.quality, match.bassPc)),
|
|
1112
|
+
match
|
|
1113
|
+
}));
|
|
787
1114
|
}
|
|
788
|
-
/**
|
|
789
|
-
|
|
790
|
-
|
|
1115
|
+
/**
|
|
1116
|
+
* The single best chord interpretation of a pitch set.
|
|
1117
|
+
*
|
|
1118
|
+
* @param pitches MIDI pitches or bare pitch classes.
|
|
1119
|
+
* @param opts How to interpret the input; see {@link DetectChordOptions}.
|
|
1120
|
+
* @returns The top-ranked chord, or null when nothing matches.
|
|
1121
|
+
*/
|
|
1122
|
+
static detectBest(pitches, opts) {
|
|
1123
|
+
const best = detectChordBest(pitches, opts);
|
|
1124
|
+
return best === null ? null : new _Chord(best);
|
|
791
1125
|
}
|
|
792
|
-
/** The
|
|
1126
|
+
/** The root pitch class (0..11). */
|
|
793
1127
|
get rootPc() {
|
|
794
|
-
return this.#
|
|
1128
|
+
return this.#data.rootPc;
|
|
795
1129
|
}
|
|
796
|
-
/**
|
|
797
|
-
get
|
|
798
|
-
return
|
|
1130
|
+
/** The chord quality. */
|
|
1131
|
+
get quality() {
|
|
1132
|
+
return this.#data.quality;
|
|
1133
|
+
}
|
|
1134
|
+
/** A copy of the semitone offsets above the root. */
|
|
1135
|
+
get intervals() {
|
|
1136
|
+
return [...this.#data.intervals];
|
|
1137
|
+
}
|
|
1138
|
+
/** The slash-chord bass pitch class, or undefined in root position. */
|
|
1139
|
+
get bassPc() {
|
|
1140
|
+
return this.#data.bassPc;
|
|
799
1141
|
}
|
|
800
1142
|
/**
|
|
801
|
-
*
|
|
1143
|
+
* A copy of the underlying plain chord object, including the spellings
|
|
1144
|
+
* derived from any attached key.
|
|
1145
|
+
*/
|
|
1146
|
+
get data() {
|
|
1147
|
+
return this.#data;
|
|
1148
|
+
}
|
|
1149
|
+
/** The carried key context, if any. */
|
|
1150
|
+
get key() {
|
|
1151
|
+
return this.#key;
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* A copy of this chord carrying the given key context.
|
|
802
1155
|
*
|
|
803
|
-
*
|
|
1156
|
+
* Only a spelling the caller supplied (via `Chord.parse`, `Chord.of` with a
|
|
1157
|
+
* named root, or plain data carrying a hint) survives; a spelling that came
|
|
1158
|
+
* from a previously attached key is re-derived, so re-keying a progression
|
|
1159
|
+
* during a modulation does not keep the old key's letters and the order of
|
|
1160
|
+
* `withKey` calls does not affect the result.
|
|
1161
|
+
*
|
|
1162
|
+
* @param key The key context to attach.
|
|
1163
|
+
* @returns The new chord.
|
|
1164
|
+
*/
|
|
1165
|
+
withKey(key) {
|
|
1166
|
+
return new _Chord(this.#given, key);
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* The chord's sorted, deduplicated pitch classes.
|
|
1170
|
+
*
|
|
1171
|
+
* @returns Pitch classes ascending in [0, 11].
|
|
804
1172
|
*/
|
|
805
1173
|
pitchClasses() {
|
|
806
|
-
return
|
|
1174
|
+
return chordPitchClasses(this.#data);
|
|
807
1175
|
}
|
|
808
1176
|
/**
|
|
809
|
-
* The
|
|
1177
|
+
* The chord's Roman numeral in a key.
|
|
810
1178
|
*
|
|
811
|
-
* @
|
|
1179
|
+
* @param key Key to analyze in; falls back to the carried context.
|
|
1180
|
+
* @returns The Roman numeral string.
|
|
1181
|
+
* @throws If no key is given and none is carried.
|
|
812
1182
|
*/
|
|
813
|
-
|
|
814
|
-
return
|
|
1183
|
+
roman(key) {
|
|
1184
|
+
return chordToRoman(this.#data, this.#resolveKey(key).scale);
|
|
815
1185
|
}
|
|
816
1186
|
/**
|
|
817
|
-
*
|
|
1187
|
+
* The chord's harmonic function (tonic / subdominant / dominant) in a key.
|
|
818
1188
|
*
|
|
819
|
-
* @
|
|
1189
|
+
* @param key Key to analyze in; falls back to the carried context.
|
|
1190
|
+
* @returns The harmonic function.
|
|
1191
|
+
* @throws If no key is given and none is carried.
|
|
820
1192
|
*/
|
|
821
|
-
|
|
822
|
-
return this
|
|
1193
|
+
function(key) {
|
|
1194
|
+
return functionOf(this.#data, this.#resolveKey(key).scale);
|
|
823
1195
|
}
|
|
824
1196
|
/**
|
|
825
|
-
*
|
|
1197
|
+
* Full functional analysis: function, borrowing, and Roman numeral.
|
|
826
1198
|
*
|
|
827
|
-
* @
|
|
1199
|
+
* @param key Key to analyze in; falls back to the carried context.
|
|
1200
|
+
* @returns The chord analysis.
|
|
1201
|
+
* @throws If no key is given and none is carried.
|
|
828
1202
|
*/
|
|
829
|
-
|
|
830
|
-
return this
|
|
1203
|
+
analyze(key) {
|
|
1204
|
+
return analyzeChord(this.#data, this.#resolveKey(key).scale);
|
|
831
1205
|
}
|
|
832
1206
|
/**
|
|
833
|
-
*
|
|
1207
|
+
* Whether the chord is borrowed from the parallel mode (modal interchange).
|
|
834
1208
|
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
*
|
|
1209
|
+
* @param key Key to analyze in; falls back to the carried context.
|
|
1210
|
+
* @returns True if the chord is borrowed.
|
|
1211
|
+
* @throws If no key is given and none is carried.
|
|
1212
|
+
*/
|
|
1213
|
+
isBorrowed(key) {
|
|
1214
|
+
return isBorrowedChord(this.#data, this.#resolveKey(key).scale);
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* The origin of a non-diatonic chord (parallel mode or Neapolitan), or null.
|
|
838
1218
|
*
|
|
839
|
-
* @param
|
|
840
|
-
* @
|
|
841
|
-
* @
|
|
1219
|
+
* @param key Key to analyze in; falls back to the carried context.
|
|
1220
|
+
* @returns The borrowing source, or null.
|
|
1221
|
+
* @throws If no key is given and none is carried.
|
|
842
1222
|
*/
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
return new Chord(data, this);
|
|
1223
|
+
borrowedSource(key) {
|
|
1224
|
+
return borrowedSource(this.#data, this.#resolveKey(key).scale);
|
|
846
1225
|
}
|
|
847
1226
|
/**
|
|
848
|
-
* The
|
|
1227
|
+
* The chord rendered as a lead-sheet symbol (e.g. `'Cmaj7'`, `'F#m7'`, `'C/G'`).
|
|
849
1228
|
*
|
|
850
|
-
* @param
|
|
851
|
-
* @returns The
|
|
1229
|
+
* @param opts Set `flats: true` to spell the root/bass with flats.
|
|
1230
|
+
* @returns The chord symbol.
|
|
852
1231
|
*/
|
|
853
|
-
|
|
854
|
-
return
|
|
1232
|
+
symbol(opts) {
|
|
1233
|
+
return formatChordSymbol(this.#data, opts);
|
|
855
1234
|
}
|
|
856
1235
|
/**
|
|
857
|
-
*
|
|
1236
|
+
* Realize the chord as one MIDI pitch per voice, ascending.
|
|
858
1237
|
*
|
|
859
|
-
*
|
|
860
|
-
*
|
|
1238
|
+
* A chord that carries a key passes it to the voicer, so the leading tone is
|
|
1239
|
+
* not doubled; an explicit `opts.key` overrides it.
|
|
1240
|
+
*
|
|
1241
|
+
* @param opts Voicing options; defaults to four SATB voices.
|
|
1242
|
+
* @returns MIDI pitches, ascending, one per voice.
|
|
1243
|
+
* @throws If no voicing fits the given ranges.
|
|
861
1244
|
*/
|
|
862
|
-
|
|
863
|
-
|
|
1245
|
+
voice(opts) {
|
|
1246
|
+
const key = opts?.key ?? this.#key?.scale;
|
|
1247
|
+
return voiceChord(this.#data, key === void 0 ? opts : { ...opts, key });
|
|
864
1248
|
}
|
|
865
1249
|
/**
|
|
866
|
-
*
|
|
867
|
-
*
|
|
1250
|
+
* Realize the chord as a single styled voicing (`close`, `drop2`, `drop3`,
|
|
1251
|
+
* `shell`, or `rootless`), optionally constraining the top voice.
|
|
868
1252
|
*
|
|
869
|
-
* @param
|
|
870
|
-
* @returns
|
|
871
|
-
* @throws If the numeral is not valid.
|
|
1253
|
+
* @param opts Styled-voicing options; defaults to a close-position voicing.
|
|
1254
|
+
* @returns MIDI pitches, ascending.
|
|
872
1255
|
*/
|
|
873
|
-
|
|
874
|
-
return
|
|
1256
|
+
styledVoicing(opts) {
|
|
1257
|
+
return voiceChordStyled(this.#data, opts);
|
|
875
1258
|
}
|
|
876
1259
|
/**
|
|
877
|
-
*
|
|
1260
|
+
* The negative-harmony mirror of the chord about the key's tonic–dominant
|
|
1261
|
+
* axis (major becomes minor and vice versa).
|
|
878
1262
|
*
|
|
879
|
-
* @param
|
|
880
|
-
*
|
|
1263
|
+
* @param key Key providing the reflection axis; falls back to the carried
|
|
1264
|
+
* context.
|
|
1265
|
+
* @returns The mirrored chord, keeping any key context.
|
|
1266
|
+
* @throws If no key is given and none is carried.
|
|
881
1267
|
*/
|
|
882
|
-
|
|
883
|
-
|
|
1268
|
+
negativeHarmony(key) {
|
|
1269
|
+
const resolved = this.#resolveKey(key);
|
|
1270
|
+
return new _Chord(negativeHarmonyMirror(this.#data, resolved.scale), key ?? this.#key);
|
|
884
1271
|
}
|
|
885
1272
|
/**
|
|
886
|
-
* The
|
|
1273
|
+
* The n-th inversion: a copy whose bass is the chord tone `n` steps above the
|
|
1274
|
+
* root in the interval template (`invert(1)` puts the third in the bass).
|
|
1275
|
+
* `n` wraps around the template length; negative values count backwards.
|
|
887
1276
|
*
|
|
888
|
-
*
|
|
889
|
-
*
|
|
890
|
-
* `KeyScale` with the spelled tonic, enough to reconstruct the key via
|
|
891
|
-
* {@link Key.of}.
|
|
1277
|
+
* `invert(0)` (and any `n` that wraps to it) is root position, so it carries
|
|
1278
|
+
* no slash bass and equals the original chord.
|
|
892
1279
|
*
|
|
893
|
-
* @
|
|
1280
|
+
* @param n The inversion number.
|
|
1281
|
+
* @returns The inverted chord, keeping any key context.
|
|
1282
|
+
* @throws If the chord has no intervals.
|
|
1283
|
+
*/
|
|
1284
|
+
invert(n) {
|
|
1285
|
+
const data = copyChord(this.#given);
|
|
1286
|
+
const intervals = data.intervals;
|
|
1287
|
+
const length = intervals.length;
|
|
1288
|
+
if (length === 0) {
|
|
1289
|
+
throw new InvalidInputError("cannot invert a chord with no intervals");
|
|
1290
|
+
}
|
|
1291
|
+
const index = (n % length + length) % length;
|
|
1292
|
+
delete data.bassSpelling;
|
|
1293
|
+
if (index === 0) {
|
|
1294
|
+
delete data.bassPc;
|
|
1295
|
+
} else {
|
|
1296
|
+
data.bassPc = pitchClassOf(data.rootPc + (intervals[index] ?? 0));
|
|
1297
|
+
}
|
|
1298
|
+
return new _Chord(data, this.#key);
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Transpose the chord by a number of semitones.
|
|
1302
|
+
*
|
|
1303
|
+
* The quality and interval template are carried over, so a chord that a
|
|
1304
|
+
* symbol round-trip could not express — a custom interval set, an inversion —
|
|
1305
|
+
* survives. A carried key moves with the chord, so the transposed chord keeps
|
|
1306
|
+
* the same degree and function inside the transposed key.
|
|
1307
|
+
*
|
|
1308
|
+
* @param semitones The signed semitone offset.
|
|
1309
|
+
* @returns The transposed chord, in the transposed key when one is carried.
|
|
1310
|
+
* @example
|
|
1311
|
+
* ```ts
|
|
1312
|
+
* import { Chord } from '@libraz/libcantus';
|
|
1313
|
+
* Chord.parse('C/G').transpose(2).symbol(); // 'D/A'
|
|
1314
|
+
* ```
|
|
1315
|
+
*/
|
|
1316
|
+
transpose(semitones) {
|
|
1317
|
+
const moved = transposeChord(this.#given, semitones);
|
|
1318
|
+
return new _Chord(moved, this.#key?.transpose(semitones));
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* The named scales that fit over this chord, best fit first, rooted on the
|
|
1322
|
+
* chord root.
|
|
1323
|
+
*
|
|
1324
|
+
* @returns The matching scales.
|
|
1325
|
+
*/
|
|
1326
|
+
scales() {
|
|
1327
|
+
return chordScales(this.#data);
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* The available tensions (usable non-chord, non-avoid scale tones) of a
|
|
1331
|
+
* scale over this chord.
|
|
1332
|
+
*
|
|
1333
|
+
* @param scaleName A named scale, rooted on the chord root.
|
|
1334
|
+
* @returns Tension pitch classes, ascending in [0, 11].
|
|
1335
|
+
* @throws If `scaleName` is not a built-in scale.
|
|
1336
|
+
*/
|
|
1337
|
+
tensions(scaleName) {
|
|
1338
|
+
return availableTensions(this.#data, scaleName);
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* The avoid notes (scale tones a semitone above a chord tone) of a scale
|
|
1342
|
+
* over this chord.
|
|
1343
|
+
*
|
|
1344
|
+
* @param scaleName A named scale, rooted on the chord root.
|
|
1345
|
+
* @returns Avoid-note pitch classes, ascending in [0, 11].
|
|
1346
|
+
* @throws If `scaleName` is not a built-in scale.
|
|
1347
|
+
*/
|
|
1348
|
+
avoidNotes(scaleName) {
|
|
1349
|
+
return avoidNotes(this.#data, scaleName);
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Spell the chord tones with letter names, root first, in the key's spelling.
|
|
1353
|
+
*
|
|
1354
|
+
* A chord that already knows how its root is spelled — one from
|
|
1355
|
+
* `Chord.parse`, or `Chord.of` with a named root — can spell itself with no
|
|
1356
|
+
* key at all; a key is only needed to choose a spelling for a bare pitch
|
|
1357
|
+
* class.
|
|
1358
|
+
*
|
|
1359
|
+
* A slash bass outside the chord tones is appended, so the spelled notes
|
|
1360
|
+
* are the notes the chord sounds — the same set {@link Chord.pitchClasses}
|
|
1361
|
+
* reports.
|
|
1362
|
+
*
|
|
1363
|
+
* @param key Key providing the spelled tonic; falls back to the carried
|
|
1364
|
+
* context, then to the chord's own root spelling.
|
|
1365
|
+
* @returns Spelled octave-less notes in the chord's own (tertian) order,
|
|
1366
|
+
* with any foreign slash bass last.
|
|
1367
|
+
* @throws If no key is given, none is carried, and the chord has no root
|
|
1368
|
+
* spelling of its own.
|
|
1369
|
+
*/
|
|
1370
|
+
spell(key) {
|
|
1371
|
+
const data = this.#data;
|
|
1372
|
+
const resolved = key ?? this.#key;
|
|
1373
|
+
let tones;
|
|
1374
|
+
if (resolved === void 0) {
|
|
1375
|
+
const root = data.rootSpelling;
|
|
1376
|
+
if (root === void 0) {
|
|
1377
|
+
throw new InvalidInputError(
|
|
1378
|
+
"chord has no key context and no root spelling; pass a Key, attach one with withKey(), or build the chord from a symbol"
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
tones = spellChordFromRoot(data, root);
|
|
1382
|
+
} else {
|
|
1383
|
+
tones = spellChord(data, resolved.tonic.data, resolved.scale);
|
|
1384
|
+
}
|
|
1385
|
+
const bassPc = data.bassPc;
|
|
1386
|
+
const bass = data.bassSpelling;
|
|
1387
|
+
if (bassPc !== void 0 && bass !== void 0 && !tones.some((tone) => pitchClassOf(noteToPitchClass(tone)) === pitchClassOf(bassPc))) {
|
|
1388
|
+
tones = [...tones, { letter: bass.letter, alter: bass.alter }];
|
|
1389
|
+
}
|
|
1390
|
+
return tones.map((note) => new Note(note));
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Start a progression with this chord followed by others, carrying this
|
|
1394
|
+
* chord's key context (if any).
|
|
1395
|
+
*
|
|
1396
|
+
* @param others The chords following this one.
|
|
1397
|
+
* @returns The progression.
|
|
1398
|
+
*/
|
|
1399
|
+
progressionTo(...others) {
|
|
1400
|
+
return new Progression([this, ...others], this.#key);
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Whether another chord has the same root, quality, intervals, and bass.
|
|
1404
|
+
* Key context is not compared.
|
|
1405
|
+
*
|
|
1406
|
+
* @param other The chord to compare.
|
|
1407
|
+
* @returns True if the chord data is identical.
|
|
1408
|
+
*/
|
|
1409
|
+
equals(other) {
|
|
1410
|
+
const a = this.#data;
|
|
1411
|
+
const b = other.data;
|
|
1412
|
+
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]);
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* The plain chord data, for JSON serialization.
|
|
1416
|
+
*
|
|
1417
|
+
* @returns A copy of the underlying plain chord object.
|
|
894
1418
|
*/
|
|
895
1419
|
toJSON() {
|
|
896
|
-
return
|
|
1420
|
+
return this.data;
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* The chord symbol, so a template literal or a log line reads as the chord.
|
|
1424
|
+
*
|
|
1425
|
+
* @returns The symbol, e.g. `'Cmaj7'`.
|
|
1426
|
+
*/
|
|
1427
|
+
toString() {
|
|
1428
|
+
return this.symbol();
|
|
1429
|
+
}
|
|
1430
|
+
/** Resolve the key for an analysis method: explicit first, then carried. */
|
|
1431
|
+
#resolveKey(key) {
|
|
1432
|
+
const resolved = key ?? this.#key;
|
|
1433
|
+
if (resolved === void 0) {
|
|
1434
|
+
throw new InvalidInputError(
|
|
1435
|
+
"chord has no key context; pass a Key or attach one with withKey()"
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
return resolved;
|
|
897
1439
|
}
|
|
898
1440
|
};
|
|
899
1441
|
|
|
900
1442
|
export {
|
|
1443
|
+
Interval,
|
|
901
1444
|
Note,
|
|
1445
|
+
Key,
|
|
902
1446
|
Progression,
|
|
903
|
-
Chord
|
|
904
|
-
Interval,
|
|
905
|
-
Key
|
|
1447
|
+
Chord
|
|
906
1448
|
};
|
|
907
|
-
//# sourceMappingURL=chunk-
|
|
1449
|
+
//# sourceMappingURL=chunk-EHVEBWM5.js.map
|