diff --git a/public/js/effects/effect-sounds.js b/public/js/effects/effect-sounds.js index 3b61c93..ee82891 100644 --- a/public/js/effects/effect-sounds.js +++ b/public/js/effects/effect-sounds.js @@ -6,6 +6,7 @@ * 对外 API: * EffectSounds.play(type) 播放指定特效的背景音效 * EffectSounds.stop() 停止并释放当前音效资源 + * EffectSounds.ding() 播放简短叮咚通知音(大/小卡片弹出时使用) * * 支持的 type: * lightning 雷鸣闪电(三层合成:放电啪声 + 低频轰鸣 + 极低频滚动余韵) @@ -428,6 +429,72 @@ const EffectSounds = (() => { } } + // ─── 叮咚通知音(大卡片 / 小卡片弹出时调用)───────────────── + + /** + * 播放简短的叮咚两音通知音效。 + * + * 音型:A5(880Hz)→ 间隔 110ms → E5(659Hz),均为正弦波, + * 快速冲击 + 铃铛式缓慢衰减,总时长约 0.5 秒。 + * 禁音状态下自动跳过。 + */ + function ding() { + 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(() => { + try { + master.disconnect(); + } catch (_) {} + }, 700); + } catch (e) { + console.warn("[EffectSounds.ding] 通知音播放失败:", e); + } + } + /** * 停止当前音效并释放资源。 */ @@ -440,5 +507,8 @@ const EffectSounds = (() => { } } - return { play, stop }; + return { play, stop, ding }; })(); + +// 将叮咚通知音暴露为独立全局变量,供 toast/banner 等组件直接调用 +window.chatSound = { ding: () => EffectSounds.ding() }; diff --git a/resources/views/chat/partials/scripts.blade.php b/resources/views/chat/partials/scripts.blade.php index ac146b9..5dc5f2b 100644 --- a/resources/views/chat/partials/scripts.blade.php +++ b/resources/views/chat/partials/scripts.blade.php @@ -822,6 +822,9 @@ function show(opts = {}) { ensureKeyframes(); + // 大卡片弹出时播放叮咚通知音 + if (window.chatSound) window.chatSound.ding(); + const id = opts.id || 'chat-banner-default'; const gradient = (opts.gradient || ['#4f46e5', '#7c3aed', '#db2777']).join(', '); const titleColor = opts.titleColor || '#fde68a'; diff --git a/resources/views/chat/partials/toast-notification.blade.php b/resources/views/chat/partials/toast-notification.blade.php index d875601..3c0f5f6 100644 --- a/resources/views/chat/partials/toast-notification.blade.php +++ b/resources/views/chat/partials/toast-notification.blade.php @@ -105,6 +105,9 @@ container.appendChild(card); + // 弹出时播放叮咚通知音 + if (window.chatSound) window.chatSound.ding(); + // 自动消失 if (duration > 0) { setTimeout(() => dismiss(card), duration);