danoniplus 47.6.11 → 47.6.12
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 +124 -10
- package/package.json +1 -1
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/
|
|
7
|
+
* Revised : 2026/08/19
|
|
8
8
|
*
|
|
9
9
|
* https://github.com/cwtickle/danoniplus
|
|
10
10
|
*/
|
|
11
|
-
const g_version = `Ver 47.6.
|
|
12
|
-
const g_revisedDate = `2026/08/
|
|
11
|
+
const g_version = `Ver 47.6.12`;
|
|
12
|
+
const g_revisedDate = `2026/08/19`;
|
|
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 = {};
|
|
@@ -2457,10 +2473,11 @@ const drawTitleResultMotion = _displayName =>
|
|
|
2457
2473
|
// WebAudioAPIでAudio要素風に再生するクラス
|
|
2458
2474
|
class AudioPlayer {
|
|
2459
2475
|
constructor() {
|
|
2460
|
-
this._context =
|
|
2476
|
+
this._context = getSharedAudioContext();
|
|
2461
2477
|
this._gain = this._context.createGain();
|
|
2462
2478
|
this._gain.connect(this._context.destination);
|
|
2463
2479
|
this._startTime = 0;
|
|
2480
|
+
this._scheduledTime = 0;
|
|
2464
2481
|
this._fadeinPosition = 0;
|
|
2465
2482
|
this._eventListeners = {};
|
|
2466
2483
|
this.playbackRate = 1;
|
|
@@ -2485,17 +2502,25 @@ class AudioPlayer {
|
|
|
2485
2502
|
* - scheduleLead は安定した再生タイミングを確保するための内部マージン
|
|
2486
2503
|
*/
|
|
2487
2504
|
play(_adjustmentTime = 0) {
|
|
2505
|
+
// AudioContextの時計は1回だけ読み、以降はその値を使い回す
|
|
2506
|
+
// - currentTimeはレンダークォンタム単位でしか進まないため、複数回読むと
|
|
2507
|
+
// 予約時刻と論理開始時刻が最大1クォンタム分ずれる
|
|
2508
|
+
const ctxNow = this._context.currentTime;
|
|
2509
|
+
|
|
2488
2510
|
this._source = this._context.createBufferSource();
|
|
2489
2511
|
this._source.buffer = this._buffer;
|
|
2490
2512
|
this._source.playbackRate.value = this.playbackRate;
|
|
2491
2513
|
this._source.connect(this._gain);
|
|
2492
2514
|
|
|
2493
2515
|
// 実際の予約時刻(内部スケジューリング用のマージンを含む)
|
|
2494
|
-
const startAt =
|
|
2516
|
+
const startAt = ctxNow + g_scheduleLead + _adjustmentTime;
|
|
2495
2517
|
this._source.start(startAt, this._fadeinPosition);
|
|
2496
2518
|
|
|
2497
2519
|
// ゲーム側の論理的開始時刻(g_scheduleLead を含めない)
|
|
2498
|
-
this._startTime =
|
|
2520
|
+
this._startTime = ctxNow + _adjustmentTime;
|
|
2521
|
+
|
|
2522
|
+
// 実際に音が鳴り始めるAudioContext上の時刻(フレーム同期の基準)
|
|
2523
|
+
this._scheduledTime = startAt;
|
|
2499
2524
|
}
|
|
2500
2525
|
|
|
2501
2526
|
pause() {
|
|
@@ -2523,6 +2548,26 @@ class AudioPlayer {
|
|
|
2523
2548
|
return this._context.currentTime - this._startTime + this._fadeinPosition;
|
|
2524
2549
|
}
|
|
2525
2550
|
|
|
2551
|
+
/** AudioContextの現在時刻(秒) */
|
|
2552
|
+
get contextTime() {
|
|
2553
|
+
return this._context.currentTime;
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
/** 実際に音が鳴り始めるAudioContext上の時刻(秒) */
|
|
2557
|
+
get scheduledTime() {
|
|
2558
|
+
return this._scheduledTime;
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
/** 音が出力デバイスから実際に出るまでの遅延(秒) */
|
|
2562
|
+
get outputLatency() {
|
|
2563
|
+
return this._context.outputLatency || this._context.baseLatency || 0;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
/** AudioContextの状態(running / suspended / closed) */
|
|
2567
|
+
get contextState() {
|
|
2568
|
+
return this._context.state;
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2526
2571
|
set currentTime(_currentTime) {
|
|
2527
2572
|
this._fadeinPosition = _currentTime;
|
|
2528
2573
|
}
|
|
@@ -2587,6 +2632,47 @@ class AudioPlayer {
|
|
|
2587
2632
|
load() { }
|
|
2588
2633
|
dispatchEvent() { }
|
|
2589
2634
|
}
|
|
2635
|
+
// グローバルで1つだけ保持(遅延生成)
|
|
2636
|
+
const getSharedAudioContext = () => {
|
|
2637
|
+
if (!g_sharedAudioContext) {
|
|
2638
|
+
g_sharedAudioContext = new AudioContext();
|
|
2639
|
+
}
|
|
2640
|
+
// タブのバックグラウンド化等でsuspendedになることがあるため念のためresume
|
|
2641
|
+
if (g_sharedAudioContext.state === `suspended`) {
|
|
2642
|
+
g_sharedAudioContext.resume();
|
|
2643
|
+
}
|
|
2644
|
+
return g_sharedAudioContext;
|
|
2645
|
+
};
|
|
2646
|
+
/**
|
|
2647
|
+
* AudioContextのウォームアップ
|
|
2648
|
+
* - 生成直後・resume直後のAudioContextは出力デバイスの起動待ちのため、
|
|
2649
|
+
* しばらく currentTime が進まない(環境により数十〜数百ms)
|
|
2650
|
+
* - この状態で音源をスケジュールすると、起動に要した時間がそのまま
|
|
2651
|
+
* 音源と譜面のずれになるため、無音を1回鳴らして時計が動き出すまで待つ
|
|
2652
|
+
* @returns {Promise<void>}
|
|
2653
|
+
*/
|
|
2654
|
+
const warmUpAudioContext = async () => {
|
|
2655
|
+
const ctx = getSharedAudioContext();
|
|
2656
|
+
if (ctx.state !== `running`) {
|
|
2657
|
+
await ctx.resume().catch(() => { });
|
|
2658
|
+
}
|
|
2659
|
+
if (ctx.state !== `running`) {
|
|
2660
|
+
return; // ジェスチャー未取得等でresumeできない場合は何もしない
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
// 無音を1サンプルだけ鳴らして出力デバイスを起動させる
|
|
2664
|
+
const source = ctx.createBufferSource();
|
|
2665
|
+
source.buffer = ctx.createBuffer(1, 1, ctx.sampleRate);
|
|
2666
|
+
source.connect(ctx.destination);
|
|
2667
|
+
source.start();
|
|
2668
|
+
|
|
2669
|
+
// currentTimeが実際に進み始めるまで待機(最大500ms)
|
|
2670
|
+
const baseTime = ctx.currentTime;
|
|
2671
|
+
const limitTime = performance.now() + 500;
|
|
2672
|
+
while (ctx.currentTime === baseTime && performance.now() < limitTime) {
|
|
2673
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
2674
|
+
}
|
|
2675
|
+
};
|
|
2590
2676
|
|
|
2591
2677
|
/**
|
|
2592
2678
|
* クリップボードコピー関数
|
|
@@ -11195,6 +11281,7 @@ const setAudio = async (_url) => {
|
|
|
11195
11281
|
g_currentPage = `loadingIos`;
|
|
11196
11282
|
lblLoading.textContent = `Click to Start!`;
|
|
11197
11283
|
divRoot.appendChild(makePlayButton(evt => {
|
|
11284
|
+
getSharedAudioContext().resume();
|
|
11198
11285
|
g_currentPage = `loading`;
|
|
11199
11286
|
resetKeyControl();
|
|
11200
11287
|
divRoot.removeChild(evt.target);
|
|
@@ -11413,6 +11500,9 @@ const loadingScoreInit = async () => {
|
|
|
11413
11500
|
// ユーザカスタムイベント
|
|
11414
11501
|
safeExecuteCustomHooks(`g_customJsObj.loading`, g_customJsObj.loading);
|
|
11415
11502
|
|
|
11503
|
+
// 初回プレイ時に出力デバイスの起動待ちでずれるのを防ぐため、
|
|
11504
|
+
// メイン画面へ移行する前にAudioContextを起動させておく
|
|
11505
|
+
await warmUpAudioContext();
|
|
11416
11506
|
mainInit();
|
|
11417
11507
|
};
|
|
11418
11508
|
|
|
@@ -13756,6 +13846,7 @@ const mainInit = () => {
|
|
|
13756
13846
|
let thisTime;
|
|
13757
13847
|
let buffTime;
|
|
13758
13848
|
let musicStartTime;
|
|
13849
|
+
let musicStartCtxTime;
|
|
13759
13850
|
let musicStartFlg = false;
|
|
13760
13851
|
|
|
13761
13852
|
g_inputKeyBuffer = {};
|
|
@@ -14967,14 +15058,32 @@ const mainInit = () => {
|
|
|
14967
15058
|
}
|
|
14968
15059
|
|
|
14969
15060
|
// 60fpsから遅延するため、その差分を取って次回のタイミングで遅れをリカバリする
|
|
15061
|
+
// - WebAudioAPI使用時は音源クロック(AudioContext.currentTime)を基準とする
|
|
15062
|
+
// performance.now()とAudioContextの時計は独立して進むため、後者を基準にしないと
|
|
15063
|
+
// 再生開始時のオフセットずれやsuspend/resumeによるずれを吸収できない
|
|
14970
15064
|
thisTime = performance.now();
|
|
14971
15065
|
buffTime = 0;
|
|
14972
|
-
|
|
15066
|
+
let holdFrame = false;
|
|
15067
|
+
|
|
15068
|
+
if (g_audio instanceof AudioPlayer && g_audioClockSync && musicStartCtxTime !== undefined) {
|
|
15069
|
+
if (g_audio.contextState === `running`) {
|
|
15070
|
+
buffTime = (g_audio.contextTime - musicStartCtxTime) * 1000
|
|
15071
|
+
- (currentFrame - musicStartFrame) * 1000 / g_fps;
|
|
15072
|
+
} else {
|
|
15073
|
+
// AudioContext停止中は音源も止まっているため、フレーム進行も止めて復帰を待つ
|
|
15074
|
+
getSharedAudioContext(); // suspended時はresumeを試行
|
|
15075
|
+
holdFrame = true;
|
|
15076
|
+
}
|
|
15077
|
+
} else if (g_audio instanceof AudioPlayer || currentFrame >= musicStartFrame) {
|
|
14973
15078
|
buffTime = (thisTime - musicStartTime - (currentFrame - musicStartFrame) * 1000 / g_fps);
|
|
14974
15079
|
}
|
|
14975
|
-
|
|
14976
|
-
|
|
14977
|
-
|
|
15080
|
+
|
|
15081
|
+
if (!holdFrame) {
|
|
15082
|
+
g_scoreObj.frameNum++;
|
|
15083
|
+
g_scoreObj.baseFrame++;
|
|
15084
|
+
}
|
|
15085
|
+
g_timeoutEvtId = setTimeout(flowTimeline, holdFrame ? g_maxFrameWait :
|
|
15086
|
+
Math.min(Math.max(1000 / g_fps - buffTime, 0), g_maxFrameWait));
|
|
14978
15087
|
}
|
|
14979
15088
|
};
|
|
14980
15089
|
safeExecuteCustomHooks(`g_skinJsObj.main`, g_skinJsObj.main);
|
|
@@ -14987,6 +15096,11 @@ const mainInit = () => {
|
|
|
14987
15096
|
const musicStartAdjustment = (g_headerObj.blankFrame - g_stateObj.decimalAdjustment + 1) / g_fps;
|
|
14988
15097
|
musicStartTime = performance.now() + (musicStartAdjustment + g_scheduleLead) * 1000;
|
|
14989
15098
|
g_audio.play(musicStartAdjustment);
|
|
15099
|
+
|
|
15100
|
+
// 音源クロック基準の開始時刻(フレーム進行の基準)
|
|
15101
|
+
// - g_audioLatencyCompensationが有効な場合は出力遅延分だけ表示を後ろへずらす
|
|
15102
|
+
musicStartCtxTime = g_audio.scheduledTime
|
|
15103
|
+
+ (g_audioLatencyCompensation ? g_audio.outputLatency : 0);
|
|
14990
15104
|
}
|
|
14991
15105
|
|
|
14992
15106
|
g_timeoutEvtId = setTimeout(flowTimeline, 1000 / g_fps);
|