@libraz/libcantus 0.9.0 → 0.9.1
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 +10 -2
- package/dist/index.cjs +577 -225
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1533 -172
- package/dist/index.d.ts +1533 -172
- package/dist/index.js +577 -225
- package/dist/index.js.map +1 -1
- package/package.json +6 -1
- package/src/analysis/index.ts +73 -7
- package/src/arrange/index.ts +241 -79
- package/src/bass/index.ts +61 -12
- package/src/chord/index.ts +66 -3
- package/src/chordscale/index.ts +67 -9
- package/src/countermelody/index.ts +44 -14
- package/src/counterpoint/index.ts +21 -0
- package/src/detect/index.ts +79 -20
- package/src/drums/fills.ts +31 -20
- package/src/drums/index.ts +125 -7
- package/src/drums/internal.ts +7 -1
- package/src/drums/swing.ts +7 -2
- package/src/functional/index.ts +160 -51
- package/src/groove/index.ts +116 -20
- package/src/harmonize/index.ts +85 -12
- package/src/harmony/index.ts +19 -2
- package/src/index.ts +1 -0
- package/src/interval/index.ts +15 -1
- package/src/meter/index.ts +108 -5
- package/src/model/index.ts +161 -12
- package/src/motif/index.ts +87 -10
- package/src/pitch/index.ts +58 -3
- package/src/progression/index.ts +56 -8
- package/src/random/index.ts +13 -1
- package/src/reharmony/index.ts +35 -4
- package/src/rhythm/index.ts +45 -11
- package/src/safety/index.ts +58 -11
- package/src/scale/index.ts +135 -19
- package/src/spelling/index.ts +18 -0
- package/src/symbol/index.ts +84 -36
- package/src/timeline/index.ts +80 -13
- package/src/tuning/index.ts +28 -1
- package/src/voicing/index.ts +121 -21
package/dist/index.cjs
CHANGED
|
@@ -557,12 +557,24 @@ function isLeadingToneResolution(prev, cur, key) {
|
|
|
557
557
|
}
|
|
558
558
|
|
|
559
559
|
// src/analysis/index.ts
|
|
560
|
+
var EPS = 1e-9;
|
|
560
561
|
function pitchClass4(pitch) {
|
|
561
562
|
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
562
563
|
}
|
|
563
564
|
function intervalAboveRoot(pitch, chord) {
|
|
564
565
|
return ((pitchClass4(pitch) - pitchClass4(chord.rootPc)) % 12 + 12) % 12;
|
|
565
566
|
}
|
|
567
|
+
function intervalAboveBass(pitch, others, chord) {
|
|
568
|
+
let bass = Number.POSITIVE_INFINITY;
|
|
569
|
+
for (const ov of others) {
|
|
570
|
+
bass = Math.min(bass, ov.pitch);
|
|
571
|
+
}
|
|
572
|
+
if (Number.isFinite(bass) && bass < pitch) {
|
|
573
|
+
return ((pitch - bass) % 12 + 12) % 12;
|
|
574
|
+
}
|
|
575
|
+
const ref = chord.bassPc ?? chord.rootPc;
|
|
576
|
+
return ((pitchClass4(pitch) - pitchClass4(ref)) % 12 + 12) % 12;
|
|
577
|
+
}
|
|
566
578
|
function tensionDegree(ic) {
|
|
567
579
|
if (ic === 1 || ic === 2) {
|
|
568
580
|
return 9;
|
|
@@ -607,8 +619,10 @@ function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
|
|
|
607
619
|
if (!note) {
|
|
608
620
|
continue;
|
|
609
621
|
}
|
|
610
|
-
const
|
|
611
|
-
const
|
|
622
|
+
const prevNote = i > 0 ? voice[i - 1] : void 0;
|
|
623
|
+
const nextNote = i + 1 < voice.length ? voice[i + 1] : void 0;
|
|
624
|
+
const prev = prevNote !== void 0 && prevNote.startBeat < note.startBeat - EPS ? prevNote : void 0;
|
|
625
|
+
const next = nextNote !== void 0 && nextNote.startBeat > note.startBeat + EPS ? nextNote : void 0;
|
|
612
626
|
const chord = chordAtBeat(note.startBeat);
|
|
613
627
|
const labels = [];
|
|
614
628
|
const member = isChordMember(note.pitch, chord);
|
|
@@ -627,11 +641,12 @@ function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
|
|
|
627
641
|
const prevChord = chordAtBeat(prev.startBeat);
|
|
628
642
|
const prepared = prev.pitch === note.pitch && isChordMember(prev.pitch, prevChord);
|
|
629
643
|
const resolves = isStep(next.pitch, note.pitch);
|
|
630
|
-
const
|
|
644
|
+
const others = otherVoicesAtBeat(note.startBeat);
|
|
645
|
+
const verticallyDissonant = others.some(
|
|
631
646
|
(ov) => createsVerticalDissonance(note.pitch, ov.pitch, true)
|
|
632
647
|
);
|
|
633
648
|
if (prepared && verticallyDissonant && resolves) {
|
|
634
|
-
const ic =
|
|
649
|
+
const ic = intervalAboveBass(note.pitch, others, chord);
|
|
635
650
|
const sus = suspensionType(ic, next.pitch - note.pitch);
|
|
636
651
|
sus.resolveTo = next.pitch;
|
|
637
652
|
labels.push(sus);
|
|
@@ -805,11 +820,43 @@ function seventhQuality(base, halfDim) {
|
|
|
805
820
|
return base === "maj" ? "dom7" : "min7";
|
|
806
821
|
}
|
|
807
822
|
var INVERSION_FIGURES = /* @__PURE__ */ new Set(["6", "64", "65", "43", "42", "2", "7"]);
|
|
808
|
-
var
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
823
|
+
var ROMAN_STYLE = {
|
|
824
|
+
maj: { lower: false, suffix: "" },
|
|
825
|
+
min: { lower: true, suffix: "" },
|
|
826
|
+
dim: { lower: true, suffix: "o" },
|
|
827
|
+
aug: { lower: false, suffix: "+" },
|
|
828
|
+
maj7: { lower: false, suffix: "maj7" },
|
|
829
|
+
min7: { lower: true, suffix: "7" },
|
|
830
|
+
dom7: { lower: false, suffix: "7" },
|
|
831
|
+
dim7: { lower: true, suffix: "o7" },
|
|
832
|
+
m7b5: { lower: true, suffix: "\xF87" },
|
|
833
|
+
minMaj7: { lower: true, suffix: "maj7" },
|
|
834
|
+
aug7: { lower: false, suffix: "+7" },
|
|
835
|
+
augMaj7: { lower: false, suffix: "+maj7" },
|
|
836
|
+
majb5: { lower: false, suffix: "b5" },
|
|
837
|
+
"6": { lower: false, suffix: "add6" },
|
|
838
|
+
min6: { lower: true, suffix: "add6" },
|
|
839
|
+
"6/9": { lower: false, suffix: "69" },
|
|
840
|
+
sus2: { lower: false, suffix: "sus2" },
|
|
841
|
+
sus4: { lower: false, suffix: "sus4" },
|
|
842
|
+
add9: { lower: false, suffix: "add9" },
|
|
843
|
+
add11: { lower: false, suffix: "add11" },
|
|
844
|
+
maj9: { lower: false, suffix: "maj9" },
|
|
845
|
+
min9: { lower: true, suffix: "9" },
|
|
846
|
+
dom9: { lower: false, suffix: "9" },
|
|
847
|
+
"7b9": { lower: false, suffix: "7b9" },
|
|
848
|
+
"7#9": { lower: false, suffix: "7#9" },
|
|
849
|
+
"7#11": { lower: false, suffix: "7#11" },
|
|
850
|
+
"7b13": { lower: false, suffix: "7b13" },
|
|
851
|
+
"11": { lower: false, suffix: "11" },
|
|
852
|
+
"13": { lower: false, suffix: "13" },
|
|
853
|
+
"5": { lower: false, suffix: "5" }
|
|
812
854
|
};
|
|
855
|
+
var SUFFIX_QUALITY = new Map(
|
|
856
|
+
Object.entries(ROMAN_STYLE).map(
|
|
857
|
+
([quality, style]) => [`${style.lower ? "l" : "u"}:${style.suffix}`, quality]
|
|
858
|
+
)
|
|
859
|
+
);
|
|
813
860
|
function parseInversion(figures) {
|
|
814
861
|
if (figures === "65") {
|
|
815
862
|
return { inversion: 1, seventh: true };
|
|
@@ -845,15 +892,15 @@ function parseSimpleRoman(text, key) {
|
|
|
845
892
|
const isUpper = (match[2] ?? "")[0] === (match[2] ?? "")[0]?.toUpperCase();
|
|
846
893
|
const suffix = match[3] ?? "";
|
|
847
894
|
const rootPc = mod12(degreeRootPc(degreeNumber, key) + accidental);
|
|
895
|
+
const canonical = SUFFIX_QUALITY.get(`${isUpper ? "u" : "l"}:${suffix}`);
|
|
896
|
+
if (canonical !== void 0) {
|
|
897
|
+
return { rootPc, quality: canonical, inversion: 0 };
|
|
898
|
+
}
|
|
848
899
|
const isDim = /o|°|dim/.test(suffix);
|
|
849
900
|
const isAug = /\+|aug/.test(suffix);
|
|
850
901
|
const isHalfDim = /ø/.test(suffix);
|
|
851
902
|
const explicitMaj7 = /maj7|M7/.test(suffix);
|
|
852
903
|
const figures = suffix.replace(/maj7|M7/g, "").replace(/[^0-9]/g, "");
|
|
853
|
-
const extension = EXTENSION_FIGURES[figures];
|
|
854
|
-
if (extension !== void 0) {
|
|
855
|
-
return { rootPc, quality: extension, inversion: 0 };
|
|
856
|
-
}
|
|
857
904
|
if (figures !== "" && !INVERSION_FIGURES.has(figures)) {
|
|
858
905
|
throw new Error(`Unsupported figured-bass or extension "${figures}" in Roman numeral: ${text}`);
|
|
859
906
|
}
|
|
@@ -869,7 +916,7 @@ function parseSimpleRoman(text, key) {
|
|
|
869
916
|
}
|
|
870
917
|
let quality;
|
|
871
918
|
if (explicitMaj7) {
|
|
872
|
-
quality = base === "min" ? "minMaj7" : "maj7";
|
|
919
|
+
quality = base === "min" ? "minMaj7" : base === "aug" ? "augMaj7" : "maj7";
|
|
873
920
|
} else if (hasSeventh) {
|
|
874
921
|
quality = seventhQuality(base, isHalfDim);
|
|
875
922
|
} else if (base === "dim") {
|
|
@@ -901,30 +948,7 @@ function romanToChord(text, key) {
|
|
|
901
948
|
return chordFromParsed(parseSimpleRoman(trimmed, key));
|
|
902
949
|
}
|
|
903
950
|
function romanStyle(quality) {
|
|
904
|
-
|
|
905
|
-
case "min":
|
|
906
|
-
return { lower: true, suffix: "" };
|
|
907
|
-
case "dim":
|
|
908
|
-
return { lower: true, suffix: "o" };
|
|
909
|
-
case "aug":
|
|
910
|
-
return { lower: false, suffix: "+" };
|
|
911
|
-
case "maj7":
|
|
912
|
-
return { lower: false, suffix: "maj7" };
|
|
913
|
-
case "dom7":
|
|
914
|
-
return { lower: false, suffix: "7" };
|
|
915
|
-
case "min7":
|
|
916
|
-
return { lower: true, suffix: "7" };
|
|
917
|
-
case "m7b5":
|
|
918
|
-
return { lower: true, suffix: "\xF87" };
|
|
919
|
-
case "dim7":
|
|
920
|
-
return { lower: true, suffix: "o7" };
|
|
921
|
-
case "minMaj7":
|
|
922
|
-
return { lower: true, suffix: "maj7" };
|
|
923
|
-
case "maj":
|
|
924
|
-
return { lower: false, suffix: "" };
|
|
925
|
-
default:
|
|
926
|
-
return { lower: false, suffix: quality };
|
|
927
|
-
}
|
|
951
|
+
return ROMAN_STYLE[quality];
|
|
928
952
|
}
|
|
929
953
|
function romanSpelling(rootPc, key) {
|
|
930
954
|
const tones = scaleTonesInDegreeOrder(key);
|
|
@@ -959,6 +983,9 @@ function baseMarker(quality) {
|
|
|
959
983
|
if (quality === "aug" || quality === "aug7") {
|
|
960
984
|
return "+";
|
|
961
985
|
}
|
|
986
|
+
if (quality === "augMaj7") {
|
|
987
|
+
return "+maj7";
|
|
988
|
+
}
|
|
962
989
|
if (quality === "maj7" || quality === "minMaj7") {
|
|
963
990
|
return "maj7";
|
|
964
991
|
}
|
|
@@ -966,6 +993,17 @@ function baseMarker(quality) {
|
|
|
966
993
|
}
|
|
967
994
|
var TRIAD_FIGURES = { 1: "6", 2: "64" };
|
|
968
995
|
var SEVENTH_FIGURES = { 1: "65", 2: "43", 3: "42" };
|
|
996
|
+
var TRIAD_FIGURE_QUALITIES = /* @__PURE__ */ new Set(["maj", "min", "dim", "aug"]);
|
|
997
|
+
var SEVENTH_FIGURE_QUALITIES = /* @__PURE__ */ new Set([
|
|
998
|
+
"maj7",
|
|
999
|
+
"min7",
|
|
1000
|
+
"dom7",
|
|
1001
|
+
"dim7",
|
|
1002
|
+
"m7b5",
|
|
1003
|
+
"minMaj7",
|
|
1004
|
+
"aug7",
|
|
1005
|
+
"augMaj7"
|
|
1006
|
+
]);
|
|
969
1007
|
function chordToRoman(chord, key) {
|
|
970
1008
|
const { degreeNumber, accidental } = romanSpelling(chord.rootPc, key);
|
|
971
1009
|
const { lower, suffix } = romanStyle(chord.quality);
|
|
@@ -979,8 +1017,7 @@ function chordToRoman(chord, key) {
|
|
|
979
1017
|
}
|
|
980
1018
|
}
|
|
981
1019
|
if (inversion > 0) {
|
|
982
|
-
const
|
|
983
|
-
const figure = (isSeventh ? SEVENTH_FIGURES : TRIAD_FIGURES)[inversion];
|
|
1020
|
+
const figure = SEVENTH_FIGURE_QUALITIES.has(chord.quality) ? SEVENTH_FIGURES[inversion] : TRIAD_FIGURE_QUALITIES.has(chord.quality) ? TRIAD_FIGURES[inversion] : void 0;
|
|
984
1021
|
if (figure !== void 0) {
|
|
985
1022
|
return `${accidental}${cased}${baseMarker(chord.quality)}${figure}`;
|
|
986
1023
|
}
|
|
@@ -1079,13 +1116,13 @@ function secondaryDominant(targetDegree, key) {
|
|
|
1079
1116
|
}
|
|
1080
1117
|
|
|
1081
1118
|
// src/meter/index.ts
|
|
1082
|
-
var
|
|
1119
|
+
var EPS2 = 1e-9;
|
|
1083
1120
|
function isMultiple(value, unit) {
|
|
1084
1121
|
if (unit === 0) {
|
|
1085
1122
|
return false;
|
|
1086
1123
|
}
|
|
1087
1124
|
const ratio = value / unit;
|
|
1088
|
-
return Math.abs(ratio - Math.round(ratio)) <
|
|
1125
|
+
return Math.abs(ratio - Math.round(ratio)) < EPS2;
|
|
1089
1126
|
}
|
|
1090
1127
|
function parseTimeSignature(text) {
|
|
1091
1128
|
const match = /^\s*(\d+)\s*\/\s*(\d+)\s*$/.exec(text);
|
|
@@ -1117,6 +1154,29 @@ function pulseBeats(ts) {
|
|
|
1117
1154
|
function pulsesPerBar(ts) {
|
|
1118
1155
|
return beatsPerBar(ts) / pulseBeats(ts);
|
|
1119
1156
|
}
|
|
1157
|
+
function isValidGrouping(grouping, pulses) {
|
|
1158
|
+
if (grouping.length === 0) {
|
|
1159
|
+
return false;
|
|
1160
|
+
}
|
|
1161
|
+
let sum = 0;
|
|
1162
|
+
for (const g of grouping) {
|
|
1163
|
+
if (!Number.isInteger(g) || g <= 0) {
|
|
1164
|
+
return false;
|
|
1165
|
+
}
|
|
1166
|
+
sum += g;
|
|
1167
|
+
}
|
|
1168
|
+
return sum === pulses;
|
|
1169
|
+
}
|
|
1170
|
+
function isGroupHead(grouping, pulseIndex) {
|
|
1171
|
+
let acc = 0;
|
|
1172
|
+
for (const g of grouping) {
|
|
1173
|
+
if (acc === pulseIndex) {
|
|
1174
|
+
return true;
|
|
1175
|
+
}
|
|
1176
|
+
acc += g;
|
|
1177
|
+
}
|
|
1178
|
+
return false;
|
|
1179
|
+
}
|
|
1120
1180
|
function beatToBarPosition(beatInQuarters, ts) {
|
|
1121
1181
|
const barLen = beatsPerBar(ts);
|
|
1122
1182
|
const bar = Math.floor(beatInQuarters / barLen);
|
|
@@ -1136,6 +1196,15 @@ function metricWeight(beatInQuarters, ts) {
|
|
|
1136
1196
|
if (pulseIndex === 0) {
|
|
1137
1197
|
return 3;
|
|
1138
1198
|
}
|
|
1199
|
+
const grouping = ts.grouping;
|
|
1200
|
+
if (grouping !== void 0) {
|
|
1201
|
+
if (!isValidGrouping(grouping, pulses)) {
|
|
1202
|
+
throw new Error(
|
|
1203
|
+
`Invalid grouping [${grouping.join(", ")}] for ${formatTimeSignature(ts)}: entries must be positive integers summing to ${pulses}`
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
return isGroupHead(grouping, pulseIndex) ? 2 : 1;
|
|
1207
|
+
}
|
|
1139
1208
|
if (pulses % 2 === 0 && pulseIndex === pulses / 2) {
|
|
1140
1209
|
return 2;
|
|
1141
1210
|
}
|
|
@@ -1296,7 +1365,7 @@ function evaluateInternal(q, collectSuggestions) {
|
|
|
1296
1365
|
if (ov.prevPitch === void 0) {
|
|
1297
1366
|
continue;
|
|
1298
1367
|
}
|
|
1299
|
-
if (createsParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)
|
|
1368
|
+
if (createsParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {
|
|
1300
1369
|
reasons |= 2048 /* ParallelPerfect */;
|
|
1301
1370
|
raise(parallelSeverity);
|
|
1302
1371
|
}
|
|
@@ -1428,7 +1497,7 @@ function detectChord(pitches) {
|
|
|
1428
1497
|
quality,
|
|
1429
1498
|
missingPcs,
|
|
1430
1499
|
extraPcs,
|
|
1431
|
-
exact: extraPcs.length === 0,
|
|
1500
|
+
exact: extraPcs.length === 0 && missingPcs.length === 0,
|
|
1432
1501
|
inversion
|
|
1433
1502
|
};
|
|
1434
1503
|
if (inversion > 0) {
|
|
@@ -1460,38 +1529,49 @@ function detectChordBest(pitches) {
|
|
|
1460
1529
|
}
|
|
1461
1530
|
return makeChord(best.rootPc, best.quality, best.bassPc);
|
|
1462
1531
|
}
|
|
1532
|
+
var MINOR_MASK_VARIANTS = [NATURAL_MINOR_MASK, HARMONIC_MINOR_MASK, MELODIC_MINOR_MASK];
|
|
1463
1533
|
function detectKey(pitches) {
|
|
1464
1534
|
const input = uniquePitchClasses(pitches);
|
|
1535
|
+
if (input.length === 0) {
|
|
1536
|
+
return [];
|
|
1537
|
+
}
|
|
1465
1538
|
const counts = /* @__PURE__ */ new Map();
|
|
1466
1539
|
for (const pc of pitches.map(pitchClass6)) {
|
|
1467
1540
|
counts.set(pc, (counts.get(pc) ?? 0) + 1);
|
|
1468
1541
|
}
|
|
1469
|
-
const total = pitches.length
|
|
1542
|
+
const total = pitches.length;
|
|
1470
1543
|
const results = [];
|
|
1471
1544
|
for (let tonic = 0; tonic < 12; tonic += 1) {
|
|
1472
1545
|
for (const mode of ["major", "minor"]) {
|
|
1473
1546
|
const key = mode === "major" ? majorKey(tonic) : minorKey(tonic);
|
|
1547
|
+
const masks = mode === "major" ? [MAJOR_MASK] : MINOR_MASK_VARIANTS;
|
|
1474
1548
|
let inScale = 0;
|
|
1475
|
-
let weighted =
|
|
1476
|
-
for (const
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1549
|
+
let weighted = Number.NEGATIVE_INFINITY;
|
|
1550
|
+
for (const mask of masks) {
|
|
1551
|
+
let variantInScale = 0;
|
|
1552
|
+
let variantWeighted = 0;
|
|
1553
|
+
for (const pc of input) {
|
|
1554
|
+
const offset = (pc - tonic + 12) % 12;
|
|
1555
|
+
if (mask >> offset & 1) {
|
|
1556
|
+
variantInScale += 1;
|
|
1557
|
+
}
|
|
1480
1558
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1559
|
+
for (const [pc, count] of counts) {
|
|
1560
|
+
const offset = (pc - tonic + 12) % 12;
|
|
1561
|
+
if (mask >> offset & 1) {
|
|
1562
|
+
variantWeighted += count;
|
|
1563
|
+
}
|
|
1486
1564
|
}
|
|
1487
|
-
if (
|
|
1488
|
-
weighted
|
|
1565
|
+
if (variantWeighted > weighted) {
|
|
1566
|
+
weighted = variantWeighted;
|
|
1567
|
+
inScale = variantInScale;
|
|
1489
1568
|
}
|
|
1490
1569
|
}
|
|
1570
|
+
weighted += (counts.get(tonic) ?? 0) * 0.5;
|
|
1491
1571
|
results.push({
|
|
1492
1572
|
key,
|
|
1493
1573
|
mode,
|
|
1494
|
-
fit:
|
|
1574
|
+
fit: inScale / input.length,
|
|
1495
1575
|
score: weighted / total
|
|
1496
1576
|
});
|
|
1497
1577
|
}
|
|
@@ -1524,7 +1604,7 @@ function segmentLookup(segments) {
|
|
|
1524
1604
|
return null;
|
|
1525
1605
|
};
|
|
1526
1606
|
}
|
|
1527
|
-
var
|
|
1607
|
+
var EPS3 = 1e-9;
|
|
1528
1608
|
var NOISE_THRESHOLD_RATIO = 0.2;
|
|
1529
1609
|
var MAX_DETECTION_PCS = 6;
|
|
1530
1610
|
var FALLBACK_PCS = 3;
|
|
@@ -1566,11 +1646,11 @@ function analyzeWindow(notes, windowStart, windowEnd, ts, key) {
|
|
|
1566
1646
|
for (const note of notes) {
|
|
1567
1647
|
const noteEnd = note.startBeat + note.durationBeat;
|
|
1568
1648
|
const overlap = Math.min(noteEnd, windowEnd) - Math.max(note.startBeat, windowStart);
|
|
1569
|
-
if (overlap <=
|
|
1649
|
+
if (overlap <= EPS3) {
|
|
1570
1650
|
continue;
|
|
1571
1651
|
}
|
|
1572
1652
|
const velocityFactor = note.velocity !== void 0 ? note.velocity / 127 : 1;
|
|
1573
|
-
const onsetInWindow = note.startBeat >= windowStart -
|
|
1653
|
+
const onsetInWindow = note.startBeat >= windowStart - EPS3 && note.startBeat < windowEnd - EPS3;
|
|
1574
1654
|
const accent = onsetInWindow ? 1 + metricWeight(note.startBeat, ts) / 3 : 1;
|
|
1575
1655
|
const pc = pitchClass7(note.pitch);
|
|
1576
1656
|
weights[pc] = (weights[pc] ?? 0) + overlap * velocityFactor * accent;
|
|
@@ -1584,7 +1664,7 @@ function analyzeWindow(notes, windowStart, windowEnd, ts, key) {
|
|
|
1584
1664
|
totalWeight += w;
|
|
1585
1665
|
maxWeight = Math.max(maxWeight, w);
|
|
1586
1666
|
}
|
|
1587
|
-
if (totalWeight <=
|
|
1667
|
+
if (totalWeight <= EPS3) {
|
|
1588
1668
|
return null;
|
|
1589
1669
|
}
|
|
1590
1670
|
const ranked = weights.map((weight, pc) => ({ pc, weight })).filter(({ weight }) => weight > 0).sort((a, b) => b.weight - a.weight || a.pc - b.pc);
|
|
@@ -1621,21 +1701,22 @@ function chordTimelineFromNotes(notes, opts = {}) {
|
|
|
1621
1701
|
if (!(harmonicRhythm > 0)) {
|
|
1622
1702
|
throw new Error(`Invalid harmonic rhythm: ${harmonicRhythm}`);
|
|
1623
1703
|
}
|
|
1624
|
-
const
|
|
1704
|
+
const sounding = notes.filter((note) => note.durationBeat > 0);
|
|
1705
|
+
const lastNoteEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
|
|
1625
1706
|
const totalBeats = opts.totalBeats ?? lastNoteEnd;
|
|
1626
|
-
const key = opts.key ?? detectKey(
|
|
1707
|
+
const key = opts.key ?? detectKey(sounding.map((n) => n.pitch))[0]?.key ?? majorKey(0);
|
|
1627
1708
|
const segments = [];
|
|
1628
1709
|
const segmentConfidence = [];
|
|
1629
|
-
const windowCount = Math.max(0, Math.ceil(totalBeats / harmonicRhythm -
|
|
1710
|
+
const windowCount = Math.max(0, Math.ceil(totalBeats / harmonicRhythm - EPS3));
|
|
1630
1711
|
for (let i = 0; i < windowCount; i += 1) {
|
|
1631
1712
|
const start = i * harmonicRhythm;
|
|
1632
1713
|
const end = Math.min(start + harmonicRhythm, totalBeats);
|
|
1633
|
-
const inferred = analyzeWindow(
|
|
1714
|
+
const inferred = analyzeWindow(sounding, start, end, ts, key);
|
|
1634
1715
|
if (!inferred) {
|
|
1635
1716
|
continue;
|
|
1636
1717
|
}
|
|
1637
1718
|
const last = segments[segments.length - 1];
|
|
1638
|
-
if (last && sameChord(last.chord, inferred.chord) && Math.abs(last.endBeat - start) <
|
|
1719
|
+
if (last && sameChord(last.chord, inferred.chord) && Math.abs(last.endBeat - start) < EPS3) {
|
|
1639
1720
|
const lastLength = last.endBeat - last.startBeat;
|
|
1640
1721
|
const length = end - start;
|
|
1641
1722
|
const lastConfidence = segmentConfidence[segmentConfidence.length - 1] ?? 0;
|
|
@@ -1660,6 +1741,9 @@ function detectCadences(timeline, key) {
|
|
|
1660
1741
|
if (!prev || !cur) {
|
|
1661
1742
|
continue;
|
|
1662
1743
|
}
|
|
1744
|
+
if (Math.abs(cur.startBeat - prev.endBeat) > EPS3) {
|
|
1745
|
+
continue;
|
|
1746
|
+
}
|
|
1663
1747
|
const type = detectCadence(prev.chord, cur.chord, key);
|
|
1664
1748
|
if (type !== null) {
|
|
1665
1749
|
hits.push({ atBeat: cur.startBeat, type, from: prev.chord, to: cur.chord });
|
|
@@ -1669,67 +1753,101 @@ function detectCadences(timeline, key) {
|
|
|
1669
1753
|
}
|
|
1670
1754
|
|
|
1671
1755
|
// src/arrange/index.ts
|
|
1672
|
-
var
|
|
1756
|
+
var EPS4 = 1e-9;
|
|
1673
1757
|
function pitchClass8(pitch) {
|
|
1674
1758
|
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
1675
1759
|
}
|
|
1676
1760
|
function covers(note, beat) {
|
|
1677
|
-
return beat >= note.startBeat -
|
|
1761
|
+
return beat >= note.startBeat - EPS4 && beat < note.endBeat - EPS4;
|
|
1678
1762
|
}
|
|
1679
1763
|
function poolNotes(tracks) {
|
|
1680
1764
|
const notes = [];
|
|
1681
1765
|
for (const track of tracks) {
|
|
1682
1766
|
for (const note of track.notes) {
|
|
1683
|
-
|
|
1767
|
+
if (note.durationBeat > 0) {
|
|
1768
|
+
notes.push(note);
|
|
1769
|
+
}
|
|
1684
1770
|
}
|
|
1685
1771
|
}
|
|
1686
1772
|
return notes;
|
|
1687
1773
|
}
|
|
1774
|
+
function splitIntoSubVoices(ordered) {
|
|
1775
|
+
const lanes = [];
|
|
1776
|
+
for (const note of ordered) {
|
|
1777
|
+
const lane = lanes.find((candidate) => {
|
|
1778
|
+
const last = candidate[candidate.length - 1];
|
|
1779
|
+
return last !== void 0 && last.startBeat + last.durationBeat <= note.startBeat + EPS4;
|
|
1780
|
+
});
|
|
1781
|
+
if (lane) {
|
|
1782
|
+
lane.push(note);
|
|
1783
|
+
} else {
|
|
1784
|
+
lanes.push([note]);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
return lanes;
|
|
1788
|
+
}
|
|
1688
1789
|
function prepareTracks(tracks) {
|
|
1689
1790
|
let nextId = 0;
|
|
1690
1791
|
return tracks.map((track, index) => {
|
|
1691
|
-
const ordered =
|
|
1692
|
-
const
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1792
|
+
const ordered = track.notes.filter((note) => note.durationBeat > 0).sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch).map((note) => ({ ...note, id: nextId++ }));
|
|
1793
|
+
const voices = splitIntoSubVoices(ordered).map((voice) => ({
|
|
1794
|
+
voice,
|
|
1795
|
+
sounding: voice.map((note, i) => {
|
|
1796
|
+
const prev = i > 0 ? voice[i - 1] : void 0;
|
|
1797
|
+
return {
|
|
1798
|
+
pitch: note.pitch,
|
|
1799
|
+
prevPitch: prev?.pitch,
|
|
1800
|
+
startBeat: note.startBeat,
|
|
1801
|
+
endBeat: note.startBeat + note.durationBeat
|
|
1802
|
+
};
|
|
1803
|
+
})
|
|
1804
|
+
}));
|
|
1702
1805
|
return {
|
|
1703
1806
|
name: track.name ?? `track ${index + 1}`,
|
|
1704
1807
|
role: track.role ?? "other",
|
|
1705
|
-
|
|
1706
|
-
sounding
|
|
1808
|
+
voices
|
|
1707
1809
|
};
|
|
1708
1810
|
});
|
|
1709
1811
|
}
|
|
1710
|
-
function otherVoicesSounding(prepared,
|
|
1812
|
+
function otherVoicesSounding(prepared, excludeTrack, excludeVoice, beat) {
|
|
1711
1813
|
const out = [];
|
|
1712
1814
|
for (let t = 0; t < prepared.length; t += 1) {
|
|
1713
|
-
if (t === excludeIndex) {
|
|
1714
|
-
continue;
|
|
1715
|
-
}
|
|
1716
1815
|
const track = prepared[t];
|
|
1717
1816
|
if (!track) {
|
|
1718
1817
|
continue;
|
|
1719
1818
|
}
|
|
1720
|
-
for (
|
|
1721
|
-
if (
|
|
1819
|
+
for (let v = 0; v < track.voices.length; v += 1) {
|
|
1820
|
+
if (t === excludeTrack && v === excludeVoice) {
|
|
1821
|
+
continue;
|
|
1822
|
+
}
|
|
1823
|
+
const subVoice = track.voices[v];
|
|
1824
|
+
if (!subVoice) {
|
|
1722
1825
|
continue;
|
|
1723
1826
|
}
|
|
1724
|
-
const
|
|
1725
|
-
|
|
1726
|
-
|
|
1827
|
+
for (const note of subVoice.sounding) {
|
|
1828
|
+
if (!covers(note, beat)) {
|
|
1829
|
+
continue;
|
|
1830
|
+
}
|
|
1831
|
+
const snap = { pitch: note.pitch };
|
|
1832
|
+
if (note.prevPitch !== void 0) {
|
|
1833
|
+
snap.prevPitch = note.prevPitch;
|
|
1834
|
+
}
|
|
1835
|
+
out.push(snap);
|
|
1727
1836
|
}
|
|
1728
|
-
out.push(snap);
|
|
1729
1837
|
}
|
|
1730
1838
|
}
|
|
1731
1839
|
return out;
|
|
1732
1840
|
}
|
|
1841
|
+
function evaluationBeats(note, timeline) {
|
|
1842
|
+
const noteEnd = note.startBeat + note.durationBeat;
|
|
1843
|
+
const beats = [note.startBeat];
|
|
1844
|
+
for (const segment of timeline.segments) {
|
|
1845
|
+
if (segment.startBeat > note.startBeat + EPS4 && segment.startBeat < noteEnd - EPS4) {
|
|
1846
|
+
beats.push(segment.startBeat);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
return beats;
|
|
1850
|
+
}
|
|
1733
1851
|
function analyzeArrangement(tracks, opts = {}) {
|
|
1734
1852
|
const ts = opts.ts ?? parseTimeSignature("4/4");
|
|
1735
1853
|
const profile = opts.profile ?? "pop";
|
|
@@ -1748,36 +1866,48 @@ function analyzeArrangement(tracks, opts = {}) {
|
|
|
1748
1866
|
if (!track) {
|
|
1749
1867
|
continue;
|
|
1750
1868
|
}
|
|
1751
|
-
const notes =
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1869
|
+
const notes = [];
|
|
1870
|
+
for (let v = 0; v < track.voices.length; v += 1) {
|
|
1871
|
+
const subVoice = track.voices[v];
|
|
1872
|
+
if (!subVoice) {
|
|
1873
|
+
continue;
|
|
1874
|
+
}
|
|
1875
|
+
notes.push(
|
|
1876
|
+
...analyzeVoice(
|
|
1877
|
+
subVoice.voice,
|
|
1878
|
+
timeline.at,
|
|
1879
|
+
key,
|
|
1880
|
+
(beat) => otherVoicesSounding(prepared, t, v, beat)
|
|
1881
|
+
)
|
|
1882
|
+
);
|
|
1883
|
+
for (const note of subVoice.voice) {
|
|
1884
|
+
for (const beat of evaluationBeats(note, timeline)) {
|
|
1885
|
+
const result = evaluateSafety({
|
|
1886
|
+
profile,
|
|
1887
|
+
candidatePitch: note.pitch,
|
|
1888
|
+
chord: timeline.at(beat),
|
|
1889
|
+
key,
|
|
1890
|
+
otherVoices: otherVoicesSounding(prepared, t, v, beat),
|
|
1891
|
+
strongBeat: isStrongBeat(beat, ts)
|
|
1892
|
+
});
|
|
1893
|
+
if (result.safety !== 0 /* Safe */) {
|
|
1894
|
+
const conflict = {
|
|
1895
|
+
beat,
|
|
1896
|
+
trackName: track.name,
|
|
1897
|
+
pitch: note.pitch,
|
|
1898
|
+
safety: result.safety,
|
|
1899
|
+
reasons: result.reasons
|
|
1900
|
+
};
|
|
1901
|
+
if (result.rationale !== void 0) {
|
|
1902
|
+
conflict.rationale = result.rationale;
|
|
1903
|
+
}
|
|
1904
|
+
conflicts.push(conflict);
|
|
1905
|
+
}
|
|
1777
1906
|
}
|
|
1778
|
-
conflicts.push(conflict);
|
|
1779
1907
|
}
|
|
1780
1908
|
}
|
|
1909
|
+
notes.sort((a, b) => a.noteId - b.noteId);
|
|
1910
|
+
trackAnalyses.push({ name: track.name, role: track.role, notes });
|
|
1781
1911
|
}
|
|
1782
1912
|
conflicts.sort((a, b) => b.safety - a.safety || a.beat - b.beat);
|
|
1783
1913
|
return { key, timeline, segmentConfidence, cadences, tracks: trackAnalyses, conflicts };
|
|
@@ -1810,7 +1940,7 @@ function tensionCurve(tracks, opts = {}) {
|
|
|
1810
1940
|
});
|
|
1811
1941
|
const prepared = prepareTracks(tracks);
|
|
1812
1942
|
const points = [];
|
|
1813
|
-
const sampleCount = Math.max(0, Math.ceil(totalBeats / step -
|
|
1943
|
+
const sampleCount = Math.max(0, Math.ceil(totalBeats / step - EPS4));
|
|
1814
1944
|
for (let i = 0; i < sampleCount; i += 1) {
|
|
1815
1945
|
const beat = i * step;
|
|
1816
1946
|
points.push({ beat, tension: sampleTension(prepared, timeline, key, ts, profile, beat) });
|
|
@@ -1824,9 +1954,11 @@ function allSounding(prepared, beat) {
|
|
|
1824
1954
|
if (!track) {
|
|
1825
1955
|
continue;
|
|
1826
1956
|
}
|
|
1827
|
-
for (const
|
|
1828
|
-
|
|
1829
|
-
|
|
1957
|
+
for (const subVoice of track.voices) {
|
|
1958
|
+
for (const note of subVoice.sounding) {
|
|
1959
|
+
if (covers(note, beat)) {
|
|
1960
|
+
out.push({ pitch: note.pitch, track: t });
|
|
1961
|
+
}
|
|
1830
1962
|
}
|
|
1831
1963
|
}
|
|
1832
1964
|
}
|
|
@@ -1902,7 +2034,7 @@ var DEFAULT_OCTAVE = 2;
|
|
|
1902
2034
|
var STRONG_VELOCITY = 100;
|
|
1903
2035
|
var WEAK_VELOCITY = 80;
|
|
1904
2036
|
var PICKUP_PROB = 0.35;
|
|
1905
|
-
var
|
|
2037
|
+
var EPS5 = 1e-9;
|
|
1906
2038
|
function pitchClass9(value) {
|
|
1907
2039
|
return (Math.trunc(value) % 12 + 12) % 12;
|
|
1908
2040
|
}
|
|
@@ -1933,15 +2065,16 @@ function chordTonePcs(chord) {
|
|
|
1933
2065
|
return pcs;
|
|
1934
2066
|
}
|
|
1935
2067
|
function fifthPcOf(chord) {
|
|
1936
|
-
const
|
|
1937
|
-
|
|
2068
|
+
const fifths = new Set(chord.intervals.map((i) => pitchClass9(i)));
|
|
2069
|
+
const chosen = fifths.has(7) ? 7 : fifths.has(6) ? 6 : fifths.has(8) ? 8 : void 0;
|
|
2070
|
+
return chosen === void 0 ? pitchClass9(chord.rootPc) : pitchClass9(chord.rootPc + chosen);
|
|
1938
2071
|
}
|
|
1939
2072
|
function bassPcOf(chord) {
|
|
1940
2073
|
return pitchClass9(chord.bassPc ?? chord.rootPc);
|
|
1941
2074
|
}
|
|
1942
2075
|
function beatPositions(start, end) {
|
|
1943
2076
|
const positions = [];
|
|
1944
|
-
for (let p = start; p < end -
|
|
2077
|
+
for (let p = start; p < end - EPS5; p += 1) {
|
|
1945
2078
|
positions.push(p);
|
|
1946
2079
|
}
|
|
1947
2080
|
if (positions.length === 0) {
|
|
@@ -1961,7 +2094,7 @@ function buildRoot(ctx, seg) {
|
|
|
1961
2094
|
function buildRootFifth(ctx, seg) {
|
|
1962
2095
|
ctx.prevMidi = emit(ctx, seg.startBeat, bassPcOf(seg.chord));
|
|
1963
2096
|
const mid = (seg.startBeat + seg.endBeat) / 2;
|
|
1964
|
-
if (mid > seg.startBeat +
|
|
2097
|
+
if (mid > seg.startBeat + EPS5 && mid < seg.endBeat - EPS5) {
|
|
1965
2098
|
ctx.prevMidi = emit(ctx, mid, fifthPcOf(seg.chord));
|
|
1966
2099
|
}
|
|
1967
2100
|
}
|
|
@@ -1976,7 +2109,7 @@ function buildPop(ctx, seg) {
|
|
|
1976
2109
|
} else if (ctx.rng.prob(PICKUP_PROB)) {
|
|
1977
2110
|
if (ctx.rng.prob(0.5)) {
|
|
1978
2111
|
const base = placePc(rootPc, ctx.prevMidi, ctx.low);
|
|
1979
|
-
emit(ctx, pos, rootPc, base - 12);
|
|
2112
|
+
emit(ctx, pos, rootPc, Math.max(ctx.low, base - 12));
|
|
1980
2113
|
} else {
|
|
1981
2114
|
ctx.prevMidi = emit(ctx, pos, fifthPcOf(seg.chord));
|
|
1982
2115
|
}
|
|
@@ -2072,7 +2205,7 @@ function generateBassLine(opts) {
|
|
|
2072
2205
|
}
|
|
2073
2206
|
const nextStart = ctx.notes[i + 1]?.startBeat ?? lastEnd;
|
|
2074
2207
|
const durationBeat = nextStart - note.startBeat;
|
|
2075
|
-
if (durationBeat <=
|
|
2208
|
+
if (durationBeat <= EPS5) {
|
|
2076
2209
|
continue;
|
|
2077
2210
|
}
|
|
2078
2211
|
out.push({
|
|
@@ -2108,18 +2241,22 @@ function scaleMatchesChord(chordPcs, scaleMask, scaleRootPc) {
|
|
|
2108
2241
|
}
|
|
2109
2242
|
return true;
|
|
2110
2243
|
}
|
|
2244
|
+
var ALIASED_SCALE_NAMES = /* @__PURE__ */ new Set(["major", "naturalMinor"]);
|
|
2245
|
+
var HEPTATONIC_SIZE = 7;
|
|
2111
2246
|
function chordScales(chord) {
|
|
2112
2247
|
const chordPcs = chordPitchClasses(chord);
|
|
2113
2248
|
const rootPc = pitchClass10(chord.rootPc);
|
|
2114
2249
|
const ranked = [];
|
|
2250
|
+
const seenMasks = /* @__PURE__ */ new Set();
|
|
2115
2251
|
for (const name of Object.keys(NAMED_SCALES)) {
|
|
2116
|
-
if (name === "chromatic") {
|
|
2252
|
+
if (name === "chromatic" || ALIASED_SCALE_NAMES.has(name)) {
|
|
2117
2253
|
continue;
|
|
2118
2254
|
}
|
|
2119
2255
|
const mask = NAMED_SCALES[name];
|
|
2120
|
-
if (mask === void 0) {
|
|
2256
|
+
if (mask === void 0 || seenMasks.has(mask)) {
|
|
2121
2257
|
continue;
|
|
2122
2258
|
}
|
|
2259
|
+
seenMasks.add(mask);
|
|
2123
2260
|
if (scaleMatchesChord(chordPcs, mask, rootPc)) {
|
|
2124
2261
|
const size = popcount12(mask);
|
|
2125
2262
|
ranked.push({ name, extra: size - chordPcs.length, size });
|
|
@@ -2131,7 +2268,11 @@ function chordScales(chord) {
|
|
|
2131
2268
|
}
|
|
2132
2269
|
return [];
|
|
2133
2270
|
}
|
|
2271
|
+
const preferHeptatonic = chordPcs.length <= 3;
|
|
2134
2272
|
ranked.sort((a, b) => {
|
|
2273
|
+
if (preferHeptatonic && a.size === HEPTATONIC_SIZE !== (b.size === HEPTATONIC_SIZE)) {
|
|
2274
|
+
return a.size === HEPTATONIC_SIZE ? -1 : 1;
|
|
2275
|
+
}
|
|
2135
2276
|
if (a.extra !== b.extra) {
|
|
2136
2277
|
return a.extra - b.extra;
|
|
2137
2278
|
}
|
|
@@ -2283,7 +2424,7 @@ function scalesForChanges(chords) {
|
|
|
2283
2424
|
|
|
2284
2425
|
// src/countermelody/index.ts
|
|
2285
2426
|
var DEFAULT_TS2 = { numerator: 4, denominator: 4 };
|
|
2286
|
-
var
|
|
2427
|
+
var EPS6 = 1e-9;
|
|
2287
2428
|
var GRID_STEP = 0.5;
|
|
2288
2429
|
var MIN_ONSET_GAP = 1;
|
|
2289
2430
|
var REGISTER_OFFSET = 12;
|
|
@@ -2294,12 +2435,12 @@ var REINFORCE_PROB = 0.35;
|
|
|
2294
2435
|
var DEFAULT_MELODY_VELOCITY = 96;
|
|
2295
2436
|
var VELOCITY_DROP = 16;
|
|
2296
2437
|
function isWholeBeat(beat) {
|
|
2297
|
-
return Math.abs(beat - Math.round(beat)) <
|
|
2438
|
+
return Math.abs(beat - Math.round(beat)) < EPS6;
|
|
2298
2439
|
}
|
|
2299
2440
|
function melodyNoteAt(melody, beat) {
|
|
2300
2441
|
let found;
|
|
2301
2442
|
for (const note of melody) {
|
|
2302
|
-
if (note.startBeat -
|
|
2443
|
+
if (note.startBeat - EPS6 <= beat && beat < note.startBeat + note.durationBeat - EPS6) {
|
|
2303
2444
|
if (!found || note.startBeat > found.startBeat) {
|
|
2304
2445
|
found = note;
|
|
2305
2446
|
}
|
|
@@ -2308,7 +2449,7 @@ function melodyNoteAt(melody, beat) {
|
|
|
2308
2449
|
return found;
|
|
2309
2450
|
}
|
|
2310
2451
|
function melodyAttacksAt(melody, beat) {
|
|
2311
|
-
return melody.some((note) => Math.abs(note.startBeat - beat) <
|
|
2452
|
+
return melody.some((note) => Math.abs(note.startBeat - beat) < EPS6);
|
|
2312
2453
|
}
|
|
2313
2454
|
function followOnsets(melody) {
|
|
2314
2455
|
return [...new Set(melody.map((note) => note.startBeat))].sort((a, b) => a - b);
|
|
@@ -2317,7 +2458,7 @@ function complementOnsets(melody, ts, spanStart, spanEnd, rng) {
|
|
|
2317
2458
|
const onsets = [];
|
|
2318
2459
|
const gridStart = Math.floor(spanStart / GRID_STEP) * GRID_STEP;
|
|
2319
2460
|
let last = Number.NEGATIVE_INFINITY;
|
|
2320
|
-
for (let beat = gridStart; beat < spanEnd -
|
|
2461
|
+
for (let beat = gridStart; beat < spanEnd - EPS6; beat += GRID_STEP) {
|
|
2321
2462
|
const attacked = melodyAttacksAt(melody, beat);
|
|
2322
2463
|
const sounding = melodyNoteAt(melody, beat) !== void 0;
|
|
2323
2464
|
let place = false;
|
|
@@ -2328,7 +2469,7 @@ function complementOnsets(melody, ts, spanStart, spanEnd, rng) {
|
|
|
2328
2469
|
} else if (isStrongBeat(beat, ts)) {
|
|
2329
2470
|
place = rng.prob(REINFORCE_PROB);
|
|
2330
2471
|
}
|
|
2331
|
-
if (place && beat - last >= MIN_ONSET_GAP -
|
|
2472
|
+
if (place && beat - last >= MIN_ONSET_GAP - EPS6) {
|
|
2332
2473
|
onsets.push(beat);
|
|
2333
2474
|
last = beat;
|
|
2334
2475
|
}
|
|
@@ -2446,13 +2587,15 @@ function generateCounterMelody(opts) {
|
|
|
2446
2587
|
if (register === "above" && candidate <= melPitch) {
|
|
2447
2588
|
continue;
|
|
2448
2589
|
}
|
|
2449
|
-
if (prevPitch !== void 0 && melPrev !== void 0 &&
|
|
2590
|
+
if (prevPitch !== void 0 && melPrev !== void 0 && // Parallel octaves are the perfect-class-zero case of this predicate,
|
|
2591
|
+
// so a single check covers both parallel fifths and octaves.
|
|
2592
|
+
createsParallelPerfect(prevPitch, candidate, melPrev, melPitch)) {
|
|
2450
2593
|
continue;
|
|
2451
2594
|
}
|
|
2452
2595
|
}
|
|
2453
2596
|
const score = scoreCandidate(candidate, melPitch, melPrev, prevPitch, chord, center);
|
|
2454
|
-
const tie = Math.abs(score - bestScore) <=
|
|
2455
|
-
if (score > bestScore +
|
|
2597
|
+
const tie = Math.abs(score - bestScore) <= EPS6 && candidate < (bestPitch ?? Number.POSITIVE_INFINITY);
|
|
2598
|
+
if (score > bestScore + EPS6 || tie) {
|
|
2456
2599
|
bestScore = score;
|
|
2457
2600
|
bestPitch = candidate;
|
|
2458
2601
|
}
|
|
@@ -2505,7 +2648,11 @@ var STYLE_MAP = {
|
|
|
2505
2648
|
bossa: { style: "latin", feel: "straight", ghostBoost: false, snareBeat3: false },
|
|
2506
2649
|
trap: { style: "trap", feel: "straight", ghostBoost: false, snareBeat3: true },
|
|
2507
2650
|
halftime: { style: "sparse", feel: "straight", ghostBoost: false, snareBeat3: true },
|
|
2508
|
-
breakbeat: { style: "rock", feel: "straight", ghostBoost: false, snareBeat3: false }
|
|
2651
|
+
breakbeat: { style: "rock", feel: "straight", ghostBoost: false, snareBeat3: false },
|
|
2652
|
+
// Four-on-the-floor dance kick with idol-style auxiliary percussion.
|
|
2653
|
+
house: { style: "fourOnFloor", feel: "straight", ghostBoost: false, snareBeat3: false },
|
|
2654
|
+
// Synth-pop pulse with denser 16th hi-hats and busy kick syncopation.
|
|
2655
|
+
synthpop: { style: "synth", feel: "straight", ghostBoost: true, snareBeat3: false }
|
|
2509
2656
|
};
|
|
2510
2657
|
function mapStyle(style) {
|
|
2511
2658
|
return STYLE_MAP[style];
|
|
@@ -2992,8 +3139,7 @@ function quantizeSwing(startBeat, swing, resolution) {
|
|
|
2992
3139
|
return base + EIGHTH2 + DELTA_8 * clamped;
|
|
2993
3140
|
}
|
|
2994
3141
|
if (offset >= EIGHTH2 + HALF_16TH && offset < 3 * SIXTEENTH2 + HALF_16TH) {
|
|
2995
|
-
|
|
2996
|
-
return Math.min(result, base + 1 - 1 / 480);
|
|
3142
|
+
return base + 3 * SIXTEENTH2 + DELTA_16 * clamped;
|
|
2997
3143
|
}
|
|
2998
3144
|
return startBeat;
|
|
2999
3145
|
}
|
|
@@ -3188,6 +3334,52 @@ function generateHiHatForBeat(ctx, sec) {
|
|
|
3188
3334
|
}
|
|
3189
3335
|
}
|
|
3190
3336
|
|
|
3337
|
+
// src/drums/euclid.ts
|
|
3338
|
+
function euclideanRhythm(pulses, steps, rotation = 0) {
|
|
3339
|
+
if (steps < 1) {
|
|
3340
|
+
return [];
|
|
3341
|
+
}
|
|
3342
|
+
const p = Math.max(0, Math.min(pulses, steps));
|
|
3343
|
+
const base = bjorklund(p, steps);
|
|
3344
|
+
const shift = (rotation % steps + steps) % steps;
|
|
3345
|
+
if (shift === 0) {
|
|
3346
|
+
return base;
|
|
3347
|
+
}
|
|
3348
|
+
return base.map((_, i) => base[(i - shift + steps) % steps] ?? false);
|
|
3349
|
+
}
|
|
3350
|
+
function hasHit(mask, step) {
|
|
3351
|
+
return (mask >> step & 1) === 1;
|
|
3352
|
+
}
|
|
3353
|
+
function patternToMask(pattern) {
|
|
3354
|
+
let mask = 0;
|
|
3355
|
+
for (let i = 0; i < pattern.length; i += 1) {
|
|
3356
|
+
if (pattern[i]) {
|
|
3357
|
+
mask |= 1 << i;
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
return mask;
|
|
3361
|
+
}
|
|
3362
|
+
function bjorklund(pulses, steps) {
|
|
3363
|
+
if (pulses === 0) {
|
|
3364
|
+
return Array.from({ length: steps }, () => false);
|
|
3365
|
+
}
|
|
3366
|
+
if (pulses >= steps) {
|
|
3367
|
+
return Array.from({ length: steps }, () => true);
|
|
3368
|
+
}
|
|
3369
|
+
let head = Array.from({ length: pulses }, () => [true]);
|
|
3370
|
+
let remainder = Array.from({ length: steps - pulses }, () => [false]);
|
|
3371
|
+
while (remainder.length > 1) {
|
|
3372
|
+
const count = Math.min(head.length, remainder.length);
|
|
3373
|
+
const next = [];
|
|
3374
|
+
for (let i = 0; i < count; i += 1) {
|
|
3375
|
+
next.push([...head[i] ?? [], ...remainder[i] ?? []]);
|
|
3376
|
+
}
|
|
3377
|
+
remainder = head.length > count ? head.slice(count) : remainder.slice(count);
|
|
3378
|
+
head = next;
|
|
3379
|
+
}
|
|
3380
|
+
return [...head.flat(), ...remainder.flat()];
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3191
3383
|
// src/drums/fills.ts
|
|
3192
3384
|
function getFillStartBeat(energy) {
|
|
3193
3385
|
switch (energy) {
|
|
@@ -3204,6 +3396,9 @@ function selectFillType(from, to, style, nextEnergy, rng) {
|
|
|
3204
3396
|
if (style === "sparse") {
|
|
3205
3397
|
return rng.range(0, 1) === 0 ? "simpleCrash" : "breakdownFill";
|
|
3206
3398
|
}
|
|
3399
|
+
const toChorus = to === "chorus";
|
|
3400
|
+
const fromIntro = from === "intro";
|
|
3401
|
+
const highEnergy = style === "rock" || style === "fourOnFloor";
|
|
3207
3402
|
if (nextEnergy === "low") {
|
|
3208
3403
|
switch (rng.range(0, 2)) {
|
|
3209
3404
|
case 0:
|
|
@@ -3214,25 +3409,10 @@ function selectFillType(from, to, style, nextEnergy, rng) {
|
|
|
3214
3409
|
return "halfTimeFill";
|
|
3215
3410
|
}
|
|
3216
3411
|
}
|
|
3217
|
-
if (nextEnergy === "peak") {
|
|
3218
|
-
switch (rng.range(0, 3)) {
|
|
3219
|
-
case 0:
|
|
3220
|
-
return "tomDescend";
|
|
3221
|
-
case 1:
|
|
3222
|
-
return "snareRoll";
|
|
3223
|
-
case 2:
|
|
3224
|
-
return "linearFill";
|
|
3225
|
-
default:
|
|
3226
|
-
return "flamsAndDrags";
|
|
3227
|
-
}
|
|
3228
|
-
}
|
|
3229
|
-
const toChorus = to === "chorus";
|
|
3230
|
-
const fromIntro = from === "intro";
|
|
3231
|
-
const highEnergy = style === "rock" || style === "fourOnFloor";
|
|
3232
|
-
const choice = rng.range(0, 7);
|
|
3233
3412
|
if (toChorus) {
|
|
3413
|
+
const choice2 = rng.range(0, 7);
|
|
3234
3414
|
if (highEnergy) {
|
|
3235
|
-
switch (
|
|
3415
|
+
switch (choice2) {
|
|
3236
3416
|
case 0:
|
|
3237
3417
|
case 1:
|
|
3238
3418
|
return "tomDescend";
|
|
@@ -3250,7 +3430,7 @@ function selectFillType(from, to, style, nextEnergy, rng) {
|
|
|
3250
3430
|
return "ghostToAccent";
|
|
3251
3431
|
}
|
|
3252
3432
|
}
|
|
3253
|
-
switch (
|
|
3433
|
+
switch (choice2) {
|
|
3254
3434
|
case 0:
|
|
3255
3435
|
case 1:
|
|
3256
3436
|
return "snareTomCombo";
|
|
@@ -3267,7 +3447,7 @@ function selectFillType(from, to, style, nextEnergy, rng) {
|
|
|
3267
3447
|
}
|
|
3268
3448
|
}
|
|
3269
3449
|
if (fromIntro) {
|
|
3270
|
-
switch (
|
|
3450
|
+
switch (rng.range(0, 5)) {
|
|
3271
3451
|
case 0:
|
|
3272
3452
|
case 1:
|
|
3273
3453
|
return "snareRoll";
|
|
@@ -3281,6 +3461,7 @@ function selectFillType(from, to, style, nextEnergy, rng) {
|
|
|
3281
3461
|
return "halfTimeFill";
|
|
3282
3462
|
}
|
|
3283
3463
|
}
|
|
3464
|
+
const choice = rng.range(0, 7);
|
|
3284
3465
|
if (highEnergy) {
|
|
3285
3466
|
switch (choice) {
|
|
3286
3467
|
case 0:
|
|
@@ -3463,6 +3644,10 @@ function generateFill(track, beatTick, beat, fillType, velocity) {
|
|
|
3463
3644
|
if (beat === 2) {
|
|
3464
3645
|
track.add(GM.SD, beatTick, 1, accentVel);
|
|
3465
3646
|
track.add(GM.BD, beatTick, 1, fillVel);
|
|
3647
|
+
} else if (beat === 3) {
|
|
3648
|
+
track.add(GM.SD, beatTick, EIGHTH, accentVel);
|
|
3649
|
+
track.add(GM.SD, beatTick + E, S, fillVel * 0.6);
|
|
3650
|
+
track.add(GM.SD, beatTick + E + S, S, accentVel);
|
|
3466
3651
|
}
|
|
3467
3652
|
break;
|
|
3468
3653
|
}
|
|
@@ -3505,6 +3690,19 @@ function isInPreChorusLift(section, bar, sectionBars, nextIsChorus) {
|
|
|
3505
3690
|
}
|
|
3506
3691
|
return bar >= sectionBars - 2;
|
|
3507
3692
|
}
|
|
3693
|
+
function euclideanToKickPattern(mask) {
|
|
3694
|
+
const slot = (step) => hasHit(mask, step) || hasHit(mask, (step + 15) % 16);
|
|
3695
|
+
return {
|
|
3696
|
+
beat1: slot(0),
|
|
3697
|
+
beat1and: slot(2),
|
|
3698
|
+
beat2: slot(4),
|
|
3699
|
+
beat2and: slot(6),
|
|
3700
|
+
beat3: slot(8),
|
|
3701
|
+
beat3and: slot(10),
|
|
3702
|
+
beat4: slot(12),
|
|
3703
|
+
beat4and: slot(14)
|
|
3704
|
+
};
|
|
3705
|
+
}
|
|
3508
3706
|
function getKickPattern(section, style, bar, rng) {
|
|
3509
3707
|
const p = { ...EMPTY };
|
|
3510
3708
|
if (section === "outro") {
|
|
@@ -3709,6 +3907,17 @@ function generateDrums(opts) {
|
|
|
3709
3907
|
const energy = sectionEnergy(section);
|
|
3710
3908
|
const fillStartBeat = getFillStartBeat(energy);
|
|
3711
3909
|
const percMood = percMoodCategory(style);
|
|
3910
|
+
const nextSection = opts.nextSection ? mapSection(opts.nextSection) : section;
|
|
3911
|
+
const nextEnergy = sectionEnergy(nextSection);
|
|
3912
|
+
const euclidKick = opts.euclideanKick ? euclideanToKickPattern(
|
|
3913
|
+
patternToMask(
|
|
3914
|
+
euclideanRhythm(
|
|
3915
|
+
opts.euclideanKick.pulses,
|
|
3916
|
+
opts.euclideanKick.steps ?? 16,
|
|
3917
|
+
opts.euclideanKick.rotation ?? 0
|
|
3918
|
+
)
|
|
3919
|
+
)
|
|
3920
|
+
) : void 0;
|
|
3712
3921
|
const reuseSectionKick = (section === "b" || section === "chorus") && style !== "sparse";
|
|
3713
3922
|
let sectionKick;
|
|
3714
3923
|
for (let bar = 0; bar < opts.bars; bar += 1) {
|
|
@@ -3724,7 +3933,9 @@ function generateDrums(opts) {
|
|
|
3724
3933
|
barHasOpenHh = !track.hasCrashNear(barStart + openHhBeatIndex);
|
|
3725
3934
|
}
|
|
3726
3935
|
let kick;
|
|
3727
|
-
if (
|
|
3936
|
+
if (euclidKick) {
|
|
3937
|
+
kick = euclidKick;
|
|
3938
|
+
} else if (reuseSectionKick) {
|
|
3728
3939
|
sectionKick ??= getKickPattern(section, style, 0, rng);
|
|
3729
3940
|
kick = sectionKick;
|
|
3730
3941
|
} else {
|
|
@@ -3737,12 +3948,17 @@ function generateDrums(opts) {
|
|
|
3737
3948
|
const velocity = calculateVelocity(section, beat);
|
|
3738
3949
|
if (opts.fills && isLastBar && !inLift && beat >= fillStartBeat) {
|
|
3739
3950
|
if (beat === fillStartBeat) {
|
|
3740
|
-
currentFill = selectFillType(section,
|
|
3951
|
+
currentFill = selectFillType(section, nextSection, style, nextEnergy, rng);
|
|
3741
3952
|
}
|
|
3742
3953
|
if (playMainVoices) {
|
|
3954
|
+
const before = track.hits.length;
|
|
3743
3955
|
generateFill(track, beatTick, beat, currentFill, velocity);
|
|
3956
|
+
if (track.hits.length > before) {
|
|
3957
|
+
continue;
|
|
3958
|
+
}
|
|
3959
|
+
} else {
|
|
3960
|
+
continue;
|
|
3744
3961
|
}
|
|
3745
|
-
continue;
|
|
3746
3962
|
}
|
|
3747
3963
|
const ctx = {
|
|
3748
3964
|
track,
|
|
@@ -3864,18 +4080,23 @@ function extractGrooveTemplate(events, ts, subdivision) {
|
|
|
3864
4080
|
for (let i = 0; i < slotsPerBar; i += 1) {
|
|
3865
4081
|
const count = counts[i] ?? 0;
|
|
3866
4082
|
if (count === 0) {
|
|
3867
|
-
slots.push({ timingOffset: 0, velocity:
|
|
4083
|
+
slots.push({ timingOffset: 0, velocity: null });
|
|
3868
4084
|
continue;
|
|
3869
4085
|
}
|
|
3870
4086
|
const velocityCount = velocityCounts[i] ?? 0;
|
|
3871
4087
|
slots.push({
|
|
3872
4088
|
timingOffset: (offsetSums[i] ?? 0) / count,
|
|
3873
|
-
velocity: velocityCount > 0 ? (velocitySums[i] ?? 0) / velocityCount :
|
|
4089
|
+
velocity: velocityCount > 0 ? (velocitySums[i] ?? 0) / velocityCount : null
|
|
3874
4090
|
});
|
|
3875
4091
|
}
|
|
3876
|
-
return { subdivision, slotsPerBar, slots };
|
|
4092
|
+
return { subdivision, slotsPerBar, slots, ts };
|
|
3877
4093
|
}
|
|
3878
4094
|
function applyGrooveTemplate(events, template, ts) {
|
|
4095
|
+
if (template.ts && (template.ts.numerator !== ts.numerator || template.ts.denominator !== ts.denominator)) {
|
|
4096
|
+
throw new Error(
|
|
4097
|
+
`Groove template meter ${formatTimeSignature(template.ts)} does not match apply-time meter ${formatTimeSignature(ts)}`
|
|
4098
|
+
);
|
|
4099
|
+
}
|
|
3879
4100
|
const barBeats = beatsPerBar(ts);
|
|
3880
4101
|
return events.map((event) => {
|
|
3881
4102
|
const { quantizedBeat, slotIndex } = quantizeToGrid(
|
|
@@ -3886,7 +4107,7 @@ function applyGrooveTemplate(events, template, ts) {
|
|
|
3886
4107
|
);
|
|
3887
4108
|
const slot = template.slots[slotIndex];
|
|
3888
4109
|
const startBeat = quantizedBeat + (slot?.timingOffset ?? 0);
|
|
3889
|
-
const velocity = slot && slot.velocity
|
|
4110
|
+
const velocity = slot && slot.velocity !== null && slot.velocity !== void 0 ? Math.round(slot.velocity) : event.velocity;
|
|
3890
4111
|
return {
|
|
3891
4112
|
pitch: event.pitch,
|
|
3892
4113
|
startBeat,
|
|
@@ -3939,6 +4160,7 @@ var DEFAULT_METER = { numerator: 4, denominator: 4 };
|
|
|
3939
4160
|
var COMFORT_LOW = 55;
|
|
3940
4161
|
var COMFORT_HIGH = 79;
|
|
3941
4162
|
var TESSITURA_WEIGHT = 1e-3;
|
|
4163
|
+
var TIE_BREAK_JITTER = 1e-6;
|
|
3942
4164
|
function pitchClass11(pitch) {
|
|
3943
4165
|
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
3944
4166
|
}
|
|
@@ -3953,26 +4175,33 @@ function mulberry32(seed) {
|
|
|
3953
4175
|
};
|
|
3954
4176
|
}
|
|
3955
4177
|
function inferKey(melody) {
|
|
3956
|
-
let best = 0;
|
|
4178
|
+
let best = majorKey(0);
|
|
3957
4179
|
let bestScore = Number.NEGATIVE_INFINITY;
|
|
4180
|
+
const candidates = [];
|
|
3958
4181
|
for (let tonic = 0; tonic < 12; tonic += 1) {
|
|
3959
|
-
|
|
4182
|
+
candidates.push(majorKey(tonic), minorKey(tonic));
|
|
4183
|
+
}
|
|
4184
|
+
for (const key of candidates) {
|
|
4185
|
+
const tonic = pitchClass11(key.rootPc);
|
|
4186
|
+
const isMinor = key.modeMask12 === NATURAL_MINOR_MASK;
|
|
4187
|
+
const leadingTone = (tonic + 11) % 12;
|
|
3960
4188
|
let score = 0;
|
|
3961
4189
|
for (const n of melody) {
|
|
3962
4190
|
const w = Math.max(0.25, n.durationBeat);
|
|
3963
|
-
|
|
4191
|
+
const pc = pitchClass11(n.pitch);
|
|
4192
|
+
if (isScaleTone(n.pitch, key) || isMinor && pc === leadingTone) {
|
|
3964
4193
|
score += w;
|
|
3965
4194
|
}
|
|
3966
|
-
if (
|
|
4195
|
+
if (pc === tonic) {
|
|
3967
4196
|
score += 0.5 * w;
|
|
3968
4197
|
}
|
|
3969
4198
|
}
|
|
3970
4199
|
if (score > bestScore) {
|
|
3971
4200
|
bestScore = score;
|
|
3972
|
-
best =
|
|
4201
|
+
best = key;
|
|
3973
4202
|
}
|
|
3974
4203
|
}
|
|
3975
|
-
return
|
|
4204
|
+
return best;
|
|
3976
4205
|
}
|
|
3977
4206
|
function buildCandidates(key, reharmonize) {
|
|
3978
4207
|
const tones = scaleTonesInDegreeOrder(key);
|
|
@@ -4124,7 +4353,7 @@ function harmonizeMelody(opts) {
|
|
|
4124
4353
|
const candidates = buildCandidates(key, opts.reharmonize);
|
|
4125
4354
|
const candAt = (i) => candidates[i] ?? FALLBACK;
|
|
4126
4355
|
const rng = mulberry32(opts.seed ?? 0);
|
|
4127
|
-
const jitter = candidates.map(() => rng() *
|
|
4356
|
+
const jitter = candidates.map(() => rng() * TIE_BREAK_JITTER);
|
|
4128
4357
|
const melodyEnd = opts.melody.reduce((m, n) => Math.max(m, n.startBeat + n.durationBeat), 0);
|
|
4129
4358
|
const hr = Math.max(0.25, opts.harmonicRhythm);
|
|
4130
4359
|
const segCount = Math.max(1, Math.ceil(melodyEnd / hr));
|
|
@@ -4321,6 +4550,8 @@ function spelledInterval(a, b) {
|
|
|
4321
4550
|
const reference = SIMPLE_REFERENCE[letterSteps + 1] ?? 0;
|
|
4322
4551
|
if (reference - rawSemis > 6) {
|
|
4323
4552
|
rawSemis += 12;
|
|
4553
|
+
} else if (rawSemis - reference > 6) {
|
|
4554
|
+
rawSemis -= 12;
|
|
4324
4555
|
}
|
|
4325
4556
|
semitones = rawSemis;
|
|
4326
4557
|
}
|
|
@@ -4545,8 +4776,8 @@ function noteNames(notes) {
|
|
|
4545
4776
|
}
|
|
4546
4777
|
|
|
4547
4778
|
// src/symbol/index.ts
|
|
4548
|
-
var ROOT_RE = /^[A-
|
|
4549
|
-
var BASS_RE = /^[A-
|
|
4779
|
+
var ROOT_RE = /^[A-Ga-g](?:#{1,2}|b{1,2})?/;
|
|
4780
|
+
var BASS_RE = /^[A-Ga-g](?:#{1,2}|b{1,2})?$/;
|
|
4550
4781
|
var QUALITY_MAP = {
|
|
4551
4782
|
"": "maj",
|
|
4552
4783
|
m: "min",
|
|
@@ -4641,8 +4872,17 @@ var CANONICAL_SUFFIX = {
|
|
|
4641
4872
|
function pitchClass12(value) {
|
|
4642
4873
|
return (Math.trunc(value) % 12 + 12) % 12;
|
|
4643
4874
|
}
|
|
4644
|
-
function
|
|
4645
|
-
|
|
4875
|
+
function makeSpelledChord(root, quality, bass) {
|
|
4876
|
+
const chord = makeChord(
|
|
4877
|
+
noteToPitchClass(root),
|
|
4878
|
+
quality,
|
|
4879
|
+
bass === void 0 ? void 0 : noteToPitchClass(bass)
|
|
4880
|
+
);
|
|
4881
|
+
chord.rootSpelling = { letter: root.letter, alter: root.alter };
|
|
4882
|
+
if (bass !== void 0) {
|
|
4883
|
+
chord.bassSpelling = { letter: bass.letter, alter: bass.alter };
|
|
4884
|
+
}
|
|
4885
|
+
return chord;
|
|
4646
4886
|
}
|
|
4647
4887
|
function parseChordSymbol(text) {
|
|
4648
4888
|
const trimmed = text.trim();
|
|
@@ -4651,37 +4891,37 @@ function parseChordSymbol(text) {
|
|
|
4651
4891
|
throw new Error(`Invalid chord symbol: ${text}`);
|
|
4652
4892
|
}
|
|
4653
4893
|
const rootToken = rootMatch[0];
|
|
4654
|
-
const
|
|
4894
|
+
const rootNote = parseNote(rootToken);
|
|
4655
4895
|
const rest = trimmed.slice(rootToken.length);
|
|
4656
|
-
const slashIndex = rest.
|
|
4657
|
-
if (slashIndex
|
|
4658
|
-
const
|
|
4659
|
-
|
|
4660
|
-
|
|
4896
|
+
const slashIndex = rest.lastIndexOf("/");
|
|
4897
|
+
if (slashIndex !== -1) {
|
|
4898
|
+
const before = rest.slice(0, slashIndex);
|
|
4899
|
+
const after = rest.slice(slashIndex + 1);
|
|
4900
|
+
const beforeQuality = QUALITY_MAP[before];
|
|
4901
|
+
if (beforeQuality !== void 0 && BASS_RE.test(after)) {
|
|
4902
|
+
return makeSpelledChord(rootNote, beforeQuality, parseNote(after));
|
|
4661
4903
|
}
|
|
4662
|
-
return makeChord(rootPc, quality);
|
|
4663
4904
|
}
|
|
4664
|
-
const
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
if (beforeQuality !== void 0 && BASS_RE.test(after)) {
|
|
4668
|
-
return makeChord(rootPc, beforeQuality, bassPitchClass(after));
|
|
4905
|
+
const quality = QUALITY_MAP[rest];
|
|
4906
|
+
if (quality === void 0) {
|
|
4907
|
+
throw new Error(`Unrecognized chord quality: ${text}`);
|
|
4669
4908
|
}
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4909
|
+
return makeSpelledChord(rootNote, quality);
|
|
4910
|
+
}
|
|
4911
|
+
function pitchClassName(pc, hint, flats) {
|
|
4912
|
+
if (flats === void 0 && hint !== void 0 && noteToPitchClass(hint) === pc) {
|
|
4913
|
+
return formatNote(hint);
|
|
4673
4914
|
}
|
|
4674
|
-
|
|
4915
|
+
const note = midiToNote(60 + pc, flats ? "flat" : "sharp");
|
|
4916
|
+
return formatNote({ letter: note.letter, alter: note.alter });
|
|
4675
4917
|
}
|
|
4676
4918
|
function formatChordSymbol(chord, opts) {
|
|
4677
|
-
const
|
|
4678
|
-
const
|
|
4679
|
-
const rootName = formatNote({ letter: rootNote.letter, alter: rootNote.alter });
|
|
4919
|
+
const rootPc = pitchClass12(chord.rootPc);
|
|
4920
|
+
const rootName = pitchClassName(rootPc, chord.rootSpelling, opts?.flats);
|
|
4680
4921
|
const suffix = CANONICAL_SUFFIX[chord.quality];
|
|
4681
4922
|
let symbol = `${rootName}${suffix}`;
|
|
4682
|
-
if (chord.bassPc !== void 0 && pitchClass12(chord.bassPc) !==
|
|
4683
|
-
|
|
4684
|
-
symbol += `/${formatNote({ letter: bassNote.letter, alter: bassNote.alter })}`;
|
|
4923
|
+
if (chord.bassPc !== void 0 && pitchClass12(chord.bassPc) !== rootPc) {
|
|
4924
|
+
symbol += `/${pitchClassName(pitchClass12(chord.bassPc), chord.bassSpelling, opts?.flats)}`;
|
|
4685
4925
|
}
|
|
4686
4926
|
return symbol;
|
|
4687
4927
|
}
|
|
@@ -4708,6 +4948,7 @@ var DERIVED_SPAN = 19;
|
|
|
4708
4948
|
var VIOLATION_PENALTY = 1e3;
|
|
4709
4949
|
var MISSING_TONE_PENALTY = 500;
|
|
4710
4950
|
var POOR_DOUBLING_PENALTY = 4;
|
|
4951
|
+
var HIDDEN_PERFECT_PENALTY = 6;
|
|
4711
4952
|
var MAX_CANDIDATES = 4e3;
|
|
4712
4953
|
function pitchClass13(value) {
|
|
4713
4954
|
return (Math.trunc(value) % 12 + 12) % 12;
|
|
@@ -4745,7 +4986,12 @@ function pitchesForPc(pc, range) {
|
|
|
4745
4986
|
result.push(midi);
|
|
4746
4987
|
}
|
|
4747
4988
|
}
|
|
4748
|
-
|
|
4989
|
+
const center = (range.min + range.max) / 2;
|
|
4990
|
+
return result.sort((a, b) => {
|
|
4991
|
+
const da = Math.abs(a - center);
|
|
4992
|
+
const db = Math.abs(b - center);
|
|
4993
|
+
return da === db ? a - b : da - db;
|
|
4994
|
+
});
|
|
4749
4995
|
}
|
|
4750
4996
|
function enumerateVoicings(chord, ranges, maxSpacing) {
|
|
4751
4997
|
const chordPcs = chordPitchClasses(chord);
|
|
@@ -4821,9 +5067,6 @@ function violationCount(prev, cur, maxSpacing) {
|
|
|
4821
5067
|
if (createsParallelPerfect(prevUpper, curUpper, prevLower, curLower)) {
|
|
4822
5068
|
count += 1;
|
|
4823
5069
|
}
|
|
4824
|
-
if (createsParallelOctave(prevUpper, curUpper, prevLower, curLower)) {
|
|
4825
|
-
count += 1;
|
|
4826
|
-
}
|
|
4827
5070
|
if (createsVoiceCrossing(curUpper, curLower)) {
|
|
4828
5071
|
count += 1;
|
|
4829
5072
|
}
|
|
@@ -4852,6 +5095,15 @@ function voiceLeadingCost(from, to) {
|
|
|
4852
5095
|
}
|
|
4853
5096
|
total += Math.abs(b - a);
|
|
4854
5097
|
}
|
|
5098
|
+
if (from.length >= 2) {
|
|
5099
|
+
const bassPrev = from[0];
|
|
5100
|
+
const bassCur = to[0];
|
|
5101
|
+
const sopPrev = from[from.length - 1];
|
|
5102
|
+
const sopCur = to[to.length - 1];
|
|
5103
|
+
if (bassPrev !== void 0 && bassCur !== void 0 && sopPrev !== void 0 && sopCur !== void 0 && createsHiddenParallelPerfect(bassPrev, bassCur, sopPrev, sopCur)) {
|
|
5104
|
+
total += HIDDEN_PERFECT_PENALTY;
|
|
5105
|
+
}
|
|
5106
|
+
}
|
|
4855
5107
|
return total;
|
|
4856
5108
|
}
|
|
4857
5109
|
function voiceChord(chord, opts) {
|
|
@@ -4990,7 +5242,10 @@ function voiceChordStyled(chord, opts) {
|
|
|
4990
5242
|
return stack.sort((a, b) => a - b);
|
|
4991
5243
|
}
|
|
4992
5244
|
function nextVoicing(current, chord, opts) {
|
|
4993
|
-
const ranges = opts?.ranges !== void 0 || opts?.voices !== void 0 ? resolveRanges(opts) : current.map((pitch) =>
|
|
5245
|
+
const ranges = opts?.ranges !== void 0 || opts?.voices !== void 0 ? resolveRanges(opts) : current.map((pitch) => {
|
|
5246
|
+
const centre = Math.min(127, Math.max(0, pitch));
|
|
5247
|
+
return { min: Math.max(0, centre - 12), max: Math.min(127, centre + 12) };
|
|
5248
|
+
});
|
|
4994
5249
|
const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
|
|
4995
5250
|
const candidates = enumerateVoicings(chord, ranges, maxSpacing);
|
|
4996
5251
|
let best;
|
|
@@ -5028,6 +5283,12 @@ function copyChord(data) {
|
|
|
5028
5283
|
if (data.bassPc !== void 0) {
|
|
5029
5284
|
copy.bassPc = data.bassPc;
|
|
5030
5285
|
}
|
|
5286
|
+
if (data.rootSpelling !== void 0) {
|
|
5287
|
+
copy.rootSpelling = { letter: data.rootSpelling.letter, alter: data.rootSpelling.alter };
|
|
5288
|
+
}
|
|
5289
|
+
if (data.bassSpelling !== void 0) {
|
|
5290
|
+
copy.bassSpelling = { letter: data.bassSpelling.letter, alter: data.bassSpelling.alter };
|
|
5291
|
+
}
|
|
5031
5292
|
return copy;
|
|
5032
5293
|
}
|
|
5033
5294
|
function spellPitchClassBare(pc, spelling) {
|
|
@@ -5113,12 +5374,16 @@ var Note = class _Note {
|
|
|
5113
5374
|
*
|
|
5114
5375
|
* When the note carries an octave the transposition happens in MIDI space and
|
|
5115
5376
|
* the result is spelled with a sharp preference. An octave-less note stays
|
|
5116
|
-
* octave-less: only its pitch class is moved.
|
|
5377
|
+
* octave-less: only its pitch class is moved. Transposing by zero is the
|
|
5378
|
+
* identity: the original spelling is preserved (no enharmonic respelling).
|
|
5117
5379
|
*
|
|
5118
5380
|
* @param semitones The signed semitone offset.
|
|
5119
5381
|
* @returns The transposed note.
|
|
5120
5382
|
*/
|
|
5121
5383
|
transpose(semitones) {
|
|
5384
|
+
if (semitones === 0) {
|
|
5385
|
+
return new _Note(this.#data);
|
|
5386
|
+
}
|
|
5122
5387
|
if (this.#data.octave !== void 0) {
|
|
5123
5388
|
return _Note.fromMidi(this.midi + semitones);
|
|
5124
5389
|
}
|
|
@@ -5142,6 +5407,17 @@ var Note = class _Note {
|
|
|
5142
5407
|
equals(other) {
|
|
5143
5408
|
return this.#data.letter === other.#data.letter && this.#data.alter === other.#data.alter && this.#data.octave === other.#data.octave;
|
|
5144
5409
|
}
|
|
5410
|
+
/**
|
|
5411
|
+
* The plain note data, for JSON serialization.
|
|
5412
|
+
*
|
|
5413
|
+
* Private class fields do not serialize, so an explicit `toJSON` keeps
|
|
5414
|
+
* `JSON.stringify(note)` from collapsing to `{}`.
|
|
5415
|
+
*
|
|
5416
|
+
* @returns A copy of the underlying plain note object.
|
|
5417
|
+
*/
|
|
5418
|
+
toJSON() {
|
|
5419
|
+
return this.data;
|
|
5420
|
+
}
|
|
5145
5421
|
};
|
|
5146
5422
|
var Interval = class _Interval {
|
|
5147
5423
|
#number;
|
|
@@ -5190,10 +5466,32 @@ var Interval = class _Interval {
|
|
|
5190
5466
|
get name() {
|
|
5191
5467
|
return `${this.#quality}${this.#number}`;
|
|
5192
5468
|
}
|
|
5469
|
+
/**
|
|
5470
|
+
* The plain interval data, for JSON serialization.
|
|
5471
|
+
*
|
|
5472
|
+
* Private class fields do not serialize, so an explicit `toJSON` keeps
|
|
5473
|
+
* `JSON.stringify(interval)` from collapsing to `{}`.
|
|
5474
|
+
*
|
|
5475
|
+
* @returns The diatonic number, quality, and semitone span.
|
|
5476
|
+
*/
|
|
5477
|
+
toJSON() {
|
|
5478
|
+
return { number: this.#number, quality: this.#quality, semitones: this.#semitones };
|
|
5479
|
+
}
|
|
5193
5480
|
};
|
|
5194
5481
|
function resolveTonic(root, spelling) {
|
|
5195
5482
|
return typeof root === "string" ? Note.of(root) : new Note(spellPitchClassBare(root, spelling));
|
|
5196
5483
|
}
|
|
5484
|
+
function accidentalLoad(tonic, scale) {
|
|
5485
|
+
return spellScale(tonic, scale).reduce((sum, note) => sum + Math.abs(note.alter), 0);
|
|
5486
|
+
}
|
|
5487
|
+
function bestTonicForScale(rootPc, scale) {
|
|
5488
|
+
const sharp = spellPitchClassBare(rootPc, "sharp");
|
|
5489
|
+
const flat = spellPitchClassBare(rootPc, "flat");
|
|
5490
|
+
if (sharp.letter === flat.letter && sharp.alter === flat.alter) {
|
|
5491
|
+
return new Note(sharp);
|
|
5492
|
+
}
|
|
5493
|
+
return accidentalLoad(flat, scale) <= accidentalLoad(sharp, scale) ? new Note(flat) : new Note(sharp);
|
|
5494
|
+
}
|
|
5197
5495
|
var Key = class _Key {
|
|
5198
5496
|
#scale;
|
|
5199
5497
|
#tonic;
|
|
@@ -5210,24 +5508,34 @@ var Key = class _Key {
|
|
|
5210
5508
|
/**
|
|
5211
5509
|
* A major key.
|
|
5212
5510
|
*
|
|
5213
|
-
* @param root Tonic as a note name (e.g. `'Eb'`) or a pitch class; numeric
|
|
5214
|
-
*
|
|
5511
|
+
* @param root Tonic as a note name (e.g. `'Eb'`) or a pitch class; a numeric
|
|
5512
|
+
* root is spelled with whichever accidental side yields the fewest
|
|
5513
|
+
* accidentals across the scale.
|
|
5215
5514
|
* @returns The major key.
|
|
5216
5515
|
*/
|
|
5217
5516
|
static major(root) {
|
|
5218
|
-
|
|
5219
|
-
|
|
5517
|
+
if (typeof root === "string") {
|
|
5518
|
+
const tonic = Note.of(root);
|
|
5519
|
+
return new _Key(majorKey(tonic.pitchClass), tonic);
|
|
5520
|
+
}
|
|
5521
|
+
const scale = majorKey(root);
|
|
5522
|
+
return new _Key(scale, bestTonicForScale(root, scale));
|
|
5220
5523
|
}
|
|
5221
5524
|
/**
|
|
5222
5525
|
* A natural-minor key.
|
|
5223
5526
|
*
|
|
5224
|
-
* @param root Tonic as a note name or a pitch class; numeric
|
|
5225
|
-
* spelled with
|
|
5527
|
+
* @param root Tonic as a note name or a pitch class; a numeric root is
|
|
5528
|
+
* spelled with whichever accidental side yields the fewest accidentals
|
|
5529
|
+
* across the scale.
|
|
5226
5530
|
* @returns The minor key.
|
|
5227
5531
|
*/
|
|
5228
5532
|
static minor(root) {
|
|
5229
|
-
|
|
5230
|
-
|
|
5533
|
+
if (typeof root === "string") {
|
|
5534
|
+
const tonic = Note.of(root);
|
|
5535
|
+
return new _Key(minorKey(tonic.pitchClass), tonic);
|
|
5536
|
+
}
|
|
5537
|
+
const scale = minorKey(root);
|
|
5538
|
+
return new _Key(scale, bestTonicForScale(root, scale));
|
|
5231
5539
|
}
|
|
5232
5540
|
/**
|
|
5233
5541
|
* A key on a named scale (e.g. `'dorian'`, `'harmonicMinor'`).
|
|
@@ -5354,6 +5662,19 @@ var Key = class _Key {
|
|
|
5354
5662
|
contains(x) {
|
|
5355
5663
|
return isScaleTone(typeof x === "number" ? x : x.pitchClass, this.#scale);
|
|
5356
5664
|
}
|
|
5665
|
+
/**
|
|
5666
|
+
* The plain key data, for JSON serialization.
|
|
5667
|
+
*
|
|
5668
|
+
* Private class fields do not serialize, so an explicit `toJSON` keeps
|
|
5669
|
+
* `JSON.stringify(key)` from collapsing to `{}`. The result pairs the
|
|
5670
|
+
* `KeyScale` with the spelled tonic, enough to reconstruct the key via
|
|
5671
|
+
* {@link Key.of}.
|
|
5672
|
+
*
|
|
5673
|
+
* @returns The key/scale and its spelled tonic.
|
|
5674
|
+
*/
|
|
5675
|
+
toJSON() {
|
|
5676
|
+
return { scale: this.scale, tonic: this.#tonic.data };
|
|
5677
|
+
}
|
|
5357
5678
|
};
|
|
5358
5679
|
var Chord = class _Chord {
|
|
5359
5680
|
#data;
|
|
@@ -5550,13 +5871,17 @@ var Chord = class _Chord {
|
|
|
5550
5871
|
* @throws If no key is given and none is carried.
|
|
5551
5872
|
*/
|
|
5552
5873
|
negativeHarmony(key) {
|
|
5553
|
-
|
|
5874
|
+
const resolved = this.#resolveKey(key);
|
|
5875
|
+
return new _Chord(negativeHarmonyMirror(this.#data, resolved.scale), key ?? this.#key);
|
|
5554
5876
|
}
|
|
5555
5877
|
/**
|
|
5556
5878
|
* The n-th inversion: a copy whose bass is the chord tone `n` steps above the
|
|
5557
5879
|
* root in the interval template (`invert(1)` puts the third in the bass).
|
|
5558
5880
|
* `n` wraps around the template length; negative values count backwards.
|
|
5559
5881
|
*
|
|
5882
|
+
* `invert(0)` (and any `n` that wraps to it) is root position, so it carries
|
|
5883
|
+
* no slash bass and equals the original chord.
|
|
5884
|
+
*
|
|
5560
5885
|
* @param n The inversion number.
|
|
5561
5886
|
* @returns The inverted chord, keeping any key context.
|
|
5562
5887
|
* @throws If the chord has no intervals.
|
|
@@ -5569,7 +5894,11 @@ var Chord = class _Chord {
|
|
|
5569
5894
|
}
|
|
5570
5895
|
const index = (n % length + length) % length;
|
|
5571
5896
|
const data = copyChord(this.#data);
|
|
5572
|
-
|
|
5897
|
+
if (index === 0) {
|
|
5898
|
+
delete data.bassPc;
|
|
5899
|
+
} else {
|
|
5900
|
+
data.bassPc = mod125(this.#data.rootPc + (intervals[index] ?? 0));
|
|
5901
|
+
}
|
|
5573
5902
|
return new _Chord(data, this.#key);
|
|
5574
5903
|
}
|
|
5575
5904
|
/**
|
|
@@ -5750,6 +6079,21 @@ var Progression = class _Progression {
|
|
|
5750
6079
|
const cadence = from !== void 0 && to !== void 0 ? detectCadence(from.data, to.data, resolved.scale) : null;
|
|
5751
6080
|
return { chords, cadence };
|
|
5752
6081
|
}
|
|
6082
|
+
/**
|
|
6083
|
+
* The plain progression data, for JSON serialization.
|
|
6084
|
+
*
|
|
6085
|
+
* Private class fields do not serialize, so an explicit `toJSON` keeps
|
|
6086
|
+
* `JSON.stringify(progression)` from collapsing to `{}`. The chords are
|
|
6087
|
+
* emitted as plain data and the carried key, when present, as its own data.
|
|
6088
|
+
*
|
|
6089
|
+
* @returns The chord data sequence and the carried key, if any.
|
|
6090
|
+
*/
|
|
6091
|
+
toJSON() {
|
|
6092
|
+
return {
|
|
6093
|
+
chords: this.#chords.map((chord) => chord.data),
|
|
6094
|
+
key: this.#key?.toJSON()
|
|
6095
|
+
};
|
|
6096
|
+
}
|
|
5753
6097
|
/** Resolve the key for an analysis method: explicit first, then carried. */
|
|
5754
6098
|
#resolveKey(key) {
|
|
5755
6099
|
const resolved = key ?? this.#key;
|
|
@@ -5827,7 +6171,6 @@ function mulberry322(seed) {
|
|
|
5827
6171
|
}
|
|
5828
6172
|
function contourOffsets(contour, count) {
|
|
5829
6173
|
const offsets = [];
|
|
5830
|
-
const half = Math.floor(count / 2);
|
|
5831
6174
|
for (let i = 0; i < count; i += 1) {
|
|
5832
6175
|
switch (contour) {
|
|
5833
6176
|
case "ascending":
|
|
@@ -5837,7 +6180,7 @@ function contourOffsets(contour, count) {
|
|
|
5837
6180
|
offsets.push(-i);
|
|
5838
6181
|
break;
|
|
5839
6182
|
case "arch":
|
|
5840
|
-
offsets.push(i
|
|
6183
|
+
offsets.push(Math.min(i, count - 1 - i));
|
|
5841
6184
|
break;
|
|
5842
6185
|
case "wave":
|
|
5843
6186
|
offsets.push([0, 1, 0, -1][i % 4] ?? 0);
|
|
@@ -5853,11 +6196,15 @@ function generateMotif(opts) {
|
|
|
5853
6196
|
const noteCount = Math.max(3, bars * 2);
|
|
5854
6197
|
const beatsPerNote = totalBeats / noteCount;
|
|
5855
6198
|
const rng = mulberry322(opts.seed ?? 0);
|
|
6199
|
+
const jitterProb = Math.min(1, Math.max(0, opts.jitter ?? 0));
|
|
5856
6200
|
const tonic = pitchClass14(opts.key.rootPc) + 60;
|
|
5857
6201
|
const offsets = contourOffsets(contour, noteCount);
|
|
5858
6202
|
const notes = [];
|
|
5859
6203
|
for (let i = 0; i < noteCount; i += 1) {
|
|
5860
|
-
|
|
6204
|
+
let jitter = 0;
|
|
6205
|
+
if (jitterProb > 0 && rng() < jitterProb) {
|
|
6206
|
+
jitter = rng() < 0.5 ? 1 : -1;
|
|
6207
|
+
}
|
|
5861
6208
|
let pitch = stepDiatonic(tonic, (offsets[i] ?? 0) + jitter, opts.key);
|
|
5862
6209
|
const startBeat = i * beatsPerNote;
|
|
5863
6210
|
if (opts.chord && startBeat % 4 === 0) {
|
|
@@ -6169,6 +6516,9 @@ function generateProgression(opts) {
|
|
|
6169
6516
|
let preset;
|
|
6170
6517
|
if (opts.presetId !== void 0) {
|
|
6171
6518
|
preset = PRESETS.find((p) => p.id === opts.presetId);
|
|
6519
|
+
if (preset === void 0) {
|
|
6520
|
+
throw new Error(`Unknown progression preset: ${opts.presetId}`);
|
|
6521
|
+
}
|
|
6172
6522
|
}
|
|
6173
6523
|
if (preset === void 0) {
|
|
6174
6524
|
const pool = PRESETS.filter((p) => p.styles.includes(opts.style));
|
|
@@ -6239,15 +6589,17 @@ function generateRhythm(ts, opts = {}) {
|
|
|
6239
6589
|
const seed = opts.seed ?? 0;
|
|
6240
6590
|
const bars = opts.bars ?? DEFAULT_BARS;
|
|
6241
6591
|
const subdivision = opts.subdivision ?? DEFAULT_SUBDIVISION;
|
|
6242
|
-
const density = opts.density ?? DEFAULT_DENSITY;
|
|
6243
|
-
const
|
|
6592
|
+
const density = Math.min(1, Math.max(0, opts.density ?? DEFAULT_DENSITY));
|
|
6593
|
+
const barBeats = beatsPerBar(ts);
|
|
6594
|
+
const spanBeats = barBeats * bars;
|
|
6244
6595
|
const step = 1 / subdivision;
|
|
6245
6596
|
const slotCount = Math.round(spanBeats * subdivision);
|
|
6597
|
+
const slotsPerBar = Math.round(barBeats * subdivision);
|
|
6246
6598
|
const rng = createRng(seed);
|
|
6247
6599
|
const positions = [];
|
|
6248
6600
|
for (let i = 0; i < slotCount; i += 1) {
|
|
6249
6601
|
const position = i * step;
|
|
6250
|
-
if (i === 0) {
|
|
6602
|
+
if (i % slotsPerBar === 0) {
|
|
6251
6603
|
positions.push(position);
|
|
6252
6604
|
continue;
|
|
6253
6605
|
}
|