@onjmin/dtm 0.1.9 → 0.1.11
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/dist/index.d.mts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +440 -156
- package/dist/index.mjs +439 -156
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -39,6 +39,7 @@ __export(index_exports, {
|
|
|
39
39
|
LinkedList: () => LinkedList,
|
|
40
40
|
MAX_VOCAL_VOLUME: () => MAX_VOCAL_VOLUME,
|
|
41
41
|
MMLCore: () => MMLCore,
|
|
42
|
+
MML_END_MARKER: () => MML_END_MARKER,
|
|
42
43
|
PITCH_MAP: () => PITCH_MAP,
|
|
43
44
|
PREWARM_NOTES: () => PREWARM_NOTES,
|
|
44
45
|
TRACKS_ADVANCED: () => TRACKS_ADVANCED,
|
|
@@ -536,60 +537,190 @@ var QUALITY_BY_PCSET = (() => {
|
|
|
536
537
|
}
|
|
537
538
|
return map;
|
|
538
539
|
})();
|
|
539
|
-
var
|
|
540
|
+
var MAJOR_PROFILE = [
|
|
541
|
+
6.35,
|
|
542
|
+
2.23,
|
|
543
|
+
3.48,
|
|
544
|
+
2.33,
|
|
545
|
+
4.38,
|
|
546
|
+
4.09,
|
|
547
|
+
2.52,
|
|
548
|
+
5.19,
|
|
549
|
+
2.39,
|
|
550
|
+
3.66,
|
|
551
|
+
2.29,
|
|
552
|
+
2.88
|
|
553
|
+
];
|
|
554
|
+
var MINOR_PROFILE = [
|
|
555
|
+
6.33,
|
|
556
|
+
2.68,
|
|
557
|
+
3.52,
|
|
558
|
+
5.38,
|
|
559
|
+
2.6,
|
|
560
|
+
3.53,
|
|
561
|
+
2.54,
|
|
562
|
+
4.75,
|
|
563
|
+
3.98,
|
|
564
|
+
2.69,
|
|
565
|
+
3.34,
|
|
566
|
+
3.17
|
|
567
|
+
];
|
|
568
|
+
var mean = (a) => a.reduce((s, v) => s + v, 0) / a.length;
|
|
569
|
+
var pearson = (a, b) => {
|
|
570
|
+
const ma = mean(a);
|
|
571
|
+
const mb = mean(b);
|
|
572
|
+
let num = 0;
|
|
573
|
+
let da = 0;
|
|
574
|
+
let db = 0;
|
|
575
|
+
for (let i = 0; i < a.length; i++) {
|
|
576
|
+
const x = a[i] - ma;
|
|
577
|
+
const y = b[i] - mb;
|
|
578
|
+
num += x * y;
|
|
579
|
+
da += x * x;
|
|
580
|
+
db += y * y;
|
|
581
|
+
}
|
|
582
|
+
const den = Math.sqrt(da * db);
|
|
583
|
+
return den === 0 ? 0 : num / den;
|
|
584
|
+
};
|
|
585
|
+
var keyName = (tonic, mode, flat) => `${noteName(tonic, flat)} ${mode}`;
|
|
586
|
+
var stripScore = (c) => ({
|
|
587
|
+
tonic: c.tonic,
|
|
588
|
+
mode: c.mode,
|
|
589
|
+
name: c.name
|
|
590
|
+
});
|
|
591
|
+
var sameKey = (a, b) => a.tonic === b.tonic && a.mode === b.mode;
|
|
592
|
+
var buildHistogram = (notes) => {
|
|
593
|
+
const h = new Array(12).fill(0);
|
|
594
|
+
for (const n of notes) {
|
|
595
|
+
if (typeof n === "number") h[toPitchClass(n)] += 1;
|
|
596
|
+
else h[toPitchClass(n.pitch)] += n.duration ?? 1;
|
|
597
|
+
}
|
|
598
|
+
return h;
|
|
599
|
+
};
|
|
600
|
+
var windowHistogram = (notes, start, end) => {
|
|
601
|
+
const h = new Array(12).fill(0);
|
|
602
|
+
for (const n of notes) {
|
|
603
|
+
if (n.duration <= 0) {
|
|
604
|
+
if (n.when >= start && n.when < end) h[toPitchClass(n.pitch)] += 1;
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
const s = Math.max(n.when, start);
|
|
608
|
+
const e = Math.min(n.when + n.duration, end);
|
|
609
|
+
const overlap = e - s;
|
|
610
|
+
if (overlap > 0) h[toPitchClass(n.pitch)] += overlap;
|
|
611
|
+
}
|
|
612
|
+
return h;
|
|
613
|
+
};
|
|
614
|
+
var rankKeys = (histogram, flat) => {
|
|
615
|
+
const candidates = [];
|
|
616
|
+
for (let tonic = 0; tonic < 12; tonic++) {
|
|
617
|
+
for (const mode of ["major", "minor"]) {
|
|
618
|
+
const profile = mode === "major" ? MAJOR_PROFILE : MINOR_PROFILE;
|
|
619
|
+
const rotated = histogram.map(
|
|
620
|
+
(_, pc) => profile[toPitchClass(pc - tonic)]
|
|
621
|
+
);
|
|
622
|
+
candidates.push({
|
|
623
|
+
tonic,
|
|
624
|
+
mode,
|
|
625
|
+
name: keyName(tonic, mode, flat),
|
|
626
|
+
score: pearson(histogram, rotated)
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
631
|
+
return candidates;
|
|
632
|
+
};
|
|
633
|
+
var detectKey = (notes, options = {}) => {
|
|
540
634
|
if (!notes.length) return [];
|
|
541
635
|
const { flat = false } = options;
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
636
|
+
const histogram = buildHistogram(notes);
|
|
637
|
+
if (histogram.every((v) => v === 0)) return [];
|
|
638
|
+
return rankKeys(histogram, flat);
|
|
639
|
+
};
|
|
640
|
+
var coalesce = (segments) => {
|
|
641
|
+
const out = [];
|
|
642
|
+
for (const s of segments) {
|
|
643
|
+
const last = out[out.length - 1];
|
|
644
|
+
if (last && sameKey(last.key, s.key)) {
|
|
645
|
+
last.duration = s.when + s.duration - last.when;
|
|
646
|
+
} else {
|
|
647
|
+
out.push({ ...s });
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return out;
|
|
651
|
+
};
|
|
652
|
+
var mergeShortSegments = (segments, min) => {
|
|
653
|
+
if (min <= 0) return segments;
|
|
654
|
+
const result = segments.map((s) => ({ ...s }));
|
|
655
|
+
let i = 0;
|
|
656
|
+
while (i < result.length && result.length > 1) {
|
|
657
|
+
if (result[i].duration >= min) {
|
|
658
|
+
i++;
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
if (i > 0) {
|
|
662
|
+
result[i - 1].duration += result[i].duration;
|
|
663
|
+
result.splice(i, 1);
|
|
664
|
+
} else {
|
|
665
|
+
result[i + 1].when = result[i].when;
|
|
666
|
+
result[i + 1].duration += result[i].duration;
|
|
667
|
+
result.splice(i, 1);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return coalesce(result);
|
|
671
|
+
};
|
|
672
|
+
var detectKeyChanges = (notes, options = {}) => {
|
|
673
|
+
if (!notes.length) return [];
|
|
674
|
+
const { flat = false } = options;
|
|
675
|
+
const start = notes.reduce(
|
|
676
|
+
(m, n) => Math.min(m, n.when),
|
|
677
|
+
Number.POSITIVE_INFINITY
|
|
545
678
|
);
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
679
|
+
const end = notes.reduce(
|
|
680
|
+
(m, n) => Math.max(m, n.when + Math.max(n.duration, 0)),
|
|
681
|
+
Number.NEGATIVE_INFINITY
|
|
682
|
+
);
|
|
683
|
+
const span = end - start;
|
|
684
|
+
if (span <= 0) {
|
|
685
|
+
const top = detectKey(
|
|
686
|
+
notes.map((n) => ({ pitch: n.pitch, duration: Math.max(n.duration, 1) })),
|
|
687
|
+
{ flat }
|
|
688
|
+
)[0];
|
|
689
|
+
return top ? [{ key: stripScore(top), when: start, duration: 0 }] : [];
|
|
690
|
+
}
|
|
691
|
+
const windowSize = options.windowSize ?? span / 4;
|
|
692
|
+
const hopSize = options.hopSize ?? windowSize / 2;
|
|
693
|
+
const minSegmentDuration = options.minSegmentDuration ?? 0;
|
|
694
|
+
const switchMargin = options.switchMargin ?? 0.08;
|
|
695
|
+
const segments = [];
|
|
696
|
+
for (let t = start; t < end - 1e-9; t += hopSize) {
|
|
697
|
+
const regionEnd = Math.min(t + hopSize, end);
|
|
698
|
+
const winEnd = Math.min(t + windowSize, end);
|
|
699
|
+
const winStart = Math.max(start, winEnd - windowSize);
|
|
700
|
+
const histogram = windowHistogram(notes, winStart, winEnd);
|
|
701
|
+
const last = segments[segments.length - 1];
|
|
702
|
+
if (histogram.every((v) => v === 0)) {
|
|
703
|
+
if (last) last.duration = regionEnd - last.when;
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
const candidates = rankKeys(histogram, flat);
|
|
707
|
+
let chosen = candidates[0];
|
|
708
|
+
if (last) {
|
|
709
|
+
const current = candidates.find((c) => sameKey(c, last.key));
|
|
710
|
+
if (current && chosen.score - current.score <= switchMargin)
|
|
711
|
+
chosen = current;
|
|
712
|
+
}
|
|
713
|
+
if (last && sameKey(last.key, chosen)) {
|
|
714
|
+
last.duration = regionEnd - last.when;
|
|
715
|
+
} else {
|
|
716
|
+
segments.push({
|
|
717
|
+
key: stripScore(chosen),
|
|
718
|
+
when: t,
|
|
719
|
+
duration: regionEnd - t
|
|
582
720
|
});
|
|
583
721
|
}
|
|
584
722
|
}
|
|
585
|
-
|
|
586
|
-
if (Math.abs(a.score - b.score) > 1e-3) return b.score - a.score;
|
|
587
|
-
if (a.candidate.inversion !== b.candidate.inversion)
|
|
588
|
-
return a.candidate.inversion ? 1 : -1;
|
|
589
|
-
if (a.priority !== b.priority) return a.priority - b.priority;
|
|
590
|
-
return a.candidate.root - b.candidate.root;
|
|
591
|
-
});
|
|
592
|
-
return scored.map((s) => s.candidate);
|
|
723
|
+
return mergeShortSegments(coalesce(segments), minSegmentDuration);
|
|
593
724
|
};
|
|
594
725
|
var toneRoleWeight = (rel) => {
|
|
595
726
|
if (rel === 0) return 1.3;
|
|
@@ -623,6 +754,231 @@ var CHORD_TEMPLATES = (() => {
|
|
|
623
754
|
}
|
|
624
755
|
return templates;
|
|
625
756
|
})();
|
|
757
|
+
var MAJOR_SCALE = [0, 2, 4, 5, 7, 9, 11];
|
|
758
|
+
var NATURAL_MINOR_SCALE = [0, 2, 3, 5, 7, 8, 10];
|
|
759
|
+
var scaleOf = (key) => {
|
|
760
|
+
const base = key.mode === "major" ? MAJOR_SCALE : NATURAL_MINOR_SCALE;
|
|
761
|
+
return base.map((d) => toPitchClass(d + key.tonic));
|
|
762
|
+
};
|
|
763
|
+
var keyBonus = (tmpl, key) => {
|
|
764
|
+
const scale = scaleOf(key);
|
|
765
|
+
const scaleSet = new Set(scale);
|
|
766
|
+
const rootDiatonic = scaleSet.has(tmpl.root);
|
|
767
|
+
let allDiatonic = true;
|
|
768
|
+
for (const pc of tmpl.pcs)
|
|
769
|
+
if (!scaleSet.has(pc)) {
|
|
770
|
+
allDiatonic = false;
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
let bonus = 0;
|
|
774
|
+
if (allDiatonic) bonus += 0.25;
|
|
775
|
+
else if (rootDiatonic) bonus += 0.1;
|
|
776
|
+
const degree = toPitchClass(tmpl.root - key.tonic);
|
|
777
|
+
if (degree === 0 || degree === 5 || degree === 7) bonus += 0.05;
|
|
778
|
+
return bonus;
|
|
779
|
+
};
|
|
780
|
+
var makeFrame = (notes, start, end) => {
|
|
781
|
+
const raw = new Array(12).fill(0);
|
|
782
|
+
let total = 0;
|
|
783
|
+
let bassPitch = Number.POSITIVE_INFINITY;
|
|
784
|
+
let bass = -1;
|
|
785
|
+
for (const n of notes) {
|
|
786
|
+
const s = Math.max(n.when, start);
|
|
787
|
+
const e = Math.min(n.when + Math.max(n.duration, 0), end);
|
|
788
|
+
const overlap = n.duration <= 0 ? n.when >= start && n.when < end ? 1 : 0 : Math.max(e - s, 0);
|
|
789
|
+
if (overlap <= 0) continue;
|
|
790
|
+
raw[toPitchClass(n.pitch)] += overlap;
|
|
791
|
+
total += overlap;
|
|
792
|
+
if (n.pitch < bassPitch) {
|
|
793
|
+
bassPitch = n.pitch;
|
|
794
|
+
bass = toPitchClass(n.pitch);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
const profile = total > 0 ? raw.map((v) => v / total) : raw;
|
|
798
|
+
return {
|
|
799
|
+
when: start,
|
|
800
|
+
duration: end - start,
|
|
801
|
+
profile,
|
|
802
|
+
bass,
|
|
803
|
+
empty: total === 0
|
|
804
|
+
};
|
|
805
|
+
};
|
|
806
|
+
var emissionScore = (frame, tmpl, key, ncTonePenalty) => {
|
|
807
|
+
let hit = 0;
|
|
808
|
+
let miss = 0;
|
|
809
|
+
for (let pc = 0; pc < 12; pc++) {
|
|
810
|
+
const w = frame.profile[pc];
|
|
811
|
+
if (w === 0) continue;
|
|
812
|
+
if (tmpl.pcs.has(pc)) hit += w * tmpl.weights[pc];
|
|
813
|
+
else miss += w;
|
|
814
|
+
}
|
|
815
|
+
let score = hit - ncTonePenalty * miss;
|
|
816
|
+
if (frame.profile[tmpl.root] === 0) score -= 0.3;
|
|
817
|
+
if (frame.bass !== -1 && tmpl.root === frame.bass) score += 0.3;
|
|
818
|
+
if (key) score += keyBonus(tmpl, key);
|
|
819
|
+
score -= tmpl.priority * 2e-3;
|
|
820
|
+
return score;
|
|
821
|
+
};
|
|
822
|
+
var ROMAN = ["I", "II", "III", "IV", "V", "VI", "VII"];
|
|
823
|
+
var chordDegree = (key, tmpl) => {
|
|
824
|
+
const scale = key.mode === "major" ? MAJOR_SCALE : NATURAL_MINOR_SCALE;
|
|
825
|
+
const rel = toPitchClass(tmpl.root - key.tonic);
|
|
826
|
+
let idx = scale.indexOf(rel);
|
|
827
|
+
let accidental = "";
|
|
828
|
+
if (idx === -1) {
|
|
829
|
+
const below = scale.indexOf(toPitchClass(rel - 1));
|
|
830
|
+
const above = scale.indexOf(toPitchClass(rel + 1));
|
|
831
|
+
if (below !== -1) {
|
|
832
|
+
idx = below;
|
|
833
|
+
accidental = "#";
|
|
834
|
+
} else if (above !== -1) {
|
|
835
|
+
idx = above;
|
|
836
|
+
accidental = "b";
|
|
837
|
+
} else {
|
|
838
|
+
idx = 0;
|
|
839
|
+
accidental = "?";
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
const hasM3 = tmpl.rel.has(4);
|
|
843
|
+
const hasm3 = tmpl.rel.has(3);
|
|
844
|
+
const hasDim5 = tmpl.rel.has(6);
|
|
845
|
+
const hasAug5 = tmpl.rel.has(8);
|
|
846
|
+
const hasMin7 = tmpl.rel.has(10);
|
|
847
|
+
let numeral = ROMAN[idx];
|
|
848
|
+
let suffix = "";
|
|
849
|
+
if (hasm3 && hasDim5) {
|
|
850
|
+
numeral = numeral.toLowerCase();
|
|
851
|
+
suffix = hasMin7 ? "\xF87" : "\xB0";
|
|
852
|
+
if (tmpl.rel.has(9)) suffix = "\xB07";
|
|
853
|
+
} else if (hasM3 && hasAug5) {
|
|
854
|
+
suffix = "+";
|
|
855
|
+
} else if (hasm3) {
|
|
856
|
+
numeral = numeral.toLowerCase();
|
|
857
|
+
} else if (!hasM3) {
|
|
858
|
+
}
|
|
859
|
+
if (!suffix) {
|
|
860
|
+
if (tmpl.rel.has(11)) suffix = "M7";
|
|
861
|
+
else if (hasMin7) suffix = "7";
|
|
862
|
+
else if (tmpl.rel.has(9) && !tmpl.rel.has(10)) suffix = "6";
|
|
863
|
+
}
|
|
864
|
+
return accidental + numeral + suffix;
|
|
865
|
+
};
|
|
866
|
+
var viterbi = (emissions, changePenalty) => {
|
|
867
|
+
const T = emissions.length;
|
|
868
|
+
const N = CHORD_TEMPLATES.length;
|
|
869
|
+
if (T === 0) return [];
|
|
870
|
+
const back = Array.from(
|
|
871
|
+
{ length: T },
|
|
872
|
+
() => new Array(N).fill(-1)
|
|
873
|
+
);
|
|
874
|
+
let prev = emissions[0].slice();
|
|
875
|
+
for (let t = 1; t < T; t++) {
|
|
876
|
+
let bestPrevVal = Number.NEGATIVE_INFINITY;
|
|
877
|
+
let bestPrevIdx = 0;
|
|
878
|
+
for (let j = 0; j < N; j++)
|
|
879
|
+
if (prev[j] > bestPrevVal) {
|
|
880
|
+
bestPrevVal = prev[j];
|
|
881
|
+
bestPrevIdx = j;
|
|
882
|
+
}
|
|
883
|
+
const curr = new Array(N).fill(0);
|
|
884
|
+
const em = emissions[t];
|
|
885
|
+
const switchVal = bestPrevVal - changePenalty;
|
|
886
|
+
for (let i = 0; i < N; i++) {
|
|
887
|
+
if (prev[i] >= switchVal) {
|
|
888
|
+
curr[i] = em[i] + prev[i];
|
|
889
|
+
back[t][i] = i;
|
|
890
|
+
} else {
|
|
891
|
+
curr[i] = em[i] + switchVal;
|
|
892
|
+
back[t][i] = bestPrevIdx;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
prev = curr;
|
|
896
|
+
}
|
|
897
|
+
let bestIdx = 0;
|
|
898
|
+
for (let i = 1; i < N; i++) if (prev[i] > prev[bestIdx]) bestIdx = i;
|
|
899
|
+
const path = new Array(T).fill(0);
|
|
900
|
+
path[T - 1] = bestIdx;
|
|
901
|
+
for (let t = T - 1; t > 0; t--) path[t - 1] = back[t][path[t]];
|
|
902
|
+
return path;
|
|
903
|
+
};
|
|
904
|
+
var keyAt = (keys, when) => {
|
|
905
|
+
for (const k of keys)
|
|
906
|
+
if (when >= k.when && when < k.when + k.duration) return k.key;
|
|
907
|
+
return keys.length ? keys[keys.length - 1].key : null;
|
|
908
|
+
};
|
|
909
|
+
var buildSymbol = (tmpl, bass, flat) => {
|
|
910
|
+
const rootSymbol = noteName(tmpl.root, flat) + tmpl.quality;
|
|
911
|
+
const inversion = bass !== -1 && bass !== tmpl.root && tmpl.pcs.has(bass);
|
|
912
|
+
return {
|
|
913
|
+
symbol: inversion ? `${rootSymbol}/${noteName(bass, flat)}` : rootSymbol,
|
|
914
|
+
rootSymbol,
|
|
915
|
+
inversion,
|
|
916
|
+
bass: bass === -1 ? tmpl.root : bass
|
|
917
|
+
};
|
|
918
|
+
};
|
|
919
|
+
var detectProgression = (notes, options = {}) => {
|
|
920
|
+
if (!notes.length) return { keys: [], chords: [] };
|
|
921
|
+
const {
|
|
922
|
+
flat = false,
|
|
923
|
+
bpm,
|
|
924
|
+
frameSize = 0.5,
|
|
925
|
+
changePenalty = 0.4,
|
|
926
|
+
nonChordTonePenalty = 0.55,
|
|
927
|
+
useKey = true
|
|
928
|
+
} = options;
|
|
929
|
+
const keys = detectKeyChanges(notes, options);
|
|
930
|
+
const start = notes.reduce(
|
|
931
|
+
(m, n) => Math.min(m, n.when),
|
|
932
|
+
Number.POSITIVE_INFINITY
|
|
933
|
+
);
|
|
934
|
+
const end = notes.reduce(
|
|
935
|
+
(m, n) => Math.max(m, n.when + Math.max(n.duration, 0)),
|
|
936
|
+
Number.NEGATIVE_INFINITY
|
|
937
|
+
);
|
|
938
|
+
if (end <= start) return { keys, chords: [] };
|
|
939
|
+
const frameDur = bpm ? 60 / bpm : Math.max(frameSize, 1e-3);
|
|
940
|
+
const frames = [];
|
|
941
|
+
for (let t = start; t < end - 1e-9; t += frameDur)
|
|
942
|
+
frames.push(makeFrame(notes, t, Math.min(t + frameDur, end)));
|
|
943
|
+
const emissions = frames.map((frame) => {
|
|
944
|
+
if (frame.empty) return new Array(CHORD_TEMPLATES.length).fill(0);
|
|
945
|
+
const key = useKey ? keyAt(keys, frame.when + frame.duration / 2) : null;
|
|
946
|
+
return CHORD_TEMPLATES.map(
|
|
947
|
+
(tmpl) => emissionScore(frame, tmpl, key, nonChordTonePenalty)
|
|
948
|
+
);
|
|
949
|
+
});
|
|
950
|
+
const path = viterbi(emissions, changePenalty);
|
|
951
|
+
const chords = [];
|
|
952
|
+
for (let t = 0; t < frames.length; t++) {
|
|
953
|
+
const frame = frames[t];
|
|
954
|
+
const tmpl = CHORD_TEMPLATES[path[t]];
|
|
955
|
+
const last = chords[chords.length - 1];
|
|
956
|
+
const sameAsLast = last && last.root === tmpl.root && last.quality === tmpl.quality;
|
|
957
|
+
if (sameAsLast) {
|
|
958
|
+
last.duration = frame.when + frame.duration - last.when;
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
const key = keyAt(keys, frame.when + frame.duration / 2);
|
|
962
|
+
const { symbol, rootSymbol, inversion, bass } = buildSymbol(
|
|
963
|
+
tmpl,
|
|
964
|
+
frame.bass,
|
|
965
|
+
flat
|
|
966
|
+
);
|
|
967
|
+
chords.push({
|
|
968
|
+
symbol,
|
|
969
|
+
rootSymbol,
|
|
970
|
+
root: tmpl.root,
|
|
971
|
+
quality: tmpl.quality,
|
|
972
|
+
bass,
|
|
973
|
+
inversion,
|
|
974
|
+
when: frame.when,
|
|
975
|
+
duration: frame.duration,
|
|
976
|
+
key,
|
|
977
|
+
degree: key ? chordDegree(key, tmpl) : null
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
return { keys, chords };
|
|
981
|
+
};
|
|
626
982
|
var toHan = (str) => str.replace(/[!-~]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) - 65248)).replace(/ /g, " ");
|
|
627
983
|
var parseChords = (str, bpm = 120) => {
|
|
628
984
|
const output = [];
|
|
@@ -1538,6 +1894,7 @@ var DEFAULT_PAN = 64;
|
|
|
1538
1894
|
var DEFAULT_VELOCITY = 100;
|
|
1539
1895
|
var DEFAULT_PLAYBACK_VELOCITY = 127;
|
|
1540
1896
|
var DEFAULT_STEPS_PER_BAR = 192;
|
|
1897
|
+
var MML_END_MARKER = "#end;";
|
|
1541
1898
|
|
|
1542
1899
|
// src/lyrics.ts
|
|
1543
1900
|
var kanaTable = {
|
|
@@ -3670,7 +4027,9 @@ var parseMML = (mml, options = {}) => {
|
|
|
3670
4027
|
const meta = parseMmlMeta(noComments);
|
|
3671
4028
|
const noMeta = stripMmlMeta(noComments);
|
|
3672
4029
|
const lyrics = collectLyrics ? parseLyrics(noMeta) : void 0;
|
|
3673
|
-
const
|
|
4030
|
+
const endMarkerBase = MML_END_MARKER.replace(/;+$/, "");
|
|
4031
|
+
const endRegex = new RegExp(`(?<![cdafgCDAFG])${endMarkerBase}\\b;?`, "gi");
|
|
4032
|
+
const fullMML = stripLyrics(noMeta).replace(endRegex, "").replace(/[\n\r]+/g, " ").trim();
|
|
3674
4033
|
const parts = fullMML.split(/(@\d+)/).filter((p) => p.trim().length > 0);
|
|
3675
4034
|
let trackIndex = 0;
|
|
3676
4035
|
let octave = 4;
|
|
@@ -5973,8 +6332,8 @@ var mountDAW = (target, options = {}) => {
|
|
|
5973
6332
|
const decomposedMini = monoTracks.map(
|
|
5974
6333
|
(notes, i) => `@${i}${refCore.getMMLFromNotes(notes, bpm, 100).trim().replace(/\s+/g, "")}`
|
|
5975
6334
|
);
|
|
5976
|
-
const full2 = [metaLine, ...decomposedFull].filter((s) => s.length > 0).join(";\n");
|
|
5977
|
-
const minified2 = [metaLine, ...decomposedMini].filter((s) => s.length > 0).join(";");
|
|
6335
|
+
const full2 = [metaLine, ...decomposedFull, MML_END_MARKER].filter((s) => s.length > 0).join(";\n");
|
|
6336
|
+
const minified2 = [metaLine, ...decomposedMini, MML_END_MARKER].filter((s) => s.length > 0).join(";");
|
|
5978
6337
|
return {
|
|
5979
6338
|
full: full2,
|
|
5980
6339
|
minified: minified2,
|
|
@@ -6014,8 +6373,13 @@ var mountDAW = (target, options = {}) => {
|
|
|
6014
6373
|
const head = params ? `${x.model} ${params}` : x.model;
|
|
6015
6374
|
return `@@${x.i} ${head} ${x.text}`;
|
|
6016
6375
|
});
|
|
6017
|
-
const full = [metaLine, ...trackLines, ...lyricLines].filter((s) => s.length > 0).join(";\n");
|
|
6018
|
-
const minified = [
|
|
6376
|
+
const full = [metaLine, ...trackLines, ...lyricLines, MML_END_MARKER].filter((s) => s.length > 0).join(";\n");
|
|
6377
|
+
const minified = [
|
|
6378
|
+
metaLine,
|
|
6379
|
+
...trackLinesMini,
|
|
6380
|
+
...lyricLines,
|
|
6381
|
+
MML_END_MARKER
|
|
6382
|
+
].filter((s) => s.length > 0).join(";");
|
|
6019
6383
|
return {
|
|
6020
6384
|
full,
|
|
6021
6385
|
minified,
|
|
@@ -6661,116 +7025,35 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6661
7025
|
const trackIndices = [...new Set(placements.map((p) => p.trackIndex))].sort(
|
|
6662
7026
|
(a, b) => a - b
|
|
6663
7027
|
);
|
|
6664
|
-
const trackStats = trackIndices.map((index) => {
|
|
6665
|
-
const trackPlacements = placements.filter((p) => p.trackIndex === index);
|
|
6666
|
-
if (trackPlacements.length === 0) {
|
|
6667
|
-
return { index, isChords: false, avgPitch: 0, noteCount: 0 };
|
|
6668
|
-
}
|
|
6669
|
-
const sumPitch = trackPlacements.reduce((sum, p) => sum + p.pitch, 0);
|
|
6670
|
-
const avgPitch = sumPitch / trackPlacements.length;
|
|
6671
|
-
const stepCounts = /* @__PURE__ */ new Map();
|
|
6672
|
-
for (const p of trackPlacements) {
|
|
6673
|
-
for (let s = p.startStep; s < p.startStep + p.durationSteps; s++) {
|
|
6674
|
-
stepCounts.set(s, (stepCounts.get(s) ?? 0) + 1);
|
|
6675
|
-
}
|
|
6676
|
-
}
|
|
6677
|
-
const polyphonicSteps = Array.from(stepCounts.values()).filter(
|
|
6678
|
-
(c) => c >= 2
|
|
6679
|
-
).length;
|
|
6680
|
-
const isChords = polyphonicSteps > 0;
|
|
6681
|
-
return {
|
|
6682
|
-
index,
|
|
6683
|
-
isChords,
|
|
6684
|
-
avgPitch,
|
|
6685
|
-
noteCount: trackPlacements.length
|
|
6686
|
-
};
|
|
6687
|
-
});
|
|
6688
|
-
const chordTrackIndices = trackStats.filter((s) => s.isChords).map((s) => s.index);
|
|
6689
|
-
const nonChordTracks = trackStats.filter(
|
|
6690
|
-
(s) => !s.isChords && s.noteCount > 0
|
|
6691
|
-
);
|
|
6692
|
-
let bassTrackIndex = null;
|
|
6693
|
-
if (nonChordTracks.length > 0) {
|
|
6694
|
-
nonChordTracks.sort((a, b) => a.avgPitch - b.avgPitch);
|
|
6695
|
-
bassTrackIndex = nonChordTracks[0].index;
|
|
6696
|
-
}
|
|
6697
|
-
const priorityTrackIndices = /* @__PURE__ */ new Set();
|
|
6698
|
-
for (const idx of chordTrackIndices) {
|
|
6699
|
-
priorityTrackIndices.add(idx);
|
|
6700
|
-
}
|
|
6701
|
-
if (bassTrackIndex !== null) {
|
|
6702
|
-
priorityTrackIndices.add(bassTrackIndex);
|
|
6703
|
-
}
|
|
6704
|
-
const hasPriorityTracksInMml = priorityTrackIndices.size > 0;
|
|
6705
7028
|
const maxStep = placements.reduce(
|
|
6706
7029
|
(max, p) => Math.max(max, p.startStep + p.durationSteps),
|
|
6707
7030
|
0
|
|
6708
7031
|
);
|
|
6709
|
-
const
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
{ length: maxStep + 1 },
|
|
6715
|
-
() => /* @__PURE__ */ new Set()
|
|
6716
|
-
);
|
|
6717
|
-
for (const p of placements) {
|
|
6718
|
-
for (let s = p.startStep; s < p.startStep + p.durationSteps; s++) {
|
|
6719
|
-
if (s >= 0 && s <= maxStep) {
|
|
6720
|
-
allPitches[s].add(p.pitch);
|
|
6721
|
-
if (priorityTrackIndices.has(p.trackIndex)) {
|
|
6722
|
-
priorityPitches[s].add(p.pitch);
|
|
6723
|
-
}
|
|
6724
|
-
}
|
|
6725
|
-
}
|
|
6726
|
-
}
|
|
7032
|
+
const timedNotes = placements.map((p) => ({
|
|
7033
|
+
pitch: p.pitch,
|
|
7034
|
+
when: p.startStep * secondsPerStep,
|
|
7035
|
+
duration: p.durationSteps * secondsPerStep
|
|
7036
|
+
}));
|
|
6727
7037
|
const stepChords = [];
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
hasPriorityStarted = true;
|
|
6741
|
-
}
|
|
6742
|
-
}
|
|
6743
|
-
const pitchDurations = /* @__PURE__ */ new Map();
|
|
6744
|
-
for (let s = startS; s <= endS; s++) {
|
|
6745
|
-
const usePriority = hasPriorityStarted && hasPriorityTracksInMml;
|
|
6746
|
-
const pitches = usePriority ? Array.from(priorityPitches[s]) : Array.from(allPitches[s]);
|
|
6747
|
-
for (const p of pitches) {
|
|
6748
|
-
pitchDurations.set(p, (pitchDurations.get(p) ?? 0) + 1);
|
|
6749
|
-
}
|
|
6750
|
-
}
|
|
6751
|
-
let activePitches = Array.from(pitchDurations.entries()).filter(([_, dur]) => dur >= MIN_DURATION).map(([p, _]) => p);
|
|
6752
|
-
if (activePitches.length === 0 && pitchDurations.size > 0) {
|
|
6753
|
-
const maxDur = Math.max(...pitchDurations.values());
|
|
6754
|
-
activePitches = Array.from(pitchDurations.entries()).filter(([_, dur]) => dur === maxDur).map(([p, _]) => p);
|
|
6755
|
-
}
|
|
6756
|
-
let gridChord = lastChord;
|
|
6757
|
-
if (activePitches.length > 0) {
|
|
6758
|
-
const sortedPitches = activePitches.sort((a, b) => a - b);
|
|
6759
|
-
const cacheKey = sortedPitches.join(",");
|
|
6760
|
-
if (chordCache.has(cacheKey)) {
|
|
6761
|
-
const chordName = chordCache.get(cacheKey);
|
|
6762
|
-
if (chordName) gridChord = chordName;
|
|
6763
|
-
} else {
|
|
6764
|
-
const candidates = detectChord(sortedPitches);
|
|
6765
|
-
const chordName = candidates[0]?.symbol ?? "";
|
|
6766
|
-
if (chordName) gridChord = chordName;
|
|
6767
|
-
chordCache.set(cacheKey, chordName);
|
|
7038
|
+
if (timedNotes.length > 0) {
|
|
7039
|
+
let chordSegments = [];
|
|
7040
|
+
try {
|
|
7041
|
+
chordSegments = detectProgression(timedNotes, { bpm }).chords;
|
|
7042
|
+
} catch {
|
|
7043
|
+
chordSegments = [];
|
|
7044
|
+
}
|
|
7045
|
+
for (const seg of chordSegments) {
|
|
7046
|
+
const startStep = Math.max(0, Math.round(seg.when / secondsPerStep));
|
|
7047
|
+
const endStep = Math.round((seg.when + seg.duration) / secondsPerStep);
|
|
7048
|
+
for (let s = startStep; s < endStep && s <= maxStep; s++) {
|
|
7049
|
+
stepChords[s] = seg.symbol;
|
|
6768
7050
|
}
|
|
6769
7051
|
}
|
|
6770
|
-
|
|
6771
|
-
|
|
7052
|
+
let lastChord = "";
|
|
7053
|
+
for (let s = 0; s <= maxStep; s++) {
|
|
7054
|
+
if (stepChords[s]) lastChord = stepChords[s];
|
|
7055
|
+
else stepChords[s] = lastChord;
|
|
6772
7056
|
}
|
|
6773
|
-
lastChord = gridChord;
|
|
6774
7057
|
}
|
|
6775
7058
|
const seqTracks = trackIndices.map((index) => {
|
|
6776
7059
|
let id = 0;
|
|
@@ -6943,7 +7226,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6943
7226
|
em.textContent = "\u{1F97A}";
|
|
6944
7227
|
scheduleBlink(em);
|
|
6945
7228
|
},
|
|
6946
|
-
|
|
7229
|
+
100 + Math.random() * 50
|
|
6947
7230
|
);
|
|
6948
7231
|
blinkTimers.push(t2);
|
|
6949
7232
|
}, delay);
|
|
@@ -7916,6 +8199,7 @@ var createDtmStudio = async (options = {}) => {
|
|
|
7916
8199
|
LinkedList,
|
|
7917
8200
|
MAX_VOCAL_VOLUME,
|
|
7918
8201
|
MMLCore,
|
|
8202
|
+
MML_END_MARKER,
|
|
7919
8203
|
PITCH_MAP,
|
|
7920
8204
|
PREWARM_NOTES,
|
|
7921
8205
|
TRACKS_ADVANCED,
|