danoniplus 44.5.22 → 44.5.24
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 +145 -25
- package/js/lib/danoni_constants.js +2 -1
- 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/
|
|
7
|
+
* Revised : 2026/09/13
|
|
8
8
|
*
|
|
9
9
|
* https://github.com/cwtickle/danoniplus
|
|
10
10
|
*/
|
|
11
|
-
const g_version = `Ver 44.5.
|
|
12
|
-
const g_revisedDate = `2026/
|
|
11
|
+
const g_version = `Ver 44.5.24`;
|
|
12
|
+
const g_revisedDate = `2026/09/13`;
|
|
13
13
|
|
|
14
14
|
// カスタム用バージョン (danoni_custom.js 等で指定可)
|
|
15
15
|
let g_localVersion = ``;
|
|
@@ -134,6 +134,19 @@ let g_fps = 60;
|
|
|
134
134
|
// プレイ画面再生時の内部スケジューリング用のマージン時間(100ms)
|
|
135
135
|
let g_scheduleLead = 0.1;
|
|
136
136
|
|
|
137
|
+
// フレーム進行を音源クロック(AudioContext.currentTime)基準で補正するか
|
|
138
|
+
// - false の場合は従来通り performance.now() 基準で動作
|
|
139
|
+
let g_audioClockSync = true;
|
|
140
|
+
|
|
141
|
+
// 出力遅延(AudioContext.outputLatency)をタイミング補正に含めるか
|
|
142
|
+
// - true にすると出力デバイス(有線/Bluetooth等)によらず同じAdjustmentが使えるが、
|
|
143
|
+
// 既存のAdjustment設定値と互換性がなくなるため既定は false
|
|
144
|
+
let g_audioLatencyCompensation = false;
|
|
145
|
+
|
|
146
|
+
// 次フレームまでの待機時間の上限(ms)
|
|
147
|
+
// - 音源クロックが一時的に停止した際に待ち続けないようにするための保険
|
|
148
|
+
let g_maxFrameWait = 50;
|
|
149
|
+
|
|
137
150
|
// 譜面データの&区切りを有効にするか
|
|
138
151
|
let g_enableAmpersandSplit = true;
|
|
139
152
|
|
|
@@ -173,6 +186,9 @@ let g_maxScore = 1000000;
|
|
|
173
186
|
let g_gameOverFlg = false;
|
|
174
187
|
let g_finishFlg = true;
|
|
175
188
|
|
|
189
|
+
// 音源のAudioContext管理
|
|
190
|
+
let g_sharedAudioContext = null;
|
|
191
|
+
|
|
176
192
|
/** 共通オブジェクト */
|
|
177
193
|
const g_loadObj = {};
|
|
178
194
|
const g_rootObj = {};
|
|
@@ -2382,10 +2398,11 @@ const drawTitleResultMotion = _displayName =>
|
|
|
2382
2398
|
// WebAudioAPIでAudio要素風に再生するクラス
|
|
2383
2399
|
class AudioPlayer {
|
|
2384
2400
|
constructor() {
|
|
2385
|
-
this._context =
|
|
2401
|
+
this._context = getSharedAudioContext();
|
|
2386
2402
|
this._gain = this._context.createGain();
|
|
2387
2403
|
this._gain.connect(this._context.destination);
|
|
2388
2404
|
this._startTime = 0;
|
|
2405
|
+
this._scheduledTime = 0;
|
|
2389
2406
|
this._fadeinPosition = 0;
|
|
2390
2407
|
this._eventListeners = {};
|
|
2391
2408
|
this.playbackRate = 1;
|
|
@@ -2410,17 +2427,25 @@ class AudioPlayer {
|
|
|
2410
2427
|
* - scheduleLead は安定した再生タイミングを確保するための内部マージン
|
|
2411
2428
|
*/
|
|
2412
2429
|
play(_adjustmentTime = 0) {
|
|
2430
|
+
// AudioContextの時計は1回だけ読み、以降はその値を使い回す
|
|
2431
|
+
// - currentTimeはレンダークォンタム単位でしか進まないため、複数回読むと
|
|
2432
|
+
// 予約時刻と論理開始時刻が最大1クォンタム分ずれる
|
|
2433
|
+
const ctxNow = this._context.currentTime;
|
|
2434
|
+
|
|
2413
2435
|
this._source = this._context.createBufferSource();
|
|
2414
2436
|
this._source.buffer = this._buffer;
|
|
2415
2437
|
this._source.playbackRate.value = this.playbackRate;
|
|
2416
2438
|
this._source.connect(this._gain);
|
|
2417
2439
|
|
|
2418
2440
|
// 実際の予約時刻(内部スケジューリング用のマージンを含む)
|
|
2419
|
-
const startAt =
|
|
2441
|
+
const startAt = ctxNow + g_scheduleLead + _adjustmentTime;
|
|
2420
2442
|
this._source.start(startAt, this._fadeinPosition);
|
|
2421
2443
|
|
|
2422
2444
|
// ゲーム側の論理的開始時刻(g_scheduleLead を含めない)
|
|
2423
|
-
this._startTime =
|
|
2445
|
+
this._startTime = ctxNow + _adjustmentTime;
|
|
2446
|
+
|
|
2447
|
+
// 実際に音が鳴り始めるAudioContext上の時刻(フレーム同期の基準)
|
|
2448
|
+
this._scheduledTime = startAt;
|
|
2424
2449
|
}
|
|
2425
2450
|
|
|
2426
2451
|
pause() {
|
|
@@ -2448,6 +2473,26 @@ class AudioPlayer {
|
|
|
2448
2473
|
return this._context.currentTime - this._startTime + this._fadeinPosition;
|
|
2449
2474
|
}
|
|
2450
2475
|
|
|
2476
|
+
/** AudioContextの現在時刻(秒) */
|
|
2477
|
+
get contextTime() {
|
|
2478
|
+
return this._context.currentTime;
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
/** 実際に音が鳴り始めるAudioContext上の時刻(秒) */
|
|
2482
|
+
get scheduledTime() {
|
|
2483
|
+
return this._scheduledTime;
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
/** 音が出力デバイスから実際に出るまでの遅延(秒) */
|
|
2487
|
+
get outputLatency() {
|
|
2488
|
+
return this._context.outputLatency || this._context.baseLatency || 0;
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
/** AudioContextの状態(running / suspended / closed) */
|
|
2492
|
+
get contextState() {
|
|
2493
|
+
return this._context.state;
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2451
2496
|
set currentTime(_currentTime) {
|
|
2452
2497
|
this._fadeinPosition = _currentTime;
|
|
2453
2498
|
}
|
|
@@ -2512,6 +2557,47 @@ class AudioPlayer {
|
|
|
2512
2557
|
load() { }
|
|
2513
2558
|
dispatchEvent() { }
|
|
2514
2559
|
}
|
|
2560
|
+
// グローバルで1つだけ保持(遅延生成)
|
|
2561
|
+
const getSharedAudioContext = () => {
|
|
2562
|
+
if (!g_sharedAudioContext) {
|
|
2563
|
+
g_sharedAudioContext = new AudioContext();
|
|
2564
|
+
}
|
|
2565
|
+
// タブのバックグラウンド化等でsuspendedになることがあるため念のためresume
|
|
2566
|
+
if (g_sharedAudioContext.state === `suspended`) {
|
|
2567
|
+
g_sharedAudioContext.resume();
|
|
2568
|
+
}
|
|
2569
|
+
return g_sharedAudioContext;
|
|
2570
|
+
};
|
|
2571
|
+
/**
|
|
2572
|
+
* AudioContextのウォームアップ
|
|
2573
|
+
* - 生成直後・resume直後のAudioContextは出力デバイスの起動待ちのため、
|
|
2574
|
+
* しばらく currentTime が進まない(環境により数十〜数百ms)
|
|
2575
|
+
* - この状態で音源をスケジュールすると、起動に要した時間がそのまま
|
|
2576
|
+
* 音源と譜面のずれになるため、無音を1回鳴らして時計が動き出すまで待つ
|
|
2577
|
+
* @returns {Promise<void>}
|
|
2578
|
+
*/
|
|
2579
|
+
const warmUpAudioContext = async () => {
|
|
2580
|
+
const ctx = getSharedAudioContext();
|
|
2581
|
+
if (ctx.state !== `running`) {
|
|
2582
|
+
await ctx.resume().catch(() => { });
|
|
2583
|
+
}
|
|
2584
|
+
if (ctx.state !== `running`) {
|
|
2585
|
+
return; // ジェスチャー未取得等でresumeできない場合は何もしない
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
// 無音を1サンプルだけ鳴らして出力デバイスを起動させる
|
|
2589
|
+
const source = ctx.createBufferSource();
|
|
2590
|
+
source.buffer = ctx.createBuffer(1, 1, ctx.sampleRate);
|
|
2591
|
+
source.connect(ctx.destination);
|
|
2592
|
+
source.start();
|
|
2593
|
+
|
|
2594
|
+
// currentTimeが実際に進み始めるまで待機(最大500ms)
|
|
2595
|
+
const baseTime = ctx.currentTime;
|
|
2596
|
+
const limitTime = performance.now() + 500;
|
|
2597
|
+
while (ctx.currentTime === baseTime && performance.now() < limitTime) {
|
|
2598
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
2599
|
+
}
|
|
2600
|
+
};
|
|
2515
2601
|
|
|
2516
2602
|
/**
|
|
2517
2603
|
* クリップボードコピー関数
|
|
@@ -5645,7 +5731,7 @@ const pauseBGM = () => {
|
|
|
5645
5731
|
g_audioForMS.load();
|
|
5646
5732
|
}
|
|
5647
5733
|
}
|
|
5648
|
-
[`bgmLooped`, `bgmFadeIn`, `bgmFadeOut`].forEach(id => {
|
|
5734
|
+
[`bgmLooped`, `bgmFadeIn`, `bgmFadeOut`, `bgmRestart`].forEach(id => {
|
|
5649
5735
|
if (g_stateObj[id]) {
|
|
5650
5736
|
clearTimeout(g_stateObj[id]);
|
|
5651
5737
|
g_stateObj[id] = null;
|
|
@@ -5770,7 +5856,8 @@ const playBGM = async (_num, _currentLoopNum = g_settings.musicLoopNum) => {
|
|
|
5770
5856
|
g_audioForMS.currentTime = musicStart;
|
|
5771
5857
|
|
|
5772
5858
|
if (isTitle()) {
|
|
5773
|
-
setTimeout(() => {
|
|
5859
|
+
g_stateObj.bgmRestart = setTimeout(() => {
|
|
5860
|
+
g_stateObj.bgmRestart = null;
|
|
5774
5861
|
fadeIn();
|
|
5775
5862
|
if (encodeFlg) repeatBGM();
|
|
5776
5863
|
}, FADE_DELAY_MS);
|
|
@@ -6052,8 +6139,8 @@ const makeWarningWindow = (_text = ``, { resetFlg = false, backBtnUse = false }
|
|
|
6052
6139
|
* @param {string} [object._textColor='#000066']
|
|
6053
6140
|
* @param {string} [object._pointerEvents=C_DIS_NONE]
|
|
6054
6141
|
*/
|
|
6055
|
-
const makeInfoWindow = (_text, _animationName = ``, { _backColor = `#ccccff`, _textColor = `#000066`, _pointerEvents = C_DIS_NONE } = {}) => {
|
|
6056
|
-
const lblWarning = setWindowStyle(`<p>${_text}</p>`, _backColor, _textColor, C_ALIGN_CENTER);
|
|
6142
|
+
const makeInfoWindow = (_text, _animationName = ``, { _backColor = `#ccccff`, _textColor = `#000066`, _pointerEvents = C_DIS_NONE, _x = g_btnX(), _y = 0 } = {}) => {
|
|
6143
|
+
const lblWarning = setWindowStyle(`<p>${_text}</p>`, _backColor, _textColor, C_ALIGN_CENTER, { _x, _y });
|
|
6057
6144
|
lblWarning.style.pointerEvents = _pointerEvents;
|
|
6058
6145
|
|
|
6059
6146
|
if (_animationName !== ``) {
|
|
@@ -9299,7 +9386,7 @@ const keyConfigInit = (_kcType = g_kcType) => {
|
|
|
9299
9386
|
lnkColorType.textContent = `${getStgDetailName(g_colorType)}${g_localStorage.colorType === g_colorType ? ' *' : ''}`;
|
|
9300
9387
|
if (_reloadFlg) {
|
|
9301
9388
|
colorPickSprite.style.display = ([`Default`, `Type0`].includes(g_colorType) ? C_DIS_NONE : C_DIS_INHERIT);
|
|
9302
|
-
g_keycons.colorCursorNum = g_keycons.colorCursorNum % Math.ceil(
|
|
9389
|
+
g_keycons.colorCursorNum = g_keycons.colorCursorNum % Math.ceil(g_dfColorObj.setColorInit.length / g_limitObj.kcColorPickerNum);
|
|
9303
9390
|
changeColorPickers();
|
|
9304
9391
|
}
|
|
9305
9392
|
};
|
|
@@ -9326,14 +9413,14 @@ const keyConfigInit = (_kcType = g_kcType) => {
|
|
|
9326
9413
|
|
|
9327
9414
|
// ColorPickerの切替
|
|
9328
9415
|
createCss2Button(`lnkColorR`, `[${g_keycons.colorCursorNum + 1} /`, () => {
|
|
9329
|
-
g_keycons.colorCursorNum = (g_keycons.colorCursorNum + 1) % Math.ceil(
|
|
9416
|
+
g_keycons.colorCursorNum = (g_keycons.colorCursorNum + 1) % Math.ceil(g_dfColorObj.setColorInit.length / g_limitObj.kcColorPickerNum);
|
|
9330
9417
|
changeColorPickers();
|
|
9331
9418
|
}, g_lblPosObj.lnkColorR, g_cssObj.button_Start),
|
|
9332
9419
|
|
|
9333
9420
|
// 矢印の配色をフリーズアローへ反映
|
|
9334
9421
|
createCss2Button(`lnkColorCopy`, `↓]`, () => {
|
|
9335
9422
|
if (window.confirm(g_msgObj.colorCopyConfirm)) {
|
|
9336
|
-
for (let j = 0; j <
|
|
9423
|
+
for (let j = 0; j < g_dfColorObj.setColorInit.length; j++) {
|
|
9337
9424
|
g_headerObj.frzColor[j] = g_headerObj[`frzColor${g_colorType}`][j] =
|
|
9338
9425
|
fillArray(g_headerObj[`frzColor${g_colorType}`][j].length, g_headerObj[`setColor${g_colorType}`][j]);
|
|
9339
9426
|
}
|
|
@@ -9819,6 +9906,7 @@ const setAudio = async (_url) => {
|
|
|
9819
9906
|
g_currentPage = `loadingIos`;
|
|
9820
9907
|
lblLoading.textContent = `Click to Start!`;
|
|
9821
9908
|
divRoot.appendChild(makePlayButton(evt => {
|
|
9909
|
+
getSharedAudioContext().resume();
|
|
9822
9910
|
g_currentPage = `loading`;
|
|
9823
9911
|
resetKeyControl();
|
|
9824
9912
|
divRoot.removeChild(evt.target);
|
|
@@ -10037,6 +10125,9 @@ const loadingScoreInit = async () => {
|
|
|
10037
10125
|
// ユーザカスタムイベント
|
|
10038
10126
|
g_customJsObj.loading.forEach(func => func());
|
|
10039
10127
|
|
|
10128
|
+
// 初回プレイ時に出力デバイスの起動待ちでずれるのを防ぐため、
|
|
10129
|
+
// メイン画面へ移行する前にAudioContextを起動させておく
|
|
10130
|
+
await warmUpAudioContext();
|
|
10040
10131
|
mainInit();
|
|
10041
10132
|
};
|
|
10042
10133
|
|
|
@@ -12297,6 +12388,7 @@ const mainInit = () => {
|
|
|
12297
12388
|
let thisTime;
|
|
12298
12389
|
let buffTime;
|
|
12299
12390
|
let musicStartTime;
|
|
12391
|
+
let musicStartCtxTime;
|
|
12300
12392
|
let musicStartFlg = false;
|
|
12301
12393
|
|
|
12302
12394
|
g_inputKeyBuffer = {};
|
|
@@ -13469,14 +13561,32 @@ const mainInit = () => {
|
|
|
13469
13561
|
}
|
|
13470
13562
|
|
|
13471
13563
|
// 60fpsから遅延するため、その差分を取って次回のタイミングで遅れをリカバリする
|
|
13564
|
+
// - WebAudioAPI使用時は音源クロック(AudioContext.currentTime)を基準とする
|
|
13565
|
+
// performance.now()とAudioContextの時計は独立して進むため、後者を基準にしないと
|
|
13566
|
+
// 再生開始時のオフセットずれやsuspend/resumeによるずれを吸収できない
|
|
13472
13567
|
thisTime = performance.now();
|
|
13473
13568
|
buffTime = 0;
|
|
13474
|
-
|
|
13569
|
+
let holdFrame = false;
|
|
13570
|
+
|
|
13571
|
+
if (g_audio instanceof AudioPlayer && g_audioClockSync && musicStartCtxTime !== undefined) {
|
|
13572
|
+
if (g_audio.contextState === `running`) {
|
|
13573
|
+
buffTime = (g_audio.contextTime - musicStartCtxTime) * 1000
|
|
13574
|
+
- (currentFrame - musicStartFrame) * 1000 / g_fps;
|
|
13575
|
+
} else {
|
|
13576
|
+
// AudioContext停止中は音源も止まっているため、フレーム進行も止めて復帰を待つ
|
|
13577
|
+
getSharedAudioContext(); // suspended時はresumeを試行
|
|
13578
|
+
holdFrame = true;
|
|
13579
|
+
}
|
|
13580
|
+
} else if (g_audio instanceof AudioPlayer || currentFrame >= musicStartFrame) {
|
|
13475
13581
|
buffTime = (thisTime - musicStartTime - (currentFrame - musicStartFrame) * 1000 / g_fps);
|
|
13476
13582
|
}
|
|
13477
|
-
|
|
13478
|
-
|
|
13479
|
-
|
|
13583
|
+
|
|
13584
|
+
if (!holdFrame) {
|
|
13585
|
+
g_scoreObj.frameNum++;
|
|
13586
|
+
g_scoreObj.baseFrame++;
|
|
13587
|
+
}
|
|
13588
|
+
g_timeoutEvtId = setTimeout(flowTimeline, holdFrame ? g_maxFrameWait :
|
|
13589
|
+
Math.min(Math.max(1000 / g_fps - buffTime, 0), g_maxFrameWait));
|
|
13480
13590
|
}
|
|
13481
13591
|
};
|
|
13482
13592
|
g_skinJsObj.main.forEach(func => func());
|
|
@@ -13489,6 +13599,11 @@ const mainInit = () => {
|
|
|
13489
13599
|
const musicStartAdjustment = (g_headerObj.blankFrame - g_stateObj.decimalAdjustment + 1) / g_fps;
|
|
13490
13600
|
musicStartTime = performance.now() + (musicStartAdjustment + g_scheduleLead) * 1000;
|
|
13491
13601
|
g_audio.play(musicStartAdjustment);
|
|
13602
|
+
|
|
13603
|
+
// 音源クロック基準の開始時刻(フレーム進行の基準)
|
|
13604
|
+
// - g_audioLatencyCompensationが有効な場合は出力遅延分だけ表示を後ろへずらす
|
|
13605
|
+
musicStartCtxTime = g_audio.scheduledTime
|
|
13606
|
+
+ (g_audioLatencyCompensation ? g_audio.outputLatency : 0);
|
|
13492
13607
|
}
|
|
13493
13608
|
|
|
13494
13609
|
g_timeoutEvtId = setTimeout(flowTimeline, 1000 / g_fps);
|
|
@@ -14995,15 +15110,20 @@ const resultInit = () => {
|
|
|
14995
15110
|
} else {
|
|
14996
15111
|
// Canvas の内容を PNG 画像として取得
|
|
14997
15112
|
canvas.toBlob(async blob => {
|
|
14998
|
-
|
|
14999
|
-
|
|
15000
|
-
|
|
15001
|
-
}
|
|
15002
|
-
|
|
15113
|
+
try {
|
|
15114
|
+
if (blob === null) {
|
|
15115
|
+
throw new Error(`Failed to create result image blob.`);
|
|
15116
|
+
}
|
|
15117
|
+
await navigator.clipboard.write([
|
|
15118
|
+
new ClipboardItem({ 'image/png': blob })
|
|
15119
|
+
]);
|
|
15120
|
+
tmpDiv.removeChild(canvas);
|
|
15121
|
+
divRoot.removeChild(tmpDiv);
|
|
15122
|
+
makeInfoWindow(_msg, `leftToRightFade`);
|
|
15123
|
+
} catch {
|
|
15124
|
+
viewResultImage();
|
|
15125
|
+
}
|
|
15003
15126
|
});
|
|
15004
|
-
tmpDiv.removeChild(canvas);
|
|
15005
|
-
divRoot.removeChild(tmpDiv);
|
|
15006
|
-
makeInfoWindow(_msg, `leftToRightFade`);
|
|
15007
15127
|
}
|
|
15008
15128
|
|
|
15009
15129
|
} catch (err) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Source by tickle
|
|
7
7
|
* Created : 2019/11/19
|
|
8
|
-
* Revised : 2026/
|
|
8
|
+
* Revised : 2026/09/13 (v44.5.24)
|
|
9
9
|
*
|
|
10
10
|
* https://github.com/cwtickle/danoniplus
|
|
11
11
|
*/
|
|
@@ -1109,6 +1109,7 @@ const g_stateObj = {
|
|
|
1109
1109
|
bgmLooped: null,
|
|
1110
1110
|
bgmFadeIn: null,
|
|
1111
1111
|
bgmFadeOut: null,
|
|
1112
|
+
bgmRestart: null,
|
|
1112
1113
|
bgmTimeupdateEvtId: null,
|
|
1113
1114
|
bgmMuteFlg: false,
|
|
1114
1115
|
|