@onjmin/dtm 0.1.51 → 0.1.55

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,16 @@ 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>
1308
+ <div class="dtm-row dtm-hidden" data-dtm="bg-opacity-row">
1309
+ <span class="dtm-label">\u80CC\u666F\u4E0D\u900F\u660E\u5EA6</span>
1310
+ <input type="range" min="0" max="100" value="40" data-dtm="bg-opacity" class="dtm-slider">
1311
+ </div>
1296
1312
  </div>
1297
1313
  </details>
1298
1314
 
@@ -1331,6 +1347,9 @@ var buildUI = (target, options) => {
1331
1347
  <textarea class="dtm-textarea dtm-grow" data-dtm="mml-input" placeholder="MML\u3092\u5165\u529B"></textarea>
1332
1348
  <button class="dtm-btn dtm-btn--primary" data-dtm="mml-load" style="flex-shrink:0">\u8AAD\u8FBC</button>
1333
1349
  </div>
1350
+ <div class="dtm-row" data-dtm="midi-search-row" style="justify-content:flex-end;${showMidiSearch ? "" : "display:none"}">
1351
+ <button class="dtm-btn dtm-btn--primary" data-dtm="midi-search-open">MML\u691C\u7D22</button>
1352
+ </div>
1334
1353
  <p class="dtm-load-note dtm-hidden" data-dtm="mml-load-note"></p>
1335
1354
  </div>
1336
1355
  </details>
@@ -1441,6 +1460,11 @@ var buildUI = (target, options) => {
1441
1460
  zoomXOut: sel("zoomx-out"),
1442
1461
  zoomYIn: sel("zoomy-in"),
1443
1462
  zoomYOut: sel("zoomy-out"),
1463
+ bgFileInput: sel("bg-file-input"),
1464
+ bgUploadBtn: sel("bg-upload"),
1465
+ bgRemoveBtn: sel("bg-remove"),
1466
+ bgOpacityInput: sel("bg-opacity"),
1467
+ bgOpacityRow: sel("bg-opacity-row"),
1444
1468
  rollContainer: sel("roll"),
1445
1469
  wrapper: sel("wrapper"),
1446
1470
  vScroll: sel("vscroll"),
@@ -1459,6 +1483,7 @@ var buildUI = (target, options) => {
1459
1483
  midiInfoBtn: sel("midi-info"),
1460
1484
  midiTrackSelection: sel("midi-track-selection"),
1461
1485
  midiPanel: sel("midi-panel"),
1486
+ midiSearchOpenBtn: sel("midi-search-open"),
1462
1487
  mmlInput: sel("mml-input"),
1463
1488
  mmlLoadBtn: sel("mml-load"),
1464
1489
  mmlLoadNote: sel("mml-load-note"),
@@ -2504,7 +2529,8 @@ var KOE_VOICEBANKS = {
2504
2529
  shiyo: "\u9769\u547D\u30B7\u30E8.koe",
2505
2530
  rei: "\u8DB3\u7ACB\u30EC\u30A4ver3.5.0.koe",
2506
2531
  mgroid: "MGRoid_\u539F\u97F3\u8A2D\u5B9A\u6E08\u307F.koe",
2507
- motroid: "MOTRoid\u5B8C\u5168\u7248V2.koe"
2532
+ motroid: "MOTRoid\u5B8C\u5168\u7248V2.koe",
2533
+ nynroid: "NYNRoidver1.4.koe"
2508
2534
  };
2509
2535
  var KOE_VOICEBANK_LABELS = {
2510
2536
  tsukuyomi: "\u3064\u304F\u3088\u307F\u3061\u3083\u3093",
@@ -2516,7 +2542,8 @@ var KOE_VOICEBANK_LABELS = {
2516
2542
  shiyo: "\u9769\u547D\u30B7\u30E8",
2517
2543
  rei: "\u8DB3\u7ACB\u30EC\u30A4",
2518
2544
  mgroid: "MGRoid",
2519
- motroid: "MOTRoid"
2545
+ motroid: "MOTRoid",
2546
+ nynroid: "NYNRoid"
2520
2547
  };
2521
2548
  var VOICE_IMAGE_KEY = {
2522
2549
  klatt: "puyuyu",
@@ -2529,7 +2556,8 @@ var VOICE_IMAGE_KEY = {
2529
2556
  shiyo: "shiyo",
2530
2557
  rei: "rei",
2531
2558
  mgroid: "MGRoid",
2532
- motroid: "MOTRoid"
2559
+ motroid: "MOTRoid",
2560
+ nynroid: "NYNRoid"
2533
2561
  };
2534
2562
  var KOE_VOICEBANK_TERMS = {
2535
2563
  tsukuyomi: "https://tyc.rei-yumesaki.net/material/utau/terms/",
@@ -2541,7 +2569,8 @@ var KOE_VOICEBANK_TERMS = {
2541
2569
  shiyo: "https://kakumeisiyo.my.canva.site/dagkuyjwycs",
2542
2570
  rei: "https://mechanicalgirl.jp/guidelines/",
2543
2571
  mgroid: "https://x.com/nisusansu/status/1048825378188353536",
2544
- motroid: "https://www.nicovideo.jp/watch/sm40031282"
2572
+ motroid: "https://www.nicovideo.jp/watch/sm40031282",
2573
+ nynroid: "https://www.bilibili.com/video/BV1V24y1a7qs"
2545
2574
  };
2546
2575
  var koeUrl = (name, base = KOE_BASE_URL) => `${base}/${encodeURIComponent(name)}`;
2547
2576
  var DEFAULT_WORLDLINE_SCRIPT = "https://onjmin.github.io/koe/demo/world/worldline.js";
@@ -3365,6 +3394,19 @@ var extractMidiPlacements = (midi, selectedTrackIndices) => {
3365
3394
  }
3366
3395
  return { placements, bpm };
3367
3396
  };
3397
+ var isPlausibleMidiTranscription = (midi, selectedIndices) => {
3398
+ const { placements } = extractMidiPlacements(midi, selectedIndices);
3399
+ if (placements.length < 4) return false;
3400
+ const shortCount = placements.filter((p) => p.durationSteps <= 1).length;
3401
+ const shortRatio = shortCount / placements.length;
3402
+ const countByStep = /* @__PURE__ */ new Map();
3403
+ for (const p of placements) {
3404
+ countByStep.set(p.startStep, (countByStep.get(p.startStep) ?? 0) + 1);
3405
+ }
3406
+ const denseSteps = [...countByStep.values()].filter((n) => n >= 6).length;
3407
+ const denseRatio = denseSteps / countByStep.size;
3408
+ return shortRatio < 0.6 && denseRatio < 0.35;
3409
+ };
3368
3410
  var extractMidiPlacementsByTrack = (midi, selectedIndices, trackIds) => {
3369
3411
  const { tracks, division } = midi;
3370
3412
  const ticksPerBeat = division;
@@ -3517,6 +3559,45 @@ var exportMIDI = (options) => {
3517
3559
  return new Blob([new Uint8Array(arr).buffer], { type: "audio/midi" });
3518
3560
  };
3519
3561
 
3562
+ // src/midi-search.ts
3563
+ var MidiSearchClient = class {
3564
+ config;
3565
+ constructor(config = {}) {
3566
+ this.config = config;
3567
+ }
3568
+ get enabled() {
3569
+ return !!this.config.apiKey;
3570
+ }
3571
+ get baseUrl() {
3572
+ return this.config.baseUrl ?? "https://rpgen-search.pages.dev/api";
3573
+ }
3574
+ headers() {
3575
+ const h = {};
3576
+ if (this.config.apiKey) {
3577
+ h.Authorization = `Bearer ${this.config.apiKey}`;
3578
+ }
3579
+ return h;
3580
+ }
3581
+ async searchSongs(params) {
3582
+ if (!this.enabled) return [];
3583
+ const qs = new URLSearchParams();
3584
+ if (params.title) qs.set("title", params.title);
3585
+ if (params.user) qs.set("user", params.user);
3586
+ if (params.twitter_id) qs.set("twitter_id", params.twitter_id);
3587
+ const url2 = `${this.baseUrl}/picotune/songs${qs.toString() ? `?${qs.toString()}` : ""}`;
3588
+ const res = await fetch(url2, { headers: this.headers() });
3589
+ if (!res.ok) throw new Error(`picotune search failed: ${res.status}`);
3590
+ const body = await res.json();
3591
+ return body.data ?? body.songs ?? body;
3592
+ }
3593
+ async fetchMidi(fileName) {
3594
+ const url2 = `${this.baseUrl}/picotune/songs/${encodeURIComponent(fileName)}`;
3595
+ const res = await fetch(url2, { headers: this.headers() });
3596
+ if (!res.ok) throw new Error(`picotune fetch failed: ${res.status}`);
3597
+ return res.arrayBuffer();
3598
+ }
3599
+ };
3600
+
3520
3601
  // src/linked-list.ts
3521
3602
  var LinkedList = class {
3522
3603
  #cursor;
@@ -3582,6 +3663,10 @@ var HEADER_HEIGHT = 20;
3582
3663
  var getRenderConfig = () => g_config;
3583
3664
  var g_draw_offset_x = 0;
3584
3665
  var g_draw_offset_y = 0;
3666
+ var g_bg_active = false;
3667
+ var setBackgroundActive = (active) => {
3668
+ g_bg_active = active;
3669
+ };
3585
3670
  var getDrawOffset = () => ({
3586
3671
  x: g_draw_offset_x,
3587
3672
  y: g_draw_offset_y
@@ -3731,7 +3816,7 @@ var drawHeader = () => {
3731
3816
  const { stepWidth, stepsPerBar } = g_config;
3732
3817
  g_header_ctx.save();
3733
3818
  g_header_ctx.translate(-g_draw_offset_x, 0);
3734
- g_header_ctx.fillStyle = "#0a0f1f";
3819
+ g_header_ctx.fillStyle = g_bg_active ? "rgba(10,15,31,0.55)" : "#0a0f1f";
3735
3820
  g_header_ctx.fillRect(
3736
3821
  g_draw_offset_x,
3737
3822
  0,
@@ -3777,7 +3862,7 @@ var drawGrid = (noteLengthSteps = 1) => {
3777
3862
  const octave = Math.floor(totalPitch / 12) - 1;
3778
3863
  const isC4Range = octave === 4;
3779
3864
  const screenY = y - g_draw_offset_y;
3780
- g_grid_ctx.fillStyle = isBlackKey ? "#080b16" : "#111628";
3865
+ g_grid_ctx.fillStyle = g_bg_active ? isBlackKey ? "rgba(8,11,22,0.55)" : "rgba(17,22,40,0.45)" : isBlackKey ? "#080b16" : "#111628";
3781
3866
  g_grid_ctx.fillRect(0, screenY, g_grid_canvas.width, keyHeight);
3782
3867
  if (isC4Range) {
3783
3868
  g_grid_ctx.fillStyle = "rgba(41,173,255,0.05)";
@@ -4482,6 +4567,7 @@ var parseMML = (mml, options = {}) => {
4482
4567
  tokenTracks: collectTokens ? tokenTracks : void 0,
4483
4568
  lyrics: collectLyrics ? /* @__PURE__ */ new Map() : void 0,
4484
4569
  mergedTrackCount: 0,
4570
+ trackVelocity: /* @__PURE__ */ new Map(),
4485
4571
  meta: {}
4486
4572
  };
4487
4573
  }
@@ -4501,6 +4587,7 @@ var parseMML = (mml, options = {}) => {
4501
4587
  let baseLength = 16;
4502
4588
  let velocity = 100;
4503
4589
  const contributors = /* @__PURE__ */ new Map();
4590
+ const trackVelocity = /* @__PURE__ */ new Map();
4504
4591
  const recordContributor = () => {
4505
4592
  let set = contributors.get(trackIndex);
4506
4593
  if (!set) {
@@ -4601,6 +4688,7 @@ var parseMML = (mml, options = {}) => {
4601
4688
  }
4602
4689
  } else if (ch === "v" && numStr) {
4603
4690
  velocity = clamp2(Number.parseInt(numStr, 10), 0, 127);
4691
+ trackVelocity.set(trackIndex, velocity);
4604
4692
  }
4605
4693
  pushTok("ctrl", currentStep, 0, tokStart);
4606
4694
  } else if (ch === "[") {
@@ -4690,6 +4778,7 @@ var parseMML = (mml, options = {}) => {
4690
4778
  tokenTracks: collectTokens ? tokenTracks : void 0,
4691
4779
  lyrics,
4692
4780
  mergedTrackCount,
4781
+ trackVelocity,
4693
4782
  meta
4694
4783
  };
4695
4784
  };
@@ -5349,6 +5438,18 @@ var DAW_CSS = `
5349
5438
  4px 4px 0 var(--c-black);
5350
5439
  overflow: hidden;
5351
5440
  }
5441
+ /* \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 */
5442
+ .dtm-roll::before {
5443
+ content: '';
5444
+ position: absolute;
5445
+ inset: 0;
5446
+ background-image: var(--dtm-roll-bg-image, none);
5447
+ background-size: cover;
5448
+ background-position: center;
5449
+ background-repeat: no-repeat;
5450
+ opacity: var(--dtm-roll-bg-opacity, 0.4);
5451
+ pointer-events: none;
5452
+ }
5352
5453
  .dtm-vscroll {
5353
5454
  position: relative;
5354
5455
  width: 20px;
@@ -5721,9 +5822,14 @@ var DAW_CSS = `
5721
5822
  .dtm-modal-body {
5722
5823
  padding: 12px;
5723
5824
  overflow-y: auto;
5825
+ overflow-x: hidden;
5724
5826
  font-size: 13px;
5725
5827
  line-height: 1.6;
5726
5828
  }
5829
+ .dtm-modal-body * {
5830
+ max-width: 100%;
5831
+ box-sizing: border-box;
5832
+ }
5727
5833
  .dtm-modal-body a {
5728
5834
  color: var(--dtm-primary);
5729
5835
  text-decoration: underline;
@@ -6260,7 +6366,10 @@ var createSynth = (ctx, destination = ctx.destination) => {
6260
6366
  osc.frequency.value = freqFromPitch(e.pitch);
6261
6367
  const t0 = ctx.currentTime + e.when;
6262
6368
  const peak = Math.max(1e-4, 0.06 * e.volume * 1.5);
6369
+ const releaseTime = Math.min(0.02, e.duration * 0.1);
6370
+ const sustainDuration = e.duration - releaseTime;
6263
6371
  gain.gain.setValueAtTime(peak, t0);
6372
+ gain.gain.setValueAtTime(peak, t0 + sustainDuration);
6264
6373
  gain.gain.exponentialRampToValueAtTime(1e-3, t0 + e.duration);
6265
6374
  osc.connect(gain);
6266
6375
  if (typeof ctx.createStereoPanner === "function" && e.pan) {
@@ -6326,6 +6435,9 @@ var MGRoid_default = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYA
6326
6435
  // assets/MOTRoid.png
6327
6436
  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
6437
 
6438
+ // assets/NYNRoid.png
6439
+ var NYNRoid_default = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABo0lEQVR42tWXoU8DMRTGf7csCAQBOcz4A6aOIOZAETKLBsQGCCwhiIWQCUKCRDAyAWgcWQgoPOHU/gBmOAkJggRTRPeSa7m7lSHWPdNc23u97+v3vvYCrCiVSir5HMdxwAjhmqfAmCPI+uIURKnPcRxjIc1d0GZi7AwU7Y63qyMA5reODaR268qEnc9fDdhaaDabAERRZLRhGDollnmtVsvvKihmqb3b7QJQq9WcGOh0OgDU63Xj/SyN+KeBMAxV2h4K8peTbQAWDy9HGpeIoiiYDA3Y6l9Z29NI788NpII8WN0FYLmgU0ZDfMJ/BiSU0tI4m57RHZWqRn5XzZ33ZDmn/wwk1KmSKg4C3a0eLvIzPbYB2P/61H7wW0ue3wdSvFxZ9Wvu+YARUb/tH1l1PzkMiBYkxOMldpZuAWg/r6eeCa5r+s+A3AuknjcrNwDMvZ4C8L5wAMB1b8Ood7kHpGjFfwZU0tkajYaxt6Ly3tSsNsbvj/QqGbwvPpLFtscMWHtnnxGuMezPyv8q+AdylzX8ZsDJIfv9vjFYLpf/lHvsDPwA5Wy6PgZtokAAAAAASUVORK5CYII=";
6440
+
6329
6441
  // assets/puyuyu.png
6330
6442
  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
6443
 
@@ -6361,7 +6473,8 @@ var VOICE_IMAGES = {
6361
6473
  tsukuyomi: tsukuyomi_default,
6362
6474
  rei: rei_default,
6363
6475
  MGRoid: MGRoid_default,
6364
- MOTRoid: MOTRoid_default
6476
+ MOTRoid: MOTRoid_default,
6477
+ NYNRoid: NYNRoid_default
6365
6478
  };
6366
6479
  var FALLBACK_VOCAL_ICON = Chip_default;
6367
6480
 
@@ -7594,12 +7707,12 @@ var KOE_INFO_HTML = `
7594
7707
 
7595
7708
  <h4>3. DTM\u3067\u4F7F\u3046</h4>
7596
7709
  <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>
7710
+ <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>
7711
+ <li>\u300C\u97F3\u58F0URL\u300D\u306B\u624B\u98062\u306E\u76F4\u63A5\u30EA\u30F3\u30AF\u3092\u8CBC\u308A\u4ED8\u3051\u307E\u3059\u3002</li>
7712
+ <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
7713
  <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
7714
  </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>
7715
+ <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
7716
  </div>
7604
7717
  `;
7605
7718
  var BASE_STEP_WIDTH = 0.5;
@@ -7743,6 +7856,24 @@ var TRACKS_ADVANCED = [
7743
7856
  ];
7744
7857
  var DEFAULT_TRACKS = TRACKS_SIMPLE;
7745
7858
  var BASE_LYRIC_MODELS = ["klatt", ...Object.keys(KOE_VOICEBANKS)];
7859
+ var LYRIC_MODEL_CATEGORIES = [
7860
+ {
7861
+ label: "kusa\u30D7\u30EA\u30BB\u30C3\u30C8",
7862
+ models: ["klatt", "tsukuyomi"]
7863
+ },
7864
+ {
7865
+ label: "\u304A\u3093J",
7866
+ models: ["roze", "shiyo", "rino"]
7867
+ },
7868
+ {
7869
+ label: "\u4E00\u822C",
7870
+ models: ["teto", "rei", "ruko_male", "ruko_female"]
7871
+ },
7872
+ {
7873
+ label: "\u30AF\u30C3\u30AD\u30FC\u2606",
7874
+ models: ["mgroid", "motroid", "nynroid"]
7875
+ }
7876
+ ];
7746
7877
  var BASE_LYRIC_MODEL_LABELS = {
7747
7878
  klatt: "\u8EFD\u91CF\u30ED\u30DC\u58F0",
7748
7879
  ...KOE_VOICEBANK_LABELS
@@ -7750,6 +7881,18 @@ var BASE_LYRIC_MODEL_LABELS = {
7750
7881
  var lyricModelLabel = (model, customMap) => BASE_LYRIC_MODEL_LABELS[model] ?? customMap.get(model)?.label ?? model;
7751
7882
  var CUSTOM_VOCAL_ADD_VALUE = "+custom";
7752
7883
  var CUSTOM_VOCAL_KEY_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
7884
+ var deriveCustomVocalKeyFromUrl = (url2) => {
7885
+ let name;
7886
+ try {
7887
+ const path = new URL(url2).pathname;
7888
+ name = decodeURIComponent(path.slice(path.lastIndexOf("/") + 1));
7889
+ } catch {
7890
+ return "";
7891
+ }
7892
+ name = name.replace(/\.[^.]*$/, "").normalize("NFKC").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
7893
+ if (name && /^[0-9]/.test(name)) name = `_${name}`;
7894
+ return CUSTOM_VOCAL_KEY_RE.test(name) ? name : "";
7895
+ };
7753
7896
  var clamp3 = (v, min, max) => Math.min(Math.max(v, min), max);
7754
7897
  var normalizeInstrumentName = (name) => {
7755
7898
  if (!name) return "";
@@ -7767,13 +7910,16 @@ var mountDAW = (target, options = {}) => {
7767
7910
  const drumPatterns = options.drumPatterns ?? DRUM_PATTERNS;
7768
7911
  const showMidi = !!options.parseMidi;
7769
7912
  const showChord = !isAdvanced;
7913
+ const midiSearchClient = options.midiSearch ? new MidiSearchClient(options.midiSearch) : void 0;
7914
+ const showMidiSearch = !!midiSearchClient?.enabled;
7770
7915
  const refs = buildUI(target, {
7771
7916
  tracks: trackConfigs,
7772
7917
  drumPatternNames: Object.keys(drumPatterns),
7773
7918
  defaultDrumPattern: drumPatterns.dance ? "dance" : Object.keys(drumPatterns)[0] ?? "none",
7774
7919
  defaultBpm: options.defaultBpm ?? DEFAULT_BPM,
7775
7920
  showMidi,
7776
- showChord
7921
+ showChord,
7922
+ showMidiSearch
7777
7923
  });
7778
7924
  const renderConfig = {
7779
7925
  stepsPerBar: 192,
@@ -8386,6 +8532,106 @@ var mountDAW = (target, options = {}) => {
8386
8532
  redrawAll();
8387
8533
  }
8388
8534
  };
8535
+ const BG_DB_NAME = "dtm-daw-db";
8536
+ const BG_DB_STORE = "settings";
8537
+ const BG_DB_KEY = "piano-roll-bg";
8538
+ const BG_OPACITY_KEY = "dtm-piano-roll-bg-opacity";
8539
+ const COMPRESSIBLE_IMAGE_TYPES = /* @__PURE__ */ new Set([
8540
+ "image/jpeg",
8541
+ "image/png",
8542
+ "image/webp",
8543
+ "image/bmp"
8544
+ ]);
8545
+ const openBgDb = () => new Promise((resolve, reject) => {
8546
+ const req = indexedDB.open(BG_DB_NAME, 1);
8547
+ req.onupgradeneeded = () => req.result.createObjectStore(BG_DB_STORE);
8548
+ req.onsuccess = () => resolve(req.result);
8549
+ req.onerror = () => reject(req.error);
8550
+ });
8551
+ const saveBgBlob = async (blob2) => {
8552
+ const db = await openBgDb();
8553
+ try {
8554
+ await new Promise((resolve, reject) => {
8555
+ const tx = db.transaction(BG_DB_STORE, "readwrite");
8556
+ tx.objectStore(BG_DB_STORE).put(blob2, BG_DB_KEY);
8557
+ tx.oncomplete = () => resolve();
8558
+ tx.onerror = () => reject(tx.error);
8559
+ });
8560
+ } finally {
8561
+ db.close();
8562
+ }
8563
+ };
8564
+ const loadBgBlob = async () => {
8565
+ const db = await openBgDb();
8566
+ try {
8567
+ return await new Promise((resolve, reject) => {
8568
+ const tx = db.transaction(BG_DB_STORE, "readonly");
8569
+ const req = tx.objectStore(BG_DB_STORE).get(BG_DB_KEY);
8570
+ req.onsuccess = () => resolve(req.result ?? null);
8571
+ req.onerror = () => reject(req.error);
8572
+ });
8573
+ } finally {
8574
+ db.close();
8575
+ }
8576
+ };
8577
+ const deleteBgBlob = async () => {
8578
+ const db = await openBgDb();
8579
+ try {
8580
+ await new Promise((resolve, reject) => {
8581
+ const tx = db.transaction(BG_DB_STORE, "readwrite");
8582
+ tx.objectStore(BG_DB_STORE).delete(BG_DB_KEY);
8583
+ tx.oncomplete = () => resolve();
8584
+ tx.onerror = () => reject(tx.error);
8585
+ });
8586
+ } finally {
8587
+ db.close();
8588
+ }
8589
+ };
8590
+ const resizeImageToBlob = (img, maxW, maxH) => {
8591
+ const scale = Math.min(1, maxW / img.width, maxH / img.height);
8592
+ const width = Math.max(1, Math.round(img.width * scale));
8593
+ const height = Math.max(1, Math.round(img.height * scale));
8594
+ const canvas = document.createElement("canvas");
8595
+ canvas.width = width;
8596
+ canvas.height = height;
8597
+ const ctx = canvas.getContext("2d");
8598
+ if (ctx) ctx.drawImage(img, 0, 0, width, height);
8599
+ return new Promise((resolve, reject) => {
8600
+ canvas.toBlob(
8601
+ (blob2) => blob2 ? resolve(blob2) : reject(new Error("toBlob failed")),
8602
+ "image/jpeg",
8603
+ 0.7
8604
+ );
8605
+ });
8606
+ };
8607
+ let currentBgObjectUrl = null;
8608
+ const applyBgOpacity = (opacityVal) => {
8609
+ refs.rollContainer.style.setProperty(
8610
+ "--dtm-roll-bg-opacity",
8611
+ String(opacityVal / 100)
8612
+ );
8613
+ refs.bgOpacityInput.value = String(opacityVal);
8614
+ };
8615
+ const applyRollBackground = (blob2) => {
8616
+ if (currentBgObjectUrl) {
8617
+ URL.revokeObjectURL(currentBgObjectUrl);
8618
+ currentBgObjectUrl = null;
8619
+ }
8620
+ if (blob2) {
8621
+ currentBgObjectUrl = URL.createObjectURL(blob2);
8622
+ refs.rollContainer.style.setProperty(
8623
+ "--dtm-roll-bg-image",
8624
+ `url(${currentBgObjectUrl})`
8625
+ );
8626
+ refs.bgOpacityRow.classList.remove("dtm-hidden");
8627
+ } else {
8628
+ refs.rollContainer.style.setProperty("--dtm-roll-bg-image", "none");
8629
+ refs.bgOpacityRow.classList.add("dtm-hidden");
8630
+ }
8631
+ refs.bgRemoveBtn.classList.toggle("dtm-hidden", !blob2);
8632
+ setBackgroundActive(!!blob2);
8633
+ redrawAll();
8634
+ };
8389
8635
  const setupCanvas = () => {
8390
8636
  const w = refs.rollContainer.clientWidth || 800;
8391
8637
  const h = refs.rollContainer.clientHeight || 450;
@@ -8751,10 +8997,8 @@ var mountDAW = (target, options = {}) => {
8751
8997
  <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
8998
  <button class="dtm-btn dtm-btn--ghost dtm-btn--xs" data-dtm="lyric-custom-guide">\u4F7F\u3044\u65B9\u30AC\u30A4\u30C9</button>
8753
8999
  </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">
9000
+ <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">
9001
+ <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
9002
  <div class="dtm-row">
8759
9003
  <span class="dtm-label dtm-grow" data-dtm="lyric-custom-note" style="color:var(--dtm-warn)"></span>
8760
9004
  <button class="dtm-btn dtm-btn--primary" data-dtm="lyric-custom-apply">\u8FFD\u52A0</button>
@@ -8819,12 +9063,6 @@ var mountDAW = (target, options = {}) => {
8819
9063
  const lyricCustomIcon = lyricDiv.querySelector(
8820
9064
  '[data-dtm="lyric-custom-icon"]'
8821
9065
  );
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
9066
  const lyricCustomGuide = lyricDiv.querySelector(
8829
9067
  '[data-dtm="lyric-custom-guide"]'
8830
9068
  );
@@ -8835,23 +9073,35 @@ var mountDAW = (target, options = {}) => {
8835
9073
  '[data-dtm="lyric-custom-apply"]'
8836
9074
  );
8837
9075
  const fmtPan = (pan) => pan === 64 ? "C" : pan < 64 ? `L${64 - pan}` : `R${pan - 64}`;
8838
- const addOpt = (value, label) => {
9076
+ const addOpt = (parent, value, label) => {
8839
9077
  const o = document.createElement("option");
8840
9078
  o.value = value;
8841
9079
  o.textContent = label;
8842
- lyricModelSel.appendChild(o);
9080
+ parent.appendChild(o);
8843
9081
  };
8844
- addOpt("", "\u30DC\u30FC\u30AB\u30EB\u306A\u3057");
8845
- for (const m of BASE_LYRIC_MODELS)
8846
- addOpt(m, lyricModelLabel(m, customVocalsMap));
8847
- 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");
9082
+ addOpt(lyricModelSel, "", "\u30DC\u30FC\u30AB\u30EB\u306A\u3057");
9083
+ for (const cat of LYRIC_MODEL_CATEGORIES) {
9084
+ const group = document.createElement("optgroup");
9085
+ group.label = cat.label;
9086
+ for (const m of cat.models) {
9087
+ addOpt(group, m, lyricModelLabel(m, customVocalsMap));
9088
+ }
9089
+ lyricModelSel.appendChild(group);
9090
+ }
9091
+ const customGroup = document.createElement("optgroup");
9092
+ customGroup.label = "\u30AB\u30B9\u30BF\u30E0\u97F3\u58F0";
9093
+ for (const [k, def] of customVocalsMap) {
9094
+ addOpt(customGroup, k, def.label ?? k);
9095
+ }
9096
+ addOpt(customGroup, CUSTOM_VOCAL_ADD_VALUE, "\u30AB\u30B9\u30BF\u30E0\u97F3\u58F0\u3092\u8FFD\u52A0\u2026");
8849
9097
  if (active.lyricModel && !BASE_LYRIC_MODELS.includes(active.lyricModel) && !customVocalsMap.has(active.lyricModel)) {
8850
9098
  addOpt(
9099
+ customGroup,
8851
9100
  active.lyricModel,
8852
9101
  lyricModelLabel(active.lyricModel, customVocalsMap)
8853
9102
  );
8854
9103
  }
9104
+ lyricModelSel.appendChild(customGroup);
8855
9105
  lyricModelSel.value = active.lyricModel;
8856
9106
  lyricOctaveSel.value = String(active.vocalOctave);
8857
9107
  lyricInput.value = active.lyrics;
@@ -8917,9 +9167,10 @@ var mountDAW = (target, options = {}) => {
8917
9167
  lyricModelSel.addEventListener("change", () => {
8918
9168
  if (lyricModelSel.value === CUSTOM_VOCAL_ADD_VALUE) {
8919
9169
  lyricCustomNote.textContent = "";
8920
- lyricCustomAlias.value = "";
8921
- lyricCustomKey.value = "";
8922
9170
  lyricCustom.classList.remove("dtm-hidden");
9171
+ lyricIcon.removeAttribute("src");
9172
+ lyricIcon.classList.add("dtm-hidden");
9173
+ lyricTerms.classList.add("dtm-hidden");
8923
9174
  return;
8924
9175
  }
8925
9176
  lyricCustom.classList.add("dtm-hidden");
@@ -8929,45 +9180,41 @@ var mountDAW = (target, options = {}) => {
8929
9180
  fireLyricsChange(active);
8930
9181
  });
8931
9182
  lyricCustomGuide.addEventListener("click", () => {
8932
- showModal("\u30AB\u30B9\u30BF\u30E0\u97F3\u6E90(.koe)\u306E\u4F7F\u3044\u65B9", KOE_INFO_HTML);
9183
+ showModal("\u30AB\u30B9\u30BF\u30E0\u97F3\u58F0(.koe)\u306E\u4F7F\u3044\u65B9", KOE_INFO_HTML);
8933
9184
  });
8934
9185
  lyricCustomApply.addEventListener("click", () => {
8935
9186
  const src = lyricCustomSrc.value.trim();
8936
9187
  if (!isValidHttpUrl(src)) {
8937
- lyricCustomNote.textContent = "\u97F3\u6E90URL\u304C\u4E0D\u6B63\u3067\u3059\uFF08http/https\u306E\u307F\u30FB2048\u6587\u5B57\u307E\u3067\uFF09";
9188
+ lyricCustomNote.textContent = "\u97F3\u58F0URL\u304C\u4E0D\u6B63\u3067\u3059\uFF08http/https\u306E\u307F\u30FB2048\u6587\u5B57\u307E\u3067\uFF09";
8938
9189
  return;
8939
9190
  }
8940
9191
  const iconRaw = lyricCustomIcon.value.trim();
8941
9192
  const iconUrl = isValidHttpUrl(iconRaw) ? iconRaw : "";
8942
- const alias = lyricCustomAlias.value.trim();
8943
9193
  const existing = [...customVocalsMap.values()].find(
8944
9194
  (d) => d.url === src
8945
9195
  );
8946
- const keyRaw = lyricCustomKey.value.trim().toLowerCase();
8947
9196
  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;
9197
+ if (existing) {
9198
+ key = existing.key;
8963
9199
  } else {
8964
- key = existing?.key ?? genCustomVocalKey();
9200
+ const derived = deriveCustomVocalKeyFromUrl(src);
9201
+ if (derived && !BASE_LYRIC_MODELS.includes(derived)) {
9202
+ if (!customVocalsMap.has(derived)) {
9203
+ key = derived;
9204
+ } else {
9205
+ let n = 2;
9206
+ while (customVocalsMap.has(`${derived}${n}`)) n++;
9207
+ key = `${derived}${n}`;
9208
+ }
9209
+ } else {
9210
+ key = genCustomVocalKey();
9211
+ }
8965
9212
  }
8966
9213
  registerCustomVocal({
8967
9214
  key,
8968
9215
  iconUrl: iconUrl || (existing?.iconUrl ?? ""),
8969
9216
  url: src,
8970
- label: alias || existing?.label
9217
+ label: existing?.label
8971
9218
  });
8972
9219
  active.lyricModel = key;
8973
9220
  fireLyricsChange(active);
@@ -9246,7 +9493,8 @@ var mountDAW = (target, options = {}) => {
9246
9493
  bpm: parsedBpm,
9247
9494
  lyrics,
9248
9495
  meta,
9249
- mergedTrackCount
9496
+ mergedTrackCount,
9497
+ trackVelocity
9250
9498
  } = parseMML(mml, {
9251
9499
  stepsPerBar: renderConfig.stepsPerBar,
9252
9500
  collectLyrics: true,
@@ -9279,6 +9527,13 @@ var mountDAW = (target, options = {}) => {
9279
9527
  options.onTrackInstrumentChange?.(i, name);
9280
9528
  }
9281
9529
  });
9530
+ trackStates.forEach((t, i) => {
9531
+ const v = trackVelocity.get(i);
9532
+ if (v !== void 0 && v !== t.volume) {
9533
+ t.volume = v;
9534
+ t.core.setVolume(v);
9535
+ }
9536
+ });
9282
9537
  for (const t of trackStates) {
9283
9538
  t.lyrics = "";
9284
9539
  t.lyricModel = "";
@@ -9470,7 +9725,7 @@ var mountDAW = (target, options = {}) => {
9470
9725
  refs.prevBarBtn.addEventListener("click", () => {
9471
9726
  const targetStep = Math.max(
9472
9727
  0,
9473
- Math.floor((getCurrentPlayStep() - 1) / renderConfig.stepsPerBar) * renderConfig.stepsPerBar
9728
+ (Math.floor((getCurrentPlayStep() - 1) / renderConfig.stepsPerBar) - 1) * renderConfig.stepsPerBar
9474
9729
  );
9475
9730
  jumpTo(targetStep);
9476
9731
  });
@@ -9515,6 +9770,41 @@ var mountDAW = (target, options = {}) => {
9515
9770
  applyZoomY();
9516
9771
  notifyViewState();
9517
9772
  });
9773
+ refs.bgUploadBtn.addEventListener("click", () => refs.bgFileInput.click());
9774
+ refs.bgFileInput.addEventListener("change", () => {
9775
+ const file = refs.bgFileInput.files?.[0];
9776
+ refs.bgFileInput.value = "";
9777
+ if (!file) return;
9778
+ if (!COMPRESSIBLE_IMAGE_TYPES.has(file.type)) {
9779
+ saveBgBlob(file).then(() => applyRollBackground(file)).catch(() => {
9780
+ });
9781
+ return;
9782
+ }
9783
+ const objectUrl = URL.createObjectURL(file);
9784
+ const img = new Image();
9785
+ img.onload = () => {
9786
+ URL.revokeObjectURL(objectUrl);
9787
+ const maxW = refs.rollContainer.clientWidth || 800;
9788
+ const maxH = refs.rollContainer.clientHeight || 450;
9789
+ resizeImageToBlob(img, maxW, maxH).then(
9790
+ (blob2) => saveBgBlob(blob2).then(() => applyRollBackground(blob2))
9791
+ ).catch(() => {
9792
+ });
9793
+ };
9794
+ img.src = objectUrl;
9795
+ });
9796
+ refs.bgRemoveBtn.addEventListener("click", () => {
9797
+ deleteBgBlob().catch(() => {
9798
+ }).finally(() => applyRollBackground(null));
9799
+ });
9800
+ refs.bgOpacityInput.addEventListener("input", () => {
9801
+ const opacityVal = Number.parseInt(refs.bgOpacityInput.value, 10);
9802
+ applyBgOpacity(opacityVal);
9803
+ try {
9804
+ localStorage.setItem(BG_OPACITY_KEY, String(opacityVal));
9805
+ } catch (_) {
9806
+ }
9807
+ });
9518
9808
  refs.decomposeChordToggle.addEventListener("change", notifyViewState);
9519
9809
  refs.ignoreChordHeavyToggle.addEventListener("change", notifyViewState);
9520
9810
  refs.masterVolume.addEventListener("input", () => {
@@ -9587,11 +9877,11 @@ var mountDAW = (target, options = {}) => {
9587
9877
  refs.mmlLoadBtn.addEventListener("click", async () => {
9588
9878
  const mml = refs.mmlInput.value;
9589
9879
  if (!isAdvanced && options.onRequestAdvancedMode) {
9590
- const { mergedTrackCount } = parseMML(mml, {
9880
+ const { mergedTrackCount, meta } = parseMML(mml, {
9591
9881
  stepsPerBar: renderConfig.stepsPerBar,
9592
9882
  clampTrackCount: trackStates.length
9593
9883
  });
9594
- if (mergedTrackCount > 0) {
9884
+ if (mergedTrackCount > 0 || meta.mode === "advanced") {
9595
9885
  const confirmed = await showConfirmModal(
9596
9886
  "\u521D\u5FC3\u8005\u30E2\u30FC\u30C9\u3067\u8AAD\u307F\u8FBC\u3080\u3068\u3001\u97F3\u304C\u5D29\u308C\u308B\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002<br>\u4E0A\u7D1A\u8005\u30E2\u30FC\u30C9\u306B\u5207\u308A\u66FF\u3048\u307E\u3059\u304B\uFF1F"
9597
9887
  );
@@ -9625,6 +9915,7 @@ var mountDAW = (target, options = {}) => {
9625
9915
  };
9626
9916
  showModal = (title, bodyHTML) => {
9627
9917
  collapseActiveSample();
9918
+ collapseSearchPreview();
9628
9919
  refs.modalTitle.textContent = title;
9629
9920
  refs.modalBody.innerHTML = bodyHTML;
9630
9921
  refs.modalOverlay.removeAttribute("hidden");
@@ -9713,11 +10004,13 @@ var mountDAW = (target, options = {}) => {
9713
10004
  };
9714
10005
  refs.modalClose.addEventListener("click", () => {
9715
10006
  collapseActiveSample();
10007
+ collapseSearchPreview();
9716
10008
  refs.modalOverlay.setAttribute("hidden", "");
9717
10009
  });
9718
10010
  refs.modalOverlay.addEventListener("click", (e) => {
9719
10011
  if (e.target === refs.modalOverlay) {
9720
10012
  collapseActiveSample();
10013
+ collapseSearchPreview();
9721
10014
  refs.modalOverlay.setAttribute("hidden", "");
9722
10015
  }
9723
10016
  });
@@ -9738,6 +10031,7 @@ var mountDAW = (target, options = {}) => {
9738
10031
  })
9739
10032
  );
9740
10033
  if (showMidi) wireMidi();
10034
+ if (showMidiSearch) wireMidiSearch();
9741
10035
  document.addEventListener("keydown", onKeyDown);
9742
10036
  refs.root.addEventListener("keydown", (e) => {
9743
10037
  const t = e.target;
@@ -9802,6 +10096,236 @@ var mountDAW = (target, options = {}) => {
9802
10096
  overlayDuring(() => applyMidiSelection(pendingMidi, selected));
9803
10097
  });
9804
10098
  };
10099
+ const buildPreviewMml = (midi) => {
10100
+ const analysis = analyzeMidiTracks(midi);
10101
+ const selected = analysis.filter((a) => a.selected).map((a) => a.index);
10102
+ const { placements, bpm: parsedBpm } = extractMidiPlacements(
10103
+ midi,
10104
+ selected
10105
+ );
10106
+ const byTrack = /* @__PURE__ */ new Map();
10107
+ for (const p of placements) {
10108
+ if (!byTrack.has(p.trackId)) byTrack.set(p.trackId, []);
10109
+ byTrack.get(p.trackId)?.push(p);
10110
+ }
10111
+ const refCore = trackStates[0].core;
10112
+ const tempo = Math.round(parsedBpm) || bpm;
10113
+ const lines = [];
10114
+ let i = 0;
10115
+ for (const notes of byTrack.values()) {
10116
+ const asNotes = notes.map((p) => ({
10117
+ id: 0,
10118
+ startStep: p.startStep,
10119
+ durationSteps: p.durationSteps,
10120
+ pitch: p.pitch,
10121
+ velocity: p.velocity
10122
+ }));
10123
+ const mml = refCore.getMMLFromNotes(asNotes, tempo, 100).trim();
10124
+ lines.push(`@${i} ${mml}`);
10125
+ i++;
10126
+ }
10127
+ return [...lines, MML_END_MARKER].join(";\n");
10128
+ };
10129
+ const fetchVerifiedMidi = async (fileName) => {
10130
+ if (!options.parseMidi || !midiSearchClient) {
10131
+ throw new Error("parseMidi/midiSearchClient not injected");
10132
+ }
10133
+ const midiBytes = await midiSearchClient.fetchMidi(fileName);
10134
+ const midi = await options.parseMidi(new Uint8Array(midiBytes));
10135
+ const analysis = analyzeMidiTracks(midi);
10136
+ const selected = analysis.filter((a) => a.selected).map((a) => a.index);
10137
+ if (!isPlausibleMidiTranscription(midi, selected)) {
10138
+ throw new Error("implausible MIDI transcription");
10139
+ }
10140
+ return midi;
10141
+ };
10142
+ let searchPreviewPlayer = null;
10143
+ let searchPreviewBtn = null;
10144
+ const collapseSearchPreview = () => {
10145
+ if (searchPreviewPlayer) {
10146
+ searchPreviewPlayer.stop();
10147
+ searchPreviewPlayer.destroy();
10148
+ searchPreviewPlayer = null;
10149
+ }
10150
+ if (searchPreviewBtn) {
10151
+ searchPreviewBtn.textContent = "\u25B6 \u8A66\u8074";
10152
+ searchPreviewBtn.classList.remove("dtm-btn--danger");
10153
+ searchPreviewBtn.classList.add("dtm-btn--ghost");
10154
+ const container = searchPreviewBtn.closest(".dtm-modal-sample-box")?.querySelector(".dtm-modal-sample-player-container");
10155
+ if (container) container.innerHTML = "";
10156
+ searchPreviewBtn = null;
10157
+ }
10158
+ };
10159
+ const wireMidiSearch = () => {
10160
+ if (!midiSearchClient) return;
10161
+ refs.midiSearchOpenBtn.addEventListener("click", () => {
10162
+ showModal(
10163
+ "MML\u691C\u7D22",
10164
+ `
10165
+ <div class="dtm-row" style="flex-wrap:nowrap">
10166
+ <input type="text" class="dtm-input dtm-grow" data-dtm="modal-midi-search-input" placeholder="\u66F2\u540D\u3067MML\u691C\u7D22" style="min-width:0">
10167
+ <button class="dtm-btn dtm-btn--primary" data-dtm="modal-midi-search-btn" style="flex-shrink:0">\u691C\u7D22</button>
10168
+ </div>
10169
+ <div class="dtm-row" data-dtm="modal-midi-search-results" style="flex-direction:column;align-items:stretch;gap:4px"></div>
10170
+ `
10171
+ );
10172
+ const input = refs.modalBody.querySelector(
10173
+ '[data-dtm="modal-midi-search-input"]'
10174
+ );
10175
+ const searchBtn = refs.modalBody.querySelector(
10176
+ '[data-dtm="modal-midi-search-btn"]'
10177
+ );
10178
+ const results = refs.modalBody.querySelector(
10179
+ '[data-dtm="modal-midi-search-results"]'
10180
+ );
10181
+ const runSearch = async () => {
10182
+ const q2 = input.value.trim();
10183
+ if (!q2) return;
10184
+ collapseSearchPreview();
10185
+ searchBtn.disabled = true;
10186
+ searchBtn.textContent = "\u691C\u7D22\u4E2D...";
10187
+ results.innerHTML = "";
10188
+ try {
10189
+ const songs = await midiSearchClient.searchSongs({ title: q2 });
10190
+ if (songs.length === 0) {
10191
+ results.innerHTML = '<span class="dtm-label" style="color:var(--dtm-warn)">\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F</span>';
10192
+ return;
10193
+ }
10194
+ for (const song of songs) {
10195
+ const box = document.createElement("div");
10196
+ box.className = "dtm-modal-sample-box";
10197
+ const row = document.createElement("div");
10198
+ row.className = "dtm-row";
10199
+ row.style.cssText = "flex-wrap:nowrap;gap:4px;padding:2px 0";
10200
+ const label = document.createElement("span");
10201
+ label.className = "dtm-label";
10202
+ label.style.cssText = "flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap";
10203
+ label.textContent = `${song.title} - ${song.user}`;
10204
+ const previewBtn = document.createElement("button");
10205
+ previewBtn.className = "dtm-btn dtm-btn--ghost";
10206
+ previewBtn.style.cssText = "flex-shrink:0";
10207
+ previewBtn.textContent = "\u25B6 \u8A66\u8074";
10208
+ const loadBtn = document.createElement("button");
10209
+ loadBtn.className = "dtm-btn dtm-btn--primary";
10210
+ loadBtn.style.cssText = "flex-shrink:0";
10211
+ loadBtn.textContent = "\u8AAD\u307F\u8FBC\u307F";
10212
+ const playerContainer = document.createElement("div");
10213
+ playerContainer.className = "dtm-modal-sample-player-container";
10214
+ previewBtn.addEventListener("click", async () => {
10215
+ if (searchPreviewBtn === previewBtn) {
10216
+ if (searchPreviewPlayer?.isPlaying()) {
10217
+ searchPreviewPlayer.stop();
10218
+ } else {
10219
+ stop();
10220
+ previewBtn.textContent = "\u8AAD\u8FBC\u4E2D...";
10221
+ try {
10222
+ const midi = await fetchVerifiedMidi(song.file);
10223
+ const mml = buildPreviewMml(midi);
10224
+ playerContainer.innerHTML = "";
10225
+ const player = mountMmlPlayer(playerContainer, mml, {
10226
+ onPlayNote: options.onPlayNote,
10227
+ onPlayDrum: options.onPlayDrum,
10228
+ onResumeAudio: options.onResumeAudio,
10229
+ getAudioTime: options.getAudioTime,
10230
+ singingVoices: options.singingVoices,
10231
+ drumPatterns: options.drumPatterns,
10232
+ volume: masterVolume,
10233
+ _skipInfoModals: true,
10234
+ onStop: () => {
10235
+ if (searchPreviewBtn === previewBtn) {
10236
+ previewBtn.textContent = "\u25B6 \u8A66\u8074";
10237
+ previewBtn.classList.remove("dtm-btn--danger");
10238
+ previewBtn.classList.add("dtm-btn--ghost");
10239
+ }
10240
+ }
10241
+ });
10242
+ searchPreviewPlayer = player;
10243
+ searchPreviewBtn = previewBtn;
10244
+ player.play();
10245
+ previewBtn.textContent = "\u25A0 \u505C\u6B62";
10246
+ previewBtn.classList.remove("dtm-btn--ghost");
10247
+ previewBtn.classList.add("dtm-btn--danger");
10248
+ } catch (e) {
10249
+ console.error("[dtm] MIDI preview failed", e);
10250
+ previewBtn.textContent = "\u5931\u6557";
10251
+ }
10252
+ }
10253
+ } else {
10254
+ collapseSearchPreview();
10255
+ stop();
10256
+ previewBtn.textContent = "\u8AAD\u8FBC\u4E2D...";
10257
+ try {
10258
+ const midi = await fetchVerifiedMidi(song.file);
10259
+ const mml = buildPreviewMml(midi);
10260
+ playerContainer.innerHTML = "";
10261
+ const player = mountMmlPlayer(playerContainer, mml, {
10262
+ onPlayNote: options.onPlayNote,
10263
+ onPlayDrum: options.onPlayDrum,
10264
+ onResumeAudio: options.onResumeAudio,
10265
+ getAudioTime: options.getAudioTime,
10266
+ singingVoices: options.singingVoices,
10267
+ drumPatterns: options.drumPatterns,
10268
+ volume: masterVolume,
10269
+ _skipInfoModals: true,
10270
+ onStop: () => {
10271
+ if (searchPreviewBtn === previewBtn) {
10272
+ previewBtn.textContent = "\u25B6 \u8A66\u8074";
10273
+ previewBtn.classList.remove("dtm-btn--danger");
10274
+ previewBtn.classList.add("dtm-btn--ghost");
10275
+ }
10276
+ }
10277
+ });
10278
+ searchPreviewPlayer = player;
10279
+ searchPreviewBtn = previewBtn;
10280
+ player.play();
10281
+ previewBtn.textContent = "\u25A0 \u505C\u6B62";
10282
+ previewBtn.classList.remove("dtm-btn--ghost");
10283
+ previewBtn.classList.add("dtm-btn--danger");
10284
+ } catch (e) {
10285
+ console.error("[dtm] MIDI preview failed", e);
10286
+ previewBtn.textContent = "\u5931\u6557";
10287
+ }
10288
+ }
10289
+ });
10290
+ loadBtn.addEventListener("click", async () => {
10291
+ loadBtn.disabled = true;
10292
+ loadBtn.textContent = "\u8AAD\u8FBC\u4E2D...";
10293
+ try {
10294
+ const pending = await fetchVerifiedMidi(song.file);
10295
+ const analysis = analyzeMidiTracks(pending);
10296
+ const sel = analysis.filter((a) => a.selected).map((a) => a.index);
10297
+ collapseSearchPreview();
10298
+ overlayDuring(() => applyMidiSelection(pending, sel));
10299
+ refs.modalOverlay.setAttribute("hidden", "");
10300
+ } catch (e) {
10301
+ console.error("[dtm] MIDI fetch failed", e);
10302
+ loadBtn.textContent = "\u5931\u6557";
10303
+ } finally {
10304
+ loadBtn.disabled = false;
10305
+ }
10306
+ });
10307
+ row.appendChild(label);
10308
+ row.appendChild(previewBtn);
10309
+ row.appendChild(loadBtn);
10310
+ box.appendChild(row);
10311
+ box.appendChild(playerContainer);
10312
+ results.appendChild(box);
10313
+ }
10314
+ } catch (e) {
10315
+ console.error("[dtm] MIDI search failed", e);
10316
+ results.innerHTML = '<span class="dtm-label" style="color:var(--dtm-warn)">\u691C\u7D22\u306B\u5931\u6557\u3057\u307E\u3057\u305F</span>';
10317
+ } finally {
10318
+ searchBtn.disabled = false;
10319
+ searchBtn.textContent = "\u691C\u7D22";
10320
+ }
10321
+ };
10322
+ searchBtn.addEventListener("click", () => void runSearch());
10323
+ input.addEventListener("keydown", (e) => {
10324
+ if (e.key === "Enter") void runSearch();
10325
+ });
10326
+ input.focus();
10327
+ });
10328
+ };
9805
10329
  const onKeyDown = (e) => {
9806
10330
  if (!(e.ctrlKey || e.metaKey)) return;
9807
10331
  if (e.code === "KeyZ" && !e.shiftKey) {
@@ -9857,6 +10381,22 @@ var mountDAW = (target, options = {}) => {
9857
10381
  updateUndoRedo();
9858
10382
  redrawAll();
9859
10383
  if (options.initialMML) loadMML(options.initialMML);
10384
+ let initialOpacity = 40;
10385
+ try {
10386
+ const storedOpacity = localStorage.getItem(BG_OPACITY_KEY);
10387
+ if (storedOpacity !== null) {
10388
+ initialOpacity = Number.parseInt(storedOpacity, 10);
10389
+ if (Number.isNaN(initialOpacity) || initialOpacity < 0 || initialOpacity > 100) {
10390
+ initialOpacity = 40;
10391
+ }
10392
+ }
10393
+ } catch (_) {
10394
+ }
10395
+ applyBgOpacity(initialOpacity);
10396
+ loadBgBlob().then((blob2) => {
10397
+ if (blob2) applyRollBackground(blob2);
10398
+ }).catch(() => {
10399
+ });
9860
10400
  let resizeTimer = null;
9861
10401
  const resizeObserver = new ResizeObserver(() => {
9862
10402
  if (resizeTimer) clearTimeout(resizeTimer);
@@ -11177,13 +11717,17 @@ var createDtmStudio = async (options = {}) => {
11177
11717
  const mountedPlayers = [];
11178
11718
  const mountedModeSwitches = [];
11179
11719
  const mountEditor = (target, opts = {}) => {
11720
+ const mergedOpts = {
11721
+ midiSearch: options.midiSearch,
11722
+ ...opts
11723
+ };
11180
11724
  const {
11181
11725
  preset,
11182
11726
  presetUI,
11183
11727
  onInstrumentChange,
11184
11728
  onTrackInstrumentChange: externalOnTrackInstrumentChange,
11185
11729
  ...dawOverrides
11186
- } = opts;
11730
+ } = mergedOpts;
11187
11731
  const tracks = dawOverrides.tracks ?? TRACKS_SIMPLE;
11188
11732
  const trackIds = tracks.map((t) => t.id);
11189
11733
  const presetKey = preset && INSTRUMENT_PRESETS[preset] ? preset : defaultPreset;
@@ -11518,6 +12062,7 @@ export {
11518
12062
  MAX_VOCAL_VOLUME,
11519
12063
  MMLCore,
11520
12064
  MML_END_MARKER,
12065
+ MidiSearchClient,
11521
12066
  PITCH_MAP,
11522
12067
  PREWARM_NOTES,
11523
12068
  TRACKS_ADVANCED,
@@ -11567,6 +12112,7 @@ export {
11567
12112
  init,
11568
12113
  injectStyles,
11569
12114
  isChordHeavyTrack,
12115
+ isPlausibleMidiTranscription,
11570
12116
  isValidHttpUrl,
11571
12117
  koeUrl,
11572
12118
  mountDAW,
@@ -11580,6 +12126,7 @@ export {
11580
12126
  parseMmlMeta,
11581
12127
  playMML,
11582
12128
  playSingingMML,
12129
+ setBackgroundActive,
11583
12130
  setDrawOffset,
11584
12131
  shiftNotes,
11585
12132
  showLoadingOverlay,