@libraz/libcantus 1.3.0 → 1.4.0
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/NOTICE +63 -0
- package/README.md +3 -1
- package/README_ja.md +13 -11
- package/dist/analyze/index.cjs +7 -7
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +14 -12
- package/dist/analyze/index.d.ts +14 -12
- package/dist/analyze/index.js +6 -6
- package/dist/{chunk-NB25P3EH.js → chunk-2G5MCGYU.js} +33 -11
- package/dist/chunk-2G5MCGYU.js.map +1 -0
- package/dist/{chunk-NM7UAMC3.cjs → chunk-3FDBMIPR.cjs} +64 -64
- package/dist/chunk-3FDBMIPR.cjs.map +1 -0
- package/dist/{chunk-YFA7X2F6.cjs → chunk-3MDOEFIQ.cjs} +411 -327
- package/dist/chunk-3MDOEFIQ.cjs.map +1 -0
- package/dist/{chunk-IPEF6NWB.js → chunk-5MWRBYAT.js} +278 -154
- package/dist/chunk-5MWRBYAT.js.map +1 -0
- package/dist/{chunk-B2PW6EHN.cjs → chunk-6TTBEON3.cjs} +1253 -889
- package/dist/chunk-6TTBEON3.cjs.map +1 -0
- package/dist/{chunk-UMWZ4ZET.js → chunk-7NQ6AL3H.js} +4 -4
- package/dist/chunk-7NQ6AL3H.js.map +1 -0
- package/dist/{chunk-PWC4CRWT.cjs → chunk-E3QO3UET.cjs} +31 -23
- package/dist/chunk-E3QO3UET.cjs.map +1 -0
- package/dist/{chunk-GNVN4PPJ.js → chunk-FJFULJZE.js} +531 -213
- package/dist/chunk-FJFULJZE.js.map +1 -0
- package/dist/{chunk-CPMB7P75.js → chunk-FK3DGAIZ.js} +608 -244
- package/dist/chunk-FK3DGAIZ.js.map +1 -0
- package/dist/chunk-GEDTJHAC.cjs.map +1 -1
- package/dist/{chunk-RFIBF2RR.js → chunk-H7FVMBUC.js} +138 -65
- package/dist/chunk-H7FVMBUC.js.map +1 -0
- package/dist/{chunk-4R2LWV7E.cjs → chunk-HYSVSFNI.cjs} +664 -540
- package/dist/chunk-HYSVSFNI.cjs.map +1 -0
- package/dist/{chunk-35NBV2FT.cjs → chunk-KMYGRFZ6.cjs} +47 -24
- package/dist/chunk-KMYGRFZ6.cjs.map +1 -0
- package/dist/{chunk-IFZDWIKM.cjs → chunk-LQZQDBCD.cjs} +1048 -730
- package/dist/chunk-LQZQDBCD.cjs.map +1 -0
- package/dist/{chunk-PM4XJE2Y.cjs → chunk-P3CPXAJ5.cjs} +76 -54
- package/dist/chunk-P3CPXAJ5.cjs.map +1 -0
- package/dist/{chunk-S5BKZZGM.cjs → chunk-S3QELJUE.cjs} +72 -56
- package/dist/chunk-S3QELJUE.cjs.map +1 -0
- package/dist/{chunk-PUKJP2Z5.js → chunk-TXFWSU5D.js} +31 -8
- package/dist/chunk-TXFWSU5D.js.map +1 -0
- package/dist/{chunk-76ZBTRG2.cjs → chunk-U4EQTDGN.cjs} +258 -218
- package/dist/chunk-U4EQTDGN.cjs.map +1 -0
- package/dist/chunk-UGLFDG7L.cjs.map +1 -1
- package/dist/{chunk-MLBPXA4Z.js → chunk-UTE5A36R.js} +198 -114
- package/dist/chunk-UTE5A36R.js.map +1 -0
- package/dist/{chunk-ZBUROTJQ.js → chunk-UZKQISRF.js} +17 -9
- package/dist/chunk-UZKQISRF.js.map +1 -0
- package/dist/{chunk-7KG3RTYS.js → chunk-VL5NXVCN.js} +281 -155
- package/dist/chunk-VL5NXVCN.js.map +1 -0
- package/dist/{chunk-CGBADIO5.cjs → chunk-WOMCXN2W.cjs} +328 -202
- package/dist/chunk-WOMCXN2W.cjs.map +1 -0
- package/dist/{chunk-3R3QVKY5.cjs → chunk-YDGMWH43.cjs} +397 -324
- package/dist/chunk-YDGMWH43.cjs.map +1 -0
- package/dist/{chunk-AGOVW7U6.js → chunk-YYVXRV3V.js} +72 -56
- package/dist/chunk-YYVXRV3V.js.map +1 -0
- package/dist/{chunk-M5RYMAFI.js → chunk-ZLNSH4UL.js} +71 -31
- package/dist/chunk-ZLNSH4UL.js.map +1 -0
- package/dist/{coerce-DRERzdve.d.cts → coerce-CUhHc3Tu.d.ts} +16 -8
- package/dist/{coerce-DP0vYu-x.d.ts → coerce-Ytt2Eiq9.d.cts} +16 -8
- package/dist/core/index.cjs +5 -5
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +9 -8
- package/dist/core/index.d.ts +9 -8
- package/dist/core/index.js +4 -4
- package/dist/generate/index.cjs +8 -8
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +28 -18
- package/dist/generate/index.d.ts +28 -18
- package/dist/generate/index.js +7 -7
- package/dist/{index-BMweUlm5.d.cts → index-5VJgKz_Q.d.cts} +6 -2
- package/dist/{index-BHIUyjhZ.d.ts → index-BLuDesVG.d.ts} +20 -13
- package/dist/{index-B9QLziDe.d.cts → index-BV6APVa6.d.cts} +20 -13
- package/dist/{index-CL4b4gUQ.d.ts → index-Bf0Sv8a9.d.ts} +6 -2
- package/dist/{index-Dw2DP7vm.d.ts → index-BkT21EMv.d.cts} +3 -2
- package/dist/{index-BL5N2Oyz.d.cts → index-C7KCaTRz.d.ts} +3 -2
- package/dist/{index-Cy15oTvh.d.ts → index-C9MYbuKS.d.cts} +7 -3
- package/dist/{index-Cy15oTvh.d.cts → index-C9MYbuKS.d.ts} +7 -3
- package/dist/{index-CY-3NVfy.d.cts → index-DVInzCDS.d.cts} +53 -29
- package/dist/{index-9eoQs2C1.d.ts → index-Dg-SAfhz.d.ts} +6 -4
- package/dist/{index-zaZdmnU4.d.ts → index-jgeug9EA.d.ts} +53 -29
- package/dist/{index-cHvbYgJI.d.cts → index-pQihL4z7.d.cts} +6 -4
- package/dist/index.cjs +13 -13
- 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 +12 -12
- package/dist/{masks-Cc7-MzzY.d.ts → masks-BnzXnBj0.d.cts} +3 -2
- package/dist/{masks-pADfTOiD.d.cts → masks-tycmkrgs.d.ts} +3 -2
- package/dist/model/index.cjs +12 -12
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +63 -48
- package/dist/model/index.d.ts +63 -48
- package/dist/model/index.js +11 -11
- package/dist/{positional-DJgs6ltD.d.ts → positional-CscpxtlE.d.cts} +3 -2
- package/dist/{positional-DMZer-FM.d.cts → positional-DLnfUgqG.d.ts} +3 -2
- package/dist/{profile-DLdlIpS9.d.ts → profile-B4WqcM4H.d.ts} +25 -17
- package/dist/{profile-CQMEqdH9.d.cts → profile-BJd1XkPB.d.cts} +25 -17
- package/dist/theory/index.cjs +5 -5
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +18 -14
- package/dist/theory/index.d.ts +18 -14
- package/dist/theory/index.js +4 -4
- package/dist/{types-D-XgxGvp.d.cts → types-7mGFwvMm.d.ts} +45 -22
- package/dist/{types-Dh7C064C.d.ts → types-_ofpb2be.d.cts} +45 -22
- package/package.json +13 -13
- package/dist/chunk-35NBV2FT.cjs.map +0 -1
- package/dist/chunk-3R3QVKY5.cjs.map +0 -1
- package/dist/chunk-4R2LWV7E.cjs.map +0 -1
- package/dist/chunk-76ZBTRG2.cjs.map +0 -1
- package/dist/chunk-7KG3RTYS.js.map +0 -1
- package/dist/chunk-AGOVW7U6.js.map +0 -1
- package/dist/chunk-B2PW6EHN.cjs.map +0 -1
- package/dist/chunk-CGBADIO5.cjs.map +0 -1
- package/dist/chunk-CPMB7P75.js.map +0 -1
- package/dist/chunk-GNVN4PPJ.js.map +0 -1
- package/dist/chunk-IFZDWIKM.cjs.map +0 -1
- package/dist/chunk-IPEF6NWB.js.map +0 -1
- package/dist/chunk-M5RYMAFI.js.map +0 -1
- package/dist/chunk-MLBPXA4Z.js.map +0 -1
- package/dist/chunk-NB25P3EH.js.map +0 -1
- package/dist/chunk-NM7UAMC3.cjs.map +0 -1
- package/dist/chunk-PM4XJE2Y.cjs.map +0 -1
- package/dist/chunk-PUKJP2Z5.js.map +0 -1
- package/dist/chunk-PWC4CRWT.cjs.map +0 -1
- package/dist/chunk-RFIBF2RR.js.map +0 -1
- package/dist/chunk-S5BKZZGM.cjs.map +0 -1
- package/dist/chunk-UMWZ4ZET.js.map +0 -1
- package/dist/chunk-YFA7X2F6.cjs.map +0 -1
- package/dist/chunk-ZBUROTJQ.js.map +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
-
sortedNoteEvents
|
|
3
|
-
|
|
2
|
+
sortedNoteEvents,
|
|
3
|
+
soundingIndices
|
|
4
|
+
} from "./chunk-TXFWSU5D.js";
|
|
4
5
|
import {
|
|
5
6
|
DORIAN_MASK,
|
|
6
7
|
HARMONIC_MINOR_MASK,
|
|
@@ -12,7 +13,8 @@ import {
|
|
|
12
13
|
MIXOLYDIAN_MASK,
|
|
13
14
|
NATURAL_MINOR_MASK,
|
|
14
15
|
PHRYGIAN_MASK,
|
|
15
|
-
|
|
16
|
+
QUALITY_INTERVALS,
|
|
17
|
+
appliedChordTarget,
|
|
16
18
|
chordFromSpan,
|
|
17
19
|
chordPitchClasses,
|
|
18
20
|
chordQualities,
|
|
@@ -20,7 +22,6 @@ import {
|
|
|
20
22
|
chordToneSpellings,
|
|
21
23
|
createsVerticalDissonance,
|
|
22
24
|
diatonicTriad,
|
|
23
|
-
hasDominantSonority,
|
|
24
25
|
hasMajorThird,
|
|
25
26
|
heptatonicFrameOf,
|
|
26
27
|
intervalAboveRoot,
|
|
@@ -34,8 +35,10 @@ import {
|
|
|
34
35
|
isScaleTone,
|
|
35
36
|
keyRelationBetween,
|
|
36
37
|
keySignatureFifths,
|
|
38
|
+
keySpelling,
|
|
37
39
|
majorKey,
|
|
38
40
|
makeChord,
|
|
41
|
+
makeSpelledChord,
|
|
39
42
|
minorKey,
|
|
40
43
|
resolveKey,
|
|
41
44
|
scaleOf,
|
|
@@ -45,10 +48,11 @@ import {
|
|
|
45
48
|
spellChord,
|
|
46
49
|
spellPitchClass,
|
|
47
50
|
spelledKeyOf,
|
|
51
|
+
spellingAbove,
|
|
48
52
|
toChordData,
|
|
49
53
|
toKeyScale,
|
|
50
54
|
tonicizableDegrees
|
|
51
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-H7FVMBUC.js";
|
|
52
56
|
import {
|
|
53
57
|
BEAT_EPS,
|
|
54
58
|
InvalidInputError,
|
|
@@ -78,10 +82,12 @@ import {
|
|
|
78
82
|
diatonicLetterOf,
|
|
79
83
|
formatNote,
|
|
80
84
|
isCompound,
|
|
85
|
+
isConsonantInterval,
|
|
81
86
|
meterAt,
|
|
82
87
|
metricGridUnit,
|
|
83
88
|
metricWeight,
|
|
84
89
|
midiToNote,
|
|
90
|
+
naturalPitchClassOf,
|
|
85
91
|
parseInterval,
|
|
86
92
|
pitchClassOf,
|
|
87
93
|
pulseBeats,
|
|
@@ -90,7 +96,7 @@ import {
|
|
|
90
96
|
toNoteData,
|
|
91
97
|
transposeByInterval,
|
|
92
98
|
transposeNote
|
|
93
|
-
} from "./chunk-
|
|
99
|
+
} from "./chunk-YYVXRV3V.js";
|
|
94
100
|
|
|
95
101
|
// src/analyze/detect/modes.ts
|
|
96
102
|
var MODAL_CANDIDATES = Object.freeze([
|
|
@@ -204,6 +210,30 @@ function windowWeights(notes, windowStart, windowEnd, meter) {
|
|
|
204
210
|
return { weights, totalWeight, maxWeight, lowestPitch };
|
|
205
211
|
}
|
|
206
212
|
|
|
213
|
+
// src/analyze/detect/internal.ts
|
|
214
|
+
var MINOR_VARIANTS = [
|
|
215
|
+
{ variant: "natural", mask: NATURAL_MINOR_MASK, scaleName: "naturalMinor" },
|
|
216
|
+
{ variant: "harmonic", mask: HARMONIC_MINOR_MASK, scaleName: "harmonicMinor" },
|
|
217
|
+
{ variant: "melodic", mask: MELODIC_MINOR_MASK, scaleName: "melodicMinor" }
|
|
218
|
+
];
|
|
219
|
+
var MAJOR_VARIANTS = [
|
|
220
|
+
{ variant: "major", mask: MAJOR_MASK, scaleName: "major" }
|
|
221
|
+
];
|
|
222
|
+
var SCALE_NAME_ORDER = [
|
|
223
|
+
"major",
|
|
224
|
+
"naturalMinor",
|
|
225
|
+
"harmonicMinor",
|
|
226
|
+
"melodicMinor",
|
|
227
|
+
"dorian",
|
|
228
|
+
"phrygian",
|
|
229
|
+
"lydian",
|
|
230
|
+
"mixolydian",
|
|
231
|
+
"locrian"
|
|
232
|
+
];
|
|
233
|
+
var EXPLAINED_RIVALS = 3;
|
|
234
|
+
var SCORE_DIGITS = 2;
|
|
235
|
+
var MAX_SCORE_DIGITS = 6;
|
|
236
|
+
|
|
207
237
|
// src/analyze/detect/profiles.ts
|
|
208
238
|
var KRUMHANSL_KESSLER_PROFILE = {
|
|
209
239
|
major: [6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88],
|
|
@@ -302,6 +332,11 @@ function profileScore(dist, vector, tonic) {
|
|
|
302
332
|
}
|
|
303
333
|
|
|
304
334
|
// src/analyze/detect/index.ts
|
|
335
|
+
var CHORD_INPUT_KINDS = [
|
|
336
|
+
"auto",
|
|
337
|
+
"midi",
|
|
338
|
+
"pitchClass"
|
|
339
|
+
];
|
|
305
340
|
function uniquePitchClasses(pitches) {
|
|
306
341
|
return [...new Set(pitches.map(pitchClassOf))].sort((a, b) => a - b);
|
|
307
342
|
}
|
|
@@ -315,12 +350,13 @@ function assertPitches(pitches, name, budget = void 0) {
|
|
|
315
350
|
function detectChord(pitches, opts = {}) {
|
|
316
351
|
const asked = assertOptions(opts, "opts");
|
|
317
352
|
assertPitches(pitches, "chord detection pitches");
|
|
353
|
+
const askedKind = assertOneOf(asked.input ?? "auto", CHORD_INPUT_KINDS, "input");
|
|
318
354
|
if (asked.bassPc !== void 0) assertFiniteNumber(asked.bassPc, "bassPc");
|
|
319
355
|
const input = uniquePitchClasses(pitches);
|
|
320
356
|
if (input.length === 0) {
|
|
321
357
|
return [];
|
|
322
358
|
}
|
|
323
|
-
const inputKind =
|
|
359
|
+
const inputKind = askedKind === "auto" ? pitches.every((pitch) => Number.isInteger(pitch) && pitch >= 0 && pitch <= 11) ? "pitchClass" : "midi" : askedKind;
|
|
324
360
|
const bassPc = asked.bassPc !== void 0 ? pitchClassOf(asked.bassPc) : inputKind === "midi" ? pitchClassOf(pitches.reduce((lowest, pitch) => Math.min(lowest, pitch), Infinity)) : void 0;
|
|
325
361
|
const inputSet = new Set(input);
|
|
326
362
|
const matches = [];
|
|
@@ -339,6 +375,9 @@ function detectChord(pitches, opts = {}) {
|
|
|
339
375
|
if (missingPcs.length > 1 || missingPcs.length === 1 && missingPcs[0] !== fifthPc) {
|
|
340
376
|
continue;
|
|
341
377
|
}
|
|
378
|
+
if (tones.length - missingPcs.length < 2) {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
342
381
|
const extraPcs = input.filter((pc) => !toneSet.has(pc));
|
|
343
382
|
const bassIndex = bassPc === void 0 ? -1 : chord.intervals.findIndex((iv) => pitchClassOf(rootPc + iv) === bassPc);
|
|
344
383
|
const inversion = bassPc === void 0 || bassIndex < 0 ? null : bassIndex;
|
|
@@ -387,31 +426,9 @@ function detectChordBest(pitches, opts = {}) {
|
|
|
387
426
|
}
|
|
388
427
|
return makeChord(best.rootPc, best.quality, best.bassPc);
|
|
389
428
|
}
|
|
390
|
-
var MINOR_VARIANTS = [
|
|
391
|
-
{ variant: "natural", mask: NATURAL_MINOR_MASK, scaleName: "naturalMinor" },
|
|
392
|
-
{ variant: "harmonic", mask: HARMONIC_MINOR_MASK, scaleName: "harmonicMinor" },
|
|
393
|
-
{ variant: "melodic", mask: MELODIC_MINOR_MASK, scaleName: "melodicMinor" }
|
|
394
|
-
];
|
|
395
|
-
var MAJOR_VARIANTS = [
|
|
396
|
-
{ variant: "major", mask: MAJOR_MASK, scaleName: "major" }
|
|
397
|
-
];
|
|
398
|
-
var SCALE_NAME_ORDER = [
|
|
399
|
-
"major",
|
|
400
|
-
"naturalMinor",
|
|
401
|
-
"harmonicMinor",
|
|
402
|
-
"melodicMinor",
|
|
403
|
-
"dorian",
|
|
404
|
-
"phrygian",
|
|
405
|
-
"lydian",
|
|
406
|
-
"mixolydian",
|
|
407
|
-
"locrian"
|
|
408
|
-
];
|
|
409
429
|
function scaleNameRank(name) {
|
|
410
430
|
return SCALE_NAME_ORDER.indexOf(name);
|
|
411
431
|
}
|
|
412
|
-
var EXPLAINED_RIVALS = 3;
|
|
413
|
-
var SCORE_DIGITS = 2;
|
|
414
|
-
var MAX_SCORE_DIGITS = 6;
|
|
415
432
|
function quoteScores(rival, winner) {
|
|
416
433
|
let digits = SCORE_DIGITS;
|
|
417
434
|
while (digits < MAX_SCORE_DIGITS && rival.toFixed(digits) === winner.toFixed(digits)) {
|
|
@@ -629,13 +646,14 @@ var CHORD_SHAPE = {
|
|
|
629
646
|
french: { rootAboveBass: 6, quality: "7b5", intervals: [0, 4, 6, 10] },
|
|
630
647
|
german: { rootAboveBass: 0, quality: "dom7", intervals: [0, 4, 7, 10] }
|
|
631
648
|
};
|
|
632
|
-
var
|
|
649
|
+
var KIND_SYMBOLS = [
|
|
633
650
|
["It6", "italian"],
|
|
634
651
|
["Fr6", "french"],
|
|
635
652
|
["Ger6", "german"],
|
|
636
653
|
// The German sixth's figures over its bass are 6/5; both spellings are in use.
|
|
637
654
|
["Ger65", "german"]
|
|
638
|
-
]
|
|
655
|
+
];
|
|
656
|
+
var KIND_BY_SYMBOL = new Map(KIND_SYMBOLS);
|
|
639
657
|
var SYMBOL_BY_KIND = {
|
|
640
658
|
italian: "It6",
|
|
641
659
|
french: "Fr6",
|
|
@@ -660,14 +678,16 @@ function augmentedSixthOn(kind, keyLike) {
|
|
|
660
678
|
const shape = CHORD_SHAPE[checked];
|
|
661
679
|
const { scale: key, tonic } = resolveKey(keyLike);
|
|
662
680
|
const bassPc = bassPcOf(key);
|
|
663
|
-
const chord = makeChord(pitchClassOf(bassPc + shape.rootAboveBass), shape.quality, bassPc);
|
|
664
|
-
chord.intervals = [...shape.intervals];
|
|
665
681
|
const bass = transposeByInterval(tonic, BASS_INTERVAL);
|
|
666
682
|
const tones = spelledInChordOrder(checked, tonic);
|
|
667
683
|
const [root = bass] = tones;
|
|
668
|
-
chord
|
|
669
|
-
|
|
670
|
-
|
|
684
|
+
const chord = makeSpelledChord(pitchClassOf(bassPc + shape.rootAboveBass), shape.quality, {
|
|
685
|
+
bassPc,
|
|
686
|
+
rootSpelling: bareSpelling(root),
|
|
687
|
+
bassSpelling: bareSpelling(bass),
|
|
688
|
+
toneSpellings: tones.map(bareSpelling)
|
|
689
|
+
});
|
|
690
|
+
chord.intervals = [...shape.intervals];
|
|
671
691
|
return chord;
|
|
672
692
|
}
|
|
673
693
|
function kindOfPitchClasses(pcs, bass) {
|
|
@@ -1009,14 +1029,6 @@ function borrowedSource(chord, key) {
|
|
|
1009
1029
|
function isTonicizableRoot(rootPc, key) {
|
|
1010
1030
|
return tonicizableDegrees(key).some((degree) => degree.rootPc === pitchClassOf(rootPc));
|
|
1011
1031
|
}
|
|
1012
|
-
function appliedTarget(chord, key) {
|
|
1013
|
-
const dominant = hasDominantSonority(chord);
|
|
1014
|
-
const leadingTone = LEADING_TONE_QUALITIES.has(chord.quality);
|
|
1015
|
-
if (!dominant && !leadingTone) {
|
|
1016
|
-
return null;
|
|
1017
|
-
}
|
|
1018
|
-
return appliedDominantTarget(chord.rootPc + (dominant ? 5 : 1), key);
|
|
1019
|
-
}
|
|
1020
1032
|
function isAppliedDominant(chord, key) {
|
|
1021
1033
|
if (isDiatonicChord(chord, key)) {
|
|
1022
1034
|
return false;
|
|
@@ -1028,7 +1040,7 @@ function isAppliedDominant(chord, key) {
|
|
|
1028
1040
|
return pointsAtTonicizableDegree(chord, key);
|
|
1029
1041
|
}
|
|
1030
1042
|
function pointsAtTonicizableDegree(chord, key) {
|
|
1031
|
-
if (
|
|
1043
|
+
if (appliedChordTarget(chord, key) !== null) {
|
|
1032
1044
|
return true;
|
|
1033
1045
|
}
|
|
1034
1046
|
return soundsDominantSeventh(chord) && isTonicizableRoot(pitchClassOf(chord.rootPc - 1), key);
|
|
@@ -1071,7 +1083,8 @@ function seventhQuality(base, halfDim) {
|
|
|
1071
1083
|
}
|
|
1072
1084
|
return base === "maj" ? "dom7" : "min7";
|
|
1073
1085
|
}
|
|
1074
|
-
var
|
|
1086
|
+
var INVERSION_FIGURE_LIST = ["6", "64", "65", "43", "42", "2", "7"];
|
|
1087
|
+
var INVERSION_FIGURES = new Set(INVERSION_FIGURE_LIST);
|
|
1075
1088
|
var ROMAN_STYLE = {
|
|
1076
1089
|
maj: { lower: false, suffix: "" },
|
|
1077
1090
|
min: { lower: true, suffix: "" },
|
|
@@ -1143,7 +1156,12 @@ function parseInversion(figures) {
|
|
|
1143
1156
|
}
|
|
1144
1157
|
return { inversion: 0, seventh: false };
|
|
1145
1158
|
}
|
|
1146
|
-
function
|
|
1159
|
+
function numeralRootSpelling(tonic, degreeNumber, rootPc) {
|
|
1160
|
+
const spelling = spellingAbove(tonic, degreeNumber - 1, rootPc);
|
|
1161
|
+
return Math.abs(spelling.alter) <= 2 ? spelling : void 0;
|
|
1162
|
+
}
|
|
1163
|
+
function parseSimpleRoman(text, resolved) {
|
|
1164
|
+
const key = resolved.scale;
|
|
1147
1165
|
const match = /^([b#]?)([iIvV]+)(.*)$/.exec(text.trim());
|
|
1148
1166
|
if (!match) {
|
|
1149
1167
|
throw new InvalidInputError(`Invalid Roman numeral: ${text}`);
|
|
@@ -1167,8 +1185,17 @@ function parseSimpleRoman(text, key) {
|
|
|
1167
1185
|
);
|
|
1168
1186
|
const rootForQuality = (quality2) => accidental === 0 && degreeNumber === 7 && loweredDegrees(frame).has(7) && LEADING_TONE_QUALITIES.has(quality2) ? pitchClassOf(diatonicRootPc + 1) : diatonicRootPc;
|
|
1169
1187
|
const canonical = SUFFIX_QUALITY.get(`${isUpper ? "u" : "l"}:${suffix}`);
|
|
1188
|
+
const parsed = (quality2, inversion2) => {
|
|
1189
|
+
const rootPc = rootForQuality(quality2);
|
|
1190
|
+
return {
|
|
1191
|
+
rootPc,
|
|
1192
|
+
rootSpelling: numeralRootSpelling(resolved.tonic, degreeNumber, rootPc),
|
|
1193
|
+
quality: quality2,
|
|
1194
|
+
inversion: inversion2
|
|
1195
|
+
};
|
|
1196
|
+
};
|
|
1170
1197
|
if (canonical !== void 0) {
|
|
1171
|
-
return
|
|
1198
|
+
return parsed(canonical, 0);
|
|
1172
1199
|
}
|
|
1173
1200
|
const parsedSuffix = /^(o|°|dim|ø|\+|aug)?(maj7|M7)?(65|64|43|42|7|6|2)?$/.exec(suffix);
|
|
1174
1201
|
if (!parsedSuffix) {
|
|
@@ -1207,20 +1234,23 @@ function parseSimpleRoman(text, key) {
|
|
|
1207
1234
|
} else {
|
|
1208
1235
|
quality = base;
|
|
1209
1236
|
}
|
|
1210
|
-
return
|
|
1237
|
+
return parsed(quality, inversion);
|
|
1211
1238
|
}
|
|
1212
1239
|
var NEAPOLITAN_SIXTH = "N6";
|
|
1213
1240
|
function chromaticSymbolChord(text, key) {
|
|
1214
1241
|
if (text === NEAPOLITAN_SIXTH) {
|
|
1215
1242
|
const rootPc = pitchClassOf(key.scale.rootPc + 1);
|
|
1216
|
-
return
|
|
1243
|
+
return makeSpelledChord(rootPc, "maj", {
|
|
1244
|
+
bassPc: pitchClassOf(rootPc + 4),
|
|
1245
|
+
rootSpelling: numeralRootSpelling(key.tonic, 2, rootPc)
|
|
1246
|
+
});
|
|
1217
1247
|
}
|
|
1218
1248
|
return augmentedSixthFromSymbol(text, key);
|
|
1219
1249
|
}
|
|
1220
|
-
function tonicizedKey(targetRoot, key) {
|
|
1250
|
+
function tonicizedKey(targetRoot, key, written) {
|
|
1221
1251
|
return resolveKey({
|
|
1222
1252
|
scale: majorKey(targetRoot),
|
|
1223
|
-
tonic: spellPitchClass(targetRoot, key.tonic, key.scale)
|
|
1253
|
+
tonic: written ?? spellPitchClass(targetRoot, key.tonic, key.scale)
|
|
1224
1254
|
});
|
|
1225
1255
|
}
|
|
1226
1256
|
function augmentedSixthTarget(chord, key) {
|
|
@@ -1238,30 +1268,34 @@ function augmentedSixthTarget(chord, key) {
|
|
|
1238
1268
|
return null;
|
|
1239
1269
|
}
|
|
1240
1270
|
function chordFromParsed(parsed) {
|
|
1241
|
-
const
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1271
|
+
const bassInterval = parsed.inversion > 0 ? QUALITY_INTERVALS[parsed.quality][parsed.inversion] : void 0;
|
|
1272
|
+
return makeSpelledChord(parsed.rootPc, parsed.quality, {
|
|
1273
|
+
bassPc: bassInterval === void 0 ? void 0 : pitchClassOf(parsed.rootPc + bassInterval),
|
|
1274
|
+
rootSpelling: parsed.rootSpelling
|
|
1275
|
+
});
|
|
1246
1276
|
}
|
|
1247
1277
|
function romanToChord(text, key) {
|
|
1248
1278
|
const resolved = resolveKey(key);
|
|
1249
|
-
|
|
1279
|
+
if (typeof text !== "string") {
|
|
1280
|
+
throw new InvalidInputError(
|
|
1281
|
+
`Roman numeral must be a string; received ${describeRejected(text)}`
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1250
1284
|
const trimmed = text.trim().replace(/(\d)\/(?=\d)/g, "$1");
|
|
1251
1285
|
const slash = trimmed.indexOf("/");
|
|
1252
1286
|
if (slash >= 0) {
|
|
1253
1287
|
const applied = trimmed.slice(0, slash);
|
|
1254
1288
|
const target = trimmed.slice(slash + 1);
|
|
1255
|
-
const
|
|
1256
|
-
const localKey = tonicizedKey(
|
|
1257
|
-
return chromaticSymbolChord(applied, localKey) ?? chordFromParsed(parseSimpleRoman(applied, localKey
|
|
1289
|
+
const targetNumeral = parseSimpleRoman(target, resolved);
|
|
1290
|
+
const localKey = tonicizedKey(targetNumeral.rootPc, resolved, targetNumeral.rootSpelling);
|
|
1291
|
+
return chromaticSymbolChord(applied, localKey) ?? chordFromParsed(parseSimpleRoman(applied, localKey));
|
|
1258
1292
|
}
|
|
1259
|
-
return chromaticSymbolChord(trimmed, resolved) ?? chordFromParsed(parseSimpleRoman(trimmed,
|
|
1293
|
+
return chromaticSymbolChord(trimmed, resolved) ?? chordFromParsed(parseSimpleRoman(trimmed, resolved));
|
|
1260
1294
|
}
|
|
1261
1295
|
function romanStyle(quality) {
|
|
1262
1296
|
return ROMAN_STYLE[quality];
|
|
1263
1297
|
}
|
|
1264
|
-
function romanSpelling(rootPc, key) {
|
|
1298
|
+
function romanSpelling(rootPc, key, quality) {
|
|
1265
1299
|
const tones = scaleTonesInDegreeOrder(romanReference(key));
|
|
1266
1300
|
const diatonic = tones.indexOf(pitchClassOf(rootPc));
|
|
1267
1301
|
if (diatonic >= 0) {
|
|
@@ -1273,7 +1307,7 @@ function romanSpelling(rootPc, key) {
|
|
|
1273
1307
|
return { degreeNumber: i + 1, accidental: "#" };
|
|
1274
1308
|
}
|
|
1275
1309
|
}
|
|
1276
|
-
for (let i = 0; i < tones.length; i += 1) {
|
|
1310
|
+
for (let i = 0; i < tones.length && !LEADING_TONE_QUALITIES.has(quality); i += 1) {
|
|
1277
1311
|
const degreeNumber2 = i + 1;
|
|
1278
1312
|
if (degreeNumber2 === 1 || degreeNumber2 === 5) {
|
|
1279
1313
|
continue;
|
|
@@ -1319,8 +1353,9 @@ function baseMarker(quality) {
|
|
|
1319
1353
|
}
|
|
1320
1354
|
var TRIAD_FIGURES = { 1: "6", 2: "64" };
|
|
1321
1355
|
var SEVENTH_FIGURES = { 1: "65", 2: "43", 3: "42" };
|
|
1322
|
-
var
|
|
1323
|
-
var
|
|
1356
|
+
var TRIAD_FIGURE_QUALITY_NAMES = ["maj", "min", "dim", "aug"];
|
|
1357
|
+
var TRIAD_FIGURE_QUALITIES = new Set(TRIAD_FIGURE_QUALITY_NAMES);
|
|
1358
|
+
var SEVENTH_FIGURE_QUALITY_NAMES = [
|
|
1324
1359
|
"maj7",
|
|
1325
1360
|
"min7",
|
|
1326
1361
|
"dom7",
|
|
@@ -1329,7 +1364,8 @@ var SEVENTH_FIGURE_QUALITIES = /* @__PURE__ */ new Set([
|
|
|
1329
1364
|
"minMaj7",
|
|
1330
1365
|
"aug7",
|
|
1331
1366
|
"augMaj7"
|
|
1332
|
-
]
|
|
1367
|
+
];
|
|
1368
|
+
var SEVENTH_FIGURE_QUALITIES = new Set(SEVENTH_FIGURE_QUALITY_NAMES);
|
|
1333
1369
|
function numeralFor(degreeNumber, lower) {
|
|
1334
1370
|
const glyph = ROMAN[degreeNumber - 1];
|
|
1335
1371
|
if (glyph === void 0) {
|
|
@@ -1340,6 +1376,47 @@ function numeralFor(degreeNumber, lower) {
|
|
|
1340
1376
|
function chordToRoman(chord, key, opts = {}) {
|
|
1341
1377
|
return renderRoman(toChordData(chord), key, assertOptions(opts, "opts")).roman;
|
|
1342
1378
|
}
|
|
1379
|
+
function readNumeral(chord, resolved, applied) {
|
|
1380
|
+
const key = resolved.scale;
|
|
1381
|
+
const written = writtenRoot(chord);
|
|
1382
|
+
if (applied) {
|
|
1383
|
+
const target = augmentedSixthTarget(chord, resolved) ?? (isAppliedDominant(chord, key) ? appliedChordTarget(chord, key) : null);
|
|
1384
|
+
if (target !== null && leadsByLetter(chord, written, target, resolved)) {
|
|
1385
|
+
return { kind: "applied", target };
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
return {
|
|
1389
|
+
kind: "root",
|
|
1390
|
+
...writtenSpelling(chord.rootPc, written, resolved) ?? romanSpelling(chord.rootPc, key, chord.quality)
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
function writtenRoot(chord) {
|
|
1394
|
+
const hint = chord.rootSpelling;
|
|
1395
|
+
if (typeof hint !== "object" || hint === null || !Number.isInteger(hint.letter) || hint.letter < 0 || hint.letter > 6 || !Number.isInteger(hint.alter) || pitchClassOf(naturalPitchClassOf(hint.letter) + hint.alter) !== pitchClassOf(chord.rootPc)) {
|
|
1396
|
+
return void 0;
|
|
1397
|
+
}
|
|
1398
|
+
return hint;
|
|
1399
|
+
}
|
|
1400
|
+
function writtenSpelling(rootPc, written, resolved) {
|
|
1401
|
+
const tones = scaleTonesInDegreeOrder(romanReference(resolved.scale));
|
|
1402
|
+
if (written === void 0 || tones.includes(pitchClassOf(rootPc))) {
|
|
1403
|
+
return void 0;
|
|
1404
|
+
}
|
|
1405
|
+
const degreeNumber = diatonicLetterOf(written.letter - resolved.tonic.letter) + 1;
|
|
1406
|
+
const degreePc = tones[degreeNumber - 1];
|
|
1407
|
+
if (degreePc === void 0) {
|
|
1408
|
+
return void 0;
|
|
1409
|
+
}
|
|
1410
|
+
const offset = pitchClassOf(rootPc - degreePc + 6) - 6;
|
|
1411
|
+
return Math.abs(offset) > 1 ? void 0 : { degreeNumber, accidental: offset < 0 ? "b" : offset > 0 ? "#" : "" };
|
|
1412
|
+
}
|
|
1413
|
+
function leadsByLetter(chord, written, target, resolved) {
|
|
1414
|
+
if (written === void 0 || !LEADING_TONE_QUALITIES.has(chord.quality)) {
|
|
1415
|
+
return true;
|
|
1416
|
+
}
|
|
1417
|
+
const targetLetter = spellPitchClass(target.rootPc, resolved.tonic, resolved.scale).letter;
|
|
1418
|
+
return diatonicLetterOf(targetLetter - written.letter) === 1;
|
|
1419
|
+
}
|
|
1343
1420
|
function renderRoman(chord, keyLike, opts) {
|
|
1344
1421
|
const resolved = resolveKey(keyLike);
|
|
1345
1422
|
const key = resolved.scale;
|
|
@@ -1347,18 +1424,16 @@ function renderRoman(chord, keyLike, opts) {
|
|
|
1347
1424
|
if (augmented !== null) {
|
|
1348
1425
|
return { roman: augmented, derivation: { kind: "augmentedSixth" } };
|
|
1349
1426
|
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
};
|
|
1359
|
-
}
|
|
1427
|
+
const reading = readNumeral(chord, resolved, opts.applied === true);
|
|
1428
|
+
if (reading.kind === "applied") {
|
|
1429
|
+
const local = renderRoman(chord, tonicizedKey(reading.target.rootPc, resolved), {}).roman;
|
|
1430
|
+
const numeral = numeralFor(reading.target.degreeNumber, reading.target.lower);
|
|
1431
|
+
return {
|
|
1432
|
+
roman: `${local}/${numeral}`,
|
|
1433
|
+
derivation: { kind: "applied", target: numeral }
|
|
1434
|
+
};
|
|
1360
1435
|
}
|
|
1361
|
-
const { degreeNumber, accidental: spelledAccidental } =
|
|
1436
|
+
const { degreeNumber, accidental: spelledAccidental } = reading;
|
|
1362
1437
|
const { lower, suffix } = romanStyle(chord.quality);
|
|
1363
1438
|
const cased = numeralFor(degreeNumber, lower);
|
|
1364
1439
|
const accidental = seventhDegreeAccidental(spelledAccidental, degreeNumber, chord.quality, key);
|
|
@@ -1405,7 +1480,15 @@ function renderRoman(chord, keyLike, opts) {
|
|
|
1405
1480
|
}
|
|
1406
1481
|
};
|
|
1407
1482
|
}
|
|
1408
|
-
var DEGREE_NAMES = [
|
|
1483
|
+
var DEGREE_NAMES = [
|
|
1484
|
+
"first",
|
|
1485
|
+
"second",
|
|
1486
|
+
"third",
|
|
1487
|
+
"fourth",
|
|
1488
|
+
"fifth",
|
|
1489
|
+
"sixth",
|
|
1490
|
+
"seventh"
|
|
1491
|
+
];
|
|
1409
1492
|
function describeDegree(degreeNumber, accidental, literal) {
|
|
1410
1493
|
const name = DEGREE_NAMES[degreeNumber - 1] ?? `${degreeNumber}th`;
|
|
1411
1494
|
if (accidental !== literal) {
|
|
@@ -1544,18 +1627,19 @@ function isMinorKey(key) {
|
|
|
1544
1627
|
return isMinorScale(toKeyScale(key));
|
|
1545
1628
|
}
|
|
1546
1629
|
function functionOf(chord, key) {
|
|
1547
|
-
return functionWithReason(toChordData(chord),
|
|
1630
|
+
return functionWithReason(toChordData(chord), resolveKey(key)).function;
|
|
1548
1631
|
}
|
|
1549
1632
|
function degreeFunction(chord, key) {
|
|
1550
1633
|
const functions = isMinorScale(key) ? MINOR_FUNCTION_BY_OFFSET : FUNCTION_BY_OFFSET;
|
|
1551
1634
|
return functions[pitchClassOf(chord.rootPc - key.rootPc)] ?? "tonic";
|
|
1552
1635
|
}
|
|
1553
|
-
function functionWithReason(chord,
|
|
1636
|
+
function functionWithReason(chord, resolved) {
|
|
1637
|
+
const key = resolved.scale;
|
|
1554
1638
|
const offset = pitchClassOf(chord.rootPc - key.rootPc);
|
|
1555
1639
|
if (isNeapolitanChordOf(chord, key)) {
|
|
1556
1640
|
return { function: "subdominant", reason: "neapolitan" };
|
|
1557
1641
|
}
|
|
1558
|
-
if (augmentedSixthKindOf(chord,
|
|
1642
|
+
if (augmentedSixthKindOf(chord, resolved) !== null) {
|
|
1559
1643
|
return { function: "subdominant", reason: "augmentedSixth" };
|
|
1560
1644
|
}
|
|
1561
1645
|
if (isAppliedDominant(chord, key)) {
|
|
@@ -1581,7 +1665,8 @@ function describeChord(fn, reason, roman, source) {
|
|
|
1581
1665
|
const head = reason === "degree" ? `${capitalize(fn)}: ${roman} takes the ${fn} function of its degree in the key` : `${capitalize(fn)}: ${roman} is ${FUNCTION_REASON_PHRASE[reason]}`;
|
|
1582
1666
|
return source === null || reason === "neapolitan" ? head : `${head}, ${BORROWED_SOURCE_PHRASE[source]}`;
|
|
1583
1667
|
}
|
|
1584
|
-
function functionAlternatives(chord,
|
|
1668
|
+
function functionAlternatives(chord, resolved, reason, accepted) {
|
|
1669
|
+
const key = resolved.scale;
|
|
1585
1670
|
const out = [];
|
|
1586
1671
|
if (reason === "degree") {
|
|
1587
1672
|
if (pointsAtTonicizableDegree(chord, key) && isDiatonicChord(chord, key)) {
|
|
@@ -1599,7 +1684,7 @@ function functionAlternatives(chord, key, reason, accepted) {
|
|
|
1599
1684
|
});
|
|
1600
1685
|
}
|
|
1601
1686
|
}
|
|
1602
|
-
if (augmentedSixthKindOf(chord,
|
|
1687
|
+
if (augmentedSixthKindOf(chord, resolved) === "german") {
|
|
1603
1688
|
out.push({
|
|
1604
1689
|
label: "tritone substitute",
|
|
1605
1690
|
reason: "The German sixth sounds a dominant seventh, but the lowered submediant under it fixes the reading as an augmented sixth"
|
|
@@ -1616,10 +1701,10 @@ function parallelKey(key) {
|
|
|
1616
1701
|
function analyzeChord(chord, key, opts = {}) {
|
|
1617
1702
|
const asked = assertOptions(opts, "opts");
|
|
1618
1703
|
const data = toChordData(chord);
|
|
1619
|
-
const
|
|
1620
|
-
const source = borrowedSourceOf(data, scale);
|
|
1621
|
-
const { function: fn, reason } = functionWithReason(data,
|
|
1622
|
-
const roman = renderRoman(data,
|
|
1704
|
+
const resolved = resolveKey(key);
|
|
1705
|
+
const source = borrowedSourceOf(data, resolved.scale);
|
|
1706
|
+
const { function: fn, reason } = functionWithReason(data, resolved);
|
|
1707
|
+
const roman = renderRoman(data, resolved, asked).roman;
|
|
1623
1708
|
return {
|
|
1624
1709
|
function: fn,
|
|
1625
1710
|
borrowed: source !== null,
|
|
@@ -1627,43 +1712,41 @@ function analyzeChord(chord, key, opts = {}) {
|
|
|
1627
1712
|
roman,
|
|
1628
1713
|
rationale: describeChord(fn, reason, roman, source),
|
|
1629
1714
|
alternatives: asked.alternatives === true ? [
|
|
1630
|
-
...functionAlternatives(data,
|
|
1631
|
-
...romanAlternatives(data,
|
|
1715
|
+
...functionAlternatives(data, resolved, reason, fn),
|
|
1716
|
+
...romanAlternatives(data, resolved, asked)
|
|
1632
1717
|
] : []
|
|
1633
1718
|
};
|
|
1634
1719
|
}
|
|
1635
1720
|
function secondaryDominant(targetDegree, key) {
|
|
1636
|
-
const
|
|
1721
|
+
const resolved = resolveKey(key);
|
|
1722
|
+
const scale = resolved.scale;
|
|
1637
1723
|
const degreeCount = scaleTonesInDegreeOrder(scale).length;
|
|
1638
1724
|
assertInteger(targetDegree, "targetDegree", 1, degreeCount);
|
|
1639
1725
|
const targetRoot = degreeRootPc(targetDegree, scale);
|
|
1640
|
-
|
|
1726
|
+
const rootSpelling = transposeByInterval(keySpelling(targetRoot, resolved), "P5");
|
|
1727
|
+
return makeSpelledChord(pitchClassOf(targetRoot + 7), "dom7", { rootSpelling });
|
|
1641
1728
|
}
|
|
1642
1729
|
function secondaryDominantOf(target) {
|
|
1643
1730
|
const data = toChordData(target);
|
|
1644
1731
|
assertFiniteNumber(data.rootPc, "target chord rootPc");
|
|
1645
|
-
const
|
|
1646
|
-
|
|
1647
|
-
const moved = transposeNote(data.rootSpelling, 7);
|
|
1648
|
-
dominant.rootSpelling = { letter: moved.letter, alter: moved.alter };
|
|
1649
|
-
}
|
|
1650
|
-
return dominant;
|
|
1732
|
+
const rootSpelling = data.rootSpelling === void 0 ? void 0 : transposeNote(data.rootSpelling, 7);
|
|
1733
|
+
return makeSpelledChord(pitchClassOf(data.rootPc + 7), "dom7", { rootSpelling });
|
|
1651
1734
|
}
|
|
1652
1735
|
|
|
1653
1736
|
// src/analyze/functional/pivot.ts
|
|
1654
1737
|
var HEPTATONIC_DEGREES = 7;
|
|
1655
1738
|
function pivotChords(from, to) {
|
|
1656
|
-
return pivotsBetween(
|
|
1739
|
+
return pivotsBetween(resolveKey(from), resolveKey(to));
|
|
1657
1740
|
}
|
|
1658
1741
|
function pivotsBetween(from, to) {
|
|
1659
|
-
if (scaleTonesInDegreeOrder(from).length !== HEPTATONIC_DEGREES) {
|
|
1742
|
+
if (scaleTonesInDegreeOrder(from.scale).length !== HEPTATONIC_DEGREES) {
|
|
1660
1743
|
return [];
|
|
1661
1744
|
}
|
|
1662
1745
|
const pivots = [];
|
|
1663
1746
|
const seen = /* @__PURE__ */ new Set();
|
|
1664
1747
|
for (let degree = 1; degree <= HEPTATONIC_DEGREES; degree += 1) {
|
|
1665
1748
|
const chord = stackedTriad(degree, from);
|
|
1666
|
-
if (chord === null || !isDiatonicChord(chord, to)) {
|
|
1749
|
+
if (chord === null || !isDiatonicChord(chord, to.scale)) {
|
|
1667
1750
|
continue;
|
|
1668
1751
|
}
|
|
1669
1752
|
const identity = `${chord.rootPc}:${chord.quality}`;
|
|
@@ -1714,6 +1797,14 @@ function firstSoundingBeat(notes, fallback = 0) {
|
|
|
1714
1797
|
}
|
|
1715
1798
|
return Number.isFinite(first) ? first : fallback;
|
|
1716
1799
|
}
|
|
1800
|
+
function resolveSpanEnd(totalBeats, startBeat, soundingEnd, name) {
|
|
1801
|
+
const given = totalBeats ?? null;
|
|
1802
|
+
if (given === null) {
|
|
1803
|
+
return Math.max(startBeat, soundingEnd);
|
|
1804
|
+
}
|
|
1805
|
+
assertRange(given, Math.min(0, startBeat), Number.MAX_SAFE_INTEGER, name);
|
|
1806
|
+
return Math.max(startBeat, given);
|
|
1807
|
+
}
|
|
1717
1808
|
function lastBarOf(meter, spanStart, spanEnd) {
|
|
1718
1809
|
const firstBar = barIndexAt(spanStart, meter);
|
|
1719
1810
|
const bar = barIndexAt(Math.max(spanStart, spanEnd), meter);
|
|
@@ -1795,6 +1886,16 @@ function gridOriginOf(firstOnset, slotBeats) {
|
|
|
1795
1886
|
function gridForNotes(notes, slotBeats) {
|
|
1796
1887
|
return gridOriginOf(firstSoundingBeat(notes), slotBeats);
|
|
1797
1888
|
}
|
|
1889
|
+
function latestSoundingEnd(notes, fallback = 0) {
|
|
1890
|
+
if (notes.length === 0) {
|
|
1891
|
+
return fallback;
|
|
1892
|
+
}
|
|
1893
|
+
let end = Number.NEGATIVE_INFINITY;
|
|
1894
|
+
for (const note of notes) {
|
|
1895
|
+
end = Math.max(end, note.startBeat + note.durationBeat);
|
|
1896
|
+
}
|
|
1897
|
+
return end;
|
|
1898
|
+
}
|
|
1798
1899
|
function barGridStart(firstOnset, meter) {
|
|
1799
1900
|
return gridOriginOf(firstOnset, beatsPerBarAt(firstOnset, meter)).startBeat;
|
|
1800
1901
|
}
|
|
@@ -2085,9 +2186,13 @@ function keyTimelineFromNotes(notes, opts = {}) {
|
|
|
2085
2186
|
const budget = opts.budget;
|
|
2086
2187
|
assertNoteEvents(notes, "key timeline notes", { allowNonPositiveDuration: true, budget });
|
|
2087
2188
|
const sounding = notes.filter((note) => note.durationBeat > 0);
|
|
2088
|
-
const
|
|
2089
|
-
|
|
2090
|
-
|
|
2189
|
+
const totalBeats = opts.totalBeats ?? latestSoundingEnd(sounding);
|
|
2190
|
+
assertRange(
|
|
2191
|
+
totalBeats,
|
|
2192
|
+
Math.min(0, firstSoundingBeat(sounding)),
|
|
2193
|
+
Number.MAX_SAFE_INTEGER,
|
|
2194
|
+
"key timeline totalBeats"
|
|
2195
|
+
);
|
|
2091
2196
|
const slotBeats = Math.min(minKeyBeats, expectedKeyBeats);
|
|
2092
2197
|
const { origin, startBeat: musicStart } = gridForNotes(sounding, slotBeats);
|
|
2093
2198
|
const slotCount = Math.max(0, Math.ceil((totalBeats - origin) / slotBeats - BEAT_EPS));
|
|
@@ -2223,9 +2328,10 @@ function detectModulations(chords, opts = {}) {
|
|
|
2223
2328
|
opts.budget
|
|
2224
2329
|
);
|
|
2225
2330
|
const sorted = [...chords].filter((segment) => segment.endBeat > segment.startBeat).sort((a, b) => a.startBeat - b.startBeat);
|
|
2226
|
-
const lastChordEnd = sorted.reduce((end, segment) => Math.max(end, segment.endBeat),
|
|
2331
|
+
const lastChordEnd = sorted.length === 0 ? 0 : sorted.reduce((end, segment) => Math.max(end, segment.endBeat), Number.NEGATIVE_INFINITY);
|
|
2227
2332
|
const totalBeats = opts.totalBeats ?? lastChordEnd;
|
|
2228
|
-
|
|
2333
|
+
const lowest = totalBeats < 0 ? Math.min(0, sorted[0]?.startBeat ?? 0) : 0;
|
|
2334
|
+
assertRange(totalBeats, lowest, Number.MAX_SAFE_INTEGER, "modulation totalBeats");
|
|
2229
2335
|
const ordered = sorted.map((segment) => ({ ...segment, endBeat: Math.min(segment.endBeat, totalBeats) })).filter((segment) => segment.endBeat > segment.startBeat);
|
|
2230
2336
|
if (ordered.length === 0) {
|
|
2231
2337
|
return [];
|
|
@@ -2422,7 +2528,7 @@ function prevailingKeyOf(regions) {
|
|
|
2422
2528
|
// src/analyze/voice/index.ts
|
|
2423
2529
|
function keyScaleAt(key) {
|
|
2424
2530
|
if (typeof key === "function") {
|
|
2425
|
-
return key;
|
|
2531
|
+
return (beat) => toKeyScale(key(beat));
|
|
2426
2532
|
}
|
|
2427
2533
|
const scale = toKeyScale(key);
|
|
2428
2534
|
return () => scale;
|
|
@@ -2451,7 +2557,8 @@ var TENSION_DEGREE_BY_INTERVAL = {
|
|
|
2451
2557
|
8: 13,
|
|
2452
2558
|
9: 13
|
|
2453
2559
|
};
|
|
2454
|
-
var
|
|
2560
|
+
var NATURAL_TENSION_INTERVAL_LIST = [2, 5, 9];
|
|
2561
|
+
var NATURAL_TENSION_INTERVALS = new Set(NATURAL_TENSION_INTERVAL_LIST);
|
|
2455
2562
|
function tensionDegree(ic) {
|
|
2456
2563
|
return TENSION_DEGREE_BY_INTERVAL[ic];
|
|
2457
2564
|
}
|
|
@@ -2466,11 +2573,22 @@ function isStep(a, b) {
|
|
|
2466
2573
|
return d === 1 || d === 2;
|
|
2467
2574
|
}
|
|
2468
2575
|
var LEAP_SEMITONES = 3;
|
|
2469
|
-
function suspensionType(ic, delta) {
|
|
2576
|
+
function suspensionType(ic, delta, resolvedIc) {
|
|
2577
|
+
const figure = suspendedFigure(ic, delta);
|
|
2578
|
+
return figure !== null && SUSPENSION_RESOLUTIONS[figure].includes(resolvedIc) ? figure : null;
|
|
2579
|
+
}
|
|
2580
|
+
var SUSPENSION_RESOLUTIONS = {
|
|
2581
|
+
"sus4-3": [3, 4],
|
|
2582
|
+
"sus6-5": [7],
|
|
2583
|
+
"sus7-6": [8, 9],
|
|
2584
|
+
"sus9-8": [0],
|
|
2585
|
+
"sus2-3": [3, 4]
|
|
2586
|
+
};
|
|
2587
|
+
function suspendedFigure(ic, delta) {
|
|
2470
2588
|
if (delta > 0) {
|
|
2471
2589
|
return ic === 1 || ic === 2 ? "sus2-3" : null;
|
|
2472
2590
|
}
|
|
2473
|
-
if (ic === 2) {
|
|
2591
|
+
if (ic === 1 || ic === 2) {
|
|
2474
2592
|
return "sus9-8";
|
|
2475
2593
|
}
|
|
2476
2594
|
if (ic === 10 || ic === 11) {
|
|
@@ -2484,6 +2602,18 @@ function suspensionType(ic, delta) {
|
|
|
2484
2602
|
}
|
|
2485
2603
|
return null;
|
|
2486
2604
|
}
|
|
2605
|
+
function clashesWithChord(pitch, chord) {
|
|
2606
|
+
if (!isConsonantInterval(intervalAboveBass(pitch, [], chord), true)) {
|
|
2607
|
+
return true;
|
|
2608
|
+
}
|
|
2609
|
+
const tones = chord.intervals.map((interval) => chord.rootPc + interval);
|
|
2610
|
+
if (chord.bassPc !== void 0) {
|
|
2611
|
+
tones.push(chord.bassPc);
|
|
2612
|
+
}
|
|
2613
|
+
return tones.some(
|
|
2614
|
+
(tone) => !isConsonantInterval(((pitchClassOf(pitch) - tone) % 12 + 12) % 12, false)
|
|
2615
|
+
);
|
|
2616
|
+
}
|
|
2487
2617
|
var STEP_RESOLUTION = 2;
|
|
2488
2618
|
var NEAREST_CHORD_TONE_REACH = 12;
|
|
2489
2619
|
function nearestChordTone(pitch, chord, maxDistance) {
|
|
@@ -2503,17 +2633,20 @@ function analyzeVoice(voice, chordAtBeat2, key, otherVoicesAtBeat = () => []) {
|
|
|
2503
2633
|
assertFunction(otherVoicesAtBeat, "otherVoicesAtBeat");
|
|
2504
2634
|
const keyAt = keyScaleAt(key);
|
|
2505
2635
|
const result = [];
|
|
2636
|
+
const sounding = soundingIndices(voice);
|
|
2637
|
+
const line = new Map(sounding.map((index, position) => [index, position]));
|
|
2506
2638
|
for (let i = 0; i < voice.length; i += 1) {
|
|
2507
2639
|
const note = voice[i];
|
|
2508
2640
|
if (!note) {
|
|
2509
2641
|
continue;
|
|
2510
2642
|
}
|
|
2511
|
-
const
|
|
2512
|
-
const
|
|
2513
|
-
const
|
|
2514
|
-
const
|
|
2515
|
-
const
|
|
2516
|
-
const
|
|
2643
|
+
const position = line.get(i);
|
|
2644
|
+
const prevIndex = position === void 0 ? void 0 : sounding[position - 1];
|
|
2645
|
+
const nextIndex = position === void 0 ? void 0 : sounding[position + 1];
|
|
2646
|
+
const prevNote = prevIndex === void 0 ? void 0 : voice[prevIndex];
|
|
2647
|
+
const nextNote = nextIndex === void 0 ? void 0 : voice[nextIndex];
|
|
2648
|
+
const prev = prevNote !== void 0 && !sameInstant(prevNote.startBeat, note.startBeat) && adjacent(prevNote.startBeat + prevNote.durationBeat, note.startBeat) ? prevNote : void 0;
|
|
2649
|
+
const next = nextNote !== void 0 && !sameInstant(nextNote.startBeat, note.startBeat) && adjacent(note.startBeat + note.durationBeat, nextNote.startBeat) ? nextNote : void 0;
|
|
2517
2650
|
const chord = chordAtBeat2(note.startBeat);
|
|
2518
2651
|
const labels = [];
|
|
2519
2652
|
const member = isChordMember(note.pitch, chord);
|
|
@@ -2540,12 +2673,11 @@ function analyzeVoice(voice, chordAtBeat2, key, otherVoicesAtBeat = () => []) {
|
|
|
2540
2673
|
const prepared = prev.pitch === note.pitch && isChordMember(prev.pitch, prevChord);
|
|
2541
2674
|
const resolves = isStep(next.pitch, note.pitch);
|
|
2542
2675
|
const others = otherVoicesAtBeat(note.startBeat);
|
|
2543
|
-
const verticallyDissonant = others.some(
|
|
2544
|
-
(ov) => createsVerticalDissonance(note.pitch, ov.pitch, true)
|
|
2545
|
-
);
|
|
2676
|
+
const verticallyDissonant = others.length > 0 ? others.some((ov) => createsVerticalDissonance(note.pitch, ov.pitch, true)) : clashesWithChord(note.pitch, chord);
|
|
2546
2677
|
if (prepared && verticallyDissonant && resolves) {
|
|
2547
2678
|
const ic = intervalAboveBass(note.pitch, others, chord);
|
|
2548
|
-
const
|
|
2679
|
+
const resolvedIc = intervalAboveBass(next.pitch, others, chord);
|
|
2680
|
+
const type = suspensionType(ic, next.pitch - note.pitch, resolvedIc);
|
|
2549
2681
|
if (type !== null) {
|
|
2550
2682
|
labels.push({ kind: "suspension", type, resolveTo: next.pitch });
|
|
2551
2683
|
handled = true;
|
|
@@ -2675,8 +2807,34 @@ function assertChordTimeline(timeline, name = "timeline") {
|
|
|
2675
2807
|
assertFiniteNumber(segment.startBeat, `${name}.segments[${index}].startBeat`);
|
|
2676
2808
|
assertFiniteNumber(segment.endBeat, `${name}.segments[${index}].endBeat`);
|
|
2677
2809
|
}
|
|
2810
|
+
assertSpansInOrder(segments, `${name}.segments`);
|
|
2678
2811
|
return read;
|
|
2679
2812
|
}
|
|
2813
|
+
function assertSpansInOrder(spans, name) {
|
|
2814
|
+
for (let index = 0; index < spans.length; index += 1) {
|
|
2815
|
+
const span = spans[index];
|
|
2816
|
+
if (span === void 0) {
|
|
2817
|
+
continue;
|
|
2818
|
+
}
|
|
2819
|
+
if (!(span.endBeat > span.startBeat)) {
|
|
2820
|
+
throw new InvalidInputError(
|
|
2821
|
+
`${name}[${index}] must end after it starts; received [${span.startBeat}, ${span.endBeat})`
|
|
2822
|
+
);
|
|
2823
|
+
}
|
|
2824
|
+
const previous = spans[index - 1];
|
|
2825
|
+
if (previous !== void 0 && span.startBeat < previous.endBeat) {
|
|
2826
|
+
throw new InvalidInputError(
|
|
2827
|
+
`${name}[${index}] must not start before ${name}[${index - 1}] ends; received ${span.startBeat} before ${previous.endBeat}`
|
|
2828
|
+
);
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
function statedKeyRegions(key, startBeat, endBeat) {
|
|
2833
|
+
if (!(endBeat > startBeat)) {
|
|
2834
|
+
return [];
|
|
2835
|
+
}
|
|
2836
|
+
return [{ startBeat, endBeat, key, confidence: 1 }];
|
|
2837
|
+
}
|
|
2680
2838
|
function chordTimelineFromChords(chords, totalBeats) {
|
|
2681
2839
|
assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, "timeline totalBeats");
|
|
2682
2840
|
assertArray(chords, "chords");
|
|
@@ -2741,6 +2899,7 @@ var EXTRA_TONE_PENALTY = 0.6;
|
|
|
2741
2899
|
var OMITTED_TONE_PENALTY = 0.15;
|
|
2742
2900
|
var BASS_ROOT_STANDING = 0.95;
|
|
2743
2901
|
var OCTAVE = 12;
|
|
2902
|
+
var CHORD_SEGMENTATIONS = ["dynamic", "grid"];
|
|
2744
2903
|
function scoreMatch(match, weights, maxWeight, key, pedalPc) {
|
|
2745
2904
|
const rootWeight = maxWeight > 0 ? (weights[match.rootPc] ?? 0) / maxWeight : 0;
|
|
2746
2905
|
const rootStanding = match.inversion === 0 ? Math.max(rootWeight, BASS_ROOT_STANDING) : rootWeight;
|
|
@@ -2757,7 +2916,10 @@ function scoreMatch(match, weights, maxWeight, key, pedalPc) {
|
|
|
2757
2916
|
score -= OMITTED_TONE_PENALTY * account.missing;
|
|
2758
2917
|
return score;
|
|
2759
2918
|
}
|
|
2760
|
-
var
|
|
2919
|
+
var SLASH_UPPER_QUALITY_LIST = ["maj", "min"];
|
|
2920
|
+
var SLASH_UPPER_QUALITIES = new Set(
|
|
2921
|
+
SLASH_UPPER_QUALITY_LIST
|
|
2922
|
+
);
|
|
2761
2923
|
function isSlashReading(match, pedalPc) {
|
|
2762
2924
|
return pedalPc !== null && match.inversion === null && match.missingPcs.length === 0 && match.extraPcs.length === 1 && match.extraPcs[0] === pedalPc && SLASH_UPPER_QUALITIES.has(match.quality);
|
|
2763
2925
|
}
|
|
@@ -2789,6 +2951,30 @@ function bassSoundsOnlyBelow(notes, windowStart, windowEnd, bassPc) {
|
|
|
2789
2951
|
}
|
|
2790
2952
|
return highestBass <= upperFloor;
|
|
2791
2953
|
}
|
|
2954
|
+
function windowBass(notes, windowStart, windowEnd, meters, harmonicRhythm) {
|
|
2955
|
+
const counted = [];
|
|
2956
|
+
let firstSounding = Number.POSITIVE_INFINITY;
|
|
2957
|
+
for (const note of notes) {
|
|
2958
|
+
const overlap = Math.min(note.startBeat + note.durationBeat, windowEnd) - Math.max(note.startBeat, windowStart);
|
|
2959
|
+
if (overlap <= BEAT_EPS) {
|
|
2960
|
+
continue;
|
|
2961
|
+
}
|
|
2962
|
+
const onsetInWindow = note.startBeat >= windowStart - BEAT_EPS && note.startBeat < windowEnd - BEAT_EPS;
|
|
2963
|
+
if (noteWeight(note, overlap, onsetInWindow ? meters : void 0) <= BEAT_EPS) {
|
|
2964
|
+
continue;
|
|
2965
|
+
}
|
|
2966
|
+
counted.push({ note, overlap });
|
|
2967
|
+
firstSounding = Math.min(firstSounding, Math.max(note.startBeat, windowStart));
|
|
2968
|
+
}
|
|
2969
|
+
const half = (windowEnd - windowStart) / 2;
|
|
2970
|
+
let bass = Number.POSITIVE_INFINITY;
|
|
2971
|
+
for (const { note, overlap } of counted) {
|
|
2972
|
+
if (note.startBeat <= firstSounding + BEAT_EPS || overlap > half || overlap >= harmonicRhythm - BEAT_EPS) {
|
|
2973
|
+
bass = Math.min(bass, note.pitch);
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
return bass;
|
|
2977
|
+
}
|
|
2792
2978
|
function chordConfidence(chord, weights, totalWeight, exact) {
|
|
2793
2979
|
const tones = new Set(chordPitchClasses(chord));
|
|
2794
2980
|
let chordWeight = 0;
|
|
@@ -2801,23 +2987,20 @@ function chordConfidence(chord, weights, totalWeight, exact) {
|
|
|
2801
2987
|
const confidence = Math.min(1, Math.max(0, raw));
|
|
2802
2988
|
return exact ? confidence : confidence * INEXACT_CONFIDENCE_FACTOR;
|
|
2803
2989
|
}
|
|
2804
|
-
function analyzeWindow(notes, windowStart, windowEnd, meters, key) {
|
|
2805
|
-
const { weights, totalWeight, maxWeight
|
|
2806
|
-
notes,
|
|
2807
|
-
windowStart,
|
|
2808
|
-
windowEnd,
|
|
2809
|
-
meters
|
|
2810
|
-
);
|
|
2990
|
+
function analyzeWindow(notes, windowStart, windowEnd, meters, key, harmonicRhythm) {
|
|
2991
|
+
const { weights, totalWeight, maxWeight } = windowWeights(notes, windowStart, windowEnd, meters);
|
|
2811
2992
|
if (totalWeight <= BEAT_EPS) {
|
|
2812
2993
|
return null;
|
|
2813
2994
|
}
|
|
2814
2995
|
const ranked = weights.map((weight, pc) => ({ pc, weight })).filter(({ weight }) => weight > 0).sort((a, b) => b.weight - a.weight || a.pc - b.pc);
|
|
2815
|
-
const
|
|
2816
|
-
const
|
|
2817
|
-
const
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2996
|
+
const bassPc = pitchClassOf(windowBass(notes, windowStart, windowEnd, meters, harmonicRhythm));
|
|
2997
|
+
const sounding = ranked.slice(0, MAX_DETECTION_PCS).map(({ pc }) => pc);
|
|
2998
|
+
const significant = ranked.filter(({ weight }) => weight >= maxWeight * NOISE_THRESHOLD_RATIO).slice(0, MAX_DETECTION_PCS).map(({ pc }) => pc);
|
|
2999
|
+
let detection = detectWindow(significant, bassPc, notes, windowStart, windowEnd);
|
|
3000
|
+
if (detection.matches.length === 0 && significant.length < sounding.length) {
|
|
3001
|
+
detection = detectWindow(sounding, bassPc, notes, windowStart, windowEnd);
|
|
3002
|
+
}
|
|
3003
|
+
const { selected, hasBass, pedalPc, matches } = detection;
|
|
2821
3004
|
let bestMatch = null;
|
|
2822
3005
|
let bestScore = Number.NEGATIVE_INFINITY;
|
|
2823
3006
|
for (const match of matches) {
|
|
@@ -2834,10 +3017,26 @@ function analyzeWindow(notes, windowStart, windowEnd, meters, key) {
|
|
|
2834
3017
|
makeChord(bestMatch.rootPc, bestMatch.quality),
|
|
2835
3018
|
pedalPc
|
|
2836
3019
|
);
|
|
3020
|
+
const explained = account.exact || isSlashReading(bestMatch, pedalPc);
|
|
2837
3021
|
const reading = {
|
|
2838
3022
|
chord: shared(chord),
|
|
2839
|
-
confidence: chordConfidence(chord, weights, totalWeight,
|
|
3023
|
+
confidence: chordConfidence(chord, weights, totalWeight, explained)
|
|
2840
3024
|
};
|
|
3025
|
+
if (bestMatch.quality === SYMMETRIC_QUALITY) {
|
|
3026
|
+
reading.rootings = chordPitchClasses(makeChord(bestMatch.rootPc, bestMatch.quality)).map(
|
|
3027
|
+
(rootPc) => {
|
|
3028
|
+
const rooted = makeChord(
|
|
3029
|
+
rootPc,
|
|
3030
|
+
bestMatch.quality,
|
|
3031
|
+
hasBass && bassPc !== rootPc ? bassPc : void 0
|
|
3032
|
+
);
|
|
3033
|
+
return {
|
|
3034
|
+
chord: shared(rooted),
|
|
3035
|
+
confidence: chordConfidence(rooted, weights, totalWeight, explained)
|
|
3036
|
+
};
|
|
3037
|
+
}
|
|
3038
|
+
);
|
|
3039
|
+
}
|
|
2841
3040
|
const augmented = hasBass ? augmentedSixthFromPitchClasses(selected, bassPc, key) : null;
|
|
2842
3041
|
if (augmented !== null) {
|
|
2843
3042
|
reading.augmented = {
|
|
@@ -2849,6 +3048,13 @@ function analyzeWindow(notes, windowStart, windowEnd, meters, key) {
|
|
|
2849
3048
|
}
|
|
2850
3049
|
return null;
|
|
2851
3050
|
}
|
|
3051
|
+
function detectWindow(selected, bassPc, notes, windowStart, windowEnd) {
|
|
3052
|
+
const hasBass = selected.includes(bassPc);
|
|
3053
|
+
const detectionPitches = hasBass ? selected.map((pc) => pc === bassPc ? pc : pc + 12) : selected;
|
|
3054
|
+
const pedalPc = hasBass && bassSoundsOnlyBelow(notes, windowStart, windowEnd, bassPc) ? bassPc : null;
|
|
3055
|
+
const matches = detectChord(detectionPitches, { input: hasBass ? "midi" : "pitchClass" });
|
|
3056
|
+
return { selected, hasBass, pedalPc, matches };
|
|
3057
|
+
}
|
|
2852
3058
|
function shared(chord) {
|
|
2853
3059
|
Object.freeze(chord.intervals);
|
|
2854
3060
|
return Object.freeze(chord);
|
|
@@ -2875,6 +3081,29 @@ function resolvesOntoDominant(readings, index, key) {
|
|
|
2875
3081
|
}
|
|
2876
3082
|
return true;
|
|
2877
3083
|
}
|
|
3084
|
+
var SYMMETRIC_QUALITY = "dim7";
|
|
3085
|
+
function settledDiminishedRoot(readings, index, key) {
|
|
3086
|
+
const reading = readings[index];
|
|
3087
|
+
const rootings = reading?.rootings;
|
|
3088
|
+
if (!reading || rootings === void 0) {
|
|
3089
|
+
return null;
|
|
3090
|
+
}
|
|
3091
|
+
const roots = rootings.map((rooting) => rooting.chord.rootPc);
|
|
3092
|
+
const tones = chordPitchClasses(reading.chord).join();
|
|
3093
|
+
for (let i = index + 1; i < readings.length; i += 1) {
|
|
3094
|
+
const next = readings[i];
|
|
3095
|
+
if (!next || chordPitchClasses(next.chord).join() === tones) {
|
|
3096
|
+
continue;
|
|
3097
|
+
}
|
|
3098
|
+
const leading = pitchClassOf(next.chord.rootPc - 1);
|
|
3099
|
+
if (roots.includes(leading)) {
|
|
3100
|
+
return leading;
|
|
3101
|
+
}
|
|
3102
|
+
break;
|
|
3103
|
+
}
|
|
3104
|
+
const leadingTone = pitchClassOf(key.rootPc - 1);
|
|
3105
|
+
return roots.includes(leadingTone) ? leadingTone : null;
|
|
3106
|
+
}
|
|
2878
3107
|
function sameChord(a, b) {
|
|
2879
3108
|
return a.rootPc === b.rootPc && a.quality === b.quality && a.bassPc === b.bassPc;
|
|
2880
3109
|
}
|
|
@@ -3123,7 +3352,11 @@ function analyzeTimeline(notes, opts = {}, previous, dirty) {
|
|
|
3123
3352
|
const meters = resolveMeters(opts, "timeline meters");
|
|
3124
3353
|
const harmonicRhythm = opts.harmonicRhythm ?? beatsPerBarAt(0, meters);
|
|
3125
3354
|
assertRange(harmonicRhythm, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "harmonic rhythm");
|
|
3126
|
-
const segmentation =
|
|
3355
|
+
const segmentation = assertOneOf(
|
|
3356
|
+
opts.segmentation ?? "dynamic",
|
|
3357
|
+
CHORD_SEGMENTATIONS,
|
|
3358
|
+
"timeline segmentation"
|
|
3359
|
+
);
|
|
3127
3360
|
const gridUnit = metricGridUnit(meters);
|
|
3128
3361
|
const minChordBeats = opts.minChordBeats ?? gridUnit;
|
|
3129
3362
|
assertRange(minChordBeats, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "minChordBeats");
|
|
@@ -3135,9 +3368,13 @@ function analyzeTimeline(notes, opts = {}, previous, dirty) {
|
|
|
3135
3368
|
}
|
|
3136
3369
|
assertNoteEvents(notes, "timeline notes", noteOptions);
|
|
3137
3370
|
const sounding = sortedNoteEvents(notes.filter((note) => note.durationBeat > 0));
|
|
3138
|
-
const
|
|
3139
|
-
|
|
3140
|
-
|
|
3371
|
+
const totalBeats = opts.totalBeats ?? latestSoundingEnd(sounding);
|
|
3372
|
+
assertRange(
|
|
3373
|
+
totalBeats,
|
|
3374
|
+
Math.min(0, firstSoundingBeat(sounding)),
|
|
3375
|
+
Number.MAX_SAFE_INTEGER,
|
|
3376
|
+
"timeline totalBeats"
|
|
3377
|
+
);
|
|
3141
3378
|
const slotBeats = segmentation === "grid" ? harmonicRhythm : slotBeatsWithin(gridUnit, minChordBeats, harmonicRhythm);
|
|
3142
3379
|
const { origin, startBeat: musicStart } = gridForNotes(sounding, slotBeats);
|
|
3143
3380
|
const grid = { origin, slotBeats };
|
|
@@ -3148,14 +3385,7 @@ function analyzeTimeline(notes, opts = {}, previous, dirty) {
|
|
|
3148
3385
|
"timeline windows",
|
|
3149
3386
|
budget
|
|
3150
3387
|
);
|
|
3151
|
-
const keys = opts.key !== void 0 ?
|
|
3152
|
-
{
|
|
3153
|
-
startBeat: musicStart,
|
|
3154
|
-
endBeat: totalBeats,
|
|
3155
|
-
key: resolveKey(opts.key),
|
|
3156
|
-
confidence: 1
|
|
3157
|
-
}
|
|
3158
|
-
] : keyTimelineFromNotes(sounding, { meters, totalBeats, budget });
|
|
3388
|
+
const keys = opts.key !== void 0 ? statedKeyRegions(resolveKey(opts.key), musicStart, totalBeats) : keyTimelineFromNotes(sounding, { meters, totalBeats, budget });
|
|
3159
3389
|
const prevailingKey = prevailingKeyOf(keys) ?? resolveKey(majorKey(0));
|
|
3160
3390
|
const keyAt = keyLookup(keys, prevailingKey);
|
|
3161
3391
|
const scaleAt = (beat) => scaleOf(keyAt(beat));
|
|
@@ -3211,12 +3441,27 @@ function analyzeTimeline(notes, opts = {}, previous, dirty) {
|
|
|
3211
3441
|
const spanKeyWhole = keyAt(start);
|
|
3212
3442
|
const id = windowId(start, end, spanKey);
|
|
3213
3443
|
const cached = carried !== void 0 && spanIsClean(grid, span, dirtyFrom, dirtyTo) ? carried.windows.get(id) : void 0;
|
|
3214
|
-
const inferred = cached !== void 0 ? cached : analyzeWindow(
|
|
3444
|
+
const inferred = cached !== void 0 ? cached : analyzeWindow(
|
|
3445
|
+
notesOfSpan(slotNotes, span, grid),
|
|
3446
|
+
start,
|
|
3447
|
+
end,
|
|
3448
|
+
meters,
|
|
3449
|
+
spanKeyWhole,
|
|
3450
|
+
harmonicRhythm
|
|
3451
|
+
);
|
|
3215
3452
|
windows.set(id, inferred);
|
|
3216
3453
|
readings.push(inferred);
|
|
3217
3454
|
readingKeys.push(spanKey);
|
|
3218
3455
|
readingBounds.push({ start, end });
|
|
3219
3456
|
}
|
|
3457
|
+
for (let i = readings.length - 1; i >= 0; i -= 1) {
|
|
3458
|
+
const spanKey = readingKeys[i];
|
|
3459
|
+
const root = spanKey === void 0 ? null : settledDiminishedRoot(readings, i, spanKey);
|
|
3460
|
+
const rooting = readings[i]?.rootings?.find((option) => option.chord.rootPc === root);
|
|
3461
|
+
if (rooting !== void 0 && root !== readings[i]?.chord.rootPc) {
|
|
3462
|
+
readings[i] = rooting;
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3220
3465
|
for (let i = 0; i < readings.length; i += 1) {
|
|
3221
3466
|
const offered = readings[i]?.augmented;
|
|
3222
3467
|
const spanKey = readingKeys[i];
|
|
@@ -3369,11 +3614,7 @@ function roundTo(value, digits) {
|
|
|
3369
3614
|
return Math.round(value * scale) / scale;
|
|
3370
3615
|
}
|
|
3371
3616
|
function endBeatOf(notes) {
|
|
3372
|
-
|
|
3373
|
-
for (const note of notes) {
|
|
3374
|
-
end = Math.max(end, note.startBeat + note.durationBeat);
|
|
3375
|
-
}
|
|
3376
|
-
return end;
|
|
3617
|
+
return latestSoundingEnd(notes);
|
|
3377
3618
|
}
|
|
3378
3619
|
function onsetSpan(notes) {
|
|
3379
3620
|
if (notes.length < 2) {
|
|
@@ -3439,7 +3680,7 @@ function melodicContour(notes) {
|
|
|
3439
3680
|
peakIndex: peak,
|
|
3440
3681
|
troughIndex: trough,
|
|
3441
3682
|
range,
|
|
3442
|
-
rationale:
|
|
3683
|
+
rationale: `The net ${net > 0 ? "rise" : "fall"} is ${share}% of the motion, so the line is ${shape}`
|
|
3443
3684
|
};
|
|
3444
3685
|
}
|
|
3445
3686
|
const peakPitch = line[peak]?.pitch ?? 0;
|
|
@@ -3460,7 +3701,7 @@ function melodicContour(notes) {
|
|
|
3460
3701
|
peakIndex: peak,
|
|
3461
3702
|
troughIndex: trough,
|
|
3462
3703
|
range,
|
|
3463
|
-
rationale: `Changes direction without settling either way (${share}% of the motion
|
|
3704
|
+
rationale: `Changes direction without settling either way (the net movement is ${share}% of the motion), so the line is a wave`
|
|
3464
3705
|
};
|
|
3465
3706
|
}
|
|
3466
3707
|
|
|
@@ -3640,8 +3881,8 @@ function spellingOf(key) {
|
|
|
3640
3881
|
return keySignatureFifths(key.tonic, key.scale) < 0 ? "flat" : "sharp";
|
|
3641
3882
|
}
|
|
3642
3883
|
function intervalBetween(from, to, key) {
|
|
3643
|
-
const
|
|
3644
|
-
return spelledInterval(
|
|
3884
|
+
const start = midiToNote(from, spellingOf(key));
|
|
3885
|
+
return spelledInterval(start, transposeNote(start, to - from));
|
|
3645
3886
|
}
|
|
3646
3887
|
function intervalName(interval) {
|
|
3647
3888
|
return `${interval.descending && interval.semitones !== 0 ? "-" : ""}${interval.quality}${interval.number}`;
|
|
@@ -3771,11 +4012,12 @@ function relateMotifs(a, b, keyLike) {
|
|
|
3771
4012
|
}
|
|
3772
4013
|
}
|
|
3773
4014
|
}
|
|
3774
|
-
|
|
4015
|
+
const literalRetrograde = backward && shapes.includes("retrograde") && answer[0]?.pitch === model[model.length - 1]?.pitch;
|
|
4016
|
+
if (forward && shapes.includes("inversion") && !(literalRetrograde && semitones !== 0)) {
|
|
3775
4017
|
return {
|
|
3776
4018
|
...base,
|
|
3777
4019
|
kind: "inversion",
|
|
3778
|
-
rationale:
|
|
4020
|
+
rationale: `Inversion: every interval turned upside down${semitones === 0 ? " about the first note" : ""}` + (stretched ? `, in ${stretchPhrase(timeRatio)}` : "")
|
|
3779
4021
|
};
|
|
3780
4022
|
}
|
|
3781
4023
|
if (backward && shapes.includes("retrograde")) {
|
|
@@ -3839,6 +4081,7 @@ function compareMelodies(a, b) {
|
|
|
3839
4081
|
const right = orderedNotes(phraseNotes(b), "melody b");
|
|
3840
4082
|
const leftIntervals = intervalsOf(left);
|
|
3841
4083
|
const rightIntervals = intervalsOf(right);
|
|
4084
|
+
assertGenerationBudget(leftIntervals.length * rightIntervals.length, "melody alignment");
|
|
3842
4085
|
const pitchSimilarity = distanceToSimilarity(
|
|
3843
4086
|
gradedEditDistance(leftIntervals, rightIntervals, intervalCost),
|
|
3844
4087
|
leftIntervals.length,
|
|
@@ -3953,7 +4196,13 @@ function motifGraph(line, motifs, opts = {}) {
|
|
|
3953
4196
|
|
|
3954
4197
|
// src/analyze/reduction/index.ts
|
|
3955
4198
|
var REDUCTION_LEVELS = Object.freeze(["structural", "passing", "neighbor"]);
|
|
3956
|
-
var
|
|
4199
|
+
var TONIC_ARRIVING_CADENCES = [
|
|
4200
|
+
"authentic",
|
|
4201
|
+
"plagal",
|
|
4202
|
+
"modal",
|
|
4203
|
+
"deceptive"
|
|
4204
|
+
];
|
|
4205
|
+
var TONIC_ARRIVING = new Set(TONIC_ARRIVING_CADENCES);
|
|
3957
4206
|
function signedStep(fromPc, toPc) {
|
|
3958
4207
|
return (pitchClassOf(toPc) - pitchClassOf(fromPc) + 18) % 12 - 6;
|
|
3959
4208
|
}
|
|
@@ -4104,6 +4353,11 @@ function reduceProgression(timeline, key, opts = {}) {
|
|
|
4104
4353
|
return result;
|
|
4105
4354
|
}
|
|
4106
4355
|
|
|
4356
|
+
// src/analyze/reference/types.ts
|
|
4357
|
+
var REFERENCE_PROFILE_VERSION = 1;
|
|
4358
|
+
var PHRASE_OUTLINE_POINTS = 8;
|
|
4359
|
+
var MIDI_MAX = 127;
|
|
4360
|
+
|
|
4107
4361
|
// src/analyze/rhythm/internal.ts
|
|
4108
4362
|
function isMultipleOf(value, unit) {
|
|
4109
4363
|
if (unit <= 0) {
|
|
@@ -4122,9 +4376,15 @@ function rhythmLevel(beatInQuarters, meters) {
|
|
|
4122
4376
|
const subUnit = pulse / (isCompound(ts) ? 3 : 2);
|
|
4123
4377
|
return isMultipleOf(offset, subUnit) ? 1 : 0;
|
|
4124
4378
|
}
|
|
4379
|
+
var IOI_MIN_EXPONENT = -4;
|
|
4380
|
+
var IOI_MAX_EXPONENT = 4;
|
|
4381
|
+
var IOI_EXPONENT_STEP = 0.5;
|
|
4125
4382
|
function ioiBinIndex(ioi) {
|
|
4126
|
-
const exponent = Math.min(
|
|
4127
|
-
|
|
4383
|
+
const exponent = Math.min(
|
|
4384
|
+
IOI_MAX_EXPONENT,
|
|
4385
|
+
Math.max(IOI_MIN_EXPONENT, Math.round(2 * Math.log2(ioi)) / 2)
|
|
4386
|
+
);
|
|
4387
|
+
return Math.round((exponent - IOI_MIN_EXPONENT) / IOI_EXPONENT_STEP);
|
|
4128
4388
|
}
|
|
4129
4389
|
|
|
4130
4390
|
// src/analyze/rhythm/index.ts
|
|
@@ -4134,6 +4394,8 @@ var RHYTHM_OFF_GRID_TOLERANCE = 1 / 32;
|
|
|
4134
4394
|
var RHYTHM_IOI_BINS = Object.freeze(
|
|
4135
4395
|
Array.from({ length: 17 }, (_, i) => 2 ** (-4 + i * 0.5))
|
|
4136
4396
|
);
|
|
4397
|
+
var BUDGET_SLOTS_PER_BAR = 48;
|
|
4398
|
+
var RHYTHM_LEVEL_COUNT = 6;
|
|
4137
4399
|
function emptyAnalysis() {
|
|
4138
4400
|
return {
|
|
4139
4401
|
startBeat: 0,
|
|
@@ -4142,7 +4404,7 @@ function emptyAnalysis() {
|
|
|
4142
4404
|
onsets: 0,
|
|
4143
4405
|
onsetDensity: 0,
|
|
4144
4406
|
barOnsets: [],
|
|
4145
|
-
onsetLevels: new Array(
|
|
4407
|
+
onsetLevels: new Array(RHYTHM_LEVEL_COUNT).fill(0),
|
|
4146
4408
|
barPositions: [],
|
|
4147
4409
|
interOnsetShares: new Array(RHYTHM_IOI_BINS.length).fill(0),
|
|
4148
4410
|
restRatio: 0,
|
|
@@ -4177,13 +4439,47 @@ function snapOnset(onset, meters) {
|
|
|
4177
4439
|
function tsKey(ts) {
|
|
4178
4440
|
return ts.grouping === void 0 ? `${ts.numerator}/${ts.denominator}` : `${ts.numerator}/${ts.denominator}:${ts.grouping.join("+")}`;
|
|
4179
4441
|
}
|
|
4180
|
-
function
|
|
4442
|
+
function pulseKey(bar, index) {
|
|
4443
|
+
return `${bar}/${index}`;
|
|
4444
|
+
}
|
|
4445
|
+
function otherDivisionPulses(snapped, meters) {
|
|
4446
|
+
const pulses = /* @__PURE__ */ new Set();
|
|
4447
|
+
for (const onset of snapped) {
|
|
4448
|
+
const pulse = pulseBeats(onset.ts);
|
|
4449
|
+
const offset = onset.snapped - onset.barStart;
|
|
4450
|
+
const index = Math.floor(offset / pulse + BEAT_EPS);
|
|
4451
|
+
const within = offset - index * pulse;
|
|
4452
|
+
const [used, other] = isCompound(onset.ts) ? [3, 2] : [2, 3];
|
|
4453
|
+
if (isMultipleOf(within, pulse / other) && !isMultipleOf(within, pulse / used)) {
|
|
4454
|
+
pulses.add(pulseKey(barIndexAt(onset.beat, meters), index));
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4457
|
+
return pulses;
|
|
4458
|
+
}
|
|
4459
|
+
function barGridPoints(bar, meters, otherPulses) {
|
|
4181
4460
|
const barStart = barPositionToBeat({ bar, beat: 0 }, meters);
|
|
4182
4461
|
const ts = meterAt(barStart, meters);
|
|
4183
4462
|
const barLength = beatsPerBarAt(barStart, meters);
|
|
4184
|
-
const
|
|
4463
|
+
const pulse = pulseBeats(ts);
|
|
4464
|
+
const [used, other] = isCompound(ts) ? [3, 2] : [2, 3];
|
|
4465
|
+
const subUnit = pulse / used;
|
|
4185
4466
|
const steps = Math.max(1, Math.round(barLength / subUnit));
|
|
4186
|
-
|
|
4467
|
+
const points = [];
|
|
4468
|
+
for (let k = 0; k < steps; k += 1) {
|
|
4469
|
+
const index = Math.floor(k * subUnit / pulse + BEAT_EPS);
|
|
4470
|
+
if (k % used === 0 || !otherPulses.has(pulseKey(bar, index))) {
|
|
4471
|
+
const beat = barStart + k * subUnit;
|
|
4472
|
+
points.push({ beat, level: rhythmLevel(beat, meters) });
|
|
4473
|
+
}
|
|
4474
|
+
}
|
|
4475
|
+
for (let index = 0; index * pulse < steps * subUnit - BEAT_EPS; index += 1) {
|
|
4476
|
+
if (otherPulses.has(pulseKey(bar, index))) {
|
|
4477
|
+
for (let m = 1; m < other; m += 1) {
|
|
4478
|
+
points.push({ beat: barStart + index * pulse + m * pulse / other, level: 1 });
|
|
4479
|
+
}
|
|
4480
|
+
}
|
|
4481
|
+
}
|
|
4482
|
+
return points.sort((a, b) => a.beat - b.beat);
|
|
4187
4483
|
}
|
|
4188
4484
|
function restRatioOf(sounding, windowStart, windowEnd) {
|
|
4189
4485
|
const span = windowEnd - windowStart;
|
|
@@ -4219,11 +4515,12 @@ function syncopationOf(snapped, startBeat, endBeat, meters) {
|
|
|
4219
4515
|
}
|
|
4220
4516
|
const firstBar = barIndexAt(startBeat, meters);
|
|
4221
4517
|
const lastBar = barIndexAt(endBeat, meters);
|
|
4518
|
+
const otherPulses = otherDivisionPulses(snapped, meters);
|
|
4222
4519
|
const points = [];
|
|
4223
4520
|
for (let bar = firstBar; bar <= lastBar; bar += 1) {
|
|
4224
|
-
for (const
|
|
4225
|
-
if (beat > startBeat + BEAT_EPS && beat <= endBeat + BEAT_EPS) {
|
|
4226
|
-
points.push(
|
|
4521
|
+
for (const point of barGridPoints(bar, meters, otherPulses)) {
|
|
4522
|
+
if (point.beat > startBeat + BEAT_EPS && point.beat <= endBeat + BEAT_EPS) {
|
|
4523
|
+
points.push(point);
|
|
4227
4524
|
}
|
|
4228
4525
|
}
|
|
4229
4526
|
}
|
|
@@ -4243,36 +4540,44 @@ function syncopationOf(snapped, startBeat, endBeat, meters) {
|
|
|
4243
4540
|
while (ptr < points.length && (points[ptr]?.beat ?? Number.POSITIVE_INFINITY) <= onset.snapped + BEAT_EPS) {
|
|
4244
4541
|
ptr += 1;
|
|
4245
4542
|
}
|
|
4543
|
+
const here = ptr > 0 ? points[ptr - 1] : void 0;
|
|
4544
|
+
const onsetLevel = here !== void 0 && Math.abs(here.beat - onset.snapped) < BEAT_EPS ? Math.max(onset.level, here.level) : onset.level;
|
|
4246
4545
|
let scan = ptr;
|
|
4247
4546
|
let maxLevel = 0;
|
|
4248
4547
|
while (scan < points.length && (points[scan]?.beat ?? Number.POSITIVE_INFINITY) < windowEnd - BEAT_EPS) {
|
|
4249
4548
|
maxLevel = Math.max(maxLevel, points[scan]?.level ?? 0);
|
|
4250
4549
|
scan += 1;
|
|
4251
4550
|
}
|
|
4252
|
-
if (maxLevel >
|
|
4253
|
-
numerator += maxLevel -
|
|
4551
|
+
if (maxLevel > onsetLevel) {
|
|
4552
|
+
numerator += maxLevel - onsetLevel;
|
|
4254
4553
|
}
|
|
4255
4554
|
ptr = scan;
|
|
4256
4555
|
}
|
|
4257
4556
|
return numerator / normalizer;
|
|
4258
4557
|
}
|
|
4259
4558
|
function analyzeRhythm(notes, opts = {}) {
|
|
4559
|
+
return rhythmOver(notes, opts, void 0);
|
|
4560
|
+
}
|
|
4561
|
+
function rhythmOver(notes, opts, windowStart) {
|
|
4260
4562
|
const meters = resolveMeters(opts, "rhythm meters");
|
|
4261
4563
|
assertNoteEvents(notes, "rhythm notes", { allowNonPositiveDuration: true, budget: opts.budget });
|
|
4262
4564
|
const sounding = notes.filter((note) => note.durationBeat > 0);
|
|
4565
|
+
const startBeat = windowStart ?? barGridStart(firstSoundingBeat(sounding), meters);
|
|
4566
|
+
const endBeat = resolveSpanEnd(
|
|
4567
|
+
opts.totalBeats,
|
|
4568
|
+
startBeat,
|
|
4569
|
+
latestSoundingEnd(sounding),
|
|
4570
|
+
"rhythm totalBeats"
|
|
4571
|
+
);
|
|
4263
4572
|
if (sounding.length === 0) {
|
|
4264
4573
|
return emptyAnalysis();
|
|
4265
4574
|
}
|
|
4266
|
-
const firstOnset = sounding.reduce(
|
|
4267
|
-
(min, n) => Math.min(min, n.startBeat),
|
|
4268
|
-
Number.POSITIVE_INFINITY
|
|
4269
|
-
);
|
|
4270
|
-
const startBeat = barGridStart(firstOnset, meters);
|
|
4271
|
-
const lastEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
|
|
4272
|
-
const endBeat = Math.max(startBeat, opts.totalBeats ?? lastEnd);
|
|
4273
|
-
assertRange(endBeat, 0, Number.MAX_SAFE_INTEGER, "rhythm totalBeats");
|
|
4274
4575
|
const bars = barSpanOf(startBeat, endBeat, meters);
|
|
4275
|
-
assertGenerationBudget(
|
|
4576
|
+
assertGenerationBudget(
|
|
4577
|
+
Math.ceil(bars) * BUDGET_SLOTS_PER_BAR,
|
|
4578
|
+
"rhythm analysis slots",
|
|
4579
|
+
opts.budget
|
|
4580
|
+
);
|
|
4276
4581
|
const onsets = foldOnsets(sounding);
|
|
4277
4582
|
const snapped = onsets.map((onset) => snapOnset(onset, meters));
|
|
4278
4583
|
const onsetDensity = bars > 0 ? onsets.length / bars : 0;
|
|
@@ -4284,7 +4589,7 @@ function analyzeRhythm(notes, opts = {}) {
|
|
|
4284
4589
|
barOnsets[index] = (barOnsets[index] ?? 0) + 1;
|
|
4285
4590
|
}
|
|
4286
4591
|
}
|
|
4287
|
-
const onsetLevels = new Array(
|
|
4592
|
+
const onsetLevels = new Array(RHYTHM_LEVEL_COUNT).fill(0);
|
|
4288
4593
|
let offGridCount = 0;
|
|
4289
4594
|
for (const onset of snapped) {
|
|
4290
4595
|
onsetLevels[onset.level] = (onsetLevels[onset.level] ?? 0) + 1;
|
|
@@ -4357,9 +4662,6 @@ function analyzeRhythm(notes, opts = {}) {
|
|
|
4357
4662
|
};
|
|
4358
4663
|
}
|
|
4359
4664
|
|
|
4360
|
-
// src/analyze/reference/types.ts
|
|
4361
|
-
var REFERENCE_PROFILE_VERSION = 1;
|
|
4362
|
-
|
|
4363
4665
|
// src/analyze/reference/validate.ts
|
|
4364
4666
|
var MASS_TOLERANCE = 1e-9;
|
|
4365
4667
|
function assertString(value, name) {
|
|
@@ -4427,7 +4729,7 @@ function assertRhythmAnalysis(value, name) {
|
|
|
4427
4729
|
`${name}.barOnsets[${index}]`
|
|
4428
4730
|
);
|
|
4429
4731
|
}
|
|
4430
|
-
assertMassDistribution(read.onsetLevels,
|
|
4732
|
+
assertMassDistribution(read.onsetLevels, RHYTHM_LEVEL_COUNT, `${name}.onsetLevels`, onsets === 0);
|
|
4431
4733
|
const barPositions = assertArray(read.barPositions, `${name}.barPositions`);
|
|
4432
4734
|
for (let index = 0; index < barPositions.length; index += 1) {
|
|
4433
4735
|
assertBarPositionProfile(barPositions[index], `${name}.barPositions[${index}]`);
|
|
@@ -4468,12 +4770,14 @@ function assertReferencePhraseMelody(value, name) {
|
|
|
4468
4770
|
assertOneOf(read.shape, MELODIC_CONTOUR_SHAPES, `${name}.shape`);
|
|
4469
4771
|
assertMidiPitch(read.low, `${name}.low`);
|
|
4470
4772
|
assertMidiPitch(read.high, `${name}.high`);
|
|
4471
|
-
assertRange(read.mean, 0,
|
|
4472
|
-
assertRange(read.peakPitch, 0,
|
|
4773
|
+
assertRange(read.mean, 0, MIDI_MAX, `${name}.mean`);
|
|
4774
|
+
assertRange(read.peakPitch, 0, MIDI_MAX, `${name}.peakPitch`);
|
|
4473
4775
|
assertRange(read.peakPosition, 0, 1, `${name}.peakPosition`);
|
|
4474
4776
|
const outline = assertArray(read.outline, `${name}.outline`);
|
|
4475
|
-
if (outline.length !==
|
|
4476
|
-
throw new InvalidInputError(
|
|
4777
|
+
if (outline.length !== PHRASE_OUTLINE_POINTS) {
|
|
4778
|
+
throw new InvalidInputError(
|
|
4779
|
+
`${name}.outline must have length ${PHRASE_OUTLINE_POINTS}; received ${outline.length}`
|
|
4780
|
+
);
|
|
4477
4781
|
}
|
|
4478
4782
|
for (let index = 0; index < outline.length; index += 1) {
|
|
4479
4783
|
assertFiniteNumber(outline[index], `${name}.outline[${index}]`);
|
|
@@ -4482,11 +4786,13 @@ function assertReferencePhraseMelody(value, name) {
|
|
|
4482
4786
|
assertRange(read.restRatio, 0, 1, `${name}.restRatio`);
|
|
4483
4787
|
return read;
|
|
4484
4788
|
}
|
|
4485
|
-
function assertReferencePhrase(value, name, sectionCount, nodeCount) {
|
|
4789
|
+
function assertReferencePhrase(value, name, span, sectionCount, nodeCount) {
|
|
4486
4790
|
const read = assertRecord(value, name);
|
|
4487
4791
|
const startBeat = assertFiniteNumber(read.startBeat, `${name}.startBeat`);
|
|
4488
4792
|
const endBeat = assertFiniteNumber(read.endBeat, `${name}.endBeat`);
|
|
4489
4793
|
assertSpanOrder(startBeat, endBeat, name);
|
|
4794
|
+
assertRange(startBeat, span.startBeat, span.endBeat, `${name}.startBeat`);
|
|
4795
|
+
assertRange(endBeat, span.startBeat, span.endBeat, `${name}.endBeat`);
|
|
4490
4796
|
assertRange(read.bars, 0, Number.MAX_SAFE_INTEGER, `${name}.bars`);
|
|
4491
4797
|
assertNullable(
|
|
4492
4798
|
read.section,
|
|
@@ -4507,7 +4813,7 @@ function assertReferencePhrase(value, name, sectionCount, nodeCount) {
|
|
|
4507
4813
|
}
|
|
4508
4814
|
return read;
|
|
4509
4815
|
}
|
|
4510
|
-
function assertReferenceForm(value, name, nodeCount) {
|
|
4816
|
+
function assertReferenceForm(value, name, span, nodeCount) {
|
|
4511
4817
|
const read = assertRecord(value, name);
|
|
4512
4818
|
const hypermeter = assertRecord(
|
|
4513
4819
|
read.hypermeter,
|
|
@@ -4521,7 +4827,13 @@ function assertReferenceForm(value, name, nodeCount) {
|
|
|
4521
4827
|
}
|
|
4522
4828
|
const phrases = assertArray(read.phrases, `${name}.phrases`);
|
|
4523
4829
|
for (let index = 0; index < phrases.length; index += 1) {
|
|
4524
|
-
assertReferencePhrase(
|
|
4830
|
+
assertReferencePhrase(
|
|
4831
|
+
phrases[index],
|
|
4832
|
+
`${name}.phrases[${index}]`,
|
|
4833
|
+
span,
|
|
4834
|
+
sections.length,
|
|
4835
|
+
nodeCount
|
|
4836
|
+
);
|
|
4525
4837
|
}
|
|
4526
4838
|
return read;
|
|
4527
4839
|
}
|
|
@@ -4661,9 +4973,9 @@ function assertReferenceMotif(value, name) {
|
|
|
4661
4973
|
low = Math.min(low, pitch);
|
|
4662
4974
|
high = Math.max(high, pitch);
|
|
4663
4975
|
}
|
|
4664
|
-
if (high - low >
|
|
4976
|
+
if (high - low > MIDI_MAX) {
|
|
4665
4977
|
throw new InvalidInputError(
|
|
4666
|
-
`${name}.intervals must span at most
|
|
4978
|
+
`${name}.intervals must span at most ${MIDI_MAX} semitones; received ${high - low}`
|
|
4667
4979
|
);
|
|
4668
4980
|
}
|
|
4669
4981
|
const rhythm = assertArray(read.rhythm, `${name}.rhythm`);
|
|
@@ -4694,7 +5006,7 @@ function assertReferenceMelody(value, name) {
|
|
|
4694
5006
|
const register = assertRecord(v, n);
|
|
4695
5007
|
assertMidiPitch(register.low, `${n}.low`);
|
|
4696
5008
|
assertMidiPitch(register.high, `${n}.high`);
|
|
4697
|
-
assertRange(register.mean, 0,
|
|
5009
|
+
assertRange(register.mean, 0, MIDI_MAX, `${n}.mean`);
|
|
4698
5010
|
return register;
|
|
4699
5011
|
});
|
|
4700
5012
|
assertNullable(read.shape, `${name}.shape`, (v, n) => assertOneOf(v, MELODIC_CONTOUR_SHAPES, n));
|
|
@@ -4725,7 +5037,7 @@ function assertReferenceProfile(value, name = "reference profile") {
|
|
|
4725
5037
|
assertRange(span.bars, 0, Number.MAX_SAFE_INTEGER, `${name}.span.bars`);
|
|
4726
5038
|
assertMeterMap(read.meters, `${name}.meters`);
|
|
4727
5039
|
const melody = assertReferenceMelody(read.melody, `${name}.melody`);
|
|
4728
|
-
assertReferenceForm(read.form, `${name}.form`, melody.graph.nodes.length);
|
|
5040
|
+
assertReferenceForm(read.form, `${name}.form`, { startBeat, endBeat }, melody.graph.nodes.length);
|
|
4729
5041
|
assertReferenceHarmony(read.harmony, `${name}.harmony`);
|
|
4730
5042
|
return read;
|
|
4731
5043
|
}
|
|
@@ -4742,11 +5054,12 @@ var PEAK_POSITION_WEIGHT = 0.5;
|
|
|
4742
5054
|
var FAMILY_SIZE_BINS = 5;
|
|
4743
5055
|
var DEPTH_BINS = 4;
|
|
4744
5056
|
var NAMED_CHORDS = ["Ger", "It", "Fr", "N"];
|
|
4745
|
-
var
|
|
5057
|
+
var PITCH_PRESERVING_KINDS = [
|
|
4746
5058
|
"repetition",
|
|
4747
5059
|
"augmentation",
|
|
4748
5060
|
"diminution"
|
|
4749
|
-
]
|
|
5061
|
+
];
|
|
5062
|
+
var PITCH_PRESERVING = new Set(PITCH_PRESERVING_KINDS);
|
|
4750
5063
|
var CELL_START_PITCH = 60;
|
|
4751
5064
|
function whenBoth(hasA, hasB, measure) {
|
|
4752
5065
|
if (!hasA && !hasB) {
|
|
@@ -4899,7 +5212,7 @@ function cellNotes(motif) {
|
|
|
4899
5212
|
low = Math.min(low, offset);
|
|
4900
5213
|
high = Math.max(high, offset);
|
|
4901
5214
|
}
|
|
4902
|
-
let pitch = Math.min(
|
|
5215
|
+
let pitch = Math.min(MIDI_MAX - high, Math.max(-low, CELL_START_PITCH));
|
|
4903
5216
|
let beat = 0;
|
|
4904
5217
|
for (let i = 0; i <= motif.intervals.length; i += 1) {
|
|
4905
5218
|
const step = motif.rhythm[i] ?? 1;
|
|
@@ -5073,8 +5386,8 @@ function phraseMelody(line, startBeat, endBeat, lineMean, meters, budget) {
|
|
|
5073
5386
|
}
|
|
5074
5387
|
const length = endBeat - startBeat;
|
|
5075
5388
|
const outline = [];
|
|
5076
|
-
for (let k = 0; k <
|
|
5077
|
-
const at = startBeat + (k + 0.5) * length /
|
|
5389
|
+
for (let k = 0; k < PHRASE_OUTLINE_POINTS; k += 1) {
|
|
5390
|
+
const at = startBeat + (k + 0.5) * length / PHRASE_OUTLINE_POINTS;
|
|
5078
5391
|
let held = first;
|
|
5079
5392
|
for (const note of inside) {
|
|
5080
5393
|
if (note.startBeat > at + BEAT_EPS) {
|
|
@@ -5084,7 +5397,7 @@ function phraseMelody(line, startBeat, endBeat, lineMean, meters, budget) {
|
|
|
5084
5397
|
}
|
|
5085
5398
|
outline.push(held.pitch - lineMean);
|
|
5086
5399
|
}
|
|
5087
|
-
const rhythm =
|
|
5400
|
+
const rhythm = rhythmOver(inside, { meters, totalBeats: endBeat, budget }, startBeat);
|
|
5088
5401
|
return {
|
|
5089
5402
|
shape: melodicContour(inside).shape,
|
|
5090
5403
|
low,
|
|
@@ -5110,6 +5423,16 @@ export {
|
|
|
5110
5423
|
detectKey,
|
|
5111
5424
|
detectKeyFromNotes,
|
|
5112
5425
|
detectKeyBest,
|
|
5426
|
+
barSpanOf,
|
|
5427
|
+
clamp01,
|
|
5428
|
+
floorMod,
|
|
5429
|
+
firstSoundingBeat,
|
|
5430
|
+
resolveSpanEnd,
|
|
5431
|
+
lastBarOf,
|
|
5432
|
+
sliceBars,
|
|
5433
|
+
weightSimilarity,
|
|
5434
|
+
harmonicNovelty,
|
|
5435
|
+
combineEvidence,
|
|
5113
5436
|
augmentedSixthChord,
|
|
5114
5437
|
augmentedSixthKind,
|
|
5115
5438
|
augmentedSixthFromPitchClasses,
|
|
@@ -5130,16 +5453,8 @@ export {
|
|
|
5130
5453
|
secondaryDominant,
|
|
5131
5454
|
secondaryDominantOf,
|
|
5132
5455
|
pivotChords,
|
|
5133
|
-
barSpanOf,
|
|
5134
|
-
clamp01,
|
|
5135
|
-
floorMod,
|
|
5136
|
-
firstSoundingBeat,
|
|
5137
|
-
lastBarOf,
|
|
5138
|
-
sliceBars,
|
|
5139
|
-
weightSimilarity,
|
|
5140
|
-
harmonicNovelty,
|
|
5141
|
-
combineEvidence,
|
|
5142
5456
|
gridForNotes,
|
|
5457
|
+
latestSoundingEnd,
|
|
5143
5458
|
barGridStart,
|
|
5144
5459
|
keyTimelineFromNotes,
|
|
5145
5460
|
detectModulations,
|
|
@@ -5149,6 +5464,8 @@ export {
|
|
|
5149
5464
|
toVoiceNotes,
|
|
5150
5465
|
analyzeVoice,
|
|
5151
5466
|
assertChordTimeline,
|
|
5467
|
+
assertSpansInOrder,
|
|
5468
|
+
statedKeyRegions,
|
|
5152
5469
|
chordTimelineFromChords,
|
|
5153
5470
|
chordAtBeat,
|
|
5154
5471
|
segmentStartingAt,
|
|
@@ -5169,6 +5486,7 @@ export {
|
|
|
5169
5486
|
motifGraph,
|
|
5170
5487
|
REDUCTION_LEVELS,
|
|
5171
5488
|
reduceProgression,
|
|
5489
|
+
REFERENCE_PROFILE_VERSION,
|
|
5172
5490
|
sectionSequenceSimilarity,
|
|
5173
5491
|
phraseLengthSimilarity,
|
|
5174
5492
|
progressionSimilarity,
|
|
@@ -5187,9 +5505,9 @@ export {
|
|
|
5187
5505
|
ioiBinIndex,
|
|
5188
5506
|
RHYTHM_IOI_BINS,
|
|
5189
5507
|
analyzeRhythm,
|
|
5190
|
-
|
|
5508
|
+
rhythmOver,
|
|
5191
5509
|
assertReferenceProfile,
|
|
5192
5510
|
weightedMean,
|
|
5193
5511
|
phraseMelody
|
|
5194
5512
|
};
|
|
5195
|
-
//# sourceMappingURL=chunk-
|
|
5513
|
+
//# sourceMappingURL=chunk-FJFULJZE.js.map
|