@onjmin/dtm 0.1.39 → 0.1.40

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.
Files changed (3) hide show
  1. package/dist/index.js +138 -35
  2. package/dist/index.mjs +138 -35
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7699,7 +7699,6 @@ var mountDAW = (target, options = {}) => {
7699
7699
  stepWidth: BASE_STEP_WIDTH * 2
7700
7700
  // zoom100% 相当
7701
7701
  };
7702
- const leftPaddingSteps = renderConfig.stepsPerBar * 16;
7703
7702
  let zoomX = 100;
7704
7703
  let zoomY = 100;
7705
7704
  let bpm = options.defaultBpm ?? DEFAULT_BPM;
@@ -7713,8 +7712,8 @@ var mountDAW = (target, options = {}) => {
7713
7712
  let snapGridSteps = 12;
7714
7713
  const gridLineSteps = 48;
7715
7714
  let currentOffsetX = 0;
7716
- const _initPitch = options.initialScrollPitch;
7717
- let currentOffsetY = _initPitch !== void 0 ? (renderConfig.keyCount - 1 - _initPitch) * renderConfig.keyHeight - 215 : (104 - 1 - 60) * renderConfig.keyHeight - 215;
7715
+ const _initPitch = options.initialScrollPitch ?? 48;
7716
+ let currentOffsetY = (renderConfig.keyCount - 1 - _initPitch) * renderConfig.keyHeight - 215;
7718
7717
  let playStartStep = 0;
7719
7718
  let isSolo = false;
7720
7719
  let lyricTrackIndices = /* @__PURE__ */ new Set();
@@ -7722,6 +7721,7 @@ var mountDAW = (target, options = {}) => {
7722
7721
  let pausedPlayStep = 0;
7723
7722
  let currentPlayStep = 0;
7724
7723
  let ready = false;
7724
+ let isLoading = false;
7725
7725
  let selectedNotes = [];
7726
7726
  let selectionRect = null;
7727
7727
  let copiedNotes = [];
@@ -7741,7 +7741,7 @@ var mountDAW = (target, options = {}) => {
7741
7741
  };
7742
7742
  if (lyricsDebounceTimer) clearTimeout(lyricsDebounceTimer);
7743
7743
  lyricsDebounceTimer = setTimeout(() => {
7744
- options.onLyricsChange(trackId, data);
7744
+ options.onLyricsChange?.(trackId, data);
7745
7745
  lyricsDebounceTimer = null;
7746
7746
  }, 300);
7747
7747
  };
@@ -7821,14 +7821,28 @@ var mountDAW = (target, options = {}) => {
7821
7821
  const getActive = () => trackStates.find((t) => t.config.id === activeTrackId) ?? trackStates[0];
7822
7822
  let showModal;
7823
7823
  const getMaxNoteStep = () => {
7824
- let maxStep = renderConfig.stepsPerBar * 4;
7824
+ let maxEndStep = 0;
7825
7825
  for (const t of trackStates) {
7826
7826
  for (const n of t.core.getNotes()) {
7827
7827
  const end = n.startStep + n.durationSteps;
7828
- if (end > maxStep) maxStep = end;
7828
+ if (end > maxEndStep) {
7829
+ maxEndStep = end;
7830
+ }
7829
7831
  }
7830
7832
  }
7831
- return maxStep;
7833
+ if (maxEndStep === 0) {
7834
+ return renderConfig.stepsPerBar * 4;
7835
+ }
7836
+ const lastNoteMeasure = Math.floor(
7837
+ (maxEndStep - 1) / renderConfig.stepsPerBar
7838
+ );
7839
+ return (lastNoteMeasure + 2) * renderConfig.stepsPerBar;
7840
+ };
7841
+ const getMaxOffsetX = () => {
7842
+ const canvas = getGridCanvas();
7843
+ const maxNoteStep = getMaxNoteStep();
7844
+ const totalContentWidth = maxNoteStep * renderConfig.stepWidth;
7845
+ return Math.max(0, totalContentWidth - canvas.width);
7832
7846
  };
7833
7847
  const getMaxOffsetY = () => {
7834
7848
  const totalHeight = renderConfig.keyCount * renderConfig.keyHeight;
@@ -7908,19 +7922,14 @@ var mountDAW = (target, options = {}) => {
7908
7922
  };
7909
7923
  const updateScrollbars = () => {
7910
7924
  const canvas = getGridCanvas();
7911
- const maxNoteStep = getMaxNoteStep();
7912
- const leftPaddingWidth = leftPaddingSteps * renderConfig.stepWidth;
7913
- const totalContentWidth = maxNoteStep * renderConfig.stepWidth;
7914
- const maxOffsetX = totalContentWidth - canvas.width + leftPaddingWidth;
7925
+ const maxOffsetX = getMaxOffsetX();
7915
7926
  const sbW = refs.hScroll.clientWidth;
7916
7927
  if (maxOffsetX <= 0) {
7917
7928
  refs.hScrollThumb.style.width = "100%";
7918
7929
  refs.hScrollThumb.style.left = "0";
7919
7930
  } else {
7920
- const thumbW = Math.max(
7921
- 40,
7922
- canvas.width / (totalContentWidth + leftPaddingWidth) * sbW
7923
- );
7931
+ const totalContentWidth = getMaxNoteStep() * renderConfig.stepWidth;
7932
+ const thumbW = Math.max(40, canvas.width / totalContentWidth * sbW);
7924
7933
  const ratio = currentOffsetX / maxOffsetX;
7925
7934
  refs.hScrollThumb.style.width = `${thumbW}px`;
7926
7935
  refs.hScrollThumb.style.left = `${clamp3(ratio * (sbW - thumbW), 0, sbW - thumbW)}px`;
@@ -7974,11 +7983,7 @@ var mountDAW = (target, options = {}) => {
7974
7983
  draggingV = false;
7975
7984
  });
7976
7985
  const moveH = (clientX) => {
7977
- const canvas = getGridCanvas();
7978
- const maxNoteStep = getMaxNoteStep();
7979
- const leftPaddingWidth = leftPaddingSteps * renderConfig.stepWidth;
7980
- const totalContentWidth = maxNoteStep * renderConfig.stepWidth;
7981
- const maxOffsetX = totalContentWidth - canvas.width + leftPaddingWidth;
7986
+ const maxOffsetX = getMaxOffsetX();
7982
7987
  if (maxOffsetX <= 0) return;
7983
7988
  const rect = refs.hScroll.getBoundingClientRect();
7984
7989
  const thumbW = Number.parseFloat(refs.hScrollThumb.style.width) || 40;
@@ -8011,9 +8016,10 @@ var mountDAW = (target, options = {}) => {
8011
8016
  let selectedOriginal = [];
8012
8017
  let lastMultiPreviewPitch = null;
8013
8018
  const playPreview = (pitch) => {
8019
+ if (isLoading) return;
8014
8020
  options.onResumeAudio?.();
8015
8021
  const active = getActive();
8016
- dispatchNote(active.config.id, pitch, active.volume, 100, 0, 0.1);
8022
+ dispatchNote(active.config.id, pitch, active.volume, 100, 0, 0.5);
8017
8023
  };
8018
8024
  const findActiveNoteAt = (x, y, margin = 0) => {
8019
8025
  const active = getActive();
@@ -8286,7 +8292,11 @@ var mountDAW = (target, options = {}) => {
8286
8292
  0,
8287
8293
  getMaxOffsetY()
8288
8294
  );
8289
- currentOffsetX = Math.max(0, currentOffsetX + event.deltaX);
8295
+ currentOffsetX = clamp3(
8296
+ currentOffsetX + event.deltaX,
8297
+ 0,
8298
+ getMaxOffsetX()
8299
+ );
8290
8300
  setDrawOffset(currentOffsetX, currentOffsetY);
8291
8301
  redrawAll();
8292
8302
  },
@@ -8321,9 +8331,10 @@ var mountDAW = (target, options = {}) => {
8321
8331
  const centerStep = (currentOffsetX + canvas.width / 2) / renderConfig.stepWidth;
8322
8332
  renderConfig.stepWidth = BASE_STEP_WIDTH * (zoomX * 2) / 100;
8323
8333
  refs.zoomXLabel.textContent = `${zoomX}%`;
8324
- currentOffsetX = Math.max(
8334
+ currentOffsetX = clamp3(
8335
+ centerStep * renderConfig.stepWidth - canvas.width / 2,
8325
8336
  0,
8326
- centerStep * renderConfig.stepWidth - canvas.width / 2
8337
+ getMaxOffsetX()
8327
8338
  );
8328
8339
  setDrawOffset(currentOffsetX, currentOffsetY);
8329
8340
  redrawAll();
@@ -8382,7 +8393,11 @@ var mountDAW = (target, options = {}) => {
8382
8393
  const threshold = currentOffsetX / renderConfig.stepWidth + visibleSteps - 4;
8383
8394
  if (currentPlayStep > threshold) {
8384
8395
  const visibleBars = Math.round(visibleSteps / renderConfig.stepsPerBar);
8385
- currentOffsetX += visibleBars * renderConfig.stepsPerBar * renderConfig.stepWidth;
8396
+ currentOffsetX = clamp3(
8397
+ currentOffsetX + visibleBars * renderConfig.stepsPerBar * renderConfig.stepWidth,
8398
+ 0,
8399
+ getMaxOffsetX()
8400
+ );
8386
8401
  setDrawOffset(currentOffsetX, currentOffsetY);
8387
8402
  }
8388
8403
  redrawAll();
@@ -8452,9 +8467,10 @@ var mountDAW = (target, options = {}) => {
8452
8467
  }
8453
8468
  if (playbackState !== "paused") {
8454
8469
  const canvas = getGridCanvas();
8455
- currentOffsetX = Math.max(
8470
+ currentOffsetX = clamp3(
8471
+ playStartStep * renderConfig.stepWidth - canvas.width * 0.5,
8456
8472
  0,
8457
- playStartStep * renderConfig.stepWidth - canvas.width * 0.5
8473
+ getMaxOffsetX()
8458
8474
  );
8459
8475
  setDrawOffset(currentOffsetX, currentOffsetY);
8460
8476
  }
@@ -9037,7 +9053,7 @@ var mountDAW = (target, options = {}) => {
9037
9053
  if (firstPitch !== null) {
9038
9054
  centerPitch(firstPitch);
9039
9055
  } else {
9040
- setDrawOffset(currentOffsetX, currentOffsetY);
9056
+ centerPitch(48);
9041
9057
  }
9042
9058
  redrawAll();
9043
9059
  updateTrackPanel();
@@ -9116,7 +9132,7 @@ var mountDAW = (target, options = {}) => {
9116
9132
  if (firstPitch !== null) {
9117
9133
  centerPitch(firstPitch);
9118
9134
  } else {
9119
- setDrawOffset(currentOffsetX, currentOffsetY);
9135
+ centerPitch(48);
9120
9136
  }
9121
9137
  redrawAll();
9122
9138
  updateTrackPanel();
@@ -9586,6 +9602,7 @@ var mountDAW = (target, options = {}) => {
9586
9602
  document.addEventListener("pointermove", onPointerMove);
9587
9603
  document.addEventListener("pointerup", onPointerUp);
9588
9604
  const setLoading = (loading) => {
9605
+ isLoading = loading;
9589
9606
  refs.topbar.classList.toggle("is-loading", loading);
9590
9607
  };
9591
9608
  const getCurrentPlayStep = () => {
@@ -9613,9 +9630,10 @@ var mountDAW = (target, options = {}) => {
9613
9630
  currentPlayStep = step;
9614
9631
  playbackState = "paused";
9615
9632
  const canvas = getGridCanvas();
9616
- currentOffsetX = Math.max(
9633
+ currentOffsetX = clamp3(
9634
+ step * renderConfig.stepWidth - canvas.width * 0.5,
9617
9635
  0,
9618
- step * renderConfig.stepWidth - canvas.width * 0.5
9636
+ getMaxOffsetX()
9619
9637
  );
9620
9638
  setDrawOffset(currentOffsetX, currentOffsetY);
9621
9639
  updateTransport();
@@ -10373,11 +10391,11 @@ var SoundFont = class _SoundFont {
10373
10391
  const { buffer, _param } = zone;
10374
10392
  if (!buffer || !_param) return;
10375
10393
  src.buffer = buffer;
10376
- g.gain.setValueAtTime(volume, ctx.currentTime);
10377
10394
  src.playbackRate.setValueAtTime(_param.playbackRate, 0);
10378
10395
  Object.assign(src, _param.src);
10379
10396
  const _duration = duration + _SoundFont.afterTime;
10380
10397
  const end = _when + (isDrum ? buffer.duration : src.loop ? _duration : Math.min(_duration, _param.max));
10398
+ g.gain.setValueAtTime(volume, ctx.currentTime);
10381
10399
  if (!isDrum) g.gain.linearRampToValueAtTime(0, end);
10382
10400
  src.connect(g).connect(destination);
10383
10401
  src.start(_when);
@@ -10424,14 +10442,99 @@ var adjustZone = async (ctx, zone) => {
10424
10442
  a[i] = n / 65536;
10425
10443
  }
10426
10444
  } else if (zone.file) {
10427
- const buf = Uint8Array.from(atob(zone.file), (c) => c.charCodeAt(0)).buffer;
10445
+ const bytes = Uint8Array.from(atob(zone.file), (c) => c.charCodeAt(0));
10446
+ const buf = bytes.buffer;
10428
10447
  if (ctx.state === "interrupted") {
10429
10448
  try {
10430
10449
  await ctx.resume();
10431
10450
  } catch {
10432
10451
  }
10433
10452
  }
10434
- zone.buffer = await ctx.decodeAudioData(buf);
10453
+ try {
10454
+ zone.buffer = await ctx.decodeAudioData(buf);
10455
+ } catch (e) {
10456
+ console.error(
10457
+ `[zone.file format] keyRange: ${zone.keyRangeLow}-${zone.keyRangeHigh} - Decode failed:`,
10458
+ e
10459
+ );
10460
+ throw e;
10461
+ }
10462
+ }
10463
+ if (zone.buffer && zone.loopStart >= 1 && zone.loopStart < zone.loopEnd) {
10464
+ const loopLenSeconds = (zone.loopEnd - zone.loopStart) / zone.sampleRate;
10465
+ if (loopLenSeconds < 0.03) {
10466
+ const oldBuf = zone.buffer;
10467
+ const sampleRate = oldBuf.sampleRate;
10468
+ const rateRatio = sampleRate / zone.sampleRate;
10469
+ const loopStartFrame = Math.round(zone.loopStart * rateRatio);
10470
+ const loopEndFrame = Math.round(zone.loopEnd * rateRatio);
10471
+ const loopLengthFrame = loopEndFrame - loopStartFrame;
10472
+ if (loopLengthFrame > 0) {
10473
+ const minLoopLenFrame = Math.round(0.2 * sampleRate);
10474
+ const repeatCount = Math.ceil(minLoopLenFrame / loopLengthFrame);
10475
+ const attackLength = Math.min(loopStartFrame, oldBuf.length);
10476
+ const releaseLength = Math.max(0, oldBuf.length - loopEndFrame);
10477
+ const newLength = attackLength + loopLengthFrame * repeatCount + releaseLength;
10478
+ let totalPeak = 0;
10479
+ let loopPeak = 0;
10480
+ if (oldBuf.numberOfChannels > 0) {
10481
+ const ch0 = oldBuf.getChannelData(0);
10482
+ for (let i = 0; i < ch0.length; i++) {
10483
+ const abs = Math.abs(ch0[i]);
10484
+ if (abs > totalPeak) totalPeak = abs;
10485
+ if (i >= loopStartFrame && i < loopEndFrame) {
10486
+ if (abs > loopPeak) loopPeak = abs;
10487
+ }
10488
+ }
10489
+ }
10490
+ let gainMultiplier = 1;
10491
+ if (loopPeak > 0 && totalPeak > 0 && loopPeak < totalPeak * 0.8) {
10492
+ gainMultiplier = totalPeak * 0.75 / loopPeak;
10493
+ if (gainMultiplier > 20) gainMultiplier = 20;
10494
+ }
10495
+ try {
10496
+ const newBuf = ctx.createBuffer(
10497
+ oldBuf.numberOfChannels,
10498
+ newLength,
10499
+ sampleRate
10500
+ );
10501
+ for (let ch = 0; ch < oldBuf.numberOfChannels; ch++) {
10502
+ const oldData = oldBuf.getChannelData(ch);
10503
+ const newData = newBuf.getChannelData(ch);
10504
+ newData.set(oldData.subarray(0, attackLength), 0);
10505
+ let offset = attackLength;
10506
+ const loopData = oldData.subarray(loopStartFrame, loopEndFrame);
10507
+ const normalizedLoopData = new Float32Array(loopLengthFrame);
10508
+ for (let i = 0; i < loopLengthFrame; i++) {
10509
+ normalizedLoopData[i] = loopData[i] * gainMultiplier;
10510
+ }
10511
+ for (let r = 0; r < repeatCount; r++) {
10512
+ newData.set(normalizedLoopData, offset);
10513
+ offset += loopLengthFrame;
10514
+ }
10515
+ if (releaseLength > 0 && loopEndFrame < oldBuf.length) {
10516
+ const releaseData = oldData.subarray(loopEndFrame);
10517
+ if (gainMultiplier !== 1) {
10518
+ const normalizedRelease = new Float32Array(releaseLength);
10519
+ for (let i = 0; i < releaseLength; i++) {
10520
+ normalizedRelease[i] = releaseData[i] * gainMultiplier;
10521
+ }
10522
+ newData.set(normalizedRelease, offset);
10523
+ } else {
10524
+ newData.set(releaseData, offset);
10525
+ }
10526
+ }
10527
+ }
10528
+ zone.buffer = newBuf;
10529
+ zone.loopEnd = zone.loopStart + (zone.loopEnd - zone.loopStart) * repeatCount;
10530
+ } catch (e) {
10531
+ console.warn(
10532
+ "[SoundFont.loopExtension] Failed to extend loop buffer:",
10533
+ e
10534
+ );
10535
+ }
10536
+ }
10537
+ }
10435
10538
  }
10436
10539
  for (const [k, v] of [
10437
10540
  ["loopStart", 0],
@@ -10592,7 +10695,7 @@ var createDtmStudio = async (options = {}) => {
10592
10695
  presetUI: true,
10593
10696
  ...options.features
10594
10697
  };
10595
- const audioCtx = options.audioContext ?? new AudioContext();
10698
+ const audioCtx = options.audioContext ?? new AudioContext({ sampleRate: 44100 });
10596
10699
  const masterGain = audioCtx.createGain();
10597
10700
  masterGain.gain.value = options.masterVolume ?? 1;
10598
10701
  masterGain.connect(audioCtx.destination);
package/dist/index.mjs CHANGED
@@ -7588,7 +7588,6 @@ var mountDAW = (target, options = {}) => {
7588
7588
  stepWidth: BASE_STEP_WIDTH * 2
7589
7589
  // zoom100% 相当
7590
7590
  };
7591
- const leftPaddingSteps = renderConfig.stepsPerBar * 16;
7592
7591
  let zoomX = 100;
7593
7592
  let zoomY = 100;
7594
7593
  let bpm = options.defaultBpm ?? DEFAULT_BPM;
@@ -7602,8 +7601,8 @@ var mountDAW = (target, options = {}) => {
7602
7601
  let snapGridSteps = 12;
7603
7602
  const gridLineSteps = 48;
7604
7603
  let currentOffsetX = 0;
7605
- const _initPitch = options.initialScrollPitch;
7606
- let currentOffsetY = _initPitch !== void 0 ? (renderConfig.keyCount - 1 - _initPitch) * renderConfig.keyHeight - 215 : (104 - 1 - 60) * renderConfig.keyHeight - 215;
7604
+ const _initPitch = options.initialScrollPitch ?? 48;
7605
+ let currentOffsetY = (renderConfig.keyCount - 1 - _initPitch) * renderConfig.keyHeight - 215;
7607
7606
  let playStartStep = 0;
7608
7607
  let isSolo = false;
7609
7608
  let lyricTrackIndices = /* @__PURE__ */ new Set();
@@ -7611,6 +7610,7 @@ var mountDAW = (target, options = {}) => {
7611
7610
  let pausedPlayStep = 0;
7612
7611
  let currentPlayStep = 0;
7613
7612
  let ready = false;
7613
+ let isLoading = false;
7614
7614
  let selectedNotes = [];
7615
7615
  let selectionRect = null;
7616
7616
  let copiedNotes = [];
@@ -7630,7 +7630,7 @@ var mountDAW = (target, options = {}) => {
7630
7630
  };
7631
7631
  if (lyricsDebounceTimer) clearTimeout(lyricsDebounceTimer);
7632
7632
  lyricsDebounceTimer = setTimeout(() => {
7633
- options.onLyricsChange(trackId, data);
7633
+ options.onLyricsChange?.(trackId, data);
7634
7634
  lyricsDebounceTimer = null;
7635
7635
  }, 300);
7636
7636
  };
@@ -7710,14 +7710,28 @@ var mountDAW = (target, options = {}) => {
7710
7710
  const getActive = () => trackStates.find((t) => t.config.id === activeTrackId) ?? trackStates[0];
7711
7711
  let showModal;
7712
7712
  const getMaxNoteStep = () => {
7713
- let maxStep = renderConfig.stepsPerBar * 4;
7713
+ let maxEndStep = 0;
7714
7714
  for (const t of trackStates) {
7715
7715
  for (const n of t.core.getNotes()) {
7716
7716
  const end = n.startStep + n.durationSteps;
7717
- if (end > maxStep) maxStep = end;
7717
+ if (end > maxEndStep) {
7718
+ maxEndStep = end;
7719
+ }
7718
7720
  }
7719
7721
  }
7720
- return maxStep;
7722
+ if (maxEndStep === 0) {
7723
+ return renderConfig.stepsPerBar * 4;
7724
+ }
7725
+ const lastNoteMeasure = Math.floor(
7726
+ (maxEndStep - 1) / renderConfig.stepsPerBar
7727
+ );
7728
+ return (lastNoteMeasure + 2) * renderConfig.stepsPerBar;
7729
+ };
7730
+ const getMaxOffsetX = () => {
7731
+ const canvas = getGridCanvas();
7732
+ const maxNoteStep = getMaxNoteStep();
7733
+ const totalContentWidth = maxNoteStep * renderConfig.stepWidth;
7734
+ return Math.max(0, totalContentWidth - canvas.width);
7721
7735
  };
7722
7736
  const getMaxOffsetY = () => {
7723
7737
  const totalHeight = renderConfig.keyCount * renderConfig.keyHeight;
@@ -7797,19 +7811,14 @@ var mountDAW = (target, options = {}) => {
7797
7811
  };
7798
7812
  const updateScrollbars = () => {
7799
7813
  const canvas = getGridCanvas();
7800
- const maxNoteStep = getMaxNoteStep();
7801
- const leftPaddingWidth = leftPaddingSteps * renderConfig.stepWidth;
7802
- const totalContentWidth = maxNoteStep * renderConfig.stepWidth;
7803
- const maxOffsetX = totalContentWidth - canvas.width + leftPaddingWidth;
7814
+ const maxOffsetX = getMaxOffsetX();
7804
7815
  const sbW = refs.hScroll.clientWidth;
7805
7816
  if (maxOffsetX <= 0) {
7806
7817
  refs.hScrollThumb.style.width = "100%";
7807
7818
  refs.hScrollThumb.style.left = "0";
7808
7819
  } else {
7809
- const thumbW = Math.max(
7810
- 40,
7811
- canvas.width / (totalContentWidth + leftPaddingWidth) * sbW
7812
- );
7820
+ const totalContentWidth = getMaxNoteStep() * renderConfig.stepWidth;
7821
+ const thumbW = Math.max(40, canvas.width / totalContentWidth * sbW);
7813
7822
  const ratio = currentOffsetX / maxOffsetX;
7814
7823
  refs.hScrollThumb.style.width = `${thumbW}px`;
7815
7824
  refs.hScrollThumb.style.left = `${clamp3(ratio * (sbW - thumbW), 0, sbW - thumbW)}px`;
@@ -7863,11 +7872,7 @@ var mountDAW = (target, options = {}) => {
7863
7872
  draggingV = false;
7864
7873
  });
7865
7874
  const moveH = (clientX) => {
7866
- const canvas = getGridCanvas();
7867
- const maxNoteStep = getMaxNoteStep();
7868
- const leftPaddingWidth = leftPaddingSteps * renderConfig.stepWidth;
7869
- const totalContentWidth = maxNoteStep * renderConfig.stepWidth;
7870
- const maxOffsetX = totalContentWidth - canvas.width + leftPaddingWidth;
7875
+ const maxOffsetX = getMaxOffsetX();
7871
7876
  if (maxOffsetX <= 0) return;
7872
7877
  const rect = refs.hScroll.getBoundingClientRect();
7873
7878
  const thumbW = Number.parseFloat(refs.hScrollThumb.style.width) || 40;
@@ -7900,9 +7905,10 @@ var mountDAW = (target, options = {}) => {
7900
7905
  let selectedOriginal = [];
7901
7906
  let lastMultiPreviewPitch = null;
7902
7907
  const playPreview = (pitch) => {
7908
+ if (isLoading) return;
7903
7909
  options.onResumeAudio?.();
7904
7910
  const active = getActive();
7905
- dispatchNote(active.config.id, pitch, active.volume, 100, 0, 0.1);
7911
+ dispatchNote(active.config.id, pitch, active.volume, 100, 0, 0.5);
7906
7912
  };
7907
7913
  const findActiveNoteAt = (x, y, margin = 0) => {
7908
7914
  const active = getActive();
@@ -8175,7 +8181,11 @@ var mountDAW = (target, options = {}) => {
8175
8181
  0,
8176
8182
  getMaxOffsetY()
8177
8183
  );
8178
- currentOffsetX = Math.max(0, currentOffsetX + event.deltaX);
8184
+ currentOffsetX = clamp3(
8185
+ currentOffsetX + event.deltaX,
8186
+ 0,
8187
+ getMaxOffsetX()
8188
+ );
8179
8189
  setDrawOffset(currentOffsetX, currentOffsetY);
8180
8190
  redrawAll();
8181
8191
  },
@@ -8210,9 +8220,10 @@ var mountDAW = (target, options = {}) => {
8210
8220
  const centerStep = (currentOffsetX + canvas.width / 2) / renderConfig.stepWidth;
8211
8221
  renderConfig.stepWidth = BASE_STEP_WIDTH * (zoomX * 2) / 100;
8212
8222
  refs.zoomXLabel.textContent = `${zoomX}%`;
8213
- currentOffsetX = Math.max(
8223
+ currentOffsetX = clamp3(
8224
+ centerStep * renderConfig.stepWidth - canvas.width / 2,
8214
8225
  0,
8215
- centerStep * renderConfig.stepWidth - canvas.width / 2
8226
+ getMaxOffsetX()
8216
8227
  );
8217
8228
  setDrawOffset(currentOffsetX, currentOffsetY);
8218
8229
  redrawAll();
@@ -8271,7 +8282,11 @@ var mountDAW = (target, options = {}) => {
8271
8282
  const threshold = currentOffsetX / renderConfig.stepWidth + visibleSteps - 4;
8272
8283
  if (currentPlayStep > threshold) {
8273
8284
  const visibleBars = Math.round(visibleSteps / renderConfig.stepsPerBar);
8274
- currentOffsetX += visibleBars * renderConfig.stepsPerBar * renderConfig.stepWidth;
8285
+ currentOffsetX = clamp3(
8286
+ currentOffsetX + visibleBars * renderConfig.stepsPerBar * renderConfig.stepWidth,
8287
+ 0,
8288
+ getMaxOffsetX()
8289
+ );
8275
8290
  setDrawOffset(currentOffsetX, currentOffsetY);
8276
8291
  }
8277
8292
  redrawAll();
@@ -8341,9 +8356,10 @@ var mountDAW = (target, options = {}) => {
8341
8356
  }
8342
8357
  if (playbackState !== "paused") {
8343
8358
  const canvas = getGridCanvas();
8344
- currentOffsetX = Math.max(
8359
+ currentOffsetX = clamp3(
8360
+ playStartStep * renderConfig.stepWidth - canvas.width * 0.5,
8345
8361
  0,
8346
- playStartStep * renderConfig.stepWidth - canvas.width * 0.5
8362
+ getMaxOffsetX()
8347
8363
  );
8348
8364
  setDrawOffset(currentOffsetX, currentOffsetY);
8349
8365
  }
@@ -8926,7 +8942,7 @@ var mountDAW = (target, options = {}) => {
8926
8942
  if (firstPitch !== null) {
8927
8943
  centerPitch(firstPitch);
8928
8944
  } else {
8929
- setDrawOffset(currentOffsetX, currentOffsetY);
8945
+ centerPitch(48);
8930
8946
  }
8931
8947
  redrawAll();
8932
8948
  updateTrackPanel();
@@ -9005,7 +9021,7 @@ var mountDAW = (target, options = {}) => {
9005
9021
  if (firstPitch !== null) {
9006
9022
  centerPitch(firstPitch);
9007
9023
  } else {
9008
- setDrawOffset(currentOffsetX, currentOffsetY);
9024
+ centerPitch(48);
9009
9025
  }
9010
9026
  redrawAll();
9011
9027
  updateTrackPanel();
@@ -9475,6 +9491,7 @@ var mountDAW = (target, options = {}) => {
9475
9491
  document.addEventListener("pointermove", onPointerMove);
9476
9492
  document.addEventListener("pointerup", onPointerUp);
9477
9493
  const setLoading = (loading) => {
9494
+ isLoading = loading;
9478
9495
  refs.topbar.classList.toggle("is-loading", loading);
9479
9496
  };
9480
9497
  const getCurrentPlayStep = () => {
@@ -9502,9 +9519,10 @@ var mountDAW = (target, options = {}) => {
9502
9519
  currentPlayStep = step;
9503
9520
  playbackState = "paused";
9504
9521
  const canvas = getGridCanvas();
9505
- currentOffsetX = Math.max(
9522
+ currentOffsetX = clamp3(
9523
+ step * renderConfig.stepWidth - canvas.width * 0.5,
9506
9524
  0,
9507
- step * renderConfig.stepWidth - canvas.width * 0.5
9525
+ getMaxOffsetX()
9508
9526
  );
9509
9527
  setDrawOffset(currentOffsetX, currentOffsetY);
9510
9528
  updateTransport();
@@ -10262,11 +10280,11 @@ var SoundFont = class _SoundFont {
10262
10280
  const { buffer, _param } = zone;
10263
10281
  if (!buffer || !_param) return;
10264
10282
  src.buffer = buffer;
10265
- g.gain.setValueAtTime(volume, ctx.currentTime);
10266
10283
  src.playbackRate.setValueAtTime(_param.playbackRate, 0);
10267
10284
  Object.assign(src, _param.src);
10268
10285
  const _duration = duration + _SoundFont.afterTime;
10269
10286
  const end = _when + (isDrum ? buffer.duration : src.loop ? _duration : Math.min(_duration, _param.max));
10287
+ g.gain.setValueAtTime(volume, ctx.currentTime);
10270
10288
  if (!isDrum) g.gain.linearRampToValueAtTime(0, end);
10271
10289
  src.connect(g).connect(destination);
10272
10290
  src.start(_when);
@@ -10313,14 +10331,99 @@ var adjustZone = async (ctx, zone) => {
10313
10331
  a[i] = n / 65536;
10314
10332
  }
10315
10333
  } else if (zone.file) {
10316
- const buf = Uint8Array.from(atob(zone.file), (c) => c.charCodeAt(0)).buffer;
10334
+ const bytes = Uint8Array.from(atob(zone.file), (c) => c.charCodeAt(0));
10335
+ const buf = bytes.buffer;
10317
10336
  if (ctx.state === "interrupted") {
10318
10337
  try {
10319
10338
  await ctx.resume();
10320
10339
  } catch {
10321
10340
  }
10322
10341
  }
10323
- zone.buffer = await ctx.decodeAudioData(buf);
10342
+ try {
10343
+ zone.buffer = await ctx.decodeAudioData(buf);
10344
+ } catch (e) {
10345
+ console.error(
10346
+ `[zone.file format] keyRange: ${zone.keyRangeLow}-${zone.keyRangeHigh} - Decode failed:`,
10347
+ e
10348
+ );
10349
+ throw e;
10350
+ }
10351
+ }
10352
+ if (zone.buffer && zone.loopStart >= 1 && zone.loopStart < zone.loopEnd) {
10353
+ const loopLenSeconds = (zone.loopEnd - zone.loopStart) / zone.sampleRate;
10354
+ if (loopLenSeconds < 0.03) {
10355
+ const oldBuf = zone.buffer;
10356
+ const sampleRate = oldBuf.sampleRate;
10357
+ const rateRatio = sampleRate / zone.sampleRate;
10358
+ const loopStartFrame = Math.round(zone.loopStart * rateRatio);
10359
+ const loopEndFrame = Math.round(zone.loopEnd * rateRatio);
10360
+ const loopLengthFrame = loopEndFrame - loopStartFrame;
10361
+ if (loopLengthFrame > 0) {
10362
+ const minLoopLenFrame = Math.round(0.2 * sampleRate);
10363
+ const repeatCount = Math.ceil(minLoopLenFrame / loopLengthFrame);
10364
+ const attackLength = Math.min(loopStartFrame, oldBuf.length);
10365
+ const releaseLength = Math.max(0, oldBuf.length - loopEndFrame);
10366
+ const newLength = attackLength + loopLengthFrame * repeatCount + releaseLength;
10367
+ let totalPeak = 0;
10368
+ let loopPeak = 0;
10369
+ if (oldBuf.numberOfChannels > 0) {
10370
+ const ch0 = oldBuf.getChannelData(0);
10371
+ for (let i = 0; i < ch0.length; i++) {
10372
+ const abs = Math.abs(ch0[i]);
10373
+ if (abs > totalPeak) totalPeak = abs;
10374
+ if (i >= loopStartFrame && i < loopEndFrame) {
10375
+ if (abs > loopPeak) loopPeak = abs;
10376
+ }
10377
+ }
10378
+ }
10379
+ let gainMultiplier = 1;
10380
+ if (loopPeak > 0 && totalPeak > 0 && loopPeak < totalPeak * 0.8) {
10381
+ gainMultiplier = totalPeak * 0.75 / loopPeak;
10382
+ if (gainMultiplier > 20) gainMultiplier = 20;
10383
+ }
10384
+ try {
10385
+ const newBuf = ctx.createBuffer(
10386
+ oldBuf.numberOfChannels,
10387
+ newLength,
10388
+ sampleRate
10389
+ );
10390
+ for (let ch = 0; ch < oldBuf.numberOfChannels; ch++) {
10391
+ const oldData = oldBuf.getChannelData(ch);
10392
+ const newData = newBuf.getChannelData(ch);
10393
+ newData.set(oldData.subarray(0, attackLength), 0);
10394
+ let offset = attackLength;
10395
+ const loopData = oldData.subarray(loopStartFrame, loopEndFrame);
10396
+ const normalizedLoopData = new Float32Array(loopLengthFrame);
10397
+ for (let i = 0; i < loopLengthFrame; i++) {
10398
+ normalizedLoopData[i] = loopData[i] * gainMultiplier;
10399
+ }
10400
+ for (let r = 0; r < repeatCount; r++) {
10401
+ newData.set(normalizedLoopData, offset);
10402
+ offset += loopLengthFrame;
10403
+ }
10404
+ if (releaseLength > 0 && loopEndFrame < oldBuf.length) {
10405
+ const releaseData = oldData.subarray(loopEndFrame);
10406
+ if (gainMultiplier !== 1) {
10407
+ const normalizedRelease = new Float32Array(releaseLength);
10408
+ for (let i = 0; i < releaseLength; i++) {
10409
+ normalizedRelease[i] = releaseData[i] * gainMultiplier;
10410
+ }
10411
+ newData.set(normalizedRelease, offset);
10412
+ } else {
10413
+ newData.set(releaseData, offset);
10414
+ }
10415
+ }
10416
+ }
10417
+ zone.buffer = newBuf;
10418
+ zone.loopEnd = zone.loopStart + (zone.loopEnd - zone.loopStart) * repeatCount;
10419
+ } catch (e) {
10420
+ console.warn(
10421
+ "[SoundFont.loopExtension] Failed to extend loop buffer:",
10422
+ e
10423
+ );
10424
+ }
10425
+ }
10426
+ }
10324
10427
  }
10325
10428
  for (const [k, v] of [
10326
10429
  ["loopStart", 0],
@@ -10480,7 +10583,7 @@ var createDtmStudio = async (options = {}) => {
10480
10583
  presetUI: true,
10481
10584
  ...options.features
10482
10585
  };
10483
- const audioCtx = options.audioContext ?? new AudioContext();
10586
+ const audioCtx = options.audioContext ?? new AudioContext({ sampleRate: 44100 });
10484
10587
  const masterGain = audioCtx.createGain();
10485
10588
  masterGain.gain.value = options.masterVolume ?? 1;
10486
10589
  masterGain.connect(audioCtx.destination);
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "author": "onjmin",
3
3
  "license": "MIT",
4
4
  "name": "@onjmin/dtm",
5
- "version": "0.1.39",
5
+ "version": "0.1.40",
6
6
  "description": "MMLを中間言語に用いた、モバイルファーストなDAW / ピアノロール打ち込みコンポーネント",
7
7
  "homepage": "https://onjmin.github.io/dtm",
8
8
  "repository": {