com.alicizax.unity.framework/Runtime/Audio/Audio.md
陈思海 46194ddee8 重构音频模块 1. 高频、大量音频反复调用时,单帧 CPU 开销与 GC 最优 2. AudioClip / AudioSource 的加载、缓存淘汰、卸载形成完整闭环,避免线性遍历 3. AudioSource 对象池 + 播放请求 struct 全部池化覆盖所有分配点 4. 支持3D环境音并具备距离衰减、遮挡等空间属性 5. 新增音频类型(BGM/SFX/Voice/Ambient) 6. 可调式监控Debug信息 及时跟踪音频缓存 处理 句柄状态
重构音频模块
1. 高频、大量音频反复调用时,单帧 CPU 开销与 GC 最优
2. AudioClip / AudioSource 的加载、缓存淘汰、卸载形成完整闭环,避免线性遍历
3. AudioSource 对象池 + 播放请求 struct 全部池化覆盖所有分配点
4. 支持3D环境音并具备距离衰减、遮挡等空间属性
5. 新增音频类型(BGM/SFX/Voice/Ambient)
6. 可调式监控Debug信息 及时跟踪音频缓存 处理 句柄状态
2026-04-23 17:21:36 +08:00

750 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Audio 模块使用文档
## 1. 模块目标
`Audio` 模块为框架提供统一的音频播放、缓存、回收、3D 空间音频与配置管理能力。
当前实现目标:
- 高频播放路径尽量稳定,避免热路径 GC
- `AudioClip` / `AudioSource` 生命周期可控,避免泄漏
- 支持 `Music`、`Sound`、`UISound`、`Voice`、`Ambient`
- 支持 2D 音频、3D 定点音频、跟随目标音频
- 支持距离衰减、空间混合、遮挡低通
- 配置从组件 Inspector 中抽离为可复用 `ScriptableObject`
模块核心代码位于:
- `Runtime/Audio/AudioComponent.cs`
- `Runtime/Audio/IAudioService.cs`
- `Runtime/Audio/AudioService.cs`
- `Runtime/Audio/AudioCategory.cs`
- `Runtime/Audio/AudioAgent.cs`
- `Runtime/Audio/AudioGroupConfig.cs`
- `Runtime/Audio/AudioGroupConfigCollection.cs`
## 2. 架构概览
模块运行时由以下几层组成:
### 2.1 AudioComponent
`AudioComponent` 是场景中的挂载入口,负责:
- 注册 `AudioService`
- 绑定 `AudioMixer`
- 绑定 `AudioListener`
- 绑定 `AudioGroupConfigCollection`
- 初始化运行时音频系统
### 2.2 AudioService
`AudioService` 是模块核心调度层,负责:
- 播放请求入口
- `AudioType` 维度的音量与开关管理
- `AudioClip` 缓存、引用计数、TTL、LRU
- `AudioSource` 对象池管理
- 播放句柄分配与控制
- 监听器注册
### 2.3 AudioCategory
每个 `AudioType` 对应一个 `AudioCategory`,负责:
- 维护该分类下的固定数量 `AudioAgent`
- 用空闲栈管理可用槽位
- 用最老播放堆管理满载抢占
- 更新该分类下活跃音频
### 2.4 AudioAgent
每个 `AudioAgent` 对应一个运行时播放槽位,负责:
- 绑定一个 `AudioSource`
- 管理单条播放状态
- 管理当前 `AudioClip` 引用
- 处理跟随、淡出、遮挡检测
### 2.5 AudioSourceObject
`AudioSourceObject``AudioSource` 的池化包装对象,接入框架 `ObjectPoolService`
### 2.6 AudioClipCacheEntry
`AudioClipCacheEntry` 是单个地址的缓存条目,负责:
- `AssetHandle`
- `AudioClip`
- 引用计数
- LRU 链表节点
- pending 加载请求链
## 3. 生命周期
### 3.1 初始化
初始化链路:
1. 场景中挂载 `AudioComponent`
2. `Awake` 时注册 `AudioService`
3. `Start` 时读取 `AudioGroupConfigCollection`
4. 显式注册 `AudioListener`
5. 创建每个分类的 `AudioCategory`
6. 为每个分类预创建固定数量 `AudioAgent`
7. 每个 `AudioAgent``ObjectPoolService` 获取一个 `AudioSourceObject`
### 3.2 播放
播放链路:
1. 业务通过 `IAudioService` 发起播放
2. `AudioService` 生成池化播放请求
3. 对应 `AudioCategory` 从空闲栈取槽位,或从最老播放堆抢占
4. `AudioAgent` 绑定配置
5. 直接播放 `AudioClip` 或进入 `AudioClip` 加载/缓存逻辑
6. 播放完成、停止、淡出结束后释放引用
### 3.3 回收
回收链路:
- `AudioAgent` 停止后释放 `AudioClip` 引用
- `AudioClip` 引用计数归零后进入 LRU
- 到达 TTL 或容量超限时淘汰
- 服务销毁时统一停止播放、释放缓存、释放未使用 `AudioSource`
## 4. 场景配置
### 4.1 AudioComponent
在场景中创建一个 GameObject 挂载 `AudioComponent`
推荐字段配置:
- `Audio Mixer`
- 指向音频混音器资源
- `Instance Root`
- 音频实例根节点
- `Audio Listener`
- 显式绑定实际生效的监听器
- `Audio Group Configs`
- 指向 `AudioGroupConfigCollection` 资源
### 4.2 为什么要显式绑定 AudioListener
当前实现不再扫描全场景查找监听器。
这样做的原因:
- 避免运行时全场景线性查找
- 避免多监听器场景中的不确定性
- 保证 3D 音频和遮挡始终基于明确目标
如果不手动绑定,`AudioComponent` 会在自身层级下尝试一次 `GetComponentInChildren<AudioListener>(true)`
推荐规则:
- 主场景主相机上的监听器手动拖给 `AudioComponent`
- 运行时如果切换监听器,应显式重新注册
## 5. 配置资产
### 5.1 AudioGroupConfigCollection
`AudioGroupConfigCollection` 是一个 `ScriptableObject`,保存全部分类配置。
默认通过 `CreateAssetMenu` 创建:
- `AlicizaX/Audio/Audio Group Configs`
### 5.2 默认配置
新建资源后默认包含以下五组:
- `Music`
- `Sound`
- `UISound`
- `Voice`
- `Ambient`
### 5.3 AudioGroupConfig 字段说明
每个 `AudioGroupConfig` 包含:
- `音频类型`
- 对应 `AudioType`
- `名称`
- 配置展示名
- `静音`
- 初始化时该分类是否关闭
- `音量`
- 初始线性音量
- `通道数`
- 该分类最大并发 `AudioAgent` 数量
- `Mixer音量参数`
- 对应 `AudioMixer` 暴露参数名
- `空间混合`
- 2D/3D 混合比例
- `多普勒`
- 多普勒系数
- `扩散`
- 声场扩散
- `AudioSource优先级`
- Unity AudioSource Priority
- `混响区混合`
- Reverb Zone Mix
- `衰减模式`
- `AudioRolloffMode`
- `最小距离`
- 3D 音频近距离阈值
- `最大距离`
- 3D 音频远距离阈值
- `开启遮挡`
- 是否进行遮挡检测
- `遮挡检测层`
- 遮挡 Raycast LayerMask
- `遮挡检测间隔`
- 检测频率
- `遮挡低通截止频率`
- 遮挡时低通频率
- `遮挡音量系数`
- 遮挡时音量乘数
## 6. Inspector 使用
### 6.1 AudioComponent Inspector
`AudioComponentInspector` 提供:
- `音频监听器` 字段
- `音频分组配置` 字段
- 一键创建默认配置资源按钮
### 6.2 AudioGroupConfigCollection Inspector
`AudioGroupConfigCollectionInspector` 提供:
- 中文字段名
- 分类分块显示
- `Occlusion` 未开启时隐藏遮挡相关字段
## 7. 运行时 API
核心接口为 `IAudioService`
### 7.1 全局开关
```csharp
IAudioService audio = GameApp.Audio;
audio.Volume = 1f;
audio.Enable = true;
```
### 7.2 通用分类音量接口
推荐统一使用以下接口:
```csharp
audio.SetCategoryVolume(AudioType.Music, 0.8f);
audio.SetCategoryVolume(AudioType.Sound, 1f);
float musicVolume = audio.GetCategoryVolume(AudioType.Music);
```
### 7.3 通用分类开关接口
```csharp
audio.SetCategoryEnable(AudioType.Music, true);
audio.SetCategoryEnable(AudioType.Ambient, false);
bool ambientEnabled = audio.GetCategoryEnable(AudioType.Ambient);
```
### 7.4 2D 地址播放
```csharp
ulong handle = audio.Play(
AudioType.Sound,
"Audio/SFX/Click",
loop: false,
volume: 1f,
async: false,
cacheClip: true);
```
参数说明:
- `type`
- 分类
- `path`
- 资源地址
- `loop`
- 是否循环
- `volume`
- 播放音量
- `async`
- 是否异步加载
- `cacheClip`
- 是否在播放后保留到缓存
### 7.5 直接播放 AudioClip
```csharp
ulong handle = audio.Play(AudioType.Music, clip, loop: true, volume: 1f);
```
### 7.6 3D 定点播放
```csharp
Vector3 position = hitPoint;
ulong handle = audio.Play3D(
AudioType.Sound,
"Audio/SFX/Explosion",
position,
loop: false,
volume: 1f,
async: true,
cacheClip: true);
```
### 7.7 跟随目标播放
```csharp
ulong handle = audio.PlayFollow(
AudioType.Voice,
"Audio/Voice/Npc001",
npcTransform,
Vector3.zero,
loop: false,
volume: 1f,
async: true,
cacheClip: true);
```
注意:
- 跟随播放不会把池化 `AudioSource` 挂到业务节点下
- 实际上是保持在音频根节点下,并同步世界位置/旋转
### 7.8 停止、暂停、恢复
```csharp
audio.Stop(handle, fadeout: true);
audio.Pause(handle);
audio.Resume(handle);
```
### 7.9 按分类停止
```csharp
audio.Stop(AudioType.Music, fadeout: true);
audio.StopAll(fadeout: false);
```
### 7.10 预加载与卸载
```csharp
audio.Preload(preloadList, pin: true);
audio.Unload(unloadList);
audio.ClearCache();
```
说明:
- `Preload(..., pin: true)`
- 预加载并常驻
- `Unload(...)`
- 取消 pin并在无引用时释放
- `ClearCache()`
- 只清 unused 条目,不会强拆播放中和加载中的 clip
## 8. 句柄语义
播放返回值为 `ulong handle`
句柄特点:
- 不是 `AudioAgent` 对象引用
- 对外只暴露控制句柄,不暴露内部实现
- 使用代际句柄避免旧句柄误命中新播放实例
推荐规则:
- 业务层如需后续停止某条音频,保存 `handle`
- 不需要控制的瞬时音效可忽略返回值
## 9. 缓存策略
### 9.1 基本机制
每个 `AudioClip` 地址对应一个 `AudioClipCacheEntry`
条目包含:
- `AssetHandle`
- `AudioClip`
- `RefCount`
- `Pinned`
- `CacheAfterUse`
- `Loading`
- `PendingHead / PendingTail`
- `LRU` 节点
### 9.2 引用计数
播放开始时:
- `RetainClip`
播放结束时:
- `ReleaseClip`
### 9.3 LRU 与 TTL
`RefCount == 0` 时:
-`CacheAfterUse == true`,进入 LRU
-`CacheAfterUse == false`,立即清理
缓存会在以下情况被淘汰:
- 超过容量
- 超过 TTL
### 9.4 Pinned 资源
通过 `Preload(..., pin: true)` 的资源为 pinned。
特点:
- 不会被普通缓存淘汰
- 必须显式 `Unload(...)` 解除 pin
## 10. AudioSource 池化
### 10.1 池化来源
`AudioSourceObject` 使用框架内置 `ObjectPoolService`
每个分类初始化时会创建固定数量 `AudioAgent`
每个 `AudioAgent` 都会对应一个可复用 `AudioSourceObject`
### 10.2 回收时机
模块销毁时:
- 停止所有播放
- 分类销毁
- `AudioSourceObject` 归还池
- 未使用池对象统一释放
## 11. 3D 空间音频
### 11.1 空间混合
由以下字段决定:
- `AudioGroupConfig.SpatialBlend`
- 播放请求是否是 3D / Follow / WorldPosition
### 11.2 距离衰减
由以下字段决定:
- `RolloffMode`
- `MinDistance`
- `MaxDistance`
### 11.3 遮挡
如果开启 `Occlusion`
- 按配置时间间隔执行 `Physics.Raycast`
- 命中遮挡层后:
- 启用 `AudioLowPassFilter`
- 设置低通频率
-`OcclusionVolumeMultiplier` 压低音量
### 11.4 监听器来源
监听器只来自显式注册:
- `AudioComponent` 绑定的 `AudioListener`
- 或未来业务显式切换注册
## 12. 抢占策略
当某个 `AudioCategory` 已满:
- 先尝试空闲栈取空槽
- 若没有空槽,则从“最老播放堆”中取最早开始播放的槽位进行复用
特点:
- 无线性扫描
- 高并发下行为确定
- CPU 成本稳定
## 13. 与资源模块的关系
音频模块不直接把 `AudioClip` 交给资源模块对象池管理,而是:
- 使用 `IResourceService.LoadAssetSyncHandle<T>()`
- 使用 `IResourceService.LoadAssetAsyncHandle<T>()`
- 自己持有 `AssetHandle`
- 在缓存淘汰或销毁时 `Dispose()`
这样做的原因:
- 音频需要精细控制引用计数
- 音频有自己的 `pin/LRU/TTL/pending` 语义
## 14. 常见使用模式
### 14.1 BGM
```csharp
var audio = GameApp.Audio;
audio.SetCategoryVolume(AudioType.Music, 0.8f);
ulong bgmHandle = audio.Play(
AudioType.Music,
"Audio/BGM/MainTheme",
loop: true,
volume: 1f,
async: true,
cacheClip: true);
```
### 14.2 UI 点击音效
```csharp
GameApp.Audio.Play(
AudioType.UISound,
"Audio/UI/Click",
loop: false,
volume: 1f,
async: false,
cacheClip: true);
```
### 14.3 角色语音
```csharp
GameApp.Audio.PlayFollow(
AudioType.Voice,
"Audio/Voice/Hero/Greeting",
heroTransform,
Vector3.up * 1.5f,
loop: false,
volume: 1f,
async: true,
cacheClip: false);
```
### 14.4 环境循环声
```csharp
GameApp.Audio.Play3D(
AudioType.Ambient,
"Audio/Ambient/WaterfallLoop",
waterfallPosition,
loop: true,
volume: 1f,
async: true,
cacheClip: true);
```
## 15. 运行时切换监听器
如果项目存在相机切换或监听器切换逻辑,应显式调用:
```csharp
IAudioService audio = GameApp.Audio;
audio.UnregisterListener(oldListener);
audio.RegisterListener(newListener);
```
建议规则:
- 保证同一时刻只有一个主监听器负责注册
- 切换时先注销旧监听器,再注册新监听器
## 16. 扩展组件
### 16.1 AudioListenerBinder
`AudioListenerBinder` 用于把场景内的 `AudioListener` 显式注册到 `IAudioService`
适用场景:
- 主相机不在 `AudioComponent` 同一节点下
- 运行时相机或监听器会切换
- 不希望 `AudioComponent` 扫描子节点查找监听器
使用方式:
1. 在带有 `AudioListener` 的对象上添加 `AudioListenerBinder`
2. 确保场景中已经有 `AudioComponent` 初始化音频服务。
3. 对象启用时自动注册,禁用时自动注销。
### 16.2 AudioEmitter
`AudioEmitter` 是场景 3D 声源组件,面向篝火、电台、瀑布、机器噪音等固定或跟随物体的循环环境声。
核心字段:
- `Audio Type`:播放分类,环境声通常使用 `Ambient`
- `Address`:音频资源地址。
- `Play On Enable`:对象启用时播放。
- `Loop`:是否循环。
- `Follow Self`:是否跟随当前物体。
- `Min Distance`:近距离清晰范围。
- `Max Distance`:超过该距离后听不到或接近听不到。
- `Use Trigger Range`:是否进入半径才播放。
- `Trigger Range`:自定义进入播放区域。
- `Trigger Hysteresis`:离开判定缓冲,避免边界频繁启停。
- `Draw Gizmos`:绘制触发范围和衰减范围。
篝火使用方式:
1. 在篝火对象上添加 `AudioEmitter`
2. `Audio Type` 设为 `Ambient`
3. `Address` 填篝火循环音效地址。
4. 开启 `Play On Enable`、`Loop`、`Follow Self`。
5. 关闭 `Use Trigger Range`
6. 设置 `Min Distance` 为清晰听到的距离,例如 `2`
7. 设置 `Max Distance` 为听不到的距离,例如 `18`
这样玩家靠近篝火时声音更清晰,远离到最大距离后由 Unity 3D 衰减处理到不可闻。
电台使用方式:
1. 在电台对象上添加 `AudioEmitter`
2. `Audio Type` 设为 `Ambient``Voice`
3. `Address` 填电台循环音效地址。
4. 开启 `Loop`、`Follow Self`、`Use Trigger Range`。
5. 设置 `Trigger Range` 为进入区域,例如 `8`
6. 设置 `Min Distance``Max Distance` 控制区域内的清晰范围与衰减。
7. 开启 `Draw Gizmos`,在 Scene 视图查看触发范围和衰减范围。
注意事项:
- `AudioEmitter` 不做场景扫描,不读取私有配置,不创建临时对象。
- 进入区域只触发一次;非循环音效播放完后不会在范围内每帧重播,离开再进入才会重新触发。
- `Play3D` 静态声源和 `PlayFollow` 跟随声源都支持单次播放覆盖 `MinDistance`、`MaxDistance`、`RolloffMode`、`SpatialBlend`。
## 17. 性能建议
### 17.1 推荐
- 高频短音效使用同步加载 + `cacheClip: true`
- 大型语音或低频资源可异步加载
- 常驻 BGM / 高频 UI 音效建议提前 `Preload`
- 明确设置分类通道数,避免过小导致过度抢占
- 3D 语音/环境音再开启遮挡,不要给所有分类都开
### 17.2 不推荐
- 每次播放都 `cacheClip: false` 且地址重复
- 不显式绑定 `AudioListener`
-`Music` 设置过高空间混合
- 给大量瞬时音效开启高频遮挡检测
## 18. 常见问题
### 18.1 为什么播放返回 0
可能原因:
- `AudioService` 尚未初始化
- `AudioType` 越界
- `AudioComponent` 未正确配置
- `AudioListener` 未注册时 3D 音频相关行为不完整
### 18.2 为什么 `ClearCache()` 后仍有部分资源未释放
因为:
- 播放中的 clip 仍有引用
- 正在加载的 clip 仍有 pending request
- pinned 资源未调用 `Unload`
### 18.3 为什么跟随音频不会挂到目标节点下
这是刻意设计:
- 防止业务节点销毁时把池对象一起销毁
- 防止池生命周期被业务层意外接管
## 19. 扩展建议
如果后续继续扩展模块,优先遵守以下方向:
- 核心接口只保留 `AudioType` 通用访问
- 不再继续膨胀 `IAudioService`
- 新分类优先通过:
- `AudioType`
- `AudioGroupConfigCollection`
- `AudioMixer` 暴露参数
- 通用 `Get/SetCategory` API
不推荐继续在核心接口里新增:
- `NpcVoiceVolume`
- `BattleMusicEnable`
- `CutsceneAmbientVolume`
这类业务语义应放在业务 facade 或上层系统里。
## 20. 自测清单
修改音频模块后建议至少回归以下场景:
- 2D 同步播放
- 2D 异步播放
- 3D 定点播放
- Follow 播放
- 目标销毁时的 Follow 回收
- 分类满载时抢占
- `Preload(pin: true)``Unload`
- `ClearCache()` 对 unused 资源的清理
- 遮挡开启/关闭切换
- 监听器切换
## 21. 总结
当前 `Audio` 模块的核心使用原则可以归纳为:
- 入口统一从 `IAudioService`
- 分类控制统一用 `AudioType`
- 监听器显式注册,不做全场景扫描
- 高频资源使用缓存与预加载
- 3D 音频通过配置资产控制,不在业务层硬编码
- 池对象生命周期永远由音频系统自己掌握
如果严格遵守以上规则,模块可以稳定支撑常规项目中的 BGM、SFX、UI、Voice、Ambient 五类音频需求。