@onjmin/dtm 0.1.28 → 0.1.29

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
@@ -309,6 +309,17 @@ type StreamPlaybackOptions = {
309
309
  * 引数には遅れたノート情報と遅延秒数が渡されます。
310
310
  */
311
311
  onLateSkip?: (note: StreamVoiceNote, delay: number) => void;
312
+ /**
313
+ * 合成が間に合わず、代替メロディ(楽器)を鳴らす際のコールバック。
314
+ * 引数には対象トラック、ノート情報、および発音すべき絶対時刻(AudioContextクロック秒)が渡されます。
315
+ */
316
+ onFallbackPlayNote?: (track: StreamVoiceTrack, note: StreamVoiceNote, t0: number) => void;
317
+ /**
318
+ * 遅延判定の閾値(秒)。発音予定時刻よりこの秒数前に合成完了していない場合、
319
+ * 遅延するとみなし代替メロディに切り替えます。
320
+ * 省略時は 0.15 秒(150ms)です。
321
+ */
322
+ fallbackThresholdSec?: number;
312
323
  };
313
324
  /**
314
325
  * 歌唱モデルをまとめて管理し、koeデモ式の「先読みストリーミング合成」で歌わせる高レベルヘルパ。
package/dist/index.d.ts CHANGED
@@ -309,6 +309,17 @@ type StreamPlaybackOptions = {
309
309
  * 引数には遅れたノート情報と遅延秒数が渡されます。
310
310
  */
311
311
  onLateSkip?: (note: StreamVoiceNote, delay: number) => void;
312
+ /**
313
+ * 合成が間に合わず、代替メロディ(楽器)を鳴らす際のコールバック。
314
+ * 引数には対象トラック、ノート情報、および発音すべき絶対時刻(AudioContextクロック秒)が渡されます。
315
+ */
316
+ onFallbackPlayNote?: (track: StreamVoiceTrack, note: StreamVoiceNote, t0: number) => void;
317
+ /**
318
+ * 遅延判定の閾値(秒)。発音予定時刻よりこの秒数前に合成完了していない場合、
319
+ * 遅延するとみなし代替メロディに切り替えます。
320
+ * 省略時は 0.15 秒(150ms)です。
321
+ */
322
+ fallbackThresholdSec?: number;
312
323
  };
313
324
  /**
314
325
  * 歌唱モデルをまとめて管理し、koeデモ式の「先読みストリーミング合成」で歌わせる高レベルヘルパ。
package/dist/index.js CHANGED
@@ -2826,13 +2826,44 @@ var createSingingVoices = (ctx, destination, options = {}) => {
2826
2826
  if (model.renderToCache && model.scheduleCached) {
2827
2827
  const renderToCache = model.renderToCache;
2828
2828
  const scheduleCached = model.scheduleCached;
2829
+ const threshold = opts?.fallbackThresholdSec ?? 0.15;
2829
2830
  void (async () => {
2830
- const key = await renderToCache(
2831
+ let isTimeout = false;
2832
+ let timeoutId;
2833
+ const renderPromise = renderToCache(
2831
2834
  note.syllable,
2832
2835
  prevVowel,
2833
2836
  note.pitch,
2834
2837
  note.durationSec * 1e3
2838
+ ).then((key2) => {
2839
+ if (timeoutId) clearTimeout(timeoutId);
2840
+ return { key: key2, type: "success" };
2841
+ });
2842
+ const timeoutLimitTime = t0 - threshold;
2843
+ const timeoutDelayMs = (timeoutLimitTime - ctx.currentTime) * 1e3;
2844
+ const timeoutPromise = new Promise(
2845
+ (resolve) => {
2846
+ if (timeoutDelayMs <= 0) {
2847
+ isTimeout = true;
2848
+ resolve({ key: null, type: "timeout" });
2849
+ } else {
2850
+ timeoutId = setTimeout(() => {
2851
+ isTimeout = true;
2852
+ resolve({ key: null, type: "timeout" });
2853
+ }, timeoutDelayMs);
2854
+ }
2855
+ }
2835
2856
  );
2857
+ const result = await Promise.race([renderPromise, timeoutPromise]);
2858
+ if (session !== streamSession) return;
2859
+ if (result.type === "timeout") {
2860
+ console.warn(
2861
+ `[dtm] Synthesizer expected late skip: ${note.syllable.kana} at ${note.startSec}s. Fallback to instrument.`
2862
+ );
2863
+ opts?.onFallbackPlayNote?.(track, note, t0);
2864
+ return;
2865
+ }
2866
+ const key = result.key;
2836
2867
  if (session !== streamSession) return;
2837
2868
  if (key) {
2838
2869
  const delay = ctx.currentTime - t0;
@@ -2840,9 +2871,10 @@ var createSingingVoices = (ctx, destination, options = {}) => {
2840
2871
  scheduleCached(key, t0, peak, track.pan);
2841
2872
  } else {
2842
2873
  console.warn(
2843
- `[dtm] Synthesizer late skip: ${note.syllable.kana} at ${note.startSec}s (delayed by ${delay.toFixed(3)}s)`
2874
+ `[dtm] Synthesizer late skip (safety): ${note.syllable.kana} at ${note.startSec}s (delayed by ${delay.toFixed(3)}s)`
2844
2875
  );
2845
2876
  opts?.onLateSkip?.(note, delay);
2877
+ opts?.onFallbackPlayNote?.(track, note, t0);
2846
2878
  }
2847
2879
  }
2848
2880
  })();
@@ -7130,10 +7162,31 @@ var mountMmlPlayer = (target, mml, options = {}) => {
7130
7162
  if (streaming && !skipSinging) {
7131
7163
  ensureVoices().startStream(tracks, seq.getStartTime(), {
7132
7164
  isAudible: (t) => !mutedTracks.has(Number(t.id)),
7133
- onLateSkip: () => {
7165
+ onFallbackPlayNote: (track, note, t0) => {
7134
7166
  showPlayerMessage(
7135
7167
  "\u97F3\u58F0\u5408\u6210\u304C\u9593\u306B\u5408\u308F\u306A\u3044\u305F\u3081\u3001\u4E00\u90E8\u306E\u767A\u97F3\u3092\u30B9\u30AD\u30C3\u30D7\u3057\u307E\u3057\u305F"
7136
7168
  );
7169
+ const trackIdx = Number(track.id);
7170
+ if (mutedTracks.has(trackIdx)) return;
7171
+ const lt = lyricTracks.get(trackIdx);
7172
+ if (!lt) return;
7173
+ const relativeWhen = t0 - ensureCtx().currentTime;
7174
+ if (relativeWhen > 0) {
7175
+ const vol = (lt.volume ?? DEFAULT_VOCAL_VOLUME) / 100 * (trackVolume / 100);
7176
+ const e = {
7177
+ trackId: track.id ?? "",
7178
+ pitch: note.pitch,
7179
+ velocity: 100,
7180
+ volume: vol,
7181
+ when: relativeWhen,
7182
+ duration: note.durationSec,
7183
+ pan: track.pan
7184
+ };
7185
+ options.onPlayNote?.(e);
7186
+ if (useSynth) ensureSynth().playNote(e);
7187
+ }
7188
+ },
7189
+ onLateSkip: () => {
7137
7190
  }
7138
7191
  });
7139
7192
  }
@@ -8143,7 +8196,27 @@ var mountDAW = (target, options = {}) => {
8143
8196
  sequencer.start(fromStep);
8144
8197
  if (streaming && voices) {
8145
8198
  voices.startStream(streamTracks, sequencer.getStartTime(), {
8146
- isAudible: (t) => !isSolo || t.id === activeTrackId
8199
+ isAudible: (t) => !isSolo || t.id === activeTrackId,
8200
+ onFallbackPlayNote: (track, note, t0) => {
8201
+ if (isSolo && track.id !== activeTrackId) return;
8202
+ const relativeWhen = t0 - getAudioTime();
8203
+ if (relativeWhen > 0) {
8204
+ const trackState = trackStates.find(
8205
+ (t) => t.config.id === track.id
8206
+ );
8207
+ const trackVol = trackState ? trackState.volume : 80;
8208
+ const volume = trackVol / 100 * (masterVolume / 100);
8209
+ options.onPlayNote?.({
8210
+ trackId: track.id ?? "",
8211
+ pitch: note.pitch,
8212
+ velocity: 100,
8213
+ volume,
8214
+ when: relativeWhen,
8215
+ duration: note.durationSec,
8216
+ pan: track.pan
8217
+ });
8218
+ }
8219
+ }
8147
8220
  });
8148
8221
  }
8149
8222
  updateTransport();
package/dist/index.mjs CHANGED
@@ -2718,13 +2718,44 @@ var createSingingVoices = (ctx, destination, options = {}) => {
2718
2718
  if (model.renderToCache && model.scheduleCached) {
2719
2719
  const renderToCache = model.renderToCache;
2720
2720
  const scheduleCached = model.scheduleCached;
2721
+ const threshold = opts?.fallbackThresholdSec ?? 0.15;
2721
2722
  void (async () => {
2722
- const key = await renderToCache(
2723
+ let isTimeout = false;
2724
+ let timeoutId;
2725
+ const renderPromise = renderToCache(
2723
2726
  note.syllable,
2724
2727
  prevVowel,
2725
2728
  note.pitch,
2726
2729
  note.durationSec * 1e3
2730
+ ).then((key2) => {
2731
+ if (timeoutId) clearTimeout(timeoutId);
2732
+ return { key: key2, type: "success" };
2733
+ });
2734
+ const timeoutLimitTime = t0 - threshold;
2735
+ const timeoutDelayMs = (timeoutLimitTime - ctx.currentTime) * 1e3;
2736
+ const timeoutPromise = new Promise(
2737
+ (resolve) => {
2738
+ if (timeoutDelayMs <= 0) {
2739
+ isTimeout = true;
2740
+ resolve({ key: null, type: "timeout" });
2741
+ } else {
2742
+ timeoutId = setTimeout(() => {
2743
+ isTimeout = true;
2744
+ resolve({ key: null, type: "timeout" });
2745
+ }, timeoutDelayMs);
2746
+ }
2747
+ }
2727
2748
  );
2749
+ const result = await Promise.race([renderPromise, timeoutPromise]);
2750
+ if (session !== streamSession) return;
2751
+ if (result.type === "timeout") {
2752
+ console.warn(
2753
+ `[dtm] Synthesizer expected late skip: ${note.syllable.kana} at ${note.startSec}s. Fallback to instrument.`
2754
+ );
2755
+ opts?.onFallbackPlayNote?.(track, note, t0);
2756
+ return;
2757
+ }
2758
+ const key = result.key;
2728
2759
  if (session !== streamSession) return;
2729
2760
  if (key) {
2730
2761
  const delay = ctx.currentTime - t0;
@@ -2732,9 +2763,10 @@ var createSingingVoices = (ctx, destination, options = {}) => {
2732
2763
  scheduleCached(key, t0, peak, track.pan);
2733
2764
  } else {
2734
2765
  console.warn(
2735
- `[dtm] Synthesizer late skip: ${note.syllable.kana} at ${note.startSec}s (delayed by ${delay.toFixed(3)}s)`
2766
+ `[dtm] Synthesizer late skip (safety): ${note.syllable.kana} at ${note.startSec}s (delayed by ${delay.toFixed(3)}s)`
2736
2767
  );
2737
2768
  opts?.onLateSkip?.(note, delay);
2769
+ opts?.onFallbackPlayNote?.(track, note, t0);
2738
2770
  }
2739
2771
  }
2740
2772
  })();
@@ -7022,10 +7054,31 @@ var mountMmlPlayer = (target, mml, options = {}) => {
7022
7054
  if (streaming && !skipSinging) {
7023
7055
  ensureVoices().startStream(tracks, seq.getStartTime(), {
7024
7056
  isAudible: (t) => !mutedTracks.has(Number(t.id)),
7025
- onLateSkip: () => {
7057
+ onFallbackPlayNote: (track, note, t0) => {
7026
7058
  showPlayerMessage(
7027
7059
  "\u97F3\u58F0\u5408\u6210\u304C\u9593\u306B\u5408\u308F\u306A\u3044\u305F\u3081\u3001\u4E00\u90E8\u306E\u767A\u97F3\u3092\u30B9\u30AD\u30C3\u30D7\u3057\u307E\u3057\u305F"
7028
7060
  );
7061
+ const trackIdx = Number(track.id);
7062
+ if (mutedTracks.has(trackIdx)) return;
7063
+ const lt = lyricTracks.get(trackIdx);
7064
+ if (!lt) return;
7065
+ const relativeWhen = t0 - ensureCtx().currentTime;
7066
+ if (relativeWhen > 0) {
7067
+ const vol = (lt.volume ?? DEFAULT_VOCAL_VOLUME) / 100 * (trackVolume / 100);
7068
+ const e = {
7069
+ trackId: track.id ?? "",
7070
+ pitch: note.pitch,
7071
+ velocity: 100,
7072
+ volume: vol,
7073
+ when: relativeWhen,
7074
+ duration: note.durationSec,
7075
+ pan: track.pan
7076
+ };
7077
+ options.onPlayNote?.(e);
7078
+ if (useSynth) ensureSynth().playNote(e);
7079
+ }
7080
+ },
7081
+ onLateSkip: () => {
7029
7082
  }
7030
7083
  });
7031
7084
  }
@@ -8035,7 +8088,27 @@ var mountDAW = (target, options = {}) => {
8035
8088
  sequencer.start(fromStep);
8036
8089
  if (streaming && voices) {
8037
8090
  voices.startStream(streamTracks, sequencer.getStartTime(), {
8038
- isAudible: (t) => !isSolo || t.id === activeTrackId
8091
+ isAudible: (t) => !isSolo || t.id === activeTrackId,
8092
+ onFallbackPlayNote: (track, note, t0) => {
8093
+ if (isSolo && track.id !== activeTrackId) return;
8094
+ const relativeWhen = t0 - getAudioTime();
8095
+ if (relativeWhen > 0) {
8096
+ const trackState = trackStates.find(
8097
+ (t) => t.config.id === track.id
8098
+ );
8099
+ const trackVol = trackState ? trackState.volume : 80;
8100
+ const volume = trackVol / 100 * (masterVolume / 100);
8101
+ options.onPlayNote?.({
8102
+ trackId: track.id ?? "",
8103
+ pitch: note.pitch,
8104
+ velocity: 100,
8105
+ volume,
8106
+ when: relativeWhen,
8107
+ duration: note.durationSec,
8108
+ pan: track.pan
8109
+ });
8110
+ }
8111
+ }
8039
8112
  });
8040
8113
  }
8041
8114
  updateTransport();
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.28",
5
+ "version": "0.1.29",
6
6
  "description": "MMLを中間言語に用いた、モバイルファーストなDAW / ピアノロール打ち込みコンポーネント",
7
7
  "homepage": "https://onjmin.github.io/dtm",
8
8
  "repository": {