@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.mjs
CHANGED
|
@@ -432,60 +432,190 @@ var QUALITY_BY_PCSET = (() => {
|
|
|
432
432
|
}
|
|
433
433
|
return map;
|
|
434
434
|
})();
|
|
435
|
-
var
|
|
435
|
+
var MAJOR_PROFILE = [
|
|
436
|
+
6.35,
|
|
437
|
+
2.23,
|
|
438
|
+
3.48,
|
|
439
|
+
2.33,
|
|
440
|
+
4.38,
|
|
441
|
+
4.09,
|
|
442
|
+
2.52,
|
|
443
|
+
5.19,
|
|
444
|
+
2.39,
|
|
445
|
+
3.66,
|
|
446
|
+
2.29,
|
|
447
|
+
2.88
|
|
448
|
+
];
|
|
449
|
+
var MINOR_PROFILE = [
|
|
450
|
+
6.33,
|
|
451
|
+
2.68,
|
|
452
|
+
3.52,
|
|
453
|
+
5.38,
|
|
454
|
+
2.6,
|
|
455
|
+
3.53,
|
|
456
|
+
2.54,
|
|
457
|
+
4.75,
|
|
458
|
+
3.98,
|
|
459
|
+
2.69,
|
|
460
|
+
3.34,
|
|
461
|
+
3.17
|
|
462
|
+
];
|
|
463
|
+
var mean = (a) => a.reduce((s, v) => s + v, 0) / a.length;
|
|
464
|
+
var pearson = (a, b) => {
|
|
465
|
+
const ma = mean(a);
|
|
466
|
+
const mb = mean(b);
|
|
467
|
+
let num = 0;
|
|
468
|
+
let da = 0;
|
|
469
|
+
let db = 0;
|
|
470
|
+
for (let i = 0; i < a.length; i++) {
|
|
471
|
+
const x = a[i] - ma;
|
|
472
|
+
const y = b[i] - mb;
|
|
473
|
+
num += x * y;
|
|
474
|
+
da += x * x;
|
|
475
|
+
db += y * y;
|
|
476
|
+
}
|
|
477
|
+
const den = Math.sqrt(da * db);
|
|
478
|
+
return den === 0 ? 0 : num / den;
|
|
479
|
+
};
|
|
480
|
+
var keyName = (tonic, mode, flat) => `${noteName(tonic, flat)} ${mode}`;
|
|
481
|
+
var stripScore = (c) => ({
|
|
482
|
+
tonic: c.tonic,
|
|
483
|
+
mode: c.mode,
|
|
484
|
+
name: c.name
|
|
485
|
+
});
|
|
486
|
+
var sameKey = (a, b) => a.tonic === b.tonic && a.mode === b.mode;
|
|
487
|
+
var buildHistogram = (notes) => {
|
|
488
|
+
const h = new Array(12).fill(0);
|
|
489
|
+
for (const n of notes) {
|
|
490
|
+
if (typeof n === "number") h[toPitchClass(n)] += 1;
|
|
491
|
+
else h[toPitchClass(n.pitch)] += n.duration ?? 1;
|
|
492
|
+
}
|
|
493
|
+
return h;
|
|
494
|
+
};
|
|
495
|
+
var windowHistogram = (notes, start, end) => {
|
|
496
|
+
const h = new Array(12).fill(0);
|
|
497
|
+
for (const n of notes) {
|
|
498
|
+
if (n.duration <= 0) {
|
|
499
|
+
if (n.when >= start && n.when < end) h[toPitchClass(n.pitch)] += 1;
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
const s = Math.max(n.when, start);
|
|
503
|
+
const e = Math.min(n.when + n.duration, end);
|
|
504
|
+
const overlap = e - s;
|
|
505
|
+
if (overlap > 0) h[toPitchClass(n.pitch)] += overlap;
|
|
506
|
+
}
|
|
507
|
+
return h;
|
|
508
|
+
};
|
|
509
|
+
var rankKeys = (histogram, flat) => {
|
|
510
|
+
const candidates = [];
|
|
511
|
+
for (let tonic = 0; tonic < 12; tonic++) {
|
|
512
|
+
for (const mode of ["major", "minor"]) {
|
|
513
|
+
const profile = mode === "major" ? MAJOR_PROFILE : MINOR_PROFILE;
|
|
514
|
+
const rotated = histogram.map(
|
|
515
|
+
(_, pc) => profile[toPitchClass(pc - tonic)]
|
|
516
|
+
);
|
|
517
|
+
candidates.push({
|
|
518
|
+
tonic,
|
|
519
|
+
mode,
|
|
520
|
+
name: keyName(tonic, mode, flat),
|
|
521
|
+
score: pearson(histogram, rotated)
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
526
|
+
return candidates;
|
|
527
|
+
};
|
|
528
|
+
var detectKey = (notes, options = {}) => {
|
|
436
529
|
if (!notes.length) return [];
|
|
437
530
|
const { flat = false } = options;
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
531
|
+
const histogram = buildHistogram(notes);
|
|
532
|
+
if (histogram.every((v) => v === 0)) return [];
|
|
533
|
+
return rankKeys(histogram, flat);
|
|
534
|
+
};
|
|
535
|
+
var coalesce = (segments) => {
|
|
536
|
+
const out = [];
|
|
537
|
+
for (const s of segments) {
|
|
538
|
+
const last = out[out.length - 1];
|
|
539
|
+
if (last && sameKey(last.key, s.key)) {
|
|
540
|
+
last.duration = s.when + s.duration - last.when;
|
|
541
|
+
} else {
|
|
542
|
+
out.push({ ...s });
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return out;
|
|
546
|
+
};
|
|
547
|
+
var mergeShortSegments = (segments, min) => {
|
|
548
|
+
if (min <= 0) return segments;
|
|
549
|
+
const result = segments.map((s) => ({ ...s }));
|
|
550
|
+
let i = 0;
|
|
551
|
+
while (i < result.length && result.length > 1) {
|
|
552
|
+
if (result[i].duration >= min) {
|
|
553
|
+
i++;
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
if (i > 0) {
|
|
557
|
+
result[i - 1].duration += result[i].duration;
|
|
558
|
+
result.splice(i, 1);
|
|
559
|
+
} else {
|
|
560
|
+
result[i + 1].when = result[i].when;
|
|
561
|
+
result[i + 1].duration += result[i].duration;
|
|
562
|
+
result.splice(i, 1);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return coalesce(result);
|
|
566
|
+
};
|
|
567
|
+
var detectKeyChanges = (notes, options = {}) => {
|
|
568
|
+
if (!notes.length) return [];
|
|
569
|
+
const { flat = false } = options;
|
|
570
|
+
const start = notes.reduce(
|
|
571
|
+
(m, n) => Math.min(m, n.when),
|
|
572
|
+
Number.POSITIVE_INFINITY
|
|
441
573
|
);
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
574
|
+
const end = notes.reduce(
|
|
575
|
+
(m, n) => Math.max(m, n.when + Math.max(n.duration, 0)),
|
|
576
|
+
Number.NEGATIVE_INFINITY
|
|
577
|
+
);
|
|
578
|
+
const span = end - start;
|
|
579
|
+
if (span <= 0) {
|
|
580
|
+
const top = detectKey(
|
|
581
|
+
notes.map((n) => ({ pitch: n.pitch, duration: Math.max(n.duration, 1) })),
|
|
582
|
+
{ flat }
|
|
583
|
+
)[0];
|
|
584
|
+
return top ? [{ key: stripScore(top), when: start, duration: 0 }] : [];
|
|
585
|
+
}
|
|
586
|
+
const windowSize = options.windowSize ?? span / 4;
|
|
587
|
+
const hopSize = options.hopSize ?? windowSize / 2;
|
|
588
|
+
const minSegmentDuration = options.minSegmentDuration ?? 0;
|
|
589
|
+
const switchMargin = options.switchMargin ?? 0.08;
|
|
590
|
+
const segments = [];
|
|
591
|
+
for (let t = start; t < end - 1e-9; t += hopSize) {
|
|
592
|
+
const regionEnd = Math.min(t + hopSize, end);
|
|
593
|
+
const winEnd = Math.min(t + windowSize, end);
|
|
594
|
+
const winStart = Math.max(start, winEnd - windowSize);
|
|
595
|
+
const histogram = windowHistogram(notes, winStart, winEnd);
|
|
596
|
+
const last = segments[segments.length - 1];
|
|
597
|
+
if (histogram.every((v) => v === 0)) {
|
|
598
|
+
if (last) last.duration = regionEnd - last.when;
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
const candidates = rankKeys(histogram, flat);
|
|
602
|
+
let chosen = candidates[0];
|
|
603
|
+
if (last) {
|
|
604
|
+
const current = candidates.find((c) => sameKey(c, last.key));
|
|
605
|
+
if (current && chosen.score - current.score <= switchMargin)
|
|
606
|
+
chosen = current;
|
|
607
|
+
}
|
|
608
|
+
if (last && sameKey(last.key, chosen)) {
|
|
609
|
+
last.duration = regionEnd - last.when;
|
|
610
|
+
} else {
|
|
611
|
+
segments.push({
|
|
612
|
+
key: stripScore(chosen),
|
|
613
|
+
when: t,
|
|
614
|
+
duration: regionEnd - t
|
|
478
615
|
});
|
|
479
616
|
}
|
|
480
617
|
}
|
|
481
|
-
|
|
482
|
-
if (Math.abs(a.score - b.score) > 1e-3) return b.score - a.score;
|
|
483
|
-
if (a.candidate.inversion !== b.candidate.inversion)
|
|
484
|
-
return a.candidate.inversion ? 1 : -1;
|
|
485
|
-
if (a.priority !== b.priority) return a.priority - b.priority;
|
|
486
|
-
return a.candidate.root - b.candidate.root;
|
|
487
|
-
});
|
|
488
|
-
return scored.map((s) => s.candidate);
|
|
618
|
+
return mergeShortSegments(coalesce(segments), minSegmentDuration);
|
|
489
619
|
};
|
|
490
620
|
var toneRoleWeight = (rel) => {
|
|
491
621
|
if (rel === 0) return 1.3;
|
|
@@ -519,6 +649,231 @@ var CHORD_TEMPLATES = (() => {
|
|
|
519
649
|
}
|
|
520
650
|
return templates;
|
|
521
651
|
})();
|
|
652
|
+
var MAJOR_SCALE = [0, 2, 4, 5, 7, 9, 11];
|
|
653
|
+
var NATURAL_MINOR_SCALE = [0, 2, 3, 5, 7, 8, 10];
|
|
654
|
+
var scaleOf = (key) => {
|
|
655
|
+
const base = key.mode === "major" ? MAJOR_SCALE : NATURAL_MINOR_SCALE;
|
|
656
|
+
return base.map((d) => toPitchClass(d + key.tonic));
|
|
657
|
+
};
|
|
658
|
+
var keyBonus = (tmpl, key) => {
|
|
659
|
+
const scale = scaleOf(key);
|
|
660
|
+
const scaleSet = new Set(scale);
|
|
661
|
+
const rootDiatonic = scaleSet.has(tmpl.root);
|
|
662
|
+
let allDiatonic = true;
|
|
663
|
+
for (const pc of tmpl.pcs)
|
|
664
|
+
if (!scaleSet.has(pc)) {
|
|
665
|
+
allDiatonic = false;
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
let bonus = 0;
|
|
669
|
+
if (allDiatonic) bonus += 0.25;
|
|
670
|
+
else if (rootDiatonic) bonus += 0.1;
|
|
671
|
+
const degree = toPitchClass(tmpl.root - key.tonic);
|
|
672
|
+
if (degree === 0 || degree === 5 || degree === 7) bonus += 0.05;
|
|
673
|
+
return bonus;
|
|
674
|
+
};
|
|
675
|
+
var makeFrame = (notes, start, end) => {
|
|
676
|
+
const raw = new Array(12).fill(0);
|
|
677
|
+
let total = 0;
|
|
678
|
+
let bassPitch = Number.POSITIVE_INFINITY;
|
|
679
|
+
let bass = -1;
|
|
680
|
+
for (const n of notes) {
|
|
681
|
+
const s = Math.max(n.when, start);
|
|
682
|
+
const e = Math.min(n.when + Math.max(n.duration, 0), end);
|
|
683
|
+
const overlap = n.duration <= 0 ? n.when >= start && n.when < end ? 1 : 0 : Math.max(e - s, 0);
|
|
684
|
+
if (overlap <= 0) continue;
|
|
685
|
+
raw[toPitchClass(n.pitch)] += overlap;
|
|
686
|
+
total += overlap;
|
|
687
|
+
if (n.pitch < bassPitch) {
|
|
688
|
+
bassPitch = n.pitch;
|
|
689
|
+
bass = toPitchClass(n.pitch);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
const profile = total > 0 ? raw.map((v) => v / total) : raw;
|
|
693
|
+
return {
|
|
694
|
+
when: start,
|
|
695
|
+
duration: end - start,
|
|
696
|
+
profile,
|
|
697
|
+
bass,
|
|
698
|
+
empty: total === 0
|
|
699
|
+
};
|
|
700
|
+
};
|
|
701
|
+
var emissionScore = (frame, tmpl, key, ncTonePenalty) => {
|
|
702
|
+
let hit = 0;
|
|
703
|
+
let miss = 0;
|
|
704
|
+
for (let pc = 0; pc < 12; pc++) {
|
|
705
|
+
const w = frame.profile[pc];
|
|
706
|
+
if (w === 0) continue;
|
|
707
|
+
if (tmpl.pcs.has(pc)) hit += w * tmpl.weights[pc];
|
|
708
|
+
else miss += w;
|
|
709
|
+
}
|
|
710
|
+
let score = hit - ncTonePenalty * miss;
|
|
711
|
+
if (frame.profile[tmpl.root] === 0) score -= 0.3;
|
|
712
|
+
if (frame.bass !== -1 && tmpl.root === frame.bass) score += 0.3;
|
|
713
|
+
if (key) score += keyBonus(tmpl, key);
|
|
714
|
+
score -= tmpl.priority * 2e-3;
|
|
715
|
+
return score;
|
|
716
|
+
};
|
|
717
|
+
var ROMAN = ["I", "II", "III", "IV", "V", "VI", "VII"];
|
|
718
|
+
var chordDegree = (key, tmpl) => {
|
|
719
|
+
const scale = key.mode === "major" ? MAJOR_SCALE : NATURAL_MINOR_SCALE;
|
|
720
|
+
const rel = toPitchClass(tmpl.root - key.tonic);
|
|
721
|
+
let idx = scale.indexOf(rel);
|
|
722
|
+
let accidental = "";
|
|
723
|
+
if (idx === -1) {
|
|
724
|
+
const below = scale.indexOf(toPitchClass(rel - 1));
|
|
725
|
+
const above = scale.indexOf(toPitchClass(rel + 1));
|
|
726
|
+
if (below !== -1) {
|
|
727
|
+
idx = below;
|
|
728
|
+
accidental = "#";
|
|
729
|
+
} else if (above !== -1) {
|
|
730
|
+
idx = above;
|
|
731
|
+
accidental = "b";
|
|
732
|
+
} else {
|
|
733
|
+
idx = 0;
|
|
734
|
+
accidental = "?";
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
const hasM3 = tmpl.rel.has(4);
|
|
738
|
+
const hasm3 = tmpl.rel.has(3);
|
|
739
|
+
const hasDim5 = tmpl.rel.has(6);
|
|
740
|
+
const hasAug5 = tmpl.rel.has(8);
|
|
741
|
+
const hasMin7 = tmpl.rel.has(10);
|
|
742
|
+
let numeral = ROMAN[idx];
|
|
743
|
+
let suffix = "";
|
|
744
|
+
if (hasm3 && hasDim5) {
|
|
745
|
+
numeral = numeral.toLowerCase();
|
|
746
|
+
suffix = hasMin7 ? "\xF87" : "\xB0";
|
|
747
|
+
if (tmpl.rel.has(9)) suffix = "\xB07";
|
|
748
|
+
} else if (hasM3 && hasAug5) {
|
|
749
|
+
suffix = "+";
|
|
750
|
+
} else if (hasm3) {
|
|
751
|
+
numeral = numeral.toLowerCase();
|
|
752
|
+
} else if (!hasM3) {
|
|
753
|
+
}
|
|
754
|
+
if (!suffix) {
|
|
755
|
+
if (tmpl.rel.has(11)) suffix = "M7";
|
|
756
|
+
else if (hasMin7) suffix = "7";
|
|
757
|
+
else if (tmpl.rel.has(9) && !tmpl.rel.has(10)) suffix = "6";
|
|
758
|
+
}
|
|
759
|
+
return accidental + numeral + suffix;
|
|
760
|
+
};
|
|
761
|
+
var viterbi = (emissions, changePenalty) => {
|
|
762
|
+
const T = emissions.length;
|
|
763
|
+
const N = CHORD_TEMPLATES.length;
|
|
764
|
+
if (T === 0) return [];
|
|
765
|
+
const back = Array.from(
|
|
766
|
+
{ length: T },
|
|
767
|
+
() => new Array(N).fill(-1)
|
|
768
|
+
);
|
|
769
|
+
let prev = emissions[0].slice();
|
|
770
|
+
for (let t = 1; t < T; t++) {
|
|
771
|
+
let bestPrevVal = Number.NEGATIVE_INFINITY;
|
|
772
|
+
let bestPrevIdx = 0;
|
|
773
|
+
for (let j = 0; j < N; j++)
|
|
774
|
+
if (prev[j] > bestPrevVal) {
|
|
775
|
+
bestPrevVal = prev[j];
|
|
776
|
+
bestPrevIdx = j;
|
|
777
|
+
}
|
|
778
|
+
const curr = new Array(N).fill(0);
|
|
779
|
+
const em = emissions[t];
|
|
780
|
+
const switchVal = bestPrevVal - changePenalty;
|
|
781
|
+
for (let i = 0; i < N; i++) {
|
|
782
|
+
if (prev[i] >= switchVal) {
|
|
783
|
+
curr[i] = em[i] + prev[i];
|
|
784
|
+
back[t][i] = i;
|
|
785
|
+
} else {
|
|
786
|
+
curr[i] = em[i] + switchVal;
|
|
787
|
+
back[t][i] = bestPrevIdx;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
prev = curr;
|
|
791
|
+
}
|
|
792
|
+
let bestIdx = 0;
|
|
793
|
+
for (let i = 1; i < N; i++) if (prev[i] > prev[bestIdx]) bestIdx = i;
|
|
794
|
+
const path = new Array(T).fill(0);
|
|
795
|
+
path[T - 1] = bestIdx;
|
|
796
|
+
for (let t = T - 1; t > 0; t--) path[t - 1] = back[t][path[t]];
|
|
797
|
+
return path;
|
|
798
|
+
};
|
|
799
|
+
var keyAt = (keys, when) => {
|
|
800
|
+
for (const k of keys)
|
|
801
|
+
if (when >= k.when && when < k.when + k.duration) return k.key;
|
|
802
|
+
return keys.length ? keys[keys.length - 1].key : null;
|
|
803
|
+
};
|
|
804
|
+
var buildSymbol = (tmpl, bass, flat) => {
|
|
805
|
+
const rootSymbol = noteName(tmpl.root, flat) + tmpl.quality;
|
|
806
|
+
const inversion = bass !== -1 && bass !== tmpl.root && tmpl.pcs.has(bass);
|
|
807
|
+
return {
|
|
808
|
+
symbol: inversion ? `${rootSymbol}/${noteName(bass, flat)}` : rootSymbol,
|
|
809
|
+
rootSymbol,
|
|
810
|
+
inversion,
|
|
811
|
+
bass: bass === -1 ? tmpl.root : bass
|
|
812
|
+
};
|
|
813
|
+
};
|
|
814
|
+
var detectProgression = (notes, options = {}) => {
|
|
815
|
+
if (!notes.length) return { keys: [], chords: [] };
|
|
816
|
+
const {
|
|
817
|
+
flat = false,
|
|
818
|
+
bpm,
|
|
819
|
+
frameSize = 0.5,
|
|
820
|
+
changePenalty = 0.4,
|
|
821
|
+
nonChordTonePenalty = 0.55,
|
|
822
|
+
useKey = true
|
|
823
|
+
} = options;
|
|
824
|
+
const keys = detectKeyChanges(notes, options);
|
|
825
|
+
const start = notes.reduce(
|
|
826
|
+
(m, n) => Math.min(m, n.when),
|
|
827
|
+
Number.POSITIVE_INFINITY
|
|
828
|
+
);
|
|
829
|
+
const end = notes.reduce(
|
|
830
|
+
(m, n) => Math.max(m, n.when + Math.max(n.duration, 0)),
|
|
831
|
+
Number.NEGATIVE_INFINITY
|
|
832
|
+
);
|
|
833
|
+
if (end <= start) return { keys, chords: [] };
|
|
834
|
+
const frameDur = bpm ? 60 / bpm : Math.max(frameSize, 1e-3);
|
|
835
|
+
const frames = [];
|
|
836
|
+
for (let t = start; t < end - 1e-9; t += frameDur)
|
|
837
|
+
frames.push(makeFrame(notes, t, Math.min(t + frameDur, end)));
|
|
838
|
+
const emissions = frames.map((frame) => {
|
|
839
|
+
if (frame.empty) return new Array(CHORD_TEMPLATES.length).fill(0);
|
|
840
|
+
const key = useKey ? keyAt(keys, frame.when + frame.duration / 2) : null;
|
|
841
|
+
return CHORD_TEMPLATES.map(
|
|
842
|
+
(tmpl) => emissionScore(frame, tmpl, key, nonChordTonePenalty)
|
|
843
|
+
);
|
|
844
|
+
});
|
|
845
|
+
const path = viterbi(emissions, changePenalty);
|
|
846
|
+
const chords = [];
|
|
847
|
+
for (let t = 0; t < frames.length; t++) {
|
|
848
|
+
const frame = frames[t];
|
|
849
|
+
const tmpl = CHORD_TEMPLATES[path[t]];
|
|
850
|
+
const last = chords[chords.length - 1];
|
|
851
|
+
const sameAsLast = last && last.root === tmpl.root && last.quality === tmpl.quality;
|
|
852
|
+
if (sameAsLast) {
|
|
853
|
+
last.duration = frame.when + frame.duration - last.when;
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
const key = keyAt(keys, frame.when + frame.duration / 2);
|
|
857
|
+
const { symbol, rootSymbol, inversion, bass } = buildSymbol(
|
|
858
|
+
tmpl,
|
|
859
|
+
frame.bass,
|
|
860
|
+
flat
|
|
861
|
+
);
|
|
862
|
+
chords.push({
|
|
863
|
+
symbol,
|
|
864
|
+
rootSymbol,
|
|
865
|
+
root: tmpl.root,
|
|
866
|
+
quality: tmpl.quality,
|
|
867
|
+
bass,
|
|
868
|
+
inversion,
|
|
869
|
+
when: frame.when,
|
|
870
|
+
duration: frame.duration,
|
|
871
|
+
key,
|
|
872
|
+
degree: key ? chordDegree(key, tmpl) : null
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
return { keys, chords };
|
|
876
|
+
};
|
|
522
877
|
var toHan = (str) => str.replace(/[!-~]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) - 65248)).replace(/ /g, " ");
|
|
523
878
|
var parseChords = (str, bpm = 120) => {
|
|
524
879
|
const output = [];
|
|
@@ -1434,6 +1789,7 @@ var DEFAULT_PAN = 64;
|
|
|
1434
1789
|
var DEFAULT_VELOCITY = 100;
|
|
1435
1790
|
var DEFAULT_PLAYBACK_VELOCITY = 127;
|
|
1436
1791
|
var DEFAULT_STEPS_PER_BAR = 192;
|
|
1792
|
+
var MML_END_MARKER = "#end;";
|
|
1437
1793
|
|
|
1438
1794
|
// src/lyrics.ts
|
|
1439
1795
|
var kanaTable = {
|
|
@@ -3566,7 +3922,9 @@ var parseMML = (mml, options = {}) => {
|
|
|
3566
3922
|
const meta = parseMmlMeta(noComments);
|
|
3567
3923
|
const noMeta = stripMmlMeta(noComments);
|
|
3568
3924
|
const lyrics = collectLyrics ? parseLyrics(noMeta) : void 0;
|
|
3569
|
-
const
|
|
3925
|
+
const endMarkerBase = MML_END_MARKER.replace(/;+$/, "");
|
|
3926
|
+
const endRegex = new RegExp(`(?<![cdafgCDAFG])${endMarkerBase}\\b;?`, "gi");
|
|
3927
|
+
const fullMML = stripLyrics(noMeta).replace(endRegex, "").replace(/[\n\r]+/g, " ").trim();
|
|
3570
3928
|
const parts = fullMML.split(/(@\d+)/).filter((p) => p.trim().length > 0);
|
|
3571
3929
|
let trackIndex = 0;
|
|
3572
3930
|
let octave = 4;
|
|
@@ -5869,8 +6227,8 @@ var mountDAW = (target, options = {}) => {
|
|
|
5869
6227
|
const decomposedMini = monoTracks.map(
|
|
5870
6228
|
(notes, i) => `@${i}${refCore.getMMLFromNotes(notes, bpm, 100).trim().replace(/\s+/g, "")}`
|
|
5871
6229
|
);
|
|
5872
|
-
const full2 = [metaLine, ...decomposedFull].filter((s) => s.length > 0).join(";\n");
|
|
5873
|
-
const minified2 = [metaLine, ...decomposedMini].filter((s) => s.length > 0).join(";");
|
|
6230
|
+
const full2 = [metaLine, ...decomposedFull, MML_END_MARKER].filter((s) => s.length > 0).join(";\n");
|
|
6231
|
+
const minified2 = [metaLine, ...decomposedMini, MML_END_MARKER].filter((s) => s.length > 0).join(";");
|
|
5874
6232
|
return {
|
|
5875
6233
|
full: full2,
|
|
5876
6234
|
minified: minified2,
|
|
@@ -5910,8 +6268,13 @@ var mountDAW = (target, options = {}) => {
|
|
|
5910
6268
|
const head = params ? `${x.model} ${params}` : x.model;
|
|
5911
6269
|
return `@@${x.i} ${head} ${x.text}`;
|
|
5912
6270
|
});
|
|
5913
|
-
const full = [metaLine, ...trackLines, ...lyricLines].filter((s) => s.length > 0).join(";\n");
|
|
5914
|
-
const minified = [
|
|
6271
|
+
const full = [metaLine, ...trackLines, ...lyricLines, MML_END_MARKER].filter((s) => s.length > 0).join(";\n");
|
|
6272
|
+
const minified = [
|
|
6273
|
+
metaLine,
|
|
6274
|
+
...trackLinesMini,
|
|
6275
|
+
...lyricLines,
|
|
6276
|
+
MML_END_MARKER
|
|
6277
|
+
].filter((s) => s.length > 0).join(";");
|
|
5915
6278
|
return {
|
|
5916
6279
|
full,
|
|
5917
6280
|
minified,
|
|
@@ -6557,116 +6920,35 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6557
6920
|
const trackIndices = [...new Set(placements.map((p) => p.trackIndex))].sort(
|
|
6558
6921
|
(a, b) => a - b
|
|
6559
6922
|
);
|
|
6560
|
-
const trackStats = trackIndices.map((index) => {
|
|
6561
|
-
const trackPlacements = placements.filter((p) => p.trackIndex === index);
|
|
6562
|
-
if (trackPlacements.length === 0) {
|
|
6563
|
-
return { index, isChords: false, avgPitch: 0, noteCount: 0 };
|
|
6564
|
-
}
|
|
6565
|
-
const sumPitch = trackPlacements.reduce((sum, p) => sum + p.pitch, 0);
|
|
6566
|
-
const avgPitch = sumPitch / trackPlacements.length;
|
|
6567
|
-
const stepCounts = /* @__PURE__ */ new Map();
|
|
6568
|
-
for (const p of trackPlacements) {
|
|
6569
|
-
for (let s = p.startStep; s < p.startStep + p.durationSteps; s++) {
|
|
6570
|
-
stepCounts.set(s, (stepCounts.get(s) ?? 0) + 1);
|
|
6571
|
-
}
|
|
6572
|
-
}
|
|
6573
|
-
const polyphonicSteps = Array.from(stepCounts.values()).filter(
|
|
6574
|
-
(c) => c >= 2
|
|
6575
|
-
).length;
|
|
6576
|
-
const isChords = polyphonicSteps > 0;
|
|
6577
|
-
return {
|
|
6578
|
-
index,
|
|
6579
|
-
isChords,
|
|
6580
|
-
avgPitch,
|
|
6581
|
-
noteCount: trackPlacements.length
|
|
6582
|
-
};
|
|
6583
|
-
});
|
|
6584
|
-
const chordTrackIndices = trackStats.filter((s) => s.isChords).map((s) => s.index);
|
|
6585
|
-
const nonChordTracks = trackStats.filter(
|
|
6586
|
-
(s) => !s.isChords && s.noteCount > 0
|
|
6587
|
-
);
|
|
6588
|
-
let bassTrackIndex = null;
|
|
6589
|
-
if (nonChordTracks.length > 0) {
|
|
6590
|
-
nonChordTracks.sort((a, b) => a.avgPitch - b.avgPitch);
|
|
6591
|
-
bassTrackIndex = nonChordTracks[0].index;
|
|
6592
|
-
}
|
|
6593
|
-
const priorityTrackIndices = /* @__PURE__ */ new Set();
|
|
6594
|
-
for (const idx of chordTrackIndices) {
|
|
6595
|
-
priorityTrackIndices.add(idx);
|
|
6596
|
-
}
|
|
6597
|
-
if (bassTrackIndex !== null) {
|
|
6598
|
-
priorityTrackIndices.add(bassTrackIndex);
|
|
6599
|
-
}
|
|
6600
|
-
const hasPriorityTracksInMml = priorityTrackIndices.size > 0;
|
|
6601
6923
|
const maxStep = placements.reduce(
|
|
6602
6924
|
(max, p) => Math.max(max, p.startStep + p.durationSteps),
|
|
6603
6925
|
0
|
|
6604
6926
|
);
|
|
6605
|
-
const
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|
|
6610
|
-
{ length: maxStep + 1 },
|
|
6611
|
-
() => /* @__PURE__ */ new Set()
|
|
6612
|
-
);
|
|
6613
|
-
for (const p of placements) {
|
|
6614
|
-
for (let s = p.startStep; s < p.startStep + p.durationSteps; s++) {
|
|
6615
|
-
if (s >= 0 && s <= maxStep) {
|
|
6616
|
-
allPitches[s].add(p.pitch);
|
|
6617
|
-
if (priorityTrackIndices.has(p.trackIndex)) {
|
|
6618
|
-
priorityPitches[s].add(p.pitch);
|
|
6619
|
-
}
|
|
6620
|
-
}
|
|
6621
|
-
}
|
|
6622
|
-
}
|
|
6927
|
+
const timedNotes = placements.map((p) => ({
|
|
6928
|
+
pitch: p.pitch,
|
|
6929
|
+
when: p.startStep * secondsPerStep,
|
|
6930
|
+
duration: p.durationSteps * secondsPerStep
|
|
6931
|
+
}));
|
|
6623
6932
|
const stepChords = [];
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
hasPriorityStarted = true;
|
|
6637
|
-
}
|
|
6638
|
-
}
|
|
6639
|
-
const pitchDurations = /* @__PURE__ */ new Map();
|
|
6640
|
-
for (let s = startS; s <= endS; s++) {
|
|
6641
|
-
const usePriority = hasPriorityStarted && hasPriorityTracksInMml;
|
|
6642
|
-
const pitches = usePriority ? Array.from(priorityPitches[s]) : Array.from(allPitches[s]);
|
|
6643
|
-
for (const p of pitches) {
|
|
6644
|
-
pitchDurations.set(p, (pitchDurations.get(p) ?? 0) + 1);
|
|
6645
|
-
}
|
|
6646
|
-
}
|
|
6647
|
-
let activePitches = Array.from(pitchDurations.entries()).filter(([_, dur]) => dur >= MIN_DURATION).map(([p, _]) => p);
|
|
6648
|
-
if (activePitches.length === 0 && pitchDurations.size > 0) {
|
|
6649
|
-
const maxDur = Math.max(...pitchDurations.values());
|
|
6650
|
-
activePitches = Array.from(pitchDurations.entries()).filter(([_, dur]) => dur === maxDur).map(([p, _]) => p);
|
|
6651
|
-
}
|
|
6652
|
-
let gridChord = lastChord;
|
|
6653
|
-
if (activePitches.length > 0) {
|
|
6654
|
-
const sortedPitches = activePitches.sort((a, b) => a - b);
|
|
6655
|
-
const cacheKey = sortedPitches.join(",");
|
|
6656
|
-
if (chordCache.has(cacheKey)) {
|
|
6657
|
-
const chordName = chordCache.get(cacheKey);
|
|
6658
|
-
if (chordName) gridChord = chordName;
|
|
6659
|
-
} else {
|
|
6660
|
-
const candidates = detectChord(sortedPitches);
|
|
6661
|
-
const chordName = candidates[0]?.symbol ?? "";
|
|
6662
|
-
if (chordName) gridChord = chordName;
|
|
6663
|
-
chordCache.set(cacheKey, chordName);
|
|
6933
|
+
if (timedNotes.length > 0) {
|
|
6934
|
+
let chordSegments = [];
|
|
6935
|
+
try {
|
|
6936
|
+
chordSegments = detectProgression(timedNotes, { bpm }).chords;
|
|
6937
|
+
} catch {
|
|
6938
|
+
chordSegments = [];
|
|
6939
|
+
}
|
|
6940
|
+
for (const seg of chordSegments) {
|
|
6941
|
+
const startStep = Math.max(0, Math.round(seg.when / secondsPerStep));
|
|
6942
|
+
const endStep = Math.round((seg.when + seg.duration) / secondsPerStep);
|
|
6943
|
+
for (let s = startStep; s < endStep && s <= maxStep; s++) {
|
|
6944
|
+
stepChords[s] = seg.symbol;
|
|
6664
6945
|
}
|
|
6665
6946
|
}
|
|
6666
|
-
|
|
6667
|
-
|
|
6947
|
+
let lastChord = "";
|
|
6948
|
+
for (let s = 0; s <= maxStep; s++) {
|
|
6949
|
+
if (stepChords[s]) lastChord = stepChords[s];
|
|
6950
|
+
else stepChords[s] = lastChord;
|
|
6668
6951
|
}
|
|
6669
|
-
lastChord = gridChord;
|
|
6670
6952
|
}
|
|
6671
6953
|
const seqTracks = trackIndices.map((index) => {
|
|
6672
6954
|
let id = 0;
|
|
@@ -6839,7 +7121,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6839
7121
|
em.textContent = "\u{1F97A}";
|
|
6840
7122
|
scheduleBlink(em);
|
|
6841
7123
|
},
|
|
6842
|
-
|
|
7124
|
+
100 + Math.random() * 50
|
|
6843
7125
|
);
|
|
6844
7126
|
blinkTimers.push(t2);
|
|
6845
7127
|
}, delay);
|
|
@@ -7810,6 +8092,7 @@ export {
|
|
|
7810
8092
|
LinkedList,
|
|
7811
8093
|
MAX_VOCAL_VOLUME,
|
|
7812
8094
|
MMLCore,
|
|
8095
|
+
MML_END_MARKER,
|
|
7813
8096
|
PITCH_MAP,
|
|
7814
8097
|
PREWARM_NOTES,
|
|
7815
8098
|
TRACKS_ADVANCED,
|
package/package.json
CHANGED