@onjmin/dtm 2.1.2 → 2.1.4

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.d.mts CHANGED
@@ -2077,6 +2077,13 @@ type DawOptions = {
2077
2077
  customVocals?: CustomVocalDef[];
2078
2078
  defaultBpm?: number;
2079
2079
  initialMML?: string;
2080
+ /**
2081
+ * ループ再生の初期状態(既定 false)。
2082
+ * MML読み込み時に #loop=on があればそちらが優先される。
2083
+ */
2084
+ initialLoop?: boolean;
2085
+ /** ループ再生のON/OFFが変化したときに呼ばれる */
2086
+ onLoopChange?: (loop: boolean) => void;
2080
2087
  /** 利用規約への同意画面の表示をスキップするかどうか */
2081
2088
  skipConsent?: boolean;
2082
2089
  /**
@@ -2123,6 +2130,10 @@ type DawInstance = {
2123
2130
  loadMIDI: (bytes: Uint8Array) => void | Promise<void>;
2124
2131
  exportMIDI: () => Blob;
2125
2132
  setBpm: (bpm: number) => void;
2133
+ /** 現在のループ再生状態(ON/OFF)を返す */
2134
+ getLoop: () => boolean;
2135
+ /** ループ再生状態を設定する。UIおよび再生シーケンサへ反映される。 */
2136
+ setLoop: (loop: boolean) => void;
2126
2137
  getPlaybackState: () => PlaybackState;
2127
2138
  getCurrentPlayStep: () => number;
2128
2139
  forcePauseAt: (step: number) => void;
@@ -2787,6 +2798,11 @@ type MmlMeta = {
2787
2798
  * チャンネルストリップ共通のパラメータ(歌詞トラック固有の `vocalDelay`/`d`トークンとは別軸)。
2788
2799
  */
2789
2800
  trackDelaySend?: Record<number, number>;
2801
+ /**
2802
+ * ループ再生の指定(#loop=on / off)。BGM等で曲末停止せずシームレスにループさせる。
2803
+ * 省略時はオフ。
2804
+ */
2805
+ loop?: boolean;
2790
2806
  };
2791
2807
  /** MMLからトップレベル宣言を抽出する */
2792
2808
  declare const parseMmlMeta: (mml: string) => MmlMeta;
package/dist/index.d.ts CHANGED
@@ -2077,6 +2077,13 @@ type DawOptions = {
2077
2077
  customVocals?: CustomVocalDef[];
2078
2078
  defaultBpm?: number;
2079
2079
  initialMML?: string;
2080
+ /**
2081
+ * ループ再生の初期状態(既定 false)。
2082
+ * MML読み込み時に #loop=on があればそちらが優先される。
2083
+ */
2084
+ initialLoop?: boolean;
2085
+ /** ループ再生のON/OFFが変化したときに呼ばれる */
2086
+ onLoopChange?: (loop: boolean) => void;
2080
2087
  /** 利用規約への同意画面の表示をスキップするかどうか */
2081
2088
  skipConsent?: boolean;
2082
2089
  /**
@@ -2123,6 +2130,10 @@ type DawInstance = {
2123
2130
  loadMIDI: (bytes: Uint8Array) => void | Promise<void>;
2124
2131
  exportMIDI: () => Blob;
2125
2132
  setBpm: (bpm: number) => void;
2133
+ /** 現在のループ再生状態(ON/OFF)を返す */
2134
+ getLoop: () => boolean;
2135
+ /** ループ再生状態を設定する。UIおよび再生シーケンサへ反映される。 */
2136
+ setLoop: (loop: boolean) => void;
2126
2137
  getPlaybackState: () => PlaybackState;
2127
2138
  getCurrentPlayStep: () => number;
2128
2139
  forcePauseAt: (step: number) => void;
@@ -2787,6 +2798,11 @@ type MmlMeta = {
2787
2798
  * チャンネルストリップ共通のパラメータ(歌詞トラック固有の `vocalDelay`/`d`トークンとは別軸)。
2788
2799
  */
2789
2800
  trackDelaySend?: Record<number, number>;
2801
+ /**
2802
+ * ループ再生の指定(#loop=on / off)。BGM等で曲末停止せずシームレスにループさせる。
2803
+ * 省略時はオフ。
2804
+ */
2805
+ loop?: boolean;
2790
2806
  };
2791
2807
  /** MMLからトップレベル宣言を抽出する */
2792
2808
  declare const parseMmlMeta: (mml: string) => MmlMeta;
package/dist/index.js CHANGED
@@ -4395,7 +4395,7 @@ var NATURAL_STEPS2 = {
4395
4395
  };
4396
4396
  var isNatural = (ch) => Object.hasOwn(NATURAL_STEPS2[12], ch);
4397
4397
  var clamp3 = (value, lo, hi) => Math.min(hi, Math.max(lo, value));
4398
- var META_DIRECTIVE = /#(inst|drum|drumfont|volume|drumvolume|reverb|reverbdecay|reverbpredelay|delay|delaydiv|mastercomp|fadein|fadeout|mode|edo)=([\w:-]+)/gi;
4398
+ var META_DIRECTIVE = /#(inst|drum|drumfont|volume|drumvolume|reverb|reverbdecay|reverbpredelay|delay|delaydiv|mastercomp|fadein|fadeout|mode|edo|loop)=([\w:-]+)/gi;
4399
4399
  var TRACK_INST_DIRECTIVE = /#t(\d+)inst=([^#;\r\n]+)/gi;
4400
4400
  var TRACK_COMP_DIRECTIVE = /#t(\d+)comp=(\d+)/gi;
4401
4401
  var TRACK_WIDTH_DIRECTIVE = /#t(\d+)width=(\d+)/gi;
@@ -4448,6 +4448,9 @@ var parseMmlMeta = (mml) => {
4448
4448
  } else if (key === "edo") {
4449
4449
  const e = Number.parseInt(m[2], 10);
4450
4450
  if (e === 12 || e === 31) meta.edo = e;
4451
+ } else if (key === "loop") {
4452
+ const v = m[2].toLowerCase();
4453
+ meta.loop = v === "on" || v === "1" || v === "true";
4451
4454
  }
4452
4455
  }
4453
4456
  for (const m of mml.matchAll(TRACK_INST_DIRECTIVE)) {
@@ -4551,6 +4554,7 @@ var formatMmlMeta = (meta, space = "") => {
4551
4554
  parts.push(`#fadeout=${meta.fadeOut}`);
4552
4555
  if (meta.mode) parts.push(`#mode=${meta.mode}`);
4553
4556
  if (meta.edo !== void 0 && meta.edo !== 12) parts.push(`#edo=${meta.edo}`);
4557
+ if (meta.loop) parts.push("#loop=on");
4554
4558
  if (meta.trackInstruments) {
4555
4559
  for (const [idx, name] of Object.entries(meta.trackInstruments)) {
4556
4560
  if (name) parts.push(`#t${idx}inst=${name}`);
@@ -5078,7 +5082,11 @@ var createSequencer = (options) => {
5078
5082
  if (!active) return;
5079
5083
  const sps = secondsPerStep();
5080
5084
  const time = options.getAudioTime() - startTime;
5081
- options.onTick(getWrappedPlayStep(time, sps));
5085
+ try {
5086
+ options.onTick(getWrappedPlayStep(time, sps));
5087
+ } catch (err2) {
5088
+ console.error("[sequencer] error in onTick callback:", err2);
5089
+ }
5082
5090
  animationId = requestAnimationFrame(animate);
5083
5091
  };
5084
5092
  const stop = () => {
@@ -10840,6 +10848,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
10840
10848
  options.onStop?.();
10841
10849
  };
10842
10850
  let isSeeking = false;
10851
+ let lastCommittedStep = null;
10843
10852
  const seekTo = (step) => {
10844
10853
  const clamped = Math.min(maxStep, Math.max(0, Math.round(step)));
10845
10854
  renderPlayhead(clamped);
@@ -10851,16 +10860,42 @@ var mountMmlPlayer = (target, mml, options = {}) => {
10851
10860
  play(clamped);
10852
10861
  }
10853
10862
  };
10854
- seekInput.addEventListener("pointerdown", () => {
10863
+ const commitSeek = () => {
10864
+ isSeeking = false;
10865
+ const step = Number(seekInput.value);
10866
+ if (lastCommittedStep !== step) {
10867
+ lastCommittedStep = step;
10868
+ seekTo(step);
10869
+ }
10870
+ };
10871
+ seekInput.addEventListener("pointerdown", (e) => {
10855
10872
  isSeeking = true;
10873
+ lastCommittedStep = null;
10874
+ try {
10875
+ seekInput.setPointerCapture(e.pointerId);
10876
+ } catch {
10877
+ }
10856
10878
  });
10857
10879
  seekInput.addEventListener("input", () => {
10858
10880
  renderPlayhead(Number(seekInput.value));
10859
10881
  syncSeekFill();
10860
10882
  });
10861
10883
  seekInput.addEventListener("change", () => {
10884
+ commitSeek();
10885
+ });
10886
+ seekInput.addEventListener("pointerup", (e) => {
10887
+ try {
10888
+ seekInput.releasePointerCapture(e.pointerId);
10889
+ } catch {
10890
+ }
10891
+ commitSeek();
10892
+ });
10893
+ seekInput.addEventListener("pointercancel", (e) => {
10894
+ try {
10895
+ seekInput.releasePointerCapture(e.pointerId);
10896
+ } catch {
10897
+ }
10862
10898
  isSeeking = false;
10863
- seekTo(Number(seekInput.value));
10864
10899
  });
10865
10900
  playBtn.addEventListener("click", () => {
10866
10901
  if (playing) pause();
@@ -12405,19 +12440,45 @@ var mountChordPlayer = (target, chords, options = {}) => {
12405
12440
  }
12406
12441
  play(idx);
12407
12442
  };
12408
- seekInput.addEventListener("pointerdown", () => {
12443
+ let lastCommittedSec = null;
12444
+ const commitSeek = () => {
12445
+ isSeeking = false;
12446
+ if (totalSec <= 0) return;
12447
+ const sec = Number(seekInput.value);
12448
+ if (lastCommittedSec !== sec) {
12449
+ lastCommittedSec = sec;
12450
+ const idx = getActiveIndexBySec(sec);
12451
+ seekTo(idx >= 0 ? idx : Math.max(0, chordEvents.length - 1));
12452
+ }
12453
+ };
12454
+ seekInput.addEventListener("pointerdown", (e) => {
12409
12455
  isSeeking = true;
12456
+ lastCommittedSec = null;
12457
+ try {
12458
+ seekInput.setPointerCapture(e.pointerId);
12459
+ } catch {
12460
+ }
12410
12461
  });
12411
12462
  seekInput.addEventListener("input", () => {
12412
12463
  updateTimeDisplay(Number(seekInput.value));
12413
12464
  syncSeekFill();
12414
12465
  });
12415
12466
  seekInput.addEventListener("change", () => {
12467
+ commitSeek();
12468
+ });
12469
+ seekInput.addEventListener("pointerup", (e) => {
12470
+ try {
12471
+ seekInput.releasePointerCapture(e.pointerId);
12472
+ } catch {
12473
+ }
12474
+ commitSeek();
12475
+ });
12476
+ seekInput.addEventListener("pointercancel", (e) => {
12477
+ try {
12478
+ seekInput.releasePointerCapture(e.pointerId);
12479
+ } catch {
12480
+ }
12416
12481
  isSeeking = false;
12417
- if (totalSec <= 0) return;
12418
- const sec = Number(seekInput.value);
12419
- const idx = getActiveIndexBySec(sec);
12420
- seekTo(idx >= 0 ? idx : Math.max(0, chordEvents.length - 1));
12421
12482
  });
12422
12483
  const stop = () => {
12423
12484
  if (isLoading) {
@@ -15530,6 +15591,15 @@ var buildUI = (target, options) => {
15530
15591
  </select>
15531
15592
  <button class="dtm-infobtn" data-dtm="edo-info" title="\u97F3\u5F8B\u306E\u89E3\u8AAC">${icon("info", 12)}</button>
15532
15593
  </div>
15594
+ <div class="dtm-row">
15595
+ <span class="dtm-label">\u30EB\u30FC\u30D7\u518D\u751F</span>
15596
+ <label class="dtm-toggle">
15597
+ <input type="checkbox" data-dtm="loop-toggle">
15598
+ <span data-dtm="loop-toggle-label">OFF</span>
15599
+ </label>
15600
+ <span class="dtm-grow"></span>
15601
+ <button class="dtm-infobtn" data-dtm="loop-info" title="\u30EB\u30FC\u30D7\u518D\u751F\u306E\u89E3\u8AAC">${icon("info", 12)}</button>
15602
+ </div>
15533
15603
  <div class="dtm-row">
15534
15604
  <span class="dtm-label">\u5168\u4F53\u97F3\u91CF</span>
15535
15605
  <input type="range" class="dtm-range dtm-grow" data-dtm="master-volume" value="50" min="0" max="100">
@@ -15867,6 +15937,9 @@ var buildUI = (target, options) => {
15867
15937
  vScrollThumb: sel("vscroll-thumb"),
15868
15938
  hScroll: sel("hscroll"),
15869
15939
  hScrollThumb: sel("hscroll-thumb"),
15940
+ loopToggle: sel("loop-toggle"),
15941
+ loopToggleLabel: sel("loop-toggle-label"),
15942
+ loopInfoBtn: sel("loop-info"),
15870
15943
  masterVolume: sel("master-volume"),
15871
15944
  masterVolumeLabel: sel("master-volume-label"),
15872
15945
  masterComp: sel("master-comp"),
@@ -17721,8 +17794,10 @@ var createRenderer = (mountTarget, width = 800, height = 450, config) => {
17721
17794
  const getXY = (e) => {
17722
17795
  const { clientX, clientY } = e;
17723
17796
  const rect = g_grid_canvas.getBoundingClientRect();
17724
- const x2 = Math.floor(clientX - rect.left);
17725
- const y = Math.floor(clientY - rect.top);
17797
+ const scaleX = rect.width > 0 ? g_grid_canvas.width / rect.width : 1;
17798
+ const scaleY = rect.height > 0 ? g_grid_canvas.height / rect.height : 1;
17799
+ const x2 = Math.floor((clientX - rect.left) * scaleX);
17800
+ const y = Math.floor((clientY - rect.top) * scaleY);
17726
17801
  return [x2, y, e.buttons];
17727
17802
  };
17728
17803
  const getGridPosition = (e) => {
@@ -17756,7 +17831,7 @@ var createRenderer = (mountTarget, width = 800, height = 450, config) => {
17756
17831
  const absoluteY = y + g_draw_offset_y;
17757
17832
  const yIndex = Math.floor(absoluteY / keyHeight);
17758
17833
  const pitch = units(pitchRangeStart + (keyCount - 1 - yIndex) * upr);
17759
- if (pitch >= pitchRangeStart && pitch < pitchRangeStart + keyCount) {
17834
+ if (pitch >= pitchRangeStart && pitch < pitchRangeStart + keyCount * upr) {
17760
17835
  requestAnimationFrame(() => callback(step, pitch));
17761
17836
  }
17762
17837
  },
@@ -18050,6 +18125,16 @@ var TRACK_DELAYSEND_INFO_HTML = `
18050
18125
  <p>[delay]\u30D1\u30CD\u30EB\u306E\u639B\u304B\u308A\u5177\u5408\u304C0%\u3060\u3068\u3001\u3053\u3053\u3092\u3044\u304F\u3089\u4E0A\u3052\u3066\u3082\u7121\u97F3\u306E\u307E\u307E\u3067\u3059\u3002\u300C\u9001\u308A\u91CF\uFF08\u3053\u306E\u30C8\u30E9\u30C3\u30AF\u304C\u3069\u308C\u3060\u3051\u63D0\u4F9B\u3059\u308B\u304B\uFF09\u300D\u3068\u300C\u30DE\u30B9\u30BF\u306E\u30C7\u30A3\u30EC\u30A4\u8A2D\u5B9A\uFF08\u5B9F\u969B\u306B\u3069\u3093\u306A\u7E70\u308A\u8FD4\u3057\u304C\u639B\u304B\u308B\u304B\uFF09\u300D\u306E\u4E8C\u6BB5\u69CB\u3048\u306B\u306A\u3063\u3066\u3044\u308B\u305F\u3081\u3067\u3059\u3002\u4E21\u65B9\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002</p>
18051
18126
  </div>
18052
18127
  `;
18128
+ var LOOP_INFO_HTML = `
18129
+ <div class="dtm-modal-body-content">
18130
+ <h4>\u4F55\u3092\u3059\u308B\u8A2D\u5B9A\u304B</h4>
18131
+ <p>\u30EB\u30FC\u30D7\u518D\u751F\u3092\u6709\u52B9\u306B\u3057\u307E\u3059\u3002ON\u306B\u3059\u308B\u3068\u3001\u518D\u751F\u304C\u66F2\u672B\u3067\u505C\u6B62\u305B\u305A\u3001\u5148\u982D\u304B\u3089\u30B7\u30FC\u30E0\u30EC\u30B9\u306B\u7E70\u308A\u8FD4\u3055\u308C\u307E\u3059\u3002</p>
18132
+ <h4>MML\u51FA\u529B\u3078\u306E\u53CD\u6620</h4>
18133
+ <p>\u30EB\u30FC\u30D7\u518D\u751F\u304CON\u306E\u5834\u5408\u3001MML\u751F\u6210\u6642\u306B <code>#loop=on</code> \u30E1\u30BF\u884C\u304C\u51FA\u529B\u3055\u308C\u307E\u3059\u3002\u57CB\u3081\u8FBC\u307F\u30D7\u30EC\u30A4\u30E4\u30FC\u3084\u518D\u751F\u5C02\u7528\u30D7\u30EC\u30A4\u30E4\u30FC\uFF08mountPlayer / mountChordPlayer\uFF09\u3067\u3082\u3053\u306EMML\u3092\u8AAD\u307F\u8FBC\u3080\u3053\u3068\u3067\u81EA\u52D5\u7684\u306B\u30EB\u30FC\u30D7\u518D\u751F\u3055\u308C\u307E\u3059\u3002</p>
18134
+ <h4>BGM\u3084\u30B2\u30FC\u30E0\u97F3\u697D\u5411\u3051</h4>
18135
+ <p>Web\u30B5\u30A4\u30C8\u306EBGM\u3084\u30B2\u30FC\u30E0\u306E\u30B9\u30C6\u30FC\u30B8\u66F2\u306A\u3069\u3001\u9014\u5207\u308C\u305A\u306B\u30EB\u30FC\u30D7\u3055\u305B\u305F\u3044\u66F2\u306E\u5236\u4F5C\u306B\u6700\u9069\u3067\u3059\u3002</p>
18136
+ </div>
18137
+ `;
18053
18138
  var MASTER_COMP_INFO_HTML = `
18054
18139
  <div class="dtm-modal-body-content">
18055
18140
  <h4>\u4F55\u3092\u3059\u308B\u8A2D\u5B9A\u304B</h4>
@@ -18595,6 +18680,18 @@ var mountDAW = (target, options = {}) => {
18595
18680
  options.singingVoices?.setVolume(masterVolume / 100);
18596
18681
  };
18597
18682
  applyMasterVolume(masterVolume);
18683
+ let loopEnabled = options.initialLoop ?? false;
18684
+ const applyLoop = (enabled) => {
18685
+ loopEnabled = enabled;
18686
+ if (refs.loopToggle) {
18687
+ refs.loopToggle.checked = enabled;
18688
+ }
18689
+ if (refs.loopToggleLabel) {
18690
+ refs.loopToggleLabel.textContent = enabled ? "ON" : "OFF";
18691
+ }
18692
+ options.onLoopChange?.(enabled);
18693
+ };
18694
+ applyLoop(loopEnabled);
18598
18695
  const snapToEdoGrid = (u) => {
18599
18696
  const upr = renderConfig.unitsPerRow ?? UNITS_PER_SEMITONE;
18600
18697
  return units(Math.round(u / upr) * upr);
@@ -19190,7 +19287,9 @@ var mountDAW = (target, options = {}) => {
19190
19287
  unitsPerRow: upr = UNITS_PER_SEMITONE
19191
19288
  } = renderConfig;
19192
19289
  const offset = renderer.getDrawOffset();
19193
- for (const note of active.core.getNotes()) {
19290
+ const notes = active.core.getNotes();
19291
+ for (let i2 = notes.length - 1; i2 >= 0; i2--) {
19292
+ const note = notes[i2];
19194
19293
  const logicalX = note.startStep * stepWidth;
19195
19294
  const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
19196
19295
  const logicalY = yIndex * keyHeight;
@@ -19266,14 +19365,18 @@ var mountDAW = (target, options = {}) => {
19266
19365
  return;
19267
19366
  }
19268
19367
  hasDragged = false;
19269
- const existing = findActiveNoteAt(x2, y, TOUCH_HIT_MARGIN);
19368
+ const hitMargin = event.pointerType === "mouse" ? 0 : Math.min(
19369
+ TOUCH_HIT_MARGIN,
19370
+ Math.max(0, Math.floor(renderConfig.keyHeight * 0.25))
19371
+ );
19372
+ const existing = findActiveNoteAt(x2, y, hitMargin);
19270
19373
  if (existing) {
19271
19374
  playPreview(existing.pitchUnits);
19272
19375
  const { stepWidth } = renderConfig;
19273
19376
  const offset = renderer.getDrawOffset();
19274
19377
  const renderX = existing.startStep * stepWidth - offset.x;
19275
19378
  const w = existing.durationSteps * stepWidth;
19276
- if (x2 >= renderX + w - resizeHandleWidth && x2 <= renderX + w) {
19379
+ if (x2 >= renderX + w - resizeHandleWidth && x2 <= renderX + w + hitMargin) {
19277
19380
  dragState = {
19278
19381
  noteId: existing.id,
19279
19382
  mode: "resize",
@@ -19288,7 +19391,7 @@ var mountDAW = (target, options = {}) => {
19288
19391
  noteId: existing.id,
19289
19392
  mode: "move",
19290
19393
  dragOffsetStep: step - existing.startStep,
19291
- dragOffsetPitch: pitch - existing.pitchUnits,
19394
+ dragOffsetPitch: 0,
19292
19395
  startStep: existing.startStep,
19293
19396
  durationSteps: existing.durationSteps,
19294
19397
  lastPreviewPitch: existing.pitchUnits
@@ -19403,7 +19506,7 @@ var mountDAW = (target, options = {}) => {
19403
19506
  const orig = selectedOriginal.find((o) => o.id === grab.id);
19404
19507
  if (orig) {
19405
19508
  const newGrab = units(orig.pitch + deltaPitch);
19406
- if (newGrab !== lastMultiPreviewPitch && newGrab >= 0 && newGrab < 128) {
19509
+ if (newGrab !== lastMultiPreviewPitch && newGrab >= PITCH_RANGE_START && newGrab <= PITCH_RANGE_END) {
19407
19510
  lastMultiPreviewPitch = newGrab;
19408
19511
  playPreview(newGrab);
19409
19512
  }
@@ -19552,11 +19655,9 @@ var mountDAW = (target, options = {}) => {
19552
19655
  gridCanvas.addEventListener("dblclick", (event) => {
19553
19656
  event.preventDefault();
19554
19657
  if (isActiveLocked()) return;
19555
- const { step, pitch } = renderer.getGridPosition(event);
19658
+ const { x: x2, y } = renderer.getGridPosition(event);
19556
19659
  const active = getActive();
19557
- const note = active.core.getNotes().find(
19558
- (n) => n.pitchUnits === pitch && step >= n.startStep && step < n.startStep + n.durationSteps
19559
- );
19660
+ const note = findActiveNoteAt(x2, y);
19560
19661
  if (note) active.core.deleteNoteById(note.id);
19561
19662
  });
19562
19663
  gridCanvas.addEventListener(
@@ -19656,6 +19757,7 @@ var mountDAW = (target, options = {}) => {
19656
19757
  getPlayStartStep: () => playStartStep,
19657
19758
  getDrumPattern: (currentBar) => resolveDrumPattern(currentDrumPattern, drumPatterns, currentBar),
19658
19759
  getSoloTrackId: () => isSolo ? activeTrackId : null,
19760
+ getLoop: () => loopEnabled,
19659
19761
  getAudioTime,
19660
19762
  onPlayNote: (e) => {
19661
19763
  if (audioMutedTracks.has(e.trackId)) return;
@@ -19683,6 +19785,19 @@ var mountDAW = (target, options = {}) => {
19683
19785
  getMaxOffsetX()
19684
19786
  );
19685
19787
  renderer.setDrawOffset(currentOffsetX, currentOffsetY);
19788
+ } else if (currentPlayStep < currentOffsetX / renderConfig.stepWidth) {
19789
+ const visibleBars = Math.max(
19790
+ 1,
19791
+ Math.round(visibleSteps / renderConfig.stepsPerBar)
19792
+ );
19793
+ currentOffsetX = clamp5(
19794
+ Math.floor(
19795
+ currentPlayStep / (visibleBars * renderConfig.stepsPerBar)
19796
+ ) * visibleBars * renderConfig.stepsPerBar * renderConfig.stepWidth,
19797
+ 0,
19798
+ getMaxOffsetX()
19799
+ );
19800
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
19686
19801
  }
19687
19802
  redrawAll();
19688
19803
  },
@@ -19798,8 +19913,18 @@ var mountDAW = (target, options = {}) => {
19798
19913
  options.onScheduleFade?.(params);
19799
19914
  }
19800
19915
  if (streaming && voices) {
19916
+ let loopLengthSec;
19917
+ let loopStartSec;
19918
+ if (loopEnabled) {
19919
+ const endStep = getSongEndStepExact();
19920
+ const sps = 60 / bpm / 48;
19921
+ loopStartSec = 0;
19922
+ loopLengthSec = endStep * sps;
19923
+ }
19801
19924
  voices.startStream(streamTracks, sequencer.getStartTime(), {
19802
19925
  isAudible: (t) => !isSolo || t.id === activeTrackId,
19926
+ loopLengthSec,
19927
+ loopStartSec,
19803
19928
  onScheduled: (t, note, t0) => {
19804
19929
  if (!t.id) return;
19805
19930
  flashTrackPill(t.id, t0 - getAudioTime(), note.durationSec);
@@ -20811,6 +20936,7 @@ var mountDAW = (target, options = {}) => {
20811
20936
  fadeOut: Math.round(fadeOutSec * 10),
20812
20937
  mode,
20813
20938
  edo: renderConfig.edo,
20939
+ loop: loopEnabled ? true : void 0,
20814
20940
  trackInstruments: trackInstMeta,
20815
20941
  trackCompression: trackCompMeta,
20816
20942
  trackWidth: trackWidthMeta,
@@ -20840,6 +20966,7 @@ var mountDAW = (target, options = {}) => {
20840
20966
  fadeOut: Math.round(fadeOutSec * 10),
20841
20967
  mode,
20842
20968
  edo: renderConfig.edo,
20969
+ loop: loopEnabled ? true : void 0,
20843
20970
  trackInstruments: trackInstMeta,
20844
20971
  trackCompression: trackCompMeta,
20845
20972
  trackWidth: trackWidthMeta,
@@ -21053,6 +21180,7 @@ var mountDAW = (target, options = {}) => {
21053
21180
  }
21054
21181
  applyEdo(meta.edo ?? 12);
21055
21182
  refs.edoSelect.value = String(renderConfig.edo ?? 12);
21183
+ applyLoop(meta.loop ?? false);
21056
21184
  if (meta.drumVolume !== void 0) {
21057
21185
  drumVolume = meta.drumVolume;
21058
21186
  refs.drumVolume.value = String(meta.drumVolume);
@@ -21565,6 +21693,12 @@ var mountDAW = (target, options = {}) => {
21565
21693
  refs.edoInfoBtn.addEventListener("click", () => {
21566
21694
  showModal("\u97F3\u5F8B\u306E\u89E3\u8AAC", EDO_INFO_HTML);
21567
21695
  });
21696
+ refs.loopToggle.addEventListener("change", () => {
21697
+ applyLoop(refs.loopToggle.checked);
21698
+ });
21699
+ refs.loopInfoBtn.addEventListener("click", () => {
21700
+ showModal("\u30EB\u30FC\u30D7\u518D\u751F\u306E\u89E3\u8AAC", LOOP_INFO_HTML);
21701
+ });
21568
21702
  refs.masterVolume.addEventListener("input", () => {
21569
21703
  applyMasterVolume(Number.parseInt(refs.masterVolume.value, 10) || 0);
21570
21704
  });
@@ -22966,6 +23100,8 @@ var mountDAW = (target, options = {}) => {
22966
23100
  },
22967
23101
  exportMIDI: exportMIDI2,
22968
23102
  setBpm,
23103
+ getLoop: () => loopEnabled,
23104
+ setLoop: (loop) => applyLoop(loop),
22969
23105
  getPlaybackState: () => playbackState,
22970
23106
  getCurrentPlayStep,
22971
23107
  forcePauseAt,
@@ -23419,7 +23555,7 @@ var createPianoRoll = (options, handlers) => {
23419
23555
  const h = keyHeight;
23420
23556
  const renderX = logicalX - offset.x;
23421
23557
  const renderY = logicalY - offset.y;
23422
- if (x2 >= renderX && x2 <= renderX + w && y >= renderY && y <= renderY + h) {
23558
+ if (x2 >= renderX && x2 < renderX + w && y >= renderY && y < renderY + h) {
23423
23559
  return note;
23424
23560
  }
23425
23561
  }
package/dist/index.mjs CHANGED
@@ -4213,7 +4213,7 @@ var NATURAL_STEPS2 = {
4213
4213
  };
4214
4214
  var isNatural = (ch) => Object.hasOwn(NATURAL_STEPS2[12], ch);
4215
4215
  var clamp3 = (value, lo, hi) => Math.min(hi, Math.max(lo, value));
4216
- var META_DIRECTIVE = /#(inst|drum|drumfont|volume|drumvolume|reverb|reverbdecay|reverbpredelay|delay|delaydiv|mastercomp|fadein|fadeout|mode|edo)=([\w:-]+)/gi;
4216
+ var META_DIRECTIVE = /#(inst|drum|drumfont|volume|drumvolume|reverb|reverbdecay|reverbpredelay|delay|delaydiv|mastercomp|fadein|fadeout|mode|edo|loop)=([\w:-]+)/gi;
4217
4217
  var TRACK_INST_DIRECTIVE = /#t(\d+)inst=([^#;\r\n]+)/gi;
4218
4218
  var TRACK_COMP_DIRECTIVE = /#t(\d+)comp=(\d+)/gi;
4219
4219
  var TRACK_WIDTH_DIRECTIVE = /#t(\d+)width=(\d+)/gi;
@@ -4266,6 +4266,9 @@ var parseMmlMeta = (mml) => {
4266
4266
  } else if (key === "edo") {
4267
4267
  const e = Number.parseInt(m[2], 10);
4268
4268
  if (e === 12 || e === 31) meta.edo = e;
4269
+ } else if (key === "loop") {
4270
+ const v = m[2].toLowerCase();
4271
+ meta.loop = v === "on" || v === "1" || v === "true";
4269
4272
  }
4270
4273
  }
4271
4274
  for (const m of mml.matchAll(TRACK_INST_DIRECTIVE)) {
@@ -4369,6 +4372,7 @@ var formatMmlMeta = (meta, space = "") => {
4369
4372
  parts.push(`#fadeout=${meta.fadeOut}`);
4370
4373
  if (meta.mode) parts.push(`#mode=${meta.mode}`);
4371
4374
  if (meta.edo !== void 0 && meta.edo !== 12) parts.push(`#edo=${meta.edo}`);
4375
+ if (meta.loop) parts.push("#loop=on");
4372
4376
  if (meta.trackInstruments) {
4373
4377
  for (const [idx, name] of Object.entries(meta.trackInstruments)) {
4374
4378
  if (name) parts.push(`#t${idx}inst=${name}`);
@@ -4896,7 +4900,11 @@ var createSequencer = (options) => {
4896
4900
  if (!active) return;
4897
4901
  const sps = secondsPerStep();
4898
4902
  const time = options.getAudioTime() - startTime;
4899
- options.onTick(getWrappedPlayStep(time, sps));
4903
+ try {
4904
+ options.onTick(getWrappedPlayStep(time, sps));
4905
+ } catch (err2) {
4906
+ console.error("[sequencer] error in onTick callback:", err2);
4907
+ }
4900
4908
  animationId = requestAnimationFrame(animate);
4901
4909
  };
4902
4910
  const stop = () => {
@@ -10658,6 +10666,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
10658
10666
  options.onStop?.();
10659
10667
  };
10660
10668
  let isSeeking = false;
10669
+ let lastCommittedStep = null;
10661
10670
  const seekTo = (step) => {
10662
10671
  const clamped = Math.min(maxStep, Math.max(0, Math.round(step)));
10663
10672
  renderPlayhead(clamped);
@@ -10669,16 +10678,42 @@ var mountMmlPlayer = (target, mml, options = {}) => {
10669
10678
  play(clamped);
10670
10679
  }
10671
10680
  };
10672
- seekInput.addEventListener("pointerdown", () => {
10681
+ const commitSeek = () => {
10682
+ isSeeking = false;
10683
+ const step = Number(seekInput.value);
10684
+ if (lastCommittedStep !== step) {
10685
+ lastCommittedStep = step;
10686
+ seekTo(step);
10687
+ }
10688
+ };
10689
+ seekInput.addEventListener("pointerdown", (e) => {
10673
10690
  isSeeking = true;
10691
+ lastCommittedStep = null;
10692
+ try {
10693
+ seekInput.setPointerCapture(e.pointerId);
10694
+ } catch {
10695
+ }
10674
10696
  });
10675
10697
  seekInput.addEventListener("input", () => {
10676
10698
  renderPlayhead(Number(seekInput.value));
10677
10699
  syncSeekFill();
10678
10700
  });
10679
10701
  seekInput.addEventListener("change", () => {
10702
+ commitSeek();
10703
+ });
10704
+ seekInput.addEventListener("pointerup", (e) => {
10705
+ try {
10706
+ seekInput.releasePointerCapture(e.pointerId);
10707
+ } catch {
10708
+ }
10709
+ commitSeek();
10710
+ });
10711
+ seekInput.addEventListener("pointercancel", (e) => {
10712
+ try {
10713
+ seekInput.releasePointerCapture(e.pointerId);
10714
+ } catch {
10715
+ }
10680
10716
  isSeeking = false;
10681
- seekTo(Number(seekInput.value));
10682
10717
  });
10683
10718
  playBtn.addEventListener("click", () => {
10684
10719
  if (playing) pause();
@@ -12223,19 +12258,45 @@ var mountChordPlayer = (target, chords, options = {}) => {
12223
12258
  }
12224
12259
  play(idx);
12225
12260
  };
12226
- seekInput.addEventListener("pointerdown", () => {
12261
+ let lastCommittedSec = null;
12262
+ const commitSeek = () => {
12263
+ isSeeking = false;
12264
+ if (totalSec <= 0) return;
12265
+ const sec = Number(seekInput.value);
12266
+ if (lastCommittedSec !== sec) {
12267
+ lastCommittedSec = sec;
12268
+ const idx = getActiveIndexBySec(sec);
12269
+ seekTo(idx >= 0 ? idx : Math.max(0, chordEvents.length - 1));
12270
+ }
12271
+ };
12272
+ seekInput.addEventListener("pointerdown", (e) => {
12227
12273
  isSeeking = true;
12274
+ lastCommittedSec = null;
12275
+ try {
12276
+ seekInput.setPointerCapture(e.pointerId);
12277
+ } catch {
12278
+ }
12228
12279
  });
12229
12280
  seekInput.addEventListener("input", () => {
12230
12281
  updateTimeDisplay(Number(seekInput.value));
12231
12282
  syncSeekFill();
12232
12283
  });
12233
12284
  seekInput.addEventListener("change", () => {
12285
+ commitSeek();
12286
+ });
12287
+ seekInput.addEventListener("pointerup", (e) => {
12288
+ try {
12289
+ seekInput.releasePointerCapture(e.pointerId);
12290
+ } catch {
12291
+ }
12292
+ commitSeek();
12293
+ });
12294
+ seekInput.addEventListener("pointercancel", (e) => {
12295
+ try {
12296
+ seekInput.releasePointerCapture(e.pointerId);
12297
+ } catch {
12298
+ }
12234
12299
  isSeeking = false;
12235
- if (totalSec <= 0) return;
12236
- const sec = Number(seekInput.value);
12237
- const idx = getActiveIndexBySec(sec);
12238
- seekTo(idx >= 0 ? idx : Math.max(0, chordEvents.length - 1));
12239
12300
  });
12240
12301
  const stop = () => {
12241
12302
  if (isLoading) {
@@ -15348,6 +15409,15 @@ var buildUI = (target, options) => {
15348
15409
  </select>
15349
15410
  <button class="dtm-infobtn" data-dtm="edo-info" title="\u97F3\u5F8B\u306E\u89E3\u8AAC">${icon("info", 12)}</button>
15350
15411
  </div>
15412
+ <div class="dtm-row">
15413
+ <span class="dtm-label">\u30EB\u30FC\u30D7\u518D\u751F</span>
15414
+ <label class="dtm-toggle">
15415
+ <input type="checkbox" data-dtm="loop-toggle">
15416
+ <span data-dtm="loop-toggle-label">OFF</span>
15417
+ </label>
15418
+ <span class="dtm-grow"></span>
15419
+ <button class="dtm-infobtn" data-dtm="loop-info" title="\u30EB\u30FC\u30D7\u518D\u751F\u306E\u89E3\u8AAC">${icon("info", 12)}</button>
15420
+ </div>
15351
15421
  <div class="dtm-row">
15352
15422
  <span class="dtm-label">\u5168\u4F53\u97F3\u91CF</span>
15353
15423
  <input type="range" class="dtm-range dtm-grow" data-dtm="master-volume" value="50" min="0" max="100">
@@ -15685,6 +15755,9 @@ var buildUI = (target, options) => {
15685
15755
  vScrollThumb: sel("vscroll-thumb"),
15686
15756
  hScroll: sel("hscroll"),
15687
15757
  hScrollThumb: sel("hscroll-thumb"),
15758
+ loopToggle: sel("loop-toggle"),
15759
+ loopToggleLabel: sel("loop-toggle-label"),
15760
+ loopInfoBtn: sel("loop-info"),
15688
15761
  masterVolume: sel("master-volume"),
15689
15762
  masterVolumeLabel: sel("master-volume-label"),
15690
15763
  masterComp: sel("master-comp"),
@@ -17539,8 +17612,10 @@ var createRenderer = (mountTarget, width = 800, height = 450, config) => {
17539
17612
  const getXY = (e) => {
17540
17613
  const { clientX, clientY } = e;
17541
17614
  const rect = g_grid_canvas.getBoundingClientRect();
17542
- const x2 = Math.floor(clientX - rect.left);
17543
- const y = Math.floor(clientY - rect.top);
17615
+ const scaleX = rect.width > 0 ? g_grid_canvas.width / rect.width : 1;
17616
+ const scaleY = rect.height > 0 ? g_grid_canvas.height / rect.height : 1;
17617
+ const x2 = Math.floor((clientX - rect.left) * scaleX);
17618
+ const y = Math.floor((clientY - rect.top) * scaleY);
17544
17619
  return [x2, y, e.buttons];
17545
17620
  };
17546
17621
  const getGridPosition = (e) => {
@@ -17574,7 +17649,7 @@ var createRenderer = (mountTarget, width = 800, height = 450, config) => {
17574
17649
  const absoluteY = y + g_draw_offset_y;
17575
17650
  const yIndex = Math.floor(absoluteY / keyHeight);
17576
17651
  const pitch = units(pitchRangeStart + (keyCount - 1 - yIndex) * upr);
17577
- if (pitch >= pitchRangeStart && pitch < pitchRangeStart + keyCount) {
17652
+ if (pitch >= pitchRangeStart && pitch < pitchRangeStart + keyCount * upr) {
17578
17653
  requestAnimationFrame(() => callback(step, pitch));
17579
17654
  }
17580
17655
  },
@@ -17868,6 +17943,16 @@ var TRACK_DELAYSEND_INFO_HTML = `
17868
17943
  <p>[delay]\u30D1\u30CD\u30EB\u306E\u639B\u304B\u308A\u5177\u5408\u304C0%\u3060\u3068\u3001\u3053\u3053\u3092\u3044\u304F\u3089\u4E0A\u3052\u3066\u3082\u7121\u97F3\u306E\u307E\u307E\u3067\u3059\u3002\u300C\u9001\u308A\u91CF\uFF08\u3053\u306E\u30C8\u30E9\u30C3\u30AF\u304C\u3069\u308C\u3060\u3051\u63D0\u4F9B\u3059\u308B\u304B\uFF09\u300D\u3068\u300C\u30DE\u30B9\u30BF\u306E\u30C7\u30A3\u30EC\u30A4\u8A2D\u5B9A\uFF08\u5B9F\u969B\u306B\u3069\u3093\u306A\u7E70\u308A\u8FD4\u3057\u304C\u639B\u304B\u308B\u304B\uFF09\u300D\u306E\u4E8C\u6BB5\u69CB\u3048\u306B\u306A\u3063\u3066\u3044\u308B\u305F\u3081\u3067\u3059\u3002\u4E21\u65B9\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002</p>
17869
17944
  </div>
17870
17945
  `;
17946
+ var LOOP_INFO_HTML = `
17947
+ <div class="dtm-modal-body-content">
17948
+ <h4>\u4F55\u3092\u3059\u308B\u8A2D\u5B9A\u304B</h4>
17949
+ <p>\u30EB\u30FC\u30D7\u518D\u751F\u3092\u6709\u52B9\u306B\u3057\u307E\u3059\u3002ON\u306B\u3059\u308B\u3068\u3001\u518D\u751F\u304C\u66F2\u672B\u3067\u505C\u6B62\u305B\u305A\u3001\u5148\u982D\u304B\u3089\u30B7\u30FC\u30E0\u30EC\u30B9\u306B\u7E70\u308A\u8FD4\u3055\u308C\u307E\u3059\u3002</p>
17950
+ <h4>MML\u51FA\u529B\u3078\u306E\u53CD\u6620</h4>
17951
+ <p>\u30EB\u30FC\u30D7\u518D\u751F\u304CON\u306E\u5834\u5408\u3001MML\u751F\u6210\u6642\u306B <code>#loop=on</code> \u30E1\u30BF\u884C\u304C\u51FA\u529B\u3055\u308C\u307E\u3059\u3002\u57CB\u3081\u8FBC\u307F\u30D7\u30EC\u30A4\u30E4\u30FC\u3084\u518D\u751F\u5C02\u7528\u30D7\u30EC\u30A4\u30E4\u30FC\uFF08mountPlayer / mountChordPlayer\uFF09\u3067\u3082\u3053\u306EMML\u3092\u8AAD\u307F\u8FBC\u3080\u3053\u3068\u3067\u81EA\u52D5\u7684\u306B\u30EB\u30FC\u30D7\u518D\u751F\u3055\u308C\u307E\u3059\u3002</p>
17952
+ <h4>BGM\u3084\u30B2\u30FC\u30E0\u97F3\u697D\u5411\u3051</h4>
17953
+ <p>Web\u30B5\u30A4\u30C8\u306EBGM\u3084\u30B2\u30FC\u30E0\u306E\u30B9\u30C6\u30FC\u30B8\u66F2\u306A\u3069\u3001\u9014\u5207\u308C\u305A\u306B\u30EB\u30FC\u30D7\u3055\u305B\u305F\u3044\u66F2\u306E\u5236\u4F5C\u306B\u6700\u9069\u3067\u3059\u3002</p>
17954
+ </div>
17955
+ `;
17871
17956
  var MASTER_COMP_INFO_HTML = `
17872
17957
  <div class="dtm-modal-body-content">
17873
17958
  <h4>\u4F55\u3092\u3059\u308B\u8A2D\u5B9A\u304B</h4>
@@ -18413,6 +18498,18 @@ var mountDAW = (target, options = {}) => {
18413
18498
  options.singingVoices?.setVolume(masterVolume / 100);
18414
18499
  };
18415
18500
  applyMasterVolume(masterVolume);
18501
+ let loopEnabled = options.initialLoop ?? false;
18502
+ const applyLoop = (enabled) => {
18503
+ loopEnabled = enabled;
18504
+ if (refs.loopToggle) {
18505
+ refs.loopToggle.checked = enabled;
18506
+ }
18507
+ if (refs.loopToggleLabel) {
18508
+ refs.loopToggleLabel.textContent = enabled ? "ON" : "OFF";
18509
+ }
18510
+ options.onLoopChange?.(enabled);
18511
+ };
18512
+ applyLoop(loopEnabled);
18416
18513
  const snapToEdoGrid = (u) => {
18417
18514
  const upr = renderConfig.unitsPerRow ?? UNITS_PER_SEMITONE;
18418
18515
  return units(Math.round(u / upr) * upr);
@@ -19008,7 +19105,9 @@ var mountDAW = (target, options = {}) => {
19008
19105
  unitsPerRow: upr = UNITS_PER_SEMITONE
19009
19106
  } = renderConfig;
19010
19107
  const offset = renderer.getDrawOffset();
19011
- for (const note of active.core.getNotes()) {
19108
+ const notes = active.core.getNotes();
19109
+ for (let i2 = notes.length - 1; i2 >= 0; i2--) {
19110
+ const note = notes[i2];
19012
19111
  const logicalX = note.startStep * stepWidth;
19013
19112
  const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
19014
19113
  const logicalY = yIndex * keyHeight;
@@ -19084,14 +19183,18 @@ var mountDAW = (target, options = {}) => {
19084
19183
  return;
19085
19184
  }
19086
19185
  hasDragged = false;
19087
- const existing = findActiveNoteAt(x2, y, TOUCH_HIT_MARGIN);
19186
+ const hitMargin = event.pointerType === "mouse" ? 0 : Math.min(
19187
+ TOUCH_HIT_MARGIN,
19188
+ Math.max(0, Math.floor(renderConfig.keyHeight * 0.25))
19189
+ );
19190
+ const existing = findActiveNoteAt(x2, y, hitMargin);
19088
19191
  if (existing) {
19089
19192
  playPreview(existing.pitchUnits);
19090
19193
  const { stepWidth } = renderConfig;
19091
19194
  const offset = renderer.getDrawOffset();
19092
19195
  const renderX = existing.startStep * stepWidth - offset.x;
19093
19196
  const w = existing.durationSteps * stepWidth;
19094
- if (x2 >= renderX + w - resizeHandleWidth && x2 <= renderX + w) {
19197
+ if (x2 >= renderX + w - resizeHandleWidth && x2 <= renderX + w + hitMargin) {
19095
19198
  dragState = {
19096
19199
  noteId: existing.id,
19097
19200
  mode: "resize",
@@ -19106,7 +19209,7 @@ var mountDAW = (target, options = {}) => {
19106
19209
  noteId: existing.id,
19107
19210
  mode: "move",
19108
19211
  dragOffsetStep: step - existing.startStep,
19109
- dragOffsetPitch: pitch - existing.pitchUnits,
19212
+ dragOffsetPitch: 0,
19110
19213
  startStep: existing.startStep,
19111
19214
  durationSteps: existing.durationSteps,
19112
19215
  lastPreviewPitch: existing.pitchUnits
@@ -19221,7 +19324,7 @@ var mountDAW = (target, options = {}) => {
19221
19324
  const orig = selectedOriginal.find((o) => o.id === grab.id);
19222
19325
  if (orig) {
19223
19326
  const newGrab = units(orig.pitch + deltaPitch);
19224
- if (newGrab !== lastMultiPreviewPitch && newGrab >= 0 && newGrab < 128) {
19327
+ if (newGrab !== lastMultiPreviewPitch && newGrab >= PITCH_RANGE_START && newGrab <= PITCH_RANGE_END) {
19225
19328
  lastMultiPreviewPitch = newGrab;
19226
19329
  playPreview(newGrab);
19227
19330
  }
@@ -19370,11 +19473,9 @@ var mountDAW = (target, options = {}) => {
19370
19473
  gridCanvas.addEventListener("dblclick", (event) => {
19371
19474
  event.preventDefault();
19372
19475
  if (isActiveLocked()) return;
19373
- const { step, pitch } = renderer.getGridPosition(event);
19476
+ const { x: x2, y } = renderer.getGridPosition(event);
19374
19477
  const active = getActive();
19375
- const note = active.core.getNotes().find(
19376
- (n) => n.pitchUnits === pitch && step >= n.startStep && step < n.startStep + n.durationSteps
19377
- );
19478
+ const note = findActiveNoteAt(x2, y);
19378
19479
  if (note) active.core.deleteNoteById(note.id);
19379
19480
  });
19380
19481
  gridCanvas.addEventListener(
@@ -19474,6 +19575,7 @@ var mountDAW = (target, options = {}) => {
19474
19575
  getPlayStartStep: () => playStartStep,
19475
19576
  getDrumPattern: (currentBar) => resolveDrumPattern(currentDrumPattern, drumPatterns, currentBar),
19476
19577
  getSoloTrackId: () => isSolo ? activeTrackId : null,
19578
+ getLoop: () => loopEnabled,
19477
19579
  getAudioTime,
19478
19580
  onPlayNote: (e) => {
19479
19581
  if (audioMutedTracks.has(e.trackId)) return;
@@ -19501,6 +19603,19 @@ var mountDAW = (target, options = {}) => {
19501
19603
  getMaxOffsetX()
19502
19604
  );
19503
19605
  renderer.setDrawOffset(currentOffsetX, currentOffsetY);
19606
+ } else if (currentPlayStep < currentOffsetX / renderConfig.stepWidth) {
19607
+ const visibleBars = Math.max(
19608
+ 1,
19609
+ Math.round(visibleSteps / renderConfig.stepsPerBar)
19610
+ );
19611
+ currentOffsetX = clamp5(
19612
+ Math.floor(
19613
+ currentPlayStep / (visibleBars * renderConfig.stepsPerBar)
19614
+ ) * visibleBars * renderConfig.stepsPerBar * renderConfig.stepWidth,
19615
+ 0,
19616
+ getMaxOffsetX()
19617
+ );
19618
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
19504
19619
  }
19505
19620
  redrawAll();
19506
19621
  },
@@ -19616,8 +19731,18 @@ var mountDAW = (target, options = {}) => {
19616
19731
  options.onScheduleFade?.(params);
19617
19732
  }
19618
19733
  if (streaming && voices) {
19734
+ let loopLengthSec;
19735
+ let loopStartSec;
19736
+ if (loopEnabled) {
19737
+ const endStep = getSongEndStepExact();
19738
+ const sps = 60 / bpm / 48;
19739
+ loopStartSec = 0;
19740
+ loopLengthSec = endStep * sps;
19741
+ }
19619
19742
  voices.startStream(streamTracks, sequencer.getStartTime(), {
19620
19743
  isAudible: (t) => !isSolo || t.id === activeTrackId,
19744
+ loopLengthSec,
19745
+ loopStartSec,
19621
19746
  onScheduled: (t, note, t0) => {
19622
19747
  if (!t.id) return;
19623
19748
  flashTrackPill(t.id, t0 - getAudioTime(), note.durationSec);
@@ -20629,6 +20754,7 @@ var mountDAW = (target, options = {}) => {
20629
20754
  fadeOut: Math.round(fadeOutSec * 10),
20630
20755
  mode,
20631
20756
  edo: renderConfig.edo,
20757
+ loop: loopEnabled ? true : void 0,
20632
20758
  trackInstruments: trackInstMeta,
20633
20759
  trackCompression: trackCompMeta,
20634
20760
  trackWidth: trackWidthMeta,
@@ -20658,6 +20784,7 @@ var mountDAW = (target, options = {}) => {
20658
20784
  fadeOut: Math.round(fadeOutSec * 10),
20659
20785
  mode,
20660
20786
  edo: renderConfig.edo,
20787
+ loop: loopEnabled ? true : void 0,
20661
20788
  trackInstruments: trackInstMeta,
20662
20789
  trackCompression: trackCompMeta,
20663
20790
  trackWidth: trackWidthMeta,
@@ -20871,6 +20998,7 @@ var mountDAW = (target, options = {}) => {
20871
20998
  }
20872
20999
  applyEdo(meta.edo ?? 12);
20873
21000
  refs.edoSelect.value = String(renderConfig.edo ?? 12);
21001
+ applyLoop(meta.loop ?? false);
20874
21002
  if (meta.drumVolume !== void 0) {
20875
21003
  drumVolume = meta.drumVolume;
20876
21004
  refs.drumVolume.value = String(meta.drumVolume);
@@ -21383,6 +21511,12 @@ var mountDAW = (target, options = {}) => {
21383
21511
  refs.edoInfoBtn.addEventListener("click", () => {
21384
21512
  showModal("\u97F3\u5F8B\u306E\u89E3\u8AAC", EDO_INFO_HTML);
21385
21513
  });
21514
+ refs.loopToggle.addEventListener("change", () => {
21515
+ applyLoop(refs.loopToggle.checked);
21516
+ });
21517
+ refs.loopInfoBtn.addEventListener("click", () => {
21518
+ showModal("\u30EB\u30FC\u30D7\u518D\u751F\u306E\u89E3\u8AAC", LOOP_INFO_HTML);
21519
+ });
21386
21520
  refs.masterVolume.addEventListener("input", () => {
21387
21521
  applyMasterVolume(Number.parseInt(refs.masterVolume.value, 10) || 0);
21388
21522
  });
@@ -22784,6 +22918,8 @@ var mountDAW = (target, options = {}) => {
22784
22918
  },
22785
22919
  exportMIDI: exportMIDI2,
22786
22920
  setBpm,
22921
+ getLoop: () => loopEnabled,
22922
+ setLoop: (loop) => applyLoop(loop),
22787
22923
  getPlaybackState: () => playbackState,
22788
22924
  getCurrentPlayStep,
22789
22925
  forcePauseAt,
@@ -23237,7 +23373,7 @@ var createPianoRoll = (options, handlers) => {
23237
23373
  const h = keyHeight;
23238
23374
  const renderX = logicalX - offset.x;
23239
23375
  const renderY = logicalY - offset.y;
23240
- if (x2 >= renderX && x2 <= renderX + w && y >= renderY && y <= renderY + h) {
23376
+ if (x2 >= renderX && x2 < renderX + w && y >= renderY && y < renderY + h) {
23241
23377
  return note;
23242
23378
  }
23243
23379
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "author": "onjmin",
3
3
  "license": "MIT",
4
4
  "name": "@onjmin/dtm",
5
- "version": "2.1.2",
5
+ "version": "2.1.4",
6
6
  "description": "MMLを中間言語に用いた、モバイルファーストなDAW / ピアノロール打ち込みコンポーネント",
7
7
  "homepage": "https://onjmin.github.io/dtm",
8
8
  "repository": {