重构音频模块 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信息 及时跟踪音频缓存 处理 句柄状态
This commit is contained in:
陈思海 2026-04-23 17:21:36 +08:00
parent 8849ccf5ce
commit 46194ddee8
38 changed files with 4397 additions and 1375 deletions

View File

@ -2,15 +2,24 @@
using AlicizaX.Audio.Runtime;
using AlicizaX.Editor;
using UnityEditor;
using UnityEngine;
namespace AlicizaX.Audio.Editor
{
[CustomEditor(typeof(AudioComponent))]
internal sealed class AudioComponentInspector : GameFrameworkInspector
{
private readonly AudioServiceDebugInfo _serviceInfo = new AudioServiceDebugInfo();
private readonly AudioCategoryDebugInfo _categoryInfo = new AudioCategoryDebugInfo();
private readonly AudioAgentDebugInfo _agentInfo = new AudioAgentDebugInfo();
private readonly AudioClipCacheDebugInfo _clipCacheInfo = new AudioClipCacheDebugInfo();
private SerializedProperty m_InstanceRoot = null;
private SerializedProperty m_AudioListener = null;
private SerializedProperty m_AudioMixer = null;
private SerializedProperty m_AudioGroupConfigs = null;
private static readonly GUIContent GroupConfigLabel = new GUIContent("音频分组配置");
private static readonly GUIContent CreateButtonLabel = new GUIContent("创建默认配置资源");
private static readonly GUIContent AudioListenerLabel = new GUIContent("音频监听器");
public override void OnInspectorGUI()
{
@ -23,22 +32,170 @@ namespace AlicizaX.Audio.Editor
EditorGUI.BeginDisabledGroup(EditorApplication.isPlayingOrWillChangePlaymode);
{
EditorGUILayout.PropertyField(m_InstanceRoot);
EditorGUILayout.PropertyField(m_AudioListener, AudioListenerLabel);
EditorGUILayout.PropertyField(m_AudioMixer);
EditorGUILayout.PropertyField(m_AudioGroupConfigs, true);
EditorGUILayout.PropertyField(m_AudioGroupConfigs, GroupConfigLabel);
if (m_AudioGroupConfigs.objectReferenceValue == null && GUILayout.Button(CreateButtonLabel))
{
CreateDefaultConfigAsset();
}
}
EditorGUI.EndDisabledGroup();
serializedObject.ApplyModifiedProperties();
DrawRuntimeDebugInfo(t);
Repaint();
}
private void OnEnable()
{
m_InstanceRoot = serializedObject.FindProperty("m_InstanceRoot");
m_AudioListener = serializedObject.FindProperty("m_AudioListener");
m_AudioMixer = serializedObject.FindProperty("m_AudioMixer");
m_AudioGroupConfigs = serializedObject.FindProperty("m_AudioGroupConfigs");
}
private void CreateDefaultConfigAsset()
{
AudioGroupConfigCollection asset = ScriptableObject.CreateInstance<AudioGroupConfigCollection>();
asset.EnsureDefaults();
string path = EditorUtility.SaveFilePanelInProject("创建音频分组配置", "AudioGroupConfigs", "asset", "请选择保存路径");
if (string.IsNullOrEmpty(path))
{
UnityEngine.Object.DestroyImmediate(asset);
return;
}
AssetDatabase.CreateAsset(asset, path);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
m_AudioGroupConfigs.objectReferenceValue = asset;
serializedObject.ApplyModifiedProperties();
EditorGUIUtility.PingObject(asset);
}
private void DrawRuntimeDebugInfo(AudioComponent component)
{
if (!EditorApplication.isPlaying)
{
return;
}
if (!AppServices.TryGet<IAudioService>(out IAudioService audioService) || audioService is not IAudioDebugService debugService)
{
EditorGUILayout.HelpBox("音频服务未初始化。", MessageType.Info);
return;
}
debugService.FillServiceDebugInfo(_serviceInfo);
EditorGUILayout.Space();
EditorGUILayout.LabelField("运行时调试", EditorStyles.boldLabel);
EditorGUILayout.LabelField("Initialized", _serviceInfo.Initialized.ToString());
EditorGUILayout.LabelField("Unity Audio Disabled", _serviceInfo.UnityAudioDisabled.ToString());
EditorGUILayout.LabelField("Enable", _serviceInfo.Enable.ToString());
EditorGUILayout.LabelField("Volume", _serviceInfo.Volume.ToString("F3"));
EditorGUILayout.ObjectField("Listener", _serviceInfo.Listener, typeof(AudioListener), true);
EditorGUILayout.ObjectField("Instance Root", _serviceInfo.InstanceRoot, typeof(Transform), true);
EditorGUILayout.LabelField("Active Agents", _serviceInfo.ActiveAgentCount.ToString());
EditorGUILayout.LabelField("Handle Capacity", _serviceInfo.HandleCapacity.ToString());
EditorGUILayout.LabelField("Clip Cache", _serviceInfo.ClipCacheCount + " / " + _serviceInfo.ClipCacheCapacity);
DrawCategoryDebugInfo(debugService);
DrawAgentDebugInfo(debugService);
DrawClipCacheDebugInfo(debugService);
}
private void DrawCategoryDebugInfo(IAudioDebugService debugService)
{
EditorGUILayout.Space();
EditorGUILayout.LabelField("分类状态", EditorStyles.boldLabel);
for (int i = 0; i < debugService.CategoryCount; i++)
{
if (!debugService.FillCategoryDebugInfo(i, _categoryInfo))
{
continue;
}
EditorGUILayout.LabelField(
_categoryInfo.Type.ToString(),
"Enabled " + _categoryInfo.Enabled
+ " | Volume " + _categoryInfo.Volume.ToString("F2")
+ " | Active " + _categoryInfo.ActiveCount
+ " | Free " + _categoryInfo.FreeCount
+ " | Capacity " + _categoryInfo.Capacity);
}
}
private void DrawAgentDebugInfo(IAudioDebugService debugService)
{
EditorGUILayout.Space();
EditorGUILayout.LabelField("活跃播放", EditorStyles.boldLabel);
bool hasActive = false;
for (int typeIndex = 0; typeIndex < debugService.CategoryCount; typeIndex++)
{
if (!debugService.FillCategoryDebugInfo(typeIndex, _categoryInfo))
{
continue;
}
for (int agentIndex = 0; agentIndex < _categoryInfo.Capacity; agentIndex++)
{
if (!debugService.FillAgentDebugInfo(typeIndex, agentIndex, _agentInfo) || _agentInfo.State == AudioAgentRuntimeState.Free)
{
continue;
}
hasActive = true;
string clipName = _agentInfo.Clip != null ? _agentInfo.Clip.name : "<None>";
string address = string.IsNullOrEmpty(_agentInfo.Address) ? "<Direct Clip>" : _agentInfo.Address;
EditorGUILayout.LabelField(
_agentInfo.Type + "[" + _agentInfo.Index + "]",
_agentInfo.State
+ " | Handle " + _agentInfo.Handle
+ " | Clip " + clipName
+ " | Address " + address
+ " | Vol " + _agentInfo.Volume.ToString("F2")
+ " | 3D " + _agentInfo.Spatial);
}
}
if (!hasActive)
{
EditorGUILayout.LabelField("无活跃播放。");
}
}
private void DrawClipCacheDebugInfo(IAudioDebugService debugService)
{
EditorGUILayout.Space();
EditorGUILayout.LabelField("Clip 缓存", EditorStyles.boldLabel);
AudioClipCacheEntry entry = debugService.FirstClipCacheEntry;
if (entry == null)
{
EditorGUILayout.LabelField("缓存为空。");
return;
}
while (entry != null)
{
AudioClipCacheEntry next = entry.AllNext;
if (debugService.FillClipCacheDebugInfo(entry, _clipCacheInfo))
{
EditorGUILayout.LabelField(
_clipCacheInfo.Address,
"Ref " + _clipCacheInfo.RefCount
+ " | Pending " + _clipCacheInfo.PendingCount
+ " | Loaded " + _clipCacheInfo.IsLoaded
+ " | Loading " + _clipCacheInfo.Loading
+ " | Pinned " + _clipCacheInfo.Pinned
+ " | LRU " + _clipCacheInfo.InLru);
}
entry = next;
}
}
}
}

View File

@ -0,0 +1,121 @@
using AlicizaX.Audio.Runtime;
using UnityEditor;
namespace AlicizaX.Audio.Editor
{
[CustomEditor(typeof(AudioEmitter))]
internal sealed class AudioEmitterInspector : UnityEditor.Editor
{
private SerializedProperty _audioType;
private SerializedProperty _clipMode;
private SerializedProperty _address;
private SerializedProperty _clip;
private SerializedProperty _playOnEnable;
private SerializedProperty _loop;
private SerializedProperty _volume;
private SerializedProperty _async;
private SerializedProperty _cacheClip;
private SerializedProperty _stopWithFadeout;
private SerializedProperty _followSelf;
private SerializedProperty _followOffset;
private SerializedProperty _spatialBlend;
private SerializedProperty _rolloffMode;
private SerializedProperty _minDistance;
private SerializedProperty _maxDistance;
private SerializedProperty _useTriggerRange;
private SerializedProperty _triggerRange;
private SerializedProperty _triggerHysteresis;
private SerializedProperty _drawGizmos;
private SerializedProperty _drawOnlyWhenSelected;
private SerializedProperty _triggerColor;
private SerializedProperty _minDistanceColor;
private SerializedProperty _maxDistanceColor;
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.LabelField("Playback", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(_audioType);
EditorGUILayout.PropertyField(_clipMode);
if (_clipMode.enumValueIndex == 1)
{
EditorGUILayout.PropertyField(_clip);
}
else
{
EditorGUILayout.PropertyField(_address);
EditorGUILayout.PropertyField(_async);
EditorGUILayout.PropertyField(_cacheClip);
}
EditorGUILayout.PropertyField(_playOnEnable);
EditorGUILayout.PropertyField(_loop);
EditorGUILayout.PropertyField(_volume);
EditorGUILayout.PropertyField(_stopWithFadeout);
EditorGUILayout.Space();
EditorGUILayout.LabelField("Spatial", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(_followSelf);
EditorGUILayout.PropertyField(_followOffset);
EditorGUILayout.PropertyField(_spatialBlend);
EditorGUILayout.PropertyField(_rolloffMode);
EditorGUILayout.PropertyField(_minDistance);
EditorGUILayout.PropertyField(_maxDistance);
EditorGUILayout.Space();
EditorGUILayout.LabelField("Trigger", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(_useTriggerRange);
if (_useTriggerRange.boolValue)
{
EditorGUILayout.PropertyField(_triggerRange);
EditorGUILayout.PropertyField(_triggerHysteresis);
}
EditorGUILayout.Space();
EditorGUILayout.LabelField("Gizmos", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(_drawGizmos);
if (_drawGizmos.boolValue)
{
EditorGUILayout.PropertyField(_drawOnlyWhenSelected);
if (_useTriggerRange.boolValue)
{
EditorGUILayout.PropertyField(_triggerColor);
}
EditorGUILayout.PropertyField(_minDistanceColor);
EditorGUILayout.PropertyField(_maxDistanceColor);
}
serializedObject.ApplyModifiedProperties();
}
private void OnEnable()
{
_audioType = serializedObject.FindProperty("m_AudioType");
_clipMode = serializedObject.FindProperty("m_ClipMode");
_address = serializedObject.FindProperty("m_Address");
_clip = serializedObject.FindProperty("m_Clip");
_playOnEnable = serializedObject.FindProperty("m_PlayOnEnable");
_loop = serializedObject.FindProperty("m_Loop");
_volume = serializedObject.FindProperty("m_Volume");
_async = serializedObject.FindProperty("m_Async");
_cacheClip = serializedObject.FindProperty("m_CacheClip");
_stopWithFadeout = serializedObject.FindProperty("m_StopWithFadeout");
_followSelf = serializedObject.FindProperty("m_FollowSelf");
_followOffset = serializedObject.FindProperty("m_FollowOffset");
_spatialBlend = serializedObject.FindProperty("m_SpatialBlend");
_rolloffMode = serializedObject.FindProperty("m_RolloffMode");
_minDistance = serializedObject.FindProperty("m_MinDistance");
_maxDistance = serializedObject.FindProperty("m_MaxDistance");
_useTriggerRange = serializedObject.FindProperty("m_UseTriggerRange");
_triggerRange = serializedObject.FindProperty("m_TriggerRange");
_triggerHysteresis = serializedObject.FindProperty("m_TriggerHysteresis");
_drawGizmos = serializedObject.FindProperty("m_DrawGizmos");
_drawOnlyWhenSelected = serializedObject.FindProperty("m_DrawOnlyWhenSelected");
_triggerColor = serializedObject.FindProperty("m_TriggerColor");
_minDistanceColor = serializedObject.FindProperty("m_MinDistanceColor");
_maxDistanceColor = serializedObject.FindProperty("m_MaxDistanceColor");
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 45735882efba4a4c9e8798553bde3a38
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,97 @@
using AlicizaX.Audio.Runtime;
using AlicizaX.Editor;
using UnityEditor;
using UnityEngine;
namespace AlicizaX.Audio.Editor
{
[CustomEditor(typeof(AudioGroupConfigCollection))]
internal sealed class AudioGroupConfigCollectionInspector : UnityEditor.Editor
{
private SerializedProperty _groupConfigs;
private static readonly GUIContent GroupConfigsLabel = new GUIContent("音频分组配置");
private static readonly GUIContent AudioTypeLabel = new GUIContent("音频类型");
private static readonly GUIContent NameLabel = new GUIContent("名称");
private static readonly GUIContent MuteLabel = new GUIContent("静音");
private static readonly GUIContent VolumeLabel = new GUIContent("音量");
private static readonly GUIContent AgentCountLabel = new GUIContent("通道数");
private static readonly GUIContent ExposedVolumeLabel = new GUIContent("Mixer音量参数");
private static readonly GUIContent SpatialBlendLabel = new GUIContent("空间混合");
private static readonly GUIContent DopplerLabel = new GUIContent("多普勒");
private static readonly GUIContent SpreadLabel = new GUIContent("扩散");
private static readonly GUIContent SourcePriorityLabel = new GUIContent("AudioSource优先级");
private static readonly GUIContent ReverbZoneMixLabel = new GUIContent("混响区混合");
private static readonly GUIContent OcclusionEnabledLabel = new GUIContent("开启遮挡");
private static readonly GUIContent OcclusionMaskLabel = new GUIContent("遮挡检测层");
private static readonly GUIContent OcclusionIntervalLabel = new GUIContent("遮挡检测间隔");
private static readonly GUIContent OcclusionCutoffLabel = new GUIContent("遮挡低通截止频率");
private static readonly GUIContent OcclusionVolumeLabel = new GUIContent("遮挡音量系数");
private static readonly GUIContent RolloffModeLabel = new GUIContent("衰减模式");
private static readonly GUIContent MinDistanceLabel = new GUIContent("最小距离");
private static readonly GUIContent MaxDistanceLabel = new GUIContent("最大距离");
private void OnEnable()
{
_groupConfigs = serializedObject.FindProperty("m_GroupConfigs");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(_groupConfigs, GroupConfigsLabel, false);
if (_groupConfigs.isExpanded)
{
EditorGUI.indentLevel++;
for (int i = 0; i < _groupConfigs.arraySize; i++)
{
SerializedProperty element = _groupConfigs.GetArrayElementAtIndex(i);
DrawConfig(element, i);
}
EditorGUI.indentLevel--;
}
serializedObject.ApplyModifiedProperties();
}
private static void DrawConfig(SerializedProperty configProperty, int index)
{
if (configProperty == null)
{
return;
}
SerializedProperty typeProperty = configProperty.FindPropertyRelative("AudioType");
string title = typeProperty != null ? typeProperty.enumDisplayNames[typeProperty.enumValueIndex] : ("配置 " + index);
EditorGUILayout.BeginVertical(GUI.skin.box);
EditorGUILayout.LabelField(title, EditorStyles.boldLabel);
EditorGUILayout.PropertyField(typeProperty, AudioTypeLabel);
EditorGUILayout.PropertyField(configProperty.FindPropertyRelative("m_Name"), NameLabel);
EditorGUILayout.PropertyField(configProperty.FindPropertyRelative("m_Mute"), MuteLabel);
EditorGUILayout.PropertyField(configProperty.FindPropertyRelative("m_Volume"), VolumeLabel);
EditorGUILayout.PropertyField(configProperty.FindPropertyRelative("m_AgentHelperCount"), AgentCountLabel);
EditorGUILayout.PropertyField(configProperty.FindPropertyRelative("m_ExposedVolumeParameter"), ExposedVolumeLabel);
EditorGUILayout.PropertyField(configProperty.FindPropertyRelative("m_SpatialBlend"), SpatialBlendLabel);
EditorGUILayout.PropertyField(configProperty.FindPropertyRelative("m_DopplerLevel"), DopplerLabel);
EditorGUILayout.PropertyField(configProperty.FindPropertyRelative("m_Spread"), SpreadLabel);
EditorGUILayout.PropertyField(configProperty.FindPropertyRelative("m_SourcePriority"), SourcePriorityLabel);
EditorGUILayout.PropertyField(configProperty.FindPropertyRelative("m_ReverbZoneMix"), ReverbZoneMixLabel);
EditorGUILayout.PropertyField(configProperty.FindPropertyRelative("audioRolloffMode"), RolloffModeLabel);
EditorGUILayout.PropertyField(configProperty.FindPropertyRelative("minDistance"), MinDistanceLabel);
EditorGUILayout.PropertyField(configProperty.FindPropertyRelative("maxDistance"), MaxDistanceLabel);
SerializedProperty occlusionEnabled = configProperty.FindPropertyRelative("m_OcclusionEnabled");
EditorGUILayout.PropertyField(occlusionEnabled, OcclusionEnabledLabel);
if (occlusionEnabled.boolValue)
{
EditorGUILayout.PropertyField(configProperty.FindPropertyRelative("m_OcclusionMask"), OcclusionMaskLabel);
EditorGUILayout.PropertyField(configProperty.FindPropertyRelative("m_OcclusionCheckInterval"), OcclusionIntervalLabel);
EditorGUILayout.PropertyField(configProperty.FindPropertyRelative("m_OcclusionLowPassCutoff"), OcclusionCutoffLabel);
EditorGUILayout.PropertyField(configProperty.FindPropertyRelative("m_OcclusionVolumeMultiplier"), OcclusionVolumeLabel);
}
EditorGUILayout.EndVertical();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fe9a4f6605709b846b72152265355a75
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -90,9 +90,10 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: f05eaceeebe870a4595e51f998ed518b, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Skin: {fileID: 11400000, guid: dce698819fdb70b42b393d9b0b6d420e, type: 2}
m_ActiveWindow: 3
m_ActiveWindow: 0
m_ShowFullWindow: 0
m_WindowOpacity: 0.8
m_EnableFloatingToggleSnap: 1
m_ConsoleWindow:
m_LockScroll: 1
m_MaxLine: 100
@ -352,6 +353,7 @@ MonoBehaviour:
checkCanReleaseInterval: 30
autoReleaseInterval: 60
maxProcessPerFrame: 50
releaseCheckThreshold: 16
--- !u!1 &6601518982324708866
GameObject:
m_ObjectHideFlags: 0
@ -362,6 +364,7 @@ GameObject:
m_Component:
- component: {fileID: 1640076400431107710}
- component: {fileID: 5037979285627885502}
- component: {fileID: 2681275099927180165}
m_Layer: 0
m_Name: Audio
m_TagString: Untagged
@ -398,39 +401,16 @@ MonoBehaviour:
m_EditorClassIdentifier:
m_AudioMixer: {fileID: 24100000, guid: 1af7a1b121ae17541a1967d430cef006, type: 2}
m_InstanceRoot: {fileID: 1640076400431107710}
m_AudioGroupConfigs:
- m_Name: Music
m_Mute: 0
m_Volume: 0.5
m_AgentHelperCount: 1
AudioType: 2
audioRolloffMode: 1
minDistance: 15
maxDistance: 50
- m_Name: Sound
m_Mute: 1
m_Volume: 0.5
m_AgentHelperCount: 4
AudioType: 0
audioRolloffMode: 0
minDistance: 1
maxDistance: 500
- m_Name: UISound
m_Mute: 0
m_Volume: 0.5
m_AgentHelperCount: 4
AudioType: 1
audioRolloffMode: 0
minDistance: 1
maxDistance: 500
- m_Name: Voice
m_Mute: 0
m_Volume: 0.5
m_AgentHelperCount: 1
AudioType: 3
audioRolloffMode: 0
minDistance: 1
maxDistance: 500
m_AudioListener: {fileID: 2681275099927180165}
m_AudioGroupConfigs: {fileID: 11400000, guid: 7ae4699f0778d2441907aeccaf6c9b29, type: 2}
--- !u!81 &2681275099927180165
AudioListener:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6601518982324708866}
m_Enabled: 1
--- !u!1 &6766524136443284204
GameObject:
m_ObjectHideFlags: 0
@ -476,6 +456,9 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
_language: ChineseSimplified
_startupTables: []
_startupTableLocations: []
_resourcePackageName:
--- !u!1 &7866404898801560120
GameObject:
m_ObjectHideFlags: 0

749
Runtime/Audio/Audio.md Normal file
View File

@ -0,0 +1,749 @@
# 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 五类音频需求。

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 6a19d8c0bd042124c9adb78b14cbb9c0
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,455 +1,470 @@
using AlicizaX.Resource.Runtime;
using AlicizaX;
using UnityEngine;
using UnityEngine.Audio;
using YooAsset;
namespace AlicizaX.Audio.Runtime
{
/// <summary>
/// 音频代理辅助器。
/// </summary>
public class AudioAgent
internal sealed class AudioAgent
{
private int _instanceId;
private const float DefaultFadeOutSeconds = 0.15f;
private const float MinFadeOutSeconds = 0.0001f;
private const float MaxCutoffFrequency = 22000f;
private AudioService _service;
private AudioCategory _category;
private AudioSourceObject _sourceObject;
private AudioSource _source;
private AudioData _audioData;
private IAudioService _audioService;
private IResourceService _resourceService;
private AudioLowPassFilter _lowPassFilter;
private AudioClipCacheEntry _clipEntry;
private Transform _transform;
private float _volume = 1.0f;
private float _duration;
private float _fadeoutTimer;
private const float FADEOUT_DURATION = 0.2f;
private bool _inPool;
private Transform _followTarget;
private Vector3 _followOffset;
private AudioAgentRuntimeState _state;
private bool _spatial;
private bool _occluded;
private bool _loop;
private int _generation;
private ulong _handle;
private float _baseVolume;
private float _pitch;
private float _fadeTimer;
private float _fadeDuration;
private float _startedAt;
private float _nextOcclusionCheckTime;
/// <summary>
/// 音频代理辅助器运行时状态。
/// </summary>
AudioAgentRuntimeState _audioAgentRuntimeState = AudioAgentRuntimeState.None;
internal int Index { get; private set; }
internal int GlobalIndex { get; private set; }
internal int HeapIndex { get; set; }
internal int ActiveIndex { get; set; }
internal ulong Handle => _handle;
internal int Generation => _generation;
internal bool IsFree => _state == AudioAgentRuntimeState.Free;
internal bool IsPlayingState => _state == AudioAgentRuntimeState.Playing || _state == AudioAgentRuntimeState.Loading || _state == AudioAgentRuntimeState.Paused || _state == AudioAgentRuntimeState.FadingOut;
internal float StartedAt => _startedAt;
/// <summary>
/// 音频代理加载请求。
/// </summary>
class LoadRequest
internal void Initialize(AudioService service, AudioCategory category, int index, int globalIndex, AudioSourceObject sourceObject)
{
/// <summary>
/// 音频代理辅助器加载路径。
/// </summary>
public string Path;
/// <summary>
/// 是否异步。
/// </summary>
public bool BAsync;
/// <summary>
/// 是否池化。
/// </summary>
public bool BInPool;
_service = service;
_category = category;
Index = index;
GlobalIndex = globalIndex;
HeapIndex = -1;
ActiveIndex = -1;
BindSource(sourceObject);
ResetState();
}
/// <summary>
/// 音频代理加载请求。
/// </summary>
LoadRequest _pendingLoad = null;
/// <summary>
/// AudioSource实例化Id
/// </summary>
public int InstanceId => _instanceId;
/// <summary>
/// 资源操作句柄。
/// </summary>
public AudioData AudioData => _audioData;
/// <summary>
/// 音频代理辅助器音频大小。
/// </summary>
public float Volume
internal ulong Play(AudioPlayRequest request)
{
set
StopImmediate(false);
_generation++;
if (_generation == int.MaxValue)
{
if (_source != null)
{
_volume = value;
_source.volume = _volume;
}
_generation = 1;
}
get => _volume;
_handle = _service.AllocateHandle(this);
_state = AudioAgentRuntimeState.Loading;
_startedAt = Time.realtimeSinceStartup;
_baseVolume = Mathf.Clamp01(request.Volume);
_pitch = request.Pitch <= 0f ? 1f : request.Pitch;
_fadeDuration = request.FadeOutSeconds > 0f ? request.FadeOutSeconds : DefaultFadeOutSeconds;
_loop = request.Loop;
_spatial = request.Spatial || request.FollowTarget != null || request.UseWorldPosition;
_followTarget = request.FollowTarget;
_followOffset = request.Position;
_nextOcclusionCheckTime = 0f;
_occluded = false;
ApplySourceSettings(request);
_category.MarkOccupied(this);
if (request.Clip != null)
{
StartClip(request.Clip);
return _handle;
}
if (string.IsNullOrEmpty(request.Address))
{
StopImmediate(true);
return 0UL;
}
AudioClipCacheEntry entry = _service.RequestClip(request.Address, request.Async, request.CacheClip, this, _generation);
if (entry != null)
{
OnClipReady(entry, _generation);
}
return _handle;
}
/// <summary>
/// 音频代理辅助器当前是否空闲。
/// </summary>
public bool IsFree
internal bool OnClipReady(AudioClipCacheEntry entry, int generation)
{
get
if (_state != AudioAgentRuntimeState.Loading || generation != _generation || entry == null || entry.Clip == null)
{
if (_source != null)
{
return _audioAgentRuntimeState == AudioAgentRuntimeState.End;
}
else
{
return true;
}
return false;
}
_clipEntry = entry;
_service.RetainClip(entry);
StartClip(entry.Clip);
return true;
}
internal void OnClipLoadFailed(int generation)
{
if (_state == AudioAgentRuntimeState.Loading && generation == _generation)
{
StopImmediate(true);
}
}
/// <summary>
/// 音频代理辅助器播放秒数。
/// </summary>
public float Duration => _duration;
/// <summary>
/// 音频代理辅助器当前音频长度。
/// </summary>
public float Length
internal void Stop(bool fadeout)
{
get
if (_state == AudioAgentRuntimeState.Free)
{
if (_source != null && _source.clip != null)
{
return _source.clip.length;
}
return 0;
}
}
/// <summary>
/// 音频代理辅助器实例位置。
/// </summary>
public Vector3 Position
{
get => _transform.position;
set => _transform.position = value;
}
/// <summary>
/// 音频代理辅助器是否循环。
/// </summary>
public bool IsLoop
{
get
{
if (_source != null)
{
return _source.loop;
}
else
{
return false;
}
}
set
{
if (_source != null)
{
_source.loop = value;
}
}
}
/// <summary>
/// 音频代理辅助器是否正在播放。
/// </summary>
internal bool IsPlaying
{
get
{
if (_source != null && _source.isPlaying)
{
return true;
}
else
{
return false;
}
}
}
/// <summary>
/// 音频代理辅助器获取当前声源。
/// </summary>
/// <returns></returns>
public AudioSource AudioResource()
{
return _source;
}
/// <summary>
/// 创建音频代理辅助器。
/// </summary>
/// <param name="path">生效路径。</param>
/// <param name="bAsync">是否异步。</param>
/// <param name="audioCategory">音频轨道(类别)。</param>
/// <param name="bInPool">是否池化。</param>
/// <returns>音频代理辅助器。</returns>
public static AudioAgent Create(string path, bool bAsync, AudioCategory audioCategory, bool bInPool = false)
{
AudioAgent audioAgent = new AudioAgent();
audioAgent.Init(audioCategory);
audioAgent.Load(path, bAsync, bInPool);
return audioAgent;
}
public static AudioAgent Create(AudioClip clip, AudioCategory audioCategory)
{
AudioAgent audioAgent = new AudioAgent();
audioAgent.Init(audioCategory);
audioAgent.SetAudioClip(clip);
return audioAgent;
}
public void SetAudioClip(AudioClip clip)
{
if (_source == null)
return;
}
Stop(false);
if (_audioData != null)
if (!fadeout || _state == AudioAgentRuntimeState.Loading)
{
AudioData.DeAlloc(_audioData);
_audioData = null;
StopImmediate(true);
return;
}
_fadeTimer = _fadeDuration;
_state = AudioAgentRuntimeState.FadingOut;
}
internal void Pause()
{
if (_state != AudioAgentRuntimeState.Playing || _source == null)
{
return;
}
_source.Pause();
_state = AudioAgentRuntimeState.Paused;
}
internal void Resume()
{
if (_state != AudioAgentRuntimeState.Paused || _source == null)
{
return;
}
_source.UnPause();
_state = AudioAgentRuntimeState.Playing;
}
internal void Update(float deltaTime)
{
if (_state == AudioAgentRuntimeState.Free)
{
return;
}
UpdateFollowTarget();
UpdateOcclusion();
if (_state == AudioAgentRuntimeState.Playing)
{
if (!_loop && _source != null && !_source.isPlaying)
{
StopImmediate(true);
}
return;
}
if (_state != AudioAgentRuntimeState.FadingOut)
{
return;
}
_fadeTimer -= deltaTime;
if (_fadeTimer <= 0f)
{
StopImmediate(true);
return;
}
float fadeScale = _fadeTimer / Mathf.Max(_fadeDuration, MinFadeOutSeconds);
ApplyRuntimeVolume(fadeScale);
}
internal void Shutdown()
{
StopImmediate(true);
_sourceObject = null;
_source = null;
_lowPassFilter = null;
_transform = null;
_service = null;
_category = null;
}
internal void FillDebugInfo(AudioAgentDebugInfo info)
{
if (info == null)
{
return;
}
info.Type = _category != null ? _category.Type : AudioType.Sound;
info.State = _state;
info.Index = Index;
info.GlobalIndex = GlobalIndex;
info.ActiveIndex = ActiveIndex;
info.Handle = _handle;
info.Address = _clipEntry != null ? _clipEntry.Address : null;
info.Clip = _source != null ? _source.clip : null;
info.FollowTarget = _followTarget;
info.Position = _transform != null ? _transform.position : Vector3.zero;
info.Loop = _loop;
info.Spatial = _spatial;
info.Occluded = _occluded;
info.Volume = _source != null ? _source.volume : 0f;
info.Pitch = _source != null ? _source.pitch : 0f;
info.SpatialBlend = _source != null ? _source.spatialBlend : 0f;
info.MinDistance = _source != null ? _source.minDistance : 0f;
info.MaxDistance = _source != null ? _source.maxDistance : 0f;
info.StartedAt = _startedAt;
}
private void BindSource(AudioSourceObject sourceObject)
{
_sourceObject = sourceObject;
_source = sourceObject.Source;
_lowPassFilter = sourceObject.LowPassFilter;
_transform = _source.transform;
}
private void StartClip(AudioClip clip)
{
if (_source == null || clip == null)
{
StopImmediate(true);
return;
}
_source.clip = clip;
if (clip != null)
_source.loop = _loop;
ApplyRuntimeVolume(1f);
_source.Play();
_state = AudioAgentRuntimeState.Playing;
}
private void StopImmediate(bool notifyCategory)
{
if (_state == AudioAgentRuntimeState.Free)
{
_source.Play();
_audioAgentRuntimeState = AudioAgentRuntimeState.Playing;
_duration = 0;
return;
}
else
_generation++;
if (_generation == int.MaxValue)
{
_audioAgentRuntimeState = AudioAgentRuntimeState.End;
_generation = 1;
}
if (_source != null)
{
_source.Stop();
_source.clip = null;
}
ReleaseClip();
ulong handle = _handle;
if (handle != 0)
{
_service.ReleaseHandle(handle, this);
}
ResetState();
if (notifyCategory)
{
_category.MarkFree(this);
}
}
/// <summary>
/// 初始化音频代理辅助器。
/// </summary>
/// <param name="audioCategory">音频轨道(类别)。</param>
/// <param name="index">音频代理辅助器编号。</param>
public void Init(AudioCategory audioCategory, int index = 0)
private void ReleaseClip()
{
_audioService = AppServices.Require<IAudioService>();
_resourceService = AppServices.Require<IResourceService>();
GameObject host = new GameObject(Utility.Text.Format("Audio Agent Helper - {0} - {1}", audioCategory.AudioMixerGroup.name, index));
host.transform.SetParent(audioCategory.InstanceRoot);
host.transform.localPosition = Vector3.zero;
_transform = host.transform;
_source = host.AddComponent<AudioSource>();
AudioClipCacheEntry entry = _clipEntry;
if (entry != null)
{
_clipEntry = null;
_service.ReleaseClip(entry);
}
}
private void ResetState()
{
_state = AudioAgentRuntimeState.Free;
_followTarget = null;
_followOffset = Vector3.zero;
_spatial = false;
_occluded = false;
_loop = false;
_handle = 0;
_baseVolume = 1f;
_pitch = 1f;
_fadeTimer = 0f;
_fadeDuration = 0f;
_startedAt = 0f;
_nextOcclusionCheckTime = 0f;
if (_source != null)
{
_source.volume = 1f;
_source.pitch = 1f;
_source.loop = false;
}
if (_lowPassFilter != null)
{
_lowPassFilter.enabled = false;
_lowPassFilter.cutoffFrequency = MaxCutoffFrequency;
}
}
private void ApplySourceSettings(AudioPlayRequest request)
{
AudioGroupConfig config = _category.Config;
_source.playOnAwake = false;
AudioMixerGroup[] audioMixerGroups =
audioCategory.AudioMixer.FindMatchingGroups(Utility.Text.Format("Master/{0}/{1}", audioCategory.AudioMixerGroup.name,
$"{audioCategory.AudioMixerGroup.name} - {index}"));
_source.outputAudioMixerGroup = audioMixerGroups.Length > 0 ? audioMixerGroups[0] : audioCategory.AudioMixerGroup;
_source.rolloffMode = audioCategory.AudioGroupConfig.audioRolloffMode;
_source.minDistance = audioCategory.AudioGroupConfig.minDistance;
_source.maxDistance = audioCategory.AudioGroupConfig.maxDistance;
_instanceId = _source.GetInstanceID();
}
_source.mute = false;
_source.bypassEffects = false;
_source.bypassListenerEffects = false;
_source.bypassReverbZones = false;
_source.priority = config.SourcePriority;
_source.pitch = _pitch;
_source.rolloffMode = request.OverrideSpatialSettings ? request.RolloffMode : config.RolloffMode;
_source.minDistance = request.OverrideSpatialSettings ? request.MinDistance : config.MinDistance;
_source.maxDistance = request.OverrideSpatialSettings ? request.MaxDistance : config.MaxDistance;
_source.dopplerLevel = config.DopplerLevel;
_source.spread = config.Spread;
_source.reverbZoneMix = config.ReverbZoneMix;
_source.outputAudioMixerGroup = _category.MixerGroup;
_source.spatialBlend = _spatial ? ResolveSpatialBlend(request, config) : 0f;
/// <summary>
/// 加载音频代理辅助器。
/// </summary>
/// <param name="path">资源路径。</param>
/// <param name="bAsync">是否异步。</param>
/// <param name="bInPool">是否池化。</param>
public void Load(string path, bool bAsync, bool bInPool = false)
{
_inPool = bInPool;
if (_audioAgentRuntimeState == AudioAgentRuntimeState.None || _audioAgentRuntimeState == AudioAgentRuntimeState.End)
Transform transform = _source.transform;
if (_followTarget != null)
{
_duration = 0;
if (!string.IsNullOrEmpty(path))
{
if (bInPool && _audioService.AudioClipPool.TryGetValue(path, out var operationHandle))
{
OnAssetLoadComplete(operationHandle);
return;
}
if (bAsync)
{
_audioAgentRuntimeState = AudioAgentRuntimeState.Loading;
AssetHandle handle = _resourceService.LoadAssetAsyncHandle<AudioClip>(path);
handle.Completed += OnAssetLoadComplete;
}
else
{
AssetHandle handle = _resourceService.LoadAssetSyncHandle<AudioClip>(path);
OnAssetLoadComplete(handle);
}
}
transform.SetParent(_category.InstanceRoot, false);
transform.position = _followTarget.position + _followOffset;
transform.rotation = _followTarget.rotation;
}
else
{
_pendingLoad = new LoadRequest { Path = path, BAsync = bAsync, BInPool = bInPool };
if (_audioAgentRuntimeState == AudioAgentRuntimeState.Playing)
transform.SetParent(_category.InstanceRoot, false);
if (request.UseWorldPosition)
{
Stop(true);
}
}
}
/// <summary>
/// 停止播放音频代理辅助器。
/// </summary>
/// <param name="fadeout">是否渐出。</param>
public void Stop(bool fadeout = false)
{
if (_source != null)
{
if (fadeout)
{
_fadeoutTimer = FADEOUT_DURATION;
_audioAgentRuntimeState = AudioAgentRuntimeState.FadingOut;
transform.position = request.Position;
}
else
{
_source.Stop();
_audioAgentRuntimeState = AudioAgentRuntimeState.End;
transform.localPosition = Vector3.zero;
}
}
}
/// <summary>
/// 暂停音频代理辅助器。
/// </summary>
public void Pause()
private static float ResolveSpatialBlend(AudioPlayRequest request, AudioGroupConfig config)
{
if (_source != null)
if (request.SpatialBlend >= 0f)
{
_source.Pause();
return Mathf.Clamp01(request.SpatialBlend);
}
return config.SpatialBlend;
}
/// <summary>
/// 取消暂停音频代理辅助器。
/// </summary>
public void UnPause()
private void UpdateFollowTarget()
{
if (_source != null)
if (_followTarget == null)
{
_source.UnPause();
return;
}
if (_transform == null)
{
return;
}
if (!_followTarget.gameObject.activeInHierarchy)
{
StopImmediate(true);
return;
}
_transform.position = _followTarget.position + _followOffset;
_transform.rotation = _followTarget.rotation;
}
/// <summary>
/// 资源加载完成。
/// </summary>
/// <param name="handle">资源操作句柄。</param>
void OnAssetLoadComplete(AssetHandle handle)
private void UpdateOcclusion()
{
if (handle != null)
AudioGroupConfig config = _category.Config;
if (!_spatial || !config.OcclusionEnabled || _transform == null)
{
if (_inPool)
{
_audioService.AudioClipPool.TryAdd(handle.GetAssetInfo().Address, handle);
}
return;
}
if (_pendingLoad != null)
Transform listener = _service.ListenerTransform;
if (listener == null)
{
if (!_inPool && handle != null)
{
handle.Dispose();
}
_audioAgentRuntimeState = AudioAgentRuntimeState.End;
string path = _pendingLoad.Path;
bool bAsync = _pendingLoad.BAsync;
bool bInPool = _pendingLoad.BInPool;
_pendingLoad = null;
Load(path, bAsync, bInPool);
return;
}
else if (handle != null)
float now = Time.realtimeSinceStartup;
if (now < _nextOcclusionCheckTime)
{
if (_audioData != null)
{
AudioData.DeAlloc(_audioData);
_audioData = null;
}
_audioData = AudioData.Alloc(handle, _inPool);
_source.clip = handle.AssetObject as AudioClip;
if (_source.clip != null)
{
_source.Play();
_audioAgentRuntimeState = AudioAgentRuntimeState.Playing;
}
else
{
_audioAgentRuntimeState = AudioAgentRuntimeState.End;
}
return;
}
else
_nextOcclusionCheckTime = now + config.OcclusionCheckInterval;
Vector3 origin = _transform.position;
Vector3 target = listener.position;
Vector3 direction = target - origin;
float distance = direction.magnitude;
if (distance <= 0.01f)
{
_audioAgentRuntimeState = AudioAgentRuntimeState.End;
SetOccluded(false, config);
return;
}
bool occluded = Physics.Raycast(origin, direction / distance, distance, config.OcclusionMask, QueryTriggerInteraction.Ignore);
SetOccluded(occluded, config);
}
/// <summary>
/// 轮询音频代理辅助器。
/// </summary>
/// <param name="elapseSeconds">逻辑流逝时间,以秒为单位。</param>
public void Update(float elapseSeconds)
private void SetOccluded(bool occluded, AudioGroupConfig config)
{
if (_audioAgentRuntimeState == AudioAgentRuntimeState.Playing)
if (_occluded == occluded)
{
if (!_source.isPlaying)
{
_audioAgentRuntimeState = AudioAgentRuntimeState.End;
}
}
else if (_audioAgentRuntimeState == AudioAgentRuntimeState.FadingOut)
{
if (_fadeoutTimer > 0f)
{
_fadeoutTimer -= elapseSeconds;
_source.volume = _volume * _fadeoutTimer / FADEOUT_DURATION;
}
else
{
Stop();
if (_pendingLoad != null)
{
string path = _pendingLoad.Path;
bool bAsync = _pendingLoad.BAsync;
bool bInPool = _pendingLoad.BInPool;
_pendingLoad = null;
Load(path, bAsync, bInPool);
}
_source.volume = _volume;
}
return;
}
_duration += elapseSeconds;
_occluded = occluded;
if (_lowPassFilter != null)
{
_lowPassFilter.enabled = occluded;
_lowPassFilter.cutoffFrequency = occluded ? config.OcclusionLowPassCutoff : MaxCutoffFrequency;
}
ApplyRuntimeVolume(_state == AudioAgentRuntimeState.FadingOut ? _fadeTimer / Mathf.Max(_fadeDuration, MinFadeOutSeconds) : 1f);
}
/// <summary>
/// 销毁音频代理辅助器。
/// </summary>
public void Destroy()
private void ApplyRuntimeVolume(float fadeScale)
{
if (_transform != null)
if (_source == null)
{
Object.Destroy(_transform.gameObject);
return;
}
if (_audioData != null)
{
AudioData.DeAlloc(_audioData);
}
float occlusionScale = _occluded ? _category.Config.OcclusionVolumeMultiplier : 1f;
_source.volume = _baseVolume * occlusionScale * fadeScale;
}
}
}

View File

@ -1,33 +1,11 @@
namespace AlicizaX.Audio.Runtime
namespace AlicizaX.Audio.Runtime
{
/// <summary>
/// 音频代理辅助器运行时状态枚举。
/// </summary>
public enum AudioAgentRuntimeState
internal enum AudioAgentRuntimeState
{
/// <summary>
/// 无状态。
/// </summary>
None,
/// <summary>
/// 加载中状态。
/// </summary>
Loading,
/// <summary>
/// 播放中状态。
/// </summary>
Playing,
/// <summary>
/// 渐渐消失状态。
/// </summary>
FadingOut,
/// <summary>
/// 结束状态。
/// </summary>
End,
};
Free = 0,
Loading = 1,
Playing = 2,
Paused = 3,
FadingOut = 4
}
}

View File

@ -1,239 +1,334 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Audio;
namespace AlicizaX.Audio.Runtime
{
/// <summary>
/// 音频轨道(类别)。
/// </summary>
[Serializable]
public class AudioCategory
internal sealed class AudioCategory
{
[SerializeField] private AudioMixer audioMixer = null;
private readonly AudioService _service;
private readonly AudioAgent[] _agents;
private readonly AudioAgent[] _activeAgents;
private readonly AudioAgent[] _playHeap;
private readonly int[] _freeStack;
private int _activeCount;
private int _heapCount;
private int _freeCount;
private bool _enabled;
public List<AudioAgent> AudioAgents;
private readonly AudioMixerGroup _audioMixerGroup;
private AudioGroupConfig _audioGroupConfig;
private int _maxChannel;
private bool _bEnable = true;
/// <summary>
/// 音频混响器。
/// </summary>
public AudioMixer AudioMixer => audioMixer;
/// <summary>
/// 音频混响器组。
/// </summary>
public AudioMixerGroup AudioMixerGroup => _audioMixerGroup;
/// <summary>
/// 音频组配置。
/// </summary>
public AudioGroupConfig AudioGroupConfig => _audioGroupConfig;
/// <summary>
/// 实例化根节点。
/// </summary>
public Transform InstanceRoot { private set; get; }
/// <summary>
/// 音频轨道是否启用。
/// </summary>
public bool Enable
internal AudioType Type { get; }
internal int TypeIndex { get; }
internal Transform InstanceRoot { get; private set; }
internal AudioMixerGroup MixerGroup { get; }
internal AudioGroupConfig Config { get; }
internal int Capacity => _agents.Length;
internal int ActiveCount => _activeCount;
internal int FreeCount => _freeCount;
internal int HeapCount => _heapCount;
internal bool Enabled
{
get => _bEnable;
get => _enabled;
set
{
if (_bEnable != value)
if (_enabled == value)
{
_bEnable = value;
if (!_bEnable)
{
foreach (var audioAgent in AudioAgents)
{
if (audioAgent != null)
{
audioAgent.Stop();
}
}
}
return;
}
_enabled = value;
if (!_enabled)
{
Stop(false);
}
}
}
/// <summary>
/// 音频轨道构造函数。
/// </summary>
/// <param name="maxChannel">最大Channel。</param>
/// <param name="audioMixer">音频混响器。</param>
/// <param name="audioGroupConfig">音频轨道组配置。</param>
public AudioCategory(int maxChannel, AudioMixer audioMixer, AudioGroupConfig audioGroupConfig)
internal AudioCategory(AudioService service, AudioMixer audioMixer, AudioGroupConfig config, int globalIndexOffset)
{
var audioModule = AppServices.Require<IAudioService>();
_service = service;
Config = config;
Type = config.AudioType;
TypeIndex = (int)config.AudioType;
_enabled = !config.Mute;
this.audioMixer = audioMixer;
_maxChannel = maxChannel;
_audioGroupConfig = audioGroupConfig;
AudioMixerGroup[] audioMixerGroups = audioMixer.FindMatchingGroups(Utility.Text.Format("Master/{0}", audioGroupConfig.AudioType.ToString()));
if (audioMixerGroups.Length > 0)
{
_audioMixerGroup = audioMixerGroups[0];
}
else
{
_audioMixerGroup = audioMixer.FindMatchingGroups("Master")[0];
}
MixerGroup = ResolveMixerGroup(audioMixer, config);
InstanceRoot = new GameObject("Audio Category - " + Type).transform;
InstanceRoot.SetParent(service.InstanceRoot, false);
AudioAgents = new List<AudioAgent>(32);
InstanceRoot = new GameObject(Utility.Text.Format("Audio Category - {0}", _audioMixerGroup.name)).transform;
InstanceRoot.SetParent(audioModule.InstanceRoot);
for (int index = 0; index < _maxChannel; index++)
int capacity = config.AgentHelperCount;
_agents = new AudioAgent[capacity];
_activeAgents = new AudioAgent[capacity];
_playHeap = new AudioAgent[capacity];
_freeStack = new int[capacity];
_freeCount = capacity;
for (int i = 0; i < capacity; i++)
{
AudioAgent audioAgent = new AudioAgent();
audioAgent.Init(this, index);
AudioAgents.Add(audioAgent);
AudioSourceObject sourceObject = service.AcquireSourceObject(this, i);
AudioAgent agent = new AudioAgent();
agent.Initialize(service, this, i, globalIndexOffset + i, sourceObject);
_agents[i] = agent;
_freeStack[i] = capacity - 1 - i;
}
}
/// <summary>
/// 增加音频。
/// </summary>
/// <param name="num"></param>
public void AddAudio(int num)
internal ulong Play(AudioPlayRequest request)
{
_maxChannel += num;
for (int i = 0; i < num; i++)
if (!_enabled)
{
AudioAgents.Add(null);
return 0UL;
}
AudioAgent agent = AcquireAgent();
return agent != null ? agent.Play(request) : 0UL;
}
internal void Stop(bool fadeout)
{
for (int i = _activeCount - 1; i >= 0; i--)
{
AudioAgent agent = _activeAgents[i];
if (agent != null)
{
agent.Stop(fadeout);
}
}
}
public AudioAgent Play(AudioClip clip)
internal void Update(float deltaTime)
{
if (!_bEnable)
int i = 0;
while (i < _activeCount)
{
AudioAgent agent = _activeAgents[i];
agent.Update(deltaTime);
if (i < _activeCount && _activeAgents[i] == agent)
{
i++;
}
}
}
internal bool TryGetAgent(int index, out AudioAgent agent)
{
if ((uint)index >= (uint)_agents.Length)
{
agent = null;
return false;
}
agent = _agents[index];
return agent != null;
}
internal void FillDebugInfo(float volume, AudioCategoryDebugInfo info)
{
if (info == null)
{
return;
}
info.Type = Type;
info.Enabled = _enabled;
info.Volume = volume;
info.Capacity = _agents.Length;
info.ActiveCount = _activeCount;
info.FreeCount = _freeCount;
info.HeapCount = _heapCount;
}
internal void MarkOccupied(AudioAgent agent)
{
if (agent.ActiveIndex < 0)
{
agent.ActiveIndex = _activeCount;
_activeAgents[_activeCount++] = agent;
}
if (agent.HeapIndex < 0)
{
int heapIndex = _heapCount++;
_playHeap[heapIndex] = agent;
agent.HeapIndex = heapIndex;
SiftHeapUp(heapIndex);
}
}
internal void MarkFree(AudioAgent agent)
{
RemoveActive(agent);
RemoveHeap(agent);
if (_freeCount < _freeStack.Length)
{
_freeStack[_freeCount++] = agent.Index;
}
}
internal void Shutdown()
{
Stop(false);
for (int i = 0; i < _agents.Length; i++)
{
AudioAgent agent = _agents[i];
if (agent != null)
{
agent.Shutdown();
_service.ReleaseSourceObject(TypeIndex, i);
_agents[i] = null;
}
}
_activeCount = 0;
_heapCount = 0;
_freeCount = 0;
if (InstanceRoot != null)
{
Object.Destroy(InstanceRoot.gameObject);
InstanceRoot = null;
}
}
private AudioAgent AcquireAgent()
{
if (_freeCount > 0)
{
int index = _freeStack[--_freeCount];
return _agents[index];
}
if (_heapCount <= 0)
{
return null;
}
int freeChannel = -1;
float duration = -1;
AudioAgent oldest = _playHeap[0];
RemoveActive(oldest);
RemoveHeapAt(0);
return oldest;
}
for (int i = 0; i < AudioAgents.Count; i++)
private void RemoveActive(AudioAgent agent)
{
int index = agent.ActiveIndex;
if (index < 0)
{
if (AudioAgents[i].IsFree)
return;
}
int lastIndex = --_activeCount;
AudioAgent last = _activeAgents[lastIndex];
_activeAgents[lastIndex] = null;
if (index != lastIndex)
{
_activeAgents[index] = last;
last.ActiveIndex = index;
}
agent.ActiveIndex = -1;
}
private void RemoveHeap(AudioAgent agent)
{
int index = agent.HeapIndex;
if (index >= 0)
{
RemoveHeapAt(index);
}
}
private void RemoveHeapAt(int index)
{
int lastIndex = --_heapCount;
AudioAgent removed = _playHeap[index];
AudioAgent last = _playHeap[lastIndex];
_playHeap[lastIndex] = null;
removed.HeapIndex = -1;
if (index == lastIndex)
{
return;
}
_playHeap[index] = last;
last.HeapIndex = index;
int parent = (index - 1) >> 1;
if (index > 0 && IsOlder(last, _playHeap[parent]))
{
SiftHeapUp(index);
}
else
{
SiftHeapDown(index);
}
}
private void SiftHeapUp(int index)
{
AudioAgent item = _playHeap[index];
while (index > 0)
{
int parent = (index - 1) >> 1;
AudioAgent parentAgent = _playHeap[parent];
if (!IsOlder(item, parentAgent))
{
freeChannel = i;
break;
}
else if (AudioAgents[i].Duration > duration)
{
duration = AudioAgents[i].Duration;
freeChannel = i;
}
_playHeap[index] = parentAgent;
parentAgent.HeapIndex = index;
index = parent;
}
if (freeChannel >= 0)
{
if (AudioAgents[freeChannel] == null)
{
AudioAgents[freeChannel] = AudioAgent.Create(clip, this);
}
else
{
AudioAgents[freeChannel].SetAudioClip(clip);
}
return AudioAgents[freeChannel];
}
else
{
Log.Error($"Here is no channel to play audio clip");
return null;
}
_playHeap[index] = item;
item.HeapIndex = index;
}
/// <summary>
/// 播放音频。
/// </summary>
/// <param name="path"></param>
/// <param name="bAsync"></param>
/// <param name="bInPool"></param>
/// <returns></returns>
public AudioAgent Play(string path, bool bAsync, bool bInPool = false)
private void SiftHeapDown(int index)
{
if (!_bEnable)
AudioAgent item = _playHeap[index];
int half = _heapCount >> 1;
while (index < half)
{
return null;
}
int freeChannel = -1;
float duration = -1;
for (int i = 0; i < AudioAgents.Count; i++)
{
if (AudioAgents[i].AudioData?.AssetHandle == null || AudioAgents[i].IsFree)
int child = (index << 1) + 1;
int right = child + 1;
AudioAgent childAgent = _playHeap[child];
if (right < _heapCount && IsOlder(_playHeap[right], childAgent))
{
child = right;
childAgent = _playHeap[child];
}
if (!IsOlder(childAgent, item))
{
freeChannel = i;
break;
}
else if (AudioAgents[i].Duration > duration)
{
duration = AudioAgents[i].Duration;
freeChannel = i;
}
_playHeap[index] = childAgent;
childAgent.HeapIndex = index;
index = child;
}
if (freeChannel >= 0)
{
if (AudioAgents[freeChannel] == null)
{
AudioAgents[freeChannel] = AudioAgent.Create(path, bAsync, this, bInPool);
}
else
{
AudioAgents[freeChannel].Load(path, bAsync, bInPool);
}
return AudioAgents[freeChannel];
}
else
{
Log.Error($"Here is no channel to play audio {path}");
return null;
}
_playHeap[index] = item;
item.HeapIndex = index;
}
/// <summary>
/// 暂停音频。
/// </summary>
/// <param name="fadeout">是否渐出</param>
public void Stop(bool fadeout)
private static bool IsOlder(AudioAgent left, AudioAgent right)
{
for (int i = 0; i < AudioAgents.Count; ++i)
{
if (AudioAgents[i] != null)
{
AudioAgents[i].Stop(fadeout);
}
}
return left.StartedAt < right.StartedAt;
}
/// <summary>
/// 音频轨道轮询。
/// </summary>
/// <param name="elapseSeconds">逻辑流逝时间,以秒为单位。</param>
public void Update(float elapseSeconds)
private static AudioMixerGroup ResolveMixerGroup(AudioMixer audioMixer, AudioGroupConfig config)
{
for (int i = 0; i < AudioAgents.Count; ++i)
AudioMixerGroup[] groups = audioMixer.FindMatchingGroups("Master/" + config.AudioType);
if (groups != null && groups.Length > 0)
{
if (AudioAgents[i] != null)
{
AudioAgents[i].Update(elapseSeconds);
}
return groups[0];
}
groups = audioMixer.FindMatchingGroups("Master");
return groups != null && groups.Length > 0 ? groups[0] : null;
}
}
}

View File

@ -0,0 +1,145 @@
using System;
using AlicizaX;
using UnityEngine;
using YooAsset;
namespace AlicizaX.Audio.Runtime
{
internal sealed class AudioClipCacheEntry : IMemory
{
private readonly Action<AssetHandle> _completedCallback;
public AudioService Owner;
public string Address;
public AssetHandle Handle;
public AudioClip Clip;
public AudioLoadRequest PendingHead;
public AudioLoadRequest PendingTail;
public AudioClipCacheEntry LruPrev;
public AudioClipCacheEntry LruNext;
public AudioClipCacheEntry AllPrev;
public AudioClipCacheEntry AllNext;
public int RefCount;
public bool Loading;
public bool Pinned;
public bool CacheAfterUse;
public bool InLru;
public float LastUseTime;
public AudioClipCacheEntry()
{
_completedCallback = OnLoadCompleted;
}
public Action<AssetHandle> CompletedCallback => _completedCallback;
public bool IsLoaded => Clip != null && Handle is { IsValid: true } && !Loading;
public void Initialize(AudioService owner, string address, bool pinned)
{
Owner = owner;
Address = address;
Pinned = pinned;
CacheAfterUse = pinned;
LastUseTime = Time.realtimeSinceStartup;
}
public void AddPending(AudioLoadRequest request)
{
request.Next = null;
if (PendingTail == null)
{
PendingHead = request;
PendingTail = request;
return;
}
PendingTail.Next = request;
PendingTail = request;
}
public int CountPending()
{
int count = 0;
AudioLoadRequest request = PendingHead;
while (request != null)
{
count++;
request = request.Next;
}
return count;
}
public void FillDebugInfo(AudioClipCacheDebugInfo info)
{
if (info == null)
{
return;
}
info.Address = Address;
info.Clip = Clip;
info.RefCount = RefCount;
info.PendingCount = CountPending();
info.Loading = Loading;
info.Pinned = Pinned;
info.CacheAfterUse = CacheAfterUse;
info.InLru = InLru;
info.IsLoaded = IsLoaded;
info.HasValidHandle = Handle is { IsValid: true };
info.LastUseTime = LastUseTime;
}
public void Clear()
{
if (Handle is { IsValid: true })
{
if (Loading)
{
Handle.Completed -= _completedCallback;
}
Handle.Dispose();
}
AudioLoadRequest request = PendingHead;
while (request != null)
{
AudioLoadRequest next = request.Next;
MemoryPool.Release(request);
request = next;
}
Owner = null;
Address = null;
Handle = null;
Clip = null;
PendingHead = null;
PendingTail = null;
LruPrev = null;
LruNext = null;
AllPrev = null;
AllNext = null;
RefCount = 0;
Loading = false;
Pinned = false;
CacheAfterUse = false;
InLru = false;
LastUseTime = 0f;
}
private void OnLoadCompleted(AssetHandle handle)
{
AudioService owner = Owner;
if (owner != null)
{
owner.OnClipLoadCompleted(this, handle);
}
else if (handle is { IsValid: true })
{
handle.Dispose();
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7e438d922e4ac1e439e5a30a0b9d9ac7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -4,19 +4,14 @@ using UnityEngine.Audio;
namespace AlicizaX.Audio.Runtime
{
/// <summary>
/// 音效管理,为游戏提供统一的音效播放接口。
/// </summary>
/// <remarks>场景3D音效挂到场景物件、技能3D音效挂到技能特效上并在AudioSource的Output上设置对应分类的AudioMixerGroup</remarks>
[DisallowMultipleComponent]
[AddComponentMenu("Game Framework/Audio")]
public sealed class AudioComponent : MonoBehaviour
{
[SerializeField] private AudioMixer m_AudioMixer;
[SerializeField] private Transform m_InstanceRoot = null;
[SerializeField] private AudioGroupConfig[] m_AudioGroupConfigs = null;
[SerializeField] private Transform m_InstanceRoot;
[SerializeField] private AudioListener m_AudioListener;
[SerializeField] private AudioGroupConfigCollection m_AudioGroupConfigs;
private IAudioService _audioService;
@ -25,25 +20,53 @@ namespace AlicizaX.Audio.Runtime
_audioService = AppServices.RegisterApp(new AudioService());
}
/// <summary>
/// 初始化音频模块。
/// </summary>
void Start()
private void Start()
{
if (m_InstanceRoot == null)
EnsureInstanceRoot();
EnsureAudioMixer();
AudioGroupConfig[] configs = m_AudioGroupConfigs != null ? m_AudioGroupConfigs.GroupConfigs : null;
_audioService.Initialize(configs, m_InstanceRoot, m_AudioMixer);
if (m_AudioListener != null)
{
m_InstanceRoot = new GameObject("[AudioService Instances]").transform;
m_InstanceRoot.SetParent(gameObject.transform);
m_InstanceRoot.localScale = Vector3.one;
_audioService.RegisterListener(m_AudioListener);
}
}
private void OnEnable()
{
if (_audioService != null && m_AudioListener != null)
{
_audioService.RegisterListener(m_AudioListener);
}
}
private void OnDisable()
{
if (_audioService != null && m_AudioListener != null)
{
_audioService.UnregisterListener(m_AudioListener);
}
}
private void EnsureInstanceRoot()
{
if (m_InstanceRoot != null)
{
return;
}
m_InstanceRoot = new GameObject("[AudioService Instances]").transform;
m_InstanceRoot.SetParent(transform, false);
m_InstanceRoot.localScale = Vector3.one;
}
private void EnsureAudioMixer()
{
if (m_AudioMixer == null)
{
m_AudioMixer = Resources.Load<AudioMixer>("AudioMixer");
}
_audioService.Initialize(m_AudioGroupConfigs, m_InstanceRoot, m_AudioMixer);
}
}
}

View File

@ -1,61 +0,0 @@
using AlicizaX;
using YooAsset;
namespace AlicizaX.Audio.Runtime
{
/// <summary>
/// 音频数据。
/// </summary>
public class AudioData : IMemory
{
/// <summary>
/// 资源句柄。
/// </summary>
public AssetHandle AssetHandle { private set; get; }
/// <summary>
/// 是否使用对象池。
/// </summary>
public bool InPool { private set; get; } = false;
/// <summary>
/// 生成音频数据。
/// </summary>
/// <param name="assetHandle">资源操作句柄。</param>
/// <param name="inPool">是否使用对象池。</param>
/// <returns>音频数据。</returns>
internal static AudioData Alloc(AssetHandle assetHandle, bool inPool)
{
AudioData ret = MemoryPool.Acquire<AudioData>();
ret.AssetHandle = assetHandle;
ret.InPool = inPool;
return ret;
}
/// <summary>
/// 回收音频数据。
/// </summary>
/// <param name="audioData"></param>
internal static void DeAlloc(AudioData audioData)
{
if (audioData == null)
return;
MemoryPool.Release(audioData);
}
public void Clear()
{
bool inPool = InPool;
AssetHandle handle = AssetHandle;
InPool = false;
AssetHandle = null;
if (!inPool && handle is { IsValid: true })
handle.Dispose();
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 036a1af4acb84666b73909ba28455cfa
timeCreated: 1742472335

View File

@ -0,0 +1,148 @@
using UnityEngine;
namespace AlicizaX.Audio.Runtime
{
internal interface IAudioDebugService
{
int CategoryCount { get; }
int ClipCacheCount { get; }
int ClipCacheCapacity { get; }
int HandleCapacity { get; }
bool Initialized { get; }
bool UnityAudioDisabled { get; }
AudioClipCacheEntry FirstClipCacheEntry { get; }
void FillServiceDebugInfo(AudioServiceDebugInfo info);
bool FillCategoryDebugInfo(int typeIndex, AudioCategoryDebugInfo info);
bool FillAgentDebugInfo(int typeIndex, int agentIndex, AudioAgentDebugInfo info);
bool FillClipCacheDebugInfo(AudioClipCacheEntry entry, AudioClipCacheDebugInfo info);
}
internal sealed class AudioServiceDebugInfo
{
public bool Initialized;
public bool UnityAudioDisabled;
public bool Enable;
public float Volume;
public int CategoryCount;
public int ActiveAgentCount;
public int HandleCapacity;
public int ClipCacheCount;
public int ClipCacheCapacity;
public AudioListener Listener;
public Transform InstanceRoot;
public void Clear()
{
Initialized = false;
UnityAudioDisabled = false;
Enable = false;
Volume = 0f;
CategoryCount = 0;
ActiveAgentCount = 0;
HandleCapacity = 0;
ClipCacheCount = 0;
ClipCacheCapacity = 0;
Listener = null;
InstanceRoot = null;
}
}
internal sealed class AudioCategoryDebugInfo
{
public AudioType Type;
public bool Enabled;
public float Volume;
public int Capacity;
public int ActiveCount;
public int FreeCount;
public int HeapCount;
public void Clear()
{
Type = AudioType.Sound;
Enabled = false;
Volume = 0f;
Capacity = 0;
ActiveCount = 0;
FreeCount = 0;
HeapCount = 0;
}
}
internal sealed class AudioAgentDebugInfo
{
public AudioType Type;
public AudioAgentRuntimeState State;
public int Index;
public int GlobalIndex;
public int ActiveIndex;
public ulong Handle;
public string Address;
public AudioClip Clip;
public Transform FollowTarget;
public Vector3 Position;
public bool Loop;
public bool Spatial;
public bool Occluded;
public float Volume;
public float Pitch;
public float SpatialBlend;
public float MinDistance;
public float MaxDistance;
public float StartedAt;
public void Clear()
{
Type = AudioType.Sound;
State = AudioAgentRuntimeState.Free;
Index = 0;
GlobalIndex = 0;
ActiveIndex = -1;
Handle = 0UL;
Address = null;
Clip = null;
FollowTarget = null;
Position = Vector3.zero;
Loop = false;
Spatial = false;
Occluded = false;
Volume = 0f;
Pitch = 0f;
SpatialBlend = 0f;
MinDistance = 0f;
MaxDistance = 0f;
StartedAt = 0f;
}
}
internal sealed class AudioClipCacheDebugInfo
{
public string Address;
public AudioClip Clip;
public int RefCount;
public int PendingCount;
public bool Loading;
public bool Pinned;
public bool CacheAfterUse;
public bool InLru;
public bool IsLoaded;
public bool HasValidHandle;
public float LastUseTime;
public void Clear()
{
Address = null;
Clip = null;
RefCount = 0;
PendingCount = 0;
Loading = false;
Pinned = false;
CacheAfterUse = false;
InLru = false;
IsLoaded = false;
HasValidHandle = false;
LastUseTime = 0f;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 81dfdc57e55b43799a3451eeca4e40fa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,48 +1,72 @@
using System;
using System;
using UnityEngine;
namespace AlicizaX.Audio.Runtime
{
/// <summary>
/// 音频轨道组配置。
/// </summary>
[Serializable]
public sealed class AudioGroupConfig
{
[SerializeField] private string m_Name = null;
[SerializeField] private bool m_Mute = false;
[SerializeField, Range(0f, 1f)] private float m_Volume = 1f;
[SerializeField] private int m_AgentHelperCount = 1;
[SerializeField, Min(1)] private int m_AgentHelperCount = 8;
[SerializeField] private string m_ExposedVolumeParameter = null;
[SerializeField, Range(0f, 1f)] private float m_SpatialBlend = 1f;
[SerializeField, Range(0f, 5f)] private float m_DopplerLevel = 1f;
[SerializeField, Range(0f, 360f)] private float m_Spread = 0f;
[SerializeField, Range(0, 256)] private int m_SourcePriority = 128;
[SerializeField, Range(0f, 1.1f)] private float m_ReverbZoneMix = 1f;
[SerializeField] private bool m_OcclusionEnabled = false;
[SerializeField] private LayerMask m_OcclusionMask = ~0;
[SerializeField, Min(0.02f)] private float m_OcclusionCheckInterval = 0.12f;
[SerializeField, Range(500f, 22000f)] private float m_OcclusionLowPassCutoff = 1200f;
[SerializeField, Range(0f, 1f)] private float m_OcclusionVolumeMultiplier = 0.55f;
public AudioType AudioType;
public AudioRolloffMode audioRolloffMode = AudioRolloffMode.Logarithmic;
public float minDistance = 1f;
public float maxDistance = 500f;
public string Name
{
get { return m_Name; }
}
public string Name => m_Name;
public bool Mute => m_Mute;
public float Volume => m_Volume;
public int AgentHelperCount => m_AgentHelperCount > 0 ? m_AgentHelperCount : 1;
public string ExposedVolumeParameter => m_ExposedVolumeParameter;
public float SpatialBlend => m_SpatialBlend;
public float DopplerLevel => m_DopplerLevel;
public float Spread => m_Spread;
public int SourcePriority => m_SourcePriority;
public float ReverbZoneMix => m_ReverbZoneMix;
public bool OcclusionEnabled => m_OcclusionEnabled;
public LayerMask OcclusionMask => m_OcclusionMask;
public float OcclusionCheckInterval => m_OcclusionCheckInterval;
public float OcclusionLowPassCutoff => m_OcclusionLowPassCutoff;
public float OcclusionVolumeMultiplier => m_OcclusionVolumeMultiplier;
public AudioRolloffMode RolloffMode => audioRolloffMode;
public float MinDistance => minDistance;
public float MaxDistance => maxDistance;
public bool Mute
internal void SetDefaults(AudioType type, string name, string exposedVolumeParameter, int agentHelperCount, float spatialBlend, bool occlusionEnabled)
{
get { return m_Mute; }
}
public float Volume
{
get { return m_Volume; }
}
public int AgentHelperCount
{
get { return m_AgentHelperCount; }
AudioType = type;
m_Name = name;
m_Mute = false;
m_Volume = 1f;
m_AgentHelperCount = agentHelperCount;
m_ExposedVolumeParameter = exposedVolumeParameter;
m_SpatialBlend = spatialBlend;
m_DopplerLevel = 1f;
m_Spread = 0f;
m_SourcePriority = type == AudioType.Music ? 32 : 128;
m_ReverbZoneMix = 1f;
m_OcclusionEnabled = occlusionEnabled;
m_OcclusionMask = ~0;
m_OcclusionCheckInterval = 0.12f;
m_OcclusionLowPassCutoff = 1200f;
m_OcclusionVolumeMultiplier = 0.55f;
audioRolloffMode = AudioRolloffMode.Logarithmic;
minDistance = type == AudioType.Music || type == AudioType.UISound ? 1f : 2f;
maxDistance = type == AudioType.Music || type == AudioType.UISound ? 25f : 80f;
}
}
}

View File

@ -0,0 +1,99 @@
using System;
using UnityEngine;
namespace AlicizaX.Audio.Runtime
{
[CreateAssetMenu(fileName = "AudioGroupConfigs", menuName = "AlicizaX/Audio/Audio Group Configs", order = 40)]
public sealed class AudioGroupConfigCollection : ScriptableObject
{
[SerializeField] private AudioGroupConfig[] m_GroupConfigs = CreateDefaultConfigs();
public AudioGroupConfig[] GroupConfigs => m_GroupConfigs;
public void EnsureDefaults()
{
if (m_GroupConfigs == null || m_GroupConfigs.Length == 0)
{
m_GroupConfigs = CreateDefaultConfigs();
return;
}
bool[] found = new bool[(int)AudioType.Max];
int validCount = 0;
for (int i = 0; i < m_GroupConfigs.Length; i++)
{
AudioGroupConfig config = m_GroupConfigs[i];
if (config == null)
{
continue;
}
int index = (int)config.AudioType;
if ((uint)index >= (uint)found.Length || found[index])
{
continue;
}
found[index] = true;
validCount++;
}
if (validCount == (int)AudioType.Max)
{
return;
}
AudioGroupConfig[] defaults = CreateDefaultConfigs();
AudioGroupConfig[] merged = new AudioGroupConfig[(int)AudioType.Max];
for (int i = 0; i < m_GroupConfigs.Length; i++)
{
AudioGroupConfig config = m_GroupConfigs[i];
if (config == null)
{
continue;
}
int index = (int)config.AudioType;
if ((uint)index < (uint)merged.Length && merged[index] == null)
{
merged[index] = config;
}
}
for (int i = 0; i < merged.Length; i++)
{
if (merged[i] == null)
{
merged[i] = defaults[i];
}
}
m_GroupConfigs = merged;
}
#if UNITY_EDITOR
private void OnValidate()
{
EnsureDefaults();
}
#endif
internal static AudioGroupConfig[] CreateDefaultConfigs()
{
AudioGroupConfig[] configs = new AudioGroupConfig[(int)AudioType.Max];
configs[(int)AudioType.Music] = CreateConfig(AudioType.Music, "音乐", "MusicVolume", 2, 0f, false);
configs[(int)AudioType.Sound] = CreateConfig(AudioType.Sound, "音效", "SoundVolume", 24, 1f, false);
configs[(int)AudioType.UISound] = CreateConfig(AudioType.UISound, "界面音效", "UISoundVolume", 12, 0f, false);
configs[(int)AudioType.Voice] = CreateConfig(AudioType.Voice, "语音", "VoiceVolume", 6, 1f, true);
configs[(int)AudioType.Ambient] = CreateConfig(AudioType.Ambient, "环境音", "AmbientVolume", 6, 1f, true);
return configs;
}
private static AudioGroupConfig CreateConfig(AudioType type, string name, string exposedVolumeParameter, int channelCount, float spatialBlend, bool occlusion)
{
AudioGroupConfig config = new AudioGroupConfig();
config.SetDefaults(type, name, exposedVolumeParameter, channelCount, spatialBlend, occlusion);
return config;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 35a9ab9e2f42f744ca3801948d9b8c2e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,18 @@
using AlicizaX;
namespace AlicizaX.Audio.Runtime
{
internal sealed class AudioLoadRequest : IMemory
{
public AudioLoadRequest Next;
public AudioAgent Agent;
public int Generation;
public void Clear()
{
Next = null;
Agent = null;
Generation = 0;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b1b1b306b2b64b24aba7fc6740d9df35
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,172 @@
using AlicizaX;
using UnityEngine;
namespace AlicizaX.Audio.Runtime
{
internal sealed class AudioPlayRequest : IMemory
{
public AudioType Type;
public string Address;
public AudioClip Clip;
public Transform FollowTarget;
public Vector3 Position;
public bool UseWorldPosition;
public bool Loop;
public bool Async;
public bool CacheClip;
public bool Spatial;
public float Volume;
public float Pitch;
public float SpatialBlend;
public float MinDistance;
public float MaxDistance;
public AudioRolloffMode RolloffMode;
public bool OverrideSpatialSettings;
public float FadeOutSeconds;
public AudioPlayRequest()
{
Reset();
}
public void Set2D(AudioType type, string address, bool loop, float volume, bool async, bool cacheClip)
{
Reset();
Type = type;
Address = address;
Loop = loop;
Volume = volume;
Async = async;
CacheClip = cacheClip;
Spatial = false;
SpatialBlend = 0f;
}
public void Set2D(AudioType type, AudioClip clip, bool loop, float volume)
{
Reset();
Type = type;
Clip = clip;
Loop = loop;
Volume = volume;
Spatial = false;
SpatialBlend = 0f;
}
public void Set3D(AudioType type, AudioClip clip, in Vector3 position, bool loop, float volume)
{
Reset();
Type = type;
Clip = clip;
Position = position;
UseWorldPosition = true;
Loop = loop;
Volume = volume;
Spatial = true;
SpatialBlend = 1f;
}
public void Set3D(AudioType type, string address, in Vector3 position, bool loop, float volume, bool async, bool cacheClip)
{
Reset();
Type = type;
Address = address;
Position = position;
UseWorldPosition = true;
Loop = loop;
Volume = volume;
Async = async;
CacheClip = cacheClip;
Spatial = true;
SpatialBlend = 1f;
}
public void Set3D(AudioType type, string address, in Vector3 position, float minDistance, float maxDistance, AudioRolloffMode rolloffMode, float spatialBlend, bool loop, float volume, bool async, bool cacheClip)
{
Set3D(type, address, position, loop, volume, async, cacheClip);
SetSpatialSettings(minDistance, maxDistance, rolloffMode, spatialBlend);
}
public void Set3D(AudioType type, AudioClip clip, in Vector3 position, float minDistance, float maxDistance, AudioRolloffMode rolloffMode, float spatialBlend, bool loop, float volume)
{
Set3D(type, clip, position, loop, volume);
SetSpatialSettings(minDistance, maxDistance, rolloffMode, spatialBlend);
}
public void SetFollow(AudioType type, string address, Transform target, in Vector3 localOffset, bool loop, float volume, bool async, bool cacheClip)
{
Reset();
Type = type;
Address = address;
FollowTarget = target;
Position = localOffset;
Loop = loop;
Volume = volume;
Async = async;
CacheClip = cacheClip;
Spatial = true;
SpatialBlend = 1f;
}
public void SetFollow(AudioType type, AudioClip clip, Transform target, in Vector3 localOffset, bool loop, float volume)
{
Reset();
Type = type;
Clip = clip;
FollowTarget = target;
Position = localOffset;
Loop = loop;
Volume = volume;
Spatial = true;
SpatialBlend = 1f;
}
public void SetFollow(AudioType type, string address, Transform target, in Vector3 localOffset, float minDistance, float maxDistance, AudioRolloffMode rolloffMode, float spatialBlend, bool loop, float volume, bool async, bool cacheClip)
{
SetFollow(type, address, target, localOffset, loop, volume, async, cacheClip);
SetSpatialSettings(minDistance, maxDistance, rolloffMode, spatialBlend);
}
public void SetFollow(AudioType type, AudioClip clip, Transform target, in Vector3 localOffset, float minDistance, float maxDistance, AudioRolloffMode rolloffMode, float spatialBlend, bool loop, float volume)
{
SetFollow(type, clip, target, localOffset, loop, volume);
SetSpatialSettings(minDistance, maxDistance, rolloffMode, spatialBlend);
}
public void Clear()
{
Reset();
}
private void Reset()
{
Type = AudioType.Sound;
Address = null;
Clip = null;
FollowTarget = null;
Position = Vector3.zero;
UseWorldPosition = false;
Loop = false;
Async = false;
CacheClip = true;
Spatial = false;
Volume = 1f;
Pitch = 1f;
SpatialBlend = -1f;
MinDistance = 1f;
MaxDistance = 500f;
RolloffMode = AudioRolloffMode.Logarithmic;
OverrideSpatialSettings = false;
FadeOutSeconds = 0.15f;
}
private void SetSpatialSettings(float minDistance, float maxDistance, AudioRolloffMode rolloffMode, float spatialBlend)
{
MinDistance = Mathf.Max(0f, minDistance);
MaxDistance = Mathf.Max(MinDistance, maxDistance);
RolloffMode = rolloffMode;
SpatialBlend = Mathf.Clamp01(spatialBlend);
OverrideSpatialSettings = true;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 708b6c6f23eadc7428400b296aae7bb6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,82 @@
using AlicizaX;
using AlicizaX.ObjectPool;
using UnityEngine;
namespace AlicizaX.Audio.Runtime
{
internal sealed class AudioSourceObject : ObjectBase
{
private AudioSource _source;
private AudioLowPassFilter _lowPassFilter;
public AudioSource Source => _source;
public AudioLowPassFilter LowPassFilter => _lowPassFilter;
public static AudioSourceObject Create(string name, AudioSource source, AudioLowPassFilter lowPassFilter)
{
if (source == null)
{
throw new GameFrameworkException("Audio source is invalid.");
}
AudioSourceObject audioSourceObject = MemoryPool.Acquire<AudioSourceObject>();
audioSourceObject.Initialize(name, source);
audioSourceObject._source = source;
audioSourceObject._lowPassFilter = lowPassFilter;
return audioSourceObject;
}
protected internal override void OnSpawn()
{
if (_source != null)
{
_source.gameObject.SetActive(true);
}
}
protected internal override void OnUnspawn()
{
ResetSource();
if (_source != null)
{
_source.gameObject.SetActive(false);
}
}
protected internal override void Release(bool isShutdown)
{
if (_source != null)
{
Object.Destroy(_source.gameObject);
}
}
public override void Clear()
{
base.Clear();
_source = null;
_lowPassFilter = null;
}
private void ResetSource()
{
if (_source == null)
{
return;
}
_source.Stop();
_source.clip = null;
_source.loop = false;
_source.volume = 1f;
_source.pitch = 1f;
_source.spatialBlend = 0f;
if (_lowPassFilter != null)
{
_lowPassFilter.enabled = false;
_lowPassFilter.cutoffFrequency = 22000f;
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c79fdc0db53879a4691d4045ecb1e9d4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,34 +1,12 @@
namespace AlicizaX.Audio.Runtime
namespace AlicizaX.Audio.Runtime
{
/// <summary>
/// 音效分类,可分别关闭/开启对应分类音效。
/// </summary>
/// <remarks>命名与AudioMixer中分类名保持一致。</remarks>
public enum AudioType
{
/// <summary>
/// 声音音效。
/// </summary>
Sound,
/// <summary>
/// UI声效。
/// </summary>
UISound,
/// <summary>
/// 背景音乐音效。
/// </summary>
Music,
/// <summary>
/// 人声音效。
/// </summary>
Voice,
/// <summary>
/// 最大。
/// </summary>
Max
Sound = 0,
UISound = 1,
Music = 2,
Voice = 3,
Ambient = 4,
Max = 5
}
}

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b4583bd26706a0f43a91b4e406ff1a7d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,286 @@
using AlicizaX;
using UnityEngine;
namespace AlicizaX.Audio.Runtime
{
[DisallowMultipleComponent]
[AddComponentMenu("Game Framework/Audio/Audio Emitter")]
public sealed class AudioEmitter : MonoBehaviour
{
private enum AudioEmitterClipMode
{
Address = 0,
Clip = 1
}
[Header("Playback")]
[SerializeField] private AudioType m_AudioType = AudioType.Ambient;
[SerializeField] private AudioEmitterClipMode m_ClipMode = AudioEmitterClipMode.Address;
[SerializeField] private string m_Address = string.Empty;
[SerializeField] private AudioClip m_Clip;
[SerializeField] private bool m_PlayOnEnable = true;
[SerializeField] private bool m_Loop = true;
[SerializeField, Range(0f, 1f)] private float m_Volume = 1f;
[SerializeField] private bool m_Async = true;
[SerializeField] private bool m_CacheClip = true;
[SerializeField] private bool m_StopWithFadeout = true;
[Header("Spatial")]
[SerializeField] private bool m_FollowSelf = true;
[SerializeField] private Vector3 m_FollowOffset = Vector3.zero;
[SerializeField, Range(0f, 1f)] private float m_SpatialBlend = 1f;
[SerializeField] private AudioRolloffMode m_RolloffMode = AudioRolloffMode.Logarithmic;
[SerializeField, Min(0f)] private float m_MinDistance = 2f;
[SerializeField, Min(0f)] private float m_MaxDistance = 30f;
[Header("Trigger")]
[SerializeField] private bool m_UseTriggerRange = false;
[SerializeField, Min(0f)] private float m_TriggerRange = 10f;
[SerializeField, Min(0f)] private float m_TriggerHysteresis = 0.5f;
[Header("Gizmos")]
[SerializeField] private bool m_DrawGizmos = true;
[SerializeField] private bool m_DrawOnlyWhenSelected = true;
[SerializeField] private Color m_TriggerColor = new Color(0.2f, 0.9f, 1f, 0.9f);
[SerializeField] private Color m_MinDistanceColor = new Color(1f, 0.9f, 0.2f, 0.9f);
[SerializeField] private Color m_MaxDistanceColor = new Color(1f, 0.45f, 0.05f, 0.9f);
private IAudioService _audioService;
private ulong _handle;
private bool _isPlaying;
private bool _insideTriggerRange;
public ulong Handle => _handle;
public bool IsPlaying => _isPlaying;
private void OnEnable()
{
TryBindService();
if (m_PlayOnEnable && !m_UseTriggerRange)
{
StartPlayback();
}
}
private void Update()
{
if (!m_UseTriggerRange)
{
RefreshPlaybackState();
return;
}
if (_audioService == null && !TryBindService())
{
return;
}
Transform listener = _audioService.ListenerTransform;
if (listener == null)
{
_insideTriggerRange = false;
StopPlayback();
return;
}
Vector3 offset = listener.position - transform.position;
float range = _isPlaying ? m_TriggerRange + m_TriggerHysteresis : m_TriggerRange;
float sqrRange = range * range;
if (offset.sqrMagnitude <= sqrRange)
{
RefreshPlaybackState();
if (!_insideTriggerRange || (m_Loop && !_isPlaying))
{
_insideTriggerRange = true;
StartPlayback();
}
}
else
{
_insideTriggerRange = false;
StopPlayback();
}
}
private void OnDisable()
{
StopPlayback();
_audioService = null;
_insideTriggerRange = false;
}
public void Play()
{
if (_audioService == null)
{
TryBindService();
}
StartPlayback();
}
public void Stop()
{
StopPlayback();
}
private bool TryBindService()
{
return AppServices.TryGet(out _audioService);
}
private void StartPlayback()
{
if (_audioService == null || _isPlaying || !HasPlayableAsset())
{
return;
}
float maxDistance = m_MaxDistance >= m_MinDistance ? m_MaxDistance : m_MinDistance;
if (m_FollowSelf)
{
_handle = m_ClipMode == AudioEmitterClipMode.Clip
? _audioService.PlayFollow(
m_AudioType,
m_Clip,
transform,
m_FollowOffset,
m_MinDistance,
maxDistance,
m_RolloffMode,
m_SpatialBlend,
m_Loop,
m_Volume)
: _audioService.PlayFollow(
m_AudioType,
m_Address,
transform,
m_FollowOffset,
m_MinDistance,
maxDistance,
m_RolloffMode,
m_SpatialBlend,
m_Loop,
m_Volume,
m_Async,
m_CacheClip);
}
else
{
Vector3 position = transform.position;
_handle = m_ClipMode == AudioEmitterClipMode.Clip
? _audioService.Play3D(
m_AudioType,
m_Clip,
position,
m_MinDistance,
maxDistance,
m_RolloffMode,
m_SpatialBlend,
m_Loop,
m_Volume)
: _audioService.Play3D(
m_AudioType,
m_Address,
position,
m_MinDistance,
maxDistance,
m_RolloffMode,
m_SpatialBlend,
m_Loop,
m_Volume,
m_Async,
m_CacheClip);
}
_isPlaying = _handle != 0UL;
}
private void StopPlayback()
{
if (!_isPlaying)
{
_handle = 0UL;
return;
}
if (_audioService != null && _handle != 0UL)
{
_audioService.Stop(_handle, m_StopWithFadeout);
}
_handle = 0UL;
_isPlaying = false;
}
private bool HasPlayableAsset()
{
return m_ClipMode == AudioEmitterClipMode.Clip
? m_Clip != null
: !string.IsNullOrEmpty(m_Address);
}
private void RefreshPlaybackState()
{
if (!_isPlaying || _audioService == null || _handle == 0UL)
{
return;
}
if (_audioService.IsPlaying(_handle))
{
return;
}
_handle = 0UL;
_isPlaying = false;
}
private void OnValidate()
{
if (m_MaxDistance < m_MinDistance)
{
m_MaxDistance = m_MinDistance;
}
}
private void OnDrawGizmos()
{
if (!m_DrawOnlyWhenSelected)
{
DrawEmitterGizmos();
}
}
private void OnDrawGizmosSelected()
{
if (m_DrawOnlyWhenSelected)
{
DrawEmitterGizmos();
}
}
private void DrawEmitterGizmos()
{
if (!m_DrawGizmos)
{
return;
}
Vector3 position = transform.position;
if (m_UseTriggerRange)
{
Gizmos.color = m_TriggerColor;
Gizmos.DrawWireSphere(position, m_TriggerRange);
}
Gizmos.color = m_MinDistanceColor;
Gizmos.DrawWireSphere(position, m_MinDistance);
Gizmos.color = m_MaxDistanceColor;
Gizmos.DrawWireSphere(position, m_MaxDistance);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f850e2c8d01392a41912893526cebe9e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,41 @@
using AlicizaX;
using UnityEngine;
namespace AlicizaX.Audio.Runtime
{
[DisallowMultipleComponent]
[RequireComponent(typeof(AudioListener))]
[AddComponentMenu("Game Framework/Audio/Audio Listener Binder")]
public sealed class AudioListenerBinder : MonoBehaviour
{
[SerializeField] private AudioListener m_Listener;
private IAudioService _audioService;
private void Awake()
{
if (m_Listener == null)
{
m_Listener = GetComponent<AudioListener>();
}
}
private void OnEnable()
{
if (m_Listener == null || !AppServices.TryGet(out _audioService))
{
return;
}
_audioService.RegisterListener(m_Listener);
}
private void OnDisable()
{
if (_audioService != null && m_Listener != null)
{
_audioService.UnregisterListener(m_Listener);
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e294cc163e4d4564187a681ba9d76597
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,130 +1,43 @@
using System.Collections.Generic;
using AlicizaX;
using UnityEngine;
using UnityEngine.Audio;
using YooAsset;
namespace AlicizaX.Audio.Runtime
{
public interface IAudioService : IService
{
/// <summary>
/// 总音量控制。
/// </summary>
public float Volume { get; set; }
float Volume { get; set; }
bool Enable { get; set; }
AudioMixer AudioMixer { get; }
Transform InstanceRoot { get; }
Transform ListenerTransform { get; }
/// <summary>
/// 总开关。
/// </summary>
public bool Enable { get; set; }
/// <summary>
/// 音乐音量
/// </summary>
public float MusicVolume { get; set; }
/// <summary>
/// 音效音量。
/// </summary>
public float SoundVolume { get; set; }
/// <summary>
/// UI音效音量。
/// </summary>
public float UISoundVolume { get; set; }
/// <summary>
/// 语音音量。
/// </summary>
public float VoiceVolume { get; set; }
/// <summary>
/// 音乐开关。
/// </summary>
public bool MusicEnable { get; set; }
/// <summary>
/// 音效开关。
/// </summary>
public bool SoundEnable { get; set; }
/// <summary>
/// UI音效开关。
/// </summary>
public bool UISoundEnable { get; set; }
/// <summary>
/// 语音开关。
/// </summary>
public bool VoiceEnable { get; set; }
/// <summary>
/// 音频混响器。
/// </summary>
public AudioMixer AudioMixer { get;}
/// <summary>
/// 实例化根节点。
/// </summary>
public Transform InstanceRoot { get;}
public Dictionary<string, AssetHandle> AudioClipPool { get; set; }
/// <summary>
/// 初始化音频模块。
/// </summary>
/// <param name="audioGroupConfigs">音频轨道组配置。</param>
/// <param name="instanceRoot">实例化根节点。</param>
/// <param name="audioMixer">音频混响器。</param>
/// <exception cref="GameFrameworkException"></exception>
public void Initialize(AudioGroupConfig[] audioGroupConfigs, Transform instanceRoot = null, AudioMixer audioMixer = null);
/// <summary>
/// 重启音频模块。
/// </summary>
public void Restart();
/// <summary>
/// 播放音频接口。
/// </summary>
/// <remarks>如果超过最大发声数采用fadeout的方式复用最久播放的AudioSource。</remarks>
/// <param name="type">声音类型。</param>
/// <param name="path">声音文件路径。</param>
/// <param name="bLoop">是否循环播放。</param>>
/// <param name="volume">音量0-1.0)。</param>
/// <param name="bAsync">是否异步加载。</param>
/// <param name="bInPool">是否支持资源池。</param>
public AudioAgent Play(AudioType type, string path, bool bLoop = false, float volume = 1.0f, bool bAsync = false, bool bInPool = false);
public AudioAgent Play(AudioType type,AudioClip clip,bool loop=false,float volume = 1.0f);
/// <summary>
/// 停止某类声音播放。
/// </summary>
/// <param name="type">声音类型。</param>
/// <param name="fadeout">是否渐消。</param>
public void Stop(AudioType type, bool fadeout);
/// <summary>
/// 停止所有声音。
/// </summary>
/// <param name="fadeout">是否渐消。</param>
public void StopAll(bool fadeout);
/// <summary>
/// 预先加载AudioClip并放入对象池。
/// </summary>
/// <param name="list">AudioClip的AssetPath集合。</param>
public void PutInAudioPool(List<string> list);
/// <summary>
/// 将部分AudioClip从对象池移出。
/// </summary>
/// <param name="list">AudioClip的AssetPath集合。</param>
public void RemoveClipFromPool(List<string> list);
/// <summary>
/// 清空AudioClip的对象池。
/// </summary>
public void CleanSoundPool();
void Initialize(AudioGroupConfig[] audioGroupConfigs, Transform instanceRoot = null, AudioMixer audioMixer = null);
void Restart();
float GetCategoryVolume(AudioType type);
void SetCategoryVolume(AudioType type, float value);
bool GetCategoryEnable(AudioType type);
void SetCategoryEnable(AudioType type, bool value);
void RegisterListener(AudioListener listener);
void UnregisterListener(AudioListener listener);
ulong Play(AudioType type, string path, bool loop = false, float volume = 1f, bool async = false, bool cacheClip = true);
ulong Play(AudioType type, AudioClip clip, bool loop = false, float volume = 1f);
ulong Play3D(AudioType type, string path, in Vector3 position, bool loop = false, float volume = 1f, bool async = false, bool cacheClip = true);
ulong Play3D(AudioType type, string path, in Vector3 position, float minDistance, float maxDistance, AudioRolloffMode rolloffMode, float spatialBlend = 1f, bool loop = false, float volume = 1f, bool async = false, bool cacheClip = true);
ulong Play3D(AudioType type, AudioClip clip, in Vector3 position, bool loop = false, float volume = 1f);
ulong Play3D(AudioType type, AudioClip clip, in Vector3 position, float minDistance, float maxDistance, AudioRolloffMode rolloffMode, float spatialBlend = 1f, bool loop = false, float volume = 1f);
ulong PlayFollow(AudioType type, string path, Transform target, in Vector3 localOffset, bool loop = false, float volume = 1f, bool async = false, bool cacheClip = true);
ulong PlayFollow(AudioType type, string path, Transform target, in Vector3 localOffset, float minDistance, float maxDistance, AudioRolloffMode rolloffMode, float spatialBlend = 1f, bool loop = false, float volume = 1f, bool async = false, bool cacheClip = true);
ulong PlayFollow(AudioType type, AudioClip clip, Transform target, in Vector3 localOffset, bool loop = false, float volume = 1f);
ulong PlayFollow(AudioType type, AudioClip clip, Transform target, in Vector3 localOffset, float minDistance, float maxDistance, AudioRolloffMode rolloffMode, float spatialBlend = 1f, bool loop = false, float volume = 1f);
bool Stop(ulong handle, bool fadeout = false);
bool IsPlaying(ulong handle);
void Stop(AudioType type, bool fadeout);
void StopAll(bool fadeout);
void Pause(ulong handle);
void Resume(ulong handle);
void Preload(IList<string> paths, bool pin = true);
void Unload(IList<string> paths);
void ClearCache();
}
}

View File

@ -0,0 +1,173 @@
using AlicizaX.Audio.Runtime;
using UnityEngine;
using UnityEngine.UIElements;
namespace AlicizaX.Debugger.Runtime
{
public sealed partial class DebuggerComponent
{
private sealed class AudioInformationWindow : PollingDebuggerWindowBase
{
private readonly AudioServiceDebugInfo _serviceInfo = new AudioServiceDebugInfo();
private readonly AudioCategoryDebugInfo _categoryInfo = new AudioCategoryDebugInfo();
private readonly AudioAgentDebugInfo _agentInfo = new AudioAgentDebugInfo();
private readonly AudioClipCacheDebugInfo _clipCacheInfo = new AudioClipCacheDebugInfo();
private IAudioDebugService _audioDebugService;
public override void Initialize(params object[] args)
{
TryBindAudioService();
}
protected override void BuildWindow(VisualElement root)
{
if (_audioDebugService == null && !TryBindAudioService())
{
VisualElement unavailable = CreateSection("Audio", out VisualElement unavailableCard);
unavailableCard.Add(CreateRow("State", "Audio service is not initialized."));
root.Add(unavailable);
return;
}
_audioDebugService.FillServiceDebugInfo(_serviceInfo);
DrawOverview(root);
DrawCategories(root);
DrawAgents(root);
DrawClipCache(root);
}
private bool TryBindAudioService()
{
if (AppServices.TryGet<IAudioService>(out IAudioService audioService) && audioService is IAudioDebugService debugService)
{
_audioDebugService = debugService;
return true;
}
_audioDebugService = null;
return false;
}
private void DrawOverview(VisualElement root)
{
VisualElement section = CreateSection("Audio Overview", out VisualElement card);
card.Add(CreateRow("Initialized", _serviceInfo.Initialized.ToString()));
card.Add(CreateRow("Unity Audio Disabled", _serviceInfo.UnityAudioDisabled.ToString()));
card.Add(CreateRow("Enable", _serviceInfo.Enable.ToString()));
card.Add(CreateRow("Volume", _serviceInfo.Volume.ToString("F3")));
card.Add(CreateRow("Listener", _serviceInfo.Listener != null ? _serviceInfo.Listener.name : "<None>"));
card.Add(CreateRow("Instance Root", _serviceInfo.InstanceRoot != null ? _serviceInfo.InstanceRoot.name : "<None>"));
card.Add(CreateRow("Active Agents", _serviceInfo.ActiveAgentCount.ToString()));
card.Add(CreateRow("Handle Capacity", _serviceInfo.HandleCapacity.ToString()));
card.Add(CreateRow("Clip Cache", _serviceInfo.ClipCacheCount + " / " + _serviceInfo.ClipCacheCapacity));
root.Add(section);
}
private void DrawCategories(VisualElement root)
{
VisualElement section = CreateSection("Audio Categories", out VisualElement card);
for (int i = 0; i < _audioDebugService.CategoryCount; i++)
{
if (!_audioDebugService.FillCategoryDebugInfo(i, _categoryInfo))
{
continue;
}
card.Add(CreateRow(
_categoryInfo.Type.ToString(),
"Enabled " + _categoryInfo.Enabled
+ " | Volume " + _categoryInfo.Volume.ToString("F2")
+ " | Active " + _categoryInfo.ActiveCount
+ " | Free " + _categoryInfo.FreeCount
+ " | Heap " + _categoryInfo.HeapCount
+ " | Capacity " + _categoryInfo.Capacity));
}
root.Add(section);
}
private void DrawAgents(VisualElement root)
{
VisualElement section = CreateSection("Active Audio Agents", out VisualElement card);
bool hasActive = false;
for (int typeIndex = 0; typeIndex < _audioDebugService.CategoryCount; typeIndex++)
{
if (!_audioDebugService.FillCategoryDebugInfo(typeIndex, _categoryInfo))
{
continue;
}
for (int agentIndex = 0; agentIndex < _categoryInfo.Capacity; agentIndex++)
{
if (!_audioDebugService.FillAgentDebugInfo(typeIndex, agentIndex, _agentInfo) || _agentInfo.State == AudioAgentRuntimeState.Free)
{
continue;
}
hasActive = true;
card.Add(CreateRow(
_agentInfo.Type + "[" + _agentInfo.Index + "]",
_agentInfo.State
+ " | Handle " + _agentInfo.Handle
+ " | Clip " + GetClipName(_agentInfo.Clip)
+ " | Source " + GetSourceName()
+ " | Volume " + _agentInfo.Volume.ToString("F2")
+ " | Spatial " + _agentInfo.Spatial
+ " | Occluded " + _agentInfo.Occluded
+ " | Range " + _agentInfo.MinDistance.ToString("F1") + "-" + _agentInfo.MaxDistance.ToString("F1")));
}
}
if (!hasActive)
{
card.Add(CreateRow("Agents", "No active audio agents."));
}
root.Add(section);
}
private void DrawClipCache(VisualElement root)
{
VisualElement section = CreateSection("Audio Clip Cache", out VisualElement card);
AudioClipCacheEntry entry = _audioDebugService.FirstClipCacheEntry;
if (entry == null)
{
card.Add(CreateRow("Cache", "Empty"));
root.Add(section);
return;
}
while (entry != null)
{
AudioClipCacheEntry next = entry.AllNext;
if (_audioDebugService.FillClipCacheDebugInfo(entry, _clipCacheInfo))
{
card.Add(CreateRow(
_clipCacheInfo.Address,
"Ref " + _clipCacheInfo.RefCount
+ " | Pending " + _clipCacheInfo.PendingCount
+ " | Loaded " + _clipCacheInfo.IsLoaded
+ " | Loading " + _clipCacheInfo.Loading
+ " | Pinned " + _clipCacheInfo.Pinned
+ " | LRU " + _clipCacheInfo.InLru
+ " | Last " + (Time.realtimeSinceStartup - _clipCacheInfo.LastUseTime).ToString("F1") + "s"));
}
entry = next;
}
root.Add(section);
}
private string GetSourceName()
{
return string.IsNullOrEmpty(_agentInfo.Address) ? "<Direct Clip>" : _agentInfo.Address;
}
private static string GetClipName(AudioClip clip)
{
return clip != null ? clip.name : "<None>";
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 611fc331e56d457abe1517f4312aaa44
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -92,6 +92,7 @@ namespace AlicizaX.Debugger.Runtime
private RuntimeMemoryInformationWindow<ScriptableObject> m_RuntimeMemoryScriptableObjectInformationWindow = new RuntimeMemoryInformationWindow<ScriptableObject>();
private ObjectPoolInformationWindow m_ObjectPoolInformationWindow = new ObjectPoolInformationWindow();
private ReferencePoolInformationWindow m_ReferencePoolInformationWindow = new ReferencePoolInformationWindow();
private AudioInformationWindow m_AudioInformationWindow = new AudioInformationWindow();
private SettingsWindow m_SettingsWindow = new SettingsWindow();
private FpsCounter m_FpsCounter;
@ -500,6 +501,7 @@ namespace AlicizaX.Debugger.Runtime
RegisterDebuggerWindow("Profiler/Memory/ScriptableObject", m_RuntimeMemoryScriptableObjectInformationWindow);
RegisterDebuggerWindow("Profiler/Object Pool", m_ObjectPoolInformationWindow);
RegisterDebuggerWindow("Profiler/Reference Pool", m_ReferencePoolInformationWindow);
RegisterDebuggerWindow("Profiler/Audio", m_AudioInformationWindow);
RegisterDebuggerWindow("Other/Settings", m_SettingsWindow);
}