@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.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 detectChord = (notes, options = {}) => {
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 pcs = [...new Set(notes.map(toPitchClass))].sort((a, b) => a - b);
543
- const bass = toPitchClass(
544
- options.bass ?? notes.reduce((m, v) => Math.min(m, v), notes[0])
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 scored = [];
547
- for (const root of pcs) {
548
- const relSet = new Set(pcs.map((pc) => toPitchClass(pc - root)));
549
- for (const def of QUALITY_BY_PCSET.values()) {
550
- let matchCount = 0;
551
- let hasRoot = false;
552
- for (const pc of def.pitchClasses) {
553
- if (relSet.has(pc)) {
554
- matchCount++;
555
- if (pc === 0) hasRoot = true;
556
- }
557
- }
558
- if (!hasRoot) continue;
559
- if (matchCount < Math.min(2, def.pitchClasses.length)) continue;
560
- const missingCount = def.pitchClasses.length - matchCount;
561
- let extraCount = 0;
562
- const defSet = new Set(def.pitchClasses);
563
- for (const pc of relSet) {
564
- if (!defSet.has(pc)) {
565
- extraCount++;
566
- }
567
- }
568
- const score = matchCount * 2 - missingCount * 1 - extraCount * 0.5;
569
- const rootSymbol = noteName(root, flat) + def.quality;
570
- const inversion = root !== bass;
571
- scored.push({
572
- score,
573
- priority: def.priority,
574
- candidate: {
575
- symbol: inversion ? `${rootSymbol}/${noteName(bass, flat)}` : rootSymbol,
576
- rootSymbol,
577
- root,
578
- quality: def.quality,
579
- bass,
580
- inversion
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
- scored.sort((a, b) => {
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 fullMML = stripLyrics(noMeta).replace(/[\n\r]+/g, " ").trim();
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 = [metaLine, ...trackLinesMini, ...lyricLines].filter((s) => s.length > 0).join(";");
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 priorityPitches = Array.from(
6710
- { length: maxStep + 1 },
6711
- () => /* @__PURE__ */ new Set()
6712
- );
6713
- const allPitches = Array.from(
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
- const chordCache = /* @__PURE__ */ new Map();
6729
- let lastChord = "";
6730
- let hasPriorityStarted = false;
6731
- const GRID_SIZE = 48;
6732
- const MIN_DURATION = 12;
6733
- for (let g = 0; g <= Math.ceil(maxStep / GRID_SIZE); g++) {
6734
- const startS = g * GRID_SIZE;
6735
- const endS = Math.min(maxStep, (g + 1) * GRID_SIZE - 1);
6736
- if (startS > maxStep) break;
6737
- for (let s = startS; s <= endS; s++) {
6738
- const priorityPitchesAtStep = Array.from(priorityPitches[s]);
6739
- if (priorityPitchesAtStep.length > 0) {
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
- for (let s = startS; s <= endS; s++) {
6771
- stepChords[s] = gridChord;
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
- 200 + Math.random() * 150
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,