@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.mjs CHANGED
@@ -432,60 +432,190 @@ var QUALITY_BY_PCSET = (() => {
432
432
  }
433
433
  return map;
434
434
  })();
435
- var detectChord = (notes, options = {}) => {
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 pcs = [...new Set(notes.map(toPitchClass))].sort((a, b) => a - b);
439
- const bass = toPitchClass(
440
- options.bass ?? notes.reduce((m, v) => Math.min(m, v), notes[0])
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 scored = [];
443
- for (const root of pcs) {
444
- const relSet = new Set(pcs.map((pc) => toPitchClass(pc - root)));
445
- for (const def of QUALITY_BY_PCSET.values()) {
446
- let matchCount = 0;
447
- let hasRoot = false;
448
- for (const pc of def.pitchClasses) {
449
- if (relSet.has(pc)) {
450
- matchCount++;
451
- if (pc === 0) hasRoot = true;
452
- }
453
- }
454
- if (!hasRoot) continue;
455
- if (matchCount < Math.min(2, def.pitchClasses.length)) continue;
456
- const missingCount = def.pitchClasses.length - matchCount;
457
- let extraCount = 0;
458
- const defSet = new Set(def.pitchClasses);
459
- for (const pc of relSet) {
460
- if (!defSet.has(pc)) {
461
- extraCount++;
462
- }
463
- }
464
- const score = matchCount * 2 - missingCount * 1 - extraCount * 0.5;
465
- const rootSymbol = noteName(root, flat) + def.quality;
466
- const inversion = root !== bass;
467
- scored.push({
468
- score,
469
- priority: def.priority,
470
- candidate: {
471
- symbol: inversion ? `${rootSymbol}/${noteName(bass, flat)}` : rootSymbol,
472
- rootSymbol,
473
- root,
474
- quality: def.quality,
475
- bass,
476
- inversion
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
- scored.sort((a, b) => {
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 fullMML = stripLyrics(noMeta).replace(/[\n\r]+/g, " ").trim();
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 = [metaLine, ...trackLinesMini, ...lyricLines].filter((s) => s.length > 0).join(";");
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 priorityPitches = Array.from(
6606
- { length: maxStep + 1 },
6607
- () => /* @__PURE__ */ new Set()
6608
- );
6609
- const allPitches = Array.from(
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
- const chordCache = /* @__PURE__ */ new Map();
6625
- let lastChord = "";
6626
- let hasPriorityStarted = false;
6627
- const GRID_SIZE = 48;
6628
- const MIN_DURATION = 12;
6629
- for (let g = 0; g <= Math.ceil(maxStep / GRID_SIZE); g++) {
6630
- const startS = g * GRID_SIZE;
6631
- const endS = Math.min(maxStep, (g + 1) * GRID_SIZE - 1);
6632
- if (startS > maxStep) break;
6633
- for (let s = startS; s <= endS; s++) {
6634
- const priorityPitchesAtStep = Array.from(priorityPitches[s]);
6635
- if (priorityPitchesAtStep.length > 0) {
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
- for (let s = startS; s <= endS; s++) {
6667
- stepChords[s] = gridChord;
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
- 200 + Math.random() * 150
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
@@ -2,7 +2,7 @@
2
2
  "author": "onjmin",
3
3
  "license": "MIT",
4
4
  "name": "@onjmin/dtm",
5
- "version": "0.1.9",
5
+ "version": "0.1.11",
6
6
  "description": "MMLを中間言語に用いた、モバイルファーストなDAW / ピアノロール打ち込みコンポーネント",
7
7
  "homepage": "https://onjmin.github.io/dtm",
8
8
  "repository": {