com.alicizax.unity.framework/Runtime/Audio/Audio.md

750 lines
17 KiB
Markdown
Raw Normal View History

# 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 五类音频需求。