@onjmin/dtm 2.1.2 → 2.1.3

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"),
@@ -18050,6 +18123,16 @@ var TRACK_DELAYSEND_INFO_HTML = `
18050
18123
  <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
18124
  </div>
18052
18125
  `;
18126
+ var LOOP_INFO_HTML = `
18127
+ <div class="dtm-modal-body-content">
18128
+ <h4>\u4F55\u3092\u3059\u308B\u8A2D\u5B9A\u304B</h4>
18129
+ <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>
18130
+ <h4>MML\u51FA\u529B\u3078\u306E\u53CD\u6620</h4>
18131
+ <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>
18132
+ <h4>BGM\u3084\u30B2\u30FC\u30E0\u97F3\u697D\u5411\u3051</h4>
18133
+ <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>
18134
+ </div>
18135
+ `;
18053
18136
  var MASTER_COMP_INFO_HTML = `
18054
18137
  <div class="dtm-modal-body-content">
18055
18138
  <h4>\u4F55\u3092\u3059\u308B\u8A2D\u5B9A\u304B</h4>
@@ -18595,6 +18678,18 @@ var mountDAW = (target, options = {}) => {
18595
18678
  options.singingVoices?.setVolume(masterVolume / 100);
18596
18679
  };
18597
18680
  applyMasterVolume(masterVolume);
18681
+ let loopEnabled = options.initialLoop ?? false;
18682
+ const applyLoop = (enabled) => {
18683
+ loopEnabled = enabled;
18684
+ if (refs.loopToggle) {
18685
+ refs.loopToggle.checked = enabled;
18686
+ }
18687
+ if (refs.loopToggleLabel) {
18688
+ refs.loopToggleLabel.textContent = enabled ? "ON" : "OFF";
18689
+ }
18690
+ options.onLoopChange?.(enabled);
18691
+ };
18692
+ applyLoop(loopEnabled);
18598
18693
  const snapToEdoGrid = (u) => {
18599
18694
  const upr = renderConfig.unitsPerRow ?? UNITS_PER_SEMITONE;
18600
18695
  return units(Math.round(u / upr) * upr);
@@ -19656,6 +19751,7 @@ var mountDAW = (target, options = {}) => {
19656
19751
  getPlayStartStep: () => playStartStep,
19657
19752
  getDrumPattern: (currentBar) => resolveDrumPattern(currentDrumPattern, drumPatterns, currentBar),
19658
19753
  getSoloTrackId: () => isSolo ? activeTrackId : null,
19754
+ getLoop: () => loopEnabled,
19659
19755
  getAudioTime,
19660
19756
  onPlayNote: (e) => {
19661
19757
  if (audioMutedTracks.has(e.trackId)) return;
@@ -19683,6 +19779,19 @@ var mountDAW = (target, options = {}) => {
19683
19779
  getMaxOffsetX()
19684
19780
  );
19685
19781
  renderer.setDrawOffset(currentOffsetX, currentOffsetY);
19782
+ } else if (currentPlayStep < currentOffsetX / renderConfig.stepWidth) {
19783
+ const visibleBars = Math.max(
19784
+ 1,
19785
+ Math.round(visibleSteps / renderConfig.stepsPerBar)
19786
+ );
19787
+ currentOffsetX = clamp5(
19788
+ Math.floor(
19789
+ currentPlayStep / (visibleBars * renderConfig.stepsPerBar)
19790
+ ) * visibleBars * renderConfig.stepsPerBar * renderConfig.stepWidth,
19791
+ 0,
19792
+ getMaxOffsetX()
19793
+ );
19794
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
19686
19795
  }
19687
19796
  redrawAll();
19688
19797
  },
@@ -19798,8 +19907,18 @@ var mountDAW = (target, options = {}) => {
19798
19907
  options.onScheduleFade?.(params);
19799
19908
  }
19800
19909
  if (streaming && voices) {
19910
+ let loopLengthSec;
19911
+ let loopStartSec;
19912
+ if (loopEnabled) {
19913
+ const endStep = getSongEndStepExact();
19914
+ const sps = 60 / bpm / 48;
19915
+ loopStartSec = 0;
19916
+ loopLengthSec = endStep * sps;
19917
+ }
19801
19918
  voices.startStream(streamTracks, sequencer.getStartTime(), {
19802
19919
  isAudible: (t) => !isSolo || t.id === activeTrackId,
19920
+ loopLengthSec,
19921
+ loopStartSec,
19803
19922
  onScheduled: (t, note, t0) => {
19804
19923
  if (!t.id) return;
19805
19924
  flashTrackPill(t.id, t0 - getAudioTime(), note.durationSec);
@@ -20811,6 +20930,7 @@ var mountDAW = (target, options = {}) => {
20811
20930
  fadeOut: Math.round(fadeOutSec * 10),
20812
20931
  mode,
20813
20932
  edo: renderConfig.edo,
20933
+ loop: loopEnabled ? true : void 0,
20814
20934
  trackInstruments: trackInstMeta,
20815
20935
  trackCompression: trackCompMeta,
20816
20936
  trackWidth: trackWidthMeta,
@@ -20840,6 +20960,7 @@ var mountDAW = (target, options = {}) => {
20840
20960
  fadeOut: Math.round(fadeOutSec * 10),
20841
20961
  mode,
20842
20962
  edo: renderConfig.edo,
20963
+ loop: loopEnabled ? true : void 0,
20843
20964
  trackInstruments: trackInstMeta,
20844
20965
  trackCompression: trackCompMeta,
20845
20966
  trackWidth: trackWidthMeta,
@@ -21053,6 +21174,7 @@ var mountDAW = (target, options = {}) => {
21053
21174
  }
21054
21175
  applyEdo(meta.edo ?? 12);
21055
21176
  refs.edoSelect.value = String(renderConfig.edo ?? 12);
21177
+ applyLoop(meta.loop ?? false);
21056
21178
  if (meta.drumVolume !== void 0) {
21057
21179
  drumVolume = meta.drumVolume;
21058
21180
  refs.drumVolume.value = String(meta.drumVolume);
@@ -21565,6 +21687,12 @@ var mountDAW = (target, options = {}) => {
21565
21687
  refs.edoInfoBtn.addEventListener("click", () => {
21566
21688
  showModal("\u97F3\u5F8B\u306E\u89E3\u8AAC", EDO_INFO_HTML);
21567
21689
  });
21690
+ refs.loopToggle.addEventListener("change", () => {
21691
+ applyLoop(refs.loopToggle.checked);
21692
+ });
21693
+ refs.loopInfoBtn.addEventListener("click", () => {
21694
+ showModal("\u30EB\u30FC\u30D7\u518D\u751F\u306E\u89E3\u8AAC", LOOP_INFO_HTML);
21695
+ });
21568
21696
  refs.masterVolume.addEventListener("input", () => {
21569
21697
  applyMasterVolume(Number.parseInt(refs.masterVolume.value, 10) || 0);
21570
21698
  });
@@ -22966,6 +23094,8 @@ var mountDAW = (target, options = {}) => {
22966
23094
  },
22967
23095
  exportMIDI: exportMIDI2,
22968
23096
  setBpm,
23097
+ getLoop: () => loopEnabled,
23098
+ setLoop: (loop) => applyLoop(loop),
22969
23099
  getPlaybackState: () => playbackState,
22970
23100
  getCurrentPlayStep,
22971
23101
  forcePauseAt,
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"),
@@ -17868,6 +17941,16 @@ var TRACK_DELAYSEND_INFO_HTML = `
17868
17941
  <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
17942
  </div>
17870
17943
  `;
17944
+ var LOOP_INFO_HTML = `
17945
+ <div class="dtm-modal-body-content">
17946
+ <h4>\u4F55\u3092\u3059\u308B\u8A2D\u5B9A\u304B</h4>
17947
+ <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>
17948
+ <h4>MML\u51FA\u529B\u3078\u306E\u53CD\u6620</h4>
17949
+ <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>
17950
+ <h4>BGM\u3084\u30B2\u30FC\u30E0\u97F3\u697D\u5411\u3051</h4>
17951
+ <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>
17952
+ </div>
17953
+ `;
17871
17954
  var MASTER_COMP_INFO_HTML = `
17872
17955
  <div class="dtm-modal-body-content">
17873
17956
  <h4>\u4F55\u3092\u3059\u308B\u8A2D\u5B9A\u304B</h4>
@@ -18413,6 +18496,18 @@ var mountDAW = (target, options = {}) => {
18413
18496
  options.singingVoices?.setVolume(masterVolume / 100);
18414
18497
  };
18415
18498
  applyMasterVolume(masterVolume);
18499
+ let loopEnabled = options.initialLoop ?? false;
18500
+ const applyLoop = (enabled) => {
18501
+ loopEnabled = enabled;
18502
+ if (refs.loopToggle) {
18503
+ refs.loopToggle.checked = enabled;
18504
+ }
18505
+ if (refs.loopToggleLabel) {
18506
+ refs.loopToggleLabel.textContent = enabled ? "ON" : "OFF";
18507
+ }
18508
+ options.onLoopChange?.(enabled);
18509
+ };
18510
+ applyLoop(loopEnabled);
18416
18511
  const snapToEdoGrid = (u) => {
18417
18512
  const upr = renderConfig.unitsPerRow ?? UNITS_PER_SEMITONE;
18418
18513
  return units(Math.round(u / upr) * upr);
@@ -19474,6 +19569,7 @@ var mountDAW = (target, options = {}) => {
19474
19569
  getPlayStartStep: () => playStartStep,
19475
19570
  getDrumPattern: (currentBar) => resolveDrumPattern(currentDrumPattern, drumPatterns, currentBar),
19476
19571
  getSoloTrackId: () => isSolo ? activeTrackId : null,
19572
+ getLoop: () => loopEnabled,
19477
19573
  getAudioTime,
19478
19574
  onPlayNote: (e) => {
19479
19575
  if (audioMutedTracks.has(e.trackId)) return;
@@ -19501,6 +19597,19 @@ var mountDAW = (target, options = {}) => {
19501
19597
  getMaxOffsetX()
19502
19598
  );
19503
19599
  renderer.setDrawOffset(currentOffsetX, currentOffsetY);
19600
+ } else if (currentPlayStep < currentOffsetX / renderConfig.stepWidth) {
19601
+ const visibleBars = Math.max(
19602
+ 1,
19603
+ Math.round(visibleSteps / renderConfig.stepsPerBar)
19604
+ );
19605
+ currentOffsetX = clamp5(
19606
+ Math.floor(
19607
+ currentPlayStep / (visibleBars * renderConfig.stepsPerBar)
19608
+ ) * visibleBars * renderConfig.stepsPerBar * renderConfig.stepWidth,
19609
+ 0,
19610
+ getMaxOffsetX()
19611
+ );
19612
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
19504
19613
  }
19505
19614
  redrawAll();
19506
19615
  },
@@ -19616,8 +19725,18 @@ var mountDAW = (target, options = {}) => {
19616
19725
  options.onScheduleFade?.(params);
19617
19726
  }
19618
19727
  if (streaming && voices) {
19728
+ let loopLengthSec;
19729
+ let loopStartSec;
19730
+ if (loopEnabled) {
19731
+ const endStep = getSongEndStepExact();
19732
+ const sps = 60 / bpm / 48;
19733
+ loopStartSec = 0;
19734
+ loopLengthSec = endStep * sps;
19735
+ }
19619
19736
  voices.startStream(streamTracks, sequencer.getStartTime(), {
19620
19737
  isAudible: (t) => !isSolo || t.id === activeTrackId,
19738
+ loopLengthSec,
19739
+ loopStartSec,
19621
19740
  onScheduled: (t, note, t0) => {
19622
19741
  if (!t.id) return;
19623
19742
  flashTrackPill(t.id, t0 - getAudioTime(), note.durationSec);
@@ -20629,6 +20748,7 @@ var mountDAW = (target, options = {}) => {
20629
20748
  fadeOut: Math.round(fadeOutSec * 10),
20630
20749
  mode,
20631
20750
  edo: renderConfig.edo,
20751
+ loop: loopEnabled ? true : void 0,
20632
20752
  trackInstruments: trackInstMeta,
20633
20753
  trackCompression: trackCompMeta,
20634
20754
  trackWidth: trackWidthMeta,
@@ -20658,6 +20778,7 @@ var mountDAW = (target, options = {}) => {
20658
20778
  fadeOut: Math.round(fadeOutSec * 10),
20659
20779
  mode,
20660
20780
  edo: renderConfig.edo,
20781
+ loop: loopEnabled ? true : void 0,
20661
20782
  trackInstruments: trackInstMeta,
20662
20783
  trackCompression: trackCompMeta,
20663
20784
  trackWidth: trackWidthMeta,
@@ -20871,6 +20992,7 @@ var mountDAW = (target, options = {}) => {
20871
20992
  }
20872
20993
  applyEdo(meta.edo ?? 12);
20873
20994
  refs.edoSelect.value = String(renderConfig.edo ?? 12);
20995
+ applyLoop(meta.loop ?? false);
20874
20996
  if (meta.drumVolume !== void 0) {
20875
20997
  drumVolume = meta.drumVolume;
20876
20998
  refs.drumVolume.value = String(meta.drumVolume);
@@ -21383,6 +21505,12 @@ var mountDAW = (target, options = {}) => {
21383
21505
  refs.edoInfoBtn.addEventListener("click", () => {
21384
21506
  showModal("\u97F3\u5F8B\u306E\u89E3\u8AAC", EDO_INFO_HTML);
21385
21507
  });
21508
+ refs.loopToggle.addEventListener("change", () => {
21509
+ applyLoop(refs.loopToggle.checked);
21510
+ });
21511
+ refs.loopInfoBtn.addEventListener("click", () => {
21512
+ showModal("\u30EB\u30FC\u30D7\u518D\u751F\u306E\u89E3\u8AAC", LOOP_INFO_HTML);
21513
+ });
21386
21514
  refs.masterVolume.addEventListener("input", () => {
21387
21515
  applyMasterVolume(Number.parseInt(refs.masterVolume.value, 10) || 0);
21388
21516
  });
@@ -22784,6 +22912,8 @@ var mountDAW = (target, options = {}) => {
22784
22912
  },
22785
22913
  exportMIDI: exportMIDI2,
22786
22914
  setBpm,
22915
+ getLoop: () => loopEnabled,
22916
+ setLoop: (loop) => applyLoop(loop),
22787
22917
  getPlaybackState: () => playbackState,
22788
22918
  getCurrentPlayStep,
22789
22919
  forcePauseAt,
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.3",
6
6
  "description": "MMLを中間言語に用いた、モバイルファーストなDAW / ピアノロール打ち込みコンポーネント",
7
7
  "homepage": "https://onjmin.github.io/dtm",
8
8
  "repository": {