@libraz/libcantus 0.9.3 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -20
- package/README_ja.md +268 -0
- package/dist/analyze/index.cjs +39 -1947
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +161 -91
- package/dist/analyze/index.d.ts +161 -91
- package/dist/analyze/index.js +19 -8
- package/dist/chunk-2A2QMA74.cjs +261 -0
- package/dist/chunk-2A2QMA74.cjs.map +1 -0
- package/dist/chunk-2PTFGVD7.js +261 -0
- package/dist/chunk-2PTFGVD7.js.map +1 -0
- package/dist/chunk-357ZDPVU.cjs +162 -0
- package/dist/chunk-357ZDPVU.cjs.map +1 -0
- package/dist/chunk-3KX76RLG.js +1548 -0
- package/dist/chunk-3KX76RLG.js.map +1 -0
- package/dist/chunk-4VZ5MZAM.cjs +889 -0
- package/dist/chunk-4VZ5MZAM.cjs.map +1 -0
- package/dist/{chunk-C5XYKUVG.js → chunk-7EONYVI5.js} +471 -171
- package/dist/chunk-7EONYVI5.js.map +1 -0
- package/dist/{chunk-IT2O4XFE.js → chunk-7JEP3MFY.js} +42 -34
- package/dist/chunk-7JEP3MFY.js.map +1 -0
- package/dist/{chunk-4MHA432G.js → chunk-AID75RN3.js} +572 -250
- package/dist/chunk-AID75RN3.js.map +1 -0
- package/dist/chunk-BSLTLGM3.cjs +497 -0
- package/dist/chunk-BSLTLGM3.cjs.map +1 -0
- package/dist/chunk-CU6VG2YM.cjs +109 -0
- package/dist/chunk-CU6VG2YM.cjs.map +1 -0
- package/dist/{chunk-EH6EM37U.js → chunk-DZMHIGI4.js} +81 -59
- package/dist/chunk-DZMHIGI4.js.map +1 -0
- package/dist/{chunk-AKLYPWIN.js → chunk-EDOTEABK.js} +45 -19
- package/dist/chunk-EDOTEABK.js.map +1 -0
- package/dist/{chunk-2ZWMSJH6.js → chunk-HN2OEUN2.js} +7 -4
- package/dist/chunk-HN2OEUN2.js.map +1 -0
- package/dist/{chunk-MIWO425M.js → chunk-KBTTEX7Q.js} +128 -43
- package/dist/chunk-KBTTEX7Q.js.map +1 -0
- package/dist/chunk-KQN2ZYND.cjs +724 -0
- package/dist/chunk-KQN2ZYND.cjs.map +1 -0
- package/dist/chunk-MPTVJIVG.cjs +3176 -0
- package/dist/chunk-MPTVJIVG.cjs.map +1 -0
- package/dist/chunk-MTMZTMWM.cjs +53 -0
- package/dist/chunk-MTMZTMWM.cjs.map +1 -0
- package/dist/{chunk-CHMXELDS.js → chunk-NNB67XA2.js} +372 -197
- package/dist/chunk-NNB67XA2.js.map +1 -0
- package/dist/chunk-OL3IHHSJ.cjs +1204 -0
- package/dist/chunk-OL3IHHSJ.cjs.map +1 -0
- package/dist/chunk-PI3UXRWK.cjs +528 -0
- package/dist/chunk-PI3UXRWK.cjs.map +1 -0
- package/dist/chunk-R2VVFAIN.js +528 -0
- package/dist/chunk-R2VVFAIN.js.map +1 -0
- package/dist/chunk-T64C7ZD7.cjs +1 -0
- package/dist/chunk-T64C7ZD7.cjs.map +1 -0
- package/dist/chunk-WFDXOLUB.cjs +337 -0
- package/dist/chunk-WFDXOLUB.cjs.map +1 -0
- package/dist/chunk-WTAN4ACG.cjs +1548 -0
- package/dist/chunk-WTAN4ACG.cjs.map +1 -0
- package/dist/chunk-YFJXS3AU.cjs +33 -0
- package/dist/chunk-YFJXS3AU.cjs.map +1 -0
- package/dist/{chunk-GOAW4VYL.js → chunk-YUE6TJ4M.js} +315 -145
- package/dist/chunk-YUE6TJ4M.js.map +1 -0
- package/dist/{chunk-62QZKCJQ.js → chunk-ZGLJM4A4.js} +10 -6
- package/dist/chunk-ZGLJM4A4.js.map +1 -0
- package/dist/core/index.cjs +140 -647
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +313 -41
- package/dist/core/index.d.ts +313 -41
- package/dist/core/index.js +64 -16
- package/dist/{function-DVH4Eusr.d.ts → function-CxixTB02.d.cts} +122 -10
- package/dist/{function-DYt4iKsn.d.cts → function-DTCt675z.d.ts} +122 -10
- package/dist/generate/index.cjs +28 -4085
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +374 -66
- package/dist/generate/index.d.ts +374 -66
- package/dist/generate/index.js +23 -9
- package/dist/{index-Jc0Ryi79.d.ts → index-B2Ulxxpo.d.cts} +21 -5
- package/dist/{index-CpOsP1y9.d.cts → index-BkYJ0xMq.d.cts} +18 -17
- package/dist/{index-CxqBQtH1.d.cts → index-BuaB-ypt.d.ts} +21 -5
- package/dist/index-CAN7v-kw.d.ts +199 -0
- package/dist/{index-Bt4tOUD9.d.ts → index-COw1Ky9g.d.ts} +93 -8
- package/dist/{index-Dikst44v.d.cts → index-Cfx8xgH0.d.ts} +14 -11
- package/dist/{index-DxvARIP2.d.ts → index-DRtIhlJj.d.ts} +18 -17
- package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
- package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
- package/dist/index-alrRninU.d.cts +199 -0
- package/dist/{index-BNlGP86A.d.cts → index-oTQbYYIs.d.cts} +93 -8
- package/dist/index-tR9jTn26.d.cts +275 -0
- package/dist/index-tR9jTn26.d.ts +275 -0
- package/dist/{index-Bg5dYMmZ.d.ts → index-x2vcsduE.d.cts} +14 -11
- package/dist/index.cjs +429 -7383
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -15
- package/dist/index.d.ts +15 -15
- package/dist/index.js +121 -27
- package/dist/model/index.cjs +8 -2562
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +471 -81
- package/dist/model/index.d.ts +471 -81
- package/dist/model/index.js +6 -7
- package/dist/styled-Bl0ZK28h.d.ts +543 -0
- package/dist/styled-Crv6SA4S.d.cts +543 -0
- package/dist/theory/index.cjs +172 -1822
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +151 -233
- package/dist/theory/index.d.ts +151 -233
- package/dist/theory/index.js +30 -7
- package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
- package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
- package/package.json +34 -13
- package/dist/chunk-2ZWMSJH6.js.map +0 -1
- package/dist/chunk-4MHA432G.js.map +0 -1
- package/dist/chunk-62QZKCJQ.js.map +0 -1
- package/dist/chunk-AKLYPWIN.js.map +0 -1
- package/dist/chunk-C5XYKUVG.js.map +0 -1
- package/dist/chunk-CHMXELDS.js.map +0 -1
- package/dist/chunk-DYWFNC4A.js +0 -152
- package/dist/chunk-DYWFNC4A.js.map +0 -1
- package/dist/chunk-EH6EM37U.js.map +0 -1
- package/dist/chunk-FBQK3ZQN.js +0 -131
- package/dist/chunk-FBQK3ZQN.js.map +0 -1
- package/dist/chunk-GOAW4VYL.js.map +0 -1
- package/dist/chunk-I5R7ZBJS.js +0 -202
- package/dist/chunk-I5R7ZBJS.js.map +0 -1
- package/dist/chunk-IT2O4XFE.js.map +0 -1
- package/dist/chunk-MIWO425M.js.map +0 -1
- package/dist/chunk-SGU7SPLK.js +0 -924
- package/dist/chunk-SGU7SPLK.js.map +0 -1
- package/dist/index-CGMZNCx6.d.cts +0 -140
- package/dist/index-CGMZNCx6.d.ts +0 -140
- package/dist/index-CnZCo29u.d.cts +0 -108
- package/dist/index-JnF4v9bx.d.ts +0 -108
- package/dist/styled-DZ7GjShb.d.ts +0 -282
- package/dist/styled-ZTbdaVFM.d.cts +0 -282
- package/src/analyze/arrange/index.ts +0 -21
- package/src/analyze/arrange/internal.ts +0 -127
- package/src/analyze/arrange/tension.ts +0 -199
- package/src/analyze/arrange/tracks.ts +0 -312
- package/src/analyze/detect/index.ts +0 -272
- package/src/analyze/functional/borrowed.ts +0 -84
- package/src/analyze/functional/cadence.ts +0 -66
- package/src/analyze/functional/function.ts +0 -184
- package/src/analyze/functional/index.ts +0 -23
- package/src/analyze/functional/internal.ts +0 -29
- package/src/analyze/functional/roman.ts +0 -402
- package/src/analyze/index.ts +0 -49
- package/src/analyze/timeline/index.ts +0 -451
- package/src/analyze/voice/index.ts +0 -339
- package/src/core/event-index/index.ts +0 -104
- package/src/core/index.ts +0 -61
- package/src/core/interval/index.ts +0 -81
- package/src/core/meter/index.ts +0 -302
- package/src/core/pitch/index.ts +0 -311
- package/src/core/random/index.ts +0 -76
- package/src/core/tuning/index.ts +0 -186
- package/src/core/types.ts +0 -29
- package/src/core/validation/index.ts +0 -119
- package/src/generate/bass/index.ts +0 -373
- package/src/generate/countermelody/index.ts +0 -470
- package/src/generate/drums/beat.ts +0 -266
- package/src/generate/drums/euclid.ts +0 -65
- package/src/generate/drums/fills.ts +0 -356
- package/src/generate/drums/ghost.ts +0 -119
- package/src/generate/drums/hihat.ts +0 -285
- package/src/generate/drums/hit.ts +0 -31
- package/src/generate/drums/index.ts +0 -361
- package/src/generate/drums/internal.ts +0 -257
- package/src/generate/drums/kick.ts +0 -160
- package/src/generate/drums/percussion.ts +0 -121
- package/src/generate/drums/rng.ts +0 -7
- package/src/generate/drums/swing.ts +0 -71
- package/src/generate/groove/index.ts +0 -351
- package/src/generate/harmonize/index.ts +0 -451
- package/src/generate/index.ts +0 -58
- package/src/generate/motif/index.ts +0 -422
- package/src/generate/progression/index.ts +0 -362
- package/src/generate/reharmony/index.ts +0 -323
- package/src/generate/rhythm/index.ts +0 -175
- package/src/index.ts +0 -13
- package/src/model/chord.ts +0 -442
- package/src/model/index.ts +0 -15
- package/src/model/interval.ts +0 -82
- package/src/model/key.ts +0 -266
- package/src/model/note.ts +0 -176
- package/src/model/progression.ts +0 -161
- package/src/model/shared.ts +0 -12
- package/src/theory/chord/index.ts +0 -375
- package/src/theory/chordscale/index.ts +0 -374
- package/src/theory/counterpoint/index.ts +0 -230
- package/src/theory/harmony/index.ts +0 -77
- package/src/theory/index.ts +0 -112
- package/src/theory/safety/index.ts +0 -488
- package/src/theory/scale/degrees.ts +0 -123
- package/src/theory/scale/index.ts +0 -28
- package/src/theory/scale/key.ts +0 -56
- package/src/theory/scale/masks.ts +0 -153
- package/src/theory/spelling/index.ts +0 -249
- package/src/theory/symbol/index.ts +0 -268
- package/src/theory/voicing/index.ts +0 -5
- package/src/theory/voicing/internal.ts +0 -168
- package/src/theory/voicing/leading.ts +0 -112
- package/src/theory/voicing/satb.ts +0 -216
- package/src/theory/voicing/styled.ts +0 -199
- package/tsconfig.json +0 -23
|
@@ -8,27 +8,215 @@ import {
|
|
|
8
8
|
isScaleTone,
|
|
9
9
|
majorKey,
|
|
10
10
|
makeChord,
|
|
11
|
-
minorKey,
|
|
12
11
|
scaleTonesInDegreeOrder
|
|
13
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-KBTTEX7Q.js";
|
|
14
13
|
import {
|
|
14
|
+
InvalidInputError,
|
|
15
15
|
assertFiniteNumber,
|
|
16
|
-
assertGenerationBudget
|
|
17
|
-
|
|
16
|
+
assertGenerationBudget,
|
|
17
|
+
assertInteger,
|
|
18
|
+
assertMidiPitch,
|
|
19
|
+
assertNoteEvents,
|
|
20
|
+
pitchClassOf
|
|
21
|
+
} from "./chunk-R2VVFAIN.js";
|
|
18
22
|
|
|
19
|
-
// src/analyze/
|
|
20
|
-
function
|
|
21
|
-
return (
|
|
23
|
+
// src/analyze/detect/index.ts
|
|
24
|
+
function uniquePitchClasses(pitches) {
|
|
25
|
+
return [...new Set(pitches.map(pitchClassOf))].sort((a, b) => a - b);
|
|
26
|
+
}
|
|
27
|
+
function assertPitches(pitches, name, budget = void 0) {
|
|
28
|
+
assertGenerationBudget(pitches.length, name, budget);
|
|
29
|
+
for (let index = 0; index < pitches.length; index += 1) {
|
|
30
|
+
assertMidiPitch(pitches[index] ?? Number.NaN, `pitches[${index}]`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function detectChord(pitches, opts = {}) {
|
|
34
|
+
assertPitches(pitches, "chord detection pitches");
|
|
35
|
+
if (opts.bassPc !== void 0) assertFiniteNumber(opts.bassPc, "bassPc");
|
|
36
|
+
const input = uniquePitchClasses(pitches);
|
|
37
|
+
if (input.length === 0) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
const inputKind = opts.input === void 0 || opts.input === "auto" ? pitches.every((pitch) => Number.isInteger(pitch) && pitch >= 0 && pitch <= 11) ? "pitchClass" : "midi" : opts.input;
|
|
41
|
+
const bassPc = opts.bassPc !== void 0 ? pitchClassOf(opts.bassPc) : inputKind === "midi" ? pitchClassOf(pitches.reduce((lowest, pitch) => Math.min(lowest, pitch), Infinity)) : void 0;
|
|
42
|
+
const inputSet = new Set(input);
|
|
43
|
+
const matches = [];
|
|
44
|
+
const qualities = chordQualities();
|
|
45
|
+
const toneCounts = new Map(
|
|
46
|
+
qualities.map((quality) => [quality, chordPitchClasses(makeChord(0, quality)).length])
|
|
47
|
+
);
|
|
48
|
+
for (const rootPc of input) {
|
|
49
|
+
for (const quality of qualities) {
|
|
50
|
+
const chord = makeChord(rootPc, quality);
|
|
51
|
+
const tones = chordPitchClasses(chord);
|
|
52
|
+
const toneSet = new Set(tones);
|
|
53
|
+
const missingPcs = tones.filter((pc) => !inputSet.has(pc));
|
|
54
|
+
const fifthPc = pitchClassOf(rootPc + 7);
|
|
55
|
+
if (missingPcs.length > 1 || missingPcs.length === 1 && missingPcs[0] !== fifthPc) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const extraPcs = input.filter((pc) => !toneSet.has(pc));
|
|
59
|
+
const bassIndex = bassPc === void 0 ? -1 : chord.intervals.findIndex((iv) => pitchClassOf(rootPc + iv) === bassPc);
|
|
60
|
+
const inversion = bassPc === void 0 || bassIndex < 0 ? null : bassIndex;
|
|
61
|
+
const match = {
|
|
62
|
+
rootPc,
|
|
63
|
+
quality,
|
|
64
|
+
missingPcs,
|
|
65
|
+
extraPcs,
|
|
66
|
+
exact: extraPcs.length === 0 && missingPcs.length === 0,
|
|
67
|
+
inversion
|
|
68
|
+
};
|
|
69
|
+
if (bassPc !== void 0 && bassPc !== rootPc) {
|
|
70
|
+
match.bassPc = bassPc;
|
|
71
|
+
}
|
|
72
|
+
matches.push(match);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
matches.sort((a, b) => {
|
|
76
|
+
if (a.extraPcs.length !== b.extraPcs.length) {
|
|
77
|
+
return a.extraPcs.length - b.extraPcs.length;
|
|
78
|
+
}
|
|
79
|
+
if (a.missingPcs.length !== b.missingPcs.length) {
|
|
80
|
+
return a.missingPcs.length - b.missingPcs.length;
|
|
81
|
+
}
|
|
82
|
+
if (a.inversion === 0 !== (b.inversion === 0)) {
|
|
83
|
+
return a.inversion === 0 ? -1 : 1;
|
|
84
|
+
}
|
|
85
|
+
const aSize = toneCounts.get(a.quality) ?? 0;
|
|
86
|
+
const bSize = toneCounts.get(b.quality) ?? 0;
|
|
87
|
+
return bSize - aSize;
|
|
88
|
+
});
|
|
89
|
+
return matches;
|
|
90
|
+
}
|
|
91
|
+
function detectChordBest(pitches, opts = {}) {
|
|
92
|
+
const best = detectChord(pitches, opts)[0];
|
|
93
|
+
if (!best) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return makeChord(best.rootPc, best.quality, best.bassPc);
|
|
97
|
+
}
|
|
98
|
+
var MINOR_VARIANTS = [
|
|
99
|
+
{ variant: "natural", mask: NATURAL_MINOR_MASK },
|
|
100
|
+
{ variant: "harmonic", mask: HARMONIC_MINOR_MASK },
|
|
101
|
+
{ variant: "melodic", mask: MELODIC_MINOR_MASK }
|
|
102
|
+
];
|
|
103
|
+
var MAJOR_VARIANTS = [{ variant: "major", mask: MAJOR_MASK }];
|
|
104
|
+
function detectKey(pitches, opts = {}) {
|
|
105
|
+
assertPitches(pitches, "key detection pitches", opts.budget);
|
|
106
|
+
const weights = opts.weights;
|
|
107
|
+
if (weights !== void 0 && weights.length !== pitches.length) {
|
|
108
|
+
throw new InvalidInputError("weights must have one entry per pitch");
|
|
109
|
+
}
|
|
110
|
+
const counts = /* @__PURE__ */ new Map();
|
|
111
|
+
let total = 0;
|
|
112
|
+
for (let index = 0; index < pitches.length; index += 1) {
|
|
113
|
+
const weight = weights === void 0 ? 1 : weights[index] ?? 0;
|
|
114
|
+
assertFiniteNumber(weight, `weights[${index}]`);
|
|
115
|
+
if (weight <= 0) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const pc = pitchClassOf(pitches[index] ?? 0);
|
|
119
|
+
counts.set(pc, (counts.get(pc) ?? 0) + weight);
|
|
120
|
+
total += weight;
|
|
121
|
+
}
|
|
122
|
+
if (total === 0) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
const input = [...counts.keys()];
|
|
126
|
+
const results = [];
|
|
127
|
+
for (let tonic = 0; tonic < 12; tonic += 1) {
|
|
128
|
+
for (const mode of ["major", "minor"]) {
|
|
129
|
+
const variants = mode === "major" ? MAJOR_VARIANTS : MINOR_VARIANTS;
|
|
130
|
+
let inScale = 0;
|
|
131
|
+
let weighted = Number.NEGATIVE_INFINITY;
|
|
132
|
+
let best = variants[0];
|
|
133
|
+
for (const candidate of variants) {
|
|
134
|
+
let variantInScale = 0;
|
|
135
|
+
let variantWeighted = 0;
|
|
136
|
+
for (const pc of input) {
|
|
137
|
+
const offset = (pc - tonic + 12) % 12;
|
|
138
|
+
if (candidate.mask >> offset & 1) {
|
|
139
|
+
variantInScale += 1;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
for (const [pc, count] of counts) {
|
|
143
|
+
const offset = (pc - tonic + 12) % 12;
|
|
144
|
+
if (candidate.mask >> offset & 1) {
|
|
145
|
+
variantWeighted += count;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (variantWeighted > weighted) {
|
|
149
|
+
weighted = variantWeighted;
|
|
150
|
+
inScale = variantInScale;
|
|
151
|
+
best = candidate;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
weighted += (counts.get(tonic) ?? 0) * 0.5;
|
|
155
|
+
results.push({
|
|
156
|
+
key: { rootPc: tonic, modeMask12: best.mask },
|
|
157
|
+
mode,
|
|
158
|
+
variant: best.variant,
|
|
159
|
+
fit: inScale / input.length,
|
|
160
|
+
score: weighted / total
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
results.sort((a, b) => b.score - a.score);
|
|
165
|
+
return results;
|
|
22
166
|
}
|
|
167
|
+
var DEFAULT_VELOCITY = 100;
|
|
168
|
+
function detectKeyFromNotes(notes, opts = {}) {
|
|
169
|
+
assertNoteEvents(notes, "key detection notes", {
|
|
170
|
+
allowNonPositiveDuration: true,
|
|
171
|
+
budget: opts.budget
|
|
172
|
+
});
|
|
173
|
+
const sounding = notes.filter((note) => note.durationBeat > 0);
|
|
174
|
+
return detectKey(
|
|
175
|
+
sounding.map((note) => note.pitch),
|
|
176
|
+
{
|
|
177
|
+
weights: sounding.map((note) => note.durationBeat * (note.velocity ?? DEFAULT_VELOCITY)),
|
|
178
|
+
budget: opts.budget
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
function detectKeyBest(pitches, opts = {}) {
|
|
183
|
+
return detectKey(pitches, opts)[0] ?? null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/analyze/functional/internal.ts
|
|
23
187
|
function degreeRootPc(degreeNumber, key) {
|
|
24
188
|
const tones = scaleTonesInDegreeOrder(key);
|
|
25
189
|
if (tones.length === 0) {
|
|
26
|
-
return
|
|
190
|
+
return pitchClassOf(key.rootPc);
|
|
191
|
+
}
|
|
192
|
+
const index = ((degreeNumber - 1) % tones.length + tones.length) % tones.length;
|
|
193
|
+
return tones[index] ?? pitchClassOf(key.rootPc);
|
|
194
|
+
}
|
|
195
|
+
function loweredDegrees(key) {
|
|
196
|
+
const tones = scaleTonesInDegreeOrder(key);
|
|
197
|
+
const out = /* @__PURE__ */ new Set();
|
|
198
|
+
if (tones.length !== 7) {
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
const major = scaleTonesInDegreeOrder({ rootPc: pitchClassOf(key.rootPc), modeMask12: MAJOR_MASK });
|
|
202
|
+
for (let index = 0; index < 7; index += 1) {
|
|
203
|
+
const tone = tones[index];
|
|
204
|
+
const majorTone = major[index];
|
|
205
|
+
if (tone !== void 0 && majorTone !== void 0 && pitchClassOf(majorTone - tone) === 1) {
|
|
206
|
+
out.add(index + 1);
|
|
207
|
+
}
|
|
27
208
|
}
|
|
28
|
-
return
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
function romanReference(key) {
|
|
212
|
+
return scaleTonesInDegreeOrder(key).length === 7 ? key : majorKey(pitchClassOf(key.rootPc));
|
|
29
213
|
}
|
|
30
214
|
function isNeapolitan(chord, key) {
|
|
31
|
-
return
|
|
215
|
+
return pitchClassOf(chord.rootPc - key.rootPc) === 1 && chord.quality === "maj";
|
|
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);
|
|
32
220
|
}
|
|
33
221
|
|
|
34
222
|
// src/analyze/functional/roman.ts
|
|
@@ -80,6 +268,9 @@ var ROMAN_STYLE = {
|
|
|
80
268
|
dim7: { lower: true, suffix: "o7" },
|
|
81
269
|
m7b5: { lower: true, suffix: "\xF87" },
|
|
82
270
|
minMaj7: { lower: true, suffix: "maj7" },
|
|
271
|
+
minMaj9: { lower: true, suffix: "maj9" },
|
|
272
|
+
minMaj11: { lower: true, suffix: "maj11" },
|
|
273
|
+
minMaj13: { lower: true, suffix: "maj13" },
|
|
83
274
|
aug7: { lower: false, suffix: "+7" },
|
|
84
275
|
augMaj7: { lower: false, suffix: "+maj7" },
|
|
85
276
|
majb5: { lower: false, suffix: "b5" },
|
|
@@ -99,7 +290,17 @@ var ROMAN_STYLE = {
|
|
|
99
290
|
"7b13": { lower: false, suffix: "7b13" },
|
|
100
291
|
"11": { lower: false, suffix: "11" },
|
|
101
292
|
"13": { lower: false, suffix: "13" },
|
|
102
|
-
"5": { lower: false, suffix: "5" }
|
|
293
|
+
"5": { lower: false, suffix: "5" },
|
|
294
|
+
"7sus4": { lower: false, suffix: "7sus4" },
|
|
295
|
+
"7b5": { lower: false, suffix: "7b5" },
|
|
296
|
+
"7alt": { lower: false, suffix: "7alt" },
|
|
297
|
+
"13b9": { lower: false, suffix: "13b9" },
|
|
298
|
+
maj13: { lower: false, suffix: "maj13" },
|
|
299
|
+
"maj7#11": { lower: false, suffix: "maj7#11" },
|
|
300
|
+
min11: { lower: true, suffix: "11" },
|
|
301
|
+
min13: { lower: true, suffix: "13" },
|
|
302
|
+
minAdd9: { lower: true, suffix: "add9" },
|
|
303
|
+
"min6/9": { lower: true, suffix: "69" }
|
|
103
304
|
};
|
|
104
305
|
var SUFFIX_QUALITY = new Map(
|
|
105
306
|
Object.entries(ROMAN_STYLE).map(
|
|
@@ -130,24 +331,33 @@ function parseInversion(figures) {
|
|
|
130
331
|
function parseSimpleRoman(text, key) {
|
|
131
332
|
const match = /^([b#]?)([iIvV]+)(.*)$/.exec(text.trim());
|
|
132
333
|
if (!match) {
|
|
133
|
-
throw new
|
|
334
|
+
throw new InvalidInputError(`Invalid Roman numeral: ${text}`);
|
|
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}`);
|
|
134
339
|
}
|
|
135
340
|
const accidental = match[1] === "b" ? -1 : match[1] === "#" ? 1 : 0;
|
|
136
|
-
const glyph =
|
|
341
|
+
const glyph = glyphText.toUpperCase();
|
|
137
342
|
const degreeNumber = ROMAN_TO_DEGREE[glyph];
|
|
138
343
|
if (degreeNumber === void 0) {
|
|
139
|
-
throw new
|
|
344
|
+
throw new InvalidInputError(`Invalid Roman numeral: ${text}`);
|
|
140
345
|
}
|
|
141
|
-
const isUpper =
|
|
346
|
+
const isUpper = glyphText[0] === glyphText[0]?.toUpperCase();
|
|
142
347
|
const suffix = match[3] ?? "";
|
|
143
|
-
const
|
|
348
|
+
const frame = romanReference(key);
|
|
349
|
+
const redundantFlat = accidental === -1 && loweredDegrees(frame).has(degreeNumber);
|
|
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;
|
|
144
354
|
const canonical = SUFFIX_QUALITY.get(`${isUpper ? "u" : "l"}:${suffix}`);
|
|
145
355
|
if (canonical !== void 0) {
|
|
146
|
-
return { rootPc, quality: canonical, inversion: 0 };
|
|
356
|
+
return { rootPc: rootForQuality(canonical), quality: canonical, inversion: 0 };
|
|
147
357
|
}
|
|
148
358
|
const parsedSuffix = /^(o|°|dim|ø|\+|aug)?(maj7|M7)?(65|64|43|42|7|6|2)?$/.exec(suffix);
|
|
149
359
|
if (!parsedSuffix) {
|
|
150
|
-
throw new
|
|
360
|
+
throw new InvalidInputError(`Unsupported suffix "${suffix}" in Roman numeral: ${text}`);
|
|
151
361
|
}
|
|
152
362
|
const qualityMarker = parsedSuffix[1] ?? "";
|
|
153
363
|
const isDim = qualityMarker === "o" || qualityMarker === "\xB0" || qualityMarker === "dim";
|
|
@@ -156,7 +366,9 @@ function parseSimpleRoman(text, key) {
|
|
|
156
366
|
const explicitMaj7 = parsedSuffix[2] !== void 0;
|
|
157
367
|
const figures = parsedSuffix[3] ?? "";
|
|
158
368
|
if (figures !== "" && !INVERSION_FIGURES.has(figures)) {
|
|
159
|
-
throw new
|
|
369
|
+
throw new InvalidInputError(
|
|
370
|
+
`Unsupported figured-bass or extension "${figures}" in Roman numeral: ${text}`
|
|
371
|
+
);
|
|
160
372
|
}
|
|
161
373
|
const { inversion, seventh } = parseInversion(figures);
|
|
162
374
|
const hasSeventh = explicitMaj7 || seventh || isHalfDim;
|
|
@@ -180,17 +392,17 @@ function parseSimpleRoman(text, key) {
|
|
|
180
392
|
} else {
|
|
181
393
|
quality = base;
|
|
182
394
|
}
|
|
183
|
-
return { rootPc, quality, inversion };
|
|
395
|
+
return { rootPc: rootForQuality(quality), quality, inversion };
|
|
184
396
|
}
|
|
185
397
|
function chordFromParsed(parsed) {
|
|
186
398
|
const chord = makeChord(parsed.rootPc, parsed.quality);
|
|
187
399
|
if (parsed.inversion > 0 && parsed.inversion < chord.intervals.length) {
|
|
188
|
-
chord.bassPc =
|
|
400
|
+
chord.bassPc = pitchClassOf(parsed.rootPc + (chord.intervals[parsed.inversion] ?? 0));
|
|
189
401
|
}
|
|
190
402
|
return chord;
|
|
191
403
|
}
|
|
192
404
|
function romanToChord(text, key) {
|
|
193
|
-
const trimmed = text.trim();
|
|
405
|
+
const trimmed = text.trim().replace(/(\d)\/(?=\d)/g, "$1");
|
|
194
406
|
const slash = trimmed.indexOf("/");
|
|
195
407
|
if (slash >= 0) {
|
|
196
408
|
const applied = trimmed.slice(0, slash);
|
|
@@ -205,26 +417,32 @@ function romanStyle(quality) {
|
|
|
205
417
|
return ROMAN_STYLE[quality];
|
|
206
418
|
}
|
|
207
419
|
function romanSpelling(rootPc, key) {
|
|
208
|
-
const tones = scaleTonesInDegreeOrder(key);
|
|
209
|
-
const diatonic = tones.indexOf(
|
|
420
|
+
const tones = scaleTonesInDegreeOrder(romanReference(key));
|
|
421
|
+
const diatonic = tones.indexOf(pitchClassOf(rootPc));
|
|
210
422
|
if (diatonic >= 0) {
|
|
211
423
|
return { degreeNumber: diatonic + 1, accidental: "" };
|
|
212
424
|
}
|
|
425
|
+
const lowered = loweredDegrees(romanReference(key));
|
|
426
|
+
for (let i = 0; i < tones.length; i += 1) {
|
|
427
|
+
if (lowered.has(i + 1) && pitchClassOf((tones[i] ?? 0) + 1) === pitchClassOf(rootPc)) {
|
|
428
|
+
return { degreeNumber: i + 1, accidental: "#" };
|
|
429
|
+
}
|
|
430
|
+
}
|
|
213
431
|
for (let i = 0; i < tones.length; i += 1) {
|
|
214
432
|
const degreeNumber2 = i + 1;
|
|
215
433
|
if (degreeNumber2 === 1 || degreeNumber2 === 5) {
|
|
216
434
|
continue;
|
|
217
435
|
}
|
|
218
|
-
if (
|
|
436
|
+
if (pitchClassOf((tones[i] ?? 0) - 1) === pitchClassOf(rootPc)) {
|
|
219
437
|
return { degreeNumber: degreeNumber2, accidental: "b" };
|
|
220
438
|
}
|
|
221
439
|
}
|
|
222
440
|
for (let i = 0; i < tones.length; i += 1) {
|
|
223
|
-
if (
|
|
441
|
+
if (pitchClassOf((tones[i] ?? 0) + 1) === pitchClassOf(rootPc)) {
|
|
224
442
|
return { degreeNumber: i + 1, accidental: "#" };
|
|
225
443
|
}
|
|
226
444
|
}
|
|
227
|
-
const [degreeNumber, accidental] = OFFSET_SPELLING[
|
|
445
|
+
const [degreeNumber, accidental] = OFFSET_SPELLING[pitchClassOf(rootPc - key.rootPc)] ?? [1, ""];
|
|
228
446
|
return { degreeNumber, accidental };
|
|
229
447
|
}
|
|
230
448
|
function baseMarker(quality) {
|
|
@@ -258,14 +476,59 @@ var SEVENTH_FIGURE_QUALITIES = /* @__PURE__ */ new Set([
|
|
|
258
476
|
"aug7",
|
|
259
477
|
"augMaj7"
|
|
260
478
|
]);
|
|
261
|
-
function
|
|
262
|
-
const
|
|
479
|
+
function numeralFor(degreeNumber, lower) {
|
|
480
|
+
const glyph = ROMAN[degreeNumber - 1];
|
|
481
|
+
if (glyph === void 0) {
|
|
482
|
+
throw new InvalidInputError(`no Roman numeral for scale degree ${degreeNumber}`);
|
|
483
|
+
}
|
|
484
|
+
return lower ? glyph.toLowerCase() : glyph;
|
|
485
|
+
}
|
|
486
|
+
function isDiatonicChord(chord, key) {
|
|
487
|
+
return chordPitchClasses(chord).every((pc) => isScaleTone(pc, key));
|
|
488
|
+
}
|
|
489
|
+
var LEADING_TONE_QUALITIES = /* @__PURE__ */ new Set(["dim", "dim7", "m7b5"]);
|
|
490
|
+
function degreeTriad(tones, index) {
|
|
491
|
+
const root = tones[index] ?? 0;
|
|
492
|
+
return {
|
|
493
|
+
third: pitchClassOf((tones[(index + 2) % tones.length] ?? 0) - root),
|
|
494
|
+
fifth: pitchClassOf((tones[(index + 4) % tones.length] ?? 0) - root)
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
function appliedTarget(chord, key) {
|
|
498
|
+
const dominant = isAppliedDominantSonority(chord);
|
|
499
|
+
const leadingTone = LEADING_TONE_QUALITIES.has(chord.quality);
|
|
500
|
+
if (!dominant && !leadingTone) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
const tones = scaleTonesInDegreeOrder(romanReference(key));
|
|
504
|
+
for (let index = 1; index < tones.length; index += 1) {
|
|
505
|
+
const targetRoot = tones[index];
|
|
506
|
+
if (targetRoot === void 0) {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
const triad = degreeTriad(tones, index);
|
|
510
|
+
const expectedRoot = pitchClassOf(targetRoot + (dominant ? 7 : -1));
|
|
511
|
+
if (expectedRoot === pitchClassOf(chord.rootPc)) {
|
|
512
|
+
return { degreeNumber: index + 1, rootPc: targetRoot, lower: triad.third === 3 };
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
function chordToRoman(chord, key, opts = {}) {
|
|
518
|
+
if (opts.applied === true && !isDiatonicChord(chord, key)) {
|
|
519
|
+
const target = appliedTarget(chord, key);
|
|
520
|
+
if (target !== null) {
|
|
521
|
+
const local = chordToRoman(chord, majorKey(target.rootPc));
|
|
522
|
+
return `${local}/${numeralFor(target.degreeNumber, target.lower)}`;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
const { degreeNumber, accidental: spelledAccidental } = romanSpelling(chord.rootPc, key);
|
|
263
526
|
const { lower, suffix } = romanStyle(chord.quality);
|
|
264
|
-
const
|
|
265
|
-
const
|
|
527
|
+
const cased = numeralFor(degreeNumber, lower);
|
|
528
|
+
const accidental = spelledAccidental === "#" && degreeNumber === 7 && loweredDegrees(romanReference(key)).has(7) && (chord.quality === "dim" || chord.quality === "dim7") ? "" : spelledAccidental;
|
|
266
529
|
let inversion = 0;
|
|
267
530
|
if (chord.bassPc !== void 0) {
|
|
268
|
-
const idx = chord.intervals.findIndex((iv) =>
|
|
531
|
+
const idx = chord.intervals.findIndex((iv) => pitchClassOf(chord.rootPc + iv) === chord.bassPc);
|
|
269
532
|
if (idx > 0) {
|
|
270
533
|
inversion = idx;
|
|
271
534
|
}
|
|
@@ -280,14 +543,20 @@ function chordToRoman(chord, key) {
|
|
|
280
543
|
}
|
|
281
544
|
|
|
282
545
|
// src/analyze/functional/borrowed.ts
|
|
283
|
-
function
|
|
284
|
-
|
|
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 });
|
|
285
551
|
}
|
|
286
552
|
function isBorrowedChord(chord, key) {
|
|
287
553
|
if (isDiatonic(chord, key)) {
|
|
288
554
|
return false;
|
|
289
555
|
}
|
|
290
|
-
if (
|
|
556
|
+
if (isNeapolitan(chord, key)) {
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
if (isMinorScaleAlteration(chord, key)) {
|
|
291
560
|
return false;
|
|
292
561
|
}
|
|
293
562
|
return isDiatonic(chord, parallelKey(key));
|
|
@@ -296,11 +565,9 @@ function borrowedSource(chord, key) {
|
|
|
296
565
|
if (isDiatonic(chord, key)) {
|
|
297
566
|
return null;
|
|
298
567
|
}
|
|
299
|
-
if (isNeapolitan(chord, key))
|
|
300
|
-
return "neapolitan";
|
|
301
|
-
}
|
|
568
|
+
if (isNeapolitan(chord, key)) return "neapolitan";
|
|
302
569
|
if (isBorrowedChord(chord, key)) {
|
|
303
|
-
return isMinorKey(key) ? "
|
|
570
|
+
return isMinorKey(key) ? "parallelMajor" : "parallelMinor";
|
|
304
571
|
}
|
|
305
572
|
return null;
|
|
306
573
|
}
|
|
@@ -332,20 +599,56 @@ var FUNCTION_BY_OFFSET = [
|
|
|
332
599
|
"dominant"
|
|
333
600
|
// 11 vii
|
|
334
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
|
+
];
|
|
335
616
|
function isMinorKey(key) {
|
|
336
617
|
const hasMinorThird = key.modeMask12 >> 3 & 1;
|
|
337
618
|
const hasMajorThird2 = key.modeMask12 >> 4 & 1;
|
|
338
619
|
return Boolean(hasMinorThird) && !hasMajorThird2;
|
|
339
620
|
}
|
|
340
621
|
function functionOf(chord, key) {
|
|
341
|
-
|
|
622
|
+
const offset = pitchClassOf(chord.rootPc - key.rootPc);
|
|
623
|
+
if (isNeapolitan(chord, key)) {
|
|
624
|
+
return "subdominant";
|
|
625
|
+
}
|
|
626
|
+
if (isAppliedDominant(chord, key)) {
|
|
627
|
+
return "dominant";
|
|
628
|
+
}
|
|
629
|
+
if (!isMinorKey(key) && hasMajorThird(chord) && (offset === 8 || offset === 10)) {
|
|
630
|
+
return "subdominant";
|
|
631
|
+
}
|
|
632
|
+
const functions = isMinorKey(key) ? MINOR_FUNCTION_BY_OFFSET : FUNCTION_BY_OFFSET;
|
|
633
|
+
return functions[offset] ?? "tonic";
|
|
634
|
+
}
|
|
635
|
+
function isAppliedDominant(chord, key) {
|
|
636
|
+
if (isDiatonic(chord, key)) {
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
const root = pitchClassOf(chord.rootPc);
|
|
640
|
+
const resolvesTo = (step) => isScaleTone(pitchClassOf(root + step), key);
|
|
641
|
+
if (isAppliedDominantSonority(chord)) {
|
|
642
|
+
return chord.quality === "maj" ? resolvesTo(5) : resolvesTo(5) || resolvesTo(11);
|
|
643
|
+
}
|
|
644
|
+
return isDiminishedQuality(chord.quality) && resolvesTo(1);
|
|
342
645
|
}
|
|
343
646
|
function isDiatonic(chord, key) {
|
|
344
647
|
return chordPitchClasses(chord).every((pc) => isScaleTone(pc, key));
|
|
345
648
|
}
|
|
346
649
|
function parallelKey(key) {
|
|
347
650
|
return {
|
|
348
|
-
rootPc:
|
|
651
|
+
rootPc: pitchClassOf(key.rootPc),
|
|
349
652
|
modeMask12: isMinorKey(key) ? MAJOR_MASK : NATURAL_MINOR_MASK
|
|
350
653
|
};
|
|
351
654
|
}
|
|
@@ -353,184 +656,59 @@ function isDiminishedQuality(quality) {
|
|
|
353
656
|
return quality === "dim" || quality === "dim7" || quality === "m7b5";
|
|
354
657
|
}
|
|
355
658
|
function hasMajorThird(chord) {
|
|
356
|
-
return chord.intervals.some((interval) =>
|
|
357
|
-
}
|
|
358
|
-
function qualityAwareFunction(chord, key) {
|
|
359
|
-
const offset = mod12(chord.rootPc - key.rootPc);
|
|
360
|
-
if (isDiminishedQuality(chord.quality) && (offset === 11 || offset === 6)) {
|
|
361
|
-
return "dominant";
|
|
362
|
-
}
|
|
363
|
-
if (isNeapolitan(chord, key)) {
|
|
364
|
-
return "subdominant";
|
|
365
|
-
}
|
|
366
|
-
if (!isMinorKey(key) && hasMajorThird(chord) && (offset === 8 || offset === 10)) {
|
|
367
|
-
return "subdominant";
|
|
368
|
-
}
|
|
369
|
-
return functionOf(chord, key);
|
|
659
|
+
return chord.intervals.some((interval) => pitchClassOf(interval) === 4);
|
|
370
660
|
}
|
|
371
|
-
function analyzeChord(chord, key) {
|
|
661
|
+
function analyzeChord(chord, key, opts = {}) {
|
|
372
662
|
const source = borrowedSource(chord, key);
|
|
373
663
|
return {
|
|
374
|
-
function:
|
|
664
|
+
function: functionOf(chord, key),
|
|
375
665
|
borrowed: source !== null,
|
|
376
666
|
source,
|
|
377
|
-
roman: chordToRoman(chord, key)
|
|
667
|
+
roman: chordToRoman(chord, key, opts)
|
|
378
668
|
};
|
|
379
669
|
}
|
|
380
670
|
function secondaryDominant(targetDegree, key) {
|
|
671
|
+
const degreeCount = scaleTonesInDegreeOrder(key).length;
|
|
672
|
+
assertInteger(targetDegree, "targetDegree", 0, degreeCount - 1);
|
|
381
673
|
const targetRoot = degreeRootPc(targetDegree + 1, key);
|
|
382
|
-
return makeChord(
|
|
674
|
+
return makeChord(pitchClassOf(targetRoot + 7), "dom7");
|
|
383
675
|
}
|
|
384
676
|
|
|
385
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
|
+
}
|
|
386
684
|
function detectCadence(from, to, key) {
|
|
387
|
-
const tonic =
|
|
388
|
-
const fromOffset =
|
|
389
|
-
const toOffset =
|
|
685
|
+
const tonic = pitchClassOf(key.rootPc);
|
|
686
|
+
const fromOffset = pitchClassOf(from.rootPc - tonic);
|
|
687
|
+
const toOffset = pitchClassOf(to.rootPc - tonic);
|
|
390
688
|
const deceptiveTargets = isMinorKey(key) ? [8] : [8, 9];
|
|
391
|
-
|
|
689
|
+
const dominant = fromOffset === 7 && hasDominantThird(from);
|
|
690
|
+
const leadingTone = fromOffset === 11 && isLeadingToneDiminished(from);
|
|
691
|
+
if ((dominant || leadingTone) && toOffset === 0) {
|
|
392
692
|
return "authentic";
|
|
393
693
|
}
|
|
394
694
|
if (fromOffset === 5 && toOffset === 0) {
|
|
395
695
|
return "plagal";
|
|
396
696
|
}
|
|
397
|
-
if (
|
|
697
|
+
if (dominant && deceptiveTargets.includes(toOffset)) {
|
|
398
698
|
return "deceptive";
|
|
399
699
|
}
|
|
400
|
-
if (toOffset === 7 && fromOffset !== 7) {
|
|
700
|
+
if (toOffset === 7 && hasDominantThird(to) && fromOffset !== 7) {
|
|
401
701
|
return "half";
|
|
402
702
|
}
|
|
403
703
|
return null;
|
|
404
704
|
}
|
|
405
705
|
|
|
406
|
-
// src/analyze/detect/index.ts
|
|
407
|
-
function pitchClass(value) {
|
|
408
|
-
return (Math.trunc(value) % 12 + 12) % 12;
|
|
409
|
-
}
|
|
410
|
-
function uniquePitchClasses(pitches) {
|
|
411
|
-
return [...new Set(pitches.map(pitchClass))].sort((a, b) => a - b);
|
|
412
|
-
}
|
|
413
|
-
function assertPitches(pitches) {
|
|
414
|
-
assertGenerationBudget(pitches.length, "detection pitches");
|
|
415
|
-
for (let index = 0; index < pitches.length; index += 1) {
|
|
416
|
-
assertFiniteNumber(pitches[index] ?? Number.NaN, `pitches[${index}]`);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
function detectChord(pitches, opts = {}) {
|
|
420
|
-
assertPitches(pitches);
|
|
421
|
-
if (opts.bassPc !== void 0) assertFiniteNumber(opts.bassPc, "bassPc");
|
|
422
|
-
const input = uniquePitchClasses(pitches);
|
|
423
|
-
if (input.length === 0) {
|
|
424
|
-
return [];
|
|
425
|
-
}
|
|
426
|
-
const inputKind = opts.input === void 0 || opts.input === "auto" ? pitches.every((pitch) => Number.isInteger(pitch) && pitch >= 0 && pitch <= 11) ? "pitchClass" : "midi" : opts.input;
|
|
427
|
-
const bassPc = opts.bassPc !== void 0 ? pitchClass(opts.bassPc) : inputKind === "midi" ? pitchClass(pitches.reduce((lowest, pitch) => Math.min(lowest, pitch), Infinity)) : void 0;
|
|
428
|
-
const inputSet = new Set(input);
|
|
429
|
-
const matches = [];
|
|
430
|
-
const qualities = chordQualities();
|
|
431
|
-
for (const rootPc of input) {
|
|
432
|
-
for (const quality of qualities) {
|
|
433
|
-
const chord = makeChord(rootPc, quality);
|
|
434
|
-
const tones = chordPitchClasses(chord);
|
|
435
|
-
const toneSet = new Set(tones);
|
|
436
|
-
const missingPcs = tones.filter((pc) => !inputSet.has(pc));
|
|
437
|
-
const fifthPc = pitchClass(rootPc + 7);
|
|
438
|
-
if (missingPcs.length > 1 || missingPcs.length === 1 && missingPcs[0] !== fifthPc) {
|
|
439
|
-
continue;
|
|
440
|
-
}
|
|
441
|
-
const extraPcs = input.filter((pc) => !toneSet.has(pc));
|
|
442
|
-
const bassIndex = bassPc === void 0 ? -1 : chord.intervals.findIndex((iv) => pitchClass(rootPc + iv) === bassPc);
|
|
443
|
-
const inversion = bassPc === void 0 ? null : bassIndex > 0 ? bassIndex : 0;
|
|
444
|
-
const match = {
|
|
445
|
-
rootPc,
|
|
446
|
-
quality,
|
|
447
|
-
missingPcs,
|
|
448
|
-
extraPcs,
|
|
449
|
-
exact: extraPcs.length === 0 && missingPcs.length === 0,
|
|
450
|
-
inversion
|
|
451
|
-
};
|
|
452
|
-
if (bassPc !== void 0 && bassPc !== rootPc) {
|
|
453
|
-
match.bassPc = bassPc;
|
|
454
|
-
}
|
|
455
|
-
matches.push(match);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
matches.sort((a, b) => {
|
|
459
|
-
if (a.extraPcs.length !== b.extraPcs.length) {
|
|
460
|
-
return a.extraPcs.length - b.extraPcs.length;
|
|
461
|
-
}
|
|
462
|
-
if (a.missingPcs.length !== b.missingPcs.length) {
|
|
463
|
-
return a.missingPcs.length - b.missingPcs.length;
|
|
464
|
-
}
|
|
465
|
-
if (a.inversion === 0 !== (b.inversion === 0)) {
|
|
466
|
-
return a.inversion === 0 ? -1 : 1;
|
|
467
|
-
}
|
|
468
|
-
const aSize = chordPitchClasses(makeChord(a.rootPc, a.quality)).length;
|
|
469
|
-
const bSize = chordPitchClasses(makeChord(b.rootPc, b.quality)).length;
|
|
470
|
-
return bSize - aSize;
|
|
471
|
-
});
|
|
472
|
-
return matches;
|
|
473
|
-
}
|
|
474
|
-
function detectChordBest(pitches, opts = {}) {
|
|
475
|
-
const best = detectChord(pitches, opts)[0];
|
|
476
|
-
if (!best) {
|
|
477
|
-
return null;
|
|
478
|
-
}
|
|
479
|
-
return makeChord(best.rootPc, best.quality, best.bassPc);
|
|
480
|
-
}
|
|
481
|
-
var MINOR_MASK_VARIANTS = [NATURAL_MINOR_MASK, HARMONIC_MINOR_MASK, MELODIC_MINOR_MASK];
|
|
482
|
-
function detectKey(pitches) {
|
|
483
|
-
assertPitches(pitches);
|
|
484
|
-
const input = uniquePitchClasses(pitches);
|
|
485
|
-
if (input.length === 0) {
|
|
486
|
-
return [];
|
|
487
|
-
}
|
|
488
|
-
const counts = /* @__PURE__ */ new Map();
|
|
489
|
-
for (const pc of pitches.map(pitchClass)) {
|
|
490
|
-
counts.set(pc, (counts.get(pc) ?? 0) + 1);
|
|
491
|
-
}
|
|
492
|
-
const total = pitches.length;
|
|
493
|
-
const results = [];
|
|
494
|
-
for (let tonic = 0; tonic < 12; tonic += 1) {
|
|
495
|
-
for (const mode of ["major", "minor"]) {
|
|
496
|
-
const key = mode === "major" ? majorKey(tonic) : minorKey(tonic);
|
|
497
|
-
const masks = mode === "major" ? [MAJOR_MASK] : MINOR_MASK_VARIANTS;
|
|
498
|
-
let inScale = 0;
|
|
499
|
-
let weighted = Number.NEGATIVE_INFINITY;
|
|
500
|
-
for (const mask of masks) {
|
|
501
|
-
let variantInScale = 0;
|
|
502
|
-
let variantWeighted = 0;
|
|
503
|
-
for (const pc of input) {
|
|
504
|
-
const offset = (pc - tonic + 12) % 12;
|
|
505
|
-
if (mask >> offset & 1) {
|
|
506
|
-
variantInScale += 1;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
for (const [pc, count] of counts) {
|
|
510
|
-
const offset = (pc - tonic + 12) % 12;
|
|
511
|
-
if (mask >> offset & 1) {
|
|
512
|
-
variantWeighted += count;
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
if (variantWeighted > weighted) {
|
|
516
|
-
weighted = variantWeighted;
|
|
517
|
-
inScale = variantInScale;
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
weighted += (counts.get(tonic) ?? 0) * 0.5;
|
|
521
|
-
results.push({
|
|
522
|
-
key,
|
|
523
|
-
mode,
|
|
524
|
-
fit: inScale / input.length,
|
|
525
|
-
score: weighted / total
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
results.sort((a, b) => b.score - a.score);
|
|
530
|
-
return results.map(({ key, mode, fit }) => ({ key, mode, fit }));
|
|
531
|
-
}
|
|
532
|
-
|
|
533
706
|
export {
|
|
707
|
+
detectChord,
|
|
708
|
+
detectChordBest,
|
|
709
|
+
detectKey,
|
|
710
|
+
detectKeyFromNotes,
|
|
711
|
+
detectKeyBest,
|
|
534
712
|
romanToChord,
|
|
535
713
|
chordToRoman,
|
|
536
714
|
isMinorKey,
|
|
@@ -541,9 +719,6 @@ export {
|
|
|
541
719
|
secondaryDominant,
|
|
542
720
|
isBorrowedChord,
|
|
543
721
|
borrowedSource,
|
|
544
|
-
detectCadence
|
|
545
|
-
detectChord,
|
|
546
|
-
detectChordBest,
|
|
547
|
-
detectKey
|
|
722
|
+
detectCadence
|
|
548
723
|
};
|
|
549
|
-
//# sourceMappingURL=chunk-
|
|
724
|
+
//# sourceMappingURL=chunk-NNB67XA2.js.map
|