@libraz/libcantus 0.9.4 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -18
- package/README_ja.md +268 -0
- package/dist/analyze/index.cjs +7 -7
- package/dist/analyze/index.d.cts +9 -99
- package/dist/analyze/index.d.ts +9 -99
- package/dist/analyze/index.js +6 -6
- package/dist/{chunk-36N73KZ3.cjs → chunk-2A2QMA74.cjs} +80 -65
- package/dist/chunk-2A2QMA74.cjs.map +1 -0
- package/dist/{chunk-YLEMGEFH.js → chunk-2PTFGVD7.js} +50 -35
- package/dist/chunk-2PTFGVD7.js.map +1 -0
- package/dist/{chunk-ZEJR4TKL.cjs → chunk-357ZDPVU.cjs} +36 -47
- package/dist/chunk-357ZDPVU.cjs.map +1 -0
- package/dist/{chunk-EHVEBWM5.js → chunk-3KX76RLG.js} +136 -37
- package/dist/chunk-3KX76RLG.js.map +1 -0
- package/dist/{chunk-RC6GBMCD.cjs → chunk-4VZ5MZAM.cjs} +155 -111
- package/dist/chunk-4VZ5MZAM.cjs.map +1 -0
- package/dist/{chunk-RK35RIN7.js → chunk-7EONYVI5.js} +115 -28
- package/dist/chunk-7EONYVI5.js.map +1 -0
- package/dist/{chunk-GQSGS2N4.js → chunk-7JEP3MFY.js} +5 -16
- package/dist/chunk-7JEP3MFY.js.map +1 -0
- package/dist/{chunk-7NKDPF74.js → chunk-AID75RN3.js} +203 -104
- package/dist/chunk-AID75RN3.js.map +1 -0
- package/dist/{chunk-FNDWXRRI.cjs → chunk-BSLTLGM3.cjs} +55 -37
- package/dist/chunk-BSLTLGM3.cjs.map +1 -0
- package/dist/{chunk-NP4NA2FB.cjs → chunk-CU6VG2YM.cjs} +39 -36
- package/dist/chunk-CU6VG2YM.cjs.map +1 -0
- package/dist/{chunk-NYYWMV2C.js → chunk-DZMHIGI4.js} +27 -17
- package/dist/chunk-DZMHIGI4.js.map +1 -0
- package/dist/{chunk-I24MV65U.js → chunk-EDOTEABK.js} +18 -15
- package/dist/{chunk-I24MV65U.js.map → chunk-EDOTEABK.js.map} +1 -1
- package/dist/{chunk-NKIBQ6PT.js → chunk-HN2OEUN2.js} +7 -4
- package/dist/chunk-HN2OEUN2.js.map +1 -0
- package/dist/{chunk-D2FAJBYL.js → chunk-KBTTEX7Q.js} +30 -12
- package/dist/chunk-KBTTEX7Q.js.map +1 -0
- package/dist/{chunk-CAN76NCU.cjs → chunk-KQN2ZYND.cjs} +147 -102
- package/dist/chunk-KQN2ZYND.cjs.map +1 -0
- package/dist/{chunk-4HLLZPXT.cjs → chunk-MPTVJIVG.cjs} +350 -251
- package/dist/chunk-MPTVJIVG.cjs.map +1 -0
- package/dist/{chunk-RUMU3UA6.cjs → chunk-MTMZTMWM.cjs} +12 -12
- package/dist/{chunk-RUMU3UA6.cjs.map → chunk-MTMZTMWM.cjs.map} +1 -1
- package/dist/{chunk-KMHOICIY.js → chunk-NNB67XA2.js} +90 -45
- package/dist/chunk-NNB67XA2.js.map +1 -0
- package/dist/{chunk-LW53AHQU.cjs → chunk-OL3IHHSJ.cjs} +228 -141
- package/dist/chunk-OL3IHHSJ.cjs.map +1 -0
- package/dist/{chunk-7MGZ7D62.cjs → chunk-PI3UXRWK.cjs} +43 -13
- package/dist/chunk-PI3UXRWK.cjs.map +1 -0
- package/dist/{chunk-XEBG3G53.js → chunk-R2VVFAIN.js} +42 -12
- package/dist/chunk-R2VVFAIN.js.map +1 -0
- package/dist/{chunk-Q6OP4U43.cjs → chunk-WFDXOLUB.cjs} +50 -40
- package/dist/chunk-WFDXOLUB.cjs.map +1 -0
- package/dist/{chunk-NVE34YGZ.cjs → chunk-WTAN4ACG.cjs} +212 -113
- package/dist/chunk-WTAN4ACG.cjs.map +1 -0
- package/dist/{chunk-FCANXBAT.cjs → chunk-YFJXS3AU.cjs} +8 -5
- package/dist/chunk-YFJXS3AU.cjs.map +1 -0
- package/dist/{chunk-Q2XUP227.js → chunk-YUE6TJ4M.js} +89 -45
- package/dist/chunk-YUE6TJ4M.js.map +1 -0
- package/dist/{chunk-HKGYCQ34.js → chunk-ZGLJM4A4.js} +2 -2
- package/dist/core/index.cjs +15 -5
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +14 -4
- package/dist/core/index.d.ts +14 -4
- package/dist/core/index.js +14 -4
- package/dist/{function-DNugAOrt.d.cts → function-CxixTB02.d.cts} +98 -7
- package/dist/{function-BtiZb9i9.d.ts → function-DTCt675z.d.ts} +98 -7
- package/dist/generate/index.cjs +10 -10
- package/dist/generate/index.d.cts +21 -21
- package/dist/generate/index.d.ts +21 -21
- package/dist/generate/index.js +9 -9
- package/dist/{index-tJhkmQgE.d.cts → index-B2Ulxxpo.d.cts} +3 -2
- package/dist/{index-Be74mcon.d.cts → index-BkYJ0xMq.d.cts} +2 -2
- package/dist/{index-6VR8-kit.d.ts → index-BuaB-ypt.d.ts} +3 -2
- package/dist/{index-70p168VM.d.ts → index-CAN7v-kw.d.ts} +9 -2
- package/dist/{index-C83mZI1P.d.ts → index-COw1Ky9g.d.ts} +1 -1
- package/dist/{index-tSQh-xwY.d.ts → index-Cfx8xgH0.d.ts} +1 -1
- package/dist/{index-C5UBbQ4G.d.ts → index-DRtIhlJj.d.ts} +2 -2
- package/dist/{index-BtXT6UQ0.d.cts → index-alrRninU.d.cts} +9 -2
- package/dist/{index-y_ak9_Hi.d.cts → index-oTQbYYIs.d.cts} +1 -1
- package/dist/{index-OG65ocOr.d.ts → index-tR9jTn26.d.cts} +4 -1
- package/dist/{index-OG65ocOr.d.cts → index-tR9jTn26.d.ts} +4 -1
- package/dist/{index-D6b6na69.d.cts → index-x2vcsduE.d.cts} +1 -1
- package/dist/index.cjs +24 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -11
- package/dist/index.d.ts +11 -11
- package/dist/index.js +23 -13
- package/dist/model/index.cjs +7 -7
- package/dist/model/index.d.cts +48 -16
- package/dist/model/index.d.ts +48 -16
- package/dist/model/index.js +6 -6
- package/dist/{styled-DIRkk9Uz.d.ts → styled-Bl0ZK28h.d.ts} +40 -3
- package/dist/{styled-DJbkJQfj.d.cts → styled-Crv6SA4S.d.cts} +40 -3
- package/dist/theory/index.cjs +6 -6
- package/dist/theory/index.d.cts +9 -9
- package/dist/theory/index.d.ts +9 -9
- package/dist/theory/index.js +5 -5
- package/package.json +12 -9
- package/dist/chunk-36N73KZ3.cjs.map +0 -1
- package/dist/chunk-4HLLZPXT.cjs.map +0 -1
- package/dist/chunk-7MGZ7D62.cjs.map +0 -1
- package/dist/chunk-7NKDPF74.js.map +0 -1
- package/dist/chunk-CAN76NCU.cjs.map +0 -1
- package/dist/chunk-D2FAJBYL.js.map +0 -1
- package/dist/chunk-EHVEBWM5.js.map +0 -1
- package/dist/chunk-FCANXBAT.cjs.map +0 -1
- package/dist/chunk-FNDWXRRI.cjs.map +0 -1
- package/dist/chunk-GQSGS2N4.js.map +0 -1
- package/dist/chunk-KMHOICIY.js.map +0 -1
- package/dist/chunk-LW53AHQU.cjs.map +0 -1
- package/dist/chunk-NKIBQ6PT.js.map +0 -1
- package/dist/chunk-NP4NA2FB.cjs.map +0 -1
- package/dist/chunk-NVE34YGZ.cjs.map +0 -1
- package/dist/chunk-NYYWMV2C.js.map +0 -1
- package/dist/chunk-Q2XUP227.js.map +0 -1
- package/dist/chunk-Q6OP4U43.cjs.map +0 -1
- package/dist/chunk-RC6GBMCD.cjs.map +0 -1
- package/dist/chunk-RK35RIN7.js.map +0 -1
- package/dist/chunk-XEBG3G53.js.map +0 -1
- package/dist/chunk-YLEMGEFH.js.map +0 -1
- package/dist/chunk-ZEJR4TKL.cjs.map +0 -1
- /package/dist/{chunk-HKGYCQ34.js.map → chunk-ZGLJM4A4.js.map} +0 -0
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
var
|
|
6
|
+
var _chunkPI3UXRWKcjs = require('./chunk-PI3UXRWK.cjs');
|
|
7
7
|
|
|
8
8
|
// src/core/random/index.ts
|
|
9
9
|
function createRng(seed) {
|
|
10
|
-
|
|
10
|
+
_chunkPI3UXRWKcjs.assertInteger.call(void 0, seed, "seed", 0, 4294967295);
|
|
11
11
|
let state = seed >>> 0;
|
|
12
12
|
const next = () => {
|
|
13
13
|
state = state + 1831565813 >>> 0;
|
|
@@ -19,29 +19,29 @@ function createRng(seed) {
|
|
|
19
19
|
return {
|
|
20
20
|
next,
|
|
21
21
|
prob: (p) => {
|
|
22
|
-
|
|
22
|
+
_chunkPI3UXRWKcjs.assertRange.call(void 0, p, 0, 1, "probability");
|
|
23
23
|
return next() < p;
|
|
24
24
|
},
|
|
25
25
|
range: (lo, hi) => {
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
_chunkPI3UXRWKcjs.assertInteger.call(void 0, lo, "range lower bound");
|
|
27
|
+
_chunkPI3UXRWKcjs.assertInteger.call(void 0, hi, "range upper bound");
|
|
28
28
|
if (lo > hi) {
|
|
29
|
-
throw new (0,
|
|
29
|
+
throw new (0, _chunkPI3UXRWKcjs.InvalidInputError)(
|
|
30
30
|
`range lower bound must not exceed upper bound; received ${lo} > ${hi}`
|
|
31
31
|
);
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
_chunkPI3UXRWKcjs.assertInteger.call(void 0, hi - lo, "range span", 0);
|
|
34
34
|
return lo + Math.floor(next() * (hi - lo + 1));
|
|
35
35
|
},
|
|
36
36
|
float: (lo, hi) => {
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
_chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, lo, "float lower bound");
|
|
38
|
+
_chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, hi, "float upper bound");
|
|
39
39
|
if (lo > hi) {
|
|
40
|
-
throw new (0,
|
|
40
|
+
throw new (0, _chunkPI3UXRWKcjs.InvalidInputError)(
|
|
41
41
|
`float lower bound must not exceed upper bound; received ${lo} > ${hi}`
|
|
42
42
|
);
|
|
43
43
|
}
|
|
44
|
-
|
|
44
|
+
_chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, hi - lo, "float span");
|
|
45
45
|
return lo + next() * (hi - lo);
|
|
46
46
|
}
|
|
47
47
|
};
|
|
@@ -50,4 +50,4 @@ function createRng(seed) {
|
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
exports.createRng = createRng;
|
|
53
|
-
//# sourceMappingURL=chunk-
|
|
53
|
+
//# sourceMappingURL=chunk-MTMZTMWM.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/libcantus/libcantus/dist/chunk-
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/libcantus/libcantus/dist/chunk-MTMZTMWM.cjs","../src/core/random/index.ts"],"names":[],"mappings":"AAAA;AACE;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACA;ACiCO,SAAS,SAAA,CAAU,IAAA,EAAmB;AAG3C,EAAA,6CAAA,IAAc,EAAM,MAAA,EAAQ,CAAA,EAAG,UAAU,CAAA;AACzC,EAAA,IAAI,MAAA,EAAQ,KAAA,IAAS,CAAA;AACrB,EAAA,MAAM,KAAA,EAAO,CAAA,EAAA,GAAM;AACjB,IAAA,MAAA,EAAS,MAAA,EAAQ,WAAA,IAAgB,CAAA;AACjC,IAAA,IAAI,EAAA,EAAI,KAAA;AACR,IAAA,EAAA,EAAI,IAAA,CAAK,IAAA,CAAK,EAAA,EAAK,EAAA,IAAM,EAAA,EAAK,EAAA,EAAI,CAAC,CAAA;AACnC,IAAA,EAAA,GAAK,EAAA,EAAI,IAAA,CAAK,IAAA,CAAK,EAAA,EAAK,EAAA,IAAM,CAAA,EAAI,EAAA,EAAI,EAAE,CAAA;AACxC,IAAA,OAAA,CAAA,CAAS,EAAA,EAAK,EAAA,IAAM,EAAA,EAAA,IAAS,CAAA,EAAA,EAAK,UAAA;AAAA,EACpC,CAAA;AACA,EAAA,OAAO;AAAA,IACL,IAAA;AAAA,IACA,IAAA,EAAM,CAAC,CAAA,EAAA,GAAM;AAIX,MAAA,2CAAA,CAAY,EAAG,CAAA,EAAG,CAAA,EAAG,aAAa,CAAA;AAClC,MAAA,OAAO,IAAA,CAAK,EAAA,EAAI,CAAA;AAAA,IAClB,CAAA;AAAA,IACA,KAAA,EAAO,CAAC,EAAA,EAAI,EAAA,EAAA,GAAO;AACjB,MAAA,6CAAA,EAAc,EAAI,mBAAmB,CAAA;AACrC,MAAA,6CAAA,EAAc,EAAI,mBAAmB,CAAA;AACrC,MAAA,GAAA,CAAI,GAAA,EAAK,EAAA,EAAI;AACX,QAAA,MAAM,IAAI,wCAAA;AAAA,UACR,CAAA,wDAAA,EAA2D,EAAE,CAAA,GAAA,EAAM,EAAE,CAAA;AAAA,QAAA;AACvE,MAAA;AAEF,MAAA;AACA,MAAA;AAA6C,IAAA;AAC/C,IAAA;AAEE,MAAA;AACA,MAAA;AACA,MAAA;AACE,QAAA;AAAU,UAAA;AAC6D,QAAA;AACvE,MAAA;AAEF,MAAA;AACA,MAAA;AAA2B,IAAA;AAC7B,EAAA;AAEJ;ADpCA;AACA;AACA;AACA","file":"/home/runner/work/libcantus/libcantus/dist/chunk-MTMZTMWM.cjs","sourcesContent":[null,"/**\n * Deterministic seeded pseudo-random number generation shared across the\n * generative modules (rhythm, drums, bass, groove, counter-melody). A single\n * mulberry32 core exposes the small set of samplers those modules need; given a\n * seed the stream is fully reproducible.\n */\n\nimport { InvalidInputError } from '../errors/index.js';\nimport { assertFiniteNumber, assertInteger, assertRange } from '../validation/index.js';\n\n/**\n * A deterministic PRNG with the sampling helpers the generators need.\n *\n * @category Utilities\n */\nexport type Rng = {\n /** Next float in [0, 1). */\n next: () => number;\n /** True with probability `p`. */\n prob: (p: number) => boolean;\n /** Integer in the inclusive range [lo, hi]. */\n range: (lo: number, hi: number) => number;\n /** Float in [lo, hi). */\n float: (lo: number, hi: number) => number;\n};\n\n/**\n * Create a seeded PRNG (mulberry32) exposing the shared samplers.\n *\n * @param seed The 32-bit seed; the same seed always yields the same stream.\n * @returns The seeded generator.\n * @example\n * ```ts\n * import { createRng } from '@libraz/libcantus';\n * const rng = createRng(42);\n * rng.next(); // deterministic float in [0, 1)\n * rng.range(1, 6); // deterministic integer in [1, 6]\n * ```\n * @category Utilities\n */\nexport function createRng(seed: number): Rng {\n // The state is 32 bits, so a wider or fractional seed would be truncated —\n // silently mapping distinct seeds onto one sequence.\n assertInteger(seed, 'seed', 0, 0xffffffff);\n let state = seed >>> 0;\n const next = () => {\n state = (state + 0x6d2b79f5) >>> 0;\n let t = state;\n t = Math.imul(t ^ (t >>> 15), t | 1);\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n };\n return {\n next,\n prob: (p) => {\n // Validate first: a draw consumed by a rejected call shifts the stream,\n // so a caller who catches the error gets a different sequence than one\n // who never made the call.\n assertRange(p, 0, 1, 'probability');\n return next() < p;\n },\n range: (lo, hi) => {\n assertInteger(lo, 'range lower bound');\n assertInteger(hi, 'range upper bound');\n if (lo > hi) {\n throw new InvalidInputError(\n `range lower bound must not exceed upper bound; received ${lo} > ${hi}`,\n );\n }\n assertInteger(hi - lo, 'range span', 0);\n return lo + Math.floor(next() * (hi - lo + 1));\n },\n float: (lo, hi) => {\n assertFiniteNumber(lo, 'float lower bound');\n assertFiniteNumber(hi, 'float upper bound');\n if (lo > hi) {\n throw new InvalidInputError(\n `float lower bound must not exceed upper bound; received ${lo} > ${hi}`,\n );\n }\n assertFiniteNumber(hi - lo, 'float span');\n return lo + next() * (hi - lo);\n },\n };\n}\n"]}
|
|
@@ -9,27 +9,29 @@ import {
|
|
|
9
9
|
majorKey,
|
|
10
10
|
makeChord,
|
|
11
11
|
scaleTonesInDegreeOrder
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-KBTTEX7Q.js";
|
|
13
13
|
import {
|
|
14
14
|
InvalidInputError,
|
|
15
15
|
assertFiniteNumber,
|
|
16
16
|
assertGenerationBudget,
|
|
17
17
|
assertInteger,
|
|
18
|
+
assertMidiPitch,
|
|
19
|
+
assertNoteEvents,
|
|
18
20
|
pitchClassOf
|
|
19
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-R2VVFAIN.js";
|
|
20
22
|
|
|
21
23
|
// src/analyze/detect/index.ts
|
|
22
24
|
function uniquePitchClasses(pitches) {
|
|
23
25
|
return [...new Set(pitches.map(pitchClassOf))].sort((a, b) => a - b);
|
|
24
26
|
}
|
|
25
|
-
function assertPitches(pitches) {
|
|
26
|
-
assertGenerationBudget(pitches.length,
|
|
27
|
+
function assertPitches(pitches, name, budget = void 0) {
|
|
28
|
+
assertGenerationBudget(pitches.length, name, budget);
|
|
27
29
|
for (let index = 0; index < pitches.length; index += 1) {
|
|
28
|
-
|
|
30
|
+
assertMidiPitch(pitches[index] ?? Number.NaN, `pitches[${index}]`);
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
33
|
function detectChord(pitches, opts = {}) {
|
|
32
|
-
assertPitches(pitches);
|
|
34
|
+
assertPitches(pitches, "chord detection pitches");
|
|
33
35
|
if (opts.bassPc !== void 0) assertFiniteNumber(opts.bassPc, "bassPc");
|
|
34
36
|
const input = uniquePitchClasses(pitches);
|
|
35
37
|
if (input.length === 0) {
|
|
@@ -40,6 +42,9 @@ function detectChord(pitches, opts = {}) {
|
|
|
40
42
|
const inputSet = new Set(input);
|
|
41
43
|
const matches = [];
|
|
42
44
|
const qualities = chordQualities();
|
|
45
|
+
const toneCounts = new Map(
|
|
46
|
+
qualities.map((quality) => [quality, chordPitchClasses(makeChord(0, quality)).length])
|
|
47
|
+
);
|
|
43
48
|
for (const rootPc of input) {
|
|
44
49
|
for (const quality of qualities) {
|
|
45
50
|
const chord = makeChord(rootPc, quality);
|
|
@@ -77,8 +82,8 @@ function detectChord(pitches, opts = {}) {
|
|
|
77
82
|
if (a.inversion === 0 !== (b.inversion === 0)) {
|
|
78
83
|
return a.inversion === 0 ? -1 : 1;
|
|
79
84
|
}
|
|
80
|
-
const aSize =
|
|
81
|
-
const bSize =
|
|
85
|
+
const aSize = toneCounts.get(a.quality) ?? 0;
|
|
86
|
+
const bSize = toneCounts.get(b.quality) ?? 0;
|
|
82
87
|
return bSize - aSize;
|
|
83
88
|
});
|
|
84
89
|
return matches;
|
|
@@ -97,11 +102,7 @@ var MINOR_VARIANTS = [
|
|
|
97
102
|
];
|
|
98
103
|
var MAJOR_VARIANTS = [{ variant: "major", mask: MAJOR_MASK }];
|
|
99
104
|
function detectKey(pitches, opts = {}) {
|
|
100
|
-
assertPitches(pitches);
|
|
101
|
-
const input = uniquePitchClasses(pitches);
|
|
102
|
-
if (input.length === 0) {
|
|
103
|
-
return [];
|
|
104
|
-
}
|
|
105
|
+
assertPitches(pitches, "key detection pitches", opts.budget);
|
|
105
106
|
const weights = opts.weights;
|
|
106
107
|
if (weights !== void 0 && weights.length !== pitches.length) {
|
|
107
108
|
throw new InvalidInputError("weights must have one entry per pitch");
|
|
@@ -121,6 +122,7 @@ function detectKey(pitches, opts = {}) {
|
|
|
121
122
|
if (total === 0) {
|
|
122
123
|
return [];
|
|
123
124
|
}
|
|
125
|
+
const input = [...counts.keys()];
|
|
124
126
|
const results = [];
|
|
125
127
|
for (let tonic = 0; tonic < 12; tonic += 1) {
|
|
126
128
|
for (const mode of ["major", "minor"]) {
|
|
@@ -163,11 +165,18 @@ function detectKey(pitches, opts = {}) {
|
|
|
163
165
|
return results;
|
|
164
166
|
}
|
|
165
167
|
var DEFAULT_VELOCITY = 100;
|
|
166
|
-
function detectKeyFromNotes(notes) {
|
|
168
|
+
function detectKeyFromNotes(notes, opts = {}) {
|
|
169
|
+
assertNoteEvents(notes, "key detection notes", {
|
|
170
|
+
allowNonPositiveDuration: true,
|
|
171
|
+
budget: opts.budget
|
|
172
|
+
});
|
|
167
173
|
const sounding = notes.filter((note) => note.durationBeat > 0);
|
|
168
174
|
return detectKey(
|
|
169
175
|
sounding.map((note) => note.pitch),
|
|
170
|
-
{
|
|
176
|
+
{
|
|
177
|
+
weights: sounding.map((note) => note.durationBeat * (note.velocity ?? DEFAULT_VELOCITY)),
|
|
178
|
+
budget: opts.budget
|
|
179
|
+
}
|
|
171
180
|
);
|
|
172
181
|
}
|
|
173
182
|
function detectKeyBest(pitches, opts = {}) {
|
|
@@ -205,6 +214,10 @@ function romanReference(key) {
|
|
|
205
214
|
function isNeapolitan(chord, key) {
|
|
206
215
|
return pitchClassOf(chord.rootPc - key.rootPc) === 1 && chord.quality === "maj";
|
|
207
216
|
}
|
|
217
|
+
function isAppliedDominantSonority(chord) {
|
|
218
|
+
const has = (semitones) => chord.intervals.some((i) => pitchClassOf(i) === semitones);
|
|
219
|
+
return chord.quality === "maj" || has(4) && has(10);
|
|
220
|
+
}
|
|
208
221
|
|
|
209
222
|
// src/analyze/functional/roman.ts
|
|
210
223
|
var ROMAN = ["I", "II", "III", "IV", "V", "VI", "VII"];
|
|
@@ -255,6 +268,9 @@ var ROMAN_STYLE = {
|
|
|
255
268
|
dim7: { lower: true, suffix: "o7" },
|
|
256
269
|
m7b5: { lower: true, suffix: "\xF87" },
|
|
257
270
|
minMaj7: { lower: true, suffix: "maj7" },
|
|
271
|
+
minMaj9: { lower: true, suffix: "maj9" },
|
|
272
|
+
minMaj11: { lower: true, suffix: "maj11" },
|
|
273
|
+
minMaj13: { lower: true, suffix: "maj13" },
|
|
258
274
|
aug7: { lower: false, suffix: "+7" },
|
|
259
275
|
augMaj7: { lower: false, suffix: "+maj7" },
|
|
260
276
|
majb5: { lower: false, suffix: "b5" },
|
|
@@ -317,20 +333,27 @@ function parseSimpleRoman(text, key) {
|
|
|
317
333
|
if (!match) {
|
|
318
334
|
throw new InvalidInputError(`Invalid Roman numeral: ${text}`);
|
|
319
335
|
}
|
|
336
|
+
const glyphText = match[2] ?? "";
|
|
337
|
+
if (glyphText !== glyphText.toUpperCase() && glyphText !== glyphText.toLowerCase()) {
|
|
338
|
+
throw new InvalidInputError(`Roman numeral must use one case: ${text}`);
|
|
339
|
+
}
|
|
320
340
|
const accidental = match[1] === "b" ? -1 : match[1] === "#" ? 1 : 0;
|
|
321
|
-
const glyph =
|
|
341
|
+
const glyph = glyphText.toUpperCase();
|
|
322
342
|
const degreeNumber = ROMAN_TO_DEGREE[glyph];
|
|
323
343
|
if (degreeNumber === void 0) {
|
|
324
344
|
throw new InvalidInputError(`Invalid Roman numeral: ${text}`);
|
|
325
345
|
}
|
|
326
|
-
const isUpper =
|
|
346
|
+
const isUpper = glyphText[0] === glyphText[0]?.toUpperCase();
|
|
327
347
|
const suffix = match[3] ?? "";
|
|
328
348
|
const frame = romanReference(key);
|
|
329
349
|
const redundantFlat = accidental === -1 && loweredDegrees(frame).has(degreeNumber);
|
|
330
|
-
const
|
|
350
|
+
const diatonicRootPc = pitchClassOf(
|
|
351
|
+
degreeRootPc(degreeNumber, frame) + (redundantFlat ? 0 : accidental)
|
|
352
|
+
);
|
|
353
|
+
const rootForQuality = (quality2) => accidental === 0 && degreeNumber === 7 && loweredDegrees(frame).has(7) && (quality2 === "dim" || quality2 === "dim7") ? pitchClassOf(diatonicRootPc + 1) : diatonicRootPc;
|
|
331
354
|
const canonical = SUFFIX_QUALITY.get(`${isUpper ? "u" : "l"}:${suffix}`);
|
|
332
355
|
if (canonical !== void 0) {
|
|
333
|
-
return { rootPc, quality: canonical, inversion: 0 };
|
|
356
|
+
return { rootPc: rootForQuality(canonical), quality: canonical, inversion: 0 };
|
|
334
357
|
}
|
|
335
358
|
const parsedSuffix = /^(o|°|dim|ø|\+|aug)?(maj7|M7)?(65|64|43|42|7|6|2)?$/.exec(suffix);
|
|
336
359
|
if (!parsedSuffix) {
|
|
@@ -369,7 +392,7 @@ function parseSimpleRoman(text, key) {
|
|
|
369
392
|
} else {
|
|
370
393
|
quality = base;
|
|
371
394
|
}
|
|
372
|
-
return { rootPc, quality, inversion };
|
|
395
|
+
return { rootPc: rootForQuality(quality), quality, inversion };
|
|
373
396
|
}
|
|
374
397
|
function chordFromParsed(parsed) {
|
|
375
398
|
const chord = makeChord(parsed.rootPc, parsed.quality);
|
|
@@ -379,7 +402,7 @@ function chordFromParsed(parsed) {
|
|
|
379
402
|
return chord;
|
|
380
403
|
}
|
|
381
404
|
function romanToChord(text, key) {
|
|
382
|
-
const trimmed = text.trim();
|
|
405
|
+
const trimmed = text.trim().replace(/(\d)\/(?=\d)/g, "$1");
|
|
383
406
|
const slash = trimmed.indexOf("/");
|
|
384
407
|
if (slash >= 0) {
|
|
385
408
|
const applied = trimmed.slice(0, slash);
|
|
@@ -463,10 +486,6 @@ function numeralFor(degreeNumber, lower) {
|
|
|
463
486
|
function isDiatonicChord(chord, key) {
|
|
464
487
|
return chordPitchClasses(chord).every((pc) => isScaleTone(pc, key));
|
|
465
488
|
}
|
|
466
|
-
function isAppliedDominantSonority(chord) {
|
|
467
|
-
const has = (semitones) => chord.intervals.some((i) => pitchClassOf(i) === semitones);
|
|
468
|
-
return chord.quality === "maj" || has(4) && has(10);
|
|
469
|
-
}
|
|
470
489
|
var LEADING_TONE_QUALITIES = /* @__PURE__ */ new Set(["dim", "dim7", "m7b5"]);
|
|
471
490
|
function degreeTriad(tones, index) {
|
|
472
491
|
const root = tones[index] ?? 0;
|
|
@@ -488,9 +507,6 @@ function appliedTarget(chord, key) {
|
|
|
488
507
|
continue;
|
|
489
508
|
}
|
|
490
509
|
const triad = degreeTriad(tones, index);
|
|
491
|
-
if (triad.fifth === 6) {
|
|
492
|
-
continue;
|
|
493
|
-
}
|
|
494
510
|
const expectedRoot = pitchClassOf(targetRoot + (dominant ? 7 : -1));
|
|
495
511
|
if (expectedRoot === pitchClassOf(chord.rootPc)) {
|
|
496
512
|
return { degreeNumber: index + 1, rootPc: targetRoot, lower: triad.third === 3 };
|
|
@@ -506,9 +522,10 @@ function chordToRoman(chord, key, opts = {}) {
|
|
|
506
522
|
return `${local}/${numeralFor(target.degreeNumber, target.lower)}`;
|
|
507
523
|
}
|
|
508
524
|
}
|
|
509
|
-
const { degreeNumber, accidental } = romanSpelling(chord.rootPc, key);
|
|
525
|
+
const { degreeNumber, accidental: spelledAccidental } = romanSpelling(chord.rootPc, key);
|
|
510
526
|
const { lower, suffix } = romanStyle(chord.quality);
|
|
511
527
|
const cased = numeralFor(degreeNumber, lower);
|
|
528
|
+
const accidental = spelledAccidental === "#" && degreeNumber === 7 && loweredDegrees(romanReference(key)).has(7) && (chord.quality === "dim" || chord.quality === "dim7") ? "" : spelledAccidental;
|
|
512
529
|
let inversion = 0;
|
|
513
530
|
if (chord.bassPc !== void 0) {
|
|
514
531
|
const idx = chord.intervals.findIndex((iv) => pitchClassOf(chord.rootPc + iv) === chord.bassPc);
|
|
@@ -526,14 +543,20 @@ function chordToRoman(chord, key, opts = {}) {
|
|
|
526
543
|
}
|
|
527
544
|
|
|
528
545
|
// src/analyze/functional/borrowed.ts
|
|
529
|
-
function
|
|
530
|
-
|
|
546
|
+
function isMinorScaleAlteration(chord, key) {
|
|
547
|
+
if (!isMinorKey(key)) return false;
|
|
548
|
+
const alteredKey = { rootPc: pitchClassOf(key.rootPc), modeMask12: HARMONIC_MINOR_MASK };
|
|
549
|
+
if (isDiatonic(chord, alteredKey)) return true;
|
|
550
|
+
return pitchClassOf(chord.rootPc - key.rootPc) === 11 && isDiatonic(chord, { rootPc: pitchClassOf(key.rootPc), modeMask12: MELODIC_MINOR_MASK });
|
|
531
551
|
}
|
|
532
552
|
function isBorrowedChord(chord, key) {
|
|
533
553
|
if (isDiatonic(chord, key)) {
|
|
534
554
|
return false;
|
|
535
555
|
}
|
|
536
|
-
if (
|
|
556
|
+
if (isNeapolitan(chord, key)) {
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
if (isMinorScaleAlteration(chord, key)) {
|
|
537
560
|
return false;
|
|
538
561
|
}
|
|
539
562
|
return isDiatonic(chord, parallelKey(key));
|
|
@@ -542,9 +565,7 @@ function borrowedSource(chord, key) {
|
|
|
542
565
|
if (isDiatonic(chord, key)) {
|
|
543
566
|
return null;
|
|
544
567
|
}
|
|
545
|
-
if (isNeapolitan(chord, key))
|
|
546
|
-
return "neapolitan";
|
|
547
|
-
}
|
|
568
|
+
if (isNeapolitan(chord, key)) return "neapolitan";
|
|
548
569
|
if (isBorrowedChord(chord, key)) {
|
|
549
570
|
return isMinorKey(key) ? "parallelMajor" : "parallelMinor";
|
|
550
571
|
}
|
|
@@ -578,6 +599,20 @@ var FUNCTION_BY_OFFSET = [
|
|
|
578
599
|
"dominant"
|
|
579
600
|
// 11 vii
|
|
580
601
|
];
|
|
602
|
+
var MINOR_FUNCTION_BY_OFFSET = [
|
|
603
|
+
"tonic",
|
|
604
|
+
"subdominant",
|
|
605
|
+
"subdominant",
|
|
606
|
+
"tonic",
|
|
607
|
+
"dominant",
|
|
608
|
+
"subdominant",
|
|
609
|
+
"dominant",
|
|
610
|
+
"dominant",
|
|
611
|
+
"tonic",
|
|
612
|
+
"tonic",
|
|
613
|
+
"subdominant",
|
|
614
|
+
"dominant"
|
|
615
|
+
];
|
|
581
616
|
function isMinorKey(key) {
|
|
582
617
|
const hasMinorThird = key.modeMask12 >> 3 & 1;
|
|
583
618
|
const hasMajorThird2 = key.modeMask12 >> 4 & 1;
|
|
@@ -594,7 +629,8 @@ function functionOf(chord, key) {
|
|
|
594
629
|
if (!isMinorKey(key) && hasMajorThird(chord) && (offset === 8 || offset === 10)) {
|
|
595
630
|
return "subdominant";
|
|
596
631
|
}
|
|
597
|
-
|
|
632
|
+
const functions = isMinorKey(key) ? MINOR_FUNCTION_BY_OFFSET : FUNCTION_BY_OFFSET;
|
|
633
|
+
return functions[offset] ?? "tonic";
|
|
598
634
|
}
|
|
599
635
|
function isAppliedDominant(chord, key) {
|
|
600
636
|
if (isDiatonic(chord, key)) {
|
|
@@ -602,8 +638,8 @@ function isAppliedDominant(chord, key) {
|
|
|
602
638
|
}
|
|
603
639
|
const root = pitchClassOf(chord.rootPc);
|
|
604
640
|
const resolvesTo = (step) => isScaleTone(pitchClassOf(root + step), key);
|
|
605
|
-
if (
|
|
606
|
-
return resolvesTo(5) || resolvesTo(11);
|
|
641
|
+
if (isAppliedDominantSonority(chord)) {
|
|
642
|
+
return chord.quality === "maj" ? resolvesTo(5) : resolvesTo(5) || resolvesTo(11);
|
|
607
643
|
}
|
|
608
644
|
return isDiminishedQuality(chord.quality) && resolvesTo(1);
|
|
609
645
|
}
|
|
@@ -622,37 +658,46 @@ function isDiminishedQuality(quality) {
|
|
|
622
658
|
function hasMajorThird(chord) {
|
|
623
659
|
return chord.intervals.some((interval) => pitchClassOf(interval) === 4);
|
|
624
660
|
}
|
|
625
|
-
function analyzeChord(chord, key) {
|
|
661
|
+
function analyzeChord(chord, key, opts = {}) {
|
|
626
662
|
const source = borrowedSource(chord, key);
|
|
627
663
|
return {
|
|
628
664
|
function: functionOf(chord, key),
|
|
629
665
|
borrowed: source !== null,
|
|
630
666
|
source,
|
|
631
|
-
roman: chordToRoman(chord, key)
|
|
667
|
+
roman: chordToRoman(chord, key, opts)
|
|
632
668
|
};
|
|
633
669
|
}
|
|
634
670
|
function secondaryDominant(targetDegree, key) {
|
|
635
|
-
|
|
671
|
+
const degreeCount = scaleTonesInDegreeOrder(key).length;
|
|
672
|
+
assertInteger(targetDegree, "targetDegree", 0, degreeCount - 1);
|
|
636
673
|
const targetRoot = degreeRootPc(targetDegree + 1, key);
|
|
637
674
|
return makeChord(pitchClassOf(targetRoot + 7), "dom7");
|
|
638
675
|
}
|
|
639
676
|
|
|
640
677
|
// src/analyze/functional/cadence.ts
|
|
678
|
+
function hasDominantThird(chord) {
|
|
679
|
+
return chord.intervals.some((interval) => pitchClassOf(interval) === 4);
|
|
680
|
+
}
|
|
681
|
+
function isLeadingToneDiminished(chord) {
|
|
682
|
+
return chord.quality === "dim" || chord.quality === "dim7" || chord.quality === "m7b5";
|
|
683
|
+
}
|
|
641
684
|
function detectCadence(from, to, key) {
|
|
642
685
|
const tonic = pitchClassOf(key.rootPc);
|
|
643
686
|
const fromOffset = pitchClassOf(from.rootPc - tonic);
|
|
644
687
|
const toOffset = pitchClassOf(to.rootPc - tonic);
|
|
645
688
|
const deceptiveTargets = isMinorKey(key) ? [8] : [8, 9];
|
|
646
|
-
|
|
689
|
+
const dominant = fromOffset === 7 && hasDominantThird(from);
|
|
690
|
+
const leadingTone = fromOffset === 11 && isLeadingToneDiminished(from);
|
|
691
|
+
if ((dominant || leadingTone) && toOffset === 0) {
|
|
647
692
|
return "authentic";
|
|
648
693
|
}
|
|
649
694
|
if (fromOffset === 5 && toOffset === 0) {
|
|
650
695
|
return "plagal";
|
|
651
696
|
}
|
|
652
|
-
if (
|
|
697
|
+
if (dominant && deceptiveTargets.includes(toOffset)) {
|
|
653
698
|
return "deceptive";
|
|
654
699
|
}
|
|
655
|
-
if (toOffset === 7 && fromOffset !== 7) {
|
|
700
|
+
if (toOffset === 7 && hasDominantThird(to) && fromOffset !== 7) {
|
|
656
701
|
return "half";
|
|
657
702
|
}
|
|
658
703
|
return null;
|
|
@@ -676,4 +721,4 @@ export {
|
|
|
676
721
|
borrowedSource,
|
|
677
722
|
detectCadence
|
|
678
723
|
};
|
|
679
|
-
//# sourceMappingURL=chunk-
|
|
724
|
+
//# sourceMappingURL=chunk-NNB67XA2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/analyze/detect/index.ts","../src/analyze/functional/internal.ts","../src/analyze/functional/roman.ts","../src/analyze/functional/borrowed.ts","../src/analyze/functional/function.ts","../src/analyze/functional/cadence.ts"],"sourcesContent":["/**\n * Recognition: infer a chord name from a set of pitches, or a key from a set of\n * pitch classes. This is the inverse direction of the chord/scale builders,\n * which only go name -> notes.\n */\n\nimport { InvalidInputError } from '../../core/errors/index.js';\nimport { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport type { KeyScale, NoteEvent } from '../../core/types.js';\nimport {\n assertFiniteNumber,\n assertGenerationBudget,\n assertMidiPitch,\n assertNoteEvents,\n} from '../../core/validation/index.js';\nimport type { Chord, ChordQuality } from '../../theory/chord/index.js';\nimport { chordPitchClasses, chordQualities, makeChord } from '../../theory/chord/index.js';\nimport {\n HARMONIC_MINOR_MASK,\n MAJOR_MASK,\n MELODIC_MINOR_MASK,\n NATURAL_MINOR_MASK,\n} from '../../theory/scale/index.js';\n\n/**\n * A candidate chord interpretation of a pitch set.\n *\n * @category Recognition\n */\nexport type ChordMatch = {\n rootPc: number;\n quality: ChordQuality;\n /** Chord tones absent from the input (an incomplete voicing). */\n missingPcs: number[];\n /** Input pitch classes not belonging to the chord. */\n extraPcs: number[];\n /** True when the input pitch-class set equals the chord exactly. */\n exact: boolean;\n /**\n * Inversion implied by a known bass: 0 for root position, 1..n for an\n * inversion. Null when no inversion can be named — either the input is an\n * unordered pitch-class set with no bass, or the bass is not a chord tone (a\n * pedal or passing bass), in which case `bassPc` still reports it.\n */\n inversion: number | null;\n /** Bass pitch class when the lowest note is not the root. */\n bassPc?: number;\n};\n\n/**\n * Input interpretation for {@link detectChord} and {@link detectChordBest}.\n *\n * @category Recognition\n */\nexport type DetectChordOptions = {\n /**\n * `midi` uses the numerically lowest pitch as bass; `pitchClass` treats the\n * input as unordered. `auto` (default) selects pitch-class mode only when all\n * values lie in 0..11.\n */\n input?: 'auto' | 'midi' | 'pitchClass';\n /** Explicit bass pitch class, including for an unordered pitch-class set. */\n bassPc?: number;\n};\n\n/**\n * A candidate key interpretation of a pitch-class set.\n *\n * @category Recognition\n */\nexport type KeyMatch = {\n /** The scale that scored best for this tonic and mode, `variant` included. */\n key: KeyScale;\n mode: 'major' | 'minor';\n /** Which scale form `key` uses; minor keys pick the best-scoring of the three. */\n variant: KeyVariant;\n /**\n * Fraction of the distinct input pitch classes that belong to `key`, in\n * [0, 1]. Measured against the returned scale, so it always agrees with\n * `isScaleTone(pc, match.key)`. This is a coverage figure, not the ranking:\n * see `score`.\n */\n fit: number;\n /**\n * The value the results are ranked by, in [0, 1.5]. Unlike `fit` it counts\n * every occurrence rather than every distinct pitch class, so a repeated tone\n * weighs more, and it adds half a count per sounding of the tonic itself to\n * break ties between keys that contain the same notes.\n */\n score: number;\n};\n\n/**\n * Which form of a scale a {@link KeyMatch} settled on.\n *\n * @category Recognition\n */\nexport type KeyVariant = 'major' | 'natural' | 'harmonic' | 'melodic';\n\n/**\n * Input weighting for {@link detectKey}.\n *\n * @category Recognition\n */\nexport type DetectKeyOptions = {\n /**\n * How much each pitch counts toward the histogram, one entry per pitch.\n * Defaults to one per pitch, which weighs a thirty-second-note ornament as\n * heavily as the whole note under it. {@link detectKeyFromNotes} supplies\n * duration times velocity, matching how chord inference weighs its own\n * histogram.\n */\n weights?: readonly number[];\n /**\n * Upper bound on the input pitches processed by this detection call.\n *\n * @defaultValue {@link DEFAULT_GENERATION_BUDGET}\n */\n budget?: number;\n};\n\n/** Unique pitch classes of the input, sorted ascending. */\nfunction uniquePitchClasses(pitches: readonly number[]): number[] {\n return [...new Set(pitches.map(pitchClass))].sort((a, b) => a - b);\n}\n\nfunction assertPitches(\n pitches: readonly number[],\n name: string,\n budget: number | undefined = undefined,\n): void {\n assertGenerationBudget(pitches.length, name, budget);\n for (let index = 0; index < pitches.length; index += 1) {\n assertMidiPitch(pitches[index] ?? Number.NaN, `pitches[${index}]`);\n }\n}\n\n/**\n * Identify chords matching a set of pitches.\n *\n * Every input pitch class is tried as a root against every known chord quality.\n * A match is reported when all of the chord's tones are present in the input, or\n * when the only absent tone is the perfect fifth (a common omission in shell\n * voicings); matches are ranked best-first by fewest extra notes, then fewest\n * missing notes, then most specific (largest) chord. An exact match (no extras,\n * no missing) is flagged and ranked first.\n *\n * @param pitches MIDI pitches or bare pitch classes (octave-agnostic).\n * @returns Ranked chord interpretations (may be empty).\n * @example\n * ```ts\n * import { detectChord } from '@libraz/libcantus';\n * const matches = detectChord([60, 64, 67]); // C E G\n * matches[0]; // { rootPc: 0, quality: 'maj', exact: true, ... }\n * ```\n * @category Recognition\n */\nexport function detectChord(\n pitches: readonly number[],\n opts: DetectChordOptions = {},\n): ChordMatch[] {\n assertPitches(pitches, 'chord detection pitches');\n if (opts.bassPc !== undefined) assertFiniteNumber(opts.bassPc, 'bassPc');\n const input = uniquePitchClasses(pitches);\n if (input.length === 0) {\n return [];\n }\n const inputKind =\n opts.input === undefined || opts.input === 'auto'\n ? pitches.every((pitch) => Number.isInteger(pitch) && pitch >= 0 && pitch <= 11)\n ? 'pitchClass'\n : 'midi'\n : opts.input;\n const bassPc =\n opts.bassPc !== undefined\n ? pitchClass(opts.bassPc)\n : inputKind === 'midi'\n ? pitchClass(pitches.reduce((lowest, pitch) => Math.min(lowest, pitch), Infinity))\n : undefined;\n const inputSet = new Set(input);\n const matches: ChordMatch[] = [];\n const qualities = chordQualities();\n const toneCounts = new Map(\n qualities.map((quality) => [quality, chordPitchClasses(makeChord(0, quality)).length]),\n );\n for (const rootPc of input) {\n for (const quality of qualities) {\n const chord = makeChord(rootPc, quality);\n const tones = chordPitchClasses(chord);\n const toneSet = new Set(tones);\n const missingPcs = tones.filter((pc) => !inputSet.has(pc));\n // Accept an exact-tone match, or one whose only absent tone is the perfect\n // fifth; any other missing tone (third, sus tone, seventh, ...) rejects.\n const fifthPc = pitchClass(rootPc + 7);\n if (missingPcs.length > 1 || (missingPcs.length === 1 && missingPcs[0] !== fifthPc)) {\n continue;\n }\n const extraPcs = input.filter((pc) => !toneSet.has(pc));\n const bassIndex =\n bassPc === undefined\n ? -1\n : chord.intervals.findIndex((iv) => pitchClass(rootPc + iv) === bassPc);\n // A bass that is not a chord tone names no inversion; reporting it as 0\n // would be indistinguishable from root position for a caller that reads\n // `inversion === 0` as \"no slash needed\".\n const inversion = bassPc === undefined || bassIndex < 0 ? null : bassIndex;\n const match: ChordMatch = {\n rootPc,\n quality,\n missingPcs,\n extraPcs,\n exact: extraPcs.length === 0 && missingPcs.length === 0,\n inversion,\n };\n if (bassPc !== undefined && bassPc !== rootPc) {\n match.bassPc = bassPc;\n }\n matches.push(match);\n }\n }\n matches.sort((a, b) => {\n if (a.extraPcs.length !== b.extraPcs.length) {\n return a.extraPcs.length - b.extraPcs.length;\n }\n if (a.missingPcs.length !== b.missingPcs.length) {\n return a.missingPcs.length - b.missingPcs.length;\n }\n // Prefer root position (the bass is the chord root) on a tie.\n if ((a.inversion === 0) !== (b.inversion === 0)) {\n return a.inversion === 0 ? -1 : 1;\n }\n const aSize = toneCounts.get(a.quality) ?? 0;\n const bSize = toneCounts.get(b.quality) ?? 0;\n return bSize - aSize;\n });\n return matches;\n}\n\n/**\n * The single best chord interpretation of a pitch set, or null if none.\n *\n * @param pitches MIDI pitches or bare pitch classes.\n * @returns The top-ranked chord, or null when nothing matches.\n * @example\n * ```ts\n * import { detectChordBest } from '@libraz/libcantus';\n * detectChordBest([60, 64, 67]); // C major triad: { rootPc: 0, quality: 'maj', ... }\n * ```\n * @category Recognition\n */\nexport function detectChordBest(\n pitches: readonly number[],\n opts: DetectChordOptions = {},\n): Chord | null {\n const best = detectChord(pitches, opts)[0];\n if (!best) {\n return null;\n }\n return makeChord(best.rootPc, best.quality, best.bassPc);\n}\n\n/**\n * Minor-scale variants scored for each minor-key candidate. Scoring against all\n * three lets the raised sixth and seventh (e.g. the leading tone G# in A minor)\n * count toward their own tonic instead of only penalizing it. Natural minor\n * comes first so it wins a tie as the most diatonic reading.\n */\nconst MINOR_VARIANTS = [\n { variant: 'natural', mask: NATURAL_MINOR_MASK },\n { variant: 'harmonic', mask: HARMONIC_MINOR_MASK },\n { variant: 'melodic', mask: MELODIC_MINOR_MASK },\n] as const satisfies readonly { variant: KeyVariant; mask: number }[];\n\n/** The single major form, kept in the same shape as the minor variants. */\nconst MAJOR_VARIANTS = [{ variant: 'major', mask: MAJOR_MASK }] as const satisfies readonly {\n variant: KeyVariant;\n mask: number;\n}[];\n\n/**\n * Rank major and minor keys by how well they contain a set of pitch classes.\n *\n * Ranking is by `score`, which counts repetitions and adds a tonic weight so\n * that, among equally-fitting keys, the one whose root is actually sounded is\n * preferred. Minor candidates are scored against the natural, harmonic, and\n * melodic minor variants and keep the best of the three, so a minor cadence\n * containing the leading tone still resolves to its own tonic; the winning\n * variant is what `key` and `variant` report, so `fit` describes the scale the\n * caller receives. Returns all 24 keys ranked best-first, or an empty array for\n * an empty input (mirroring {@link detectChord}).\n *\n * @param pitches MIDI pitches or bare pitch classes.\n * @returns Ranked key interpretations (empty for an empty input).\n * @example\n * ```ts\n * import { detectKey } from '@libraz/libcantus';\n * const keys = detectKey([60, 62, 64, 65, 67, 69, 71]); // C major scale\n * keys[0].mode; // 'major', with keys[0].key.rootPc === 0\n * ```\n * @category Recognition\n */\nexport function detectKey(pitches: readonly number[], opts: DetectKeyOptions = {}): KeyMatch[] {\n assertPitches(pitches, 'key detection pitches', opts.budget);\n const weights = opts.weights;\n if (weights !== undefined && weights.length !== pitches.length) {\n throw new InvalidInputError('weights must have one entry per pitch');\n }\n const counts = new Map<number, number>();\n let total = 0;\n for (let index = 0; index < pitches.length; index += 1) {\n const weight = weights === undefined ? 1 : (weights[index] ?? 0);\n assertFiniteNumber(weight, `weights[${index}]`);\n if (weight <= 0) {\n continue;\n }\n const pc = pitchClass(pitches[index] ?? 0);\n counts.set(pc, (counts.get(pc) ?? 0) + weight);\n total += weight;\n }\n if (total === 0) {\n return [];\n }\n const input = [...counts.keys()];\n const results: KeyMatch[] = [];\n for (let tonic = 0; tonic < 12; tonic += 1) {\n for (const mode of ['major', 'minor'] as const) {\n const variants = mode === 'major' ? MAJOR_VARIANTS : MINOR_VARIANTS;\n // Score each scale variant and keep the best; ties keep the earlier\n // (more diatonic) variant. The winner is what the match reports, so its\n // fit is measured against the very scale the caller receives.\n let inScale = 0;\n let weighted = Number.NEGATIVE_INFINITY;\n let best: { variant: KeyVariant; mask: number } = variants[0];\n for (const candidate of variants) {\n let variantInScale = 0;\n let variantWeighted = 0;\n for (const pc of input) {\n const offset = (pc - tonic + 12) % 12;\n if ((candidate.mask >> offset) & 1) {\n variantInScale += 1;\n }\n }\n for (const [pc, count] of counts) {\n const offset = (pc - tonic + 12) % 12;\n if ((candidate.mask >> offset) & 1) {\n variantWeighted += count;\n }\n }\n if (variantWeighted > weighted) {\n weighted = variantWeighted;\n inScale = variantInScale;\n best = candidate;\n }\n }\n weighted += (counts.get(tonic) ?? 0) * 0.5;\n results.push({\n key: { rootPc: tonic, modeMask12: best.mask },\n mode,\n variant: best.variant,\n fit: inScale / input.length,\n score: weighted / total,\n });\n }\n }\n results.sort((a, b) => b.score - a.score);\n return results;\n}\n\n/** Default MIDI velocity assumed when a note event does not carry one. */\nconst DEFAULT_VELOCITY = 100;\n\n/**\n * Rank keys by how well they contain a set of note events, weighting each note\n * by how much of the music it actually occupies.\n *\n * {@link detectKey} counts every pitch once, which lets a run of fast ornamental\n * notes outvote the sustained harmony that establishes the key. This weighs\n * each note by duration times velocity, the same measure chord inference uses,\n * so the two agree on what the music emphasises. Notes that never sound (zero\n * or negative duration) are ignored.\n *\n * @param notes The note events to weigh.\n * @param opts Budget for processing imported note events.\n * @returns Ranked key interpretations (empty when nothing sounds).\n * @example\n * ```ts\n * import { detectKeyFromNotes } from '@libraz/libcantus';\n * detectKeyFromNotes([{ pitch: 60, startBeat: 0, durationBeat: 4 }])[0]?.key.rootPc; // 0\n * ```\n * @category Recognition\n */\nexport function detectKeyFromNotes(\n notes: readonly NoteEvent[],\n opts: Omit<DetectKeyOptions, 'weights'> = {},\n): KeyMatch[] {\n assertNoteEvents(notes, 'key detection notes', {\n allowNonPositiveDuration: true,\n budget: opts.budget,\n });\n const sounding = notes.filter((note) => note.durationBeat > 0);\n return detectKey(\n sounding.map((note) => note.pitch),\n {\n weights: sounding.map((note) => note.durationBeat * (note.velocity ?? DEFAULT_VELOCITY)),\n budget: opts.budget,\n },\n );\n}\n\n/**\n * The single best key interpretation of a pitch set.\n *\n * The counterpart of {@link detectChordBest}: the ranked list is the general\n * answer, but a caller that just wants \"what key is this\" should not have to\n * index into it and assert the result is there.\n *\n * @param pitches MIDI pitches or bare pitch classes.\n * @param opts How to weigh the input; see {@link DetectKeyOptions}.\n * @returns The top-ranked key, or null when nothing sounds.\n * @example\n * ```ts\n * import { detectKeyBest } from '@libraz/libcantus';\n * detectKeyBest([0, 2, 4, 5, 7, 9, 11])?.mode; // 'major'\n * ```\n * @category Recognition\n */\nexport function detectKeyBest(\n pitches: readonly number[],\n opts: DetectKeyOptions = {},\n): KeyMatch | null {\n return detectKey(pitches, opts)[0] ?? null;\n}\n","/**\n * Shared helpers for the functional-harmony modules.\n *\n * Roots are pitch classes measured against the key tonic, so borrowed and\n * chromatic chords are handled by their semitone offset rather than requiring a\n * spelled key signature.\n */\n\nimport { pitchClassOf as mod12 } from '../../core/pitch/index.js';\n\nexport { pitchClassOf as mod12 } from '../../core/pitch/index.js';\n\nimport type { KeyScale } from '../../core/types.js';\nimport type { Chord } from '../../theory/chord/index.js';\nimport { MAJOR_MASK, majorKey, scaleTonesInDegreeOrder } from '../../theory/scale/index.js';\n\n/** Diatonic pitch class of a 1-based scale degree in a key. */\nexport function degreeRootPc(degreeNumber: number, key: KeyScale): number {\n const tones = scaleTonesInDegreeOrder(key);\n if (tones.length === 0) {\n return mod12(key.rootPc);\n }\n // A plain `%` yields a negative index for a degree below the first, which\n // reads as undefined and collapses to the tonic; wrap into range instead.\n const index = (((degreeNumber - 1) % tones.length) + tones.length) % tones.length;\n return tones[index] ?? mod12(key.rootPc);\n}\n\n/**\n * The 1-based scale degrees that already sit a semitone below their parallel\n * major counterpart — the third, sixth and seventh of a natural minor key, the\n * third and seventh of dorian, and so on.\n *\n * Roman-numeral spelling and parsing both need this: a flat sign on such a\n * degree is the degree itself rather than a further lowering, and a chromatic\n * root a semitone above one of them is that degree raised (`#III` in a minor\n * key), not a flattened version of the degree above it.\n *\n * Only heptatonic keys have a degree-for-degree correspondence with a major\n * scale, so anything else reports no lowered degrees.\n */\nexport function loweredDegrees(key: KeyScale): Set<number> {\n const tones = scaleTonesInDegreeOrder(key);\n const out = new Set<number>();\n if (tones.length !== 7) {\n return out;\n }\n const major = scaleTonesInDegreeOrder({ rootPc: mod12(key.rootPc), modeMask12: MAJOR_MASK });\n for (let index = 0; index < 7; index += 1) {\n const tone = tones[index];\n const majorTone = major[index];\n if (tone !== undefined && majorTone !== undefined && mod12(majorTone - tone) === 1) {\n out.add(index + 1);\n }\n }\n return out;\n}\n\n/**\n * The heptatonic frame a Roman numeral is measured against for a key.\n *\n * Roman numerals only have seven glyphs, so a scale with any other number of\n * tones has no degree-for-degree numeral of its own: a pentatonic key would\n * make `VI` wrap around to the tonic and an octatonic or chromatic key would\n * have degrees no numeral can name. Such keys are read against their parallel\n * major instead — the same frame the chromatic fallback spelling already uses —\n * so `chordToRoman` and `romanToChord` stay mutual inverses for every key.\n */\nexport function romanReference(key: KeyScale): KeyScale {\n return scaleTonesInDegreeOrder(key).length === 7 ? key : majorKey(mod12(key.rootPc));\n}\n\n/** Whether a chord is the Neapolitan: a major triad on the flat second degree. */\nexport function isNeapolitan(chord: Chord, key: KeyScale): boolean {\n return mod12(chord.rootPc - key.rootPc) === 1 && chord.quality === 'maj';\n}\n\n/** Whether a chord has the sonority that can tonicize another scale degree. */\nexport function isAppliedDominantSonority(chord: Chord): boolean {\n const has = (semitones: number): boolean => chord.intervals.some((i) => mod12(i) === semitones);\n return chord.quality === 'maj' || (has(4) && has(10));\n}\n","/**\n * Roman-numeral parsing and formatting.\n *\n * The {@link ROMAN_STYLE} table is the single source of truth for both\n * directions, so {@link chordToRoman} and {@link romanToChord} stay mutual\n * inverses by construction.\n */\n\nimport { InvalidInputError } from '../../core/errors/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport type { Chord, ChordQuality } from '../../theory/chord/index.js';\nimport { chordPitchClasses, makeChord } from '../../theory/chord/index.js';\nimport { isScaleTone, majorKey, scaleTonesInDegreeOrder } from '../../theory/scale/index.js';\nimport {\n degreeRootPc,\n isAppliedDominantSonority,\n loweredDegrees,\n mod12,\n romanReference,\n} from './internal.js';\n\n/** Roman numeral glyphs indexed by degree number - 1. */\nconst ROMAN = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII'] as const;\n\n/** Roman glyph -> degree number (1..7). */\nconst ROMAN_TO_DEGREE: Record<string, number> = {\n I: 1,\n II: 2,\n III: 3,\n IV: 4,\n V: 5,\n VI: 6,\n VII: 7,\n};\n\n/** Major-key Roman spelling for each semitone offset above the tonic. */\nconst OFFSET_SPELLING: readonly [number, string][] = [\n [1, ''],\n [2, 'b'],\n [2, ''],\n [3, 'b'],\n [3, ''],\n [4, ''],\n [4, '#'],\n [5, ''],\n [6, 'b'],\n [6, ''],\n [7, 'b'],\n [7, ''],\n];\n\n/** Choose a seventh-chord quality from a triad label and the numeral case. */\nfunction seventhQuality(base: 'maj' | 'min' | 'dim' | 'aug', halfDim: boolean): ChordQuality {\n if (halfDim) {\n return 'm7b5';\n }\n if (base === 'dim') {\n return 'dim7';\n }\n if (base === 'aug') {\n return 'aug7';\n }\n return base === 'maj' ? 'dom7' : 'min7';\n}\n\n/** Figure strings that denote a recognized figured-bass inversion. */\nconst INVERSION_FIGURES = new Set(['6', '64', '65', '43', '42', '2', '7']);\n\n/**\n * Canonical Roman-numeral rendering (numeral case and quality suffix) for every\n * supported chord quality. The table is the single source of truth for both\n * directions: {@link chordToRoman} renders from it and {@link romanToChord}\n * recognizes its exact case-sensitive suffixes, so the two stay mutual\n * inverses by construction. Suffixes are chosen to never collide with\n * figured-bass inversion digits (`6`, `64`, `65`, `43`, `42`, `2`) — the added\n * sixth is `add6`, the six-nine chord is `69` — and each (case, suffix) pair is\n * unique.\n */\nconst ROMAN_STYLE: Record<ChordQuality, { lower: boolean; suffix: string }> = {\n maj: { lower: false, suffix: '' },\n min: { lower: true, suffix: '' },\n dim: { lower: true, suffix: 'o' },\n aug: { lower: false, suffix: '+' },\n maj7: { lower: false, suffix: 'maj7' },\n min7: { lower: true, suffix: '7' },\n dom7: { lower: false, suffix: '7' },\n dim7: { lower: true, suffix: 'o7' },\n m7b5: { lower: true, suffix: 'ø7' },\n minMaj7: { lower: true, suffix: 'maj7' },\n minMaj9: { lower: true, suffix: 'maj9' },\n minMaj11: { lower: true, suffix: 'maj11' },\n minMaj13: { lower: true, suffix: 'maj13' },\n aug7: { lower: false, suffix: '+7' },\n augMaj7: { lower: false, suffix: '+maj7' },\n majb5: { lower: false, suffix: 'b5' },\n '6': { lower: false, suffix: 'add6' },\n min6: { lower: true, suffix: 'add6' },\n '6/9': { lower: false, suffix: '69' },\n sus2: { lower: false, suffix: 'sus2' },\n sus4: { lower: false, suffix: 'sus4' },\n add9: { lower: false, suffix: 'add9' },\n add11: { lower: false, suffix: 'add11' },\n maj9: { lower: false, suffix: 'maj9' },\n min9: { lower: true, suffix: '9' },\n dom9: { lower: false, suffix: '9' },\n '7b9': { lower: false, suffix: '7b9' },\n '7#9': { lower: false, suffix: '7#9' },\n '7#11': { lower: false, suffix: '7#11' },\n '7b13': { lower: false, suffix: '7b13' },\n '11': { lower: false, suffix: '11' },\n '13': { lower: false, suffix: '13' },\n '5': { lower: false, suffix: '5' },\n '7sus4': { lower: false, suffix: '7sus4' },\n '7b5': { lower: false, suffix: '7b5' },\n '7alt': { lower: false, suffix: '7alt' },\n '13b9': { lower: false, suffix: '13b9' },\n maj13: { lower: false, suffix: 'maj13' },\n 'maj7#11': { lower: false, suffix: 'maj7#11' },\n min11: { lower: true, suffix: '11' },\n min13: { lower: true, suffix: '13' },\n minAdd9: { lower: true, suffix: 'add9' },\n 'min6/9': { lower: true, suffix: '69' },\n};\n\n/** Reverse lookup of {@link ROMAN_STYLE}: `(case, exact suffix) -> quality`. */\nconst SUFFIX_QUALITY: ReadonlyMap<string, ChordQuality> = new Map(\n (Object.entries(ROMAN_STYLE) as [ChordQuality, { lower: boolean; suffix: string }][]).map(\n ([quality, style]) => [`${style.lower ? 'l' : 'u'}:${style.suffix}`, quality],\n ),\n);\n\n/** Read figured-bass digits into a chord inversion and whether a seventh is implied. */\nfunction parseInversion(figures: string): { inversion: number; seventh: boolean } {\n if (figures === '65') {\n return { inversion: 1, seventh: true };\n }\n if (figures === '43') {\n return { inversion: 2, seventh: true };\n }\n if (figures === '42' || figures === '2') {\n return { inversion: 3, seventh: true };\n }\n if (figures === '7') {\n return { inversion: 0, seventh: true };\n }\n if (figures === '64') {\n return { inversion: 2, seventh: false };\n }\n if (figures === '6') {\n return { inversion: 1, seventh: false };\n }\n return { inversion: 0, seventh: false };\n}\n\n/** Parse a Roman numeral (no secondary '/') into a root, quality, and inversion. */\nfunction parseSimpleRoman(\n text: string,\n key: KeyScale,\n): { rootPc: number; quality: ChordQuality; inversion: number } {\n const match = /^([b#]?)([iIvV]+)(.*)$/.exec(text.trim());\n if (!match) {\n throw new InvalidInputError(`Invalid Roman numeral: ${text}`);\n }\n const glyphText = match[2] ?? '';\n if (glyphText !== glyphText.toUpperCase() && glyphText !== glyphText.toLowerCase()) {\n throw new InvalidInputError(`Roman numeral must use one case: ${text}`);\n }\n const accidental = match[1] === 'b' ? -1 : match[1] === '#' ? 1 : 0;\n const glyph = glyphText.toUpperCase();\n const degreeNumber = ROMAN_TO_DEGREE[glyph];\n if (degreeNumber === undefined) {\n throw new InvalidInputError(`Invalid Roman numeral: ${text}`);\n }\n const isUpper = glyphText[0] === glyphText[0]?.toUpperCase();\n const suffix = match[3] ?? '';\n // Degrees are read in the key's heptatonic frame, so a numeral means the same\n // root in both directions even when the key itself is not heptatonic.\n const frame = romanReference(key);\n // A flat on a degree the mode already lowers is that degree, not a further\n // lowering: `bVII` in A minor is G, the pop reading, rather than F#.\n const redundantFlat = accidental === -1 && loweredDegrees(frame).has(degreeNumber);\n const diatonicRootPc = mod12(\n degreeRootPc(degreeNumber, frame) + (redundantFlat ? 0 : accidental),\n );\n\n // In a minor key, an unaltered diminished seventh-degree numeral conventionally\n // denotes the harmonic-minor leading tone: `viio` in A minor is G#, not G.\n // An explicit accidental remains literal, so callers can still write `#viio`\n // (or `bviio`) when that distinction is meaningful to them.\n const rootForQuality = (quality: ChordQuality): number =>\n accidental === 0 &&\n degreeNumber === 7 &&\n loweredDegrees(frame).has(7) &&\n (quality === 'dim' || quality === 'dim7')\n ? mod12(diatonicRootPc + 1)\n : diatonicRootPc;\n\n // Canonical quality suffixes (exact match, case-sensitive on the numeral)\n // come first so every chordToRoman rendering re-parses to the same quality:\n // `V9` -> dom9 but `ii9` -> min9 and `Imaj9` -> maj9, and added-tone suffixes\n // whose digits would otherwise be misread as figured bass (`Iadd6`, `I69`,\n // `Isus2`, `Iadd11`) resolve to their qualities in root position.\n const canonical = SUFFIX_QUALITY.get(`${isUpper ? 'u' : 'l'}:${suffix}`);\n if (canonical !== undefined) {\n return { rootPc: rootForQuality(canonical), quality: canonical, inversion: 0 };\n }\n\n // Non-canonical spellings are limited to a complete quality marker followed\n // by an optional major-seventh marker and a recognized figured bass. Never\n // search substrings: `Vfoo` must not become diminished merely because it\n // contains an \"o\", and unknown trailing text must be rejected in full.\n const parsedSuffix = /^(o|°|dim|ø|\\+|aug)?(maj7|M7)?(65|64|43|42|7|6|2)?$/.exec(suffix);\n if (!parsedSuffix) {\n throw new InvalidInputError(`Unsupported suffix \"${suffix}\" in Roman numeral: ${text}`);\n }\n const qualityMarker = parsedSuffix[1] ?? '';\n const isDim = qualityMarker === 'o' || qualityMarker === '°' || qualityMarker === 'dim';\n const isAug = qualityMarker === '+' || qualityMarker === 'aug';\n const isHalfDim = qualityMarker === 'ø';\n const explicitMaj7 = parsedSuffix[2] !== undefined;\n const figures = parsedSuffix[3] ?? '';\n\n // Reject figure strings that are neither a known figured-bass inversion nor a\n // canonical quality suffix rather than silently degrading to a root triad.\n if (figures !== '' && !INVERSION_FIGURES.has(figures)) {\n throw new InvalidInputError(\n `Unsupported figured-bass or extension \"${figures}\" in Roman numeral: ${text}`,\n );\n }\n\n const { inversion, seventh } = parseInversion(figures);\n // The ø glyph is the half-diminished seventh by definition (there is no\n // half-diminished triad), so it always implies a seventh.\n const hasSeventh = explicitMaj7 || seventh || isHalfDim;\n\n let base: 'maj' | 'min' | 'dim' | 'aug';\n if (isDim || isHalfDim) {\n base = 'dim';\n } else if (isAug) {\n base = 'aug';\n } else {\n base = isUpper ? 'maj' : 'min';\n }\n\n let quality: ChordQuality;\n if (explicitMaj7) {\n quality = base === 'min' ? 'minMaj7' : base === 'aug' ? 'augMaj7' : 'maj7';\n } else if (hasSeventh) {\n quality = seventhQuality(base, isHalfDim);\n } else if (base === 'dim') {\n quality = 'dim';\n } else if (base === 'aug') {\n quality = 'aug';\n } else {\n quality = base;\n }\n return { rootPc: rootForQuality(quality), quality, inversion };\n}\n\n/** Build a chord from a parsed Roman numeral, attaching a bass for inversions. */\nfunction chordFromParsed(parsed: {\n rootPc: number;\n quality: ChordQuality;\n inversion: number;\n}): Chord {\n const chord = makeChord(parsed.rootPc, parsed.quality);\n if (parsed.inversion > 0 && parsed.inversion < chord.intervals.length) {\n chord.bassPc = mod12(parsed.rootPc + (chord.intervals[parsed.inversion] ?? 0));\n }\n return chord;\n}\n\n/**\n * Build the chord denoted by a Roman numeral in a key.\n *\n * Supports accidentals (`bVII`, `#iv`), case-based triad quality, the `o`/`ø`/`+`\n * suffixes, sevenths (`V7`, `viio7`, `Imaj7`), figured-bass inversions\n * (`V6`, `V64`, `V65`, `V43`, `V42`), and applied/secondary chords via a slash\n * (`V7/V`, `viio/ii`). The target after the slash is read as a scale degree\n * whose root becomes a local major tonic for the applied chord. Inverted chords\n * carry a `bassPc`.\n *\n * The `ø` glyph always denotes the half-diminished seventh (`iiø` == `iiø7` ==\n * `m7b5`), since no half-diminished triad exists. Extension and added-tone\n * suffixes are the canonical case-sensitive forms emitted by\n * {@link chordToRoman} and honor the numeral case: `V9` -> `dom9`, `ii9` ->\n * `min9`, `Imaj9` -> `maj9`, `V11` -> `11`, `V13` -> `13`, `Iadd6`/`iadd6` ->\n * `6`/`min6`, `I69` -> `6/9`, `Isus2`/`Isus4`, `Iadd9`/`Iadd11`, `Ib5` ->\n * `majb5`, `I5` -> `5`, and the altered dominants `V7b9`/`V7#9`/`V7#11`/`V7b13`.\n * A figure string that is neither a recognized inversion nor a supported\n * quality suffix throws rather than silently degrading to a triad.\n *\n * @param text The Roman numeral.\n * @param key The prevailing key.\n * @returns The chord.\n * @example\n * ```ts\n * import { romanToChord, majorKey } from '@libraz/libcantus';\n * romanToChord('V7', majorKey(0)); // G7 in C major: { rootPc: 7, quality: 'dom7', ... }\n * ```\n * @category Functional Harmony\n */\nexport function romanToChord(text: string, key: KeyScale): Chord {\n // Accept the conventional slashes in figured bass (`V6/4`, `V6/5`) while\n // retaining `/` as the separator for applied dominants (`V7/V`).\n const trimmed = text.trim().replace(/(\\d)\\/(?=\\d)/g, '$1');\n const slash = trimmed.indexOf('/');\n if (slash >= 0) {\n const applied = trimmed.slice(0, slash);\n const target = trimmed.slice(slash + 1);\n const targetRoot = parseSimpleRoman(target, key).rootPc;\n const localKey = majorKey(targetRoot);\n return chordFromParsed(parseSimpleRoman(applied, localKey));\n }\n return chordFromParsed(parseSimpleRoman(trimmed, key));\n}\n\n/** Case and suffix for rendering a chord quality as a Roman numeral. */\nfunction romanStyle(quality: ChordQuality): { lower: boolean; suffix: string } {\n return ROMAN_STYLE[quality];\n}\n\n/** Choose the degree number and accidental to spell a root as a Roman numeral. */\nfunction romanSpelling(\n rootPc: number,\n key: KeyScale,\n): { degreeNumber: number; accidental: string } {\n const tones = scaleTonesInDegreeOrder(romanReference(key));\n const diatonic = tones.indexOf(mod12(rootPc));\n if (diatonic >= 0) {\n return { degreeNumber: diatonic + 1, accidental: '' };\n }\n // A chromatic root a semitone above a degree the mode already lowers is that\n // degree raised: C# in A minor is `#III`, not `bIV`.\n const lowered = loweredDegrees(romanReference(key));\n for (let i = 0; i < tones.length; i += 1) {\n if (lowered.has(i + 1) && mod12((tones[i] ?? 0) + 1) === mod12(rootPc)) {\n return { degreeNumber: i + 1, accidental: '#' };\n }\n }\n // Otherwise prefer a flat of the diatonic degree a semitone above it,\n // then a sharp of the degree a semitone below. Degree 1 (the tonic) and\n // degree 5 (the dominant) are skipped in the flat pass, so a raised leading\n // tone spells as `#vii` rather than a flat tonic `bI`, and the tritone above\n // the tonic spells as `#iv` rather than a flat fifth `bV` (matching the\n // conventional `OFFSET_SPELLING` for offset 6).\n for (let i = 0; i < tones.length; i += 1) {\n const degreeNumber = i + 1;\n if (degreeNumber === 1 || degreeNumber === 5) {\n continue;\n }\n if (mod12((tones[i] ?? 0) - 1) === mod12(rootPc)) {\n return { degreeNumber, accidental: 'b' };\n }\n }\n for (let i = 0; i < tones.length; i += 1) {\n if (mod12((tones[i] ?? 0) + 1) === mod12(rootPc)) {\n return { degreeNumber: i + 1, accidental: '#' };\n }\n }\n const [degreeNumber, accidental] = OFFSET_SPELLING[mod12(rootPc - key.rootPc)] ?? [1, ''];\n return { degreeNumber, accidental };\n}\n\n/** Quality marker (without the seventh digit) used when a figure carries the 7. */\nfunction baseMarker(quality: ChordQuality): string {\n if (quality === 'dim' || quality === 'dim7') {\n return 'o';\n }\n if (quality === 'm7b5') {\n return 'ø';\n }\n if (quality === 'aug' || quality === 'aug7') {\n return '+';\n }\n if (quality === 'augMaj7') {\n return '+maj7';\n }\n if (quality === 'maj7' || quality === 'minMaj7') {\n return 'maj7';\n }\n return '';\n}\n\nconst TRIAD_FIGURES: Record<number, string> = { 1: '6', 2: '64' };\nconst SEVENTH_FIGURES: Record<number, string> = { 1: '65', 2: '43', 3: '42' };\n\n/** Qualities whose inversions render as lossless triad figures (6, 64). */\nconst TRIAD_FIGURE_QUALITIES: ReadonlySet<ChordQuality> = new Set(['maj', 'min', 'dim', 'aug']);\n\n/**\n * Qualities carrying a chordal seventh, whose inversions render as seventh\n * figures (65, 43, 42). Membership is decided by the quality itself — a 7th\n * above the root (10 or 11 semitones, or the diminished 7th of `dim7`) — never\n * by interval count, which would turn added-tone chords into false sevenths.\n */\nconst SEVENTH_FIGURE_QUALITIES: ReadonlySet<ChordQuality> = new Set([\n 'maj7',\n 'min7',\n 'dom7',\n 'dim7',\n 'm7b5',\n 'minMaj7',\n 'aug7',\n 'augMaj7',\n]);\n\n/**\n * The numeral glyph for a degree number, cased for the chord quality.\n *\n * Only the seven glyphs exist, so a degree outside 1..7 is a contract violation\n * rather than something to paper over with a default numeral.\n */\nfunction numeralFor(degreeNumber: number, lower: boolean): string {\n const glyph = ROMAN[degreeNumber - 1];\n if (glyph === undefined) {\n throw new InvalidInputError(`no Roman numeral for scale degree ${degreeNumber}`);\n }\n return lower ? glyph.toLowerCase() : glyph;\n}\n\n/** Whether every pitch class of a chord belongs to the key's scale. */\nfunction isDiatonicChord(chord: Chord, key: KeyScale): boolean {\n return chordPitchClasses(chord).every((pc) => isScaleTone(pc, key));\n}\n\n/** Diminished-family qualities, which tonicize from a semitone below. */\nconst LEADING_TONE_QUALITIES: ReadonlySet<ChordQuality> = new Set(['dim', 'dim7', 'm7b5']);\n\n/** The third and fifth above a scale degree, measured within its own scale. */\nfunction degreeTriad(tones: readonly number[], index: number): { third: number; fifth: number } {\n const root = tones[index] ?? 0;\n return {\n third: mod12((tones[(index + 2) % tones.length] ?? 0) - root),\n fifth: mod12((tones[(index + 4) % tones.length] ?? 0) - root),\n };\n}\n\n/**\n * The scale degree a chromatic chord tonicizes, or null when it tonicizes\n * nothing. A dominant sonority points a fifth below itself, a diminished one a\n * semitone above itself; the tonic is never a target (that chord is the key's own\n * dominant) and neither is a degree carrying a diminished triad, which has no\n * fifth to be tonicized.\n */\nfunction appliedTarget(\n chord: Chord,\n key: KeyScale,\n): { degreeNumber: number; rootPc: number; lower: boolean } | null {\n const dominant = isAppliedDominantSonority(chord);\n const leadingTone = LEADING_TONE_QUALITIES.has(chord.quality);\n if (!dominant && !leadingTone) {\n return null;\n }\n const tones = scaleTonesInDegreeOrder(romanReference(key));\n for (let index = 1; index < tones.length; index += 1) {\n const targetRoot = tones[index];\n if (targetRoot === undefined) {\n continue;\n }\n const triad = degreeTriad(tones, index);\n const expectedRoot = mod12(targetRoot + (dominant ? 7 : -1));\n if (expectedRoot === mod12(chord.rootPc)) {\n return { degreeNumber: index + 1, rootPc: targetRoot, lower: triad.third === 3 };\n }\n }\n return null;\n}\n\n/**\n * Options for {@link chordToRoman}.\n *\n * @category Functional Harmony\n */\nexport type ChordToRomanOptions = {\n /**\n * Render a tonicizing chord as an applied numeral (`V7/V`, `viio7/ii`)\n * instead of naming its root against the home key (`II7`, `#ivo7`).\n *\n * Off by default: naming the root is always a correct spelling, whereas\n * whether a chromatic dominant is genuinely applied is a reading only the\n * caller can make. Turning it on makes {@link chordToRoman} the exact inverse\n * of {@link romanToChord} for the chords {@link secondaryDominant} builds.\n */\n applied?: boolean;\n};\n\n/**\n * Render a chord as a Roman numeral relative to a key.\n *\n * Diatonic roots take their scale-degree numeral directly, so numerals are\n * correct in both major and minor keys (and any custom scale). The quality\n * selects the case and suffix; chromatic roots receive a flat/sharp spelling by\n * convention. A key that is not heptatonic has no numeral of its own for every\n * degree, so its roots are named against its parallel major.\n *\n * When the chord carries a `bassPc` on a chord tone, a figured-bass inversion\n * (`6`, `64`, `65`, `43`, `42`) is emitted for plain triads and true seventh\n * chords; added-tone and extended qualities have no lossless figure and render\n * in root position instead (the bass is dropped, pitch classes are preserved).\n *\n * A `bassPc` that is *not* a chord tone — a pedal or passing bass such as\n * `C/D` — is likewise dropped, and the numeral names the upper chord alone.\n * Roman-numeral notation spends its slash on applied chords (`V7/V`), so there\n * is no unambiguous way to write such a bass; a caller that must keep it should\n * read `chord.bassPc` alongside the numeral.\n *\n * @param chord The chord to name.\n * @param key The prevailing key.\n * @param opts `applied` renders tonicizing chords as `V7/V`-style numerals.\n * @returns The Roman numeral string.\n * @example\n * ```ts\n * import { chordToRoman, makeChord, majorKey } from '@libraz/libcantus';\n * chordToRoman(makeChord(7, 'dom7'), majorKey(0)); // => 'V7' (G7 in C major)\n * chordToRoman(makeChord(2, 'dom7'), majorKey(0), { applied: true }); // => 'V7/V'\n * ```\n * @category Functional Harmony\n */\nexport function chordToRoman(chord: Chord, key: KeyScale, opts: ChordToRomanOptions = {}): string {\n if (opts.applied === true && !isDiatonicChord(chord, key)) {\n const target = appliedTarget(chord, key);\n if (target !== null) {\n const local = chordToRoman(chord, majorKey(target.rootPc));\n return `${local}/${numeralFor(target.degreeNumber, target.lower)}`;\n }\n }\n const { degreeNumber, accidental: spelledAccidental } = romanSpelling(chord.rootPc, key);\n const { lower, suffix } = romanStyle(chord.quality);\n const cased = numeralFor(degreeNumber, lower);\n // Render the conventional unaltered `viio`/`viio7` for the raised leading\n // tone in a minor key. Keeping the parser and formatter aligned makes the\n // common harmonic-minor progression a true round trip.\n const accidental =\n spelledAccidental === '#' &&\n degreeNumber === 7 &&\n loweredDegrees(romanReference(key)).has(7) &&\n (chord.quality === 'dim' || chord.quality === 'dim7')\n ? ''\n : spelledAccidental;\n\n let inversion = 0;\n if (chord.bassPc !== undefined) {\n const idx = chord.intervals.findIndex((iv) => mod12(chord.rootPc + iv) === chord.bassPc);\n if (idx > 0) {\n inversion = idx;\n }\n }\n if (inversion > 0) {\n const figure = SEVENTH_FIGURE_QUALITIES.has(chord.quality)\n ? SEVENTH_FIGURES[inversion]\n : TRIAD_FIGURE_QUALITIES.has(chord.quality)\n ? TRIAD_FIGURES[inversion]\n : undefined;\n if (figure !== undefined) {\n return `${accidental}${cased}${baseMarker(chord.quality)}${figure}`;\n }\n // No lossless figured-bass symbol exists — the quality is an added-tone or\n // extended chord, or the bass falls on a tension beyond the seventh — so\n // fall back to root-position rendering with the quality suffix rather than\n // emitting a figure that would re-parse as a different chord.\n }\n return `${accidental}${cased}${suffix}`;\n}\n","/**\n * Borrowed chords (modal interchange) and the Neapolitan.\n *\n * Roots are pitch classes measured against the key tonic, so borrowed and\n * chromatic chords are handled by their semitone offset rather than requiring a\n * spelled key signature.\n */\n\nimport type { KeyScale } from '../../core/types.js';\nimport type { Chord } from '../../theory/chord/index.js';\nimport { HARMONIC_MINOR_MASK, MELODIC_MINOR_MASK } from '../../theory/scale/index.js';\nimport { isDiatonic, isMinorKey, parallelKey } from './function.js';\nimport { isNeapolitan, mod12 } from './internal.js';\n\n/**\n * The origin of a recognized non-diatonic chord, or null when none applies.\n *\n * @category Functional Harmony\n */\nexport type BorrowedSource = 'parallelMinor' | 'parallelMajor' | 'neapolitan' | null;\n\n/**\n * In a minor key, whether a non-diatonic chord is explained by the harmonic\n * minor scale on the same tonic (major V, V7, raised-leading-tone vii°), or\n * by the melodic-minor raised-leading-tone half-diminished seventh. Such\n * chords are in-key chromatic alterations, not modal interchange.\n */\nfunction isMinorScaleAlteration(chord: Chord, key: KeyScale): boolean {\n if (!isMinorKey(key)) return false;\n const alteredKey = { rootPc: mod12(key.rootPc), modeMask12: HARMONIC_MINOR_MASK };\n if (isDiatonic(chord, alteredKey)) return true;\n // Melodic minor contributes the raised-leading-tone half-diminished seventh,\n // but treating every chord in its ascending collection as a non-borrowed\n // alteration would hide ordinary modal interchange such as minor-key IV.\n return (\n mod12(chord.rootPc - key.rootPc) === 11 &&\n isDiatonic(chord, { rootPc: mod12(key.rootPc), modeMask12: MELODIC_MINOR_MASK })\n );\n}\n\n/**\n * Whether a chord is borrowed from the parallel mode (modal interchange).\n *\n * True when the chord is not diatonic to `key` but is diatonic to its\n * {@link parallelKey} — e.g. iv, bVI, or bVII in a major key, or the Picardy\n * tonic and major IV in a minor key. Two non-diatonic families are excluded:\n * chords diatonic to neither mode (they are chromatic, not borrowed), and, in\n * a minor key, chords explained by the altered minor forms (the major\n * dominant and raised-leading-tone chords), which are in-key alterations\n * rather than interchange even though they happen to fit the parallel major.\n *\n * @param chord The chord to test.\n * @param key The prevailing key.\n * @returns True if the chord is borrowed from the parallel mode.\n * @category Functional Harmony\n */\nexport function isBorrowedChord(chord: Chord, key: KeyScale): boolean {\n if (isDiatonic(chord, key)) {\n return false;\n }\n if (isNeapolitan(chord, key)) {\n return true;\n }\n if (isMinorScaleAlteration(chord, key)) {\n return false;\n }\n return isDiatonic(chord, parallelKey(key));\n}\n\n/**\n * Identify where a non-diatonic chord comes from.\n *\n * The Neapolitan (major triad on b2) is recognized first, since it sits\n * outside both parallel modes; parallel-mode borrowing follows the\n * {@link isBorrowedChord} rules. Diatonic chords and altered-minor chords,\n * and unrecognized chromatic chords all yield null.\n *\n * @param chord The chord to classify.\n * @param key The prevailing key.\n * @returns The borrowing source, or null.\n * @category Functional Harmony\n */\nexport function borrowedSource(chord: Chord, key: KeyScale): BorrowedSource {\n if (isDiatonic(chord, key)) {\n return null;\n }\n if (isNeapolitan(chord, key)) return 'neapolitan';\n if (isBorrowedChord(chord, key)) {\n return isMinorKey(key) ? 'parallelMajor' : 'parallelMinor';\n }\n return null;\n}\n","/**\n * Harmonic function (tonic / subdominant / dominant), diatonicity, parallel-key\n * mirroring, and full chord analysis.\n *\n * Roots are pitch classes measured against the key tonic, so borrowed and\n * chromatic chords are handled by their semitone offset rather than requiring a\n * spelled key signature.\n */\n\nimport type { KeyScale } from '../../core/types.js';\nimport { assertInteger } from '../../core/validation/index.js';\nimport type { Chord, ChordQuality } from '../../theory/chord/index.js';\nimport { chordPitchClasses, makeChord } from '../../theory/chord/index.js';\nimport {\n isScaleTone,\n MAJOR_MASK,\n NATURAL_MINOR_MASK,\n scaleTonesInDegreeOrder,\n} from '../../theory/scale/index.js';\nimport { type BorrowedSource, borrowedSource } from './borrowed.js';\nimport { degreeRootPc, isAppliedDominantSonority, isNeapolitan, mod12 } from './internal.js';\nimport { type ChordToRomanOptions, chordToRoman } from './roman.js';\n\n/**\n * The three broad harmonic functions of tonal music.\n *\n * @category Functional Harmony\n */\nexport type HarmonicFunction = 'tonic' | 'subdominant' | 'dominant';\n\n/** Harmonic function of each semitone offset above the tonic (major context). */\nconst FUNCTION_BY_OFFSET: readonly HarmonicFunction[] = [\n 'tonic', // 0 I\n 'subdominant', // 1 bII (Neapolitan)\n 'subdominant', // 2 ii\n 'tonic', // 3 bIII\n 'tonic', // 4 iii\n 'subdominant', // 5 IV\n 'dominant', // 6 #IV / bV\n 'dominant', // 7 V\n 'subdominant', // 8 bVI\n 'tonic', // 9 vi\n 'subdominant', // 10 bVII\n 'dominant', // 11 vii\n];\n\n/** Harmonic function of each root offset in a natural-minor context. */\nconst MINOR_FUNCTION_BY_OFFSET: readonly HarmonicFunction[] = [\n 'tonic',\n 'subdominant',\n 'subdominant',\n 'tonic',\n 'dominant',\n 'subdominant',\n 'dominant',\n 'dominant',\n 'tonic',\n 'tonic',\n 'subdominant',\n 'dominant',\n];\n\n/**\n * Whether a key's scale has a minor third and no major third (a minor key).\n *\n * @category Functional Harmony\n */\nexport function isMinorKey(key: KeyScale): boolean {\n const hasMinorThird = (key.modeMask12 >> 3) & 1;\n const hasMajorThird = (key.modeMask12 >> 4) & 1;\n return Boolean(hasMinorThird) && !hasMajorThird;\n}\n\n/**\n * The harmonic function of a chord in a key.\n *\n * The root's offset above the tonic gives the baseline mapping, which follows\n * common-practice major-key function and is a useful approximation in minor and\n * for borrowed chords. Chord quality then refines it where the sonority settles\n * an ambiguity the root offset cannot:\n *\n * - A chord that *sounds* like a dominant — a major third with a minor seventh,\n * or a diminished-family chord — and is not diatonic to the key has dominant\n * function when its root resolves down a fifth or by a semitone onto a\n * diatonic degree. This is what makes an applied dominant (`A7` in C, which\n * tonicizes ii) and its tritone substitute (`Db7`) read as dominant rather\n * than inheriting the function of the degree they happen to sit on.\n * - The Neapolitan is subdominant.\n * - A major triad on bVI or bVII of a major key is subdominant — the borrowed\n * pop cadence chord, distinct from the bVII7 above, which has a seventh and\n * is a dominant sonority.\n *\n * @param chord The chord.\n * @param key The prevailing key.\n * @returns The harmonic function.\n * @example\n * ```ts\n * import { functionOf, makeChord, majorKey } from '@libraz/libcantus';\n * functionOf(makeChord(9, 'dom7'), majorKey(0)); // 'dominant' — A7 tonicizes ii\n * functionOf(makeChord(10, 'maj'), majorKey(0)); // 'subdominant' — borrowed bVII\n * ```\n * @category Functional Harmony\n */\nexport function functionOf(chord: Chord, key: KeyScale): HarmonicFunction {\n const offset = mod12(chord.rootPc - key.rootPc);\n if (isNeapolitan(chord, key)) {\n return 'subdominant';\n }\n if (isAppliedDominant(chord, key)) {\n return 'dominant';\n }\n if (!isMinorKey(key) && hasMajorThird(chord) && (offset === 8 || offset === 10)) {\n return 'subdominant';\n }\n const functions = isMinorKey(key) ? MINOR_FUNCTION_BY_OFFSET : FUNCTION_BY_OFFSET;\n return functions[offset] ?? 'tonic';\n}\n\n/**\n * Whether the chord is a dominant sonority pointing at a diatonic degree.\n *\n * A chord already diatonic to the key keeps the offset table's reading; what is\n * classified here is the chromatic chord whose sonority and resolution give it\n * dominant function regardless of the degree it sits on.\n *\n * The resolution required depends on the sonority. A dominant seventh (major\n * third plus minor seventh) must fall a perfect fifth — the applied dominants —\n * or a semitone, which is the same motion its tritone substitute makes. A\n * diminished-family chord must rise a semitone, the leading-tone resolution;\n * requiring that is what keeps a borrowed `iiø7`, which falls a fifth like any\n * other supertonic chord, reading as a predominant rather than a dominant.\n */\nfunction isAppliedDominant(chord: Chord, key: KeyScale): boolean {\n if (isDiatonic(chord, key)) {\n return false;\n }\n const root = mod12(chord.rootPc);\n const resolvesTo = (step: number) => isScaleTone(mod12(root + step), key);\n if (isAppliedDominantSonority(chord)) {\n return chord.quality === 'maj' ? resolvesTo(5) : resolvesTo(5) || resolvesTo(11);\n }\n return isDiminishedQuality(chord.quality) && resolvesTo(1);\n}\n\n/**\n * The result of {@link analyzeChord}: function, borrowing, and Roman numeral.\n *\n * @category Functional Harmony\n */\nexport type ChordAnalysis = {\n function: HarmonicFunction;\n borrowed: boolean;\n source: BorrowedSource;\n roman: string;\n};\n\n/**\n * Whether every pitch class of a chord belongs to the key's scale.\n *\n * The test is strict against the key's own mode mask: in a natural-minor key\n * the harmonic-minor dominant (major V) is *not* diatonic, since the raised\n * leading tone lies outside the mask. Borrowing predicates treat that case as\n * an in-key alteration separately (see {@link isBorrowedChord}).\n *\n * @param chord The chord to test.\n * @param key The prevailing key.\n * @returns True if all chord pitch classes are scale tones.\n * @category Functional Harmony\n */\nexport function isDiatonic(chord: Chord, key: KeyScale): boolean {\n return chordPitchClasses(chord).every((pc) => isScaleTone(pc, key));\n}\n\n/**\n * The parallel key: same tonic, opposite mode.\n *\n * A key with a minor third (natural/harmonic/melodic minor, dorian, phrygian)\n * maps to the parallel major; any other key maps to the parallel natural minor.\n *\n * @param key The key to mirror.\n * @returns The parallel major or natural-minor key on the same tonic.\n * @category Functional Harmony\n */\nexport function parallelKey(key: KeyScale): KeyScale {\n return {\n rootPc: mod12(key.rootPc),\n modeMask12: isMinorKey(key) ? MAJOR_MASK : NATURAL_MINOR_MASK,\n };\n}\n\n/** Diminished-family qualities: diminished triad, dim7, half-diminished. */\nfunction isDiminishedQuality(quality: ChordQuality): boolean {\n return quality === 'dim' || quality === 'dim7' || quality === 'm7b5';\n}\n\n/** Whether the chord's interval template carries a major third above the root. */\nfunction hasMajorThird(chord: Chord): boolean {\n return chord.intervals.some((interval) => mod12(interval) === 4);\n}\n\n/**\n * Analyze a chord in a key: harmonic function, borrowing, and Roman numeral.\n *\n * The function is quality-aware (see the predicates behind it), the source\n * follows {@link borrowedSource}, and the numeral comes from\n * {@link chordToRoman}. `borrowed` is true whenever a source is identified —\n * including the Neapolitan, which the stricter parallel-mode predicate\n * {@link isBorrowedChord} does not count.\n *\n * @param chord The chord to analyze.\n * @param key The prevailing key.\n * @param opts Options used when rendering the Roman numeral.\n * @returns The chord analysis.\n * @example\n * ```ts\n * import { analyzeChord, makeChord, majorKey } from '@libraz/libcantus';\n * analyzeChord(makeChord(7, 'dom7'), majorKey(0));\n * // { function: 'dominant', borrowed: false, source: null, roman: 'V7' }\n * ```\n * @category Functional Harmony\n */\nexport function analyzeChord(\n chord: Chord,\n key: KeyScale,\n opts: ChordToRomanOptions = {},\n): ChordAnalysis {\n const source = borrowedSource(chord, key);\n return {\n function: functionOf(chord, key),\n borrowed: source !== null,\n source,\n roman: chordToRoman(chord, key, opts),\n };\n}\n\n/**\n * The secondary dominant (V7) that tonicizes a scale degree.\n *\n * @param targetDegree 0-based scale degree to tonicize.\n * @param key The prevailing key.\n * @returns A dominant-seventh chord a fifth above the target's root.\n * @throws If `targetDegree` is not an integer naming a degree in `key`.\n * @category Functional Harmony\n */\nexport function secondaryDominant(targetDegree: number, key: KeyScale): Chord {\n // A degree outside the scale is a caller error, not a wrap-around: silently\n // tonicizing some other degree produces a chord that reads as intentional.\n const degreeCount = scaleTonesInDegreeOrder(key).length;\n assertInteger(targetDegree, 'targetDegree', 0, degreeCount - 1);\n const targetRoot = degreeRootPc(targetDegree + 1, key);\n return makeChord(mod12(targetRoot + 7), 'dom7');\n}\n","/**\n * Cadence detection between two successive chords.\n *\n * Roots are pitch classes measured against the key tonic, so borrowed and\n * chromatic chords are handled by their semitone offset rather than requiring a\n * spelled key signature.\n */\n\nimport type { KeyScale } from '../../core/types.js';\nimport type { Chord } from '../../theory/chord/index.js';\nimport { isMinorKey } from './function.js';\nimport { mod12 } from './internal.js';\n\n/** Whether a chord can carry dominant function through its major third. */\nfunction hasDominantThird(chord: Chord): boolean {\n return chord.intervals.some((interval) => mod12(interval) === 4);\n}\n\n/** Whether a leading-tone chord is a diminished-family resolution chord. */\nfunction isLeadingToneDiminished(chord: Chord): boolean {\n return chord.quality === 'dim' || chord.quality === 'dim7' || chord.quality === 'm7b5';\n}\n\n/**\n * A recognized cadence type, or null when a chord pair forms none.\n *\n * @category Functional Harmony\n */\nexport type Cadence = 'authentic' | 'plagal' | 'half' | 'deceptive' | null;\n\n/**\n * Classify the cadence formed by moving from one chord to the next.\n *\n * - authentic: V (dominant a fifth above the tonic) to I\n * - plagal: IV (a fourth above the tonic) to I\n * - deceptive: V to the submediant (diatonic vi or borrowed bVI in major, VI in\n * minor)\n * - half: any chord other than the dominant itself to V\n *\n * A static V-to-V repeat with no root motion is not a cadence and yields null.\n *\n * @param from The penultimate chord.\n * @param to The final chord.\n * @param key The prevailing key.\n * @returns The cadence type, or null.\n * @example\n * ```ts\n * import { detectCadence, makeChord, majorKey } from '@libraz/libcantus';\n * detectCadence(makeChord(7, 'maj'), makeChord(0, 'maj'), majorKey(0)); // => 'authentic'\n * ```\n * @category Functional Harmony\n */\nexport function detectCadence(from: Chord, to: Chord, key: KeyScale): Cadence {\n const tonic = mod12(key.rootPc);\n const fromOffset = mod12(from.rootPc - tonic);\n const toOffset = mod12(to.rootPc - tonic);\n // Deceptive targets: the diatonic submediant (vi at offset 9 in major, VI at\n // offset 8 in minor) plus, in a major key, the borrowed flat-submediant bVI\n // at offset 8.\n const deceptiveTargets = isMinorKey(key) ? [8] : [8, 9];\n const dominant = fromOffset === 7 && hasDominantThird(from);\n const leadingTone = fromOffset === 11 && isLeadingToneDiminished(from);\n if ((dominant || leadingTone) && toOffset === 0) {\n return 'authentic';\n }\n if (fromOffset === 5 && toOffset === 0) {\n return 'plagal';\n }\n if (dominant && deceptiveTargets.includes(toOffset)) {\n return 'deceptive';\n }\n // A move to the dominant is a half cadence, but a no-root-motion V-to-V repeat\n // is not a cadence at all.\n if (toOffset === 7 && hasDominantThird(to) && fromOffset !== 7) {\n return 'half';\n }\n return null;\n}\n"],"mappings":"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","names":["quality","degreeNumber","hasMajorThird"]}
|