danoniplus 49.1.1 → 49.2.0
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 +288 -160
- 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/07/
|
|
7
|
+
* Revised : 2026/07/07
|
|
8
8
|
*
|
|
9
9
|
* https://github.com/cwtickle/danoniplus
|
|
10
10
|
*/
|
|
11
|
-
const g_version = `Ver 49.
|
|
12
|
-
const g_revisedDate = `2026/07/
|
|
11
|
+
const g_version = `Ver 49.2.0`;
|
|
12
|
+
const g_revisedDate = `2026/07/07`;
|
|
13
13
|
|
|
14
14
|
// カスタム用バージョン (danoni_custom.js 等で指定可)
|
|
15
15
|
let g_localVersion = ``;
|
|
@@ -177,6 +177,10 @@ let g_maxScore = 1000000;
|
|
|
177
177
|
let g_gameOverFlg = false;
|
|
178
178
|
let g_finishFlg = true;
|
|
179
179
|
|
|
180
|
+
// 音源のAudioContext管理、リトライ中フラグ
|
|
181
|
+
let g_sharedAudioContext = null;
|
|
182
|
+
let g_retryInProgress = false;
|
|
183
|
+
|
|
180
184
|
/** 共通オブジェクト */
|
|
181
185
|
const g_loadObj = {};
|
|
182
186
|
const g_rootObj = {};
|
|
@@ -2060,10 +2064,9 @@ const makeBgCanvas = (_ctx, { w = g_sWidth, h = g_sHeight } = {}) => {
|
|
|
2060
2064
|
* - divオブジェクト(ボタンなど)はdivRoot配下で管理しているため、子要素のみを全削除している。
|
|
2061
2065
|
* - dicRoot自体を削除しないよう注意すること。
|
|
2062
2066
|
* - 再描画時に共通で表示する箇所はここで指定している。
|
|
2063
|
-
* @param {boolean} [_redrawFlg=false] 画面横幅を再定義し、Canvas背景を再描画するかどうか
|
|
2064
2067
|
* @param {string} [_customDisplayName=''] 画面名(メイン画面: 'Main', それ以外: 空)
|
|
2065
2068
|
*/
|
|
2066
|
-
const clearWindow = (
|
|
2069
|
+
const clearWindow = (_customDisplayName = ``) => {
|
|
2067
2070
|
closeDisplayPreview();
|
|
2068
2071
|
resetKeyControl();
|
|
2069
2072
|
resetTransform();
|
|
@@ -2110,37 +2113,33 @@ const clearWindow = (_redrawFlg = false, _customDisplayName = ``) => {
|
|
|
2110
2113
|
getLayerWithClear(`layer2`);
|
|
2111
2114
|
}
|
|
2112
2115
|
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
makeBgCanvas(l0ctx, { w: g_sWidth + diffX });
|
|
2119
|
-
}
|
|
2116
|
+
// 画面背景を指定 (background-color)
|
|
2117
|
+
$id(`canvas-frame`).width = wUnit(g_sWidth + diffX);
|
|
2118
|
+
layer0.width = g_sWidth + diffX;
|
|
2119
|
+
if (!g_headerObj[`customBack${_customDisplayName}Use`]) {
|
|
2120
|
+
makeBgCanvas(l0ctx, { w: g_sWidth + diffX });
|
|
2120
2121
|
}
|
|
2121
2122
|
}
|
|
2122
2123
|
|
|
2123
2124
|
// 背景を再描画
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
g_cxtDeleteFlg = {};
|
|
2125
|
+
g_btnAddFunc = {};
|
|
2126
|
+
g_btnDeleteFlg = {};
|
|
2127
|
+
g_cxtAddFunc = {};
|
|
2128
|
+
g_cxtDeleteFlg = {};
|
|
2129
2129
|
|
|
2130
|
-
|
|
2131
|
-
|
|
2130
|
+
if (document.getElementById(`layer0`) === null ||
|
|
2131
|
+
(!g_headerObj[`customBack${_customDisplayName}Use`] && !g_headerObj.defaultSkinFlg)) {
|
|
2132
2132
|
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2133
|
+
$id(`canvas-frame`).width = wUnit(g_sWidth + diffX);
|
|
2134
|
+
createEmptySprite(divRoot, `divBack`, { w: g_sWidth + diffX });
|
|
2135
|
+
}
|
|
2136
2136
|
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2137
|
+
// CSSスタイルの初期化
|
|
2138
|
+
Object.keys(g_cssBkProperties).forEach(prop =>
|
|
2139
|
+
document.documentElement.style.setProperty(prop, g_cssBkProperties[prop]));
|
|
2140
2140
|
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
}
|
|
2141
|
+
Object.keys(g_headerObj).filter(val => val.startsWith(`--`) && hasVal(g_headerObj[val])).forEach(prop =>
|
|
2142
|
+
document.documentElement.style.setProperty(prop, getCssCustomProperty(prop, g_headerObj[prop])));
|
|
2144
2143
|
};
|
|
2145
2144
|
|
|
2146
2145
|
/**
|
|
@@ -2653,7 +2652,6 @@ class AudioPlayer {
|
|
|
2653
2652
|
}
|
|
2654
2653
|
|
|
2655
2654
|
// グローバルで1つだけ保持(遅延生成)
|
|
2656
|
-
let g_sharedAudioContext = null;
|
|
2657
2655
|
const getSharedAudioContext = () => {
|
|
2658
2656
|
if (!g_sharedAudioContext) {
|
|
2659
2657
|
g_sharedAudioContext = new AudioContext();
|
|
@@ -3254,7 +3252,6 @@ const loadChartFile = async (_scoreId = g_stateObj.scoreId) => {
|
|
|
3254
3252
|
|
|
3255
3253
|
await loadScript2(`${filename}?${Date.now()}`, false, charset);
|
|
3256
3254
|
if (typeof externalDosInit === C_TYP_FUNCTION) {
|
|
3257
|
-
deleteDiv(divRoot, `lblLoading`);
|
|
3258
3255
|
|
|
3259
3256
|
// 外部データを読込(ファイルが見つからなかった場合は譜面追記をスキップ)
|
|
3260
3257
|
externalDosInit();
|
|
@@ -5660,7 +5657,7 @@ const setKeyDfVal = _ptnName => {
|
|
|
5660
5657
|
*/
|
|
5661
5658
|
const titleInit = (_initFlg = false) => {
|
|
5662
5659
|
|
|
5663
|
-
clearWindow(
|
|
5660
|
+
clearWindow();
|
|
5664
5661
|
g_currentPage = `title`;
|
|
5665
5662
|
g_stateObj.settingSummaryVisible = false;
|
|
5666
5663
|
|
|
@@ -6391,9 +6388,10 @@ const playBGM = async (_num, _currentLoopNum = g_settings.musicLoopNum) => {
|
|
|
6391
6388
|
if (encodeFlg) {
|
|
6392
6389
|
try {
|
|
6393
6390
|
closeExistingAudio();
|
|
6394
|
-
|
|
6391
|
+
const cachedBuffer = getAudioBufferFromCache(url);
|
|
6392
|
+
if (cachedBuffer !== undefined) {
|
|
6395
6393
|
const tmpAudio = new AudioPlayer();
|
|
6396
|
-
tmpAudio.setBuffer(
|
|
6394
|
+
tmpAudio.setBuffer(cachedBuffer);
|
|
6397
6395
|
g_audioForMS = tmpAudio;
|
|
6398
6396
|
} else {
|
|
6399
6397
|
await loadScript2(url);
|
|
@@ -6668,7 +6666,7 @@ const setWindowStyle = (_text, _bkColor, _textColor, _align = C_ALIGN_LEFT, { _x
|
|
|
6668
6666
|
/*-----------------------------------------------------------*/
|
|
6669
6667
|
|
|
6670
6668
|
const dataMgtInit = () => {
|
|
6671
|
-
clearWindow(
|
|
6669
|
+
clearWindow();
|
|
6672
6670
|
pauseBGM();
|
|
6673
6671
|
const prevPage = g_currentPage;
|
|
6674
6672
|
g_currentPage = `dataMgt`;
|
|
@@ -6906,7 +6904,7 @@ const dataMgtInit = () => {
|
|
|
6906
6904
|
/*-----------------------------------------------------------*/
|
|
6907
6905
|
|
|
6908
6906
|
const preconditionInit = () => {
|
|
6909
|
-
clearWindow(
|
|
6907
|
+
clearWindow();
|
|
6910
6908
|
pauseBGM();
|
|
6911
6909
|
const prevPage = g_currentPage;
|
|
6912
6910
|
g_currentPage = `precondition`;
|
|
@@ -7134,7 +7132,7 @@ const makePlayButton = _func => createCss2Button(`btnPlay`, g_lblNameObj.b_play,
|
|
|
7134
7132
|
*/
|
|
7135
7133
|
const optionInit = () => {
|
|
7136
7134
|
|
|
7137
|
-
clearWindow(
|
|
7135
|
+
clearWindow();
|
|
7138
7136
|
pauseBGM();
|
|
7139
7137
|
const divRoot = document.getElementById(`divRoot`);
|
|
7140
7138
|
g_currentPage = `option`;
|
|
@@ -9212,7 +9210,7 @@ const resetGroupList = (_type, _keyCtrlPtn) => {
|
|
|
9212
9210
|
|
|
9213
9211
|
const settingsDisplayInit = () => {
|
|
9214
9212
|
|
|
9215
|
-
clearWindow(
|
|
9213
|
+
clearWindow();
|
|
9216
9214
|
const divRoot = document.getElementById(`divRoot`);
|
|
9217
9215
|
g_currentPage = `settingsDisplay`;
|
|
9218
9216
|
|
|
@@ -10019,7 +10017,7 @@ const interlockingButton = (_headerObj, _name, _current, _next, _buttonFlg = fal
|
|
|
10019
10017
|
/*-----------------------------------------------------------*/
|
|
10020
10018
|
|
|
10021
10019
|
const exSettingInit = () => {
|
|
10022
|
-
clearWindow(
|
|
10020
|
+
clearWindow();
|
|
10023
10021
|
g_currentPage = `exSetting`;
|
|
10024
10022
|
|
|
10025
10023
|
multiAppend(divRoot,
|
|
@@ -10180,7 +10178,7 @@ const createGeneralSettingEx = (_spriteList, _name, { defaultList = [C_FLG_OFF],
|
|
|
10180
10178
|
*/
|
|
10181
10179
|
const keyConfigInit = (_kcType = g_kcType, _initFlg = false) => {
|
|
10182
10180
|
|
|
10183
|
-
clearWindow(
|
|
10181
|
+
clearWindow();
|
|
10184
10182
|
const divRoot = document.getElementById(`divRoot`);
|
|
10185
10183
|
g_kcType = _kcType;
|
|
10186
10184
|
g_currentPage = `keyConfig`;
|
|
@@ -11956,9 +11954,9 @@ const changeShuffleConfigColor = (_keyCtrlPtn, _vals, _j = -1) => {
|
|
|
11956
11954
|
/* Scene : LOADING [strawberry] */
|
|
11957
11955
|
/*-----------------------------------------------------------*/
|
|
11958
11956
|
|
|
11959
|
-
const loadMusic = () => {
|
|
11957
|
+
const loadMusic = async () => {
|
|
11960
11958
|
|
|
11961
|
-
clearWindow(
|
|
11959
|
+
clearWindow();
|
|
11962
11960
|
pauseBGM();
|
|
11963
11961
|
g_currentPage = `loading`;
|
|
11964
11962
|
|
|
@@ -11971,128 +11969,228 @@ const loadMusic = () => {
|
|
|
11971
11969
|
const lblLoading = getLoadingLabel();
|
|
11972
11970
|
divRoot.appendChild(lblLoading);
|
|
11973
11971
|
|
|
11974
|
-
//
|
|
11975
|
-
|
|
11976
|
-
|
|
11977
|
-
|
|
11972
|
+
// 音源準備Promise(ローカル/オンライン双方を吸収)
|
|
11973
|
+
const audioReadyPromise = loadAndSetupAudio(url, lblLoading);
|
|
11974
|
+
|
|
11975
|
+
// 譜面データ読込・変換処理Promise(g_audioに依存しない部分)
|
|
11976
|
+
const chartReadyPromise = loadChartFile().then(() => prepareScoreData());
|
|
11977
|
+
|
|
11978
|
+
// 両方の完了を待つ
|
|
11979
|
+
let loadSucceeded = true;
|
|
11980
|
+
try {
|
|
11981
|
+
await Promise.all([audioReadyPromise, chartReadyPromise]);
|
|
11982
|
+
} catch (e) {
|
|
11983
|
+
console.warn(`Loading error: ${e}`);
|
|
11984
|
+
loadSucceeded = false;
|
|
11985
|
+
} finally {
|
|
11986
|
+
deleteDiv(divRoot, `lblLoading`);
|
|
11987
|
+
}
|
|
11988
|
+
|
|
11989
|
+
if (loadSucceeded) {
|
|
11990
|
+
mainInit(); // 音源・譜面変換双方の完了後にまとめて呼ぶ
|
|
11978
11991
|
}
|
|
11992
|
+
};
|
|
11979
11993
|
|
|
11980
|
-
|
|
11994
|
+
/**
|
|
11995
|
+
* 音源の取得とセットアップ
|
|
11996
|
+
* - エンコード形式(base64)か通常の音声ファイルかを判定し、それぞれの準備処理に振り分ける
|
|
11997
|
+
* - iOSの場合はユーザー操作(ジェスチャー)を待ってから再生準備を行う(readyToStart経由)
|
|
11998
|
+
* - キャッシュヒット時はダウンロード自体を行わないよう、取得処理はsetupWebAudioへ
|
|
11999
|
+
* コールバックとして渡し、キャッシュミス時にのみ評価されるようにしている
|
|
12000
|
+
* @param {string} _url 音源の取得元URL
|
|
12001
|
+
* @param {HTMLDivElement} _lblLoading ローディング表示用のDiv要素
|
|
12002
|
+
* @returns {Promise<void>} 再生準備(canplaythrough相当)が完了したら解決するPromise
|
|
12003
|
+
*/
|
|
12004
|
+
const loadAndSetupAudio = async (_url, _lblLoading) => {
|
|
12005
|
+
|
|
12006
|
+
/**
|
|
12007
|
+
* iOSの場合はユーザー操作(ジェスチャー)を待ってから_funcを実行する
|
|
12008
|
+
* - AudioContextの制約上、iOSはジェスチャーを起点にしないと音声再生が許可されないため
|
|
12009
|
+
* @param {() => Promise<void>} _func 実行する音源準備処理
|
|
12010
|
+
* @returns {Promise<void>}
|
|
12011
|
+
*/
|
|
12012
|
+
const readyToStart = _func => {
|
|
12013
|
+
if (!g_isIos) {
|
|
12014
|
+
return _func(); // 通常環境はそのまま実行するだけ
|
|
12015
|
+
}
|
|
12016
|
+
return new Promise((resolve, reject) => {
|
|
12017
|
+
g_currentPage = `loadingIos`;
|
|
12018
|
+
_lblLoading.textContent = `Click to Start!`;
|
|
12019
|
+
divRoot.appendChild(makePlayButton(evt => {
|
|
12020
|
+
getSharedAudioContext().resume();
|
|
12021
|
+
g_currentPage = `loading`;
|
|
12022
|
+
resetKeyControl();
|
|
12023
|
+
divRoot.removeChild(evt.target);
|
|
12024
|
+
_func().then(resolve).catch(reject);
|
|
12025
|
+
}));
|
|
12026
|
+
setShortcutEvent(g_currentPage);
|
|
12027
|
+
});
|
|
12028
|
+
};
|
|
12029
|
+
|
|
12030
|
+
// 音源準備処理そのものを、条件に応じて組み立てる
|
|
12031
|
+
const setupAudioFunc = (() => {
|
|
12032
|
+
|
|
12033
|
+
// エンコードなし & ローカル実行: Audio要素で直接再生
|
|
12034
|
+
if (!g_musicEncodedFlg && g_isFile) {
|
|
12035
|
+
return () => {
|
|
12036
|
+
g_audio = new Audio();
|
|
12037
|
+
g_audio.src = _url;
|
|
12038
|
+
return musicAfterLoaded();
|
|
12039
|
+
};
|
|
12040
|
+
}
|
|
12041
|
+
|
|
12042
|
+
// エンコードなし & オンライン: WebAudioAPI経由(URLからfetch)
|
|
12043
|
+
if (!g_musicEncodedFlg) {
|
|
12044
|
+
return () => setupWebAudio(async () => {
|
|
12045
|
+
const blobUrl = await fetchMusicBlobUrl(_url, _lblLoading);
|
|
12046
|
+
try {
|
|
12047
|
+
const response = await fetch(blobUrl);
|
|
12048
|
+
return await response.arrayBuffer();
|
|
12049
|
+
} finally {
|
|
12050
|
+
URL.revokeObjectURL(blobUrl);
|
|
12051
|
+
}
|
|
12052
|
+
}, _url);
|
|
12053
|
+
}
|
|
12054
|
+
|
|
12055
|
+
// エンコードあり: スクリプト読込・musicInit実行を経てWebAudioAPI準備
|
|
12056
|
+
return () => setupWebAudio(async () => {
|
|
12057
|
+
const scriptSrc = g_isFile ? _url : await fetchMusicBlobUrl(_url, _lblLoading);
|
|
12058
|
+
try {
|
|
12059
|
+
await loadScript2(scriptSrc);
|
|
12060
|
+
} finally {
|
|
12061
|
+
if (!g_isFile) {
|
|
12062
|
+
URL.revokeObjectURL(scriptSrc);
|
|
12063
|
+
}
|
|
12064
|
+
}
|
|
12065
|
+
if (typeof musicInit !== C_TYP_FUNCTION) {
|
|
12066
|
+
makeWarningWindow(g_msgInfoObj.E_0031, { backBtnUse: true });
|
|
12067
|
+
throw new Error(`musicInit is not defined`);
|
|
12068
|
+
}
|
|
12069
|
+
musicInit();
|
|
12070
|
+
return base64ToUint8Array(g_musicdata).buffer;
|
|
12071
|
+
}, _url);
|
|
12072
|
+
})();
|
|
12073
|
+
|
|
12074
|
+
return readyToStart(setupAudioFunc);
|
|
12075
|
+
};
|
|
12076
|
+
|
|
12077
|
+
/**
|
|
12078
|
+
* XHRによる音源ファイルのダウンロード(Promise化)
|
|
12079
|
+
* - ダウンロードして Blob URL を返す
|
|
12080
|
+
* @param {string} _url 音源ファイルのURL
|
|
12081
|
+
* @param {HTMLDivElement} _lblLoading ローディング表示用のDiv要素
|
|
12082
|
+
* @returns {Promise<string>} Blob URL
|
|
12083
|
+
*/
|
|
12084
|
+
const fetchMusicBlobUrl = (_url, _lblLoading) => new Promise((resolve, reject) => {
|
|
11981
12085
|
const request = new XMLHttpRequest();
|
|
11982
|
-
request.open(`GET`,
|
|
12086
|
+
request.open(`GET`, _url, true);
|
|
11983
12087
|
request.responseType = `blob`;
|
|
11984
12088
|
|
|
12089
|
+
const STALL_TIMEOUT_MS = 30000; // 30秒間、進捗がなければ停滞とみなす
|
|
12090
|
+
let stallTimer = null;
|
|
12091
|
+
|
|
12092
|
+
// 停滞タイマーをリセット
|
|
12093
|
+
const resetStallTimer = () => {
|
|
12094
|
+
clearTimeout(stallTimer);
|
|
12095
|
+
stallTimer = setTimeout(() => {
|
|
12096
|
+
request.abort();
|
|
12097
|
+
makeWarningWindow(g_msgInfoObj.E_0033, { backBtnUse: true });
|
|
12098
|
+
reject(new Error(`stalled`));
|
|
12099
|
+
}, STALL_TIMEOUT_MS);
|
|
12100
|
+
};
|
|
12101
|
+
|
|
11985
12102
|
// 読み込み完了時
|
|
11986
12103
|
request.addEventListener(`load`, () => {
|
|
12104
|
+
clearTimeout(stallTimer);
|
|
11987
12105
|
if (request.status >= 200 && request.status < 300) {
|
|
11988
12106
|
const blobUrl = URL.createObjectURL(request.response);
|
|
11989
12107
|
createEmptySprite(divRoot, `loader`, g_windowObj.loader);
|
|
11990
|
-
|
|
11991
|
-
|
|
12108
|
+
_lblLoading.textContent = g_lblNameObj.pleaseWait;
|
|
12109
|
+
resolve(blobUrl);
|
|
11992
12110
|
} else {
|
|
11993
|
-
makeWarningWindow(`${g_msgInfoObj.E_0041.split('{0}').join(getFullPath(
|
|
12111
|
+
makeWarningWindow(`${g_msgInfoObj.E_0041.split('{0}').join(getFullPath(_url))}<br>(${request.status} ${request.statusText})`, { backBtnUse: true });
|
|
12112
|
+
reject(new Error(`HTTP ${request.status}`));
|
|
11994
12113
|
}
|
|
11995
12114
|
});
|
|
11996
12115
|
|
|
11997
12116
|
// 進捗時
|
|
11998
12117
|
request.addEventListener(`progress`, _event => {
|
|
11999
|
-
|
|
12118
|
+
resetStallTimer(); // 進捗があるたびにタイマーをリセット
|
|
12119
|
+
const lblLoadingElem = document.getElementById(`lblLoading`);
|
|
12120
|
+
if (lblLoadingElem === null) return; // 並列処理で先に削除されている場合の防御
|
|
12000
12121
|
|
|
12001
12122
|
if (_event.lengthComputable) {
|
|
12002
12123
|
const rate = _event.loaded / _event.total;
|
|
12003
12124
|
createEmptySprite(divRoot, `loader`, { y: g_sHeight - 10, h: 10, w: g_sWidth * rate, backgroundColor: `#eeeeee` });
|
|
12004
|
-
|
|
12125
|
+
lblLoadingElem.textContent = `${g_lblNameObj.nowLoading} ${Math.floor(rate * 100)}%`;
|
|
12005
12126
|
} else {
|
|
12006
|
-
|
|
12127
|
+
lblLoadingElem.textContent = `${g_lblNameObj.nowLoading} ${_event.loaded}Bytes`;
|
|
12007
12128
|
}
|
|
12008
12129
|
// ユーザカスタムイベント
|
|
12009
12130
|
safeExecuteCustomHooks(`g_customJsObj.progress`, g_customJsObj.progress, _event);
|
|
12010
12131
|
});
|
|
12011
12132
|
|
|
12012
|
-
|
|
12013
|
-
|
|
12014
|
-
|
|
12133
|
+
request.addEventListener(`error`, () => {
|
|
12134
|
+
clearTimeout(stallTimer);
|
|
12135
|
+
makeWarningWindow(g_msgInfoObj.E_0034, { backBtnUse: true });
|
|
12136
|
+
reject(new Error(`network error`));
|
|
12137
|
+
});
|
|
12015
12138
|
|
|
12139
|
+
resetStallTimer(); // 初回(最初のprogressが来るまで)のタイマーも開始
|
|
12016
12140
|
request.send();
|
|
12017
|
-
};
|
|
12141
|
+
});
|
|
12018
12142
|
|
|
12019
12143
|
/**
|
|
12020
|
-
*
|
|
12021
|
-
*
|
|
12022
|
-
*
|
|
12023
|
-
* @param {
|
|
12144
|
+
* WebAudioAPIによる音源再生の準備(共通処理)
|
|
12145
|
+
* - AudioPlayerを生成し、canplaythrough/errorの発火待ちを開始した上で、
|
|
12146
|
+
* ArrayBufferを取得してデコードする(キャッシュヒット時はデコードをスキップ)
|
|
12147
|
+
* @param {() => Promise<ArrayBuffer>} _fetchArrayBuffer 未キャッシュの場合にArrayBufferを取得する関数
|
|
12148
|
+
* @param {string} [_cacheKey] AudioBufferキャッシュ照合用のキー(省略時はキャッシュを使わない)
|
|
12149
|
+
* @returns {Promise<void>} 再生準備(canplaythrough相当)が完了したら解決するPromise
|
|
12024
12150
|
*/
|
|
12025
|
-
const
|
|
12026
|
-
|
|
12027
|
-
const
|
|
12028
|
-
if (g_isFile) {
|
|
12029
|
-
g_audio = new Audio();
|
|
12030
|
-
g_audio.src = _url;
|
|
12031
|
-
musicAfterLoaded();
|
|
12032
|
-
} else {
|
|
12033
|
-
initWebAudioAPIfromURL(_url);
|
|
12034
|
-
}
|
|
12035
|
-
};
|
|
12036
|
-
|
|
12037
|
-
const readyToStart = _func => {
|
|
12038
|
-
if (g_isIos) {
|
|
12039
|
-
g_currentPage = `loadingIos`;
|
|
12040
|
-
lblLoading.textContent = `Click to Start!`;
|
|
12041
|
-
divRoot.appendChild(makePlayButton(evt => {
|
|
12042
|
-
getSharedAudioContext().resume();
|
|
12043
|
-
g_currentPage = `loading`;
|
|
12044
|
-
resetKeyControl();
|
|
12045
|
-
divRoot.removeChild(evt.target);
|
|
12046
|
-
_func();
|
|
12047
|
-
}));
|
|
12048
|
-
setShortcutEvent(g_currentPage);
|
|
12049
|
-
} else {
|
|
12050
|
-
_func();
|
|
12051
|
-
}
|
|
12052
|
-
};
|
|
12151
|
+
const setupWebAudio = async (_fetchArrayBuffer, _cacheKey) => {
|
|
12152
|
+
g_audio = new AudioPlayer();
|
|
12153
|
+
const loadedPromise = musicAfterLoaded(); // canplaythrough/errorの発火をここで待つ
|
|
12053
12154
|
|
|
12054
|
-
|
|
12055
|
-
|
|
12056
|
-
|
|
12057
|
-
musicInit();
|
|
12058
|
-
readyToStart(() => initWebAudioAPIfromBase64(g_musicdata, _cacheKey));
|
|
12059
|
-
} else {
|
|
12060
|
-
makeWarningWindow(g_msgInfoObj.E_0031);
|
|
12061
|
-
musicAfterLoaded();
|
|
12062
|
-
}
|
|
12155
|
+
const cachedBuffer = _cacheKey ? getAudioBufferFromCache(_cacheKey) : undefined;
|
|
12156
|
+
if (cachedBuffer !== undefined) {
|
|
12157
|
+
g_audio.setBuffer(cachedBuffer);
|
|
12063
12158
|
} else {
|
|
12064
|
-
|
|
12159
|
+
const arrayBuffer = await _fetchArrayBuffer();
|
|
12160
|
+
await g_audio.init(arrayBuffer);
|
|
12161
|
+
if (_cacheKey) {
|
|
12162
|
+
cacheAudioBuffer(_cacheKey, g_audio.getBuffer());
|
|
12163
|
+
}
|
|
12065
12164
|
}
|
|
12165
|
+
return loadedPromise;
|
|
12066
12166
|
};
|
|
12067
12167
|
|
|
12068
|
-
|
|
12069
|
-
const
|
|
12070
|
-
g_audio = new AudioPlayer();
|
|
12071
|
-
musicAfterLoaded();
|
|
12168
|
+
const g_audioBufferCache = new Map();
|
|
12169
|
+
const AUDIO_CACHE_MAX = 5;
|
|
12072
12170
|
|
|
12073
|
-
|
|
12074
|
-
|
|
12075
|
-
|
|
12076
|
-
|
|
12077
|
-
|
|
12078
|
-
|
|
12079
|
-
|
|
12080
|
-
|
|
12171
|
+
/**
|
|
12172
|
+
* デコード済みAudioBufferのキャッシュからの取得
|
|
12173
|
+
* - Mapは挿入順を保持するのみでアクセス順を保持しないため、
|
|
12174
|
+
* ヒット時にエントリを一度削除して再挿入し、最新として扱う(LRU方式)
|
|
12175
|
+
* @param {string} _key キャッシュキー(楽曲の実URL)
|
|
12176
|
+
* @returns {AudioBuffer|undefined} キャッシュされたAudioBuffer。存在しない場合はundefined
|
|
12177
|
+
*/
|
|
12178
|
+
const getAudioBufferFromCache = (_key) => {
|
|
12179
|
+
if (!g_audioBufferCache.has(_key)) {
|
|
12180
|
+
return undefined;
|
|
12081
12181
|
}
|
|
12182
|
+
const buffer = g_audioBufferCache.get(_key);
|
|
12183
|
+
g_audioBufferCache.delete(_key);
|
|
12184
|
+
g_audioBufferCache.set(_key, buffer); // 末尾(最新)に再挿入
|
|
12185
|
+
return buffer;
|
|
12082
12186
|
};
|
|
12083
12187
|
|
|
12084
|
-
|
|
12085
|
-
|
|
12086
|
-
|
|
12087
|
-
|
|
12088
|
-
|
|
12089
|
-
|
|
12090
|
-
await g_audio.init(arrayBuffer);
|
|
12091
|
-
};
|
|
12092
|
-
|
|
12093
|
-
const g_audioBufferCache = new Map();
|
|
12094
|
-
const AUDIO_CACHE_MAX = 5;
|
|
12095
|
-
|
|
12188
|
+
/**
|
|
12189
|
+
* デコード済みAudioBufferのキャッシュへの登録
|
|
12190
|
+
* - 直近 AUDIO_CACHE_MAX 件を保持するLRU方式。上限を超えた場合は最も古いエントリから破棄する
|
|
12191
|
+
* @param {string} _key キャッシュキー(楽曲の実URL)
|
|
12192
|
+
* @param {AudioBuffer} _buffer デコード済みのAudioBuffer
|
|
12193
|
+
*/
|
|
12096
12194
|
const cacheAudioBuffer = (_key, _buffer) => {
|
|
12097
12195
|
g_audioBufferCache.set(_key, _buffer);
|
|
12098
12196
|
if (g_audioBufferCache.size > AUDIO_CACHE_MAX) {
|
|
@@ -12100,6 +12198,13 @@ const cacheAudioBuffer = (_key, _buffer) => {
|
|
|
12100
12198
|
}
|
|
12101
12199
|
};
|
|
12102
12200
|
|
|
12201
|
+
/**
|
|
12202
|
+
* base64文字列をUint8Arrayに変換
|
|
12203
|
+
* - Uint8Array.from(atob(str), callback)によるコールバック呼び出し形式は
|
|
12204
|
+
* 大容量データで変換オーバーヘッドが大きいため、forループによる直接代入で高速化している
|
|
12205
|
+
* @param {string} _base64Str base64エンコードされた文字列
|
|
12206
|
+
* @returns {Uint8Array} 変換後のバイト配列
|
|
12207
|
+
*/
|
|
12103
12208
|
const base64ToUint8Array = (_base64Str) => {
|
|
12104
12209
|
const binaryStr = atob(_base64Str);
|
|
12105
12210
|
const len = binaryStr.length;
|
|
@@ -12110,34 +12215,41 @@ const base64ToUint8Array = (_base64Str) => {
|
|
|
12110
12215
|
return array;
|
|
12111
12216
|
};
|
|
12112
12217
|
|
|
12113
|
-
|
|
12218
|
+
/**
|
|
12219
|
+
* 音源の再生準備完了を待つ
|
|
12220
|
+
* - g_audio が Audio要素の場合は canplaythrough/error イベントの発火を待つ
|
|
12221
|
+
* - g_audio が AudioPlayer の場合、readyState が既に4(デコード済み、キャッシュヒット時など)であれば即時解決する
|
|
12222
|
+
* - canplaythrough/error のどちらが発火しても、もう一方のリスナーも確実に削除する(cleanup)
|
|
12223
|
+
* @returns {Promise<void>} 再生準備が完了したら解決し、読込エラー時は例外を投げて拒否するPromise
|
|
12224
|
+
*/
|
|
12225
|
+
const musicAfterLoaded = () => new Promise((resolve, reject) => {
|
|
12114
12226
|
g_audio.load();
|
|
12115
12227
|
|
|
12116
12228
|
if (g_audio.readyState === 4) {
|
|
12117
|
-
|
|
12118
|
-
loadingScoreInit();
|
|
12229
|
+
resolve();
|
|
12119
12230
|
} else {
|
|
12120
|
-
|
|
12121
|
-
|
|
12122
|
-
|
|
12123
|
-
loadingScoreInit();
|
|
12124
|
-
})(), false);
|
|
12125
|
-
|
|
12126
|
-
// エラー時
|
|
12127
|
-
g_audio.addEventListener(`error`, (() => function f() {
|
|
12128
|
-
g_audio.removeEventListener(`error`, f, false);
|
|
12231
|
+
const onCanPlay = () => { cleanup(); resolve(); };
|
|
12232
|
+
const onError = () => {
|
|
12233
|
+
cleanup();
|
|
12129
12234
|
makeWarningWindow(g_msgInfoObj.E_0041.split(`{0}`).join(g_audio.src), { backBtnUse: true });
|
|
12130
|
-
|
|
12235
|
+
reject(new Error(`audio load error`));
|
|
12236
|
+
};
|
|
12237
|
+
const cleanup = () => {
|
|
12238
|
+
g_audio.removeEventListener(`canplaythrough`, onCanPlay, false);
|
|
12239
|
+
g_audio.removeEventListener(`error`, onError, false);
|
|
12240
|
+
};
|
|
12241
|
+
g_audio.addEventListener(`canplaythrough`, onCanPlay, false);
|
|
12242
|
+
g_audio.addEventListener(`error`, onError, false);
|
|
12131
12243
|
}
|
|
12132
|
-
};
|
|
12244
|
+
});
|
|
12133
12245
|
|
|
12134
12246
|
/**
|
|
12135
|
-
*
|
|
12247
|
+
* 譜面データの変換処理
|
|
12248
|
+
* - 音源データの状態に依存しない部分のみを担う(g_audio非参照)
|
|
12249
|
+
* - loadMusic経由(並行フロー)、executeRetry経由(曲中リトライ)の両方から呼ばれる
|
|
12136
12250
|
*/
|
|
12137
|
-
const
|
|
12251
|
+
const prepareScoreData = () => {
|
|
12138
12252
|
|
|
12139
|
-
// 譜面データの読み込み
|
|
12140
|
-
await loadChartFile();
|
|
12141
12253
|
const tkObj = getKeyInfo();
|
|
12142
12254
|
const [keyCtrlPtn, keyNum] = [tkObj.keyCtrlPtn, tkObj.keyNum];
|
|
12143
12255
|
g_headerObj.blankFrameDef = setVal(g_headerObj.blankFrameDefs[g_stateObj.scoreId], g_headerObj.blankFrameDefs[0]);
|
|
@@ -12285,8 +12397,6 @@ const loadingScoreInit = async () => {
|
|
|
12285
12397
|
|
|
12286
12398
|
// ユーザカスタムイベント
|
|
12287
12399
|
safeExecuteCustomHooks(`g_customJsObj.loading`, g_customJsObj.loading);
|
|
12288
|
-
|
|
12289
|
-
mainInit();
|
|
12290
12400
|
};
|
|
12291
12401
|
|
|
12292
12402
|
/**
|
|
@@ -14244,9 +14354,6 @@ const getArrowSettings = () => {
|
|
|
14244
14354
|
g_workObj.frzReturnTimerId = null;
|
|
14245
14355
|
}
|
|
14246
14356
|
|
|
14247
|
-
// AutoRetryの初期化
|
|
14248
|
-
g_workObj.autoRetryFlg = false;
|
|
14249
|
-
|
|
14250
14357
|
// Camoufrageの設定
|
|
14251
14358
|
if (g_stateObj.camoufrage !== C_FLG_OFF) {
|
|
14252
14359
|
|
|
@@ -14423,7 +14530,7 @@ const setKeyCtrl = (_localStorage, _keyNum, _keyCtrlPtn) => {
|
|
|
14423
14530
|
* メイン画面初期化
|
|
14424
14531
|
*/
|
|
14425
14532
|
const mainInit = () => {
|
|
14426
|
-
clearWindow(
|
|
14533
|
+
clearWindow(`Main`);
|
|
14427
14534
|
const divRoot = document.getElementById(`divRoot`);
|
|
14428
14535
|
document.oncontextmenu = () => false;
|
|
14429
14536
|
g_currentPage = `main`;
|
|
@@ -14876,7 +14983,7 @@ const mainInit = () => {
|
|
|
14876
14983
|
};
|
|
14877
14984
|
|
|
14878
14985
|
// キー操作イベント
|
|
14879
|
-
document.onkeydown = evt => {
|
|
14986
|
+
document.onkeydown = async evt => {
|
|
14880
14987
|
evt.preventDefault();
|
|
14881
14988
|
const setCode = transCode(evt);
|
|
14882
14989
|
|
|
@@ -14888,17 +14995,16 @@ const mainInit = () => {
|
|
|
14888
14995
|
|
|
14889
14996
|
// 曲中リトライ、タイトルバック
|
|
14890
14997
|
if (setCode === g_kCdN[g_headerObj.keyRetry]) {
|
|
14891
|
-
g_audio.pause();
|
|
14892
|
-
clearTimeout(g_timeoutEvtId);
|
|
14893
14998
|
|
|
14894
14999
|
if (g_isMac && keyIsShift()) {
|
|
14895
15000
|
// Mac OS、IPad OSはDeleteキーが無いためShift+BSで代用
|
|
15001
|
+
g_audio.pause();
|
|
15002
|
+
clearTimeout(g_timeoutEvtId);
|
|
14896
15003
|
titleInit();
|
|
14897
15004
|
|
|
14898
15005
|
} else {
|
|
14899
15006
|
// その他の環境では単にRetryに対応するキーのみで適用
|
|
14900
|
-
|
|
14901
|
-
musicAfterLoaded();
|
|
15007
|
+
await executeRetry(`Retry`);
|
|
14902
15008
|
}
|
|
14903
15009
|
|
|
14904
15010
|
} else if (setCode === g_kCdN[g_headerObj.keyTitleBack]) {
|
|
@@ -16180,6 +16286,32 @@ const executeFrzReturn = (_seq, _idx, _axis) => {
|
|
|
16180
16286
|
g_workObj.frzReturnTimerId = setTimeout(() => executeFrzReturn(_seq, _idx + 1, _axis), 20);
|
|
16181
16287
|
};
|
|
16182
16288
|
|
|
16289
|
+
/**
|
|
16290
|
+
* 曲中リトライの共通処理
|
|
16291
|
+
* - 手動リトライ、AutoRetryの両方から呼ばれる
|
|
16292
|
+
* - g_retryInProgressによる多重実行防止、失敗時のログ出力・フラグ復帰を一括で行う
|
|
16293
|
+
* @param {string} [_logLabel='Retry'] エラーログに表示するラベル(手動/自動の区別用)
|
|
16294
|
+
*/
|
|
16295
|
+
const executeRetry = async (_logLabel = `Retry`) => {
|
|
16296
|
+
if (g_retryInProgress) {
|
|
16297
|
+
return;
|
|
16298
|
+
}
|
|
16299
|
+
g_retryInProgress = true;
|
|
16300
|
+
try {
|
|
16301
|
+
g_audio.pause();
|
|
16302
|
+
clearTimeout(g_timeoutEvtId);
|
|
16303
|
+
clearWindow(`Main`);
|
|
16304
|
+
await musicAfterLoaded();
|
|
16305
|
+
await loadChartFile();
|
|
16306
|
+
prepareScoreData();
|
|
16307
|
+
mainInit();
|
|
16308
|
+
} catch (e) {
|
|
16309
|
+
console.warn(`${_logLabel} audio load error: ${e}`);
|
|
16310
|
+
} finally {
|
|
16311
|
+
g_retryInProgress = false;
|
|
16312
|
+
}
|
|
16313
|
+
};
|
|
16314
|
+
|
|
16183
16315
|
/**
|
|
16184
16316
|
* AutoRetryの設定
|
|
16185
16317
|
* @param {string} _retryCondition リトライ基準となるAutoRetry名
|
|
@@ -16189,13 +16321,9 @@ const quickRetry = (_retryCondition) => {
|
|
|
16189
16321
|
if (retryNum < 0) {
|
|
16190
16322
|
return;
|
|
16191
16323
|
}
|
|
16192
|
-
if (g_settings.autoRetryNum >= retryNum && !
|
|
16193
|
-
|
|
16194
|
-
|
|
16195
|
-
g_audio.pause();
|
|
16196
|
-
clearTimeout(g_timeoutEvtId);
|
|
16197
|
-
clearWindow();
|
|
16198
|
-
musicAfterLoaded();
|
|
16324
|
+
if (g_settings.autoRetryNum >= retryNum && !g_retryInProgress) {
|
|
16325
|
+
setTimeout(async () => {
|
|
16326
|
+
await executeRetry(`AutoRetry`);
|
|
16199
16327
|
}, 16);
|
|
16200
16328
|
}
|
|
16201
16329
|
};
|
|
@@ -16796,7 +16924,7 @@ const finishViewing = () => {
|
|
|
16796
16924
|
*/
|
|
16797
16925
|
const resultInit = () => {
|
|
16798
16926
|
|
|
16799
|
-
clearWindow(
|
|
16927
|
+
clearWindow();
|
|
16800
16928
|
g_currentPage = `result`;
|
|
16801
16929
|
|
|
16802
16930
|
// 結果画面用フレーム初期化
|