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

17 KiB
Raw Blame History

Audio 模块使用文档

1. 模块目标

Audio 模块为框架提供统一的音频播放、缓存、回收、3D 空间音频与配置管理能力。

当前实现目标:

  • 高频播放路径尽量稳定,避免热路径 GC
  • AudioClip / AudioSource 生命周期可控,避免泄漏
  • 支持 MusicSoundUISoundVoiceAmbient
  • 支持 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

AudioSourceObjectAudioSource 的池化包装对象,接入框架 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. 每个 AudioAgentObjectPoolService 获取一个 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 全局开关

IAudioService audio = GameApp.Audio;

audio.Volume = 1f;
audio.Enable = true;

7.2 通用分类音量接口

推荐统一使用以下接口:

audio.SetCategoryVolume(AudioType.Music, 0.8f);
audio.SetCategoryVolume(AudioType.Sound, 1f);

float musicVolume = audio.GetCategoryVolume(AudioType.Music);

7.3 通用分类开关接口

audio.SetCategoryEnable(AudioType.Music, true);
audio.SetCategoryEnable(AudioType.Ambient, false);

bool ambientEnabled = audio.GetCategoryEnable(AudioType.Ambient);

7.4 2D 地址播放

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

ulong handle = audio.Play(AudioType.Music, clip, loop: true, volume: 1f);

7.6 3D 定点播放

Vector3 position = hitPoint;
ulong handle = audio.Play3D(
    AudioType.Sound,
    "Audio/SFX/Explosion",
    position,
    loop: false,
    volume: 1f,
    async: true,
    cacheClip: true);

7.7 跟随目标播放

ulong handle = audio.PlayFollow(
    AudioType.Voice,
    "Audio/Voice/Npc001",
    npcTransform,
    Vector3.zero,
    loop: false,
    volume: 1f,
    async: true,
    cacheClip: true);

注意:

  • 跟随播放不会把池化 AudioSource 挂到业务节点下
  • 实际上是保持在音频根节点下,并同步世界位置/旋转

7.8 停止、暂停、恢复

audio.Stop(handle, fadeout: true);
audio.Pause(handle);
audio.Resume(handle);

7.9 按分类停止

audio.Stop(AudioType.Music, fadeout: true);
audio.StopAll(fadeout: false);

7.10 预加载与卸载

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

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 点击音效

GameApp.Audio.Play(
    AudioType.UISound,
    "Audio/UI/Click",
    loop: false,
    volume: 1f,
    async: false,
    cacheClip: true);

14.3 角色语音

GameApp.Audio.PlayFollow(
    AudioType.Voice,
    "Audio/Voice/Hero/Greeting",
    heroTransform,
    Vector3.up * 1.5f,
    loop: false,
    volume: 1f,
    async: true,
    cacheClip: false);

14.4 环境循环声

GameApp.Audio.Play3D(
    AudioType.Ambient,
    "Audio/Ambient/WaterfallLoop",
    waterfallPosition,
    loop: true,
    volume: 1f,
    async: true,
    cacheClip: true);

15. 运行时切换监听器

如果项目存在相机切换或监听器切换逻辑,应显式调用:

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 EnableLoopFollow Self
  5. 关闭 Use Trigger Range
  6. 设置 Min Distance 为清晰听到的距离,例如 2
  7. 设置 Max Distance 为听不到的距离,例如 18

这样玩家靠近篝火时声音更清晰,远离到最大距离后由 Unity 3D 衰减处理到不可闻。

电台使用方式:

  1. 在电台对象上添加 AudioEmitter
  2. Audio Type 设为 AmbientVoice
  3. Address 填电台循环音效地址。
  4. 开启 LoopFollow SelfUse Trigger Range
  5. 设置 Trigger Range 为进入区域,例如 8
  6. 设置 Min DistanceMax Distance 控制区域内的清晰范围与衰减。
  7. 开启 Draw Gizmos,在 Scene 视图查看触发范围和衰减范围。

注意事项:

  • AudioEmitter 不做场景扫描,不读取私有配置,不创建临时对象。
  • 进入区域只触发一次;非循环音效播放完后不会在范围内每帧重播,离开再进入才会重新触发。
  • Play3D 静态声源和 PlayFollow 跟随声源都支持单次播放覆盖 MinDistanceMaxDistanceRolloffModeSpatialBlend

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