diff --git a/public/js/effects/effect-sounds.js b/public/js/effects/effect-sounds.js index ee82891..35d2b44 100644 --- a/public/js/effects/effect-sounds.js +++ b/public/js/effects/effect-sounds.js @@ -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); }