修复:管理员进房烟花无声问题(AudioContext suspended)

根本原因:管理员进房特效在 800ms 后自动触发,
此时用户尚未与新页面交互,浏览器的 AudioContext
处于 suspended 状态,之前代码同步调用 resume()
但未 await 其 Promise,导致音频节点创建后无法出声。

修复方式:
- play() 和 ding() 均改为先检查 ctx.state
- 若为 suspended,用 ctx.resume().then(...) 链式执行
- resolver 成功后真正创建音频节点并播放
- 若浏览器拒绝 resume(无用户手势),catch 静默处理

此修复使所有自动触发的音效(进房烟花、任命公告等)
在 AudioContext 未激活时也能正确播放。
This commit is contained in:
2026-03-01 13:32:00 +08:00
parent 58b63fa8d3
commit 48b31e7cff
+88 -61
View File
@@ -397,6 +397,8 @@ const EffectSounds = (() => {
/**
* 播放指定特效对应的音效(自动停止上一个)。
* 静音状态下直接跳过,不做任何音频操作。
* 当 AudioContext 处于 suspended 状态时,先 resume() 再播放,
* 解决页面无用户手势时的自动静音问题(如管理员进房自动烟花)。
*
* @param {string} type 'lightning' | 'fireworks' | 'rain' | 'snow'
*/
@@ -404,22 +406,42 @@ const EffectSounds = (() => {
// 用户开启禁音则跳过
if (localStorage.getItem("chat_sound_muted") === "1") return;
stop();
try {
switch (type) {
case "lightning":
_stopFn = _startLightning();
break;
case "fireworks":
_stopFn = _startFireworks();
break;
case "rain":
_stopFn = _startRain();
break;
case "snow":
_stopFn = _startSnow();
break;
default:
break;
const ctx = _getCtx();
const _doPlay = () => {
try {
switch (type) {
case "lightning":
_stopFn = _startLightning();
break;
case "fireworks":
_stopFn = _startFireworks();
break;
case "rain":
_stopFn = _startRain();
break;
case "snow":
_stopFn = _startSnow();
break;
default:
break;
}
} catch (e) {
console.warn("[EffectSounds] 音效内部错误:", e);
}
};
if (ctx.state === "suspended") {
// AudioContext 尚未被用户手势激活,先 resume 再播放
ctx.resume()
.then(_doPlay)
.catch(() => {
// 浏览器拒绝 resume(无用户手势),静默处理
});
} else {
_doPlay();
}
} catch (e) {
console.warn(
@@ -442,54 +464,59 @@ const EffectSounds = (() => {
if (localStorage.getItem("chat_sound_muted") === "1") return;
try {
const ctx = _getCtx();
const master = ctx.createGain();
master.gain.value = 0.45;
master.connect(ctx.destination);
/**
* 播放单音:快速冲击 + 铃铛衰减包络
*
* @param {number} freq 频率(Hz
* @param {number} t0 相对于 ctx.currentTime 的时刻
* @param {number} decay 衰减时长(秒)
*/
function _tone(freq, t0, decay) {
const osc = ctx.createOscillator();
osc.type = "sine";
osc.frequency.value = freq;
// 轻柔泛音(×2.76 铃铛泛音比,音量 10%)
const osc2 = ctx.createOscillator();
osc2.type = "sine";
osc2.frequency.value = freq * 2.76;
const g2 = ctx.createGain();
g2.gain.value = 0.1;
const env = ctx.createGain();
env.gain.setValueAtTime(1.0, t0);
env.gain.exponentialRampToValueAtTime(0.001, t0 + decay);
osc.connect(env);
osc2.connect(g2);
g2.connect(env);
env.connect(master);
osc.start(t0);
osc.stop(t0 + decay + 0.05);
osc2.start(t0);
osc2.stop(t0 + decay + 0.05);
}
const now = ctx.currentTime;
_tone(880, now, 0.35); // 叮:A5,先响
_tone(659, now + 0.11, 0.4); // 咚:E5,稍低稍长
// 0.7 秒后释放主节点
setTimeout(() => {
const _doDing = () => {
try {
master.disconnect();
} catch (_) {}
}, 700);
const master = ctx.createGain();
master.gain.value = 0.45;
master.connect(ctx.destination);
function _tone(freq, t0, decay) {
const osc = ctx.createOscillator();
osc.type = "sine";
osc.frequency.value = freq;
const osc2 = ctx.createOscillator();
osc2.type = "sine";
osc2.frequency.value = freq * 2.76;
const g2 = ctx.createGain();
g2.gain.value = 0.1;
const env = ctx.createGain();
env.gain.setValueAtTime(1.0, t0);
env.gain.exponentialRampToValueAtTime(
0.001,
t0 + decay,
);
osc.connect(env);
osc2.connect(g2);
g2.connect(env);
env.connect(master);
osc.start(t0);
osc.stop(t0 + decay + 0.05);
osc2.start(t0);
osc2.stop(t0 + decay + 0.05);
}
const now = ctx.currentTime;
_tone(880, now, 0.35); // 叮:A5
_tone(659, now + 0.11, 0.4); // 咚:E5
setTimeout(() => {
try {
master.disconnect();
} catch (_) {}
}, 700);
} catch (e) {
console.warn("[EffectSounds.ding] 通知音内部错误:", e);
}
};
if (ctx.state === "suspended") {
ctx.resume()
.then(_doDing)
.catch(() => {});
} else {
_doDing();
}
} catch (e) {
console.warn("[EffectSounds.ding] 通知音播放失败:", e);
}