0.2.6-dev: no BGM download while the music is muted or at BGM volume 0; the wanted track is fetched once it is audible again (host bandwidth layer 0.1); golden: unchanged
This commit is contained in:
1 parent
a8ff5e7c1d
commit
69011d2b54
3 files changed
+79
-3
No files matched your search
@@ -62,7 +62,7 @@ its board-space position and facing, including on a mirrored boss half. They can
|
|||||||
from this preview. The inspection affordance does not make the preview a combat unit; the battle sim still creates
|
from this preview. The inspection affordance does not make the preview a combat unit; the battle sim still creates
|
||||||
the selected variant at battle start (§30.17).
|
the selected variant at battle start (§30.17).
|
||||||
|
|
||||||
Audio: autochess BGM per phase, UI SFX (buy/sell/refresh/level/merge/ready/timer), per-unit attack/hit SFX (throttled; an impact plays for a hostile attack's phys / arts / true damage only, never an operator's skill-mode file for a normal attack, ≤ 2 copies of one battle sound — §18.4; a skill cue that reaches the client before its unit — the sim emits a deploy-tick cast ahead of the unit's `['spawn']` — is held and sounds once when the unit is tracked, `AudioManager.pendingSkill`, GitHub PR #292 by @LimitlessHPPK; entering a field late hands the sound the buffered `spawn` tuples only, `screens/game/early.js audioEarly`, so no old death, deploy or cast replays), boss music in R14/15.
|
Audio: autochess BGM per phase, UI SFX (buy/sell/refresh/level/merge/ready/timer), per-unit attack/hit SFX (throttled; an impact plays for a hostile attack's phys / arts / true damage only, never an operator's skill-mode file for a normal attack, ≤ 2 copies of one battle sound — §18.4; a skill cue that reaches the client before its unit — the sim emits a deploy-tick cast ahead of the unit's `['spawn']` — is held and sounds once when the unit is tracked, `AudioManager.pendingSkill`, GitHub PR #292 by @LimitlessHPPK; entering a field late hands the sound the buffered `spawn` tuples only, `screens/game/early.js audioEarly`, so no old death, deploy or cast replays), boss music in R14/15. While the music is silent — muted, or the BGM volume at 0 — no BGM is downloaded: the track the screen wants is remembered, a different one still playing stops, and the wanted track is fetched once the music is audible again (`AudioManager.bgmSilent`; about 14 MB a match for a player who turned the music off).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+21
-2
@@ -13,7 +13,9 @@
|
|||||||
// never block or warn; everything requested before that is remembered (BGM) or dropped (SFX).
|
// never block or warn; everything requested before that is remembered (BGM) or dropped (SFX).
|
||||||
// - Channels: master → { bgm, sfx, voice } gains; volumes from settings (0..1) + mute. Tab hidden ⇒ suspend.
|
// - Channels: master → { bgm, sfx, voice } gains; volumes from settings (0..1) + mute. Tab hidden ⇒ suspend.
|
||||||
// - BGM: `intro` then `loop` (1 s crossfade); switching tracks fades out/in (0.8 s). The same loop URL
|
// - BGM: `intro` then `loop` (1 s crossfade); switching tracks fades out/in (0.8 s). The same loop URL
|
||||||
// keeps playing across phases (prep and combat share a track).
|
// keeps playing across phases (prep and combat share a track). While the music is silent — muted, or the BGM volume
|
||||||
|
// at 0 — no track is downloaded (≈ 14 MB a match): the wanted one is remembered, another one playing stops, and the
|
||||||
|
// track the screen wants then starts once the music is audible again (`setVolumes`; host bandwidth, layer 0).
|
||||||
// - 开战 BGM: `bgm.combatAlts` are the mode's own battle tracks (塞壬唱片 骑士之日 / 无畏者). The track is fixed per
|
// - 开战 BGM: `bgm.combatAlts` are the mode's own battle tracks (塞壬唱片 骑士之日 / 无畏者). The track is fixed per
|
||||||
// round, not drawn: 无畏者 through rounds 1–7 and 骑士之日 from round 8 on (`combatTrackFor`, the official
|
// round, not drawn: 无畏者 through rounds 1–7 and 骑士之日 from round 8 on (`combatTrackFor`, the official
|
||||||
// schedule), so every client of a match hears the same one, a fight never switches track halfway through and the
|
// schedule), so every client of a match hears the same one, a fight never switches track halfway through and the
|
||||||
@@ -662,6 +664,7 @@ export class AudioManager {
|
|||||||
*/
|
*/
|
||||||
setVolumes(v) {
|
setVolumes(v) {
|
||||||
const n = (x, d) => (Number.isFinite(x) ? Math.max(0, Math.min(1, x)) : d);
|
const n = (x, d) => (Number.isFinite(x) ? Math.max(0, Math.min(1, x)) : d);
|
||||||
|
const wasSilent = this.bgmSilent;
|
||||||
this.volumes = {
|
this.volumes = {
|
||||||
bgm: n(v?.bgm, this.volumes.bgm),
|
bgm: n(v?.bgm, this.volumes.bgm),
|
||||||
sfx: n(v?.sfx, this.volumes.sfx),
|
sfx: n(v?.sfx, this.volumes.sfx),
|
||||||
@@ -670,8 +673,13 @@ export class AudioManager {
|
|||||||
};
|
};
|
||||||
this._applyVolumes();
|
this._applyVolumes();
|
||||||
if (this.volumes.muted || this.volumes.sfx <= 0) this.stopBondCue();
|
if (this.volumes.muted || this.volumes.sfx <= 0) this.stopBondCue();
|
||||||
|
// the music became audible again: download and start the track the screen wants now (none was fetched meanwhile)
|
||||||
|
if (wasSilent && !this.bgmSilent && this.ctx && this.wantBgm) { const k = this.wantBgm; this.wantBgm = null; this.playBgm(k); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Is the music silent — muted, or the BGM volume at 0? Then no BGM is downloaded (playBgm). */
|
||||||
|
get bgmSilent() { return this.volumes.muted || this.volumes.bgm <= 0; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The voice dub (settings 语音语言): 'jp' plays `audio.voiceJp`, anything else `audio.voice` (中文, the default). The
|
* The voice dub (settings 语音语言): 'jp' plays `audio.voiceJp`, anything else `audio.voice` (中文, the default). The
|
||||||
* line on air finishes in its own dub; the next one follows the setting.
|
* line on air finishes in its own dub; the next one follows the setting.
|
||||||
@@ -766,12 +774,23 @@ export class AudioManager {
|
|||||||
// ---- BGM ------------------------------------------------------------------------------------------------
|
// ---- BGM ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Switch BGM (null stops). Same loop URL ⇒ no restart.
|
* Switch BGM (null stops). Same loop URL ⇒ no restart. Before the first gesture, or while the music is silent
|
||||||
|
* (`bgmSilent`), the key is only remembered (`wantBgm`) and nothing is downloaded; a different track that is still
|
||||||
|
* playing stops. `_unlock` / `setVolumes` start the remembered track once it can be heard.
|
||||||
* @param {string|null} key see bgmKeyFor
|
* @param {string|null} key see bgmKeyFor
|
||||||
*/
|
*/
|
||||||
playBgm(key) {
|
playBgm(key) {
|
||||||
try {
|
try {
|
||||||
if (!this.ctx) { this.wantBgm = key; return; }
|
if (!this.ctx) { this.wantBgm = key; return; }
|
||||||
|
if (this.bgmSilent) {
|
||||||
|
this.wantBgm = key;
|
||||||
|
const silentTrack = resolveBgm(this.getManifest(), key);
|
||||||
|
if (this.bgm && silentTrack && this.bgm.loopUrl === silentTrack.loop) { this.bgm.key = key; return; }
|
||||||
|
++this.bgmToken; // a start still waiting for its download stays silent too
|
||||||
|
this._fadeOutBgm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.wantBgm = null;
|
||||||
const track = resolveBgm(this.getManifest(), key);
|
const track = resolveBgm(this.getManifest(), key);
|
||||||
if (this.bgm && track && this.bgm.loopUrl === track.loop) { this.bgm.key = key; return; }
|
if (this.bgm && track && this.bgm.loopUrl === track.loop) { this.bgm.key = key; return; }
|
||||||
if (!track && !this.bgm) return;
|
if (!track && !this.bgm) return;
|
||||||
|
|||||||
@@ -577,6 +577,63 @@ describe('AudioManager', () => {
|
|||||||
globalThis.fetch = origFetch;
|
globalThis.fetch = origFetch;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
test('BGM is not downloaded while the music is muted or at volume 0; the wanted track starts once it is audible', async () => {
|
||||||
|
const fw = fakeWindow();
|
||||||
|
const origFetch = globalThis.fetch;
|
||||||
|
const urls = [];
|
||||||
|
globalThis.fetch = async (u) => { urls.push(u); return { ok: true, arrayBuffer: async () => new ArrayBuffer(8) }; };
|
||||||
|
const tick = () => new Promise((r) => setTimeout(r, 10));
|
||||||
|
const lobby = manifest.audio.bgm.lobby.loop;
|
||||||
|
const prep = manifest.audio.bgm.prep.loop;
|
||||||
|
assert.notEqual(lobby, prep, 'premise: two different loops');
|
||||||
|
try {
|
||||||
|
const a = new AudioManager({ win: fw.win, getManifest: () => manifest });
|
||||||
|
a.setVolumes({ muted: true });
|
||||||
|
a.install();
|
||||||
|
a.playBgm('lobby');
|
||||||
|
fw.fire('pointerdown');
|
||||||
|
await tick();
|
||||||
|
assert.equal(a.unlocked, true);
|
||||||
|
assert.equal(a.bgmSilent, true);
|
||||||
|
assert.ok(!asked(urls, lobby) && !asked(urls, manifest.audio.bgm.lobby.intro), 'muted: nothing fetched at unlock');
|
||||||
|
a.playBgm('prep');
|
||||||
|
await tick();
|
||||||
|
assert.ok(!asked(urls, prep), 'muted: a phase change fetches nothing');
|
||||||
|
assert.equal(a.wantBgm, 'prep', 'the wanted track is remembered');
|
||||||
|
a.setVolumes({ muted: false });
|
||||||
|
await tick();
|
||||||
|
assert.ok(asked(urls, prep), 'unmuted: the track the screen wants now is fetched');
|
||||||
|
assert.ok(!asked(urls, lobby), 'never the track wanted before');
|
||||||
|
assert.equal(a.bgm?.loopUrl, prep);
|
||||||
|
|
||||||
|
// BGM volume 0 is silent too: another track stops and is not fetched; SFX keep playing
|
||||||
|
a.setVolumes({ bgm: 0 });
|
||||||
|
a.playBgm('lobby');
|
||||||
|
await tick();
|
||||||
|
assert.ok(!asked(urls, lobby), 'BGM volume 0: no download');
|
||||||
|
assert.equal(a.bgm, null, 'the other track stopped');
|
||||||
|
a.sfx('buy');
|
||||||
|
await tick();
|
||||||
|
assert.ok(asked(urls, manifest.audio.sfx.ui.buy), 'SFX are not gated by the BGM volume');
|
||||||
|
a.setVolumes({ bgm: 0.5 });
|
||||||
|
await tick();
|
||||||
|
assert.ok(asked(urls, lobby), 'audible again: fetched');
|
||||||
|
assert.equal(a.bgm?.loopUrl, lobby);
|
||||||
|
|
||||||
|
// the loop that already plays keeps playing through a silent phase change, and is not fetched again
|
||||||
|
a.setVolumes({ muted: true });
|
||||||
|
const n = urls.length;
|
||||||
|
const started = fw.made.started;
|
||||||
|
a.playBgm('lobby');
|
||||||
|
a.setVolumes({ muted: false });
|
||||||
|
await tick();
|
||||||
|
assert.equal(a.bgm?.loopUrl, lobby);
|
||||||
|
assert.equal(urls.length, n, 'no new request');
|
||||||
|
assert.equal(fw.made.started, started, 'no restart');
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = origFetch;
|
||||||
|
}
|
||||||
|
});
|
||||||
test('fetch failures are swallowed', async () => {
|
test('fetch failures are swallowed', async () => {
|
||||||
const fw = fakeWindow();
|
const fw = fakeWindow();
|
||||||
const origFetch = globalThis.fetch;
|
const origFetch = globalThis.fetch;
|
||||||
|
|||||||
Reference in new issue
Block a user