统一音效系统
GZA 的菜单、比赛、街区、合作模式与皮肤试听共用 GameAudio。玩法提交带类型的声音事件,音色、混音路由、冷却和并发策略集中维护,业务组件不自行创建音频节点。基础 UI 组件保持展示职责,点击与输入反馈由宿主接入,见 UI 组件复用约定。
设计依据与范围
MDN Web Audio 最佳实践 建议按内容选择媒体元素或缓冲播放、在用户操作中解锁音频,并提供音量控制。本项目继续用媒体元素播放长音乐,用合成器生成短音效,通过 AudioParam 平滑调整音量;音乐和音效模块按需加载。
Unity Audio Mixer 的分类路由与状态快照,以及 AudioSource 优先级 的声部管理,提供了组织方式的参考。这里以原生 Web Audio 实现事件目录、分类增益和有界声部调度,没有引入音频中间件。下文的数量、冷却和优先级均为项目参数,不是行业统一默认值;本项目数字越大越优先,方向与 Unity 的参数不同。
当前支持合成音效、立体声方向、流式音乐、连续引擎与风声、生命周期清理和联机去重。没有实现基于世界坐标的 3D 声源、距离衰减、声部虚拟化或旁链 ducking;这些能力应在明确玩法需求后扩展。
分层与路由
| 模块 | 职责 |
|---|---|
game/audio.ts | GameAudio 门面:解锁、音量与静音、音乐/引擎、设置订阅、暂停和释放 |
game/audio/events.ts | 事件 ID、事件参数、播放结果、交互语义映射 |
game/audio/catalog.ts | 音色层、分类、优先级、冷却、实例数量和混音状态 |
game/audio/voicePool.ts | 整个事件的原子声部准入、抢占、跨作用域预算 |
game/audio/context.ts | 当前文档共享的 AudioContext、租约、白噪声缓冲 |
game/audioEffects.ts | 可选的合成节点创建、调度、短音包络和清理 |
coop/audio.ts / city-coop/audio.ts | 权威事件、本地预测与回包对齐、模式状态 |
短音效 → 目录与声部准入 → ui / world / notification 分类增益 → 滤波/压缩 → 音效音量
流式音乐 → 音乐音量
连续引擎/风声 → 引擎音量
以上三路 → 作用域主音量/静音 → 共享输出增益 → 音频设备用户设置仍保留主音量、音乐、引擎与音效四个通道,滑杆使用现有感知曲线。ui、world、notification 是音效内部分类,用于调度与状态控制,不增加设置项。音效继续经过既有滤波与压缩,音乐不经过该压缩器;共享输出使用单位增益节点。W3C Web Audio 规范 说明压缩器具有固定前瞻延迟,因此没有给已校准的音乐路径追加总线压缩器。
| 状态 | ui | world | notification | 音乐与引擎 |
|---|---|---|---|---|
| playing | 开启 | 开启 | 开启 | 按场景与用户设置播放 |
| paused | 开启 | 停止 | 开启 | 暂停/静音 |
| background | 停止 | 停止 | 停止 | 暂停/静音 |
暂停菜单可以播放导航与关闭音,结算旋律可以完成;玩法适配器仍验证当前模式是否允许产生新的通知。失焦/进入后台清除当前作用域的短音,全部持有者都进入后台后暂停共享上下文。恢复焦点不自动解锁设备,下一次真实用户操作可以重试。打开菜单的音频解锁不会自动解除游戏暂停。
声部与事件策略
一个声部是一个振荡器或缓冲源,包含延迟开始但已调度的音符。连续引擎、风声和流式音乐不计入短音效池。所有作用域共享最多 32 个短音效声部,内部分类上限为 ui: 6、world: 24、notification: 12;分类上限不代表额外预留,总量仍不超过 32。
每个事件还有实例上限,例如开火最多 8 个实例,结算最多 1 个。准入先计算整段音效所需声部,再规划同类旧实例及低优先级候选;优先替换低优先级和较旧的事件。预算仍不足时拒绝新事件,不先截断已有音效,也不播放半段旋律。允许替换的同优先级事件按旧实例优先处理。真实结束、抢占、暂停及卸载均释放声部与节点。
骑行操作优先级低于战斗确认,菜单高于常规世界音效,紧急标记与追逐提示更高,结算最高。常规命中、菜单导航等使用短冷却,持续按住的驾驶输入仅在按下边沿播放。打开和关闭菜单分别计时,快速的真实状态转换不会相互吞音。冷却以音频时钟计算,旋律各层以同一个起始时间调度,不使用逐音符的 JS 定时器。
联机事件使用包含模式、局 ID、玩家 ID 与射击 ID/事件 ID 的稳定键。预测射击和权威回包使用相同键,适配器同时保留已确认事件游标;音效层再保留最近 256 个键。静音或暂停中送达音效层的事件也消耗去重键,避免恢复时补播旧声;换局重置适配器与声音历史。去重缓存是有界的,不替代可靠的网络事件生命周期。
接入方式
// 真实 UI 点击/按键:允许在当前用户操作中初始化或恢复音频。
void audio.play({ id: "ui.confirm" }, { activate: true });
// 已生效的玩法动作:不为后台网络回包创建或解锁设备。
void audio.play({ id: "player.kick", side: -1 });
void audio.play(
{ id: "combat.shot", weapon: "smg" },
{ eventKey: `city:${matchId}:shot:${userId}:${shotId}` },
);
// 同步 UI,卸载时取消订阅并释放当前作用域。
const unsubscribe = audio.subscribeSettings(({ muted, mix }) => {
audioState.muted = muted;
audioState.mix = mix;
});play 返回 scheduled、muted、paused、unavailable、cooldown、budget、duplicate、stale 或 disposed,用于验证与诊断;scheduled 表示已调度,不能据此认定设备扬声器实际可听。初始化失败不阻塞游戏,后续操作可重试;异步首响只保留最新请求,超过 1 秒或经历失焦、暂停、卸载的过期请求被丢弃。
setPaused 同步模式状态,resume 在用户操作中恢复设备;不要从网络快照或焦点变化自动调用 resume。常规菜单通过 interactionFeedback 接入,已有专用动作音效的控件关闭通用确认音。输入框编辑、禁用控件、键盘重复及纯焦点移动不解锁设备。普通按钮不导入 GameAudio 或音效目录。
每个 GameAudio 是一个可释放作用域,游戏、试听和嵌入活动通过租约复用一个上下文。关闭试听只释放自己的音乐、连续声与短音;最后一个作用域释放时才关闭设备。白噪声缓冲在同一上下文只生成一次,每次播放仍创建单次使用的源节点。主音量、音效等混音设置与静音在同一文档的作用域之间实时同步,宿主通过设置订阅更新界面。
扩展与验证
新增声音先在 events.ts 声明语义 ID 和必需参数,再在 catalog.ts 定义音色、分类及策略。声音在有效业务动作或权威事件处触发,避免每帧调用、重复的 DOM 与玩法反馈、隐式设备解锁;循环声音继续由明确的生命周期接口控制。getDiagnostics() 提供上下文状态、当前声部/实例/分类数,以及调度和丢弃原因计数。
合成音效不增加外部媒体。新增采样音效应复用 COS 资源与离线构建 的清单、发布、恢复及许可流程,不内嵌媒体或另建下载器;为短样本增加按需缓冲加载前,应明确缓存预算、失败重试和声部清理。
相关测试覆盖优先级抢占与原子准入、作用域释放、暂停/后台分类、设置订阅、解锁与过期请求、联机预测去重。浏览器回归验证实际 Web Audio 波形的能量与峰值、方向、音乐循环和节拍时钟、桌面与窄屏操作、按需加载;发布构建继续检查原有资源与性能预算。体积检查通过不能代替整机 CPU 或移动设备延迟测试;真实 iOS、音频设备切换和实体手柄听感仍需对应设备验收。