@onjmin/dtm 0.1.49 → 0.1.53

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
@@ -41,6 +41,7 @@ __export(index_exports, {
41
41
  MAX_VOCAL_VOLUME: () => MAX_VOCAL_VOLUME,
42
42
  MMLCore: () => MMLCore,
43
43
  MML_END_MARKER: () => MML_END_MARKER,
44
+ MidiSearchClient: () => MidiSearchClient,
44
45
  PITCH_MAP: () => PITCH_MAP,
45
46
  PREWARM_NOTES: () => PREWARM_NOTES,
46
47
  TRACKS_ADVANCED: () => TRACKS_ADVANCED,
@@ -90,6 +91,7 @@ __export(index_exports, {
90
91
  init: () => init,
91
92
  injectStyles: () => injectStyles,
92
93
  isChordHeavyTrack: () => isChordHeavyTrack,
94
+ isPlausibleMidiTranscription: () => isPlausibleMidiTranscription,
93
95
  isValidHttpUrl: () => isValidHttpUrl,
94
96
  koeUrl: () => koeUrl,
95
97
  mountDAW: () => mountDAW,
@@ -103,6 +105,7 @@ __export(index_exports, {
103
105
  parseMmlMeta: () => parseMmlMeta,
104
106
  playMML: () => playMML,
105
107
  playSingingMML: () => playSingingMML,
108
+ setBackgroundActive: () => setBackgroundActive,
106
109
  setDrawOffset: () => setDrawOffset,
107
110
  shiftNotes: () => shiftNotes,
108
111
  showLoadingOverlay: () => showLoadingOverlay,
@@ -1330,7 +1333,13 @@ var icon = (name, size = 20) => {
1330
1333
  // src/daw-ui.ts
1331
1334
  var q = (root, sel) => root.querySelector(sel);
1332
1335
  var buildUI = (target, options) => {
1333
- const { drumPatternNames, defaultDrumPattern, defaultBpm, showMidi } = options;
1336
+ const {
1337
+ drumPatternNames,
1338
+ defaultDrumPattern,
1339
+ defaultBpm,
1340
+ showMidi,
1341
+ showMidiSearch
1342
+ } = options;
1334
1343
  const drumOptions = [`<option value="none">\u306A\u3057</option>`].concat(
1335
1344
  drumPatternNames.map(
1336
1345
  (name) => `<option value="${name}" ${name === defaultDrumPattern ? "selected" : ""}>${name}</option>`
@@ -1408,6 +1417,12 @@ var buildUI = (target, options) => {
1408
1417
  <span class="dtm-label" data-dtm="zoomy-label">100%</span>
1409
1418
  <button class="dtm-iconbtn" data-dtm="zoomy-in" title="\u62E1\u5927">\uFF0B</button>
1410
1419
  </div>
1420
+ <div class="dtm-row">
1421
+ <span class="dtm-label">\u80CC\u666F\u753B\u50CF</span>
1422
+ <input type="file" accept="image/*" data-dtm="bg-file-input" class="dtm-hidden">
1423
+ <button class="dtm-btn dtm-btn--primary" data-dtm="bg-upload">\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9</button>
1424
+ <button class="dtm-btn dtm-btn--danger dtm-hidden" data-dtm="bg-remove">\u524A\u9664</button>
1425
+ </div>
1411
1426
  </div>
1412
1427
  </details>
1413
1428
 
@@ -1446,6 +1461,9 @@ var buildUI = (target, options) => {
1446
1461
  <textarea class="dtm-textarea dtm-grow" data-dtm="mml-input" placeholder="MML\u3092\u5165\u529B"></textarea>
1447
1462
  <button class="dtm-btn dtm-btn--primary" data-dtm="mml-load" style="flex-shrink:0">\u8AAD\u8FBC</button>
1448
1463
  </div>
1464
+ <div class="dtm-row" data-dtm="midi-search-row" style="justify-content:flex-end;${showMidiSearch ? "" : "display:none"}">
1465
+ <button class="dtm-btn dtm-btn--primary" data-dtm="midi-search-open">MML\u691C\u7D22</button>
1466
+ </div>
1449
1467
  <p class="dtm-load-note dtm-hidden" data-dtm="mml-load-note"></p>
1450
1468
  </div>
1451
1469
  </details>
@@ -1556,6 +1574,9 @@ var buildUI = (target, options) => {
1556
1574
  zoomXOut: sel("zoomx-out"),
1557
1575
  zoomYIn: sel("zoomy-in"),
1558
1576
  zoomYOut: sel("zoomy-out"),
1577
+ bgFileInput: sel("bg-file-input"),
1578
+ bgUploadBtn: sel("bg-upload"),
1579
+ bgRemoveBtn: sel("bg-remove"),
1559
1580
  rollContainer: sel("roll"),
1560
1581
  wrapper: sel("wrapper"),
1561
1582
  vScroll: sel("vscroll"),
@@ -1574,6 +1595,7 @@ var buildUI = (target, options) => {
1574
1595
  midiInfoBtn: sel("midi-info"),
1575
1596
  midiTrackSelection: sel("midi-track-selection"),
1576
1597
  midiPanel: sel("midi-panel"),
1598
+ midiSearchOpenBtn: sel("midi-search-open"),
1577
1599
  mmlInput: sel("mml-input"),
1578
1600
  mmlLoadBtn: sel("mml-load"),
1579
1601
  mmlLoadNote: sel("mml-load-note"),
@@ -2618,8 +2640,9 @@ var KOE_VOICEBANKS = {
2618
2640
  teto: "\u91CD\u97F3\u30C6\u30C8\u5358\u72EC\u97F3.koe",
2619
2641
  shiyo: "\u9769\u547D\u30B7\u30E8.koe",
2620
2642
  rei: "\u8DB3\u7ACB\u30EC\u30A4ver3.5.0.koe",
2621
- MGRoid: "MGRoid_\u539F\u97F3\u8A2D\u5B9A\u6E08\u307F.koe",
2622
- MOTRoid: "MOTRoid\u5B8C\u5168\u7248V2.koe"
2643
+ mgroid: "MGRoid_\u539F\u97F3\u8A2D\u5B9A\u6E08\u307F.koe",
2644
+ motroid: "MOTRoid\u5B8C\u5168\u7248V2.koe",
2645
+ nynroid: "NYNRoidver1.4.koe"
2623
2646
  };
2624
2647
  var KOE_VOICEBANK_LABELS = {
2625
2648
  tsukuyomi: "\u3064\u304F\u3088\u307F\u3061\u3083\u3093",
@@ -2630,8 +2653,9 @@ var KOE_VOICEBANK_LABELS = {
2630
2653
  teto: "\u91CD\u97F3\u30C6\u30C8",
2631
2654
  shiyo: "\u9769\u547D\u30B7\u30E8",
2632
2655
  rei: "\u8DB3\u7ACB\u30EC\u30A4",
2633
- MGRoid: "MGRoid",
2634
- MOTRoid: "MOTRoid"
2656
+ mgroid: "MGRoid",
2657
+ motroid: "MOTRoid",
2658
+ nynroid: "NYNRoid"
2635
2659
  };
2636
2660
  var VOICE_IMAGE_KEY = {
2637
2661
  klatt: "puyuyu",
@@ -2643,8 +2667,9 @@ var VOICE_IMAGE_KEY = {
2643
2667
  teto: "teto",
2644
2668
  shiyo: "shiyo",
2645
2669
  rei: "rei",
2646
- MGRoid: "MGRoid",
2647
- MOTRoid: "MOTRoid"
2670
+ mgroid: "MGRoid",
2671
+ motroid: "MOTRoid",
2672
+ nynroid: "NYNRoid"
2648
2673
  };
2649
2674
  var KOE_VOICEBANK_TERMS = {
2650
2675
  tsukuyomi: "https://tyc.rei-yumesaki.net/material/utau/terms/",
@@ -2655,8 +2680,9 @@ var KOE_VOICEBANK_TERMS = {
2655
2680
  teto: "https://kasaneteto.jp/guidelines/voice.html",
2656
2681
  shiyo: "https://kakumeisiyo.my.canva.site/dagkuyjwycs",
2657
2682
  rei: "https://mechanicalgirl.jp/guidelines/",
2658
- MGRoid: "https://x.com/nisusansu/status/1048825378188353536",
2659
- MOTRoid: "https://www.nicovideo.jp/watch/sm40031282"
2683
+ mgroid: "https://x.com/nisusansu/status/1048825378188353536",
2684
+ motroid: "https://www.nicovideo.jp/watch/sm40031282",
2685
+ nynroid: "https://www.bilibili.com/video/BV1V24y1a7qs"
2660
2686
  };
2661
2687
  var koeUrl = (name, base = KOE_BASE_URL) => `${base}/${encodeURIComponent(name)}`;
2662
2688
  var DEFAULT_WORLDLINE_SCRIPT = "https://onjmin.github.io/koe/demo/world/worldline.js";
@@ -3480,6 +3506,19 @@ var extractMidiPlacements = (midi, selectedTrackIndices) => {
3480
3506
  }
3481
3507
  return { placements, bpm };
3482
3508
  };
3509
+ var isPlausibleMidiTranscription = (midi, selectedIndices) => {
3510
+ const { placements } = extractMidiPlacements(midi, selectedIndices);
3511
+ if (placements.length < 4) return false;
3512
+ const shortCount = placements.filter((p) => p.durationSteps <= 1).length;
3513
+ const shortRatio = shortCount / placements.length;
3514
+ const countByStep = /* @__PURE__ */ new Map();
3515
+ for (const p of placements) {
3516
+ countByStep.set(p.startStep, (countByStep.get(p.startStep) ?? 0) + 1);
3517
+ }
3518
+ const denseSteps = [...countByStep.values()].filter((n) => n >= 6).length;
3519
+ const denseRatio = denseSteps / countByStep.size;
3520
+ return shortRatio < 0.6 && denseRatio < 0.35;
3521
+ };
3483
3522
  var extractMidiPlacementsByTrack = (midi, selectedIndices, trackIds) => {
3484
3523
  const { tracks, division } = midi;
3485
3524
  const ticksPerBeat = division;
@@ -3632,6 +3671,45 @@ var exportMIDI = (options) => {
3632
3671
  return new Blob([new Uint8Array(arr).buffer], { type: "audio/midi" });
3633
3672
  };
3634
3673
 
3674
+ // src/midi-search.ts
3675
+ var MidiSearchClient = class {
3676
+ config;
3677
+ constructor(config = {}) {
3678
+ this.config = config;
3679
+ }
3680
+ get enabled() {
3681
+ return !!this.config.apiKey;
3682
+ }
3683
+ get baseUrl() {
3684
+ return this.config.baseUrl ?? "https://rpgen-search.pages.dev/api";
3685
+ }
3686
+ headers() {
3687
+ const h = {};
3688
+ if (this.config.apiKey) {
3689
+ h.Authorization = `Bearer ${this.config.apiKey}`;
3690
+ }
3691
+ return h;
3692
+ }
3693
+ async searchSongs(params) {
3694
+ if (!this.enabled) return [];
3695
+ const qs = new URLSearchParams();
3696
+ if (params.title) qs.set("title", params.title);
3697
+ if (params.user) qs.set("user", params.user);
3698
+ if (params.twitter_id) qs.set("twitter_id", params.twitter_id);
3699
+ const url2 = `${this.baseUrl}/picotune/songs${qs.toString() ? `?${qs.toString()}` : ""}`;
3700
+ const res = await fetch(url2, { headers: this.headers() });
3701
+ if (!res.ok) throw new Error(`picotune search failed: ${res.status}`);
3702
+ const body = await res.json();
3703
+ return body.data ?? body.songs ?? body;
3704
+ }
3705
+ async fetchMidi(fileName) {
3706
+ const url2 = `${this.baseUrl}/picotune/songs/${encodeURIComponent(fileName)}`;
3707
+ const res = await fetch(url2, { headers: this.headers() });
3708
+ if (!res.ok) throw new Error(`picotune fetch failed: ${res.status}`);
3709
+ return res.arrayBuffer();
3710
+ }
3711
+ };
3712
+
3635
3713
  // src/linked-list.ts
3636
3714
  var LinkedList = class {
3637
3715
  #cursor;
@@ -3697,6 +3775,10 @@ var HEADER_HEIGHT = 20;
3697
3775
  var getRenderConfig = () => g_config;
3698
3776
  var g_draw_offset_x = 0;
3699
3777
  var g_draw_offset_y = 0;
3778
+ var g_bg_active = false;
3779
+ var setBackgroundActive = (active) => {
3780
+ g_bg_active = active;
3781
+ };
3700
3782
  var getDrawOffset = () => ({
3701
3783
  x: g_draw_offset_x,
3702
3784
  y: g_draw_offset_y
@@ -3846,7 +3928,7 @@ var drawHeader = () => {
3846
3928
  const { stepWidth, stepsPerBar } = g_config;
3847
3929
  g_header_ctx.save();
3848
3930
  g_header_ctx.translate(-g_draw_offset_x, 0);
3849
- g_header_ctx.fillStyle = "#0a0f1f";
3931
+ g_header_ctx.fillStyle = g_bg_active ? "rgba(10,15,31,0.55)" : "#0a0f1f";
3850
3932
  g_header_ctx.fillRect(
3851
3933
  g_draw_offset_x,
3852
3934
  0,
@@ -3892,7 +3974,7 @@ var drawGrid = (noteLengthSteps = 1) => {
3892
3974
  const octave = Math.floor(totalPitch / 12) - 1;
3893
3975
  const isC4Range = octave === 4;
3894
3976
  const screenY = y - g_draw_offset_y;
3895
- g_grid_ctx.fillStyle = isBlackKey ? "#080b16" : "#111628";
3977
+ g_grid_ctx.fillStyle = g_bg_active ? isBlackKey ? "rgba(8,11,22,0.55)" : "rgba(17,22,40,0.45)" : isBlackKey ? "#080b16" : "#111628";
3896
3978
  g_grid_ctx.fillRect(0, screenY, g_grid_canvas.width, keyHeight);
3897
3979
  if (isC4Range) {
3898
3980
  g_grid_ctx.fillStyle = "rgba(41,173,255,0.05)";
@@ -5464,6 +5546,18 @@ var DAW_CSS = `
5464
5546
  4px 4px 0 var(--c-black);
5465
5547
  overflow: hidden;
5466
5548
  }
5549
+ /* \u30AB\u30B9\u30BF\u30E0\u80CC\u666F\u753B\u50CF\u30EC\u30A4\u30E4\u30FC\uFF08\u753B\u50CF\u81EA\u4F53\u3092\u534A\u900F\u660E\u5316\u3057\u3001\u30AD\u30E3\u30F3\u30D0\u30B9\u5074\u306E\u5857\u308A\u306F\u7DAD\u6301\uFF09 */
5550
+ .dtm-roll::before {
5551
+ content: '';
5552
+ position: absolute;
5553
+ inset: 0;
5554
+ background-image: var(--dtm-roll-bg-image, none);
5555
+ background-size: cover;
5556
+ background-position: center;
5557
+ background-repeat: no-repeat;
5558
+ opacity: 0.4;
5559
+ pointer-events: none;
5560
+ }
5467
5561
  .dtm-vscroll {
5468
5562
  position: relative;
5469
5563
  width: 20px;
@@ -5836,9 +5930,14 @@ var DAW_CSS = `
5836
5930
  .dtm-modal-body {
5837
5931
  padding: 12px;
5838
5932
  overflow-y: auto;
5933
+ overflow-x: hidden;
5839
5934
  font-size: 13px;
5840
5935
  line-height: 1.6;
5841
5936
  }
5937
+ .dtm-modal-body * {
5938
+ max-width: 100%;
5939
+ box-sizing: border-box;
5940
+ }
5842
5941
  .dtm-modal-body a {
5843
5942
  color: var(--dtm-primary);
5844
5943
  text-decoration: underline;
@@ -6441,6 +6540,9 @@ var MGRoid_default = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYA
6441
6540
  // assets/MOTRoid.png
6442
6541
  var MOTRoid_default = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAByElEQVR42s2WTyiEURTFfyMbS1ZSlIUxScpCNrNAkpLdhClFlJRJdpQ/5V+xE7PQFJHFyJKUJKLZaBZqkhjKQslCLG3UWIw7ec/7xidq3lnNve++23fOO2/u86ChPbyd+hp3tDbzH9g5OFLivVCXByCPHMPjlrkw+KsiuhI5VyBfT/z3mev9JJZ1ezywdfeccnNmbhVyuy/3Coj7nVwv2KwpAaAn8ZiVkezT652UsO8WCK5jJ0pcH9OYOrha9un1GVivwMzEOADJ6CoApQtx48bdQNnnr3cAugMNAMRWpoz1/usbALzBQQB8/kZLFZAv0/EwVpdVESfGMV+lsb94xz4FMu4f6lTybpkLdObf+lurwNTcvOLWh6XpXzX0n54Z89JP+ts/CwRtj1cAFHq9Sj4+uWisr5sdVeLXZBKA/ZIqy6fhT3O7YmNJPdOWFmNd8vBQiW97R7JOTXtfxYKjyAwAzQPp//jxp8v0tAuFjA3Pw2EA5ourjfvt84DTQkFNVQrgrb9Dyb/UNhlvhe76ootjtd9a+gXxlrjy2P0eEOZOyDDTGLpFRtlPJezzgNPZC6LBPgDKlyPG9fvhAQCC0XWzAms7dt2CDzsGp242tsuqAAAAAElFTkSuQmCC";
6443
6542
 
6543
+ // assets/NYNRoid.png
6544
+ var NYNRoid_default = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABo0lEQVR42tWXoU8DMRTGf7csCAQBOcz4A6aOIOZAETKLBsQGCCwhiIWQCUKCRDAyAWgcWQgoPOHU/gBmOAkJggRTRPeSa7m7lSHWPdNc23u97+v3vvYCrCiVSir5HMdxwAjhmqfAmCPI+uIURKnPcRxjIc1d0GZi7AwU7Y63qyMA5reODaR268qEnc9fDdhaaDabAERRZLRhGDollnmtVsvvKihmqb3b7QJQq9WcGOh0OgDU63Xj/SyN+KeBMAxV2h4K8peTbQAWDy9HGpeIoiiYDA3Y6l9Z29NI788NpII8WN0FYLmgU0ZDfMJ/BiSU0tI4m57RHZWqRn5XzZ33ZDmn/wwk1KmSKg4C3a0eLvIzPbYB2P/61H7wW0ue3wdSvFxZ9Wvu+YARUb/tH1l1PzkMiBYkxOMldpZuAWg/r6eeCa5r+s+A3AuknjcrNwDMvZ4C8L5wAMB1b8Ood7kHpGjFfwZU0tkajYaxt6Ly3tSsNsbvj/QqGbwvPpLFtscMWHtnnxGuMezPyv8q+AdylzX8ZsDJIfv9vjFYLpf/lHvsDPwA5Wy6PgZtokAAAAAASUVORK5CYII=";
6545
+
6444
6546
  // assets/puyuyu.png
6445
6547
  var puyuyu_default = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAB60lEQVR42tWXPUxTURiGn9vWMCgtEQoDcWhAFgdtB0dMGjfcIAwMjcYoKIObq3F1EwKixNR0cCC46YQ4ODqIi4kRjUPj0Jaa1roYi9fh9Kuck3tvb7v08C73J+/5Tt7nnPvlHoeQend3yKULXXxQc8L4IvRZsU6JM9NJAKLZOQCazox6jiU0/2Gzrgq6r5RvetsFeP+2EkjEPgJ+yd3olDL81RObEp+My7Ct6oLrRcIeAmbyyKULngOikf2uJpA6GT54kug7gfZ6HO6cdwF+pYYBGBxT12vzrwF49mKpq8JXZzcAyG9dBqBRqgJw8lNRob+ybwkBc+2vrx1oBknQq4Sg6OnyCEf7g319YGU1rSM6taWujtou9S/ZwILxid1gf2sP2NsJZZdGZz57DkhMvlG9/uVZvT906bePwO9GDYCBwSFtDUWLN1UfePxkw/tzCumXeawh0O4D59InNAL54gIAN27d1wZsPrqnvu8zz/V+EdIvBD7u/bGUgEhI/BjLae9PlwqBBf385trbQ0Buvj2ccAFGR396kuhVZvJyOQ5A6s5Xx64+UPlebd2p/wAhYSYQ3S40tef1XCxwIkn+fx4s2wPmv2FyXCdhJjcT+703k1v3V+x0OhkJiV7ll9x+Ap1Px25giWNzOv4HHKK9IIEhxAYAAAAASUVORK5CYII=";
6446
6548
 
@@ -6476,7 +6578,8 @@ var VOICE_IMAGES = {
6476
6578
  tsukuyomi: tsukuyomi_default,
6477
6579
  rei: rei_default,
6478
6580
  MGRoid: MGRoid_default,
6479
- MOTRoid: MOTRoid_default
6581
+ MOTRoid: MOTRoid_default,
6582
+ NYNRoid: NYNRoid_default
6480
6583
  };
6481
6584
  var FALLBACK_VOCAL_ICON = Chip_default;
6482
6585
 
@@ -7709,12 +7812,12 @@ var KOE_INFO_HTML = `
7709
7812
 
7710
7813
  <h4>3. DTM\u3067\u4F7F\u3046</h4>
7711
7814
  <ul>
7712
- <li>\u6B4C\u5531\u30E2\u30C7\u30EB\u306E\u30D7\u30EB\u30C0\u30A6\u30F3\u304B\u3089\u300C\u30AB\u30B9\u30BF\u30E0\u97F3\u6E90\u3092\u8FFD\u52A0\u2026\u300D\u3092\u9078\u3073\u307E\u3059\u3002</li>
7713
- <li>\u300C\u97F3\u6E90URL\u300D\u306B\u624B\u98062\u306E\u76F4\u63A5\u30EA\u30F3\u30AF\u3092\u8CBC\u308A\u4ED8\u3051\u307E\u3059\u3002</li>
7714
- <li>\u30A2\u30A4\u30B3\u30F3\u753B\u50CFUR\u30EB\u30FB\u8868\u793A\u540D\u30FB\u8B58\u5225\u5B50\u306F\u4EFB\u610F\u3067\u3059\uFF08\u7701\u7565\u53EF\uFF09\u3002</li>
7815
+ <li>\u6B4C\u5531\u30E2\u30C7\u30EB\u306E\u30D7\u30EB\u30C0\u30A6\u30F3\u304B\u3089\u300C\u30AB\u30B9\u30BF\u30E0\u97F3\u58F0\u3092\u8FFD\u52A0\u2026\u300D\u3092\u9078\u3073\u307E\u3059\u3002</li>
7816
+ <li>\u300C\u97F3\u58F0URL\u300D\u306B\u624B\u98062\u306E\u76F4\u63A5\u30EA\u30F3\u30AF\u3092\u8CBC\u308A\u4ED8\u3051\u307E\u3059\u3002</li>
7817
+ <li>\u30A2\u30A4\u30B3\u30F3\u753B\u50CFURL\u306F\u4EFB\u610F\u3067\u3059\uFF08\u7701\u7565\u53EF\uFF09\u3002\u8B58\u5225\u5B50\u306F\u30D5\u30A1\u30A4\u30EB\u540D\u304B\u3089\u81EA\u52D5\u751F\u6210\u3055\u308C\u307E\u3059\u3002</li>
7715
7818
  <li>\u300C\u8FFD\u52A0\u300D\u3092\u62BC\u3059\u3068\u30D7\u30EB\u30C0\u30A6\u30F3\u306B\u767B\u9332\u3055\u308C\u3001\u4EE5\u964D\u305D\u306E\u30C8\u30E9\u30C3\u30AF\u3067\u4F7F\u3048\u307E\u3059\u3002</li>
7716
7819
  </ul>
7717
- <p style="margin-top:4px;"><small>\u203B\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u3057\u305F\u97F3\u6E90\u306F\u81EA\u5DF1\u8CAC\u4EFB\u3067\u7BA1\u7406\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u6A29\u5229\u95A2\u4FC2\uFF08\u914D\u5E03\u5143\u306E\u5229\u7528\u898F\u7D04\uFF09\u306B\u3082\u5F93\u3063\u3066\u304F\u3060\u3055\u3044\u3002</small></p>
7820
+ <p style="margin-top:4px;color:var(--dtm-warn);"><small>\u203BUTAU\u97F3\u6E90\u3092 <code>.koe</code> \u306B\u5909\u63DB\u3057\u3066\u30CD\u30C3\u30C8\u4E0A\u306B\u7F6E\u304F\u884C\u70BA\u306F\u3001\u97F3\u6E90\u30C7\u30FC\u30BF\u306E\u518D\u52A0\u5DE5\u30FB\u518D\u914D\u5E03\u306B\u3042\u305F\u308A\u307E\u3059\u3002\u3053\u308C\u304C\u914D\u5E03\u5143\u306E\u5229\u7528\u898F\u7D04\u306B\u53CD\u3057\u3066\u3044\u306A\u3044\u304B\u306F\u5FC5\u305A\u3054\u81EA\u8EAB\u3067\u78BA\u8A8D\u3057\u3001\u305D\u306E\u8CAC\u4EFB\u3092\u8CA0\u3063\u3066\u304F\u3060\u3055\u3044\u3002</small></p>
7718
7821
  </div>
7719
7822
  `;
7720
7823
  var BASE_STEP_WIDTH = 0.5;
@@ -7865,6 +7968,18 @@ var BASE_LYRIC_MODEL_LABELS = {
7865
7968
  var lyricModelLabel = (model, customMap) => BASE_LYRIC_MODEL_LABELS[model] ?? customMap.get(model)?.label ?? model;
7866
7969
  var CUSTOM_VOCAL_ADD_VALUE = "+custom";
7867
7970
  var CUSTOM_VOCAL_KEY_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
7971
+ var deriveCustomVocalKeyFromUrl = (url2) => {
7972
+ let name;
7973
+ try {
7974
+ const path = new URL(url2).pathname;
7975
+ name = decodeURIComponent(path.slice(path.lastIndexOf("/") + 1));
7976
+ } catch {
7977
+ return "";
7978
+ }
7979
+ name = name.replace(/\.[^.]*$/, "").normalize("NFKC").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
7980
+ if (name && /^[0-9]/.test(name)) name = `_${name}`;
7981
+ return CUSTOM_VOCAL_KEY_RE.test(name) ? name : "";
7982
+ };
7868
7983
  var clamp3 = (v, min, max) => Math.min(Math.max(v, min), max);
7869
7984
  var normalizeInstrumentName = (name) => {
7870
7985
  if (!name) return "";
@@ -7882,13 +7997,16 @@ var mountDAW = (target, options = {}) => {
7882
7997
  const drumPatterns = options.drumPatterns ?? DRUM_PATTERNS;
7883
7998
  const showMidi = !!options.parseMidi;
7884
7999
  const showChord = !isAdvanced;
8000
+ const midiSearchClient = options.midiSearch ? new MidiSearchClient(options.midiSearch) : void 0;
8001
+ const showMidiSearch = !!midiSearchClient?.enabled;
7885
8002
  const refs = buildUI(target, {
7886
8003
  tracks: trackConfigs,
7887
8004
  drumPatternNames: Object.keys(drumPatterns),
7888
8005
  defaultDrumPattern: drumPatterns.dance ? "dance" : Object.keys(drumPatterns)[0] ?? "none",
7889
8006
  defaultBpm: options.defaultBpm ?? DEFAULT_BPM,
7890
8007
  showMidi,
7891
- showChord
8008
+ showChord,
8009
+ showMidiSearch
7892
8010
  });
7893
8011
  const renderConfig = {
7894
8012
  stepsPerBar: 192,
@@ -8501,6 +8619,28 @@ var mountDAW = (target, options = {}) => {
8501
8619
  redrawAll();
8502
8620
  }
8503
8621
  };
8622
+ const ROLL_BG_STORAGE_KEY = "dtm-piano-roll-bg";
8623
+ const resizeImageToDataUrl = (img, maxW, maxH) => {
8624
+ const scale = Math.min(1, maxW / img.width, maxH / img.height);
8625
+ const width = Math.max(1, Math.round(img.width * scale));
8626
+ const height = Math.max(1, Math.round(img.height * scale));
8627
+ const canvas = document.createElement("canvas");
8628
+ canvas.width = width;
8629
+ canvas.height = height;
8630
+ const ctx = canvas.getContext("2d");
8631
+ if (!ctx) return img.src;
8632
+ ctx.drawImage(img, 0, 0, width, height);
8633
+ return canvas.toDataURL("image/jpeg", 0.7);
8634
+ };
8635
+ const applyRollBackground = (dataUrl) => {
8636
+ refs.rollContainer.style.setProperty(
8637
+ "--dtm-roll-bg-image",
8638
+ dataUrl ? `url(${dataUrl})` : "none"
8639
+ );
8640
+ refs.bgRemoveBtn.classList.toggle("dtm-hidden", !dataUrl);
8641
+ setBackgroundActive(!!dataUrl);
8642
+ redrawAll();
8643
+ };
8504
8644
  const setupCanvas = () => {
8505
8645
  const w = refs.rollContainer.clientWidth || 800;
8506
8646
  const h = refs.rollContainer.clientHeight || 450;
@@ -8866,10 +9006,8 @@ var mountDAW = (target, options = {}) => {
8866
9006
  <a data-dtm="lyric-custom-conv-link" href="https://onjmin.github.io/koe/demo/" target="_blank" rel="noopener" style="font-size:11px;color:var(--dtm-primary);text-decoration:underline">UTAU\u97F3\u6E90(zip)\u3092.koe\u306B\u5909\u63DB</a>
8867
9007
  <button class="dtm-btn dtm-btn--ghost dtm-btn--xs" data-dtm="lyric-custom-guide">\u4F7F\u3044\u65B9\u30AC\u30A4\u30C9</button>
8868
9008
  </div>
8869
- <input type="url" class="dtm-input" data-dtm="lyric-custom-src" placeholder="\u97F3\u6E90URL\uFF08https://\u301C.koe\uFF09" aria-label="\u30AB\u30B9\u30BF\u30E0\u97F3\u6E90\uFF08.koe\uFF09\u306EURL">
8870
- <input type="url" class="dtm-input" data-dtm="lyric-custom-icon" placeholder="\u30A2\u30A4\u30B3\u30F3\u753B\u50CFURL\uFF08\u4EFB\u610F\uFF09" aria-label="\u30AB\u30B9\u30BF\u30E0\u97F3\u6E90\u306E\u30A2\u30A4\u30B3\u30F3\u753B\u50CFURL">
8871
- <input type="text" class="dtm-input" data-dtm="lyric-custom-alias" placeholder="\u8868\u793A\u540D\uFF08\u4EFB\u610F\uFF09" aria-label="\u30AB\u30B9\u30BF\u30E0\u97F3\u6E90\u306E\u8868\u793A\u540D\uFF08\u30D7\u30EB\u30C0\u30A6\u30F3\u306E\u5225\u540D\uFF09" maxlength="64">
8872
- <input type="text" class="dtm-input" data-dtm="lyric-custom-key" placeholder="\u8B58\u5225\u5B50\uFF08\u4EFB\u610F\u30FB\u7701\u7565\u3067\u81EA\u52D5\u63A1\u756A\u3001\u82F1\u5B57\u59CB\u307E\u308A\u82F1\u6570\u5B57\u3068_\uFF09" aria-label="\u30AB\u30B9\u30BF\u30E0\u97F3\u6E90\u306EMML\u8B58\u5225\u5B50\uFF08\u30AD\u30FC\uFF09" maxlength="32">
9009
+ <input type="url" class="dtm-input" data-dtm="lyric-custom-src" placeholder="\u97F3\u58F0URL\uFF08https://\u301C.koe\uFF09" aria-label="\u30AB\u30B9\u30BF\u30E0\u97F3\u58F0\uFF08.koe\uFF09\u306EURL">
9010
+ <input type="url" class="dtm-input" data-dtm="lyric-custom-icon" placeholder="\u30A2\u30A4\u30B3\u30F3\u753B\u50CFURL\uFF08\u4EFB\u610F\uFF09" aria-label="\u30AB\u30B9\u30BF\u30E0\u97F3\u58F0\u306E\u30A2\u30A4\u30B3\u30F3\u753B\u50CFURL">
8873
9011
  <div class="dtm-row">
8874
9012
  <span class="dtm-label dtm-grow" data-dtm="lyric-custom-note" style="color:var(--dtm-warn)"></span>
8875
9013
  <button class="dtm-btn dtm-btn--primary" data-dtm="lyric-custom-apply">\u8FFD\u52A0</button>
@@ -8934,12 +9072,6 @@ var mountDAW = (target, options = {}) => {
8934
9072
  const lyricCustomIcon = lyricDiv.querySelector(
8935
9073
  '[data-dtm="lyric-custom-icon"]'
8936
9074
  );
8937
- const lyricCustomAlias = lyricDiv.querySelector(
8938
- '[data-dtm="lyric-custom-alias"]'
8939
- );
8940
- const lyricCustomKey = lyricDiv.querySelector(
8941
- '[data-dtm="lyric-custom-key"]'
8942
- );
8943
9075
  const lyricCustomGuide = lyricDiv.querySelector(
8944
9076
  '[data-dtm="lyric-custom-guide"]'
8945
9077
  );
@@ -8960,7 +9092,7 @@ var mountDAW = (target, options = {}) => {
8960
9092
  for (const m of BASE_LYRIC_MODELS)
8961
9093
  addOpt(m, lyricModelLabel(m, customVocalsMap));
8962
9094
  for (const [k, def] of customVocalsMap) addOpt(k, def.label ?? k);
8963
- addOpt(CUSTOM_VOCAL_ADD_VALUE, "\u30AB\u30B9\u30BF\u30E0\u97F3\u6E90\u3092\u8FFD\u52A0\u2026");
9095
+ addOpt(CUSTOM_VOCAL_ADD_VALUE, "\u30AB\u30B9\u30BF\u30E0\u97F3\u58F0\u3092\u8FFD\u52A0\u2026");
8964
9096
  if (active.lyricModel && !BASE_LYRIC_MODELS.includes(active.lyricModel) && !customVocalsMap.has(active.lyricModel)) {
8965
9097
  addOpt(
8966
9098
  active.lyricModel,
@@ -9032,9 +9164,10 @@ var mountDAW = (target, options = {}) => {
9032
9164
  lyricModelSel.addEventListener("change", () => {
9033
9165
  if (lyricModelSel.value === CUSTOM_VOCAL_ADD_VALUE) {
9034
9166
  lyricCustomNote.textContent = "";
9035
- lyricCustomAlias.value = "";
9036
- lyricCustomKey.value = "";
9037
9167
  lyricCustom.classList.remove("dtm-hidden");
9168
+ lyricIcon.removeAttribute("src");
9169
+ lyricIcon.classList.add("dtm-hidden");
9170
+ lyricTerms.classList.add("dtm-hidden");
9038
9171
  return;
9039
9172
  }
9040
9173
  lyricCustom.classList.add("dtm-hidden");
@@ -9044,45 +9177,41 @@ var mountDAW = (target, options = {}) => {
9044
9177
  fireLyricsChange(active);
9045
9178
  });
9046
9179
  lyricCustomGuide.addEventListener("click", () => {
9047
- showModal("\u30AB\u30B9\u30BF\u30E0\u97F3\u6E90(.koe)\u306E\u4F7F\u3044\u65B9", KOE_INFO_HTML);
9180
+ showModal("\u30AB\u30B9\u30BF\u30E0\u97F3\u58F0(.koe)\u306E\u4F7F\u3044\u65B9", KOE_INFO_HTML);
9048
9181
  });
9049
9182
  lyricCustomApply.addEventListener("click", () => {
9050
9183
  const src = lyricCustomSrc.value.trim();
9051
9184
  if (!isValidHttpUrl(src)) {
9052
- lyricCustomNote.textContent = "\u97F3\u6E90URL\u304C\u4E0D\u6B63\u3067\u3059\uFF08http/https\u306E\u307F\u30FB2048\u6587\u5B57\u307E\u3067\uFF09";
9185
+ lyricCustomNote.textContent = "\u97F3\u58F0URL\u304C\u4E0D\u6B63\u3067\u3059\uFF08http/https\u306E\u307F\u30FB2048\u6587\u5B57\u307E\u3067\uFF09";
9053
9186
  return;
9054
9187
  }
9055
9188
  const iconRaw = lyricCustomIcon.value.trim();
9056
9189
  const iconUrl = isValidHttpUrl(iconRaw) ? iconRaw : "";
9057
- const alias = lyricCustomAlias.value.trim();
9058
9190
  const existing = [...customVocalsMap.values()].find(
9059
9191
  (d) => d.url === src
9060
9192
  );
9061
- const keyRaw = lyricCustomKey.value.trim().toLowerCase();
9062
9193
  let key;
9063
- if (keyRaw) {
9064
- if (!CUSTOM_VOCAL_KEY_RE.test(keyRaw)) {
9065
- lyricCustomNote.textContent = "\u8B58\u5225\u5B50\u304C\u4E0D\u6B63\u3067\u3059\uFF08\u82F1\u5B57\u307E\u305F\u306F\u30A2\u30F3\u30C0\u30FC\u30B9\u30B3\u30A2\u3067\u59CB\u307E\u308B\u82F1\u6570\u5B57\u30FB_\u306E\u307F\uFF09";
9066
- return;
9067
- }
9068
- if (BASE_LYRIC_MODELS.includes(keyRaw)) {
9069
- lyricCustomNote.textContent = "\u305D\u306E\u8B58\u5225\u5B50\u306F\u5185\u8535\u30E2\u30C7\u30EB\u540D\u3068\u885D\u7A81\u3057\u3066\u3044\u307E\u3059";
9070
- return;
9071
- }
9072
- const conflict = customVocalsMap.get(keyRaw);
9073
- if (conflict && conflict.url !== src) {
9074
- lyricCustomNote.textContent = "\u305D\u306E\u8B58\u5225\u5B50\u306F\u5225\u306E\u97F3\u6E90\u306B\u4F7F\u7528\u6E08\u307F\u3067\u3059";
9075
- return;
9076
- }
9077
- key = keyRaw;
9194
+ if (existing) {
9195
+ key = existing.key;
9078
9196
  } else {
9079
- key = existing?.key ?? genCustomVocalKey();
9197
+ const derived = deriveCustomVocalKeyFromUrl(src);
9198
+ if (derived && !BASE_LYRIC_MODELS.includes(derived)) {
9199
+ if (!customVocalsMap.has(derived)) {
9200
+ key = derived;
9201
+ } else {
9202
+ let n = 2;
9203
+ while (customVocalsMap.has(`${derived}${n}`)) n++;
9204
+ key = `${derived}${n}`;
9205
+ }
9206
+ } else {
9207
+ key = genCustomVocalKey();
9208
+ }
9080
9209
  }
9081
9210
  registerCustomVocal({
9082
9211
  key,
9083
9212
  iconUrl: iconUrl || (existing?.iconUrl ?? ""),
9084
9213
  url: src,
9085
- label: alias || existing?.label
9214
+ label: existing?.label
9086
9215
  });
9087
9216
  active.lyricModel = key;
9088
9217
  fireLyricsChange(active);
@@ -9583,9 +9712,10 @@ var mountDAW = (target, options = {}) => {
9583
9712
  refs.playBtn.addEventListener("click", togglePlay);
9584
9713
  refs.playBtn.disabled = false;
9585
9714
  refs.prevBarBtn.addEventListener("click", () => {
9715
+ const barsBack = playbackState === "playing" ? 2 : 1;
9586
9716
  const targetStep = Math.max(
9587
9717
  0,
9588
- Math.floor((getCurrentPlayStep() - 1) / renderConfig.stepsPerBar) * renderConfig.stepsPerBar
9718
+ (Math.floor((getCurrentPlayStep() - 1) / renderConfig.stepsPerBar) - barsBack) * renderConfig.stepsPerBar
9589
9719
  );
9590
9720
  jumpTo(targetStep);
9591
9721
  });
@@ -9630,6 +9760,35 @@ var mountDAW = (target, options = {}) => {
9630
9760
  applyZoomY();
9631
9761
  notifyViewState();
9632
9762
  });
9763
+ refs.bgUploadBtn.addEventListener("click", () => refs.bgFileInput.click());
9764
+ refs.bgFileInput.addEventListener("change", () => {
9765
+ const file = refs.bgFileInput.files?.[0];
9766
+ refs.bgFileInput.value = "";
9767
+ if (!file) return;
9768
+ const reader = new FileReader();
9769
+ reader.onload = () => {
9770
+ const img = new Image();
9771
+ img.onload = () => {
9772
+ const maxW = refs.rollContainer.clientWidth || 800;
9773
+ const maxH = refs.rollContainer.clientHeight || 450;
9774
+ const dataUrl = resizeImageToDataUrl(img, maxW, maxH);
9775
+ try {
9776
+ localStorage.setItem(ROLL_BG_STORAGE_KEY, dataUrl);
9777
+ } catch {
9778
+ }
9779
+ applyRollBackground(dataUrl);
9780
+ };
9781
+ img.src = reader.result;
9782
+ };
9783
+ reader.readAsDataURL(file);
9784
+ });
9785
+ refs.bgRemoveBtn.addEventListener("click", () => {
9786
+ try {
9787
+ localStorage.removeItem(ROLL_BG_STORAGE_KEY);
9788
+ } catch {
9789
+ }
9790
+ applyRollBackground(null);
9791
+ });
9633
9792
  refs.decomposeChordToggle.addEventListener("change", notifyViewState);
9634
9793
  refs.ignoreChordHeavyToggle.addEventListener("change", notifyViewState);
9635
9794
  refs.masterVolume.addEventListener("input", () => {
@@ -9740,6 +9899,7 @@ var mountDAW = (target, options = {}) => {
9740
9899
  };
9741
9900
  showModal = (title, bodyHTML) => {
9742
9901
  collapseActiveSample();
9902
+ collapseSearchPreview();
9743
9903
  refs.modalTitle.textContent = title;
9744
9904
  refs.modalBody.innerHTML = bodyHTML;
9745
9905
  refs.modalOverlay.removeAttribute("hidden");
@@ -9828,11 +9988,13 @@ var mountDAW = (target, options = {}) => {
9828
9988
  };
9829
9989
  refs.modalClose.addEventListener("click", () => {
9830
9990
  collapseActiveSample();
9991
+ collapseSearchPreview();
9831
9992
  refs.modalOverlay.setAttribute("hidden", "");
9832
9993
  });
9833
9994
  refs.modalOverlay.addEventListener("click", (e) => {
9834
9995
  if (e.target === refs.modalOverlay) {
9835
9996
  collapseActiveSample();
9997
+ collapseSearchPreview();
9836
9998
  refs.modalOverlay.setAttribute("hidden", "");
9837
9999
  }
9838
10000
  });
@@ -9853,6 +10015,7 @@ var mountDAW = (target, options = {}) => {
9853
10015
  })
9854
10016
  );
9855
10017
  if (showMidi) wireMidi();
10018
+ if (showMidiSearch) wireMidiSearch();
9856
10019
  document.addEventListener("keydown", onKeyDown);
9857
10020
  refs.root.addEventListener("keydown", (e) => {
9858
10021
  const t = e.target;
@@ -9917,6 +10080,236 @@ var mountDAW = (target, options = {}) => {
9917
10080
  overlayDuring(() => applyMidiSelection(pendingMidi, selected));
9918
10081
  });
9919
10082
  };
10083
+ const buildPreviewMml = (midi) => {
10084
+ const analysis = analyzeMidiTracks(midi);
10085
+ const selected = analysis.filter((a) => a.selected).map((a) => a.index);
10086
+ const { placements, bpm: parsedBpm } = extractMidiPlacements(
10087
+ midi,
10088
+ selected
10089
+ );
10090
+ const byTrack = /* @__PURE__ */ new Map();
10091
+ for (const p of placements) {
10092
+ if (!byTrack.has(p.trackId)) byTrack.set(p.trackId, []);
10093
+ byTrack.get(p.trackId)?.push(p);
10094
+ }
10095
+ const refCore = trackStates[0].core;
10096
+ const tempo = Math.round(parsedBpm) || bpm;
10097
+ const lines = [];
10098
+ let i = 0;
10099
+ for (const notes of byTrack.values()) {
10100
+ const asNotes = notes.map((p) => ({
10101
+ id: 0,
10102
+ startStep: p.startStep,
10103
+ durationSteps: p.durationSteps,
10104
+ pitch: p.pitch,
10105
+ velocity: p.velocity
10106
+ }));
10107
+ const mml = refCore.getMMLFromNotes(asNotes, tempo, 100).trim();
10108
+ lines.push(`@${i} ${mml}`);
10109
+ i++;
10110
+ }
10111
+ return [...lines, MML_END_MARKER].join(";\n");
10112
+ };
10113
+ const fetchVerifiedMidi = async (fileName) => {
10114
+ if (!options.parseMidi || !midiSearchClient) {
10115
+ throw new Error("parseMidi/midiSearchClient not injected");
10116
+ }
10117
+ const midiBytes = await midiSearchClient.fetchMidi(fileName);
10118
+ const midi = await options.parseMidi(new Uint8Array(midiBytes));
10119
+ const analysis = analyzeMidiTracks(midi);
10120
+ const selected = analysis.filter((a) => a.selected).map((a) => a.index);
10121
+ if (!isPlausibleMidiTranscription(midi, selected)) {
10122
+ throw new Error("implausible MIDI transcription");
10123
+ }
10124
+ return midi;
10125
+ };
10126
+ let searchPreviewPlayer = null;
10127
+ let searchPreviewBtn = null;
10128
+ const collapseSearchPreview = () => {
10129
+ if (searchPreviewPlayer) {
10130
+ searchPreviewPlayer.stop();
10131
+ searchPreviewPlayer.destroy();
10132
+ searchPreviewPlayer = null;
10133
+ }
10134
+ if (searchPreviewBtn) {
10135
+ searchPreviewBtn.textContent = "\u25B6 \u8A66\u8074";
10136
+ searchPreviewBtn.classList.remove("dtm-btn--danger");
10137
+ searchPreviewBtn.classList.add("dtm-btn--ghost");
10138
+ const container = searchPreviewBtn.closest(".dtm-modal-sample-box")?.querySelector(".dtm-modal-sample-player-container");
10139
+ if (container) container.innerHTML = "";
10140
+ searchPreviewBtn = null;
10141
+ }
10142
+ };
10143
+ const wireMidiSearch = () => {
10144
+ if (!midiSearchClient) return;
10145
+ refs.midiSearchOpenBtn.addEventListener("click", () => {
10146
+ showModal(
10147
+ "MML\u691C\u7D22",
10148
+ `
10149
+ <div class="dtm-row" style="flex-wrap:nowrap">
10150
+ <input type="text" class="dtm-input dtm-grow" data-dtm="modal-midi-search-input" placeholder="\u66F2\u540D\u3067MML\u691C\u7D22" style="min-width:0">
10151
+ <button class="dtm-btn dtm-btn--primary" data-dtm="modal-midi-search-btn" style="flex-shrink:0">\u691C\u7D22</button>
10152
+ </div>
10153
+ <div class="dtm-row" data-dtm="modal-midi-search-results" style="flex-direction:column;align-items:stretch;gap:4px"></div>
10154
+ `
10155
+ );
10156
+ const input = refs.modalBody.querySelector(
10157
+ '[data-dtm="modal-midi-search-input"]'
10158
+ );
10159
+ const searchBtn = refs.modalBody.querySelector(
10160
+ '[data-dtm="modal-midi-search-btn"]'
10161
+ );
10162
+ const results = refs.modalBody.querySelector(
10163
+ '[data-dtm="modal-midi-search-results"]'
10164
+ );
10165
+ const runSearch = async () => {
10166
+ const q2 = input.value.trim();
10167
+ if (!q2) return;
10168
+ collapseSearchPreview();
10169
+ searchBtn.disabled = true;
10170
+ searchBtn.textContent = "\u691C\u7D22\u4E2D...";
10171
+ results.innerHTML = "";
10172
+ try {
10173
+ const songs = await midiSearchClient.searchSongs({ title: q2 });
10174
+ if (songs.length === 0) {
10175
+ results.innerHTML = '<span class="dtm-label" style="color:var(--dtm-warn)">\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F</span>';
10176
+ return;
10177
+ }
10178
+ for (const song of songs) {
10179
+ const box = document.createElement("div");
10180
+ box.className = "dtm-modal-sample-box";
10181
+ const row = document.createElement("div");
10182
+ row.className = "dtm-row";
10183
+ row.style.cssText = "flex-wrap:nowrap;gap:4px;padding:2px 0";
10184
+ const label = document.createElement("span");
10185
+ label.className = "dtm-label";
10186
+ label.style.cssText = "flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap";
10187
+ label.textContent = `${song.title} - ${song.user}`;
10188
+ const previewBtn = document.createElement("button");
10189
+ previewBtn.className = "dtm-btn dtm-btn--ghost";
10190
+ previewBtn.style.cssText = "flex-shrink:0";
10191
+ previewBtn.textContent = "\u25B6 \u8A66\u8074";
10192
+ const loadBtn = document.createElement("button");
10193
+ loadBtn.className = "dtm-btn dtm-btn--primary";
10194
+ loadBtn.style.cssText = "flex-shrink:0";
10195
+ loadBtn.textContent = "\u8AAD\u307F\u8FBC\u307F";
10196
+ const playerContainer = document.createElement("div");
10197
+ playerContainer.className = "dtm-modal-sample-player-container";
10198
+ previewBtn.addEventListener("click", async () => {
10199
+ if (searchPreviewBtn === previewBtn) {
10200
+ if (searchPreviewPlayer?.isPlaying()) {
10201
+ searchPreviewPlayer.stop();
10202
+ } else {
10203
+ stop();
10204
+ previewBtn.textContent = "\u8AAD\u8FBC\u4E2D...";
10205
+ try {
10206
+ const midi = await fetchVerifiedMidi(song.file);
10207
+ const mml = buildPreviewMml(midi);
10208
+ playerContainer.innerHTML = "";
10209
+ const player = mountMmlPlayer(playerContainer, mml, {
10210
+ onPlayNote: options.onPlayNote,
10211
+ onPlayDrum: options.onPlayDrum,
10212
+ onResumeAudio: options.onResumeAudio,
10213
+ getAudioTime: options.getAudioTime,
10214
+ singingVoices: options.singingVoices,
10215
+ drumPatterns: options.drumPatterns,
10216
+ volume: masterVolume,
10217
+ _skipInfoModals: true,
10218
+ onStop: () => {
10219
+ if (searchPreviewBtn === previewBtn) {
10220
+ previewBtn.textContent = "\u25B6 \u8A66\u8074";
10221
+ previewBtn.classList.remove("dtm-btn--danger");
10222
+ previewBtn.classList.add("dtm-btn--ghost");
10223
+ }
10224
+ }
10225
+ });
10226
+ searchPreviewPlayer = player;
10227
+ searchPreviewBtn = previewBtn;
10228
+ player.play();
10229
+ previewBtn.textContent = "\u25A0 \u505C\u6B62";
10230
+ previewBtn.classList.remove("dtm-btn--ghost");
10231
+ previewBtn.classList.add("dtm-btn--danger");
10232
+ } catch (e) {
10233
+ console.error("[dtm] MIDI preview failed", e);
10234
+ previewBtn.textContent = "\u5931\u6557";
10235
+ }
10236
+ }
10237
+ } else {
10238
+ collapseSearchPreview();
10239
+ stop();
10240
+ previewBtn.textContent = "\u8AAD\u8FBC\u4E2D...";
10241
+ try {
10242
+ const midi = await fetchVerifiedMidi(song.file);
10243
+ const mml = buildPreviewMml(midi);
10244
+ playerContainer.innerHTML = "";
10245
+ const player = mountMmlPlayer(playerContainer, mml, {
10246
+ onPlayNote: options.onPlayNote,
10247
+ onPlayDrum: options.onPlayDrum,
10248
+ onResumeAudio: options.onResumeAudio,
10249
+ getAudioTime: options.getAudioTime,
10250
+ singingVoices: options.singingVoices,
10251
+ drumPatterns: options.drumPatterns,
10252
+ volume: masterVolume,
10253
+ _skipInfoModals: true,
10254
+ onStop: () => {
10255
+ if (searchPreviewBtn === previewBtn) {
10256
+ previewBtn.textContent = "\u25B6 \u8A66\u8074";
10257
+ previewBtn.classList.remove("dtm-btn--danger");
10258
+ previewBtn.classList.add("dtm-btn--ghost");
10259
+ }
10260
+ }
10261
+ });
10262
+ searchPreviewPlayer = player;
10263
+ searchPreviewBtn = previewBtn;
10264
+ player.play();
10265
+ previewBtn.textContent = "\u25A0 \u505C\u6B62";
10266
+ previewBtn.classList.remove("dtm-btn--ghost");
10267
+ previewBtn.classList.add("dtm-btn--danger");
10268
+ } catch (e) {
10269
+ console.error("[dtm] MIDI preview failed", e);
10270
+ previewBtn.textContent = "\u5931\u6557";
10271
+ }
10272
+ }
10273
+ });
10274
+ loadBtn.addEventListener("click", async () => {
10275
+ loadBtn.disabled = true;
10276
+ loadBtn.textContent = "\u8AAD\u8FBC\u4E2D...";
10277
+ try {
10278
+ const pending = await fetchVerifiedMidi(song.file);
10279
+ const analysis = analyzeMidiTracks(pending);
10280
+ const sel = analysis.filter((a) => a.selected).map((a) => a.index);
10281
+ collapseSearchPreview();
10282
+ overlayDuring(() => applyMidiSelection(pending, sel));
10283
+ refs.modalOverlay.setAttribute("hidden", "");
10284
+ } catch (e) {
10285
+ console.error("[dtm] MIDI fetch failed", e);
10286
+ loadBtn.textContent = "\u5931\u6557";
10287
+ } finally {
10288
+ loadBtn.disabled = false;
10289
+ }
10290
+ });
10291
+ row.appendChild(label);
10292
+ row.appendChild(previewBtn);
10293
+ row.appendChild(loadBtn);
10294
+ box.appendChild(row);
10295
+ box.appendChild(playerContainer);
10296
+ results.appendChild(box);
10297
+ }
10298
+ } catch (e) {
10299
+ console.error("[dtm] MIDI search failed", e);
10300
+ results.innerHTML = '<span class="dtm-label" style="color:var(--dtm-warn)">\u691C\u7D22\u306B\u5931\u6557\u3057\u307E\u3057\u305F</span>';
10301
+ } finally {
10302
+ searchBtn.disabled = false;
10303
+ searchBtn.textContent = "\u691C\u7D22";
10304
+ }
10305
+ };
10306
+ searchBtn.addEventListener("click", () => void runSearch());
10307
+ input.addEventListener("keydown", (e) => {
10308
+ if (e.key === "Enter") void runSearch();
10309
+ });
10310
+ input.focus();
10311
+ });
10312
+ };
9920
10313
  const onKeyDown = (e) => {
9921
10314
  if (!(e.ctrlKey || e.metaKey)) return;
9922
10315
  if (e.code === "KeyZ" && !e.shiftKey) {
@@ -9972,6 +10365,11 @@ var mountDAW = (target, options = {}) => {
9972
10365
  updateUndoRedo();
9973
10366
  redrawAll();
9974
10367
  if (options.initialMML) loadMML(options.initialMML);
10368
+ try {
10369
+ const storedBg = localStorage.getItem(ROLL_BG_STORAGE_KEY);
10370
+ if (storedBg) applyRollBackground(storedBg);
10371
+ } catch {
10372
+ }
9975
10373
  let resizeTimer = null;
9976
10374
  const resizeObserver = new ResizeObserver(() => {
9977
10375
  if (resizeTimer) clearTimeout(resizeTimer);
@@ -11293,13 +11691,17 @@ var createDtmStudio = async (options = {}) => {
11293
11691
  const mountedPlayers = [];
11294
11692
  const mountedModeSwitches = [];
11295
11693
  const mountEditor = (target, opts = {}) => {
11694
+ const mergedOpts = {
11695
+ midiSearch: options.midiSearch,
11696
+ ...opts
11697
+ };
11296
11698
  const {
11297
11699
  preset,
11298
11700
  presetUI,
11299
11701
  onInstrumentChange,
11300
11702
  onTrackInstrumentChange: externalOnTrackInstrumentChange,
11301
11703
  ...dawOverrides
11302
- } = opts;
11704
+ } = mergedOpts;
11303
11705
  const tracks = dawOverrides.tracks ?? TRACKS_SIMPLE;
11304
11706
  const trackIds = tracks.map((t) => t.id);
11305
11707
  const presetKey = preset && INSTRUMENT_PRESETS[preset] ? preset : defaultPreset;
@@ -11635,6 +12037,7 @@ var createDtmStudio = async (options = {}) => {
11635
12037
  MAX_VOCAL_VOLUME,
11636
12038
  MMLCore,
11637
12039
  MML_END_MARKER,
12040
+ MidiSearchClient,
11638
12041
  PITCH_MAP,
11639
12042
  PREWARM_NOTES,
11640
12043
  TRACKS_ADVANCED,
@@ -11684,6 +12087,7 @@ var createDtmStudio = async (options = {}) => {
11684
12087
  init,
11685
12088
  injectStyles,
11686
12089
  isChordHeavyTrack,
12090
+ isPlausibleMidiTranscription,
11687
12091
  isValidHttpUrl,
11688
12092
  koeUrl,
11689
12093
  mountDAW,
@@ -11697,6 +12101,7 @@ var createDtmStudio = async (options = {}) => {
11697
12101
  parseMmlMeta,
11698
12102
  playMML,
11699
12103
  playSingingMML,
12104
+ setBackgroundActive,
11700
12105
  setDrawOffset,
11701
12106
  shiftNotes,
11702
12107
  showLoadingOverlay,