@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.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,16 @@ 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>
1426
+ <div class="dtm-row dtm-hidden" data-dtm="bg-opacity-row">
1427
+ <span class="dtm-label">\u80CC\u666F\u4E0D\u900F\u660E\u5EA6</span>
1428
+ <input type="range" min="0" max="100" value="40" data-dtm="bg-opacity" class="dtm-slider">
1429
+ </div>
1411
1430
  </div>
1412
1431
  </details>
1413
1432
 
@@ -1446,6 +1465,9 @@ var buildUI = (target, options) => {
1446
1465
  <textarea class="dtm-textarea dtm-grow" data-dtm="mml-input" placeholder="MML\u3092\u5165\u529B"></textarea>
1447
1466
  <button class="dtm-btn dtm-btn--primary" data-dtm="mml-load" style="flex-shrink:0">\u8AAD\u8FBC</button>
1448
1467
  </div>
1468
+ <div class="dtm-row" data-dtm="midi-search-row" style="justify-content:flex-end;${showMidiSearch ? "" : "display:none"}">
1469
+ <button class="dtm-btn dtm-btn--primary" data-dtm="midi-search-open">MML\u691C\u7D22</button>
1470
+ </div>
1449
1471
  <p class="dtm-load-note dtm-hidden" data-dtm="mml-load-note"></p>
1450
1472
  </div>
1451
1473
  </details>
@@ -1556,6 +1578,11 @@ var buildUI = (target, options) => {
1556
1578
  zoomXOut: sel("zoomx-out"),
1557
1579
  zoomYIn: sel("zoomy-in"),
1558
1580
  zoomYOut: sel("zoomy-out"),
1581
+ bgFileInput: sel("bg-file-input"),
1582
+ bgUploadBtn: sel("bg-upload"),
1583
+ bgRemoveBtn: sel("bg-remove"),
1584
+ bgOpacityInput: sel("bg-opacity"),
1585
+ bgOpacityRow: sel("bg-opacity-row"),
1559
1586
  rollContainer: sel("roll"),
1560
1587
  wrapper: sel("wrapper"),
1561
1588
  vScroll: sel("vscroll"),
@@ -1574,6 +1601,7 @@ var buildUI = (target, options) => {
1574
1601
  midiInfoBtn: sel("midi-info"),
1575
1602
  midiTrackSelection: sel("midi-track-selection"),
1576
1603
  midiPanel: sel("midi-panel"),
1604
+ midiSearchOpenBtn: sel("midi-search-open"),
1577
1605
  mmlInput: sel("mml-input"),
1578
1606
  mmlLoadBtn: sel("mml-load"),
1579
1607
  mmlLoadNote: sel("mml-load-note"),
@@ -2619,7 +2647,8 @@ var KOE_VOICEBANKS = {
2619
2647
  shiyo: "\u9769\u547D\u30B7\u30E8.koe",
2620
2648
  rei: "\u8DB3\u7ACB\u30EC\u30A4ver3.5.0.koe",
2621
2649
  mgroid: "MGRoid_\u539F\u97F3\u8A2D\u5B9A\u6E08\u307F.koe",
2622
- motroid: "MOTRoid\u5B8C\u5168\u7248V2.koe"
2650
+ motroid: "MOTRoid\u5B8C\u5168\u7248V2.koe",
2651
+ nynroid: "NYNRoidver1.4.koe"
2623
2652
  };
2624
2653
  var KOE_VOICEBANK_LABELS = {
2625
2654
  tsukuyomi: "\u3064\u304F\u3088\u307F\u3061\u3083\u3093",
@@ -2631,7 +2660,8 @@ var KOE_VOICEBANK_LABELS = {
2631
2660
  shiyo: "\u9769\u547D\u30B7\u30E8",
2632
2661
  rei: "\u8DB3\u7ACB\u30EC\u30A4",
2633
2662
  mgroid: "MGRoid",
2634
- motroid: "MOTRoid"
2663
+ motroid: "MOTRoid",
2664
+ nynroid: "NYNRoid"
2635
2665
  };
2636
2666
  var VOICE_IMAGE_KEY = {
2637
2667
  klatt: "puyuyu",
@@ -2644,7 +2674,8 @@ var VOICE_IMAGE_KEY = {
2644
2674
  shiyo: "shiyo",
2645
2675
  rei: "rei",
2646
2676
  mgroid: "MGRoid",
2647
- motroid: "MOTRoid"
2677
+ motroid: "MOTRoid",
2678
+ nynroid: "NYNRoid"
2648
2679
  };
2649
2680
  var KOE_VOICEBANK_TERMS = {
2650
2681
  tsukuyomi: "https://tyc.rei-yumesaki.net/material/utau/terms/",
@@ -2656,7 +2687,8 @@ var KOE_VOICEBANK_TERMS = {
2656
2687
  shiyo: "https://kakumeisiyo.my.canva.site/dagkuyjwycs",
2657
2688
  rei: "https://mechanicalgirl.jp/guidelines/",
2658
2689
  mgroid: "https://x.com/nisusansu/status/1048825378188353536",
2659
- motroid: "https://www.nicovideo.jp/watch/sm40031282"
2690
+ motroid: "https://www.nicovideo.jp/watch/sm40031282",
2691
+ nynroid: "https://www.bilibili.com/video/BV1V24y1a7qs"
2660
2692
  };
2661
2693
  var koeUrl = (name, base = KOE_BASE_URL) => `${base}/${encodeURIComponent(name)}`;
2662
2694
  var DEFAULT_WORLDLINE_SCRIPT = "https://onjmin.github.io/koe/demo/world/worldline.js";
@@ -3480,6 +3512,19 @@ var extractMidiPlacements = (midi, selectedTrackIndices) => {
3480
3512
  }
3481
3513
  return { placements, bpm };
3482
3514
  };
3515
+ var isPlausibleMidiTranscription = (midi, selectedIndices) => {
3516
+ const { placements } = extractMidiPlacements(midi, selectedIndices);
3517
+ if (placements.length < 4) return false;
3518
+ const shortCount = placements.filter((p) => p.durationSteps <= 1).length;
3519
+ const shortRatio = shortCount / placements.length;
3520
+ const countByStep = /* @__PURE__ */ new Map();
3521
+ for (const p of placements) {
3522
+ countByStep.set(p.startStep, (countByStep.get(p.startStep) ?? 0) + 1);
3523
+ }
3524
+ const denseSteps = [...countByStep.values()].filter((n) => n >= 6).length;
3525
+ const denseRatio = denseSteps / countByStep.size;
3526
+ return shortRatio < 0.6 && denseRatio < 0.35;
3527
+ };
3483
3528
  var extractMidiPlacementsByTrack = (midi, selectedIndices, trackIds) => {
3484
3529
  const { tracks, division } = midi;
3485
3530
  const ticksPerBeat = division;
@@ -3632,6 +3677,45 @@ var exportMIDI = (options) => {
3632
3677
  return new Blob([new Uint8Array(arr).buffer], { type: "audio/midi" });
3633
3678
  };
3634
3679
 
3680
+ // src/midi-search.ts
3681
+ var MidiSearchClient = class {
3682
+ config;
3683
+ constructor(config = {}) {
3684
+ this.config = config;
3685
+ }
3686
+ get enabled() {
3687
+ return !!this.config.apiKey;
3688
+ }
3689
+ get baseUrl() {
3690
+ return this.config.baseUrl ?? "https://rpgen-search.pages.dev/api";
3691
+ }
3692
+ headers() {
3693
+ const h = {};
3694
+ if (this.config.apiKey) {
3695
+ h.Authorization = `Bearer ${this.config.apiKey}`;
3696
+ }
3697
+ return h;
3698
+ }
3699
+ async searchSongs(params) {
3700
+ if (!this.enabled) return [];
3701
+ const qs = new URLSearchParams();
3702
+ if (params.title) qs.set("title", params.title);
3703
+ if (params.user) qs.set("user", params.user);
3704
+ if (params.twitter_id) qs.set("twitter_id", params.twitter_id);
3705
+ const url2 = `${this.baseUrl}/picotune/songs${qs.toString() ? `?${qs.toString()}` : ""}`;
3706
+ const res = await fetch(url2, { headers: this.headers() });
3707
+ if (!res.ok) throw new Error(`picotune search failed: ${res.status}`);
3708
+ const body = await res.json();
3709
+ return body.data ?? body.songs ?? body;
3710
+ }
3711
+ async fetchMidi(fileName) {
3712
+ const url2 = `${this.baseUrl}/picotune/songs/${encodeURIComponent(fileName)}`;
3713
+ const res = await fetch(url2, { headers: this.headers() });
3714
+ if (!res.ok) throw new Error(`picotune fetch failed: ${res.status}`);
3715
+ return res.arrayBuffer();
3716
+ }
3717
+ };
3718
+
3635
3719
  // src/linked-list.ts
3636
3720
  var LinkedList = class {
3637
3721
  #cursor;
@@ -3697,6 +3781,10 @@ var HEADER_HEIGHT = 20;
3697
3781
  var getRenderConfig = () => g_config;
3698
3782
  var g_draw_offset_x = 0;
3699
3783
  var g_draw_offset_y = 0;
3784
+ var g_bg_active = false;
3785
+ var setBackgroundActive = (active) => {
3786
+ g_bg_active = active;
3787
+ };
3700
3788
  var getDrawOffset = () => ({
3701
3789
  x: g_draw_offset_x,
3702
3790
  y: g_draw_offset_y
@@ -3846,7 +3934,7 @@ var drawHeader = () => {
3846
3934
  const { stepWidth, stepsPerBar } = g_config;
3847
3935
  g_header_ctx.save();
3848
3936
  g_header_ctx.translate(-g_draw_offset_x, 0);
3849
- g_header_ctx.fillStyle = "#0a0f1f";
3937
+ g_header_ctx.fillStyle = g_bg_active ? "rgba(10,15,31,0.55)" : "#0a0f1f";
3850
3938
  g_header_ctx.fillRect(
3851
3939
  g_draw_offset_x,
3852
3940
  0,
@@ -3892,7 +3980,7 @@ var drawGrid = (noteLengthSteps = 1) => {
3892
3980
  const octave = Math.floor(totalPitch / 12) - 1;
3893
3981
  const isC4Range = octave === 4;
3894
3982
  const screenY = y - g_draw_offset_y;
3895
- g_grid_ctx.fillStyle = isBlackKey ? "#080b16" : "#111628";
3983
+ g_grid_ctx.fillStyle = g_bg_active ? isBlackKey ? "rgba(8,11,22,0.55)" : "rgba(17,22,40,0.45)" : isBlackKey ? "#080b16" : "#111628";
3896
3984
  g_grid_ctx.fillRect(0, screenY, g_grid_canvas.width, keyHeight);
3897
3985
  if (isC4Range) {
3898
3986
  g_grid_ctx.fillStyle = "rgba(41,173,255,0.05)";
@@ -4597,6 +4685,7 @@ var parseMML = (mml, options = {}) => {
4597
4685
  tokenTracks: collectTokens ? tokenTracks : void 0,
4598
4686
  lyrics: collectLyrics ? /* @__PURE__ */ new Map() : void 0,
4599
4687
  mergedTrackCount: 0,
4688
+ trackVelocity: /* @__PURE__ */ new Map(),
4600
4689
  meta: {}
4601
4690
  };
4602
4691
  }
@@ -4616,6 +4705,7 @@ var parseMML = (mml, options = {}) => {
4616
4705
  let baseLength = 16;
4617
4706
  let velocity = 100;
4618
4707
  const contributors = /* @__PURE__ */ new Map();
4708
+ const trackVelocity = /* @__PURE__ */ new Map();
4619
4709
  const recordContributor = () => {
4620
4710
  let set = contributors.get(trackIndex);
4621
4711
  if (!set) {
@@ -4716,6 +4806,7 @@ var parseMML = (mml, options = {}) => {
4716
4806
  }
4717
4807
  } else if (ch === "v" && numStr) {
4718
4808
  velocity = clamp2(Number.parseInt(numStr, 10), 0, 127);
4809
+ trackVelocity.set(trackIndex, velocity);
4719
4810
  }
4720
4811
  pushTok("ctrl", currentStep, 0, tokStart);
4721
4812
  } else if (ch === "[") {
@@ -4805,6 +4896,7 @@ var parseMML = (mml, options = {}) => {
4805
4896
  tokenTracks: collectTokens ? tokenTracks : void 0,
4806
4897
  lyrics,
4807
4898
  mergedTrackCount,
4899
+ trackVelocity,
4808
4900
  meta
4809
4901
  };
4810
4902
  };
@@ -5464,6 +5556,18 @@ var DAW_CSS = `
5464
5556
  4px 4px 0 var(--c-black);
5465
5557
  overflow: hidden;
5466
5558
  }
5559
+ /* \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 */
5560
+ .dtm-roll::before {
5561
+ content: '';
5562
+ position: absolute;
5563
+ inset: 0;
5564
+ background-image: var(--dtm-roll-bg-image, none);
5565
+ background-size: cover;
5566
+ background-position: center;
5567
+ background-repeat: no-repeat;
5568
+ opacity: var(--dtm-roll-bg-opacity, 0.4);
5569
+ pointer-events: none;
5570
+ }
5467
5571
  .dtm-vscroll {
5468
5572
  position: relative;
5469
5573
  width: 20px;
@@ -5836,9 +5940,14 @@ var DAW_CSS = `
5836
5940
  .dtm-modal-body {
5837
5941
  padding: 12px;
5838
5942
  overflow-y: auto;
5943
+ overflow-x: hidden;
5839
5944
  font-size: 13px;
5840
5945
  line-height: 1.6;
5841
5946
  }
5947
+ .dtm-modal-body * {
5948
+ max-width: 100%;
5949
+ box-sizing: border-box;
5950
+ }
5842
5951
  .dtm-modal-body a {
5843
5952
  color: var(--dtm-primary);
5844
5953
  text-decoration: underline;
@@ -6375,7 +6484,10 @@ var createSynth = (ctx, destination = ctx.destination) => {
6375
6484
  osc.frequency.value = freqFromPitch(e.pitch);
6376
6485
  const t0 = ctx.currentTime + e.when;
6377
6486
  const peak = Math.max(1e-4, 0.06 * e.volume * 1.5);
6487
+ const releaseTime = Math.min(0.02, e.duration * 0.1);
6488
+ const sustainDuration = e.duration - releaseTime;
6378
6489
  gain.gain.setValueAtTime(peak, t0);
6490
+ gain.gain.setValueAtTime(peak, t0 + sustainDuration);
6379
6491
  gain.gain.exponentialRampToValueAtTime(1e-3, t0 + e.duration);
6380
6492
  osc.connect(gain);
6381
6493
  if (typeof ctx.createStereoPanner === "function" && e.pan) {
@@ -6441,6 +6553,9 @@ var MGRoid_default = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYA
6441
6553
  // assets/MOTRoid.png
6442
6554
  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
6555
 
6556
+ // assets/NYNRoid.png
6557
+ var NYNRoid_default = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABo0lEQVR42tWXoU8DMRTGf7csCAQBOcz4A6aOIOZAETKLBsQGCCwhiIWQCUKCRDAyAWgcWQgoPOHU/gBmOAkJggRTRPeSa7m7lSHWPdNc23u97+v3vvYCrCiVSir5HMdxwAjhmqfAmCPI+uIURKnPcRxjIc1d0GZi7AwU7Y63qyMA5reODaR268qEnc9fDdhaaDabAERRZLRhGDollnmtVsvvKihmqb3b7QJQq9WcGOh0OgDU63Xj/SyN+KeBMAxV2h4K8peTbQAWDy9HGpeIoiiYDA3Y6l9Z29NI788NpII8WN0FYLmgU0ZDfMJ/BiSU0tI4m57RHZWqRn5XzZ33ZDmn/wwk1KmSKg4C3a0eLvIzPbYB2P/61H7wW0ue3wdSvFxZ9Wvu+YARUb/tH1l1PzkMiBYkxOMldpZuAWg/r6eeCa5r+s+A3AuknjcrNwDMvZ4C8L5wAMB1b8Ood7kHpGjFfwZU0tkajYaxt6Ly3tSsNsbvj/QqGbwvPpLFtscMWHtnnxGuMezPyv8q+AdylzX8ZsDJIfv9vjFYLpf/lHvsDPwA5Wy6PgZtokAAAAAASUVORK5CYII=";
6558
+
6444
6559
  // assets/puyuyu.png
6445
6560
  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
6561
 
@@ -6476,7 +6591,8 @@ var VOICE_IMAGES = {
6476
6591
  tsukuyomi: tsukuyomi_default,
6477
6592
  rei: rei_default,
6478
6593
  MGRoid: MGRoid_default,
6479
- MOTRoid: MOTRoid_default
6594
+ MOTRoid: MOTRoid_default,
6595
+ NYNRoid: NYNRoid_default
6480
6596
  };
6481
6597
  var FALLBACK_VOCAL_ICON = Chip_default;
6482
6598
 
@@ -7709,12 +7825,12 @@ var KOE_INFO_HTML = `
7709
7825
 
7710
7826
  <h4>3. DTM\u3067\u4F7F\u3046</h4>
7711
7827
  <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>
7828
+ <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>
7829
+ <li>\u300C\u97F3\u58F0URL\u300D\u306B\u624B\u98062\u306E\u76F4\u63A5\u30EA\u30F3\u30AF\u3092\u8CBC\u308A\u4ED8\u3051\u307E\u3059\u3002</li>
7830
+ <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
7831
  <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
7832
  </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>
7833
+ <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
7834
  </div>
7719
7835
  `;
7720
7836
  var BASE_STEP_WIDTH = 0.5;
@@ -7858,6 +7974,24 @@ var TRACKS_ADVANCED = [
7858
7974
  ];
7859
7975
  var DEFAULT_TRACKS = TRACKS_SIMPLE;
7860
7976
  var BASE_LYRIC_MODELS = ["klatt", ...Object.keys(KOE_VOICEBANKS)];
7977
+ var LYRIC_MODEL_CATEGORIES = [
7978
+ {
7979
+ label: "kusa\u30D7\u30EA\u30BB\u30C3\u30C8",
7980
+ models: ["klatt", "tsukuyomi"]
7981
+ },
7982
+ {
7983
+ label: "\u304A\u3093J",
7984
+ models: ["roze", "shiyo", "rino"]
7985
+ },
7986
+ {
7987
+ label: "\u4E00\u822C",
7988
+ models: ["teto", "rei", "ruko_male", "ruko_female"]
7989
+ },
7990
+ {
7991
+ label: "\u30AF\u30C3\u30AD\u30FC\u2606",
7992
+ models: ["mgroid", "motroid", "nynroid"]
7993
+ }
7994
+ ];
7861
7995
  var BASE_LYRIC_MODEL_LABELS = {
7862
7996
  klatt: "\u8EFD\u91CF\u30ED\u30DC\u58F0",
7863
7997
  ...KOE_VOICEBANK_LABELS
@@ -7865,6 +7999,18 @@ var BASE_LYRIC_MODEL_LABELS = {
7865
7999
  var lyricModelLabel = (model, customMap) => BASE_LYRIC_MODEL_LABELS[model] ?? customMap.get(model)?.label ?? model;
7866
8000
  var CUSTOM_VOCAL_ADD_VALUE = "+custom";
7867
8001
  var CUSTOM_VOCAL_KEY_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
8002
+ var deriveCustomVocalKeyFromUrl = (url2) => {
8003
+ let name;
8004
+ try {
8005
+ const path = new URL(url2).pathname;
8006
+ name = decodeURIComponent(path.slice(path.lastIndexOf("/") + 1));
8007
+ } catch {
8008
+ return "";
8009
+ }
8010
+ name = name.replace(/\.[^.]*$/, "").normalize("NFKC").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
8011
+ if (name && /^[0-9]/.test(name)) name = `_${name}`;
8012
+ return CUSTOM_VOCAL_KEY_RE.test(name) ? name : "";
8013
+ };
7868
8014
  var clamp3 = (v, min, max) => Math.min(Math.max(v, min), max);
7869
8015
  var normalizeInstrumentName = (name) => {
7870
8016
  if (!name) return "";
@@ -7882,13 +8028,16 @@ var mountDAW = (target, options = {}) => {
7882
8028
  const drumPatterns = options.drumPatterns ?? DRUM_PATTERNS;
7883
8029
  const showMidi = !!options.parseMidi;
7884
8030
  const showChord = !isAdvanced;
8031
+ const midiSearchClient = options.midiSearch ? new MidiSearchClient(options.midiSearch) : void 0;
8032
+ const showMidiSearch = !!midiSearchClient?.enabled;
7885
8033
  const refs = buildUI(target, {
7886
8034
  tracks: trackConfigs,
7887
8035
  drumPatternNames: Object.keys(drumPatterns),
7888
8036
  defaultDrumPattern: drumPatterns.dance ? "dance" : Object.keys(drumPatterns)[0] ?? "none",
7889
8037
  defaultBpm: options.defaultBpm ?? DEFAULT_BPM,
7890
8038
  showMidi,
7891
- showChord
8039
+ showChord,
8040
+ showMidiSearch
7892
8041
  });
7893
8042
  const renderConfig = {
7894
8043
  stepsPerBar: 192,
@@ -8501,6 +8650,106 @@ var mountDAW = (target, options = {}) => {
8501
8650
  redrawAll();
8502
8651
  }
8503
8652
  };
8653
+ const BG_DB_NAME = "dtm-daw-db";
8654
+ const BG_DB_STORE = "settings";
8655
+ const BG_DB_KEY = "piano-roll-bg";
8656
+ const BG_OPACITY_KEY = "dtm-piano-roll-bg-opacity";
8657
+ const COMPRESSIBLE_IMAGE_TYPES = /* @__PURE__ */ new Set([
8658
+ "image/jpeg",
8659
+ "image/png",
8660
+ "image/webp",
8661
+ "image/bmp"
8662
+ ]);
8663
+ const openBgDb = () => new Promise((resolve, reject) => {
8664
+ const req = indexedDB.open(BG_DB_NAME, 1);
8665
+ req.onupgradeneeded = () => req.result.createObjectStore(BG_DB_STORE);
8666
+ req.onsuccess = () => resolve(req.result);
8667
+ req.onerror = () => reject(req.error);
8668
+ });
8669
+ const saveBgBlob = async (blob2) => {
8670
+ const db = await openBgDb();
8671
+ try {
8672
+ await new Promise((resolve, reject) => {
8673
+ const tx = db.transaction(BG_DB_STORE, "readwrite");
8674
+ tx.objectStore(BG_DB_STORE).put(blob2, BG_DB_KEY);
8675
+ tx.oncomplete = () => resolve();
8676
+ tx.onerror = () => reject(tx.error);
8677
+ });
8678
+ } finally {
8679
+ db.close();
8680
+ }
8681
+ };
8682
+ const loadBgBlob = async () => {
8683
+ const db = await openBgDb();
8684
+ try {
8685
+ return await new Promise((resolve, reject) => {
8686
+ const tx = db.transaction(BG_DB_STORE, "readonly");
8687
+ const req = tx.objectStore(BG_DB_STORE).get(BG_DB_KEY);
8688
+ req.onsuccess = () => resolve(req.result ?? null);
8689
+ req.onerror = () => reject(req.error);
8690
+ });
8691
+ } finally {
8692
+ db.close();
8693
+ }
8694
+ };
8695
+ const deleteBgBlob = async () => {
8696
+ const db = await openBgDb();
8697
+ try {
8698
+ await new Promise((resolve, reject) => {
8699
+ const tx = db.transaction(BG_DB_STORE, "readwrite");
8700
+ tx.objectStore(BG_DB_STORE).delete(BG_DB_KEY);
8701
+ tx.oncomplete = () => resolve();
8702
+ tx.onerror = () => reject(tx.error);
8703
+ });
8704
+ } finally {
8705
+ db.close();
8706
+ }
8707
+ };
8708
+ const resizeImageToBlob = (img, maxW, maxH) => {
8709
+ const scale = Math.min(1, maxW / img.width, maxH / img.height);
8710
+ const width = Math.max(1, Math.round(img.width * scale));
8711
+ const height = Math.max(1, Math.round(img.height * scale));
8712
+ const canvas = document.createElement("canvas");
8713
+ canvas.width = width;
8714
+ canvas.height = height;
8715
+ const ctx = canvas.getContext("2d");
8716
+ if (ctx) ctx.drawImage(img, 0, 0, width, height);
8717
+ return new Promise((resolve, reject) => {
8718
+ canvas.toBlob(
8719
+ (blob2) => blob2 ? resolve(blob2) : reject(new Error("toBlob failed")),
8720
+ "image/jpeg",
8721
+ 0.7
8722
+ );
8723
+ });
8724
+ };
8725
+ let currentBgObjectUrl = null;
8726
+ const applyBgOpacity = (opacityVal) => {
8727
+ refs.rollContainer.style.setProperty(
8728
+ "--dtm-roll-bg-opacity",
8729
+ String(opacityVal / 100)
8730
+ );
8731
+ refs.bgOpacityInput.value = String(opacityVal);
8732
+ };
8733
+ const applyRollBackground = (blob2) => {
8734
+ if (currentBgObjectUrl) {
8735
+ URL.revokeObjectURL(currentBgObjectUrl);
8736
+ currentBgObjectUrl = null;
8737
+ }
8738
+ if (blob2) {
8739
+ currentBgObjectUrl = URL.createObjectURL(blob2);
8740
+ refs.rollContainer.style.setProperty(
8741
+ "--dtm-roll-bg-image",
8742
+ `url(${currentBgObjectUrl})`
8743
+ );
8744
+ refs.bgOpacityRow.classList.remove("dtm-hidden");
8745
+ } else {
8746
+ refs.rollContainer.style.setProperty("--dtm-roll-bg-image", "none");
8747
+ refs.bgOpacityRow.classList.add("dtm-hidden");
8748
+ }
8749
+ refs.bgRemoveBtn.classList.toggle("dtm-hidden", !blob2);
8750
+ setBackgroundActive(!!blob2);
8751
+ redrawAll();
8752
+ };
8504
8753
  const setupCanvas = () => {
8505
8754
  const w = refs.rollContainer.clientWidth || 800;
8506
8755
  const h = refs.rollContainer.clientHeight || 450;
@@ -8866,10 +9115,8 @@ var mountDAW = (target, options = {}) => {
8866
9115
  <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
9116
  <button class="dtm-btn dtm-btn--ghost dtm-btn--xs" data-dtm="lyric-custom-guide">\u4F7F\u3044\u65B9\u30AC\u30A4\u30C9</button>
8868
9117
  </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">
9118
+ <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">
9119
+ <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
9120
  <div class="dtm-row">
8874
9121
  <span class="dtm-label dtm-grow" data-dtm="lyric-custom-note" style="color:var(--dtm-warn)"></span>
8875
9122
  <button class="dtm-btn dtm-btn--primary" data-dtm="lyric-custom-apply">\u8FFD\u52A0</button>
@@ -8934,12 +9181,6 @@ var mountDAW = (target, options = {}) => {
8934
9181
  const lyricCustomIcon = lyricDiv.querySelector(
8935
9182
  '[data-dtm="lyric-custom-icon"]'
8936
9183
  );
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
9184
  const lyricCustomGuide = lyricDiv.querySelector(
8944
9185
  '[data-dtm="lyric-custom-guide"]'
8945
9186
  );
@@ -8950,23 +9191,35 @@ var mountDAW = (target, options = {}) => {
8950
9191
  '[data-dtm="lyric-custom-apply"]'
8951
9192
  );
8952
9193
  const fmtPan = (pan) => pan === 64 ? "C" : pan < 64 ? `L${64 - pan}` : `R${pan - 64}`;
8953
- const addOpt = (value, label) => {
9194
+ const addOpt = (parent, value, label) => {
8954
9195
  const o = document.createElement("option");
8955
9196
  o.value = value;
8956
9197
  o.textContent = label;
8957
- lyricModelSel.appendChild(o);
9198
+ parent.appendChild(o);
8958
9199
  };
8959
- addOpt("", "\u30DC\u30FC\u30AB\u30EB\u306A\u3057");
8960
- for (const m of BASE_LYRIC_MODELS)
8961
- addOpt(m, lyricModelLabel(m, customVocalsMap));
8962
- 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");
9200
+ addOpt(lyricModelSel, "", "\u30DC\u30FC\u30AB\u30EB\u306A\u3057");
9201
+ for (const cat of LYRIC_MODEL_CATEGORIES) {
9202
+ const group = document.createElement("optgroup");
9203
+ group.label = cat.label;
9204
+ for (const m of cat.models) {
9205
+ addOpt(group, m, lyricModelLabel(m, customVocalsMap));
9206
+ }
9207
+ lyricModelSel.appendChild(group);
9208
+ }
9209
+ const customGroup = document.createElement("optgroup");
9210
+ customGroup.label = "\u30AB\u30B9\u30BF\u30E0\u97F3\u58F0";
9211
+ for (const [k, def] of customVocalsMap) {
9212
+ addOpt(customGroup, k, def.label ?? k);
9213
+ }
9214
+ addOpt(customGroup, CUSTOM_VOCAL_ADD_VALUE, "\u30AB\u30B9\u30BF\u30E0\u97F3\u58F0\u3092\u8FFD\u52A0\u2026");
8964
9215
  if (active.lyricModel && !BASE_LYRIC_MODELS.includes(active.lyricModel) && !customVocalsMap.has(active.lyricModel)) {
8965
9216
  addOpt(
9217
+ customGroup,
8966
9218
  active.lyricModel,
8967
9219
  lyricModelLabel(active.lyricModel, customVocalsMap)
8968
9220
  );
8969
9221
  }
9222
+ lyricModelSel.appendChild(customGroup);
8970
9223
  lyricModelSel.value = active.lyricModel;
8971
9224
  lyricOctaveSel.value = String(active.vocalOctave);
8972
9225
  lyricInput.value = active.lyrics;
@@ -9032,9 +9285,10 @@ var mountDAW = (target, options = {}) => {
9032
9285
  lyricModelSel.addEventListener("change", () => {
9033
9286
  if (lyricModelSel.value === CUSTOM_VOCAL_ADD_VALUE) {
9034
9287
  lyricCustomNote.textContent = "";
9035
- lyricCustomAlias.value = "";
9036
- lyricCustomKey.value = "";
9037
9288
  lyricCustom.classList.remove("dtm-hidden");
9289
+ lyricIcon.removeAttribute("src");
9290
+ lyricIcon.classList.add("dtm-hidden");
9291
+ lyricTerms.classList.add("dtm-hidden");
9038
9292
  return;
9039
9293
  }
9040
9294
  lyricCustom.classList.add("dtm-hidden");
@@ -9044,45 +9298,41 @@ var mountDAW = (target, options = {}) => {
9044
9298
  fireLyricsChange(active);
9045
9299
  });
9046
9300
  lyricCustomGuide.addEventListener("click", () => {
9047
- showModal("\u30AB\u30B9\u30BF\u30E0\u97F3\u6E90(.koe)\u306E\u4F7F\u3044\u65B9", KOE_INFO_HTML);
9301
+ showModal("\u30AB\u30B9\u30BF\u30E0\u97F3\u58F0(.koe)\u306E\u4F7F\u3044\u65B9", KOE_INFO_HTML);
9048
9302
  });
9049
9303
  lyricCustomApply.addEventListener("click", () => {
9050
9304
  const src = lyricCustomSrc.value.trim();
9051
9305
  if (!isValidHttpUrl(src)) {
9052
- lyricCustomNote.textContent = "\u97F3\u6E90URL\u304C\u4E0D\u6B63\u3067\u3059\uFF08http/https\u306E\u307F\u30FB2048\u6587\u5B57\u307E\u3067\uFF09";
9306
+ lyricCustomNote.textContent = "\u97F3\u58F0URL\u304C\u4E0D\u6B63\u3067\u3059\uFF08http/https\u306E\u307F\u30FB2048\u6587\u5B57\u307E\u3067\uFF09";
9053
9307
  return;
9054
9308
  }
9055
9309
  const iconRaw = lyricCustomIcon.value.trim();
9056
9310
  const iconUrl = isValidHttpUrl(iconRaw) ? iconRaw : "";
9057
- const alias = lyricCustomAlias.value.trim();
9058
9311
  const existing = [...customVocalsMap.values()].find(
9059
9312
  (d) => d.url === src
9060
9313
  );
9061
- const keyRaw = lyricCustomKey.value.trim().toLowerCase();
9062
9314
  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;
9315
+ if (existing) {
9316
+ key = existing.key;
9078
9317
  } else {
9079
- key = existing?.key ?? genCustomVocalKey();
9318
+ const derived = deriveCustomVocalKeyFromUrl(src);
9319
+ if (derived && !BASE_LYRIC_MODELS.includes(derived)) {
9320
+ if (!customVocalsMap.has(derived)) {
9321
+ key = derived;
9322
+ } else {
9323
+ let n = 2;
9324
+ while (customVocalsMap.has(`${derived}${n}`)) n++;
9325
+ key = `${derived}${n}`;
9326
+ }
9327
+ } else {
9328
+ key = genCustomVocalKey();
9329
+ }
9080
9330
  }
9081
9331
  registerCustomVocal({
9082
9332
  key,
9083
9333
  iconUrl: iconUrl || (existing?.iconUrl ?? ""),
9084
9334
  url: src,
9085
- label: alias || existing?.label
9335
+ label: existing?.label
9086
9336
  });
9087
9337
  active.lyricModel = key;
9088
9338
  fireLyricsChange(active);
@@ -9361,7 +9611,8 @@ var mountDAW = (target, options = {}) => {
9361
9611
  bpm: parsedBpm,
9362
9612
  lyrics,
9363
9613
  meta,
9364
- mergedTrackCount
9614
+ mergedTrackCount,
9615
+ trackVelocity
9365
9616
  } = parseMML(mml, {
9366
9617
  stepsPerBar: renderConfig.stepsPerBar,
9367
9618
  collectLyrics: true,
@@ -9394,6 +9645,13 @@ var mountDAW = (target, options = {}) => {
9394
9645
  options.onTrackInstrumentChange?.(i, name);
9395
9646
  }
9396
9647
  });
9648
+ trackStates.forEach((t, i) => {
9649
+ const v = trackVelocity.get(i);
9650
+ if (v !== void 0 && v !== t.volume) {
9651
+ t.volume = v;
9652
+ t.core.setVolume(v);
9653
+ }
9654
+ });
9397
9655
  for (const t of trackStates) {
9398
9656
  t.lyrics = "";
9399
9657
  t.lyricModel = "";
@@ -9585,7 +9843,7 @@ var mountDAW = (target, options = {}) => {
9585
9843
  refs.prevBarBtn.addEventListener("click", () => {
9586
9844
  const targetStep = Math.max(
9587
9845
  0,
9588
- Math.floor((getCurrentPlayStep() - 1) / renderConfig.stepsPerBar) * renderConfig.stepsPerBar
9846
+ (Math.floor((getCurrentPlayStep() - 1) / renderConfig.stepsPerBar) - 1) * renderConfig.stepsPerBar
9589
9847
  );
9590
9848
  jumpTo(targetStep);
9591
9849
  });
@@ -9630,6 +9888,41 @@ var mountDAW = (target, options = {}) => {
9630
9888
  applyZoomY();
9631
9889
  notifyViewState();
9632
9890
  });
9891
+ refs.bgUploadBtn.addEventListener("click", () => refs.bgFileInput.click());
9892
+ refs.bgFileInput.addEventListener("change", () => {
9893
+ const file = refs.bgFileInput.files?.[0];
9894
+ refs.bgFileInput.value = "";
9895
+ if (!file) return;
9896
+ if (!COMPRESSIBLE_IMAGE_TYPES.has(file.type)) {
9897
+ saveBgBlob(file).then(() => applyRollBackground(file)).catch(() => {
9898
+ });
9899
+ return;
9900
+ }
9901
+ const objectUrl = URL.createObjectURL(file);
9902
+ const img = new Image();
9903
+ img.onload = () => {
9904
+ URL.revokeObjectURL(objectUrl);
9905
+ const maxW = refs.rollContainer.clientWidth || 800;
9906
+ const maxH = refs.rollContainer.clientHeight || 450;
9907
+ resizeImageToBlob(img, maxW, maxH).then(
9908
+ (blob2) => saveBgBlob(blob2).then(() => applyRollBackground(blob2))
9909
+ ).catch(() => {
9910
+ });
9911
+ };
9912
+ img.src = objectUrl;
9913
+ });
9914
+ refs.bgRemoveBtn.addEventListener("click", () => {
9915
+ deleteBgBlob().catch(() => {
9916
+ }).finally(() => applyRollBackground(null));
9917
+ });
9918
+ refs.bgOpacityInput.addEventListener("input", () => {
9919
+ const opacityVal = Number.parseInt(refs.bgOpacityInput.value, 10);
9920
+ applyBgOpacity(opacityVal);
9921
+ try {
9922
+ localStorage.setItem(BG_OPACITY_KEY, String(opacityVal));
9923
+ } catch (_) {
9924
+ }
9925
+ });
9633
9926
  refs.decomposeChordToggle.addEventListener("change", notifyViewState);
9634
9927
  refs.ignoreChordHeavyToggle.addEventListener("change", notifyViewState);
9635
9928
  refs.masterVolume.addEventListener("input", () => {
@@ -9702,11 +9995,11 @@ var mountDAW = (target, options = {}) => {
9702
9995
  refs.mmlLoadBtn.addEventListener("click", async () => {
9703
9996
  const mml = refs.mmlInput.value;
9704
9997
  if (!isAdvanced && options.onRequestAdvancedMode) {
9705
- const { mergedTrackCount } = parseMML(mml, {
9998
+ const { mergedTrackCount, meta } = parseMML(mml, {
9706
9999
  stepsPerBar: renderConfig.stepsPerBar,
9707
10000
  clampTrackCount: trackStates.length
9708
10001
  });
9709
- if (mergedTrackCount > 0) {
10002
+ if (mergedTrackCount > 0 || meta.mode === "advanced") {
9710
10003
  const confirmed = await showConfirmModal(
9711
10004
  "\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"
9712
10005
  );
@@ -9740,6 +10033,7 @@ var mountDAW = (target, options = {}) => {
9740
10033
  };
9741
10034
  showModal = (title, bodyHTML) => {
9742
10035
  collapseActiveSample();
10036
+ collapseSearchPreview();
9743
10037
  refs.modalTitle.textContent = title;
9744
10038
  refs.modalBody.innerHTML = bodyHTML;
9745
10039
  refs.modalOverlay.removeAttribute("hidden");
@@ -9828,11 +10122,13 @@ var mountDAW = (target, options = {}) => {
9828
10122
  };
9829
10123
  refs.modalClose.addEventListener("click", () => {
9830
10124
  collapseActiveSample();
10125
+ collapseSearchPreview();
9831
10126
  refs.modalOverlay.setAttribute("hidden", "");
9832
10127
  });
9833
10128
  refs.modalOverlay.addEventListener("click", (e) => {
9834
10129
  if (e.target === refs.modalOverlay) {
9835
10130
  collapseActiveSample();
10131
+ collapseSearchPreview();
9836
10132
  refs.modalOverlay.setAttribute("hidden", "");
9837
10133
  }
9838
10134
  });
@@ -9853,6 +10149,7 @@ var mountDAW = (target, options = {}) => {
9853
10149
  })
9854
10150
  );
9855
10151
  if (showMidi) wireMidi();
10152
+ if (showMidiSearch) wireMidiSearch();
9856
10153
  document.addEventListener("keydown", onKeyDown);
9857
10154
  refs.root.addEventListener("keydown", (e) => {
9858
10155
  const t = e.target;
@@ -9917,6 +10214,236 @@ var mountDAW = (target, options = {}) => {
9917
10214
  overlayDuring(() => applyMidiSelection(pendingMidi, selected));
9918
10215
  });
9919
10216
  };
10217
+ const buildPreviewMml = (midi) => {
10218
+ const analysis = analyzeMidiTracks(midi);
10219
+ const selected = analysis.filter((a) => a.selected).map((a) => a.index);
10220
+ const { placements, bpm: parsedBpm } = extractMidiPlacements(
10221
+ midi,
10222
+ selected
10223
+ );
10224
+ const byTrack = /* @__PURE__ */ new Map();
10225
+ for (const p of placements) {
10226
+ if (!byTrack.has(p.trackId)) byTrack.set(p.trackId, []);
10227
+ byTrack.get(p.trackId)?.push(p);
10228
+ }
10229
+ const refCore = trackStates[0].core;
10230
+ const tempo = Math.round(parsedBpm) || bpm;
10231
+ const lines = [];
10232
+ let i = 0;
10233
+ for (const notes of byTrack.values()) {
10234
+ const asNotes = notes.map((p) => ({
10235
+ id: 0,
10236
+ startStep: p.startStep,
10237
+ durationSteps: p.durationSteps,
10238
+ pitch: p.pitch,
10239
+ velocity: p.velocity
10240
+ }));
10241
+ const mml = refCore.getMMLFromNotes(asNotes, tempo, 100).trim();
10242
+ lines.push(`@${i} ${mml}`);
10243
+ i++;
10244
+ }
10245
+ return [...lines, MML_END_MARKER].join(";\n");
10246
+ };
10247
+ const fetchVerifiedMidi = async (fileName) => {
10248
+ if (!options.parseMidi || !midiSearchClient) {
10249
+ throw new Error("parseMidi/midiSearchClient not injected");
10250
+ }
10251
+ const midiBytes = await midiSearchClient.fetchMidi(fileName);
10252
+ const midi = await options.parseMidi(new Uint8Array(midiBytes));
10253
+ const analysis = analyzeMidiTracks(midi);
10254
+ const selected = analysis.filter((a) => a.selected).map((a) => a.index);
10255
+ if (!isPlausibleMidiTranscription(midi, selected)) {
10256
+ throw new Error("implausible MIDI transcription");
10257
+ }
10258
+ return midi;
10259
+ };
10260
+ let searchPreviewPlayer = null;
10261
+ let searchPreviewBtn = null;
10262
+ const collapseSearchPreview = () => {
10263
+ if (searchPreviewPlayer) {
10264
+ searchPreviewPlayer.stop();
10265
+ searchPreviewPlayer.destroy();
10266
+ searchPreviewPlayer = null;
10267
+ }
10268
+ if (searchPreviewBtn) {
10269
+ searchPreviewBtn.textContent = "\u25B6 \u8A66\u8074";
10270
+ searchPreviewBtn.classList.remove("dtm-btn--danger");
10271
+ searchPreviewBtn.classList.add("dtm-btn--ghost");
10272
+ const container = searchPreviewBtn.closest(".dtm-modal-sample-box")?.querySelector(".dtm-modal-sample-player-container");
10273
+ if (container) container.innerHTML = "";
10274
+ searchPreviewBtn = null;
10275
+ }
10276
+ };
10277
+ const wireMidiSearch = () => {
10278
+ if (!midiSearchClient) return;
10279
+ refs.midiSearchOpenBtn.addEventListener("click", () => {
10280
+ showModal(
10281
+ "MML\u691C\u7D22",
10282
+ `
10283
+ <div class="dtm-row" style="flex-wrap:nowrap">
10284
+ <input type="text" class="dtm-input dtm-grow" data-dtm="modal-midi-search-input" placeholder="\u66F2\u540D\u3067MML\u691C\u7D22" style="min-width:0">
10285
+ <button class="dtm-btn dtm-btn--primary" data-dtm="modal-midi-search-btn" style="flex-shrink:0">\u691C\u7D22</button>
10286
+ </div>
10287
+ <div class="dtm-row" data-dtm="modal-midi-search-results" style="flex-direction:column;align-items:stretch;gap:4px"></div>
10288
+ `
10289
+ );
10290
+ const input = refs.modalBody.querySelector(
10291
+ '[data-dtm="modal-midi-search-input"]'
10292
+ );
10293
+ const searchBtn = refs.modalBody.querySelector(
10294
+ '[data-dtm="modal-midi-search-btn"]'
10295
+ );
10296
+ const results = refs.modalBody.querySelector(
10297
+ '[data-dtm="modal-midi-search-results"]'
10298
+ );
10299
+ const runSearch = async () => {
10300
+ const q2 = input.value.trim();
10301
+ if (!q2) return;
10302
+ collapseSearchPreview();
10303
+ searchBtn.disabled = true;
10304
+ searchBtn.textContent = "\u691C\u7D22\u4E2D...";
10305
+ results.innerHTML = "";
10306
+ try {
10307
+ const songs = await midiSearchClient.searchSongs({ title: q2 });
10308
+ if (songs.length === 0) {
10309
+ results.innerHTML = '<span class="dtm-label" style="color:var(--dtm-warn)">\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F</span>';
10310
+ return;
10311
+ }
10312
+ for (const song of songs) {
10313
+ const box = document.createElement("div");
10314
+ box.className = "dtm-modal-sample-box";
10315
+ const row = document.createElement("div");
10316
+ row.className = "dtm-row";
10317
+ row.style.cssText = "flex-wrap:nowrap;gap:4px;padding:2px 0";
10318
+ const label = document.createElement("span");
10319
+ label.className = "dtm-label";
10320
+ label.style.cssText = "flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap";
10321
+ label.textContent = `${song.title} - ${song.user}`;
10322
+ const previewBtn = document.createElement("button");
10323
+ previewBtn.className = "dtm-btn dtm-btn--ghost";
10324
+ previewBtn.style.cssText = "flex-shrink:0";
10325
+ previewBtn.textContent = "\u25B6 \u8A66\u8074";
10326
+ const loadBtn = document.createElement("button");
10327
+ loadBtn.className = "dtm-btn dtm-btn--primary";
10328
+ loadBtn.style.cssText = "flex-shrink:0";
10329
+ loadBtn.textContent = "\u8AAD\u307F\u8FBC\u307F";
10330
+ const playerContainer = document.createElement("div");
10331
+ playerContainer.className = "dtm-modal-sample-player-container";
10332
+ previewBtn.addEventListener("click", async () => {
10333
+ if (searchPreviewBtn === previewBtn) {
10334
+ if (searchPreviewPlayer?.isPlaying()) {
10335
+ searchPreviewPlayer.stop();
10336
+ } else {
10337
+ stop();
10338
+ previewBtn.textContent = "\u8AAD\u8FBC\u4E2D...";
10339
+ try {
10340
+ const midi = await fetchVerifiedMidi(song.file);
10341
+ const mml = buildPreviewMml(midi);
10342
+ playerContainer.innerHTML = "";
10343
+ const player = mountMmlPlayer(playerContainer, mml, {
10344
+ onPlayNote: options.onPlayNote,
10345
+ onPlayDrum: options.onPlayDrum,
10346
+ onResumeAudio: options.onResumeAudio,
10347
+ getAudioTime: options.getAudioTime,
10348
+ singingVoices: options.singingVoices,
10349
+ drumPatterns: options.drumPatterns,
10350
+ volume: masterVolume,
10351
+ _skipInfoModals: true,
10352
+ onStop: () => {
10353
+ if (searchPreviewBtn === previewBtn) {
10354
+ previewBtn.textContent = "\u25B6 \u8A66\u8074";
10355
+ previewBtn.classList.remove("dtm-btn--danger");
10356
+ previewBtn.classList.add("dtm-btn--ghost");
10357
+ }
10358
+ }
10359
+ });
10360
+ searchPreviewPlayer = player;
10361
+ searchPreviewBtn = previewBtn;
10362
+ player.play();
10363
+ previewBtn.textContent = "\u25A0 \u505C\u6B62";
10364
+ previewBtn.classList.remove("dtm-btn--ghost");
10365
+ previewBtn.classList.add("dtm-btn--danger");
10366
+ } catch (e) {
10367
+ console.error("[dtm] MIDI preview failed", e);
10368
+ previewBtn.textContent = "\u5931\u6557";
10369
+ }
10370
+ }
10371
+ } else {
10372
+ collapseSearchPreview();
10373
+ stop();
10374
+ previewBtn.textContent = "\u8AAD\u8FBC\u4E2D...";
10375
+ try {
10376
+ const midi = await fetchVerifiedMidi(song.file);
10377
+ const mml = buildPreviewMml(midi);
10378
+ playerContainer.innerHTML = "";
10379
+ const player = mountMmlPlayer(playerContainer, mml, {
10380
+ onPlayNote: options.onPlayNote,
10381
+ onPlayDrum: options.onPlayDrum,
10382
+ onResumeAudio: options.onResumeAudio,
10383
+ getAudioTime: options.getAudioTime,
10384
+ singingVoices: options.singingVoices,
10385
+ drumPatterns: options.drumPatterns,
10386
+ volume: masterVolume,
10387
+ _skipInfoModals: true,
10388
+ onStop: () => {
10389
+ if (searchPreviewBtn === previewBtn) {
10390
+ previewBtn.textContent = "\u25B6 \u8A66\u8074";
10391
+ previewBtn.classList.remove("dtm-btn--danger");
10392
+ previewBtn.classList.add("dtm-btn--ghost");
10393
+ }
10394
+ }
10395
+ });
10396
+ searchPreviewPlayer = player;
10397
+ searchPreviewBtn = previewBtn;
10398
+ player.play();
10399
+ previewBtn.textContent = "\u25A0 \u505C\u6B62";
10400
+ previewBtn.classList.remove("dtm-btn--ghost");
10401
+ previewBtn.classList.add("dtm-btn--danger");
10402
+ } catch (e) {
10403
+ console.error("[dtm] MIDI preview failed", e);
10404
+ previewBtn.textContent = "\u5931\u6557";
10405
+ }
10406
+ }
10407
+ });
10408
+ loadBtn.addEventListener("click", async () => {
10409
+ loadBtn.disabled = true;
10410
+ loadBtn.textContent = "\u8AAD\u8FBC\u4E2D...";
10411
+ try {
10412
+ const pending = await fetchVerifiedMidi(song.file);
10413
+ const analysis = analyzeMidiTracks(pending);
10414
+ const sel = analysis.filter((a) => a.selected).map((a) => a.index);
10415
+ collapseSearchPreview();
10416
+ overlayDuring(() => applyMidiSelection(pending, sel));
10417
+ refs.modalOverlay.setAttribute("hidden", "");
10418
+ } catch (e) {
10419
+ console.error("[dtm] MIDI fetch failed", e);
10420
+ loadBtn.textContent = "\u5931\u6557";
10421
+ } finally {
10422
+ loadBtn.disabled = false;
10423
+ }
10424
+ });
10425
+ row.appendChild(label);
10426
+ row.appendChild(previewBtn);
10427
+ row.appendChild(loadBtn);
10428
+ box.appendChild(row);
10429
+ box.appendChild(playerContainer);
10430
+ results.appendChild(box);
10431
+ }
10432
+ } catch (e) {
10433
+ console.error("[dtm] MIDI search failed", e);
10434
+ results.innerHTML = '<span class="dtm-label" style="color:var(--dtm-warn)">\u691C\u7D22\u306B\u5931\u6557\u3057\u307E\u3057\u305F</span>';
10435
+ } finally {
10436
+ searchBtn.disabled = false;
10437
+ searchBtn.textContent = "\u691C\u7D22";
10438
+ }
10439
+ };
10440
+ searchBtn.addEventListener("click", () => void runSearch());
10441
+ input.addEventListener("keydown", (e) => {
10442
+ if (e.key === "Enter") void runSearch();
10443
+ });
10444
+ input.focus();
10445
+ });
10446
+ };
9920
10447
  const onKeyDown = (e) => {
9921
10448
  if (!(e.ctrlKey || e.metaKey)) return;
9922
10449
  if (e.code === "KeyZ" && !e.shiftKey) {
@@ -9972,6 +10499,22 @@ var mountDAW = (target, options = {}) => {
9972
10499
  updateUndoRedo();
9973
10500
  redrawAll();
9974
10501
  if (options.initialMML) loadMML(options.initialMML);
10502
+ let initialOpacity = 40;
10503
+ try {
10504
+ const storedOpacity = localStorage.getItem(BG_OPACITY_KEY);
10505
+ if (storedOpacity !== null) {
10506
+ initialOpacity = Number.parseInt(storedOpacity, 10);
10507
+ if (Number.isNaN(initialOpacity) || initialOpacity < 0 || initialOpacity > 100) {
10508
+ initialOpacity = 40;
10509
+ }
10510
+ }
10511
+ } catch (_) {
10512
+ }
10513
+ applyBgOpacity(initialOpacity);
10514
+ loadBgBlob().then((blob2) => {
10515
+ if (blob2) applyRollBackground(blob2);
10516
+ }).catch(() => {
10517
+ });
9975
10518
  let resizeTimer = null;
9976
10519
  const resizeObserver = new ResizeObserver(() => {
9977
10520
  if (resizeTimer) clearTimeout(resizeTimer);
@@ -11293,13 +11836,17 @@ var createDtmStudio = async (options = {}) => {
11293
11836
  const mountedPlayers = [];
11294
11837
  const mountedModeSwitches = [];
11295
11838
  const mountEditor = (target, opts = {}) => {
11839
+ const mergedOpts = {
11840
+ midiSearch: options.midiSearch,
11841
+ ...opts
11842
+ };
11296
11843
  const {
11297
11844
  preset,
11298
11845
  presetUI,
11299
11846
  onInstrumentChange,
11300
11847
  onTrackInstrumentChange: externalOnTrackInstrumentChange,
11301
11848
  ...dawOverrides
11302
- } = opts;
11849
+ } = mergedOpts;
11303
11850
  const tracks = dawOverrides.tracks ?? TRACKS_SIMPLE;
11304
11851
  const trackIds = tracks.map((t) => t.id);
11305
11852
  const presetKey = preset && INSTRUMENT_PRESETS[preset] ? preset : defaultPreset;
@@ -11635,6 +12182,7 @@ var createDtmStudio = async (options = {}) => {
11635
12182
  MAX_VOCAL_VOLUME,
11636
12183
  MMLCore,
11637
12184
  MML_END_MARKER,
12185
+ MidiSearchClient,
11638
12186
  PITCH_MAP,
11639
12187
  PREWARM_NOTES,
11640
12188
  TRACKS_ADVANCED,
@@ -11684,6 +12232,7 @@ var createDtmStudio = async (options = {}) => {
11684
12232
  init,
11685
12233
  injectStyles,
11686
12234
  isChordHeavyTrack,
12235
+ isPlausibleMidiTranscription,
11687
12236
  isValidHttpUrl,
11688
12237
  koeUrl,
11689
12238
  mountDAW,
@@ -11697,6 +12246,7 @@ var createDtmStudio = async (options = {}) => {
11697
12246
  parseMmlMeta,
11698
12247
  playMML,
11699
12248
  playSingingMML,
12249
+ setBackgroundActive,
11700
12250
  setDrawOffset,
11701
12251
  shiftNotes,
11702
12252
  showLoadingOverlay,