danoniplus 48.5.7 → 48.5.9

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/js/danoni_main.js CHANGED
@@ -4,12 +4,12 @@
4
4
  *
5
5
  * Source by tickle
6
6
  * Created : 2018/10/08
7
- * Revised : 2026/08/12
7
+ * Revised : 2026/09/13
8
8
  *
9
9
  * https://github.com/cwtickle/danoniplus
10
10
  */
11
- const g_version = `Ver 48.5.7`;
12
- const g_revisedDate = `2026/08/12`;
11
+ const g_version = `Ver 48.5.9`;
12
+ const g_revisedDate = `2026/09/13`;
13
13
 
14
14
  // カスタム用バージョン (danoni_custom.js 等で指定可)
15
15
  let g_localVersion = ``;
@@ -135,6 +135,19 @@ let g_fps = 60;
135
135
  // プレイ画面再生時の内部スケジューリング用のマージン時間(100ms)
136
136
  let g_scheduleLead = 0.1;
137
137
 
138
+ // フレーム進行を音源クロック(AudioContext.currentTime)基準で補正するか
139
+ // - false の場合は従来通り performance.now() 基準で動作
140
+ let g_audioClockSync = true;
141
+
142
+ // 出力遅延(AudioContext.outputLatency)をタイミング補正に含めるか
143
+ // - true にすると出力デバイス(有線/Bluetooth等)によらず同じAdjustmentが使えるが、
144
+ // 既存のAdjustment設定値と互換性がなくなるため既定は false
145
+ let g_audioLatencyCompensation = false;
146
+
147
+ // 次フレームまでの待機時間の上限(ms)
148
+ // - 音源クロックが一時的に停止した際に待ち続けないようにするための保険
149
+ let g_maxFrameWait = 50;
150
+
138
151
  // 譜面データの&区切りを有効にするか
139
152
  let g_enableAmpersandSplit = true;
140
153
 
@@ -177,6 +190,9 @@ let g_maxScore = 1000000;
177
190
  let g_gameOverFlg = false;
178
191
  let g_finishFlg = true;
179
192
 
193
+ // 音源のAudioContext管理
194
+ let g_sharedAudioContext = null;
195
+
180
196
  /** 共通オブジェクト */
181
197
  const g_loadObj = {};
182
198
  const g_rootObj = {};
@@ -2459,10 +2475,11 @@ const drawTitleResultMotion = _displayName =>
2459
2475
  // WebAudioAPIでAudio要素風に再生するクラス
2460
2476
  class AudioPlayer {
2461
2477
  constructor() {
2462
- this._context = new AudioContext();
2478
+ this._context = getSharedAudioContext();
2463
2479
  this._gain = this._context.createGain();
2464
2480
  this._gain.connect(this._context.destination);
2465
2481
  this._startTime = 0;
2482
+ this._scheduledTime = 0;
2466
2483
  this._fadeinPosition = 0;
2467
2484
  this._eventListeners = {};
2468
2485
  this.playbackRate = 1;
@@ -2487,17 +2504,25 @@ class AudioPlayer {
2487
2504
  * - scheduleLead は安定した再生タイミングを確保するための内部マージン
2488
2505
  */
2489
2506
  play(_adjustmentTime = 0) {
2507
+ // AudioContextの時計は1回だけ読み、以降はその値を使い回す
2508
+ // - currentTimeはレンダークォンタム単位でしか進まないため、複数回読むと
2509
+ // 予約時刻と論理開始時刻が最大1クォンタム分ずれる
2510
+ const ctxNow = this._context.currentTime;
2511
+
2490
2512
  this._source = this._context.createBufferSource();
2491
2513
  this._source.buffer = this._buffer;
2492
2514
  this._source.playbackRate.value = this.playbackRate;
2493
2515
  this._source.connect(this._gain);
2494
2516
 
2495
2517
  // 実際の予約時刻(内部スケジューリング用のマージンを含む)
2496
- const startAt = this._context.currentTime + g_scheduleLead + _adjustmentTime;
2518
+ const startAt = ctxNow + g_scheduleLead + _adjustmentTime;
2497
2519
  this._source.start(startAt, this._fadeinPosition);
2498
2520
 
2499
2521
  // ゲーム側の論理的開始時刻(g_scheduleLead を含めない)
2500
- this._startTime = this._context.currentTime + _adjustmentTime;
2522
+ this._startTime = ctxNow + _adjustmentTime;
2523
+
2524
+ // 実際に音が鳴り始めるAudioContext上の時刻(フレーム同期の基準)
2525
+ this._scheduledTime = startAt;
2501
2526
  }
2502
2527
 
2503
2528
  pause() {
@@ -2525,6 +2550,26 @@ class AudioPlayer {
2525
2550
  return this._context.currentTime - this._startTime + this._fadeinPosition;
2526
2551
  }
2527
2552
 
2553
+ /** AudioContextの現在時刻(秒) */
2554
+ get contextTime() {
2555
+ return this._context.currentTime;
2556
+ }
2557
+
2558
+ /** 実際に音が鳴り始めるAudioContext上の時刻(秒) */
2559
+ get scheduledTime() {
2560
+ return this._scheduledTime;
2561
+ }
2562
+
2563
+ /** 音が出力デバイスから実際に出るまでの遅延(秒) */
2564
+ get outputLatency() {
2565
+ return this._context.outputLatency || this._context.baseLatency || 0;
2566
+ }
2567
+
2568
+ /** AudioContextの状態(running / suspended / closed) */
2569
+ get contextState() {
2570
+ return this._context.state;
2571
+ }
2572
+
2528
2573
  set currentTime(_currentTime) {
2529
2574
  this._fadeinPosition = _currentTime;
2530
2575
  }
@@ -2589,6 +2634,47 @@ class AudioPlayer {
2589
2634
  load() { }
2590
2635
  dispatchEvent() { }
2591
2636
  }
2637
+ // グローバルで1つだけ保持(遅延生成)
2638
+ const getSharedAudioContext = () => {
2639
+ if (!g_sharedAudioContext) {
2640
+ g_sharedAudioContext = new AudioContext();
2641
+ }
2642
+ // タブのバックグラウンド化等でsuspendedになることがあるため念のためresume
2643
+ if (g_sharedAudioContext.state === `suspended`) {
2644
+ g_sharedAudioContext.resume();
2645
+ }
2646
+ return g_sharedAudioContext;
2647
+ };
2648
+ /**
2649
+ * AudioContextのウォームアップ
2650
+ * - 生成直後・resume直後のAudioContextは出力デバイスの起動待ちのため、
2651
+ * しばらく currentTime が進まない(環境により数十〜数百ms)
2652
+ * - この状態で音源をスケジュールすると、起動に要した時間がそのまま
2653
+ * 音源と譜面のずれになるため、無音を1回鳴らして時計が動き出すまで待つ
2654
+ * @returns {Promise<void>}
2655
+ */
2656
+ const warmUpAudioContext = async () => {
2657
+ const ctx = getSharedAudioContext();
2658
+ if (ctx.state !== `running`) {
2659
+ await ctx.resume().catch(() => { });
2660
+ }
2661
+ if (ctx.state !== `running`) {
2662
+ return; // ジェスチャー未取得等でresumeできない場合は何もしない
2663
+ }
2664
+
2665
+ // 無音を1サンプルだけ鳴らして出力デバイスを起動させる
2666
+ const source = ctx.createBufferSource();
2667
+ source.buffer = ctx.createBuffer(1, 1, ctx.sampleRate);
2668
+ source.connect(ctx.destination);
2669
+ source.start();
2670
+
2671
+ // currentTimeが実際に進み始めるまで待機(最大500ms)
2672
+ const baseTime = ctx.currentTime;
2673
+ const limitTime = performance.now() + 500;
2674
+ while (ctx.currentTime === baseTime && performance.now() < limitTime) {
2675
+ await new Promise(resolve => setTimeout(resolve, 10));
2676
+ }
2677
+ };
2592
2678
 
2593
2679
  /**
2594
2680
  * クリップボードコピー関数
@@ -6100,7 +6186,7 @@ const pauseBGM = () => {
6100
6186
  g_audioForMS.load();
6101
6187
  }
6102
6188
  }
6103
- [`bgmLooped`, `bgmFadeIn`, `bgmFadeOut`].forEach(id => {
6189
+ [`bgmLooped`, `bgmFadeIn`, `bgmFadeOut`, `bgmRestart`].forEach(id => {
6104
6190
  if (g_stateObj[id]) {
6105
6191
  clearTimeout(g_stateObj[id]);
6106
6192
  g_stateObj[id] = null;
@@ -6225,7 +6311,8 @@ const playBGM = async (_num, _currentLoopNum = g_settings.musicLoopNum) => {
6225
6311
  g_audioForMS.currentTime = musicStart;
6226
6312
 
6227
6313
  if (isTitle()) {
6228
- setTimeout(() => {
6314
+ g_stateObj.bgmRestart = setTimeout(() => {
6315
+ g_stateObj.bgmRestart = null;
6229
6316
  fadeIn();
6230
6317
  if (encodeFlg) repeatBGM();
6231
6318
  }, FADE_DELAY_MS);
@@ -6534,8 +6621,8 @@ const makeWarningWindow = (_text = ``, { resetFlg = false, backBtnUse = false }
6534
6621
  * @param {string} [object._textColor='#000066']
6535
6622
  * @param {string} [object._pointerEvents=C_DIS_NONE]
6536
6623
  */
6537
- const makeInfoWindow = (_text, _animationName = ``, { _backColor = `#ccccff`, _textColor = `#000066`, _pointerEvents = C_DIS_NONE } = {}) => {
6538
- const lblWarning = setWindowStyle(`<p>${_text}</p>`, _backColor, _textColor, C_ALIGN_CENTER);
6624
+ const makeInfoWindow = (_text, _animationName = ``, { _backColor = `#ccccff`, _textColor = `#000066`, _pointerEvents = C_DIS_NONE, _x = g_btnX(), _y = 0 } = {}) => {
6625
+ const lblWarning = setWindowStyle(`<p>${_text}</p>`, _backColor, _textColor, C_ALIGN_CENTER, { _x, _y });
6539
6626
  lblWarning.style.pointerEvents = _pointerEvents;
6540
6627
 
6541
6628
  if (_animationName !== ``) {
@@ -10694,7 +10781,7 @@ const keyConfigInit = (_kcType = g_kcType, _initFlg = false) => {
10694
10781
  kcMsg2.style.fontSize = wUnit(getFontSize2(kcMsg2.textContent, g_btnWidth()));
10695
10782
  if (_reloadFlg) {
10696
10783
  colorPickSprite.style.display = isDefault ? C_DIS_NONE : C_DIS_INHERIT;
10697
- g_keycons.colorCursorNum = g_keycons.colorCursorNum % Math.ceil(g_headerObj.setColor.length / g_limitObj.kcColorPickerNum);
10784
+ g_keycons.colorCursorNum = g_keycons.colorCursorNum % Math.ceil(g_dfColorObj.setColorInit.length / g_limitObj.kcColorPickerNum);
10698
10785
  changeColorPickers();
10699
10786
  }
10700
10787
  };
@@ -10722,14 +10809,14 @@ const keyConfigInit = (_kcType = g_kcType, _initFlg = false) => {
10722
10809
 
10723
10810
  // ColorPickerの切替
10724
10811
  createCss2Button(`lnkColorR`, `[${g_keycons.colorCursorNum + 1} /`, () => {
10725
- g_keycons.colorCursorNum = (g_keycons.colorCursorNum + 1) % Math.ceil(g_headerObj.setColor.length / g_limitObj.kcColorPickerNum);
10812
+ g_keycons.colorCursorNum = (g_keycons.colorCursorNum + 1) % Math.ceil(g_dfColorObj.setColorInit.length / g_limitObj.kcColorPickerNum);
10726
10813
  changeColorPickers();
10727
10814
  }, g_lblPosObj.lnkColorR, g_cssObj.button_Start),
10728
10815
 
10729
10816
  // 矢印の配色をフリーズアローへ反映
10730
10817
  createCss2Button(`lnkColorCopy`, `↓]`, () => {
10731
10818
  if (window.confirm(g_msgObj.colorCopyConfirm)) {
10732
- for (let j = 0; j < g_headerObj.setColor.length; j++) {
10819
+ for (let j = 0; j < g_dfColorObj.setColorInit.length; j++) {
10733
10820
  g_headerObj.frzColor[j] = g_headerObj[`frzColor${g_colorType}`][j] =
10734
10821
  fillArray(g_headerObj[`frzColor${g_colorType}`][j].length, g_headerObj[`setColor${g_colorType}`][j]);
10735
10822
  }
@@ -10900,7 +10987,10 @@ const keyConfigInit = (_kcType = g_kcType, _initFlg = false) => {
10900
10987
  g_currentk = 0;
10901
10988
  g_prevKey = 0;
10902
10989
  }, {
10903
- ...g_lblPosObj.btnKcBack, resetFunc: () => g_moveSettingWindow(false),
10990
+ ...g_lblPosObj.btnKcBack, resetFunc: () => {
10991
+ keyconfigKeyboardPreview.dispose();
10992
+ g_moveSettingWindow(false);
10993
+ },
10904
10994
  }, g_cssObj.button_Back),
10905
10995
 
10906
10996
  createDivCss2Label(`lblPattern`, `${g_lblNameObj.KeyPattern}: ${g_keyObj.currentPtn === -1 ?
@@ -11882,6 +11972,7 @@ const setAudio = async (_url) => {
11882
11972
  g_currentPage = `loadingIos`;
11883
11973
  lblLoading.textContent = `Click to Start!`;
11884
11974
  divRoot.appendChild(makePlayButton(evt => {
11975
+ getSharedAudioContext().resume();
11885
11976
  g_currentPage = `loading`;
11886
11977
  resetKeyControl();
11887
11978
  divRoot.removeChild(evt.target);
@@ -12100,6 +12191,9 @@ const loadingScoreInit = async () => {
12100
12191
  // ユーザカスタムイベント
12101
12192
  safeExecuteCustomHooks(`g_customJsObj.loading`, g_customJsObj.loading);
12102
12193
 
12194
+ // 初回プレイ時に出力デバイスの起動待ちでずれるのを防ぐため、
12195
+ // メイン画面へ移行する前にAudioContextを起動させておく
12196
+ await warmUpAudioContext();
12103
12197
  mainInit();
12104
12198
  };
12105
12199
 
@@ -14448,6 +14542,7 @@ const mainInit = () => {
14448
14542
  let thisTime;
14449
14543
  let buffTime;
14450
14544
  let musicStartTime;
14545
+ let musicStartCtxTime;
14451
14546
  let musicStartFlg = false;
14452
14547
 
14453
14548
  g_inputKeyBuffer = {};
@@ -15682,14 +15777,32 @@ const mainInit = () => {
15682
15777
  }
15683
15778
 
15684
15779
  // 60fpsから遅延するため、その差分を取って次回のタイミングで遅れをリカバリする
15780
+ // - WebAudioAPI使用時は音源クロック(AudioContext.currentTime)を基準とする
15781
+ // performance.now()とAudioContextの時計は独立して進むため、後者を基準にしないと
15782
+ // 再生開始時のオフセットずれやsuspend/resumeによるずれを吸収できない
15685
15783
  thisTime = performance.now();
15686
15784
  buffTime = 0;
15687
- if (g_audio instanceof AudioPlayer || currentFrame >= musicStartFrame) {
15785
+ let holdFrame = false;
15786
+
15787
+ if (g_audio instanceof AudioPlayer && g_audioClockSync && musicStartCtxTime !== undefined) {
15788
+ if (g_audio.contextState === `running`) {
15789
+ buffTime = (g_audio.contextTime - musicStartCtxTime) * 1000
15790
+ - (currentFrame - musicStartFrame) * 1000 / g_fps;
15791
+ } else {
15792
+ // AudioContext停止中は音源も止まっているため、フレーム進行も止めて復帰を待つ
15793
+ getSharedAudioContext(); // suspended時はresumeを試行
15794
+ holdFrame = true;
15795
+ }
15796
+ } else if (g_audio instanceof AudioPlayer || currentFrame >= musicStartFrame) {
15688
15797
  buffTime = (thisTime - musicStartTime - (currentFrame - musicStartFrame) * 1000 / g_fps);
15689
15798
  }
15690
- g_scoreObj.frameNum++;
15691
- g_scoreObj.baseFrame++;
15692
- g_timeoutEvtId = setTimeout(flowTimeline, 1000 / g_fps - buffTime);
15799
+
15800
+ if (!holdFrame) {
15801
+ g_scoreObj.frameNum++;
15802
+ g_scoreObj.baseFrame++;
15803
+ }
15804
+ g_timeoutEvtId = setTimeout(flowTimeline, holdFrame ? g_maxFrameWait :
15805
+ Math.min(Math.max(1000 / g_fps - buffTime, 0), g_maxFrameWait));
15693
15806
  }
15694
15807
  };
15695
15808
  safeExecuteCustomHooks(`g_skinJsObj.main`, g_skinJsObj.main);
@@ -15702,6 +15815,11 @@ const mainInit = () => {
15702
15815
  const musicStartAdjustment = (g_headerObj.blankFrame - g_stateObj.decimalAdjustment + 1) / g_fps;
15703
15816
  musicStartTime = performance.now() + (musicStartAdjustment + g_scheduleLead) * 1000;
15704
15817
  g_audio.play(musicStartAdjustment);
15818
+
15819
+ // 音源クロック基準の開始時刻(フレーム進行の基準)
15820
+ // - g_audioLatencyCompensationが有効な場合は出力遅延分だけ表示を後ろへずらす
15821
+ musicStartCtxTime = g_audio.scheduledTime
15822
+ + (g_audioLatencyCompensation ? g_audio.outputLatency : 0);
15705
15823
  }
15706
15824
 
15707
15825
  g_timeoutEvtId = setTimeout(flowTimeline, 1000 / g_fps);
@@ -17331,15 +17449,20 @@ const resultInit = () => {
17331
17449
  } else {
17332
17450
  // Canvas の内容を PNG 画像として取得
17333
17451
  canvas.toBlob(async blob => {
17334
- await navigator.clipboard.write([
17335
- new ClipboardItem({
17336
- 'image/png': blob
17337
- })
17338
- ]);
17452
+ try {
17453
+ if (blob === null) {
17454
+ throw new Error(`Failed to create result image blob.`);
17455
+ }
17456
+ await navigator.clipboard.write([
17457
+ new ClipboardItem({ 'image/png': blob })
17458
+ ]);
17459
+ tmpDiv.removeChild(canvas);
17460
+ divRoot.removeChild(tmpDiv);
17461
+ makeInfoWindow(_msg, `leftToRightFade`);
17462
+ } catch {
17463
+ viewResultImage();
17464
+ }
17339
17465
  });
17340
- tmpDiv.removeChild(canvas);
17341
- divRoot.removeChild(tmpDiv);
17342
- makeInfoWindow(_msg, `leftToRightFade`);
17343
17466
  }
17344
17467
 
17345
17468
  } catch (err) {
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Source by tickle
7
7
  * Created : 2019/11/19
8
- * Revised : 2026/08/01 (v48.5.6)
8
+ * Revised : 2026/09/13 (v48.5.9)
9
9
  *
10
10
  * https://github.com/cwtickle/danoniplus
11
11
  */
@@ -1215,6 +1215,7 @@ const g_stateObj = {
1215
1215
  bgmLooped: null,
1216
1216
  bgmFadeIn: null,
1217
1217
  bgmFadeOut: null,
1218
+ bgmRestart: null,
1218
1219
  bgmTimeupdateEvtId: null,
1219
1220
  bgmMuteFlg: false,
1220
1221
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "danoniplus",
3
- "version": "48.5.7",
3
+ "version": "48.5.9",
4
4
  "description": "Dancing☆Onigiri (CW Edition) - Web-based Rhythm Game",
5
5
  "main": "./js/danoni_main.js",
6
6
  "jsdelivr": "./js/danoni_main.js",