@onjmin/dtm 0.1.25 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -99,7 +99,6 @@ __export(index_exports, {
99
99
  playMML: () => playMML,
100
100
  playSingingMML: () => playSingingMML,
101
101
  setDrawOffset: () => setDrawOffset,
102
- setupRecorder: () => setupRecorder,
103
102
  shiftNotes: () => shiftNotes,
104
103
  stripLyrics: () => stripLyrics,
105
104
  stripMmlMeta: () => stripMmlMeta,
@@ -116,34 +115,6 @@ function createAudioContext() {
116
115
  drumGainNode.connect(audioCtx.destination);
117
116
  return { audioCtx, gainNode, drumGainNode };
118
117
  }
119
- function setupRecorder(audioCtx, gainNode, drumGainNode) {
120
- let isRecording = false;
121
- let recordedData = [[], []];
122
- const recorderProcessor = audioCtx.createScriptProcessor(4096, 2, 2);
123
- recorderProcessor.onaudioprocess = (e) => {
124
- if (!isRecording) return;
125
- const left = e.inputBuffer.getChannelData(0);
126
- const right = e.inputBuffer.getChannelData(1);
127
- recordedData[0].push(left.slice());
128
- recordedData[1].push(right.slice());
129
- };
130
- gainNode.connect(recorderProcessor);
131
- drumGainNode.connect(recorderProcessor);
132
- recorderProcessor.connect(audioCtx.destination);
133
- return {
134
- startRecording: () => {
135
- isRecording = true;
136
- },
137
- stopRecording: () => {
138
- isRecording = false;
139
- },
140
- getRecordedData: () => recordedData,
141
- isRecording: () => isRecording,
142
- clearRecordedData: () => {
143
- recordedData = [[], []];
144
- }
145
- };
146
- }
147
118
  async function fetchSoundFontList(ttl) {
148
119
  const res = await fetch(`https://rpgen3.github.io/soundfont/list/${ttl}.txt`);
149
120
  const str = await res.text();
@@ -1240,8 +1211,9 @@ var buildUI = (target, options) => {
1240
1211
  target.innerHTML = `
1241
1212
  <div class="dtm-daw" data-dtm="root">
1242
1213
  <div class="dtm-topbar" data-dtm="transport">
1214
+ <button class="dtm-iconbtn" data-dtm="prev-bar" title="1\u5C0F\u7BC0\u524D">${icon("chevronLeft")}</button>
1243
1215
  <button class="dtm-play" data-dtm="play" disabled>${icon("play")}<span>\u8A66\u8074</span></button>
1244
- <button class="dtm-iconbtn dtm-rec" data-dtm="rec" title="\u9332\u97F3">${icon("record")}</button>
1216
+ <button class="dtm-iconbtn" data-dtm="next-bar" title="1\u5C0F\u7BC0\u5F8C">${icon("chevronRight")}</button>
1245
1217
  <label class="dtm-toggle"><input type="checkbox" data-dtm="solo"><span>\u30BD\u30ED</span></label>
1246
1218
  <span class="dtm-topbar-loading dtm-blink" data-dtm="topbar-loading">... LOADING ...</span>
1247
1219
  <span class="dtm-grow"></span>
@@ -1406,10 +1378,12 @@ var buildUI = (target, options) => {
1406
1378
  </div>
1407
1379
  <div class="dtm-output dtm-hidden" data-dtm="output-container">
1408
1380
  <p class="dtm-label" data-dtm="output-status"></p>
1381
+ <div class="dtm-output-label">\u6539\u884C\u3042\u308A\u7248</div>
1409
1382
  <div class="dtm-output-row">
1410
1383
  <pre><code data-dtm="output-full"></code></pre>
1411
1384
  <button class="dtm-btn dtm-btn--primary dtm-btn--icon" data-dtm="copy-full" title="\u30B3\u30D4\u30FC">${icon("copy")}</button>
1412
1385
  </div>
1386
+ <div class="dtm-output-label">\uFF11\u884C\u7248</div>
1413
1387
  <div class="dtm-output-row">
1414
1388
  <pre><code data-dtm="output-mini"></code></pre>
1415
1389
  <button class="dtm-btn dtm-btn--primary dtm-btn--icon" data-dtm="copy-mini" title="\u30B3\u30D4\u30FC">${icon("copy")}</button>
@@ -1437,7 +1411,8 @@ var buildUI = (target, options) => {
1437
1411
  topbar: sel("transport"),
1438
1412
  topbarLoading: sel("topbar-loading"),
1439
1413
  playBtn: sel("play"),
1440
- recBtn: sel("rec"),
1414
+ prevBarBtn: sel("prev-bar"),
1415
+ nextBarBtn: sel("next-bar"),
1441
1416
  soloCheckbox: sel("solo"),
1442
1417
  toolPen: sel("tool-pen"),
1443
1418
  toolSelect: sel("tool-select"),
@@ -2742,7 +2717,7 @@ var createKoeVoice = async (ctx, destination, options) => {
2742
2717
  return model;
2743
2718
  };
2744
2719
  var PREWARM_NOTES = 3;
2745
- var STREAM_LOOKAHEAD_SEC = 1.5;
2720
+ var STREAM_LOOKAHEAD_SEC = 6;
2746
2721
  var STREAM_POLL_MS = 100;
2747
2722
  var FALLBACK_MODEL = "klatt";
2748
2723
  var createSingingVoices = (ctx, destination, options = {}) => {
@@ -2806,7 +2781,7 @@ var createSingingVoices = (ctx, destination, options = {}) => {
2806
2781
  if (!m?.renderToCache) continue;
2807
2782
  let n = 0;
2808
2783
  forEachSungNote(track, (note, prevVowel) => {
2809
- if (n >= count) return;
2784
+ if (n >= count && note.startSec >= STREAM_LOOKAHEAD_SEC) return;
2810
2785
  n++;
2811
2786
  promises.push(
2812
2787
  m.renderToCache?.(
@@ -2839,14 +2814,18 @@ var createSingingVoices = (ctx, destination, options = {}) => {
2839
2814
  if (opts?.isAudible && !opts.isAudible(track)) continue;
2840
2815
  const t0 = anchorTime + note.startSec;
2841
2816
  if (model.renderToCache && model.scheduleCached) {
2842
- const key = await model.renderToCache(
2843
- note.syllable,
2844
- prevVowel,
2845
- note.pitch,
2846
- note.durationSec * 1e3
2847
- );
2848
- if (session !== streamSession) return;
2849
- if (key) model.scheduleCached(key, t0, peak, track.pan);
2817
+ const renderToCache = model.renderToCache;
2818
+ const scheduleCached = model.scheduleCached;
2819
+ void (async () => {
2820
+ const key = await renderToCache(
2821
+ note.syllable,
2822
+ prevVowel,
2823
+ note.pitch,
2824
+ note.durationSec * 1e3
2825
+ );
2826
+ if (session !== streamSession) return;
2827
+ if (key) scheduleCached(key, t0, peak, track.pan);
2828
+ })();
2850
2829
  } else {
2851
2830
  const when = t0 - ctx.currentTime;
2852
2831
  model(note.syllable, {
@@ -4255,7 +4234,7 @@ var PITCH_MAP2 = {
4255
4234
  b: 11
4256
4235
  };
4257
4236
  var clamp2 = (value, lo, hi) => Math.min(hi, Math.max(lo, value));
4258
- var META_DIRECTIVE = /#(inst|drum|volume|mode)=([\w-]+)/gi;
4237
+ var META_DIRECTIVE = /#(inst|drum|volume|drumvolume|mode)=([\w-]+)/gi;
4259
4238
  var parseMmlMeta = (mml) => {
4260
4239
  const meta = {};
4261
4240
  for (const m of mml.matchAll(META_DIRECTIVE)) {
@@ -4265,6 +4244,9 @@ var parseMmlMeta = (mml) => {
4265
4244
  else if (key === "volume") {
4266
4245
  const v = Number.parseInt(m[2], 10);
4267
4246
  if (!Number.isNaN(v)) meta.volume = v;
4247
+ } else if (key === "drumvolume") {
4248
+ const dv = Number.parseInt(m[2], 10);
4249
+ if (!Number.isNaN(dv)) meta.drumVolume = dv;
4268
4250
  } else if (key === "mode") {
4269
4251
  if (m[2] === "simple" || m[2] === "advanced") {
4270
4252
  meta.mode = m[2];
@@ -4279,6 +4261,8 @@ var formatMmlMeta = (meta) => {
4279
4261
  if (meta.instrument) parts.push(`#inst=${meta.instrument}`);
4280
4262
  if (meta.drum) parts.push(`#drum=${meta.drum}`);
4281
4263
  if (meta.volume !== void 0) parts.push(`#volume=${meta.volume}`);
4264
+ if (meta.drumVolume !== void 0)
4265
+ parts.push(`#drumvolume=${meta.drumVolume}`);
4282
4266
  if (meta.mode) parts.push(`#mode=${meta.mode}`);
4283
4267
  return parts.join(" ");
4284
4268
  };
@@ -5000,7 +4984,7 @@ var DAW_CSS = `
5000
4984
  top: 0;
5001
4985
  z-index: 20;
5002
4986
  display: flex;
5003
- flex-wrap: wrap;
4987
+ flex-wrap: nowrap;
5004
4988
  align-items: center;
5005
4989
  gap: var(--dtm-gap);
5006
4990
  padding: 6px;
@@ -5010,6 +4994,17 @@ var DAW_CSS = `
5010
4994
  inset 0 0 0 2px var(--c-black),
5011
4995
  0 0 0 2px var(--dtm-success),
5012
4996
  4px 4px 0 var(--c-black);
4997
+ overflow-x: auto;
4998
+ scrollbar-width: none;
4999
+ }
5000
+ .dtm-topbar::-webkit-scrollbar {
5001
+ display: none;
5002
+ }
5003
+ .dtm-topbar > * {
5004
+ flex-shrink: 0;
5005
+ }
5006
+ .dtm-topbar > .dtm-grow {
5007
+ flex-shrink: 1;
5013
5008
  }
5014
5009
 
5015
5010
  /* PLAY\u30DC\u30BF\u30F3 \u2014 \u30B2\u30FC\u30E0\u306E\u300C\u6C7A\u5B9A\u30DC\u30BF\u30F3\u300D\u7684\u5B58\u5728\u611F */
@@ -5360,6 +5355,15 @@ var DAW_CSS = `
5360
5355
  line-height: 1.8;
5361
5356
  color: var(--dtm-success);
5362
5357
  }
5358
+ .dtm-output-label {
5359
+ font-size: 11px;
5360
+ color: var(--dtm-muted);
5361
+ font-family: var(--dtm-font);
5362
+ margin-top: 10px;
5363
+ }
5364
+ .dtm-output-label:first-of-type {
5365
+ margin-top: 0;
5366
+ }
5363
5367
  .dtm-output-row { display: flex; gap: 8px; align-items: flex-start; margin-top: 6px; }
5364
5368
  .dtm-output-row pre { flex: 1; }
5365
5369
 
@@ -6203,6 +6207,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6203
6207
  const drumPatternDict = options.drumPatterns ?? DRUM_PATTERNS;
6204
6208
  const drumPattern = meta.drum ? drumPatternDict[meta.drum] ?? null : null;
6205
6209
  const trackVolume = meta.volume ?? options.volume ?? 100;
6210
+ const drumVolume = meta.drumVolume ?? 80;
6206
6211
  const colors = options.trackColors ?? DEFAULT_TRACK_COLORS;
6207
6212
  const useSynth = options.synth ?? !options.onPlayNote;
6208
6213
  const secondsPerStep = 60 / bpm / STEPS_PER_BEAT3;
@@ -6819,9 +6824,9 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6819
6824
  }
6820
6825
  target.appendChild(root);
6821
6826
  let consentOverlayEl = null;
6822
- const checkConsentAndShow = () => {
6827
+ const checkConsentAndShow = (onAgree) => {
6823
6828
  try {
6824
- if (options.skipConsent) return;
6829
+ if (options.skipConsent) return false;
6825
6830
  const unagreed = termsModels.filter((model) => {
6826
6831
  if (agreedModelsInSession.has(model)) return false;
6827
6832
  try {
@@ -6835,7 +6840,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6835
6840
  return true;
6836
6841
  }
6837
6842
  });
6838
- if (unagreed.length === 0) return;
6843
+ if (unagreed.length === 0) return false;
6839
6844
  const consentOverlay = doc.createElement("div");
6840
6845
  consentOverlay.className = "dtm-consent-overlay";
6841
6846
  const modal = doc.createElement("div");
@@ -6878,17 +6883,19 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6878
6883
  }
6879
6884
  consentOverlay.remove();
6880
6885
  consentOverlayEl = null;
6886
+ if (onAgree) onAgree();
6881
6887
  };
6882
6888
  footer.appendChild(btn);
6883
6889
  modal.append(header, body2, footer);
6884
6890
  consentOverlay.appendChild(modal);
6885
6891
  doc.body.appendChild(consentOverlay);
6886
6892
  consentOverlayEl = consentOverlay;
6893
+ return true;
6887
6894
  } catch (err2) {
6888
6895
  console.error("[dtm-player] Error in checkConsentAndShow:", err2);
6896
+ return false;
6889
6897
  }
6890
6898
  };
6891
- checkConsentAndShow();
6892
6899
  const autoScroll = (lane, el) => {
6893
6900
  if (el.offsetWidth === 0 || lane.clientWidth === 0) return;
6894
6901
  const elementCenter = el.offsetLeft + el.offsetWidth / 2;
@@ -6947,7 +6954,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6947
6954
  if (useSynth) ensureSynth().playNote(e);
6948
6955
  },
6949
6956
  onPlayDrum: (e) => {
6950
- const velocity = e.velocity * (trackVolume / 100);
6957
+ const velocity = e.velocity * (drumVolume / 100) * (trackVolume / 100);
6951
6958
  options.onPlayDrum?.({ ...e, velocity });
6952
6959
  if (useSynth) ensureSynth().playDrum({ ...e, velocity });
6953
6960
  },
@@ -7021,6 +7028,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
7021
7028
  };
7022
7029
  const play = () => {
7023
7030
  if (playing || trackIndices.length === 0) return;
7031
+ if (checkConsentAndShow(() => play())) return;
7024
7032
  if (activePlayer && activePlayer !== instance) activePlayer.stop();
7025
7033
  activePlayer = instance;
7026
7034
  setPlayingUI(true);
@@ -8049,7 +8057,7 @@ var mountDAW = (target, options = {}) => {
8049
8057
  };
8050
8058
  const updateTransport = () => {
8051
8059
  const playing = playbackState === "playing";
8052
- const label = playing ? "\u4E00\u6642\u505C\u6B62" : playbackState === "paused" ? "\u518D\u958B" : "\u8A66\u8074";
8060
+ const label = playing ? "\u505C\u6B62" : playbackState === "paused" ? "\u518D\u958B" : "\u8A66\u8074";
8053
8061
  refs.playBtn.innerHTML = `${icon(playing ? "pause" : "play")}<span>${label}</span>`;
8054
8062
  refs.playBtn.classList.toggle("dtm-play--stop", playing);
8055
8063
  };
@@ -8317,6 +8325,7 @@ var mountDAW = (target, options = {}) => {
8317
8325
  instrument: currentInstrument || void 0,
8318
8326
  drum: currentDrumPattern !== "none" ? currentDrumPattern : void 0,
8319
8327
  volume: masterVolume,
8328
+ drumVolume,
8320
8329
  mode
8321
8330
  });
8322
8331
  if (refs.decomposeChordToggle.checked) {
@@ -8468,6 +8477,11 @@ var mountDAW = (target, options = {}) => {
8468
8477
  refs.masterVolume.value = String(meta.volume);
8469
8478
  refs.masterVolumeLabel.textContent = `${meta.volume}%`;
8470
8479
  }
8480
+ if (meta.drumVolume !== void 0) {
8481
+ drumVolume = meta.drumVolume;
8482
+ refs.drumVolume.value = String(meta.drumVolume);
8483
+ refs.drumVolumeLabel.textContent = `${meta.drumVolume}%`;
8484
+ }
8471
8485
  for (const t of trackStates) {
8472
8486
  t.lyrics = "";
8473
8487
  t.lyricModel = "";
@@ -8621,8 +8635,17 @@ var mountDAW = (target, options = {}) => {
8621
8635
  const wireEvents = () => {
8622
8636
  refs.playBtn.addEventListener("click", togglePlay);
8623
8637
  refs.playBtn.disabled = false;
8624
- refs.recBtn.addEventListener("click", () => options.onToggleRecord?.());
8625
- refs.recBtn.style.display = options.onToggleRecord ? "" : "none";
8638
+ refs.prevBarBtn.addEventListener("click", () => {
8639
+ const targetStep = Math.max(
8640
+ 0,
8641
+ Math.floor((getCurrentPlayStep() - 1) / renderConfig.stepsPerBar) * renderConfig.stepsPerBar
8642
+ );
8643
+ jumpTo(targetStep);
8644
+ });
8645
+ refs.nextBarBtn.addEventListener("click", () => {
8646
+ const targetStep = Math.floor(getCurrentPlayStep() / renderConfig.stepsPerBar + 1) * renderConfig.stepsPerBar;
8647
+ jumpTo(targetStep);
8648
+ });
8626
8649
  refs.soloCheckbox.addEventListener("change", () => {
8627
8650
  isSolo = refs.soloCheckbox.checked;
8628
8651
  });
@@ -8985,6 +9008,20 @@ var mountDAW = (target, options = {}) => {
8985
9008
  if (playbackState === "paused") return pausedPlayStep;
8986
9009
  return playStartStep;
8987
9010
  };
9011
+ const jumpTo = async (step) => {
9012
+ const wasPlaying = playbackState === "playing";
9013
+ if (wasPlaying) {
9014
+ sequencer.stop();
9015
+ options.singingVoices?.stopStream();
9016
+ playStartStep = step;
9017
+ pausedPlayStep = step;
9018
+ currentPlayStep = step;
9019
+ playbackState = "paused";
9020
+ await play();
9021
+ } else {
9022
+ forcePauseAt(step);
9023
+ }
9024
+ };
8988
9025
  const forcePauseAt = (step) => {
8989
9026
  playStartStep = step;
8990
9027
  pausedPlayStep = step;
@@ -9059,6 +9096,7 @@ var playMML = (mml, options = {}) => {
9059
9096
  const drumPatternDict = options.drumPatterns ?? DRUM_PATTERNS;
9060
9097
  const drumPattern = meta.drum ? drumPatternDict[meta.drum] ?? null : null;
9061
9098
  let masterVolume = meta.volume ?? options.volume ?? 100;
9099
+ let drumVolume = meta.drumVolume ?? 80;
9062
9100
  const trackIndices = [...new Set(placements.map((p) => p.trackIndex))].sort(
9063
9101
  (a, b) => a - b
9064
9102
  );
@@ -9095,7 +9133,7 @@ var playMML = (mml, options = {}) => {
9095
9133
  synth?.playNote(e);
9096
9134
  },
9097
9135
  onPlayDrum: (e) => {
9098
- const velocity = e.velocity * (masterVolume / 100);
9136
+ const velocity = e.velocity * (drumVolume / 100) * (masterVolume / 100);
9099
9137
  options.onPlayDrum?.({ ...e, velocity });
9100
9138
  synth?.playDrum({ ...e, velocity });
9101
9139
  },
@@ -9654,7 +9692,6 @@ var importFrom = async (url2, name) => {
9654
9692
  var createDtmStudio = async (options = {}) => {
9655
9693
  const cdn = { ...DEFAULT_CDN, ...options.cdn };
9656
9694
  const features = {
9657
- recorder: true,
9658
9695
  midi: true,
9659
9696
  chord: true,
9660
9697
  presetUI: true,
@@ -9693,68 +9730,6 @@ var createDtmStudio = async (options = {}) => {
9693
9730
  const singingVoices = createSingingVoices(audioCtx, masterGain, {
9694
9731
  voiceWorkerUrl
9695
9732
  });
9696
- const recorder = features.recorder ? setupRecorder(audioCtx, masterGain, drumGain) : null;
9697
- const downloadWav = () => {
9698
- if (!recorder) return;
9699
- const recordedData = recorder.getRecordedData();
9700
- const ch = recordedData.length;
9701
- const len = recordedData[0].length;
9702
- if (len === 0) return;
9703
- const bufSize = recordedData[0][0].length;
9704
- const wave = new Float32Array(ch * len * bufSize);
9705
- let idx = 0;
9706
- for (let i = 0; i < len; i++)
9707
- for (let j = 0; j < bufSize; j++)
9708
- for (let k = 0; k < ch; k++) wave[idx++] = recordedData[k][i][j];
9709
- const sampleRate = audioCtx.sampleRate;
9710
- const channels = 2;
9711
- const bitRate = 16;
9712
- const step = bitRate / 8;
9713
- const blockSize = channels * step;
9714
- const byteLen = wave.length * step;
9715
- const view = new DataView(new ArrayBuffer(44 + byteLen));
9716
- const ws = (off2, s) => {
9717
- for (let i = 0; i < s.length; i++)
9718
- view.setUint8(off2 + i, s.charCodeAt(i));
9719
- };
9720
- ws(0, "RIFF");
9721
- view.setUint32(4, 32 + byteLen, true);
9722
- ws(8, "WAVE");
9723
- ws(12, "fmt ");
9724
- view.setUint32(16, 16, true);
9725
- view.setUint16(20, 1, true);
9726
- view.setUint16(22, channels, true);
9727
- view.setUint32(24, sampleRate, true);
9728
- view.setUint32(28, sampleRate * blockSize, true);
9729
- view.setUint16(32, blockSize, true);
9730
- view.setUint16(34, bitRate, true);
9731
- ws(36, "data");
9732
- view.setUint32(40, byteLen, true);
9733
- const clamp4 = (n, a2, b) => Math.max(a2, Math.min(b, n));
9734
- let off = 44;
9735
- for (let i = 0; i < wave.length; i++, off += step)
9736
- view.setInt16(
9737
- off,
9738
- clamp4(Math.round(wave[i] * 32768), -32768, 32767),
9739
- true
9740
- );
9741
- const blob2 = new Blob([view], { type: "audio/wav" });
9742
- const url2 = URL.createObjectURL(blob2);
9743
- const a = document.createElement("a");
9744
- a.href = url2;
9745
- a.download = "record.wav";
9746
- a.click();
9747
- URL.revokeObjectURL(url2);
9748
- };
9749
- const onToggleRecord = recorder ? () => {
9750
- if (recorder.isRecording()) {
9751
- recorder.stopRecording();
9752
- downloadWav();
9753
- } else {
9754
- recorder.clearRecordedData();
9755
- recorder.startRecording();
9756
- }
9757
- } : void 0;
9758
9733
  const listReady = new Promise((resolve) => {
9759
9734
  SoundFont_list.init();
9760
9735
  SoundFont_list.onload(() => resolve());
@@ -9961,7 +9936,6 @@ var createDtmStudio = async (options = {}) => {
9961
9936
  onPlayDrum: playDrum,
9962
9937
  singingVoices,
9963
9938
  parseMidi,
9964
- onToggleRecord,
9965
9939
  onInstrumentChange: handleInstrumentChange,
9966
9940
  ...dawOverrides
9967
9941
  };
@@ -10253,7 +10227,6 @@ var createDtmStudio = async (options = {}) => {
10253
10227
  playMML,
10254
10228
  playSingingMML,
10255
10229
  setDrawOffset,
10256
- setupRecorder,
10257
10230
  shiftNotes,
10258
10231
  stripLyrics,
10259
10232
  stripMmlMeta,