danoniplus 52.1.0 → 52.2.1

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.
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Source by tickle
7
7
  * Created : 2026/09/13
8
- * Revised : 2026/10/20 (v51.4.1)
8
+ * Revised : 2026/11/08 (v52.2.1)
9
9
  *
10
10
  * https://github.com/cwtickle/danoniplus
11
11
  */
@@ -35,10 +35,13 @@ const loadMusic = async () => {
35
35
  // 譜面データ読込・変換処理Promise(g_audioに依存しない部分)
36
36
  const chartReadyPromise = loadChartFile().then(() => prepareScoreData());
37
37
 
38
+ // リングノート用の効果音 (失敗しても譜面進行は止めない)
39
+ const ringSoundPromise = loadRingSounds();
40
+
38
41
  // 両方の完了を待つ
39
42
  let loadSucceeded = true;
40
43
  try {
41
- await Promise.all([audioReadyPromise, chartReadyPromise]);
44
+ await Promise.all([audioReadyPromise, chartReadyPromise, ringSoundPromise]);
42
45
  } catch (e) {
43
46
  console.warn(`Loading error: ${e}`);
44
47
  loadSucceeded = false;
@@ -279,6 +282,190 @@ const base64ToUint8Array = (_base64Str) => {
279
282
  return array;
280
283
  };
281
284
 
285
+ const g_ringSoundBuffers = new Map(); // 音番号 -> AudioBuffer (現在の作品)
286
+ const g_ringSoundCache = new Map(); // URL -> AudioBuffer (リトライ・再プレイ用)
287
+
288
+ /**
289
+ * 帯域ノイズ型の効果音を合成 (_def.bands を持つデフォルト音用)
290
+ * 白色ノイズを帯域ごとの共鳴フィルタ(バイクアッド)に通し、帯域ごとの2段減衰エンベロープを掛けて足し合わせる
291
+ * @param {object} _def
292
+ * @param {number} _rate サンプルレート
293
+ * @returns {Float32Array}
294
+ */
295
+ const synthRingBands = (_def, _rate) => {
296
+ const len = Math.max(Math.floor(_rate * _def.dur), 1);
297
+ const out = new Float32Array(len);
298
+ const attack = Math.max(Math.floor(_rate * (_def.attack ?? 0.008)), 1);
299
+
300
+ // 種付き乱数 (毎回同じ音になる)
301
+ let seed = 12345;
302
+ const rnd = () => {
303
+ seed = (seed + 0x6D2B79F5) >>> 0;
304
+ let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
305
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
306
+ return (((t ^ (t >>> 14)) >>> 0) / 4294967296) * 2 - 1;
307
+ };
308
+ const noise = new Float32Array(len).map(() => rnd());
309
+
310
+ _def.bands.forEach(b => {
311
+ const w0 = 2 * Math.PI * b.f / _rate;
312
+ const alpha = Math.sin(w0) / (2 * b.q);
313
+ const a0 = 1 + alpha;
314
+ const b0 = alpha / a0, b2 = -alpha / a0;
315
+ const a1 = -2 * Math.cos(w0) / a0, a2 = (1 - alpha) / a0;
316
+ const norm = 1 / Math.max(alpha / a0 * Math.sqrt(b.q) * 4, 1e-6);
317
+ const mix2 = b.mix2 ?? 0, tau2 = b.tau2 ?? b.tau;
318
+ let x1 = 0, x2 = 0, y1 = 0, y2 = 0;
319
+ for (let i = 0; i < len; i++) {
320
+ const x = noise[i];
321
+ const y = b0 * x + b2 * x2 - a1 * y1 - a2 * y2;
322
+ x2 = x1; x1 = x; y2 = y1; y1 = y;
323
+ const t = i / _rate;
324
+ const env = (1 - mix2) * Math.exp(-t / b.tau) + mix2 * Math.exp(-t / tau2);
325
+ out[i] += y * norm * b.gain * env * Math.min(1, (i + 1) / attack);
326
+ }
327
+ });
328
+
329
+ // 高域カット (2次ローパスを lpPasses 段)
330
+ if (_def.lowpass) {
331
+ const w0 = 2 * Math.PI * _def.lowpass / _rate;
332
+ const alpha = Math.sin(w0) / (2 * Math.SQRT1_2);
333
+ const a0 = 1 + alpha, cs = Math.cos(w0);
334
+ const b0 = (1 - cs) / 2 / a0, b1 = (1 - cs) / a0, a1 = -2 * cs / a0, a2 = (1 - alpha) / a0;
335
+ for (let pass = 0; pass < (_def.lpPasses ?? 2); pass++) {
336
+ let x1 = 0, x2 = 0, y1 = 0, y2 = 0;
337
+ for (let i = 0; i < len; i++) {
338
+ const x = out[i];
339
+ const y = b0 * x + b1 * x1 + b0 * x2 - a1 * y1 - a2 * y2;
340
+ x2 = x1; x1 = x; y2 = y1; y1 = y; out[i] = y;
341
+ }
342
+ }
343
+ }
344
+
345
+ // 末尾2msをフェードアウト (クリック防止) + ピーク正規化
346
+ const fade = Math.floor(_rate * 0.002);
347
+ for (let i = 0; i < fade; i++) {
348
+ out[len - 1 - i] *= i / fade;
349
+ }
350
+ const peak = out.reduce((max, v) => Math.max(max, Math.abs(v)), 0) || 1;
351
+ return out.map(v => v * (_def.level ?? 0.5) / peak);
352
+ };
353
+
354
+ /**
355
+ * デフォルト音の生成 (外部ファイル不要)
356
+ * @param {{wave: string, freq: number, dur: number}} _def
357
+ * @returns {AudioBuffer}
358
+ */
359
+ const makeDefaultRingSound = _def => {
360
+ const ctx = getSharedAudioContext();
361
+ const rate = ctx.sampleRate;
362
+ const len = Math.max(Math.floor(rate * _def.dur), 1);
363
+ const buffer = ctx.createBuffer(1, len, rate);
364
+ const data = buffer.getChannelData(0);
365
+
366
+ // 帯域ノイズ型 (bands を持つ定義)
367
+ if (_def.bands) {
368
+ data.set(synthRingBands(_def, rate));
369
+ return buffer;
370
+ }
371
+ const waves = {
372
+ sine: p => Math.sin(2 * Math.PI * p),
373
+ triangle: p => 2 * Math.abs(2 * (p - Math.floor(p + 0.5))) - 1,
374
+ square: p => (p % 1 < 0.5 ? 1 : -1) * 0.6,
375
+ };
376
+ const wave = waves[_def.wave] ?? waves.sine;
377
+ const noiseMix = _def.noise ?? 0;
378
+ const decay = _def.decay ?? 6;
379
+ const freqEnd = _def.freqEnd ?? _def.freq;
380
+ const sweepRate = rate * (_def.sweep ?? 0.03);
381
+ let phase = 0;
382
+ let prevNoise = 0;
383
+ for (let i = 0; i < len; i++) {
384
+ // ピッチは freq から freqEnd へ指数的に収束 (freqEnd 未指定なら一定)
385
+ const freq = freqEnd + (_def.freq - freqEnd) * Math.exp(-i / sweepRate);
386
+ phase += freq / rate;
387
+
388
+ // ノイズは1次差分で高域寄りにする (破裂音の「パ」成分)
389
+ const n = Math.random() * 2 - 1;
390
+ const highNoise = (n - prevNoise) * 0.5;
391
+ prevNoise = n;
392
+
393
+ // 立ち上がり2ms + 指数減衰 (クリックノイズ防止)
394
+ const env = Math.min(1, i / (rate * 0.002)) * Math.exp(-decay * i / len);
395
+ data[i] = 0.5 * env * ((1 - noiseMix) * wave(phase) + noiseMix * highNoise);
396
+ }
397
+ return buffer;
398
+ };
399
+
400
+ /**
401
+ * リングノート用効果音の実際のパスを取得
402
+ * @param {string} _soundUrl
403
+ * @returns {string}
404
+ */
405
+ const getRingSoundUrl = (_soundUrl = ``) => {
406
+ const [soundFile, soundPath] = getFilePath(_soundUrl, C_DIR_SOUND);
407
+ return `${soundPath}${soundFile}`;
408
+ };
409
+
410
+ /**
411
+ * リングノート用効果音の読込
412
+ * - |ringSound=a.mp3,b.mp3| の並び順が音番号 (1始まり)。0は無音
413
+ * - 読込失敗時は該当の音のみ無音とし、例外は投げない
414
+ * @returns {Promise<void>}
415
+ */
416
+ const loadRingSounds = async () => {
417
+ g_ringSoundBuffers.clear();
418
+ // デフォルト音 (音番号1~)
419
+ g_ringDefaultSounds.forEach((def, i) => {
420
+ const key = `default:${i + 1}`;
421
+ if (!g_ringSoundCache.has(key)) {
422
+ g_ringSoundCache.set(key, makeDefaultRingSound(def));
423
+ }
424
+ g_ringSoundBuffers.set(i + 1, g_ringSoundCache.get(key));
425
+ });
426
+
427
+ // |ringSound=| の指定でデフォルト音を上書き (空欄はデフォルトのまま)
428
+ const names = g_headerObj.ringSound.split(`,`).map(val => trimStr(val));
429
+ if (names.every(name => !hasVal(name))) {
430
+ return;
431
+ }
432
+ if (g_isFile) {
433
+ console.warn(`ringSound: file scheme is not supported (default sounds are used)`);
434
+ return;
435
+ }
436
+ await Promise.all(names.map(async (name, i) => {
437
+ if (!hasVal(name)) {
438
+ return;
439
+ }
440
+ const url = getRingSoundUrl(name);
441
+ try {
442
+ let buffer = g_ringSoundCache.get(url);
443
+ if (buffer === undefined) {
444
+ const controller = new AbortController();
445
+ const timer = setTimeout(() => controller.abort(), 30000);
446
+ let arrayBuffer;
447
+
448
+ try {
449
+ const response = await fetch(url, { signal: controller.signal });
450
+ if (!response.ok) {
451
+ throw new Error(`HTTP ${response.status}`);
452
+ }
453
+ arrayBuffer = await response.arrayBuffer();
454
+ } finally {
455
+ clearTimeout(timer);
456
+ }
457
+
458
+ buffer = await getSharedAudioContext().decodeAudioData(arrayBuffer);
459
+ g_ringSoundCache.set(url, buffer);
460
+ }
461
+ g_ringSoundBuffers.set(i + 1, buffer);
462
+ } catch (e) {
463
+ g_ringSoundBuffers.delete(i + 1);
464
+ console.warn(`ringSound load error (${name}): ${e}`);
465
+ }
466
+ }));
467
+ };
468
+
282
469
  /**
283
470
  * 音源の再生準備完了を待つ
284
471
  * - g_audio が Audio要素の場合は canplaythrough/error イベントの発火を待つ
@@ -383,8 +570,12 @@ const prepareScoreData = (_retryFlg = false) => {
383
570
  // フレーム・曲開始位置調整
384
571
  let preblankFrame = 0;
385
572
  if (g_scoreObj.frameNum === 0) {
386
- if (firstArrowFrame - g_limitObj.adjustment < arrivalFrame) {
387
- preblankFrame = arrivalFrame - firstArrowFrame + g_limitObj.adjustment;
573
+ // 矢印・リングノートそれぞれについて、出現が開始前になる場合の補完フレーム数を求め、大きい方を採用
574
+ const arrowLead = firstArrowFrame - g_limitObj.adjustment < arrivalFrame
575
+ ? arrivalFrame - firstArrowFrame + g_limitObj.adjustment : 0;
576
+ const ringLead = getRingLeadFrame(g_scoreObj);
577
+ if (arrowLead > 0 || ringLead > 0) {
578
+ preblankFrame = Math.max(arrowLead, ringLead);
388
579
 
389
580
  // 譜面データの再読み込み
390
581
  const noteExistObj = {
@@ -447,6 +638,8 @@ const prepareScoreData = (_retryFlg = false) => {
447
638
  g_allFrz = 0;
448
639
  g_scoreObj.arrowData.forEach(data => g_allArrow += data.length);
449
640
  g_scoreObj.frzData.forEach(data => g_allFrz += Math.floor(data.length / 2) * 2);
641
+ // リングノート数を矢印数に加算 (ライフ回復量・パーフェクト判定等の母数に含める)
642
+ g_allArrow += Math.floor((g_scoreObj.ringData?.length ?? 0) / g_ringObj.cols);
450
643
 
451
644
  // ライフ回復・ダメージ量の計算
452
645
  // フリーズ始点でも通常判定させる場合は総矢印数を水増しする
@@ -978,6 +1171,42 @@ const scoreConvert = (_dosObj, _scoreId, _preblankFrame, _dummyNo = ``,
978
1171
  return [];
979
1172
  };
980
1173
 
1174
+ /**
1175
+ * リングノートデータの分解 (1行1ノート)
1176
+ * 入力: 到達フレーム, X, Y, [音番号], [半径], [色], [出現~到達フレーム数]
1177
+ * 出力: g_ringObj.cols 個で1セットの単純配列
1178
+ * [到達フレーム, X, Y, 音番号, 半径, 色, 出現~到達フレーム数(0:未指定)]
1179
+ * @param {string} _scoreNo
1180
+ * @returns {any[]}
1181
+ */
1182
+ const setRingData = _scoreNo => {
1183
+ const dosRingData = getRefData(`ring`, `${_scoreNo}_data`);
1184
+ if (!hasVal(dosRingData)) {
1185
+ return [];
1186
+ }
1187
+ const ringData = [];
1188
+ const getNum = (_val, _default, _condition = true, _default2 = _default) =>
1189
+ hasVal(_val) && !isNaN(parseFloat(_val))
1190
+ ? (_condition ? parseFloat(_val) : _default2)
1191
+ : _default;
1192
+
1193
+ splitLF(dosRingData).filter(data => hasVal(data)).forEach(tmpData => {
1194
+ const t = tmpData.split(`,`).map(val => trimStr(val));
1195
+ if ([0, 1, 2].some(k => isNaN(parseFloat(t[k])))) {
1196
+ console.warn(`ring_data: 到達フレーム,X,Y は必須です: ${tmpData}`);
1197
+ return;
1198
+ }
1199
+ ringData.push([
1200
+ calcFrame(t[0]), parseFloat(t[1]), parseFloat(t[2]),
1201
+ getNum(t[3], g_ringObj.sound),
1202
+ getNum(t[4], g_ringObj.radius, t[4] >= g_ringObj.minRadius, g_ringObj.minRadius),
1203
+ hasVal(t[5]) ? t[5] : (g_headerObj.baseBrightFlg ? g_ringObj.colorLight : g_ringObj.color),
1204
+ getNum(t[6], 0),
1205
+ ]);
1206
+ });
1207
+ return ringData.sort((_a, _b) => _a[0] - _b[0]).flat();
1208
+ };
1209
+
981
1210
  /**
982
1211
  * 譜面データに別の関連名が含まれていた場合、関連名の変数を返す
983
1212
  * 例) |backA2_data=back_data| -> back_dataで定義された値を使用
@@ -1181,6 +1410,10 @@ const scoreConvert = (_dosObj, _scoreId, _preblankFrame, _dummyNo = ``,
1181
1410
  obj.ncolorDummyData = setColor2Data(`ncolor`, _dummyNo);
1182
1411
  }
1183
1412
 
1413
+ // リングノート・リング速度変化の分解 (速度変化は既存のsetSpeedDataを流用)
1414
+ obj.ringSpeedData = setSpeedData(`ringspeed`, scoreIdHeader);
1415
+ obj.ringData = setRingData(scoreIdHeader);
1416
+
1184
1417
  if (_scoreAnalyzeFlg) {
1185
1418
  return obj;
1186
1419
  }
@@ -1310,6 +1543,12 @@ const getLastFrame = (_dataObj, _keyCtrlPtn = `${g_keyObj.currentKey}_${g_keyObj
1310
1543
  }
1311
1544
  });
1312
1545
  }
1546
+
1547
+ // リングノートの最終到達フレーム (到達フレーム昇順・6要素1セット)
1548
+ const ringData = _dataObj.ringData ?? [];
1549
+ if (ringData.length >= g_ringObj.cols) {
1550
+ tmpLastNum = Math.max(tmpLastNum, ringData[ringData.length - g_ringObj.cols]);
1551
+ }
1313
1552
  return tmpLastNum;
1314
1553
  };
1315
1554
 
@@ -1317,9 +1556,10 @@ const getLastFrame = (_dataObj, _keyCtrlPtn = `${g_keyObj.currentKey}_${g_keyObj
1317
1556
  * 最初の矢印フレームの取得
1318
1557
  * @param {object} _dataObj
1319
1558
  * @param {string} [_keyCtrlPtn]
1559
+ * @param {boolean} [_includeRing=false] リングノートも含めるか
1320
1560
  * @returns {number}
1321
1561
  */
1322
- const getFirstArrowFrame = (_dataObj, _keyCtrlPtn = `${g_keyObj.currentKey}_${g_keyObj.currentPtn}`) => {
1562
+ const getFirstArrowFrame = (_dataObj, _keyCtrlPtn = `${g_keyObj.currentKey}_${g_keyObj.currentPtn}`, _includeRing = false) => {
1323
1563
 
1324
1564
  let tmpFirstNum = Infinity;
1325
1565
  const keyNum = g_keyObj[`${g_keyObj.defaultProp}${_keyCtrlPtn}`].length;
@@ -1338,7 +1578,16 @@ const getFirstArrowFrame = (_dataObj, _keyCtrlPtn = `${g_keyObj.currentKey}_${g_
1338
1578
  }
1339
1579
  });
1340
1580
  }
1341
- return (tmpFirstNum === Infinity ? 0 : tmpFirstNum);
1581
+ if (_includeRing) {
1582
+ const ringData = _dataObj.ringData ?? [];
1583
+ for (let k = 0; k + g_ringObj.cols <= ringData.length; k += g_ringObj.cols) {
1584
+ const frame = ringData[k];
1585
+ if (Number.isFinite(frame)) {
1586
+ tmpFirstNum = Math.min(tmpFirstNum, frame);
1587
+ }
1588
+ }
1589
+ }
1590
+ return (tmpFirstNum === Infinity ? g_headerObj.blankFrame : tmpFirstNum);
1342
1591
  };
1343
1592
 
1344
1593
  /**
@@ -1652,6 +1901,27 @@ const pushArrows = (_dataObj, _speedOnFrame, _firstArrivalFrame) => {
1652
1901
  calcNotes(j, _dataObj.dummyFrzData[j], `dummyFrz`, true);
1653
1902
  }
1654
1903
 
1904
+ // リングノートの出現フレーム別格納 (矢印とは独立のため速度計算は不要)
1905
+ // - 開始フレーム前に到達済みのものは除外
1906
+ // - 出現が開始フレーム前のもの(フェードイン時)は、開始フレームから途中の大きさで出現させる
1907
+ g_workObj.mkRing = [];
1908
+ g_workObj.ringArrivals = []; // 通し番号 -> 到達フレーム (到達順)
1909
+ const ringData = _dataObj.ringData ?? [];
1910
+ const cols = g_ringObj.cols;
1911
+ for (let k = 0; k < ringData.length; k += cols) {
1912
+ const [arrival, x, y, sound, radius, color, frames] = ringData.slice(k, k + cols);
1913
+ if (arrival < g_scoreObj.frameNum) {
1914
+ continue;
1915
+ }
1916
+ const total = getRingDuration(_dataObj.ringSpeedData, arrival, frames);
1917
+ const appear = Math.max(arrival - total, g_scoreObj.frameNum);
1918
+ (g_workObj.mkRing[appear] ??= []).push({
1919
+ x, y, radius, color, sound, total, arrivalFrame: arrival - appear,
1920
+ arrival, seq: g_workObj.ringArrivals.length,
1921
+ });
1922
+ g_workObj.ringArrivals.push(arrival);
1923
+ }
1924
+
1655
1925
  /**
1656
1926
  * 色変化・モーションデータ・スクロール反転データのタイミング更新
1657
1927
  * - この関数を使用する場合、配列グループの先頭2つが「フレーム数、矢印番号」となっていないと動作しない
@@ -1865,6 +2135,50 @@ const getFrzLength = (_speedOnFrame, _startFrame, _endFrame) => {
1865
2135
  return frzLength;
1866
2136
  };
1867
2137
 
2138
+ /**
2139
+ * 指定フレーム時点で有効なリング速度の取得
2140
+ * @param {number[]} _ringSpeedData [フレーム数, 速度, ...] (フレーム昇順)
2141
+ * @param {number} _frame
2142
+ * @returns {number}
2143
+ */
2144
+ const getRingSpeed = (_ringSpeedData, _frame) => {
2145
+ let speed = 1;
2146
+ for (let k = 0; k < (_ringSpeedData?.length ?? 0); k += 2) {
2147
+ if (_ringSpeedData[k] > _frame) {
2148
+ break;
2149
+ }
2150
+ speed = _ringSpeedData[k + 1];
2151
+ }
2152
+ return speed > 0 ? speed : 1;
2153
+ };
2154
+
2155
+ /**
2156
+ * リングの出現~到達フレーム数
2157
+ * - 個別指定(_frames > 0)があればそれを優先、なければ到達フレーム時点のリング速度に反比例
2158
+ * - リング速度オプション(g_stateObj.ringSpeed)はどちらにも掛かる
2159
+ */
2160
+ const getRingDuration = (_ringSpeedData, _arrivalFrame, _frames = 0) => {
2161
+ const base = _frames > 0 ? _frames : g_ringObj.baseFrame / getRingSpeed(_ringSpeedData, _arrivalFrame);
2162
+ return Math.max(Math.round(base / g_stateObj.ringSpeed), 1);
2163
+ };
2164
+
2165
+ /**
2166
+ * リングを0フレーム以降に出現させるために必要な補完フレーム数
2167
+ * (矢印のプレブランク計算と同様、Adjustment分の余裕を含める)
2168
+ */
2169
+ const getRingLeadFrame = _dataObj => {
2170
+ const ringData = _dataObj.ringData ?? [];
2171
+ let lead = 0;
2172
+ const cols = g_ringObj.cols;
2173
+ for (let k = 0; k < ringData.length; k += cols) {
2174
+ const duration = getRingDuration(_dataObj.ringSpeedData, ringData[k], ringData[k + cols - 1]);
2175
+ if (ringData[k] - g_limitObj.adjustment < duration) {
2176
+ lead = Math.max(lead, duration - ringData[k] + g_limitObj.adjustment);
2177
+ }
2178
+ }
2179
+ return lead;
2180
+ };
2181
+
1868
2182
  /**
1869
2183
  * キーパターン(デフォルト)に対応する矢印番号を格納
1870
2184
  * - 色変化、矢印・フリーズアローモーション、スクロール変化で
@@ -2835,13 +3149,21 @@ class AudioPlayer {
2835
3149
  }
2836
3150
 
2837
3151
  // グローバルで1つだけ保持(遅延生成)
3152
+ let g_sharedAudioResumePending = false;
3153
+ let g_sharedAudioResumeNextAt = 0;
2838
3154
  const getSharedAudioContext = () => {
2839
3155
  if (!g_sharedAudioContext) {
2840
3156
  g_sharedAudioContext = new AudioContext();
2841
3157
  }
2842
- // タブのバックグラウンド化等でsuspendedになることがあるため念のためresume
2843
- if (g_sharedAudioContext.state === `suspended`) {
2844
- g_sharedAudioContext.resume();
3158
+ // 復帰待ちのPromiseは重複させず、失敗時の再試行は1秒以上空ける。
3159
+ const now = performance.now();
3160
+ if ((g_sharedAudioContext.state === `suspended` || g_sharedAudioContext.state === `interrupted`) &&
3161
+ !g_sharedAudioResumePending && now >= g_sharedAudioResumeNextAt) {
3162
+ g_sharedAudioResumePending = true;
3163
+ g_sharedAudioResumeNextAt = now + 1000;
3164
+ g_sharedAudioContext.resume().catch(() => { }).finally(() => {
3165
+ g_sharedAudioResumePending = false;
3166
+ });
2845
3167
  }
2846
3168
  return g_sharedAudioContext;
2847
3169
  };
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Source by tickle
7
7
  * Created : 2026/09/13
8
- * Revised : 2026/10/12 (v51.4.0)
8
+ * Revised : 2026/11/08 (v52.2.1)
9
9
  *
10
10
  * https://github.com/cwtickle/danoniplus
11
11
  */
@@ -502,6 +502,8 @@ const loadLocalStorage = (_musicId = ``) => {
502
502
  checkLocalParam(`appearance`);
503
503
  checkLocalParam(`opacity`, C_TYP_NUMBER, g_settings.opacitys.length - 1);
504
504
  checkLocalParam(`hitPosition`, C_TYP_FLOAT, g_settings.hitPositionNum);
505
+ checkLocalParam(`ringSpeed`, C_TYP_FLOAT, Math.max(g_settings.ringSpeeds.indexOf(g_stateObj.ringSpeed), 0));
506
+ checkLocalParam(`ringGuide`);
505
507
 
506
508
  // ハイスコア取得準備
507
509
  if (g_localStorage.highscores === undefined) {
@@ -645,12 +647,12 @@ const getFullMusicUrl = (_musicUrl = ``) => {
645
647
  const storeBaseData = (_scoreId, _scoreObj, _keyCtrlPtn) => {
646
648
  const lastFrame = getLastFrame(_scoreObj, _keyCtrlPtn) + 1;
647
649
  const startFrame = getStartFrame(lastFrame, 0, _scoreId);
648
- const firstArrowFrame = getFirstArrowFrame(_scoreObj, _keyCtrlPtn);
650
+ const firstArrowFrame = getFirstArrowFrame(_scoreObj, _keyCtrlPtn, true);
649
651
  const playingFrame = lastFrame - firstArrowFrame;
650
652
  const keyNum = g_keyObj[`${g_keyObj.defaultProp}${_keyCtrlPtn}`].length;
651
653
 
652
654
  // 譜面密度グラフ用のデータ作成
653
- const noteCnt = { arrow: [], frz: [] };
655
+ const noteCnt = { arrow: [], frz: [], ring: Math.floor((_scoreObj.ringData?.length ?? 0) / g_ringObj.cols) };
654
656
  const densityData = fillArray(g_limitObj.densityDivision);
655
657
  let allData = 0;
656
658
 
@@ -675,6 +677,20 @@ const storeBaseData = (_scoreId, _scoreObj, _keyCtrlPtn) => {
675
677
  }));
676
678
  fullData = fullData.concat(..._scoreObj.arrowData[j], ...tmpFrzData);
677
679
  }
680
+ if (_scoreObj.ringData !== undefined) {
681
+ const ringData = _scoreObj.ringData.filter((data, k) => k % g_ringObj.cols === 0);
682
+ ringData.forEach(note => {
683
+ if (isNaN(parseFloat(note))) {
684
+ return;
685
+ }
686
+ const point = Math.floor((note - firstArrowFrame) / playingFrame * g_limitObj.densityDivision);
687
+ if (point >= 0) {
688
+ densityData[point]++;
689
+ allData++;
690
+ }
691
+ });
692
+ fullData = fullData.concat(ringData ?? []);
693
+ }
678
694
 
679
695
  fullData = fullData.filter(val => !isNaN(parseFloat(val))).sort((a, b) => a - b);
680
696
  let pushCnt = 1;
@@ -725,6 +741,7 @@ const storeBaseData = (_scoreId, _scoreObj, _keyCtrlPtn) => {
725
741
 
726
742
  g_detailObj.arrowCnt[_scoreId] = noteCnt.arrow.concat();
727
743
  g_detailObj.frzCnt[_scoreId] = noteCnt.frz.map((val, k) => _scoreObj.frzData[k].length % 2 === 0 ? val : val - 0.5);
744
+ g_detailObj.ringCnt[_scoreId] = noteCnt.ring;
728
745
  g_detailObj.startFrame[_scoreId] = startFrame;
729
746
  g_detailObj.playingFrame[_scoreId] = playingFrame;
730
747
  g_detailObj.playingFrameWithBlank[_scoreId] = lastFrame - startFrame;
@@ -740,11 +757,13 @@ const storeBaseData = (_scoreId, _scoreObj, _keyCtrlPtn) => {
740
757
  timeMargin: 35,
741
758
  mmWidthBase: (g_sWidth - 500) / 2 + 290,
742
759
  mmMarginY: 2,
760
+ // リングノート用の帯 (リングがある譜面のみ。余白4px + 帯28px)
761
+ ringBandWidth: noteCnt.ring > 0 ? 32 : 0,
743
762
  get laneWidth() {
744
- return Math.min((this.mmWidthBase - this.timeMargin) / keyNum, 40);
763
+ return Math.min((this.mmWidthBase - this.timeMargin - this.ringBandWidth) / keyNum, 40);
745
764
  },
746
765
  get logicalWidth() {
747
- const logicalWidth = this.timeMargin + (this.laneWidth * keyNum);
766
+ const logicalWidth = this.timeMargin + (this.laneWidth * keyNum) + this.ringBandWidth;
748
767
  return Math.ceil(logicalWidth * g_dpr) / g_dpr;
749
768
  }
750
769
  },
@@ -1370,6 +1389,11 @@ const headerConvert = _dosObj => {
1370
1389
  // カラーコードのゼロパディング有無設定
1371
1390
  obj.colorCdPaddingUse = setBoolVal(_dosObj.colorCdPaddingUse);
1372
1391
 
1392
+ // 明背景の場合はガイドリング色を明背景用に切り替える
1393
+ if (obj.baseBrightFlg) {
1394
+ g_ringObj.guideColor = g_ringObj.guideColorLight;
1395
+ }
1396
+
1373
1397
  // 最大ライフ
1374
1398
  obj.maxLifeVal = setVal(_dosObj.maxLifeVal, C_VAL_MAXLIFE, C_TYP_FLOAT);
1375
1399
  if (obj.maxLifeVal <= 0) {
@@ -1521,6 +1545,9 @@ const headerConvert = _dosObj => {
1521
1545
  makeWarningWindow(g_msgInfoObj.E_0031);
1522
1546
  }
1523
1547
 
1548
+ // リング音の設定
1549
+ obj.ringSound = _dosObj.ringSound ?? ``;
1550
+
1524
1551
  // ハッシュタグ
1525
1552
  obj.hashTag = _dosObj.hashTag ?? ``;
1526
1553