@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.mjs CHANGED
@@ -1215,7 +1215,13 @@ var icon = (name, size = 20) => {
1215
1215
  // src/daw-ui.ts
1216
1216
  var q = (root, sel) => root.querySelector(sel);
1217
1217
  var buildUI = (target, options) => {
1218
- const { drumPatternNames, defaultDrumPattern, defaultBpm, showMidi } = options;
1218
+ const {
1219
+ drumPatternNames,
1220
+ defaultDrumPattern,
1221
+ defaultBpm,
1222
+ showMidi,
1223
+ showMidiSearch
1224
+ } = options;
1219
1225
  const drumOptions = [`<option value="none">\u306A\u3057</option>`].concat(
1220
1226
  drumPatternNames.map(
1221
1227
  (name) => `<option value="${name}" ${name === defaultDrumPattern ? "selected" : ""}>${name}</option>`
@@ -1293,6 +1299,12 @@ var buildUI = (target, options) => {
1293
1299
  <span class="dtm-label" data-dtm="zoomy-label">100%</span>
1294
1300
  <button class="dtm-iconbtn" data-dtm="zoomy-in" title="\u62E1\u5927">\uFF0B</button>
1295
1301
  </div>
1302
+ <div class="dtm-row">
1303
+ <span class="dtm-label">\u80CC\u666F\u753B\u50CF</span>
1304
+ <input type="file" accept="image/*" data-dtm="bg-file-input" class="dtm-hidden">
1305
+ <button class="dtm-btn dtm-btn--primary" data-dtm="bg-upload">\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9</button>
1306
+ <button class="dtm-btn dtm-btn--danger dtm-hidden" data-dtm="bg-remove">\u524A\u9664</button>
1307
+ </div>
1296
1308
  </div>
1297
1309
  </details>
1298
1310
 
@@ -1331,6 +1343,9 @@ var buildUI = (target, options) => {
1331
1343
  <textarea class="dtm-textarea dtm-grow" data-dtm="mml-input" placeholder="MML\u3092\u5165\u529B"></textarea>
1332
1344
  <button class="dtm-btn dtm-btn--primary" data-dtm="mml-load" style="flex-shrink:0">\u8AAD\u8FBC</button>
1333
1345
  </div>
1346
+ <div class="dtm-row" data-dtm="midi-search-row" style="justify-content:flex-end;${showMidiSearch ? "" : "display:none"}">
1347
+ <button class="dtm-btn dtm-btn--primary" data-dtm="midi-search-open">MML\u691C\u7D22</button>
1348
+ </div>
1334
1349
  <p class="dtm-load-note dtm-hidden" data-dtm="mml-load-note"></p>
1335
1350
  </div>
1336
1351
  </details>
@@ -1441,6 +1456,9 @@ var buildUI = (target, options) => {
1441
1456
  zoomXOut: sel("zoomx-out"),
1442
1457
  zoomYIn: sel("zoomy-in"),
1443
1458
  zoomYOut: sel("zoomy-out"),
1459
+ bgFileInput: sel("bg-file-input"),
1460
+ bgUploadBtn: sel("bg-upload"),
1461
+ bgRemoveBtn: sel("bg-remove"),
1444
1462
  rollContainer: sel("roll"),
1445
1463
  wrapper: sel("wrapper"),
1446
1464
  vScroll: sel("vscroll"),
@@ -1459,6 +1477,7 @@ var buildUI = (target, options) => {
1459
1477
  midiInfoBtn: sel("midi-info"),
1460
1478
  midiTrackSelection: sel("midi-track-selection"),
1461
1479
  midiPanel: sel("midi-panel"),
1480
+ midiSearchOpenBtn: sel("midi-search-open"),
1462
1481
  mmlInput: sel("mml-input"),
1463
1482
  mmlLoadBtn: sel("mml-load"),
1464
1483
  mmlLoadNote: sel("mml-load-note"),
@@ -2503,8 +2522,9 @@ var KOE_VOICEBANKS = {
2503
2522
  teto: "\u91CD\u97F3\u30C6\u30C8\u5358\u72EC\u97F3.koe",
2504
2523
  shiyo: "\u9769\u547D\u30B7\u30E8.koe",
2505
2524
  rei: "\u8DB3\u7ACB\u30EC\u30A4ver3.5.0.koe",
2506
- MGRoid: "MGRoid_\u539F\u97F3\u8A2D\u5B9A\u6E08\u307F.koe",
2507
- MOTRoid: "MOTRoid\u5B8C\u5168\u7248V2.koe"
2525
+ mgroid: "MGRoid_\u539F\u97F3\u8A2D\u5B9A\u6E08\u307F.koe",
2526
+ motroid: "MOTRoid\u5B8C\u5168\u7248V2.koe",
2527
+ nynroid: "NYNRoidver1.4.koe"
2508
2528
  };
2509
2529
  var KOE_VOICEBANK_LABELS = {
2510
2530
  tsukuyomi: "\u3064\u304F\u3088\u307F\u3061\u3083\u3093",
@@ -2515,8 +2535,9 @@ var KOE_VOICEBANK_LABELS = {
2515
2535
  teto: "\u91CD\u97F3\u30C6\u30C8",
2516
2536
  shiyo: "\u9769\u547D\u30B7\u30E8",
2517
2537
  rei: "\u8DB3\u7ACB\u30EC\u30A4",
2518
- MGRoid: "MGRoid",
2519
- MOTRoid: "MOTRoid"
2538
+ mgroid: "MGRoid",
2539
+ motroid: "MOTRoid",
2540
+ nynroid: "NYNRoid"
2520
2541
  };
2521
2542
  var VOICE_IMAGE_KEY = {
2522
2543
  klatt: "puyuyu",
@@ -2528,8 +2549,9 @@ var VOICE_IMAGE_KEY = {
2528
2549
  teto: "teto",
2529
2550
  shiyo: "shiyo",
2530
2551
  rei: "rei",
2531
- MGRoid: "MGRoid",
2532
- MOTRoid: "MOTRoid"
2552
+ mgroid: "MGRoid",
2553
+ motroid: "MOTRoid",
2554
+ nynroid: "NYNRoid"
2533
2555
  };
2534
2556
  var KOE_VOICEBANK_TERMS = {
2535
2557
  tsukuyomi: "https://tyc.rei-yumesaki.net/material/utau/terms/",
@@ -2540,8 +2562,9 @@ var KOE_VOICEBANK_TERMS = {
2540
2562
  teto: "https://kasaneteto.jp/guidelines/voice.html",
2541
2563
  shiyo: "https://kakumeisiyo.my.canva.site/dagkuyjwycs",
2542
2564
  rei: "https://mechanicalgirl.jp/guidelines/",
2543
- MGRoid: "https://x.com/nisusansu/status/1048825378188353536",
2544
- MOTRoid: "https://www.nicovideo.jp/watch/sm40031282"
2565
+ mgroid: "https://x.com/nisusansu/status/1048825378188353536",
2566
+ motroid: "https://www.nicovideo.jp/watch/sm40031282",
2567
+ nynroid: "https://www.bilibili.com/video/BV1V24y1a7qs"
2545
2568
  };
2546
2569
  var koeUrl = (name, base = KOE_BASE_URL) => `${base}/${encodeURIComponent(name)}`;
2547
2570
  var DEFAULT_WORLDLINE_SCRIPT = "https://onjmin.github.io/koe/demo/world/worldline.js";
@@ -3365,6 +3388,19 @@ var extractMidiPlacements = (midi, selectedTrackIndices) => {
3365
3388
  }
3366
3389
  return { placements, bpm };
3367
3390
  };
3391
+ var isPlausibleMidiTranscription = (midi, selectedIndices) => {
3392
+ const { placements } = extractMidiPlacements(midi, selectedIndices);
3393
+ if (placements.length < 4) return false;
3394
+ const shortCount = placements.filter((p) => p.durationSteps <= 1).length;
3395
+ const shortRatio = shortCount / placements.length;
3396
+ const countByStep = /* @__PURE__ */ new Map();
3397
+ for (const p of placements) {
3398
+ countByStep.set(p.startStep, (countByStep.get(p.startStep) ?? 0) + 1);
3399
+ }
3400
+ const denseSteps = [...countByStep.values()].filter((n) => n >= 6).length;
3401
+ const denseRatio = denseSteps / countByStep.size;
3402
+ return shortRatio < 0.6 && denseRatio < 0.35;
3403
+ };
3368
3404
  var extractMidiPlacementsByTrack = (midi, selectedIndices, trackIds) => {
3369
3405
  const { tracks, division } = midi;
3370
3406
  const ticksPerBeat = division;
@@ -3517,6 +3553,45 @@ var exportMIDI = (options) => {
3517
3553
  return new Blob([new Uint8Array(arr).buffer], { type: "audio/midi" });
3518
3554
  };
3519
3555
 
3556
+ // src/midi-search.ts
3557
+ var MidiSearchClient = class {
3558
+ config;
3559
+ constructor(config = {}) {
3560
+ this.config = config;
3561
+ }
3562
+ get enabled() {
3563
+ return !!this.config.apiKey;
3564
+ }
3565
+ get baseUrl() {
3566
+ return this.config.baseUrl ?? "https://rpgen-search.pages.dev/api";
3567
+ }
3568
+ headers() {
3569
+ const h = {};
3570
+ if (this.config.apiKey) {
3571
+ h.Authorization = `Bearer ${this.config.apiKey}`;
3572
+ }
3573
+ return h;
3574
+ }
3575
+ async searchSongs(params) {
3576
+ if (!this.enabled) return [];
3577
+ const qs = new URLSearchParams();
3578
+ if (params.title) qs.set("title", params.title);
3579
+ if (params.user) qs.set("user", params.user);
3580
+ if (params.twitter_id) qs.set("twitter_id", params.twitter_id);
3581
+ const url2 = `${this.baseUrl}/picotune/songs${qs.toString() ? `?${qs.toString()}` : ""}`;
3582
+ const res = await fetch(url2, { headers: this.headers() });
3583
+ if (!res.ok) throw new Error(`picotune search failed: ${res.status}`);
3584
+ const body = await res.json();
3585
+ return body.data ?? body.songs ?? body;
3586
+ }
3587
+ async fetchMidi(fileName) {
3588
+ const url2 = `${this.baseUrl}/picotune/songs/${encodeURIComponent(fileName)}`;
3589
+ const res = await fetch(url2, { headers: this.headers() });
3590
+ if (!res.ok) throw new Error(`picotune fetch failed: ${res.status}`);
3591
+ return res.arrayBuffer();
3592
+ }
3593
+ };
3594
+
3520
3595
  // src/linked-list.ts
3521
3596
  var LinkedList = class {
3522
3597
  #cursor;
@@ -3582,6 +3657,10 @@ var HEADER_HEIGHT = 20;
3582
3657
  var getRenderConfig = () => g_config;
3583
3658
  var g_draw_offset_x = 0;
3584
3659
  var g_draw_offset_y = 0;
3660
+ var g_bg_active = false;
3661
+ var setBackgroundActive = (active) => {
3662
+ g_bg_active = active;
3663
+ };
3585
3664
  var getDrawOffset = () => ({
3586
3665
  x: g_draw_offset_x,
3587
3666
  y: g_draw_offset_y
@@ -3731,7 +3810,7 @@ var drawHeader = () => {
3731
3810
  const { stepWidth, stepsPerBar } = g_config;
3732
3811
  g_header_ctx.save();
3733
3812
  g_header_ctx.translate(-g_draw_offset_x, 0);
3734
- g_header_ctx.fillStyle = "#0a0f1f";
3813
+ g_header_ctx.fillStyle = g_bg_active ? "rgba(10,15,31,0.55)" : "#0a0f1f";
3735
3814
  g_header_ctx.fillRect(
3736
3815
  g_draw_offset_x,
3737
3816
  0,
@@ -3777,7 +3856,7 @@ var drawGrid = (noteLengthSteps = 1) => {
3777
3856
  const octave = Math.floor(totalPitch / 12) - 1;
3778
3857
  const isC4Range = octave === 4;
3779
3858
  const screenY = y - g_draw_offset_y;
3780
- g_grid_ctx.fillStyle = isBlackKey ? "#080b16" : "#111628";
3859
+ g_grid_ctx.fillStyle = g_bg_active ? isBlackKey ? "rgba(8,11,22,0.55)" : "rgba(17,22,40,0.45)" : isBlackKey ? "#080b16" : "#111628";
3781
3860
  g_grid_ctx.fillRect(0, screenY, g_grid_canvas.width, keyHeight);
3782
3861
  if (isC4Range) {
3783
3862
  g_grid_ctx.fillStyle = "rgba(41,173,255,0.05)";
@@ -5349,6 +5428,18 @@ var DAW_CSS = `
5349
5428
  4px 4px 0 var(--c-black);
5350
5429
  overflow: hidden;
5351
5430
  }
5431
+ /* \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 */
5432
+ .dtm-roll::before {
5433
+ content: '';
5434
+ position: absolute;
5435
+ inset: 0;
5436
+ background-image: var(--dtm-roll-bg-image, none);
5437
+ background-size: cover;
5438
+ background-position: center;
5439
+ background-repeat: no-repeat;
5440
+ opacity: 0.4;
5441
+ pointer-events: none;
5442
+ }
5352
5443
  .dtm-vscroll {
5353
5444
  position: relative;
5354
5445
  width: 20px;
@@ -5721,9 +5812,14 @@ var DAW_CSS = `
5721
5812
  .dtm-modal-body {
5722
5813
  padding: 12px;
5723
5814
  overflow-y: auto;
5815
+ overflow-x: hidden;
5724
5816
  font-size: 13px;
5725
5817
  line-height: 1.6;
5726
5818
  }
5819
+ .dtm-modal-body * {
5820
+ max-width: 100%;
5821
+ box-sizing: border-box;
5822
+ }
5727
5823
  .dtm-modal-body a {
5728
5824
  color: var(--dtm-primary);
5729
5825
  text-decoration: underline;
@@ -6326,6 +6422,9 @@ var MGRoid_default = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYA
6326
6422
  // assets/MOTRoid.png
6327
6423
  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";
6328
6424
 
6425
+ // assets/NYNRoid.png
6426
+ var NYNRoid_default = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABo0lEQVR42tWXoU8DMRTGf7csCAQBOcz4A6aOIOZAETKLBsQGCCwhiIWQCUKCRDAyAWgcWQgoPOHU/gBmOAkJggRTRPeSa7m7lSHWPdNc23u97+v3vvYCrCiVSir5HMdxwAjhmqfAmCPI+uIURKnPcRxjIc1d0GZi7AwU7Y63qyMA5reODaR268qEnc9fDdhaaDabAERRZLRhGDollnmtVsvvKihmqb3b7QJQq9WcGOh0OgDU63Xj/SyN+KeBMAxV2h4K8peTbQAWDy9HGpeIoiiYDA3Y6l9Z29NI788NpII8WN0FYLmgU0ZDfMJ/BiSU0tI4m57RHZWqRn5XzZ33ZDmn/wwk1KmSKg4C3a0eLvIzPbYB2P/61H7wW0ue3wdSvFxZ9Wvu+YARUb/tH1l1PzkMiBYkxOMldpZuAWg/r6eeCa5r+s+A3AuknjcrNwDMvZ4C8L5wAMB1b8Ood7kHpGjFfwZU0tkajYaxt6Ly3tSsNsbvj/QqGbwvPpLFtscMWHtnnxGuMezPyv8q+AdylzX8ZsDJIfv9vjFYLpf/lHvsDPwA5Wy6PgZtokAAAAAASUVORK5CYII=";
6427
+
6329
6428
  // assets/puyuyu.png
6330
6429
  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=";
6331
6430
 
@@ -6361,7 +6460,8 @@ var VOICE_IMAGES = {
6361
6460
  tsukuyomi: tsukuyomi_default,
6362
6461
  rei: rei_default,
6363
6462
  MGRoid: MGRoid_default,
6364
- MOTRoid: MOTRoid_default
6463
+ MOTRoid: MOTRoid_default,
6464
+ NYNRoid: NYNRoid_default
6365
6465
  };
6366
6466
  var FALLBACK_VOCAL_ICON = Chip_default;
6367
6467
 
@@ -7594,12 +7694,12 @@ var KOE_INFO_HTML = `
7594
7694
 
7595
7695
  <h4>3. DTM\u3067\u4F7F\u3046</h4>
7596
7696
  <ul>
7597
- <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>
7598
- <li>\u300C\u97F3\u6E90URL\u300D\u306B\u624B\u98062\u306E\u76F4\u63A5\u30EA\u30F3\u30AF\u3092\u8CBC\u308A\u4ED8\u3051\u307E\u3059\u3002</li>
7599
- <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>
7697
+ <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>
7698
+ <li>\u300C\u97F3\u58F0URL\u300D\u306B\u624B\u98062\u306E\u76F4\u63A5\u30EA\u30F3\u30AF\u3092\u8CBC\u308A\u4ED8\u3051\u307E\u3059\u3002</li>
7699
+ <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>
7600
7700
  <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>
7601
7701
  </ul>
7602
- <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>
7702
+ <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>
7603
7703
  </div>
7604
7704
  `;
7605
7705
  var BASE_STEP_WIDTH = 0.5;
@@ -7750,6 +7850,18 @@ var BASE_LYRIC_MODEL_LABELS = {
7750
7850
  var lyricModelLabel = (model, customMap) => BASE_LYRIC_MODEL_LABELS[model] ?? customMap.get(model)?.label ?? model;
7751
7851
  var CUSTOM_VOCAL_ADD_VALUE = "+custom";
7752
7852
  var CUSTOM_VOCAL_KEY_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
7853
+ var deriveCustomVocalKeyFromUrl = (url2) => {
7854
+ let name;
7855
+ try {
7856
+ const path = new URL(url2).pathname;
7857
+ name = decodeURIComponent(path.slice(path.lastIndexOf("/") + 1));
7858
+ } catch {
7859
+ return "";
7860
+ }
7861
+ name = name.replace(/\.[^.]*$/, "").normalize("NFKC").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
7862
+ if (name && /^[0-9]/.test(name)) name = `_${name}`;
7863
+ return CUSTOM_VOCAL_KEY_RE.test(name) ? name : "";
7864
+ };
7753
7865
  var clamp3 = (v, min, max) => Math.min(Math.max(v, min), max);
7754
7866
  var normalizeInstrumentName = (name) => {
7755
7867
  if (!name) return "";
@@ -7767,13 +7879,16 @@ var mountDAW = (target, options = {}) => {
7767
7879
  const drumPatterns = options.drumPatterns ?? DRUM_PATTERNS;
7768
7880
  const showMidi = !!options.parseMidi;
7769
7881
  const showChord = !isAdvanced;
7882
+ const midiSearchClient = options.midiSearch ? new MidiSearchClient(options.midiSearch) : void 0;
7883
+ const showMidiSearch = !!midiSearchClient?.enabled;
7770
7884
  const refs = buildUI(target, {
7771
7885
  tracks: trackConfigs,
7772
7886
  drumPatternNames: Object.keys(drumPatterns),
7773
7887
  defaultDrumPattern: drumPatterns.dance ? "dance" : Object.keys(drumPatterns)[0] ?? "none",
7774
7888
  defaultBpm: options.defaultBpm ?? DEFAULT_BPM,
7775
7889
  showMidi,
7776
- showChord
7890
+ showChord,
7891
+ showMidiSearch
7777
7892
  });
7778
7893
  const renderConfig = {
7779
7894
  stepsPerBar: 192,
@@ -8386,6 +8501,28 @@ var mountDAW = (target, options = {}) => {
8386
8501
  redrawAll();
8387
8502
  }
8388
8503
  };
8504
+ const ROLL_BG_STORAGE_KEY = "dtm-piano-roll-bg";
8505
+ const resizeImageToDataUrl = (img, maxW, maxH) => {
8506
+ const scale = Math.min(1, maxW / img.width, maxH / img.height);
8507
+ const width = Math.max(1, Math.round(img.width * scale));
8508
+ const height = Math.max(1, Math.round(img.height * scale));
8509
+ const canvas = document.createElement("canvas");
8510
+ canvas.width = width;
8511
+ canvas.height = height;
8512
+ const ctx = canvas.getContext("2d");
8513
+ if (!ctx) return img.src;
8514
+ ctx.drawImage(img, 0, 0, width, height);
8515
+ return canvas.toDataURL("image/jpeg", 0.7);
8516
+ };
8517
+ const applyRollBackground = (dataUrl) => {
8518
+ refs.rollContainer.style.setProperty(
8519
+ "--dtm-roll-bg-image",
8520
+ dataUrl ? `url(${dataUrl})` : "none"
8521
+ );
8522
+ refs.bgRemoveBtn.classList.toggle("dtm-hidden", !dataUrl);
8523
+ setBackgroundActive(!!dataUrl);
8524
+ redrawAll();
8525
+ };
8389
8526
  const setupCanvas = () => {
8390
8527
  const w = refs.rollContainer.clientWidth || 800;
8391
8528
  const h = refs.rollContainer.clientHeight || 450;
@@ -8751,10 +8888,8 @@ var mountDAW = (target, options = {}) => {
8751
8888
  <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>
8752
8889
  <button class="dtm-btn dtm-btn--ghost dtm-btn--xs" data-dtm="lyric-custom-guide">\u4F7F\u3044\u65B9\u30AC\u30A4\u30C9</button>
8753
8890
  </div>
8754
- <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">
8755
- <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">
8756
- <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">
8757
- <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">
8891
+ <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">
8892
+ <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">
8758
8893
  <div class="dtm-row">
8759
8894
  <span class="dtm-label dtm-grow" data-dtm="lyric-custom-note" style="color:var(--dtm-warn)"></span>
8760
8895
  <button class="dtm-btn dtm-btn--primary" data-dtm="lyric-custom-apply">\u8FFD\u52A0</button>
@@ -8819,12 +8954,6 @@ var mountDAW = (target, options = {}) => {
8819
8954
  const lyricCustomIcon = lyricDiv.querySelector(
8820
8955
  '[data-dtm="lyric-custom-icon"]'
8821
8956
  );
8822
- const lyricCustomAlias = lyricDiv.querySelector(
8823
- '[data-dtm="lyric-custom-alias"]'
8824
- );
8825
- const lyricCustomKey = lyricDiv.querySelector(
8826
- '[data-dtm="lyric-custom-key"]'
8827
- );
8828
8957
  const lyricCustomGuide = lyricDiv.querySelector(
8829
8958
  '[data-dtm="lyric-custom-guide"]'
8830
8959
  );
@@ -8845,7 +8974,7 @@ var mountDAW = (target, options = {}) => {
8845
8974
  for (const m of BASE_LYRIC_MODELS)
8846
8975
  addOpt(m, lyricModelLabel(m, customVocalsMap));
8847
8976
  for (const [k, def] of customVocalsMap) addOpt(k, def.label ?? k);
8848
- addOpt(CUSTOM_VOCAL_ADD_VALUE, "\u30AB\u30B9\u30BF\u30E0\u97F3\u6E90\u3092\u8FFD\u52A0\u2026");
8977
+ addOpt(CUSTOM_VOCAL_ADD_VALUE, "\u30AB\u30B9\u30BF\u30E0\u97F3\u58F0\u3092\u8FFD\u52A0\u2026");
8849
8978
  if (active.lyricModel && !BASE_LYRIC_MODELS.includes(active.lyricModel) && !customVocalsMap.has(active.lyricModel)) {
8850
8979
  addOpt(
8851
8980
  active.lyricModel,
@@ -8917,9 +9046,10 @@ var mountDAW = (target, options = {}) => {
8917
9046
  lyricModelSel.addEventListener("change", () => {
8918
9047
  if (lyricModelSel.value === CUSTOM_VOCAL_ADD_VALUE) {
8919
9048
  lyricCustomNote.textContent = "";
8920
- lyricCustomAlias.value = "";
8921
- lyricCustomKey.value = "";
8922
9049
  lyricCustom.classList.remove("dtm-hidden");
9050
+ lyricIcon.removeAttribute("src");
9051
+ lyricIcon.classList.add("dtm-hidden");
9052
+ lyricTerms.classList.add("dtm-hidden");
8923
9053
  return;
8924
9054
  }
8925
9055
  lyricCustom.classList.add("dtm-hidden");
@@ -8929,45 +9059,41 @@ var mountDAW = (target, options = {}) => {
8929
9059
  fireLyricsChange(active);
8930
9060
  });
8931
9061
  lyricCustomGuide.addEventListener("click", () => {
8932
- showModal("\u30AB\u30B9\u30BF\u30E0\u97F3\u6E90(.koe)\u306E\u4F7F\u3044\u65B9", KOE_INFO_HTML);
9062
+ showModal("\u30AB\u30B9\u30BF\u30E0\u97F3\u58F0(.koe)\u306E\u4F7F\u3044\u65B9", KOE_INFO_HTML);
8933
9063
  });
8934
9064
  lyricCustomApply.addEventListener("click", () => {
8935
9065
  const src = lyricCustomSrc.value.trim();
8936
9066
  if (!isValidHttpUrl(src)) {
8937
- lyricCustomNote.textContent = "\u97F3\u6E90URL\u304C\u4E0D\u6B63\u3067\u3059\uFF08http/https\u306E\u307F\u30FB2048\u6587\u5B57\u307E\u3067\uFF09";
9067
+ lyricCustomNote.textContent = "\u97F3\u58F0URL\u304C\u4E0D\u6B63\u3067\u3059\uFF08http/https\u306E\u307F\u30FB2048\u6587\u5B57\u307E\u3067\uFF09";
8938
9068
  return;
8939
9069
  }
8940
9070
  const iconRaw = lyricCustomIcon.value.trim();
8941
9071
  const iconUrl = isValidHttpUrl(iconRaw) ? iconRaw : "";
8942
- const alias = lyricCustomAlias.value.trim();
8943
9072
  const existing = [...customVocalsMap.values()].find(
8944
9073
  (d) => d.url === src
8945
9074
  );
8946
- const keyRaw = lyricCustomKey.value.trim().toLowerCase();
8947
9075
  let key;
8948
- if (keyRaw) {
8949
- if (!CUSTOM_VOCAL_KEY_RE.test(keyRaw)) {
8950
- 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";
8951
- return;
8952
- }
8953
- if (BASE_LYRIC_MODELS.includes(keyRaw)) {
8954
- lyricCustomNote.textContent = "\u305D\u306E\u8B58\u5225\u5B50\u306F\u5185\u8535\u30E2\u30C7\u30EB\u540D\u3068\u885D\u7A81\u3057\u3066\u3044\u307E\u3059";
8955
- return;
8956
- }
8957
- const conflict = customVocalsMap.get(keyRaw);
8958
- if (conflict && conflict.url !== src) {
8959
- lyricCustomNote.textContent = "\u305D\u306E\u8B58\u5225\u5B50\u306F\u5225\u306E\u97F3\u6E90\u306B\u4F7F\u7528\u6E08\u307F\u3067\u3059";
8960
- return;
8961
- }
8962
- key = keyRaw;
9076
+ if (existing) {
9077
+ key = existing.key;
8963
9078
  } else {
8964
- key = existing?.key ?? genCustomVocalKey();
9079
+ const derived = deriveCustomVocalKeyFromUrl(src);
9080
+ if (derived && !BASE_LYRIC_MODELS.includes(derived)) {
9081
+ if (!customVocalsMap.has(derived)) {
9082
+ key = derived;
9083
+ } else {
9084
+ let n = 2;
9085
+ while (customVocalsMap.has(`${derived}${n}`)) n++;
9086
+ key = `${derived}${n}`;
9087
+ }
9088
+ } else {
9089
+ key = genCustomVocalKey();
9090
+ }
8965
9091
  }
8966
9092
  registerCustomVocal({
8967
9093
  key,
8968
9094
  iconUrl: iconUrl || (existing?.iconUrl ?? ""),
8969
9095
  url: src,
8970
- label: alias || existing?.label
9096
+ label: existing?.label
8971
9097
  });
8972
9098
  active.lyricModel = key;
8973
9099
  fireLyricsChange(active);
@@ -9468,9 +9594,10 @@ var mountDAW = (target, options = {}) => {
9468
9594
  refs.playBtn.addEventListener("click", togglePlay);
9469
9595
  refs.playBtn.disabled = false;
9470
9596
  refs.prevBarBtn.addEventListener("click", () => {
9597
+ const barsBack = playbackState === "playing" ? 2 : 1;
9471
9598
  const targetStep = Math.max(
9472
9599
  0,
9473
- Math.floor((getCurrentPlayStep() - 1) / renderConfig.stepsPerBar) * renderConfig.stepsPerBar
9600
+ (Math.floor((getCurrentPlayStep() - 1) / renderConfig.stepsPerBar) - barsBack) * renderConfig.stepsPerBar
9474
9601
  );
9475
9602
  jumpTo(targetStep);
9476
9603
  });
@@ -9515,6 +9642,35 @@ var mountDAW = (target, options = {}) => {
9515
9642
  applyZoomY();
9516
9643
  notifyViewState();
9517
9644
  });
9645
+ refs.bgUploadBtn.addEventListener("click", () => refs.bgFileInput.click());
9646
+ refs.bgFileInput.addEventListener("change", () => {
9647
+ const file = refs.bgFileInput.files?.[0];
9648
+ refs.bgFileInput.value = "";
9649
+ if (!file) return;
9650
+ const reader = new FileReader();
9651
+ reader.onload = () => {
9652
+ const img = new Image();
9653
+ img.onload = () => {
9654
+ const maxW = refs.rollContainer.clientWidth || 800;
9655
+ const maxH = refs.rollContainer.clientHeight || 450;
9656
+ const dataUrl = resizeImageToDataUrl(img, maxW, maxH);
9657
+ try {
9658
+ localStorage.setItem(ROLL_BG_STORAGE_KEY, dataUrl);
9659
+ } catch {
9660
+ }
9661
+ applyRollBackground(dataUrl);
9662
+ };
9663
+ img.src = reader.result;
9664
+ };
9665
+ reader.readAsDataURL(file);
9666
+ });
9667
+ refs.bgRemoveBtn.addEventListener("click", () => {
9668
+ try {
9669
+ localStorage.removeItem(ROLL_BG_STORAGE_KEY);
9670
+ } catch {
9671
+ }
9672
+ applyRollBackground(null);
9673
+ });
9518
9674
  refs.decomposeChordToggle.addEventListener("change", notifyViewState);
9519
9675
  refs.ignoreChordHeavyToggle.addEventListener("change", notifyViewState);
9520
9676
  refs.masterVolume.addEventListener("input", () => {
@@ -9625,6 +9781,7 @@ var mountDAW = (target, options = {}) => {
9625
9781
  };
9626
9782
  showModal = (title, bodyHTML) => {
9627
9783
  collapseActiveSample();
9784
+ collapseSearchPreview();
9628
9785
  refs.modalTitle.textContent = title;
9629
9786
  refs.modalBody.innerHTML = bodyHTML;
9630
9787
  refs.modalOverlay.removeAttribute("hidden");
@@ -9713,11 +9870,13 @@ var mountDAW = (target, options = {}) => {
9713
9870
  };
9714
9871
  refs.modalClose.addEventListener("click", () => {
9715
9872
  collapseActiveSample();
9873
+ collapseSearchPreview();
9716
9874
  refs.modalOverlay.setAttribute("hidden", "");
9717
9875
  });
9718
9876
  refs.modalOverlay.addEventListener("click", (e) => {
9719
9877
  if (e.target === refs.modalOverlay) {
9720
9878
  collapseActiveSample();
9879
+ collapseSearchPreview();
9721
9880
  refs.modalOverlay.setAttribute("hidden", "");
9722
9881
  }
9723
9882
  });
@@ -9738,6 +9897,7 @@ var mountDAW = (target, options = {}) => {
9738
9897
  })
9739
9898
  );
9740
9899
  if (showMidi) wireMidi();
9900
+ if (showMidiSearch) wireMidiSearch();
9741
9901
  document.addEventListener("keydown", onKeyDown);
9742
9902
  refs.root.addEventListener("keydown", (e) => {
9743
9903
  const t = e.target;
@@ -9802,6 +9962,236 @@ var mountDAW = (target, options = {}) => {
9802
9962
  overlayDuring(() => applyMidiSelection(pendingMidi, selected));
9803
9963
  });
9804
9964
  };
9965
+ const buildPreviewMml = (midi) => {
9966
+ const analysis = analyzeMidiTracks(midi);
9967
+ const selected = analysis.filter((a) => a.selected).map((a) => a.index);
9968
+ const { placements, bpm: parsedBpm } = extractMidiPlacements(
9969
+ midi,
9970
+ selected
9971
+ );
9972
+ const byTrack = /* @__PURE__ */ new Map();
9973
+ for (const p of placements) {
9974
+ if (!byTrack.has(p.trackId)) byTrack.set(p.trackId, []);
9975
+ byTrack.get(p.trackId)?.push(p);
9976
+ }
9977
+ const refCore = trackStates[0].core;
9978
+ const tempo = Math.round(parsedBpm) || bpm;
9979
+ const lines = [];
9980
+ let i = 0;
9981
+ for (const notes of byTrack.values()) {
9982
+ const asNotes = notes.map((p) => ({
9983
+ id: 0,
9984
+ startStep: p.startStep,
9985
+ durationSteps: p.durationSteps,
9986
+ pitch: p.pitch,
9987
+ velocity: p.velocity
9988
+ }));
9989
+ const mml = refCore.getMMLFromNotes(asNotes, tempo, 100).trim();
9990
+ lines.push(`@${i} ${mml}`);
9991
+ i++;
9992
+ }
9993
+ return [...lines, MML_END_MARKER].join(";\n");
9994
+ };
9995
+ const fetchVerifiedMidi = async (fileName) => {
9996
+ if (!options.parseMidi || !midiSearchClient) {
9997
+ throw new Error("parseMidi/midiSearchClient not injected");
9998
+ }
9999
+ const midiBytes = await midiSearchClient.fetchMidi(fileName);
10000
+ const midi = await options.parseMidi(new Uint8Array(midiBytes));
10001
+ const analysis = analyzeMidiTracks(midi);
10002
+ const selected = analysis.filter((a) => a.selected).map((a) => a.index);
10003
+ if (!isPlausibleMidiTranscription(midi, selected)) {
10004
+ throw new Error("implausible MIDI transcription");
10005
+ }
10006
+ return midi;
10007
+ };
10008
+ let searchPreviewPlayer = null;
10009
+ let searchPreviewBtn = null;
10010
+ const collapseSearchPreview = () => {
10011
+ if (searchPreviewPlayer) {
10012
+ searchPreviewPlayer.stop();
10013
+ searchPreviewPlayer.destroy();
10014
+ searchPreviewPlayer = null;
10015
+ }
10016
+ if (searchPreviewBtn) {
10017
+ searchPreviewBtn.textContent = "\u25B6 \u8A66\u8074";
10018
+ searchPreviewBtn.classList.remove("dtm-btn--danger");
10019
+ searchPreviewBtn.classList.add("dtm-btn--ghost");
10020
+ const container = searchPreviewBtn.closest(".dtm-modal-sample-box")?.querySelector(".dtm-modal-sample-player-container");
10021
+ if (container) container.innerHTML = "";
10022
+ searchPreviewBtn = null;
10023
+ }
10024
+ };
10025
+ const wireMidiSearch = () => {
10026
+ if (!midiSearchClient) return;
10027
+ refs.midiSearchOpenBtn.addEventListener("click", () => {
10028
+ showModal(
10029
+ "MML\u691C\u7D22",
10030
+ `
10031
+ <div class="dtm-row" style="flex-wrap:nowrap">
10032
+ <input type="text" class="dtm-input dtm-grow" data-dtm="modal-midi-search-input" placeholder="\u66F2\u540D\u3067MML\u691C\u7D22" style="min-width:0">
10033
+ <button class="dtm-btn dtm-btn--primary" data-dtm="modal-midi-search-btn" style="flex-shrink:0">\u691C\u7D22</button>
10034
+ </div>
10035
+ <div class="dtm-row" data-dtm="modal-midi-search-results" style="flex-direction:column;align-items:stretch;gap:4px"></div>
10036
+ `
10037
+ );
10038
+ const input = refs.modalBody.querySelector(
10039
+ '[data-dtm="modal-midi-search-input"]'
10040
+ );
10041
+ const searchBtn = refs.modalBody.querySelector(
10042
+ '[data-dtm="modal-midi-search-btn"]'
10043
+ );
10044
+ const results = refs.modalBody.querySelector(
10045
+ '[data-dtm="modal-midi-search-results"]'
10046
+ );
10047
+ const runSearch = async () => {
10048
+ const q2 = input.value.trim();
10049
+ if (!q2) return;
10050
+ collapseSearchPreview();
10051
+ searchBtn.disabled = true;
10052
+ searchBtn.textContent = "\u691C\u7D22\u4E2D...";
10053
+ results.innerHTML = "";
10054
+ try {
10055
+ const songs = await midiSearchClient.searchSongs({ title: q2 });
10056
+ if (songs.length === 0) {
10057
+ results.innerHTML = '<span class="dtm-label" style="color:var(--dtm-warn)">\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F</span>';
10058
+ return;
10059
+ }
10060
+ for (const song of songs) {
10061
+ const box = document.createElement("div");
10062
+ box.className = "dtm-modal-sample-box";
10063
+ const row = document.createElement("div");
10064
+ row.className = "dtm-row";
10065
+ row.style.cssText = "flex-wrap:nowrap;gap:4px;padding:2px 0";
10066
+ const label = document.createElement("span");
10067
+ label.className = "dtm-label";
10068
+ label.style.cssText = "flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap";
10069
+ label.textContent = `${song.title} - ${song.user}`;
10070
+ const previewBtn = document.createElement("button");
10071
+ previewBtn.className = "dtm-btn dtm-btn--ghost";
10072
+ previewBtn.style.cssText = "flex-shrink:0";
10073
+ previewBtn.textContent = "\u25B6 \u8A66\u8074";
10074
+ const loadBtn = document.createElement("button");
10075
+ loadBtn.className = "dtm-btn dtm-btn--primary";
10076
+ loadBtn.style.cssText = "flex-shrink:0";
10077
+ loadBtn.textContent = "\u8AAD\u307F\u8FBC\u307F";
10078
+ const playerContainer = document.createElement("div");
10079
+ playerContainer.className = "dtm-modal-sample-player-container";
10080
+ previewBtn.addEventListener("click", async () => {
10081
+ if (searchPreviewBtn === previewBtn) {
10082
+ if (searchPreviewPlayer?.isPlaying()) {
10083
+ searchPreviewPlayer.stop();
10084
+ } else {
10085
+ stop();
10086
+ previewBtn.textContent = "\u8AAD\u8FBC\u4E2D...";
10087
+ try {
10088
+ const midi = await fetchVerifiedMidi(song.file);
10089
+ const mml = buildPreviewMml(midi);
10090
+ playerContainer.innerHTML = "";
10091
+ const player = mountMmlPlayer(playerContainer, mml, {
10092
+ onPlayNote: options.onPlayNote,
10093
+ onPlayDrum: options.onPlayDrum,
10094
+ onResumeAudio: options.onResumeAudio,
10095
+ getAudioTime: options.getAudioTime,
10096
+ singingVoices: options.singingVoices,
10097
+ drumPatterns: options.drumPatterns,
10098
+ volume: masterVolume,
10099
+ _skipInfoModals: true,
10100
+ onStop: () => {
10101
+ if (searchPreviewBtn === previewBtn) {
10102
+ previewBtn.textContent = "\u25B6 \u8A66\u8074";
10103
+ previewBtn.classList.remove("dtm-btn--danger");
10104
+ previewBtn.classList.add("dtm-btn--ghost");
10105
+ }
10106
+ }
10107
+ });
10108
+ searchPreviewPlayer = player;
10109
+ searchPreviewBtn = previewBtn;
10110
+ player.play();
10111
+ previewBtn.textContent = "\u25A0 \u505C\u6B62";
10112
+ previewBtn.classList.remove("dtm-btn--ghost");
10113
+ previewBtn.classList.add("dtm-btn--danger");
10114
+ } catch (e) {
10115
+ console.error("[dtm] MIDI preview failed", e);
10116
+ previewBtn.textContent = "\u5931\u6557";
10117
+ }
10118
+ }
10119
+ } else {
10120
+ collapseSearchPreview();
10121
+ stop();
10122
+ previewBtn.textContent = "\u8AAD\u8FBC\u4E2D...";
10123
+ try {
10124
+ const midi = await fetchVerifiedMidi(song.file);
10125
+ const mml = buildPreviewMml(midi);
10126
+ playerContainer.innerHTML = "";
10127
+ const player = mountMmlPlayer(playerContainer, mml, {
10128
+ onPlayNote: options.onPlayNote,
10129
+ onPlayDrum: options.onPlayDrum,
10130
+ onResumeAudio: options.onResumeAudio,
10131
+ getAudioTime: options.getAudioTime,
10132
+ singingVoices: options.singingVoices,
10133
+ drumPatterns: options.drumPatterns,
10134
+ volume: masterVolume,
10135
+ _skipInfoModals: true,
10136
+ onStop: () => {
10137
+ if (searchPreviewBtn === previewBtn) {
10138
+ previewBtn.textContent = "\u25B6 \u8A66\u8074";
10139
+ previewBtn.classList.remove("dtm-btn--danger");
10140
+ previewBtn.classList.add("dtm-btn--ghost");
10141
+ }
10142
+ }
10143
+ });
10144
+ searchPreviewPlayer = player;
10145
+ searchPreviewBtn = previewBtn;
10146
+ player.play();
10147
+ previewBtn.textContent = "\u25A0 \u505C\u6B62";
10148
+ previewBtn.classList.remove("dtm-btn--ghost");
10149
+ previewBtn.classList.add("dtm-btn--danger");
10150
+ } catch (e) {
10151
+ console.error("[dtm] MIDI preview failed", e);
10152
+ previewBtn.textContent = "\u5931\u6557";
10153
+ }
10154
+ }
10155
+ });
10156
+ loadBtn.addEventListener("click", async () => {
10157
+ loadBtn.disabled = true;
10158
+ loadBtn.textContent = "\u8AAD\u8FBC\u4E2D...";
10159
+ try {
10160
+ const pending = await fetchVerifiedMidi(song.file);
10161
+ const analysis = analyzeMidiTracks(pending);
10162
+ const sel = analysis.filter((a) => a.selected).map((a) => a.index);
10163
+ collapseSearchPreview();
10164
+ overlayDuring(() => applyMidiSelection(pending, sel));
10165
+ refs.modalOverlay.setAttribute("hidden", "");
10166
+ } catch (e) {
10167
+ console.error("[dtm] MIDI fetch failed", e);
10168
+ loadBtn.textContent = "\u5931\u6557";
10169
+ } finally {
10170
+ loadBtn.disabled = false;
10171
+ }
10172
+ });
10173
+ row.appendChild(label);
10174
+ row.appendChild(previewBtn);
10175
+ row.appendChild(loadBtn);
10176
+ box.appendChild(row);
10177
+ box.appendChild(playerContainer);
10178
+ results.appendChild(box);
10179
+ }
10180
+ } catch (e) {
10181
+ console.error("[dtm] MIDI search failed", e);
10182
+ results.innerHTML = '<span class="dtm-label" style="color:var(--dtm-warn)">\u691C\u7D22\u306B\u5931\u6557\u3057\u307E\u3057\u305F</span>';
10183
+ } finally {
10184
+ searchBtn.disabled = false;
10185
+ searchBtn.textContent = "\u691C\u7D22";
10186
+ }
10187
+ };
10188
+ searchBtn.addEventListener("click", () => void runSearch());
10189
+ input.addEventListener("keydown", (e) => {
10190
+ if (e.key === "Enter") void runSearch();
10191
+ });
10192
+ input.focus();
10193
+ });
10194
+ };
9805
10195
  const onKeyDown = (e) => {
9806
10196
  if (!(e.ctrlKey || e.metaKey)) return;
9807
10197
  if (e.code === "KeyZ" && !e.shiftKey) {
@@ -9857,6 +10247,11 @@ var mountDAW = (target, options = {}) => {
9857
10247
  updateUndoRedo();
9858
10248
  redrawAll();
9859
10249
  if (options.initialMML) loadMML(options.initialMML);
10250
+ try {
10251
+ const storedBg = localStorage.getItem(ROLL_BG_STORAGE_KEY);
10252
+ if (storedBg) applyRollBackground(storedBg);
10253
+ } catch {
10254
+ }
9860
10255
  let resizeTimer = null;
9861
10256
  const resizeObserver = new ResizeObserver(() => {
9862
10257
  if (resizeTimer) clearTimeout(resizeTimer);
@@ -11177,13 +11572,17 @@ var createDtmStudio = async (options = {}) => {
11177
11572
  const mountedPlayers = [];
11178
11573
  const mountedModeSwitches = [];
11179
11574
  const mountEditor = (target, opts = {}) => {
11575
+ const mergedOpts = {
11576
+ midiSearch: options.midiSearch,
11577
+ ...opts
11578
+ };
11180
11579
  const {
11181
11580
  preset,
11182
11581
  presetUI,
11183
11582
  onInstrumentChange,
11184
11583
  onTrackInstrumentChange: externalOnTrackInstrumentChange,
11185
11584
  ...dawOverrides
11186
- } = opts;
11585
+ } = mergedOpts;
11187
11586
  const tracks = dawOverrides.tracks ?? TRACKS_SIMPLE;
11188
11587
  const trackIds = tracks.map((t) => t.id);
11189
11588
  const presetKey = preset && INSTRUMENT_PRESETS[preset] ? preset : defaultPreset;
@@ -11518,6 +11917,7 @@ export {
11518
11917
  MAX_VOCAL_VOLUME,
11519
11918
  MMLCore,
11520
11919
  MML_END_MARKER,
11920
+ MidiSearchClient,
11521
11921
  PITCH_MAP,
11522
11922
  PREWARM_NOTES,
11523
11923
  TRACKS_ADVANCED,
@@ -11567,6 +11967,7 @@ export {
11567
11967
  init,
11568
11968
  injectStyles,
11569
11969
  isChordHeavyTrack,
11970
+ isPlausibleMidiTranscription,
11570
11971
  isValidHttpUrl,
11571
11972
  koeUrl,
11572
11973
  mountDAW,
@@ -11580,6 +11981,7 @@ export {
11580
11981
  parseMmlMeta,
11581
11982
  playMML,
11582
11983
  playSingingMML,
11984
+ setBackgroundActive,
11583
11985
  setDrawOffset,
11584
11986
  shiftNotes,
11585
11987
  showLoadingOverlay,