@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.js
CHANGED
|
@@ -390,12 +390,24 @@ function isLeadingToneResolution(prev, cur, key) {
|
|
|
390
390
|
}
|
|
391
391
|
|
|
392
392
|
// src/analysis/index.ts
|
|
393
|
+
var EPS = 1e-9;
|
|
393
394
|
function pitchClass4(pitch) {
|
|
394
395
|
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
395
396
|
}
|
|
396
397
|
function intervalAboveRoot(pitch, chord) {
|
|
397
398
|
return ((pitchClass4(pitch) - pitchClass4(chord.rootPc)) % 12 + 12) % 12;
|
|
398
399
|
}
|
|
400
|
+
function intervalAboveBass(pitch, others, chord) {
|
|
401
|
+
let bass = Number.POSITIVE_INFINITY;
|
|
402
|
+
for (const ov of others) {
|
|
403
|
+
bass = Math.min(bass, ov.pitch);
|
|
404
|
+
}
|
|
405
|
+
if (Number.isFinite(bass) && bass < pitch) {
|
|
406
|
+
return ((pitch - bass) % 12 + 12) % 12;
|
|
407
|
+
}
|
|
408
|
+
const ref = chord.bassPc ?? chord.rootPc;
|
|
409
|
+
return ((pitchClass4(pitch) - pitchClass4(ref)) % 12 + 12) % 12;
|
|
410
|
+
}
|
|
399
411
|
function tensionDegree(ic) {
|
|
400
412
|
if (ic === 1 || ic === 2) {
|
|
401
413
|
return 9;
|
|
@@ -440,8 +452,10 @@ function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
|
|
|
440
452
|
if (!note) {
|
|
441
453
|
continue;
|
|
442
454
|
}
|
|
443
|
-
const
|
|
444
|
-
const
|
|
455
|
+
const prevNote = i > 0 ? voice[i - 1] : void 0;
|
|
456
|
+
const nextNote = i + 1 < voice.length ? voice[i + 1] : void 0;
|
|
457
|
+
const prev = prevNote !== void 0 && prevNote.startBeat < note.startBeat - EPS ? prevNote : void 0;
|
|
458
|
+
const next = nextNote !== void 0 && nextNote.startBeat > note.startBeat + EPS ? nextNote : void 0;
|
|
445
459
|
const chord = chordAtBeat(note.startBeat);
|
|
446
460
|
const labels = [];
|
|
447
461
|
const member = isChordMember(note.pitch, chord);
|
|
@@ -460,11 +474,12 @@ function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
|
|
|
460
474
|
const prevChord = chordAtBeat(prev.startBeat);
|
|
461
475
|
const prepared = prev.pitch === note.pitch && isChordMember(prev.pitch, prevChord);
|
|
462
476
|
const resolves = isStep(next.pitch, note.pitch);
|
|
463
|
-
const
|
|
477
|
+
const others = otherVoicesAtBeat(note.startBeat);
|
|
478
|
+
const verticallyDissonant = others.some(
|
|
464
479
|
(ov) => createsVerticalDissonance(note.pitch, ov.pitch, true)
|
|
465
480
|
);
|
|
466
481
|
if (prepared && verticallyDissonant && resolves) {
|
|
467
|
-
const ic =
|
|
482
|
+
const ic = intervalAboveBass(note.pitch, others, chord);
|
|
468
483
|
const sus = suspensionType(ic, next.pitch - note.pitch);
|
|
469
484
|
sus.resolveTo = next.pitch;
|
|
470
485
|
labels.push(sus);
|
|
@@ -638,11 +653,43 @@ function seventhQuality(base, halfDim) {
|
|
|
638
653
|
return base === "maj" ? "dom7" : "min7";
|
|
639
654
|
}
|
|
640
655
|
var INVERSION_FIGURES = /* @__PURE__ */ new Set(["6", "64", "65", "43", "42", "2", "7"]);
|
|
641
|
-
var
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
656
|
+
var ROMAN_STYLE = {
|
|
657
|
+
maj: { lower: false, suffix: "" },
|
|
658
|
+
min: { lower: true, suffix: "" },
|
|
659
|
+
dim: { lower: true, suffix: "o" },
|
|
660
|
+
aug: { lower: false, suffix: "+" },
|
|
661
|
+
maj7: { lower: false, suffix: "maj7" },
|
|
662
|
+
min7: { lower: true, suffix: "7" },
|
|
663
|
+
dom7: { lower: false, suffix: "7" },
|
|
664
|
+
dim7: { lower: true, suffix: "o7" },
|
|
665
|
+
m7b5: { lower: true, suffix: "\xF87" },
|
|
666
|
+
minMaj7: { lower: true, suffix: "maj7" },
|
|
667
|
+
aug7: { lower: false, suffix: "+7" },
|
|
668
|
+
augMaj7: { lower: false, suffix: "+maj7" },
|
|
669
|
+
majb5: { lower: false, suffix: "b5" },
|
|
670
|
+
"6": { lower: false, suffix: "add6" },
|
|
671
|
+
min6: { lower: true, suffix: "add6" },
|
|
672
|
+
"6/9": { lower: false, suffix: "69" },
|
|
673
|
+
sus2: { lower: false, suffix: "sus2" },
|
|
674
|
+
sus4: { lower: false, suffix: "sus4" },
|
|
675
|
+
add9: { lower: false, suffix: "add9" },
|
|
676
|
+
add11: { lower: false, suffix: "add11" },
|
|
677
|
+
maj9: { lower: false, suffix: "maj9" },
|
|
678
|
+
min9: { lower: true, suffix: "9" },
|
|
679
|
+
dom9: { lower: false, suffix: "9" },
|
|
680
|
+
"7b9": { lower: false, suffix: "7b9" },
|
|
681
|
+
"7#9": { lower: false, suffix: "7#9" },
|
|
682
|
+
"7#11": { lower: false, suffix: "7#11" },
|
|
683
|
+
"7b13": { lower: false, suffix: "7b13" },
|
|
684
|
+
"11": { lower: false, suffix: "11" },
|
|
685
|
+
"13": { lower: false, suffix: "13" },
|
|
686
|
+
"5": { lower: false, suffix: "5" }
|
|
645
687
|
};
|
|
688
|
+
var SUFFIX_QUALITY = new Map(
|
|
689
|
+
Object.entries(ROMAN_STYLE).map(
|
|
690
|
+
([quality, style]) => [`${style.lower ? "l" : "u"}:${style.suffix}`, quality]
|
|
691
|
+
)
|
|
692
|
+
);
|
|
646
693
|
function parseInversion(figures) {
|
|
647
694
|
if (figures === "65") {
|
|
648
695
|
return { inversion: 1, seventh: true };
|
|
@@ -678,15 +725,15 @@ function parseSimpleRoman(text, key) {
|
|
|
678
725
|
const isUpper = (match[2] ?? "")[0] === (match[2] ?? "")[0]?.toUpperCase();
|
|
679
726
|
const suffix = match[3] ?? "";
|
|
680
727
|
const rootPc = mod12(degreeRootPc(degreeNumber, key) + accidental);
|
|
728
|
+
const canonical = SUFFIX_QUALITY.get(`${isUpper ? "u" : "l"}:${suffix}`);
|
|
729
|
+
if (canonical !== void 0) {
|
|
730
|
+
return { rootPc, quality: canonical, inversion: 0 };
|
|
731
|
+
}
|
|
681
732
|
const isDim = /o|°|dim/.test(suffix);
|
|
682
733
|
const isAug = /\+|aug/.test(suffix);
|
|
683
734
|
const isHalfDim = /ø/.test(suffix);
|
|
684
735
|
const explicitMaj7 = /maj7|M7/.test(suffix);
|
|
685
736
|
const figures = suffix.replace(/maj7|M7/g, "").replace(/[^0-9]/g, "");
|
|
686
|
-
const extension = EXTENSION_FIGURES[figures];
|
|
687
|
-
if (extension !== void 0) {
|
|
688
|
-
return { rootPc, quality: extension, inversion: 0 };
|
|
689
|
-
}
|
|
690
737
|
if (figures !== "" && !INVERSION_FIGURES.has(figures)) {
|
|
691
738
|
throw new Error(`Unsupported figured-bass or extension "${figures}" in Roman numeral: ${text}`);
|
|
692
739
|
}
|
|
@@ -702,7 +749,7 @@ function parseSimpleRoman(text, key) {
|
|
|
702
749
|
}
|
|
703
750
|
let quality;
|
|
704
751
|
if (explicitMaj7) {
|
|
705
|
-
quality = base === "min" ? "minMaj7" : "maj7";
|
|
752
|
+
quality = base === "min" ? "minMaj7" : base === "aug" ? "augMaj7" : "maj7";
|
|
706
753
|
} else if (hasSeventh) {
|
|
707
754
|
quality = seventhQuality(base, isHalfDim);
|
|
708
755
|
} else if (base === "dim") {
|
|
@@ -734,30 +781,7 @@ function romanToChord(text, key) {
|
|
|
734
781
|
return chordFromParsed(parseSimpleRoman(trimmed, key));
|
|
735
782
|
}
|
|
736
783
|
function romanStyle(quality) {
|
|
737
|
-
|
|
738
|
-
case "min":
|
|
739
|
-
return { lower: true, suffix: "" };
|
|
740
|
-
case "dim":
|
|
741
|
-
return { lower: true, suffix: "o" };
|
|
742
|
-
case "aug":
|
|
743
|
-
return { lower: false, suffix: "+" };
|
|
744
|
-
case "maj7":
|
|
745
|
-
return { lower: false, suffix: "maj7" };
|
|
746
|
-
case "dom7":
|
|
747
|
-
return { lower: false, suffix: "7" };
|
|
748
|
-
case "min7":
|
|
749
|
-
return { lower: true, suffix: "7" };
|
|
750
|
-
case "m7b5":
|
|
751
|
-
return { lower: true, suffix: "\xF87" };
|
|
752
|
-
case "dim7":
|
|
753
|
-
return { lower: true, suffix: "o7" };
|
|
754
|
-
case "minMaj7":
|
|
755
|
-
return { lower: true, suffix: "maj7" };
|
|
756
|
-
case "maj":
|
|
757
|
-
return { lower: false, suffix: "" };
|
|
758
|
-
default:
|
|
759
|
-
return { lower: false, suffix: quality };
|
|
760
|
-
}
|
|
784
|
+
return ROMAN_STYLE[quality];
|
|
761
785
|
}
|
|
762
786
|
function romanSpelling(rootPc, key) {
|
|
763
787
|
const tones = scaleTonesInDegreeOrder(key);
|
|
@@ -792,6 +816,9 @@ function baseMarker(quality) {
|
|
|
792
816
|
if (quality === "aug" || quality === "aug7") {
|
|
793
817
|
return "+";
|
|
794
818
|
}
|
|
819
|
+
if (quality === "augMaj7") {
|
|
820
|
+
return "+maj7";
|
|
821
|
+
}
|
|
795
822
|
if (quality === "maj7" || quality === "minMaj7") {
|
|
796
823
|
return "maj7";
|
|
797
824
|
}
|
|
@@ -799,6 +826,17 @@ function baseMarker(quality) {
|
|
|
799
826
|
}
|
|
800
827
|
var TRIAD_FIGURES = { 1: "6", 2: "64" };
|
|
801
828
|
var SEVENTH_FIGURES = { 1: "65", 2: "43", 3: "42" };
|
|
829
|
+
var TRIAD_FIGURE_QUALITIES = /* @__PURE__ */ new Set(["maj", "min", "dim", "aug"]);
|
|
830
|
+
var SEVENTH_FIGURE_QUALITIES = /* @__PURE__ */ new Set([
|
|
831
|
+
"maj7",
|
|
832
|
+
"min7",
|
|
833
|
+
"dom7",
|
|
834
|
+
"dim7",
|
|
835
|
+
"m7b5",
|
|
836
|
+
"minMaj7",
|
|
837
|
+
"aug7",
|
|
838
|
+
"augMaj7"
|
|
839
|
+
]);
|
|
802
840
|
function chordToRoman(chord, key) {
|
|
803
841
|
const { degreeNumber, accidental } = romanSpelling(chord.rootPc, key);
|
|
804
842
|
const { lower, suffix } = romanStyle(chord.quality);
|
|
@@ -812,8 +850,7 @@ function chordToRoman(chord, key) {
|
|
|
812
850
|
}
|
|
813
851
|
}
|
|
814
852
|
if (inversion > 0) {
|
|
815
|
-
const
|
|
816
|
-
const figure = (isSeventh ? SEVENTH_FIGURES : TRIAD_FIGURES)[inversion];
|
|
853
|
+
const figure = SEVENTH_FIGURE_QUALITIES.has(chord.quality) ? SEVENTH_FIGURES[inversion] : TRIAD_FIGURE_QUALITIES.has(chord.quality) ? TRIAD_FIGURES[inversion] : void 0;
|
|
817
854
|
if (figure !== void 0) {
|
|
818
855
|
return `${accidental}${cased}${baseMarker(chord.quality)}${figure}`;
|
|
819
856
|
}
|
|
@@ -912,13 +949,13 @@ function secondaryDominant(targetDegree, key) {
|
|
|
912
949
|
}
|
|
913
950
|
|
|
914
951
|
// src/meter/index.ts
|
|
915
|
-
var
|
|
952
|
+
var EPS2 = 1e-9;
|
|
916
953
|
function isMultiple(value, unit) {
|
|
917
954
|
if (unit === 0) {
|
|
918
955
|
return false;
|
|
919
956
|
}
|
|
920
957
|
const ratio = value / unit;
|
|
921
|
-
return Math.abs(ratio - Math.round(ratio)) <
|
|
958
|
+
return Math.abs(ratio - Math.round(ratio)) < EPS2;
|
|
922
959
|
}
|
|
923
960
|
function parseTimeSignature(text) {
|
|
924
961
|
const match = /^\s*(\d+)\s*\/\s*(\d+)\s*$/.exec(text);
|
|
@@ -950,6 +987,29 @@ function pulseBeats(ts) {
|
|
|
950
987
|
function pulsesPerBar(ts) {
|
|
951
988
|
return beatsPerBar(ts) / pulseBeats(ts);
|
|
952
989
|
}
|
|
990
|
+
function isValidGrouping(grouping, pulses) {
|
|
991
|
+
if (grouping.length === 0) {
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
let sum = 0;
|
|
995
|
+
for (const g of grouping) {
|
|
996
|
+
if (!Number.isInteger(g) || g <= 0) {
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
999
|
+
sum += g;
|
|
1000
|
+
}
|
|
1001
|
+
return sum === pulses;
|
|
1002
|
+
}
|
|
1003
|
+
function isGroupHead(grouping, pulseIndex) {
|
|
1004
|
+
let acc = 0;
|
|
1005
|
+
for (const g of grouping) {
|
|
1006
|
+
if (acc === pulseIndex) {
|
|
1007
|
+
return true;
|
|
1008
|
+
}
|
|
1009
|
+
acc += g;
|
|
1010
|
+
}
|
|
1011
|
+
return false;
|
|
1012
|
+
}
|
|
953
1013
|
function beatToBarPosition(beatInQuarters, ts) {
|
|
954
1014
|
const barLen = beatsPerBar(ts);
|
|
955
1015
|
const bar = Math.floor(beatInQuarters / barLen);
|
|
@@ -969,6 +1029,15 @@ function metricWeight(beatInQuarters, ts) {
|
|
|
969
1029
|
if (pulseIndex === 0) {
|
|
970
1030
|
return 3;
|
|
971
1031
|
}
|
|
1032
|
+
const grouping = ts.grouping;
|
|
1033
|
+
if (grouping !== void 0) {
|
|
1034
|
+
if (!isValidGrouping(grouping, pulses)) {
|
|
1035
|
+
throw new Error(
|
|
1036
|
+
`Invalid grouping [${grouping.join(", ")}] for ${formatTimeSignature(ts)}: entries must be positive integers summing to ${pulses}`
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
return isGroupHead(grouping, pulseIndex) ? 2 : 1;
|
|
1040
|
+
}
|
|
972
1041
|
if (pulses % 2 === 0 && pulseIndex === pulses / 2) {
|
|
973
1042
|
return 2;
|
|
974
1043
|
}
|
|
@@ -1129,7 +1198,7 @@ function evaluateInternal(q, collectSuggestions) {
|
|
|
1129
1198
|
if (ov.prevPitch === void 0) {
|
|
1130
1199
|
continue;
|
|
1131
1200
|
}
|
|
1132
|
-
if (createsParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)
|
|
1201
|
+
if (createsParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {
|
|
1133
1202
|
reasons |= 2048 /* ParallelPerfect */;
|
|
1134
1203
|
raise(parallelSeverity);
|
|
1135
1204
|
}
|
|
@@ -1261,7 +1330,7 @@ function detectChord(pitches) {
|
|
|
1261
1330
|
quality,
|
|
1262
1331
|
missingPcs,
|
|
1263
1332
|
extraPcs,
|
|
1264
|
-
exact: extraPcs.length === 0,
|
|
1333
|
+
exact: extraPcs.length === 0 && missingPcs.length === 0,
|
|
1265
1334
|
inversion
|
|
1266
1335
|
};
|
|
1267
1336
|
if (inversion > 0) {
|
|
@@ -1293,38 +1362,49 @@ function detectChordBest(pitches) {
|
|
|
1293
1362
|
}
|
|
1294
1363
|
return makeChord(best.rootPc, best.quality, best.bassPc);
|
|
1295
1364
|
}
|
|
1365
|
+
var MINOR_MASK_VARIANTS = [NATURAL_MINOR_MASK, HARMONIC_MINOR_MASK, MELODIC_MINOR_MASK];
|
|
1296
1366
|
function detectKey(pitches) {
|
|
1297
1367
|
const input = uniquePitchClasses(pitches);
|
|
1368
|
+
if (input.length === 0) {
|
|
1369
|
+
return [];
|
|
1370
|
+
}
|
|
1298
1371
|
const counts = /* @__PURE__ */ new Map();
|
|
1299
1372
|
for (const pc of pitches.map(pitchClass6)) {
|
|
1300
1373
|
counts.set(pc, (counts.get(pc) ?? 0) + 1);
|
|
1301
1374
|
}
|
|
1302
|
-
const total = pitches.length
|
|
1375
|
+
const total = pitches.length;
|
|
1303
1376
|
const results = [];
|
|
1304
1377
|
for (let tonic = 0; tonic < 12; tonic += 1) {
|
|
1305
1378
|
for (const mode of ["major", "minor"]) {
|
|
1306
1379
|
const key = mode === "major" ? majorKey(tonic) : minorKey(tonic);
|
|
1380
|
+
const masks = mode === "major" ? [MAJOR_MASK] : MINOR_MASK_VARIANTS;
|
|
1307
1381
|
let inScale = 0;
|
|
1308
|
-
let weighted =
|
|
1309
|
-
for (const
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1382
|
+
let weighted = Number.NEGATIVE_INFINITY;
|
|
1383
|
+
for (const mask of masks) {
|
|
1384
|
+
let variantInScale = 0;
|
|
1385
|
+
let variantWeighted = 0;
|
|
1386
|
+
for (const pc of input) {
|
|
1387
|
+
const offset = (pc - tonic + 12) % 12;
|
|
1388
|
+
if (mask >> offset & 1) {
|
|
1389
|
+
variantInScale += 1;
|
|
1390
|
+
}
|
|
1313
1391
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1392
|
+
for (const [pc, count] of counts) {
|
|
1393
|
+
const offset = (pc - tonic + 12) % 12;
|
|
1394
|
+
if (mask >> offset & 1) {
|
|
1395
|
+
variantWeighted += count;
|
|
1396
|
+
}
|
|
1319
1397
|
}
|
|
1320
|
-
if (
|
|
1321
|
-
weighted
|
|
1398
|
+
if (variantWeighted > weighted) {
|
|
1399
|
+
weighted = variantWeighted;
|
|
1400
|
+
inScale = variantInScale;
|
|
1322
1401
|
}
|
|
1323
1402
|
}
|
|
1403
|
+
weighted += (counts.get(tonic) ?? 0) * 0.5;
|
|
1324
1404
|
results.push({
|
|
1325
1405
|
key,
|
|
1326
1406
|
mode,
|
|
1327
|
-
fit:
|
|
1407
|
+
fit: inScale / input.length,
|
|
1328
1408
|
score: weighted / total
|
|
1329
1409
|
});
|
|
1330
1410
|
}
|
|
@@ -1357,7 +1437,7 @@ function segmentLookup(segments) {
|
|
|
1357
1437
|
return null;
|
|
1358
1438
|
};
|
|
1359
1439
|
}
|
|
1360
|
-
var
|
|
1440
|
+
var EPS3 = 1e-9;
|
|
1361
1441
|
var NOISE_THRESHOLD_RATIO = 0.2;
|
|
1362
1442
|
var MAX_DETECTION_PCS = 6;
|
|
1363
1443
|
var FALLBACK_PCS = 3;
|
|
@@ -1399,11 +1479,11 @@ function analyzeWindow(notes, windowStart, windowEnd, ts, key) {
|
|
|
1399
1479
|
for (const note of notes) {
|
|
1400
1480
|
const noteEnd = note.startBeat + note.durationBeat;
|
|
1401
1481
|
const overlap = Math.min(noteEnd, windowEnd) - Math.max(note.startBeat, windowStart);
|
|
1402
|
-
if (overlap <=
|
|
1482
|
+
if (overlap <= EPS3) {
|
|
1403
1483
|
continue;
|
|
1404
1484
|
}
|
|
1405
1485
|
const velocityFactor = note.velocity !== void 0 ? note.velocity / 127 : 1;
|
|
1406
|
-
const onsetInWindow = note.startBeat >= windowStart -
|
|
1486
|
+
const onsetInWindow = note.startBeat >= windowStart - EPS3 && note.startBeat < windowEnd - EPS3;
|
|
1407
1487
|
const accent = onsetInWindow ? 1 + metricWeight(note.startBeat, ts) / 3 : 1;
|
|
1408
1488
|
const pc = pitchClass7(note.pitch);
|
|
1409
1489
|
weights[pc] = (weights[pc] ?? 0) + overlap * velocityFactor * accent;
|
|
@@ -1417,7 +1497,7 @@ function analyzeWindow(notes, windowStart, windowEnd, ts, key) {
|
|
|
1417
1497
|
totalWeight += w;
|
|
1418
1498
|
maxWeight = Math.max(maxWeight, w);
|
|
1419
1499
|
}
|
|
1420
|
-
if (totalWeight <=
|
|
1500
|
+
if (totalWeight <= EPS3) {
|
|
1421
1501
|
return null;
|
|
1422
1502
|
}
|
|
1423
1503
|
const ranked = weights.map((weight, pc) => ({ pc, weight })).filter(({ weight }) => weight > 0).sort((a, b) => b.weight - a.weight || a.pc - b.pc);
|
|
@@ -1454,21 +1534,22 @@ function chordTimelineFromNotes(notes, opts = {}) {
|
|
|
1454
1534
|
if (!(harmonicRhythm > 0)) {
|
|
1455
1535
|
throw new Error(`Invalid harmonic rhythm: ${harmonicRhythm}`);
|
|
1456
1536
|
}
|
|
1457
|
-
const
|
|
1537
|
+
const sounding = notes.filter((note) => note.durationBeat > 0);
|
|
1538
|
+
const lastNoteEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
|
|
1458
1539
|
const totalBeats = opts.totalBeats ?? lastNoteEnd;
|
|
1459
|
-
const key = opts.key ?? detectKey(
|
|
1540
|
+
const key = opts.key ?? detectKey(sounding.map((n) => n.pitch))[0]?.key ?? majorKey(0);
|
|
1460
1541
|
const segments = [];
|
|
1461
1542
|
const segmentConfidence = [];
|
|
1462
|
-
const windowCount = Math.max(0, Math.ceil(totalBeats / harmonicRhythm -
|
|
1543
|
+
const windowCount = Math.max(0, Math.ceil(totalBeats / harmonicRhythm - EPS3));
|
|
1463
1544
|
for (let i = 0; i < windowCount; i += 1) {
|
|
1464
1545
|
const start = i * harmonicRhythm;
|
|
1465
1546
|
const end = Math.min(start + harmonicRhythm, totalBeats);
|
|
1466
|
-
const inferred = analyzeWindow(
|
|
1547
|
+
const inferred = analyzeWindow(sounding, start, end, ts, key);
|
|
1467
1548
|
if (!inferred) {
|
|
1468
1549
|
continue;
|
|
1469
1550
|
}
|
|
1470
1551
|
const last = segments[segments.length - 1];
|
|
1471
|
-
if (last && sameChord(last.chord, inferred.chord) && Math.abs(last.endBeat - start) <
|
|
1552
|
+
if (last && sameChord(last.chord, inferred.chord) && Math.abs(last.endBeat - start) < EPS3) {
|
|
1472
1553
|
const lastLength = last.endBeat - last.startBeat;
|
|
1473
1554
|
const length = end - start;
|
|
1474
1555
|
const lastConfidence = segmentConfidence[segmentConfidence.length - 1] ?? 0;
|
|
@@ -1493,6 +1574,9 @@ function detectCadences(timeline, key) {
|
|
|
1493
1574
|
if (!prev || !cur) {
|
|
1494
1575
|
continue;
|
|
1495
1576
|
}
|
|
1577
|
+
if (Math.abs(cur.startBeat - prev.endBeat) > EPS3) {
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
1496
1580
|
const type = detectCadence(prev.chord, cur.chord, key);
|
|
1497
1581
|
if (type !== null) {
|
|
1498
1582
|
hits.push({ atBeat: cur.startBeat, type, from: prev.chord, to: cur.chord });
|
|
@@ -1502,67 +1586,101 @@ function detectCadences(timeline, key) {
|
|
|
1502
1586
|
}
|
|
1503
1587
|
|
|
1504
1588
|
// src/arrange/index.ts
|
|
1505
|
-
var
|
|
1589
|
+
var EPS4 = 1e-9;
|
|
1506
1590
|
function pitchClass8(pitch) {
|
|
1507
1591
|
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
1508
1592
|
}
|
|
1509
1593
|
function covers(note, beat) {
|
|
1510
|
-
return beat >= note.startBeat -
|
|
1594
|
+
return beat >= note.startBeat - EPS4 && beat < note.endBeat - EPS4;
|
|
1511
1595
|
}
|
|
1512
1596
|
function poolNotes(tracks) {
|
|
1513
1597
|
const notes = [];
|
|
1514
1598
|
for (const track of tracks) {
|
|
1515
1599
|
for (const note of track.notes) {
|
|
1516
|
-
|
|
1600
|
+
if (note.durationBeat > 0) {
|
|
1601
|
+
notes.push(note);
|
|
1602
|
+
}
|
|
1517
1603
|
}
|
|
1518
1604
|
}
|
|
1519
1605
|
return notes;
|
|
1520
1606
|
}
|
|
1607
|
+
function splitIntoSubVoices(ordered) {
|
|
1608
|
+
const lanes = [];
|
|
1609
|
+
for (const note of ordered) {
|
|
1610
|
+
const lane = lanes.find((candidate) => {
|
|
1611
|
+
const last = candidate[candidate.length - 1];
|
|
1612
|
+
return last !== void 0 && last.startBeat + last.durationBeat <= note.startBeat + EPS4;
|
|
1613
|
+
});
|
|
1614
|
+
if (lane) {
|
|
1615
|
+
lane.push(note);
|
|
1616
|
+
} else {
|
|
1617
|
+
lanes.push([note]);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
return lanes;
|
|
1621
|
+
}
|
|
1521
1622
|
function prepareTracks(tracks) {
|
|
1522
1623
|
let nextId = 0;
|
|
1523
1624
|
return tracks.map((track, index) => {
|
|
1524
|
-
const ordered =
|
|
1525
|
-
const
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1625
|
+
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++ }));
|
|
1626
|
+
const voices = splitIntoSubVoices(ordered).map((voice) => ({
|
|
1627
|
+
voice,
|
|
1628
|
+
sounding: voice.map((note, i) => {
|
|
1629
|
+
const prev = i > 0 ? voice[i - 1] : void 0;
|
|
1630
|
+
return {
|
|
1631
|
+
pitch: note.pitch,
|
|
1632
|
+
prevPitch: prev?.pitch,
|
|
1633
|
+
startBeat: note.startBeat,
|
|
1634
|
+
endBeat: note.startBeat + note.durationBeat
|
|
1635
|
+
};
|
|
1636
|
+
})
|
|
1637
|
+
}));
|
|
1535
1638
|
return {
|
|
1536
1639
|
name: track.name ?? `track ${index + 1}`,
|
|
1537
1640
|
role: track.role ?? "other",
|
|
1538
|
-
|
|
1539
|
-
sounding
|
|
1641
|
+
voices
|
|
1540
1642
|
};
|
|
1541
1643
|
});
|
|
1542
1644
|
}
|
|
1543
|
-
function otherVoicesSounding(prepared,
|
|
1645
|
+
function otherVoicesSounding(prepared, excludeTrack, excludeVoice, beat) {
|
|
1544
1646
|
const out = [];
|
|
1545
1647
|
for (let t = 0; t < prepared.length; t += 1) {
|
|
1546
|
-
if (t === excludeIndex) {
|
|
1547
|
-
continue;
|
|
1548
|
-
}
|
|
1549
1648
|
const track = prepared[t];
|
|
1550
1649
|
if (!track) {
|
|
1551
1650
|
continue;
|
|
1552
1651
|
}
|
|
1553
|
-
for (
|
|
1554
|
-
if (
|
|
1652
|
+
for (let v = 0; v < track.voices.length; v += 1) {
|
|
1653
|
+
if (t === excludeTrack && v === excludeVoice) {
|
|
1654
|
+
continue;
|
|
1655
|
+
}
|
|
1656
|
+
const subVoice = track.voices[v];
|
|
1657
|
+
if (!subVoice) {
|
|
1555
1658
|
continue;
|
|
1556
1659
|
}
|
|
1557
|
-
const
|
|
1558
|
-
|
|
1559
|
-
|
|
1660
|
+
for (const note of subVoice.sounding) {
|
|
1661
|
+
if (!covers(note, beat)) {
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
const snap = { pitch: note.pitch };
|
|
1665
|
+
if (note.prevPitch !== void 0) {
|
|
1666
|
+
snap.prevPitch = note.prevPitch;
|
|
1667
|
+
}
|
|
1668
|
+
out.push(snap);
|
|
1560
1669
|
}
|
|
1561
|
-
out.push(snap);
|
|
1562
1670
|
}
|
|
1563
1671
|
}
|
|
1564
1672
|
return out;
|
|
1565
1673
|
}
|
|
1674
|
+
function evaluationBeats(note, timeline) {
|
|
1675
|
+
const noteEnd = note.startBeat + note.durationBeat;
|
|
1676
|
+
const beats = [note.startBeat];
|
|
1677
|
+
for (const segment of timeline.segments) {
|
|
1678
|
+
if (segment.startBeat > note.startBeat + EPS4 && segment.startBeat < noteEnd - EPS4) {
|
|
1679
|
+
beats.push(segment.startBeat);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
return beats;
|
|
1683
|
+
}
|
|
1566
1684
|
function analyzeArrangement(tracks, opts = {}) {
|
|
1567
1685
|
const ts = opts.ts ?? parseTimeSignature("4/4");
|
|
1568
1686
|
const profile = opts.profile ?? "pop";
|
|
@@ -1581,36 +1699,48 @@ function analyzeArrangement(tracks, opts = {}) {
|
|
|
1581
1699
|
if (!track) {
|
|
1582
1700
|
continue;
|
|
1583
1701
|
}
|
|
1584
|
-
const notes =
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1702
|
+
const notes = [];
|
|
1703
|
+
for (let v = 0; v < track.voices.length; v += 1) {
|
|
1704
|
+
const subVoice = track.voices[v];
|
|
1705
|
+
if (!subVoice) {
|
|
1706
|
+
continue;
|
|
1707
|
+
}
|
|
1708
|
+
notes.push(
|
|
1709
|
+
...analyzeVoice(
|
|
1710
|
+
subVoice.voice,
|
|
1711
|
+
timeline.at,
|
|
1712
|
+
key,
|
|
1713
|
+
(beat) => otherVoicesSounding(prepared, t, v, beat)
|
|
1714
|
+
)
|
|
1715
|
+
);
|
|
1716
|
+
for (const note of subVoice.voice) {
|
|
1717
|
+
for (const beat of evaluationBeats(note, timeline)) {
|
|
1718
|
+
const result = evaluateSafety({
|
|
1719
|
+
profile,
|
|
1720
|
+
candidatePitch: note.pitch,
|
|
1721
|
+
chord: timeline.at(beat),
|
|
1722
|
+
key,
|
|
1723
|
+
otherVoices: otherVoicesSounding(prepared, t, v, beat),
|
|
1724
|
+
strongBeat: isStrongBeat(beat, ts)
|
|
1725
|
+
});
|
|
1726
|
+
if (result.safety !== 0 /* Safe */) {
|
|
1727
|
+
const conflict = {
|
|
1728
|
+
beat,
|
|
1729
|
+
trackName: track.name,
|
|
1730
|
+
pitch: note.pitch,
|
|
1731
|
+
safety: result.safety,
|
|
1732
|
+
reasons: result.reasons
|
|
1733
|
+
};
|
|
1734
|
+
if (result.rationale !== void 0) {
|
|
1735
|
+
conflict.rationale = result.rationale;
|
|
1736
|
+
}
|
|
1737
|
+
conflicts.push(conflict);
|
|
1738
|
+
}
|
|
1610
1739
|
}
|
|
1611
|
-
conflicts.push(conflict);
|
|
1612
1740
|
}
|
|
1613
1741
|
}
|
|
1742
|
+
notes.sort((a, b) => a.noteId - b.noteId);
|
|
1743
|
+
trackAnalyses.push({ name: track.name, role: track.role, notes });
|
|
1614
1744
|
}
|
|
1615
1745
|
conflicts.sort((a, b) => b.safety - a.safety || a.beat - b.beat);
|
|
1616
1746
|
return { key, timeline, segmentConfidence, cadences, tracks: trackAnalyses, conflicts };
|
|
@@ -1643,7 +1773,7 @@ function tensionCurve(tracks, opts = {}) {
|
|
|
1643
1773
|
});
|
|
1644
1774
|
const prepared = prepareTracks(tracks);
|
|
1645
1775
|
const points = [];
|
|
1646
|
-
const sampleCount = Math.max(0, Math.ceil(totalBeats / step -
|
|
1776
|
+
const sampleCount = Math.max(0, Math.ceil(totalBeats / step - EPS4));
|
|
1647
1777
|
for (let i = 0; i < sampleCount; i += 1) {
|
|
1648
1778
|
const beat = i * step;
|
|
1649
1779
|
points.push({ beat, tension: sampleTension(prepared, timeline, key, ts, profile, beat) });
|
|
@@ -1657,9 +1787,11 @@ function allSounding(prepared, beat) {
|
|
|
1657
1787
|
if (!track) {
|
|
1658
1788
|
continue;
|
|
1659
1789
|
}
|
|
1660
|
-
for (const
|
|
1661
|
-
|
|
1662
|
-
|
|
1790
|
+
for (const subVoice of track.voices) {
|
|
1791
|
+
for (const note of subVoice.sounding) {
|
|
1792
|
+
if (covers(note, beat)) {
|
|
1793
|
+
out.push({ pitch: note.pitch, track: t });
|
|
1794
|
+
}
|
|
1663
1795
|
}
|
|
1664
1796
|
}
|
|
1665
1797
|
}
|
|
@@ -1735,7 +1867,7 @@ var DEFAULT_OCTAVE = 2;
|
|
|
1735
1867
|
var STRONG_VELOCITY = 100;
|
|
1736
1868
|
var WEAK_VELOCITY = 80;
|
|
1737
1869
|
var PICKUP_PROB = 0.35;
|
|
1738
|
-
var
|
|
1870
|
+
var EPS5 = 1e-9;
|
|
1739
1871
|
function pitchClass9(value) {
|
|
1740
1872
|
return (Math.trunc(value) % 12 + 12) % 12;
|
|
1741
1873
|
}
|
|
@@ -1766,15 +1898,16 @@ function chordTonePcs(chord) {
|
|
|
1766
1898
|
return pcs;
|
|
1767
1899
|
}
|
|
1768
1900
|
function fifthPcOf(chord) {
|
|
1769
|
-
const
|
|
1770
|
-
|
|
1901
|
+
const fifths = new Set(chord.intervals.map((i) => pitchClass9(i)));
|
|
1902
|
+
const chosen = fifths.has(7) ? 7 : fifths.has(6) ? 6 : fifths.has(8) ? 8 : void 0;
|
|
1903
|
+
return chosen === void 0 ? pitchClass9(chord.rootPc) : pitchClass9(chord.rootPc + chosen);
|
|
1771
1904
|
}
|
|
1772
1905
|
function bassPcOf(chord) {
|
|
1773
1906
|
return pitchClass9(chord.bassPc ?? chord.rootPc);
|
|
1774
1907
|
}
|
|
1775
1908
|
function beatPositions(start, end) {
|
|
1776
1909
|
const positions = [];
|
|
1777
|
-
for (let p = start; p < end -
|
|
1910
|
+
for (let p = start; p < end - EPS5; p += 1) {
|
|
1778
1911
|
positions.push(p);
|
|
1779
1912
|
}
|
|
1780
1913
|
if (positions.length === 0) {
|
|
@@ -1794,7 +1927,7 @@ function buildRoot(ctx, seg) {
|
|
|
1794
1927
|
function buildRootFifth(ctx, seg) {
|
|
1795
1928
|
ctx.prevMidi = emit(ctx, seg.startBeat, bassPcOf(seg.chord));
|
|
1796
1929
|
const mid = (seg.startBeat + seg.endBeat) / 2;
|
|
1797
|
-
if (mid > seg.startBeat +
|
|
1930
|
+
if (mid > seg.startBeat + EPS5 && mid < seg.endBeat - EPS5) {
|
|
1798
1931
|
ctx.prevMidi = emit(ctx, mid, fifthPcOf(seg.chord));
|
|
1799
1932
|
}
|
|
1800
1933
|
}
|
|
@@ -1809,7 +1942,7 @@ function buildPop(ctx, seg) {
|
|
|
1809
1942
|
} else if (ctx.rng.prob(PICKUP_PROB)) {
|
|
1810
1943
|
if (ctx.rng.prob(0.5)) {
|
|
1811
1944
|
const base = placePc(rootPc, ctx.prevMidi, ctx.low);
|
|
1812
|
-
emit(ctx, pos, rootPc, base - 12);
|
|
1945
|
+
emit(ctx, pos, rootPc, Math.max(ctx.low, base - 12));
|
|
1813
1946
|
} else {
|
|
1814
1947
|
ctx.prevMidi = emit(ctx, pos, fifthPcOf(seg.chord));
|
|
1815
1948
|
}
|
|
@@ -1905,7 +2038,7 @@ function generateBassLine(opts) {
|
|
|
1905
2038
|
}
|
|
1906
2039
|
const nextStart = ctx.notes[i + 1]?.startBeat ?? lastEnd;
|
|
1907
2040
|
const durationBeat = nextStart - note.startBeat;
|
|
1908
|
-
if (durationBeat <=
|
|
2041
|
+
if (durationBeat <= EPS5) {
|
|
1909
2042
|
continue;
|
|
1910
2043
|
}
|
|
1911
2044
|
out.push({
|
|
@@ -1941,18 +2074,22 @@ function scaleMatchesChord(chordPcs, scaleMask, scaleRootPc) {
|
|
|
1941
2074
|
}
|
|
1942
2075
|
return true;
|
|
1943
2076
|
}
|
|
2077
|
+
var ALIASED_SCALE_NAMES = /* @__PURE__ */ new Set(["major", "naturalMinor"]);
|
|
2078
|
+
var HEPTATONIC_SIZE = 7;
|
|
1944
2079
|
function chordScales(chord) {
|
|
1945
2080
|
const chordPcs = chordPitchClasses(chord);
|
|
1946
2081
|
const rootPc = pitchClass10(chord.rootPc);
|
|
1947
2082
|
const ranked = [];
|
|
2083
|
+
const seenMasks = /* @__PURE__ */ new Set();
|
|
1948
2084
|
for (const name of Object.keys(NAMED_SCALES)) {
|
|
1949
|
-
if (name === "chromatic") {
|
|
2085
|
+
if (name === "chromatic" || ALIASED_SCALE_NAMES.has(name)) {
|
|
1950
2086
|
continue;
|
|
1951
2087
|
}
|
|
1952
2088
|
const mask = NAMED_SCALES[name];
|
|
1953
|
-
if (mask === void 0) {
|
|
2089
|
+
if (mask === void 0 || seenMasks.has(mask)) {
|
|
1954
2090
|
continue;
|
|
1955
2091
|
}
|
|
2092
|
+
seenMasks.add(mask);
|
|
1956
2093
|
if (scaleMatchesChord(chordPcs, mask, rootPc)) {
|
|
1957
2094
|
const size = popcount12(mask);
|
|
1958
2095
|
ranked.push({ name, extra: size - chordPcs.length, size });
|
|
@@ -1964,7 +2101,11 @@ function chordScales(chord) {
|
|
|
1964
2101
|
}
|
|
1965
2102
|
return [];
|
|
1966
2103
|
}
|
|
2104
|
+
const preferHeptatonic = chordPcs.length <= 3;
|
|
1967
2105
|
ranked.sort((a, b) => {
|
|
2106
|
+
if (preferHeptatonic && a.size === HEPTATONIC_SIZE !== (b.size === HEPTATONIC_SIZE)) {
|
|
2107
|
+
return a.size === HEPTATONIC_SIZE ? -1 : 1;
|
|
2108
|
+
}
|
|
1968
2109
|
if (a.extra !== b.extra) {
|
|
1969
2110
|
return a.extra - b.extra;
|
|
1970
2111
|
}
|
|
@@ -2116,7 +2257,7 @@ function scalesForChanges(chords) {
|
|
|
2116
2257
|
|
|
2117
2258
|
// src/countermelody/index.ts
|
|
2118
2259
|
var DEFAULT_TS2 = { numerator: 4, denominator: 4 };
|
|
2119
|
-
var
|
|
2260
|
+
var EPS6 = 1e-9;
|
|
2120
2261
|
var GRID_STEP = 0.5;
|
|
2121
2262
|
var MIN_ONSET_GAP = 1;
|
|
2122
2263
|
var REGISTER_OFFSET = 12;
|
|
@@ -2127,12 +2268,12 @@ var REINFORCE_PROB = 0.35;
|
|
|
2127
2268
|
var DEFAULT_MELODY_VELOCITY = 96;
|
|
2128
2269
|
var VELOCITY_DROP = 16;
|
|
2129
2270
|
function isWholeBeat(beat) {
|
|
2130
|
-
return Math.abs(beat - Math.round(beat)) <
|
|
2271
|
+
return Math.abs(beat - Math.round(beat)) < EPS6;
|
|
2131
2272
|
}
|
|
2132
2273
|
function melodyNoteAt(melody, beat) {
|
|
2133
2274
|
let found;
|
|
2134
2275
|
for (const note of melody) {
|
|
2135
|
-
if (note.startBeat -
|
|
2276
|
+
if (note.startBeat - EPS6 <= beat && beat < note.startBeat + note.durationBeat - EPS6) {
|
|
2136
2277
|
if (!found || note.startBeat > found.startBeat) {
|
|
2137
2278
|
found = note;
|
|
2138
2279
|
}
|
|
@@ -2141,7 +2282,7 @@ function melodyNoteAt(melody, beat) {
|
|
|
2141
2282
|
return found;
|
|
2142
2283
|
}
|
|
2143
2284
|
function melodyAttacksAt(melody, beat) {
|
|
2144
|
-
return melody.some((note) => Math.abs(note.startBeat - beat) <
|
|
2285
|
+
return melody.some((note) => Math.abs(note.startBeat - beat) < EPS6);
|
|
2145
2286
|
}
|
|
2146
2287
|
function followOnsets(melody) {
|
|
2147
2288
|
return [...new Set(melody.map((note) => note.startBeat))].sort((a, b) => a - b);
|
|
@@ -2150,7 +2291,7 @@ function complementOnsets(melody, ts, spanStart, spanEnd, rng) {
|
|
|
2150
2291
|
const onsets = [];
|
|
2151
2292
|
const gridStart = Math.floor(spanStart / GRID_STEP) * GRID_STEP;
|
|
2152
2293
|
let last = Number.NEGATIVE_INFINITY;
|
|
2153
|
-
for (let beat = gridStart; beat < spanEnd -
|
|
2294
|
+
for (let beat = gridStart; beat < spanEnd - EPS6; beat += GRID_STEP) {
|
|
2154
2295
|
const attacked = melodyAttacksAt(melody, beat);
|
|
2155
2296
|
const sounding = melodyNoteAt(melody, beat) !== void 0;
|
|
2156
2297
|
let place = false;
|
|
@@ -2161,7 +2302,7 @@ function complementOnsets(melody, ts, spanStart, spanEnd, rng) {
|
|
|
2161
2302
|
} else if (isStrongBeat(beat, ts)) {
|
|
2162
2303
|
place = rng.prob(REINFORCE_PROB);
|
|
2163
2304
|
}
|
|
2164
|
-
if (place && beat - last >= MIN_ONSET_GAP -
|
|
2305
|
+
if (place && beat - last >= MIN_ONSET_GAP - EPS6) {
|
|
2165
2306
|
onsets.push(beat);
|
|
2166
2307
|
last = beat;
|
|
2167
2308
|
}
|
|
@@ -2279,13 +2420,15 @@ function generateCounterMelody(opts) {
|
|
|
2279
2420
|
if (register === "above" && candidate <= melPitch) {
|
|
2280
2421
|
continue;
|
|
2281
2422
|
}
|
|
2282
|
-
if (prevPitch !== void 0 && melPrev !== void 0 &&
|
|
2423
|
+
if (prevPitch !== void 0 && melPrev !== void 0 && // Parallel octaves are the perfect-class-zero case of this predicate,
|
|
2424
|
+
// so a single check covers both parallel fifths and octaves.
|
|
2425
|
+
createsParallelPerfect(prevPitch, candidate, melPrev, melPitch)) {
|
|
2283
2426
|
continue;
|
|
2284
2427
|
}
|
|
2285
2428
|
}
|
|
2286
2429
|
const score = scoreCandidate(candidate, melPitch, melPrev, prevPitch, chord, center);
|
|
2287
|
-
const tie = Math.abs(score - bestScore) <=
|
|
2288
|
-
if (score > bestScore +
|
|
2430
|
+
const tie = Math.abs(score - bestScore) <= EPS6 && candidate < (bestPitch ?? Number.POSITIVE_INFINITY);
|
|
2431
|
+
if (score > bestScore + EPS6 || tie) {
|
|
2289
2432
|
bestScore = score;
|
|
2290
2433
|
bestPitch = candidate;
|
|
2291
2434
|
}
|
|
@@ -2338,7 +2481,11 @@ var STYLE_MAP = {
|
|
|
2338
2481
|
bossa: { style: "latin", feel: "straight", ghostBoost: false, snareBeat3: false },
|
|
2339
2482
|
trap: { style: "trap", feel: "straight", ghostBoost: false, snareBeat3: true },
|
|
2340
2483
|
halftime: { style: "sparse", feel: "straight", ghostBoost: false, snareBeat3: true },
|
|
2341
|
-
breakbeat: { style: "rock", feel: "straight", ghostBoost: false, snareBeat3: false }
|
|
2484
|
+
breakbeat: { style: "rock", feel: "straight", ghostBoost: false, snareBeat3: false },
|
|
2485
|
+
// Four-on-the-floor dance kick with idol-style auxiliary percussion.
|
|
2486
|
+
house: { style: "fourOnFloor", feel: "straight", ghostBoost: false, snareBeat3: false },
|
|
2487
|
+
// Synth-pop pulse with denser 16th hi-hats and busy kick syncopation.
|
|
2488
|
+
synthpop: { style: "synth", feel: "straight", ghostBoost: true, snareBeat3: false }
|
|
2342
2489
|
};
|
|
2343
2490
|
function mapStyle(style) {
|
|
2344
2491
|
return STYLE_MAP[style];
|
|
@@ -2825,8 +2972,7 @@ function quantizeSwing(startBeat, swing, resolution) {
|
|
|
2825
2972
|
return base + EIGHTH2 + DELTA_8 * clamped;
|
|
2826
2973
|
}
|
|
2827
2974
|
if (offset >= EIGHTH2 + HALF_16TH && offset < 3 * SIXTEENTH2 + HALF_16TH) {
|
|
2828
|
-
|
|
2829
|
-
return Math.min(result, base + 1 - 1 / 480);
|
|
2975
|
+
return base + 3 * SIXTEENTH2 + DELTA_16 * clamped;
|
|
2830
2976
|
}
|
|
2831
2977
|
return startBeat;
|
|
2832
2978
|
}
|
|
@@ -3021,6 +3167,52 @@ function generateHiHatForBeat(ctx, sec) {
|
|
|
3021
3167
|
}
|
|
3022
3168
|
}
|
|
3023
3169
|
|
|
3170
|
+
// src/drums/euclid.ts
|
|
3171
|
+
function euclideanRhythm(pulses, steps, rotation = 0) {
|
|
3172
|
+
if (steps < 1) {
|
|
3173
|
+
return [];
|
|
3174
|
+
}
|
|
3175
|
+
const p = Math.max(0, Math.min(pulses, steps));
|
|
3176
|
+
const base = bjorklund(p, steps);
|
|
3177
|
+
const shift = (rotation % steps + steps) % steps;
|
|
3178
|
+
if (shift === 0) {
|
|
3179
|
+
return base;
|
|
3180
|
+
}
|
|
3181
|
+
return base.map((_, i) => base[(i - shift + steps) % steps] ?? false);
|
|
3182
|
+
}
|
|
3183
|
+
function hasHit(mask, step) {
|
|
3184
|
+
return (mask >> step & 1) === 1;
|
|
3185
|
+
}
|
|
3186
|
+
function patternToMask(pattern) {
|
|
3187
|
+
let mask = 0;
|
|
3188
|
+
for (let i = 0; i < pattern.length; i += 1) {
|
|
3189
|
+
if (pattern[i]) {
|
|
3190
|
+
mask |= 1 << i;
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
return mask;
|
|
3194
|
+
}
|
|
3195
|
+
function bjorklund(pulses, steps) {
|
|
3196
|
+
if (pulses === 0) {
|
|
3197
|
+
return Array.from({ length: steps }, () => false);
|
|
3198
|
+
}
|
|
3199
|
+
if (pulses >= steps) {
|
|
3200
|
+
return Array.from({ length: steps }, () => true);
|
|
3201
|
+
}
|
|
3202
|
+
let head = Array.from({ length: pulses }, () => [true]);
|
|
3203
|
+
let remainder = Array.from({ length: steps - pulses }, () => [false]);
|
|
3204
|
+
while (remainder.length > 1) {
|
|
3205
|
+
const count = Math.min(head.length, remainder.length);
|
|
3206
|
+
const next = [];
|
|
3207
|
+
for (let i = 0; i < count; i += 1) {
|
|
3208
|
+
next.push([...head[i] ?? [], ...remainder[i] ?? []]);
|
|
3209
|
+
}
|
|
3210
|
+
remainder = head.length > count ? head.slice(count) : remainder.slice(count);
|
|
3211
|
+
head = next;
|
|
3212
|
+
}
|
|
3213
|
+
return [...head.flat(), ...remainder.flat()];
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3024
3216
|
// src/drums/fills.ts
|
|
3025
3217
|
function getFillStartBeat(energy) {
|
|
3026
3218
|
switch (energy) {
|
|
@@ -3037,6 +3229,9 @@ function selectFillType(from, to, style, nextEnergy, rng) {
|
|
|
3037
3229
|
if (style === "sparse") {
|
|
3038
3230
|
return rng.range(0, 1) === 0 ? "simpleCrash" : "breakdownFill";
|
|
3039
3231
|
}
|
|
3232
|
+
const toChorus = to === "chorus";
|
|
3233
|
+
const fromIntro = from === "intro";
|
|
3234
|
+
const highEnergy = style === "rock" || style === "fourOnFloor";
|
|
3040
3235
|
if (nextEnergy === "low") {
|
|
3041
3236
|
switch (rng.range(0, 2)) {
|
|
3042
3237
|
case 0:
|
|
@@ -3047,25 +3242,10 @@ function selectFillType(from, to, style, nextEnergy, rng) {
|
|
|
3047
3242
|
return "halfTimeFill";
|
|
3048
3243
|
}
|
|
3049
3244
|
}
|
|
3050
|
-
if (nextEnergy === "peak") {
|
|
3051
|
-
switch (rng.range(0, 3)) {
|
|
3052
|
-
case 0:
|
|
3053
|
-
return "tomDescend";
|
|
3054
|
-
case 1:
|
|
3055
|
-
return "snareRoll";
|
|
3056
|
-
case 2:
|
|
3057
|
-
return "linearFill";
|
|
3058
|
-
default:
|
|
3059
|
-
return "flamsAndDrags";
|
|
3060
|
-
}
|
|
3061
|
-
}
|
|
3062
|
-
const toChorus = to === "chorus";
|
|
3063
|
-
const fromIntro = from === "intro";
|
|
3064
|
-
const highEnergy = style === "rock" || style === "fourOnFloor";
|
|
3065
|
-
const choice = rng.range(0, 7);
|
|
3066
3245
|
if (toChorus) {
|
|
3246
|
+
const choice2 = rng.range(0, 7);
|
|
3067
3247
|
if (highEnergy) {
|
|
3068
|
-
switch (
|
|
3248
|
+
switch (choice2) {
|
|
3069
3249
|
case 0:
|
|
3070
3250
|
case 1:
|
|
3071
3251
|
return "tomDescend";
|
|
@@ -3083,7 +3263,7 @@ function selectFillType(from, to, style, nextEnergy, rng) {
|
|
|
3083
3263
|
return "ghostToAccent";
|
|
3084
3264
|
}
|
|
3085
3265
|
}
|
|
3086
|
-
switch (
|
|
3266
|
+
switch (choice2) {
|
|
3087
3267
|
case 0:
|
|
3088
3268
|
case 1:
|
|
3089
3269
|
return "snareTomCombo";
|
|
@@ -3100,7 +3280,7 @@ function selectFillType(from, to, style, nextEnergy, rng) {
|
|
|
3100
3280
|
}
|
|
3101
3281
|
}
|
|
3102
3282
|
if (fromIntro) {
|
|
3103
|
-
switch (
|
|
3283
|
+
switch (rng.range(0, 5)) {
|
|
3104
3284
|
case 0:
|
|
3105
3285
|
case 1:
|
|
3106
3286
|
return "snareRoll";
|
|
@@ -3114,6 +3294,7 @@ function selectFillType(from, to, style, nextEnergy, rng) {
|
|
|
3114
3294
|
return "halfTimeFill";
|
|
3115
3295
|
}
|
|
3116
3296
|
}
|
|
3297
|
+
const choice = rng.range(0, 7);
|
|
3117
3298
|
if (highEnergy) {
|
|
3118
3299
|
switch (choice) {
|
|
3119
3300
|
case 0:
|
|
@@ -3296,6 +3477,10 @@ function generateFill(track, beatTick, beat, fillType, velocity) {
|
|
|
3296
3477
|
if (beat === 2) {
|
|
3297
3478
|
track.add(GM.SD, beatTick, 1, accentVel);
|
|
3298
3479
|
track.add(GM.BD, beatTick, 1, fillVel);
|
|
3480
|
+
} else if (beat === 3) {
|
|
3481
|
+
track.add(GM.SD, beatTick, EIGHTH, accentVel);
|
|
3482
|
+
track.add(GM.SD, beatTick + E, S, fillVel * 0.6);
|
|
3483
|
+
track.add(GM.SD, beatTick + E + S, S, accentVel);
|
|
3299
3484
|
}
|
|
3300
3485
|
break;
|
|
3301
3486
|
}
|
|
@@ -3338,6 +3523,19 @@ function isInPreChorusLift(section, bar, sectionBars, nextIsChorus) {
|
|
|
3338
3523
|
}
|
|
3339
3524
|
return bar >= sectionBars - 2;
|
|
3340
3525
|
}
|
|
3526
|
+
function euclideanToKickPattern(mask) {
|
|
3527
|
+
const slot = (step) => hasHit(mask, step) || hasHit(mask, (step + 15) % 16);
|
|
3528
|
+
return {
|
|
3529
|
+
beat1: slot(0),
|
|
3530
|
+
beat1and: slot(2),
|
|
3531
|
+
beat2: slot(4),
|
|
3532
|
+
beat2and: slot(6),
|
|
3533
|
+
beat3: slot(8),
|
|
3534
|
+
beat3and: slot(10),
|
|
3535
|
+
beat4: slot(12),
|
|
3536
|
+
beat4and: slot(14)
|
|
3537
|
+
};
|
|
3538
|
+
}
|
|
3341
3539
|
function getKickPattern(section, style, bar, rng) {
|
|
3342
3540
|
const p = { ...EMPTY };
|
|
3343
3541
|
if (section === "outro") {
|
|
@@ -3542,6 +3740,17 @@ function generateDrums(opts) {
|
|
|
3542
3740
|
const energy = sectionEnergy(section);
|
|
3543
3741
|
const fillStartBeat = getFillStartBeat(energy);
|
|
3544
3742
|
const percMood = percMoodCategory(style);
|
|
3743
|
+
const nextSection = opts.nextSection ? mapSection(opts.nextSection) : section;
|
|
3744
|
+
const nextEnergy = sectionEnergy(nextSection);
|
|
3745
|
+
const euclidKick = opts.euclideanKick ? euclideanToKickPattern(
|
|
3746
|
+
patternToMask(
|
|
3747
|
+
euclideanRhythm(
|
|
3748
|
+
opts.euclideanKick.pulses,
|
|
3749
|
+
opts.euclideanKick.steps ?? 16,
|
|
3750
|
+
opts.euclideanKick.rotation ?? 0
|
|
3751
|
+
)
|
|
3752
|
+
)
|
|
3753
|
+
) : void 0;
|
|
3545
3754
|
const reuseSectionKick = (section === "b" || section === "chorus") && style !== "sparse";
|
|
3546
3755
|
let sectionKick;
|
|
3547
3756
|
for (let bar = 0; bar < opts.bars; bar += 1) {
|
|
@@ -3557,7 +3766,9 @@ function generateDrums(opts) {
|
|
|
3557
3766
|
barHasOpenHh = !track.hasCrashNear(barStart + openHhBeatIndex);
|
|
3558
3767
|
}
|
|
3559
3768
|
let kick;
|
|
3560
|
-
if (
|
|
3769
|
+
if (euclidKick) {
|
|
3770
|
+
kick = euclidKick;
|
|
3771
|
+
} else if (reuseSectionKick) {
|
|
3561
3772
|
sectionKick ??= getKickPattern(section, style, 0, rng);
|
|
3562
3773
|
kick = sectionKick;
|
|
3563
3774
|
} else {
|
|
@@ -3570,12 +3781,17 @@ function generateDrums(opts) {
|
|
|
3570
3781
|
const velocity = calculateVelocity(section, beat);
|
|
3571
3782
|
if (opts.fills && isLastBar && !inLift && beat >= fillStartBeat) {
|
|
3572
3783
|
if (beat === fillStartBeat) {
|
|
3573
|
-
currentFill = selectFillType(section,
|
|
3784
|
+
currentFill = selectFillType(section, nextSection, style, nextEnergy, rng);
|
|
3574
3785
|
}
|
|
3575
3786
|
if (playMainVoices) {
|
|
3787
|
+
const before = track.hits.length;
|
|
3576
3788
|
generateFill(track, beatTick, beat, currentFill, velocity);
|
|
3789
|
+
if (track.hits.length > before) {
|
|
3790
|
+
continue;
|
|
3791
|
+
}
|
|
3792
|
+
} else {
|
|
3793
|
+
continue;
|
|
3577
3794
|
}
|
|
3578
|
-
continue;
|
|
3579
3795
|
}
|
|
3580
3796
|
const ctx = {
|
|
3581
3797
|
track,
|
|
@@ -3697,18 +3913,23 @@ function extractGrooveTemplate(events, ts, subdivision) {
|
|
|
3697
3913
|
for (let i = 0; i < slotsPerBar; i += 1) {
|
|
3698
3914
|
const count = counts[i] ?? 0;
|
|
3699
3915
|
if (count === 0) {
|
|
3700
|
-
slots.push({ timingOffset: 0, velocity:
|
|
3916
|
+
slots.push({ timingOffset: 0, velocity: null });
|
|
3701
3917
|
continue;
|
|
3702
3918
|
}
|
|
3703
3919
|
const velocityCount = velocityCounts[i] ?? 0;
|
|
3704
3920
|
slots.push({
|
|
3705
3921
|
timingOffset: (offsetSums[i] ?? 0) / count,
|
|
3706
|
-
velocity: velocityCount > 0 ? (velocitySums[i] ?? 0) / velocityCount :
|
|
3922
|
+
velocity: velocityCount > 0 ? (velocitySums[i] ?? 0) / velocityCount : null
|
|
3707
3923
|
});
|
|
3708
3924
|
}
|
|
3709
|
-
return { subdivision, slotsPerBar, slots };
|
|
3925
|
+
return { subdivision, slotsPerBar, slots, ts };
|
|
3710
3926
|
}
|
|
3711
3927
|
function applyGrooveTemplate(events, template, ts) {
|
|
3928
|
+
if (template.ts && (template.ts.numerator !== ts.numerator || template.ts.denominator !== ts.denominator)) {
|
|
3929
|
+
throw new Error(
|
|
3930
|
+
`Groove template meter ${formatTimeSignature(template.ts)} does not match apply-time meter ${formatTimeSignature(ts)}`
|
|
3931
|
+
);
|
|
3932
|
+
}
|
|
3712
3933
|
const barBeats = beatsPerBar(ts);
|
|
3713
3934
|
return events.map((event) => {
|
|
3714
3935
|
const { quantizedBeat, slotIndex } = quantizeToGrid(
|
|
@@ -3719,7 +3940,7 @@ function applyGrooveTemplate(events, template, ts) {
|
|
|
3719
3940
|
);
|
|
3720
3941
|
const slot = template.slots[slotIndex];
|
|
3721
3942
|
const startBeat = quantizedBeat + (slot?.timingOffset ?? 0);
|
|
3722
|
-
const velocity = slot && slot.velocity
|
|
3943
|
+
const velocity = slot && slot.velocity !== null && slot.velocity !== void 0 ? Math.round(slot.velocity) : event.velocity;
|
|
3723
3944
|
return {
|
|
3724
3945
|
pitch: event.pitch,
|
|
3725
3946
|
startBeat,
|
|
@@ -3772,6 +3993,7 @@ var DEFAULT_METER = { numerator: 4, denominator: 4 };
|
|
|
3772
3993
|
var COMFORT_LOW = 55;
|
|
3773
3994
|
var COMFORT_HIGH = 79;
|
|
3774
3995
|
var TESSITURA_WEIGHT = 1e-3;
|
|
3996
|
+
var TIE_BREAK_JITTER = 1e-6;
|
|
3775
3997
|
function pitchClass11(pitch) {
|
|
3776
3998
|
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
3777
3999
|
}
|
|
@@ -3786,26 +4008,33 @@ function mulberry32(seed) {
|
|
|
3786
4008
|
};
|
|
3787
4009
|
}
|
|
3788
4010
|
function inferKey(melody) {
|
|
3789
|
-
let best = 0;
|
|
4011
|
+
let best = majorKey(0);
|
|
3790
4012
|
let bestScore = Number.NEGATIVE_INFINITY;
|
|
4013
|
+
const candidates = [];
|
|
3791
4014
|
for (let tonic = 0; tonic < 12; tonic += 1) {
|
|
3792
|
-
|
|
4015
|
+
candidates.push(majorKey(tonic), minorKey(tonic));
|
|
4016
|
+
}
|
|
4017
|
+
for (const key of candidates) {
|
|
4018
|
+
const tonic = pitchClass11(key.rootPc);
|
|
4019
|
+
const isMinor = key.modeMask12 === NATURAL_MINOR_MASK;
|
|
4020
|
+
const leadingTone = (tonic + 11) % 12;
|
|
3793
4021
|
let score = 0;
|
|
3794
4022
|
for (const n of melody) {
|
|
3795
4023
|
const w = Math.max(0.25, n.durationBeat);
|
|
3796
|
-
|
|
4024
|
+
const pc = pitchClass11(n.pitch);
|
|
4025
|
+
if (isScaleTone(n.pitch, key) || isMinor && pc === leadingTone) {
|
|
3797
4026
|
score += w;
|
|
3798
4027
|
}
|
|
3799
|
-
if (
|
|
4028
|
+
if (pc === tonic) {
|
|
3800
4029
|
score += 0.5 * w;
|
|
3801
4030
|
}
|
|
3802
4031
|
}
|
|
3803
4032
|
if (score > bestScore) {
|
|
3804
4033
|
bestScore = score;
|
|
3805
|
-
best =
|
|
4034
|
+
best = key;
|
|
3806
4035
|
}
|
|
3807
4036
|
}
|
|
3808
|
-
return
|
|
4037
|
+
return best;
|
|
3809
4038
|
}
|
|
3810
4039
|
function buildCandidates(key, reharmonize) {
|
|
3811
4040
|
const tones = scaleTonesInDegreeOrder(key);
|
|
@@ -3957,7 +4186,7 @@ function harmonizeMelody(opts) {
|
|
|
3957
4186
|
const candidates = buildCandidates(key, opts.reharmonize);
|
|
3958
4187
|
const candAt = (i) => candidates[i] ?? FALLBACK;
|
|
3959
4188
|
const rng = mulberry32(opts.seed ?? 0);
|
|
3960
|
-
const jitter = candidates.map(() => rng() *
|
|
4189
|
+
const jitter = candidates.map(() => rng() * TIE_BREAK_JITTER);
|
|
3961
4190
|
const melodyEnd = opts.melody.reduce((m, n) => Math.max(m, n.startBeat + n.durationBeat), 0);
|
|
3962
4191
|
const hr = Math.max(0.25, opts.harmonicRhythm);
|
|
3963
4192
|
const segCount = Math.max(1, Math.ceil(melodyEnd / hr));
|
|
@@ -4154,6 +4383,8 @@ function spelledInterval(a, b) {
|
|
|
4154
4383
|
const reference = SIMPLE_REFERENCE[letterSteps + 1] ?? 0;
|
|
4155
4384
|
if (reference - rawSemis > 6) {
|
|
4156
4385
|
rawSemis += 12;
|
|
4386
|
+
} else if (rawSemis - reference > 6) {
|
|
4387
|
+
rawSemis -= 12;
|
|
4157
4388
|
}
|
|
4158
4389
|
semitones = rawSemis;
|
|
4159
4390
|
}
|
|
@@ -4378,8 +4609,8 @@ function noteNames(notes) {
|
|
|
4378
4609
|
}
|
|
4379
4610
|
|
|
4380
4611
|
// src/symbol/index.ts
|
|
4381
|
-
var ROOT_RE = /^[A-
|
|
4382
|
-
var BASS_RE = /^[A-
|
|
4612
|
+
var ROOT_RE = /^[A-Ga-g](?:#{1,2}|b{1,2})?/;
|
|
4613
|
+
var BASS_RE = /^[A-Ga-g](?:#{1,2}|b{1,2})?$/;
|
|
4383
4614
|
var QUALITY_MAP = {
|
|
4384
4615
|
"": "maj",
|
|
4385
4616
|
m: "min",
|
|
@@ -4474,8 +4705,17 @@ var CANONICAL_SUFFIX = {
|
|
|
4474
4705
|
function pitchClass12(value) {
|
|
4475
4706
|
return (Math.trunc(value) % 12 + 12) % 12;
|
|
4476
4707
|
}
|
|
4477
|
-
function
|
|
4478
|
-
|
|
4708
|
+
function makeSpelledChord(root, quality, bass) {
|
|
4709
|
+
const chord = makeChord(
|
|
4710
|
+
noteToPitchClass(root),
|
|
4711
|
+
quality,
|
|
4712
|
+
bass === void 0 ? void 0 : noteToPitchClass(bass)
|
|
4713
|
+
);
|
|
4714
|
+
chord.rootSpelling = { letter: root.letter, alter: root.alter };
|
|
4715
|
+
if (bass !== void 0) {
|
|
4716
|
+
chord.bassSpelling = { letter: bass.letter, alter: bass.alter };
|
|
4717
|
+
}
|
|
4718
|
+
return chord;
|
|
4479
4719
|
}
|
|
4480
4720
|
function parseChordSymbol(text) {
|
|
4481
4721
|
const trimmed = text.trim();
|
|
@@ -4484,37 +4724,37 @@ function parseChordSymbol(text) {
|
|
|
4484
4724
|
throw new Error(`Invalid chord symbol: ${text}`);
|
|
4485
4725
|
}
|
|
4486
4726
|
const rootToken = rootMatch[0];
|
|
4487
|
-
const
|
|
4727
|
+
const rootNote = parseNote(rootToken);
|
|
4488
4728
|
const rest = trimmed.slice(rootToken.length);
|
|
4489
|
-
const slashIndex = rest.
|
|
4490
|
-
if (slashIndex
|
|
4491
|
-
const
|
|
4492
|
-
|
|
4493
|
-
|
|
4729
|
+
const slashIndex = rest.lastIndexOf("/");
|
|
4730
|
+
if (slashIndex !== -1) {
|
|
4731
|
+
const before = rest.slice(0, slashIndex);
|
|
4732
|
+
const after = rest.slice(slashIndex + 1);
|
|
4733
|
+
const beforeQuality = QUALITY_MAP[before];
|
|
4734
|
+
if (beforeQuality !== void 0 && BASS_RE.test(after)) {
|
|
4735
|
+
return makeSpelledChord(rootNote, beforeQuality, parseNote(after));
|
|
4494
4736
|
}
|
|
4495
|
-
return makeChord(rootPc, quality);
|
|
4496
4737
|
}
|
|
4497
|
-
const
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
if (beforeQuality !== void 0 && BASS_RE.test(after)) {
|
|
4501
|
-
return makeChord(rootPc, beforeQuality, bassPitchClass(after));
|
|
4738
|
+
const quality = QUALITY_MAP[rest];
|
|
4739
|
+
if (quality === void 0) {
|
|
4740
|
+
throw new Error(`Unrecognized chord quality: ${text}`);
|
|
4502
4741
|
}
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4742
|
+
return makeSpelledChord(rootNote, quality);
|
|
4743
|
+
}
|
|
4744
|
+
function pitchClassName(pc, hint, flats) {
|
|
4745
|
+
if (flats === void 0 && hint !== void 0 && noteToPitchClass(hint) === pc) {
|
|
4746
|
+
return formatNote(hint);
|
|
4506
4747
|
}
|
|
4507
|
-
|
|
4748
|
+
const note = midiToNote(60 + pc, flats ? "flat" : "sharp");
|
|
4749
|
+
return formatNote({ letter: note.letter, alter: note.alter });
|
|
4508
4750
|
}
|
|
4509
4751
|
function formatChordSymbol(chord, opts) {
|
|
4510
|
-
const
|
|
4511
|
-
const
|
|
4512
|
-
const rootName = formatNote({ letter: rootNote.letter, alter: rootNote.alter });
|
|
4752
|
+
const rootPc = pitchClass12(chord.rootPc);
|
|
4753
|
+
const rootName = pitchClassName(rootPc, chord.rootSpelling, opts?.flats);
|
|
4513
4754
|
const suffix = CANONICAL_SUFFIX[chord.quality];
|
|
4514
4755
|
let symbol = `${rootName}${suffix}`;
|
|
4515
|
-
if (chord.bassPc !== void 0 && pitchClass12(chord.bassPc) !==
|
|
4516
|
-
|
|
4517
|
-
symbol += `/${formatNote({ letter: bassNote.letter, alter: bassNote.alter })}`;
|
|
4756
|
+
if (chord.bassPc !== void 0 && pitchClass12(chord.bassPc) !== rootPc) {
|
|
4757
|
+
symbol += `/${pitchClassName(pitchClass12(chord.bassPc), chord.bassSpelling, opts?.flats)}`;
|
|
4518
4758
|
}
|
|
4519
4759
|
return symbol;
|
|
4520
4760
|
}
|
|
@@ -4541,6 +4781,7 @@ var DERIVED_SPAN = 19;
|
|
|
4541
4781
|
var VIOLATION_PENALTY = 1e3;
|
|
4542
4782
|
var MISSING_TONE_PENALTY = 500;
|
|
4543
4783
|
var POOR_DOUBLING_PENALTY = 4;
|
|
4784
|
+
var HIDDEN_PERFECT_PENALTY = 6;
|
|
4544
4785
|
var MAX_CANDIDATES = 4e3;
|
|
4545
4786
|
function pitchClass13(value) {
|
|
4546
4787
|
return (Math.trunc(value) % 12 + 12) % 12;
|
|
@@ -4578,7 +4819,12 @@ function pitchesForPc(pc, range) {
|
|
|
4578
4819
|
result.push(midi);
|
|
4579
4820
|
}
|
|
4580
4821
|
}
|
|
4581
|
-
|
|
4822
|
+
const center = (range.min + range.max) / 2;
|
|
4823
|
+
return result.sort((a, b) => {
|
|
4824
|
+
const da = Math.abs(a - center);
|
|
4825
|
+
const db = Math.abs(b - center);
|
|
4826
|
+
return da === db ? a - b : da - db;
|
|
4827
|
+
});
|
|
4582
4828
|
}
|
|
4583
4829
|
function enumerateVoicings(chord, ranges, maxSpacing) {
|
|
4584
4830
|
const chordPcs = chordPitchClasses(chord);
|
|
@@ -4654,9 +4900,6 @@ function violationCount(prev, cur, maxSpacing) {
|
|
|
4654
4900
|
if (createsParallelPerfect(prevUpper, curUpper, prevLower, curLower)) {
|
|
4655
4901
|
count += 1;
|
|
4656
4902
|
}
|
|
4657
|
-
if (createsParallelOctave(prevUpper, curUpper, prevLower, curLower)) {
|
|
4658
|
-
count += 1;
|
|
4659
|
-
}
|
|
4660
4903
|
if (createsVoiceCrossing(curUpper, curLower)) {
|
|
4661
4904
|
count += 1;
|
|
4662
4905
|
}
|
|
@@ -4685,6 +4928,15 @@ function voiceLeadingCost(from, to) {
|
|
|
4685
4928
|
}
|
|
4686
4929
|
total += Math.abs(b - a);
|
|
4687
4930
|
}
|
|
4931
|
+
if (from.length >= 2) {
|
|
4932
|
+
const bassPrev = from[0];
|
|
4933
|
+
const bassCur = to[0];
|
|
4934
|
+
const sopPrev = from[from.length - 1];
|
|
4935
|
+
const sopCur = to[to.length - 1];
|
|
4936
|
+
if (bassPrev !== void 0 && bassCur !== void 0 && sopPrev !== void 0 && sopCur !== void 0 && createsHiddenParallelPerfect(bassPrev, bassCur, sopPrev, sopCur)) {
|
|
4937
|
+
total += HIDDEN_PERFECT_PENALTY;
|
|
4938
|
+
}
|
|
4939
|
+
}
|
|
4688
4940
|
return total;
|
|
4689
4941
|
}
|
|
4690
4942
|
function voiceChord(chord, opts) {
|
|
@@ -4823,7 +5075,10 @@ function voiceChordStyled(chord, opts) {
|
|
|
4823
5075
|
return stack.sort((a, b) => a - b);
|
|
4824
5076
|
}
|
|
4825
5077
|
function nextVoicing(current, chord, opts) {
|
|
4826
|
-
const ranges = opts?.ranges !== void 0 || opts?.voices !== void 0 ? resolveRanges(opts) : current.map((pitch) =>
|
|
5078
|
+
const ranges = opts?.ranges !== void 0 || opts?.voices !== void 0 ? resolveRanges(opts) : current.map((pitch) => {
|
|
5079
|
+
const centre = Math.min(127, Math.max(0, pitch));
|
|
5080
|
+
return { min: Math.max(0, centre - 12), max: Math.min(127, centre + 12) };
|
|
5081
|
+
});
|
|
4827
5082
|
const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
|
|
4828
5083
|
const candidates = enumerateVoicings(chord, ranges, maxSpacing);
|
|
4829
5084
|
let best;
|
|
@@ -4861,6 +5116,12 @@ function copyChord(data) {
|
|
|
4861
5116
|
if (data.bassPc !== void 0) {
|
|
4862
5117
|
copy.bassPc = data.bassPc;
|
|
4863
5118
|
}
|
|
5119
|
+
if (data.rootSpelling !== void 0) {
|
|
5120
|
+
copy.rootSpelling = { letter: data.rootSpelling.letter, alter: data.rootSpelling.alter };
|
|
5121
|
+
}
|
|
5122
|
+
if (data.bassSpelling !== void 0) {
|
|
5123
|
+
copy.bassSpelling = { letter: data.bassSpelling.letter, alter: data.bassSpelling.alter };
|
|
5124
|
+
}
|
|
4864
5125
|
return copy;
|
|
4865
5126
|
}
|
|
4866
5127
|
function spellPitchClassBare(pc, spelling) {
|
|
@@ -4946,12 +5207,16 @@ var Note = class _Note {
|
|
|
4946
5207
|
*
|
|
4947
5208
|
* When the note carries an octave the transposition happens in MIDI space and
|
|
4948
5209
|
* the result is spelled with a sharp preference. An octave-less note stays
|
|
4949
|
-
* octave-less: only its pitch class is moved.
|
|
5210
|
+
* octave-less: only its pitch class is moved. Transposing by zero is the
|
|
5211
|
+
* identity: the original spelling is preserved (no enharmonic respelling).
|
|
4950
5212
|
*
|
|
4951
5213
|
* @param semitones The signed semitone offset.
|
|
4952
5214
|
* @returns The transposed note.
|
|
4953
5215
|
*/
|
|
4954
5216
|
transpose(semitones) {
|
|
5217
|
+
if (semitones === 0) {
|
|
5218
|
+
return new _Note(this.#data);
|
|
5219
|
+
}
|
|
4955
5220
|
if (this.#data.octave !== void 0) {
|
|
4956
5221
|
return _Note.fromMidi(this.midi + semitones);
|
|
4957
5222
|
}
|
|
@@ -4975,6 +5240,17 @@ var Note = class _Note {
|
|
|
4975
5240
|
equals(other) {
|
|
4976
5241
|
return this.#data.letter === other.#data.letter && this.#data.alter === other.#data.alter && this.#data.octave === other.#data.octave;
|
|
4977
5242
|
}
|
|
5243
|
+
/**
|
|
5244
|
+
* The plain note data, for JSON serialization.
|
|
5245
|
+
*
|
|
5246
|
+
* Private class fields do not serialize, so an explicit `toJSON` keeps
|
|
5247
|
+
* `JSON.stringify(note)` from collapsing to `{}`.
|
|
5248
|
+
*
|
|
5249
|
+
* @returns A copy of the underlying plain note object.
|
|
5250
|
+
*/
|
|
5251
|
+
toJSON() {
|
|
5252
|
+
return this.data;
|
|
5253
|
+
}
|
|
4978
5254
|
};
|
|
4979
5255
|
var Interval = class _Interval {
|
|
4980
5256
|
#number;
|
|
@@ -5023,10 +5299,32 @@ var Interval = class _Interval {
|
|
|
5023
5299
|
get name() {
|
|
5024
5300
|
return `${this.#quality}${this.#number}`;
|
|
5025
5301
|
}
|
|
5302
|
+
/**
|
|
5303
|
+
* The plain interval data, for JSON serialization.
|
|
5304
|
+
*
|
|
5305
|
+
* Private class fields do not serialize, so an explicit `toJSON` keeps
|
|
5306
|
+
* `JSON.stringify(interval)` from collapsing to `{}`.
|
|
5307
|
+
*
|
|
5308
|
+
* @returns The diatonic number, quality, and semitone span.
|
|
5309
|
+
*/
|
|
5310
|
+
toJSON() {
|
|
5311
|
+
return { number: this.#number, quality: this.#quality, semitones: this.#semitones };
|
|
5312
|
+
}
|
|
5026
5313
|
};
|
|
5027
5314
|
function resolveTonic(root, spelling) {
|
|
5028
5315
|
return typeof root === "string" ? Note.of(root) : new Note(spellPitchClassBare(root, spelling));
|
|
5029
5316
|
}
|
|
5317
|
+
function accidentalLoad(tonic, scale) {
|
|
5318
|
+
return spellScale(tonic, scale).reduce((sum, note) => sum + Math.abs(note.alter), 0);
|
|
5319
|
+
}
|
|
5320
|
+
function bestTonicForScale(rootPc, scale) {
|
|
5321
|
+
const sharp = spellPitchClassBare(rootPc, "sharp");
|
|
5322
|
+
const flat = spellPitchClassBare(rootPc, "flat");
|
|
5323
|
+
if (sharp.letter === flat.letter && sharp.alter === flat.alter) {
|
|
5324
|
+
return new Note(sharp);
|
|
5325
|
+
}
|
|
5326
|
+
return accidentalLoad(flat, scale) <= accidentalLoad(sharp, scale) ? new Note(flat) : new Note(sharp);
|
|
5327
|
+
}
|
|
5030
5328
|
var Key = class _Key {
|
|
5031
5329
|
#scale;
|
|
5032
5330
|
#tonic;
|
|
@@ -5043,24 +5341,34 @@ var Key = class _Key {
|
|
|
5043
5341
|
/**
|
|
5044
5342
|
* A major key.
|
|
5045
5343
|
*
|
|
5046
|
-
* @param root Tonic as a note name (e.g. `'Eb'`) or a pitch class; numeric
|
|
5047
|
-
*
|
|
5344
|
+
* @param root Tonic as a note name (e.g. `'Eb'`) or a pitch class; a numeric
|
|
5345
|
+
* root is spelled with whichever accidental side yields the fewest
|
|
5346
|
+
* accidentals across the scale.
|
|
5048
5347
|
* @returns The major key.
|
|
5049
5348
|
*/
|
|
5050
5349
|
static major(root) {
|
|
5051
|
-
|
|
5052
|
-
|
|
5350
|
+
if (typeof root === "string") {
|
|
5351
|
+
const tonic = Note.of(root);
|
|
5352
|
+
return new _Key(majorKey(tonic.pitchClass), tonic);
|
|
5353
|
+
}
|
|
5354
|
+
const scale = majorKey(root);
|
|
5355
|
+
return new _Key(scale, bestTonicForScale(root, scale));
|
|
5053
5356
|
}
|
|
5054
5357
|
/**
|
|
5055
5358
|
* A natural-minor key.
|
|
5056
5359
|
*
|
|
5057
|
-
* @param root Tonic as a note name or a pitch class; numeric
|
|
5058
|
-
* spelled with
|
|
5360
|
+
* @param root Tonic as a note name or a pitch class; a numeric root is
|
|
5361
|
+
* spelled with whichever accidental side yields the fewest accidentals
|
|
5362
|
+
* across the scale.
|
|
5059
5363
|
* @returns The minor key.
|
|
5060
5364
|
*/
|
|
5061
5365
|
static minor(root) {
|
|
5062
|
-
|
|
5063
|
-
|
|
5366
|
+
if (typeof root === "string") {
|
|
5367
|
+
const tonic = Note.of(root);
|
|
5368
|
+
return new _Key(minorKey(tonic.pitchClass), tonic);
|
|
5369
|
+
}
|
|
5370
|
+
const scale = minorKey(root);
|
|
5371
|
+
return new _Key(scale, bestTonicForScale(root, scale));
|
|
5064
5372
|
}
|
|
5065
5373
|
/**
|
|
5066
5374
|
* A key on a named scale (e.g. `'dorian'`, `'harmonicMinor'`).
|
|
@@ -5187,6 +5495,19 @@ var Key = class _Key {
|
|
|
5187
5495
|
contains(x) {
|
|
5188
5496
|
return isScaleTone(typeof x === "number" ? x : x.pitchClass, this.#scale);
|
|
5189
5497
|
}
|
|
5498
|
+
/**
|
|
5499
|
+
* The plain key data, for JSON serialization.
|
|
5500
|
+
*
|
|
5501
|
+
* Private class fields do not serialize, so an explicit `toJSON` keeps
|
|
5502
|
+
* `JSON.stringify(key)` from collapsing to `{}`. The result pairs the
|
|
5503
|
+
* `KeyScale` with the spelled tonic, enough to reconstruct the key via
|
|
5504
|
+
* {@link Key.of}.
|
|
5505
|
+
*
|
|
5506
|
+
* @returns The key/scale and its spelled tonic.
|
|
5507
|
+
*/
|
|
5508
|
+
toJSON() {
|
|
5509
|
+
return { scale: this.scale, tonic: this.#tonic.data };
|
|
5510
|
+
}
|
|
5190
5511
|
};
|
|
5191
5512
|
var Chord = class _Chord {
|
|
5192
5513
|
#data;
|
|
@@ -5383,13 +5704,17 @@ var Chord = class _Chord {
|
|
|
5383
5704
|
* @throws If no key is given and none is carried.
|
|
5384
5705
|
*/
|
|
5385
5706
|
negativeHarmony(key) {
|
|
5386
|
-
|
|
5707
|
+
const resolved = this.#resolveKey(key);
|
|
5708
|
+
return new _Chord(negativeHarmonyMirror(this.#data, resolved.scale), key ?? this.#key);
|
|
5387
5709
|
}
|
|
5388
5710
|
/**
|
|
5389
5711
|
* The n-th inversion: a copy whose bass is the chord tone `n` steps above the
|
|
5390
5712
|
* root in the interval template (`invert(1)` puts the third in the bass).
|
|
5391
5713
|
* `n` wraps around the template length; negative values count backwards.
|
|
5392
5714
|
*
|
|
5715
|
+
* `invert(0)` (and any `n` that wraps to it) is root position, so it carries
|
|
5716
|
+
* no slash bass and equals the original chord.
|
|
5717
|
+
*
|
|
5393
5718
|
* @param n The inversion number.
|
|
5394
5719
|
* @returns The inverted chord, keeping any key context.
|
|
5395
5720
|
* @throws If the chord has no intervals.
|
|
@@ -5402,7 +5727,11 @@ var Chord = class _Chord {
|
|
|
5402
5727
|
}
|
|
5403
5728
|
const index = (n % length + length) % length;
|
|
5404
5729
|
const data = copyChord(this.#data);
|
|
5405
|
-
|
|
5730
|
+
if (index === 0) {
|
|
5731
|
+
delete data.bassPc;
|
|
5732
|
+
} else {
|
|
5733
|
+
data.bassPc = mod125(this.#data.rootPc + (intervals[index] ?? 0));
|
|
5734
|
+
}
|
|
5406
5735
|
return new _Chord(data, this.#key);
|
|
5407
5736
|
}
|
|
5408
5737
|
/**
|
|
@@ -5583,6 +5912,21 @@ var Progression = class _Progression {
|
|
|
5583
5912
|
const cadence = from !== void 0 && to !== void 0 ? detectCadence(from.data, to.data, resolved.scale) : null;
|
|
5584
5913
|
return { chords, cadence };
|
|
5585
5914
|
}
|
|
5915
|
+
/**
|
|
5916
|
+
* The plain progression data, for JSON serialization.
|
|
5917
|
+
*
|
|
5918
|
+
* Private class fields do not serialize, so an explicit `toJSON` keeps
|
|
5919
|
+
* `JSON.stringify(progression)` from collapsing to `{}`. The chords are
|
|
5920
|
+
* emitted as plain data and the carried key, when present, as its own data.
|
|
5921
|
+
*
|
|
5922
|
+
* @returns The chord data sequence and the carried key, if any.
|
|
5923
|
+
*/
|
|
5924
|
+
toJSON() {
|
|
5925
|
+
return {
|
|
5926
|
+
chords: this.#chords.map((chord) => chord.data),
|
|
5927
|
+
key: this.#key?.toJSON()
|
|
5928
|
+
};
|
|
5929
|
+
}
|
|
5586
5930
|
/** Resolve the key for an analysis method: explicit first, then carried. */
|
|
5587
5931
|
#resolveKey(key) {
|
|
5588
5932
|
const resolved = key ?? this.#key;
|
|
@@ -5660,7 +6004,6 @@ function mulberry322(seed) {
|
|
|
5660
6004
|
}
|
|
5661
6005
|
function contourOffsets(contour, count) {
|
|
5662
6006
|
const offsets = [];
|
|
5663
|
-
const half = Math.floor(count / 2);
|
|
5664
6007
|
for (let i = 0; i < count; i += 1) {
|
|
5665
6008
|
switch (contour) {
|
|
5666
6009
|
case "ascending":
|
|
@@ -5670,7 +6013,7 @@ function contourOffsets(contour, count) {
|
|
|
5670
6013
|
offsets.push(-i);
|
|
5671
6014
|
break;
|
|
5672
6015
|
case "arch":
|
|
5673
|
-
offsets.push(i
|
|
6016
|
+
offsets.push(Math.min(i, count - 1 - i));
|
|
5674
6017
|
break;
|
|
5675
6018
|
case "wave":
|
|
5676
6019
|
offsets.push([0, 1, 0, -1][i % 4] ?? 0);
|
|
@@ -5686,11 +6029,15 @@ function generateMotif(opts) {
|
|
|
5686
6029
|
const noteCount = Math.max(3, bars * 2);
|
|
5687
6030
|
const beatsPerNote = totalBeats / noteCount;
|
|
5688
6031
|
const rng = mulberry322(opts.seed ?? 0);
|
|
6032
|
+
const jitterProb = Math.min(1, Math.max(0, opts.jitter ?? 0));
|
|
5689
6033
|
const tonic = pitchClass14(opts.key.rootPc) + 60;
|
|
5690
6034
|
const offsets = contourOffsets(contour, noteCount);
|
|
5691
6035
|
const notes = [];
|
|
5692
6036
|
for (let i = 0; i < noteCount; i += 1) {
|
|
5693
|
-
|
|
6037
|
+
let jitter = 0;
|
|
6038
|
+
if (jitterProb > 0 && rng() < jitterProb) {
|
|
6039
|
+
jitter = rng() < 0.5 ? 1 : -1;
|
|
6040
|
+
}
|
|
5694
6041
|
let pitch = stepDiatonic(tonic, (offsets[i] ?? 0) + jitter, opts.key);
|
|
5695
6042
|
const startBeat = i * beatsPerNote;
|
|
5696
6043
|
if (opts.chord && startBeat % 4 === 0) {
|
|
@@ -6002,6 +6349,9 @@ function generateProgression(opts) {
|
|
|
6002
6349
|
let preset;
|
|
6003
6350
|
if (opts.presetId !== void 0) {
|
|
6004
6351
|
preset = PRESETS.find((p) => p.id === opts.presetId);
|
|
6352
|
+
if (preset === void 0) {
|
|
6353
|
+
throw new Error(`Unknown progression preset: ${opts.presetId}`);
|
|
6354
|
+
}
|
|
6005
6355
|
}
|
|
6006
6356
|
if (preset === void 0) {
|
|
6007
6357
|
const pool = PRESETS.filter((p) => p.styles.includes(opts.style));
|
|
@@ -6072,15 +6422,17 @@ function generateRhythm(ts, opts = {}) {
|
|
|
6072
6422
|
const seed = opts.seed ?? 0;
|
|
6073
6423
|
const bars = opts.bars ?? DEFAULT_BARS;
|
|
6074
6424
|
const subdivision = opts.subdivision ?? DEFAULT_SUBDIVISION;
|
|
6075
|
-
const density = opts.density ?? DEFAULT_DENSITY;
|
|
6076
|
-
const
|
|
6425
|
+
const density = Math.min(1, Math.max(0, opts.density ?? DEFAULT_DENSITY));
|
|
6426
|
+
const barBeats = beatsPerBar(ts);
|
|
6427
|
+
const spanBeats = barBeats * bars;
|
|
6077
6428
|
const step = 1 / subdivision;
|
|
6078
6429
|
const slotCount = Math.round(spanBeats * subdivision);
|
|
6430
|
+
const slotsPerBar = Math.round(barBeats * subdivision);
|
|
6079
6431
|
const rng = createRng(seed);
|
|
6080
6432
|
const positions = [];
|
|
6081
6433
|
for (let i = 0; i < slotCount; i += 1) {
|
|
6082
6434
|
const position = i * step;
|
|
6083
|
-
if (i === 0) {
|
|
6435
|
+
if (i % slotsPerBar === 0) {
|
|
6084
6436
|
positions.push(position);
|
|
6085
6437
|
continue;
|
|
6086
6438
|
}
|