重构音频模块 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:
parent
8849ccf5ce
commit
46194ddee8
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
121
Editor/Audio/AudioEmitterInspector.cs
Normal file
121
Editor/Audio/AudioEmitterInspector.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Editor/Audio/AudioEmitterInspector.cs.meta
Normal file
11
Editor/Audio/AudioEmitterInspector.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 45735882efba4a4c9e8798553bde3a38
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
97
Editor/Audio/AudioGroupConfigCollectionInspector.cs
Normal file
97
Editor/Audio/AudioGroupConfigCollectionInspector.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Editor/Audio/AudioGroupConfigCollectionInspector.cs.meta
Normal file
11
Editor/Audio/AudioGroupConfigCollectionInspector.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fe9a4f6605709b846b72152265355a75
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
53
Entry.prefab
53
Entry.prefab
@ -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
749
Runtime/Audio/Audio.md
Normal 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 五类音频需求。
|
||||
7
Runtime/Audio/Audio.md.meta
Normal file
7
Runtime/Audio/Audio.md.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6a19d8c0bd042124c9adb78b14cbb9c0
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
145
Runtime/Audio/AudioClipCacheEntry.cs
Normal file
145
Runtime/Audio/AudioClipCacheEntry.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Runtime/Audio/AudioClipCacheEntry.cs.meta
Normal file
11
Runtime/Audio/AudioClipCacheEntry.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7e438d922e4ac1e439e5a30a0b9d9ac7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 036a1af4acb84666b73909ba28455cfa
|
||||
timeCreated: 1742472335
|
||||
148
Runtime/Audio/AudioDebugInfo.cs
Normal file
148
Runtime/Audio/AudioDebugInfo.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Runtime/Audio/AudioDebugInfo.cs.meta
Normal file
11
Runtime/Audio/AudioDebugInfo.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 81dfdc57e55b43799a3451eeca4e40fa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
99
Runtime/Audio/AudioGroupConfigCollection.cs
Normal file
99
Runtime/Audio/AudioGroupConfigCollection.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Runtime/Audio/AudioGroupConfigCollection.cs.meta
Normal file
11
Runtime/Audio/AudioGroupConfigCollection.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 35a9ab9e2f42f744ca3801948d9b8c2e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
18
Runtime/Audio/AudioLoadRequest.cs
Normal file
18
Runtime/Audio/AudioLoadRequest.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Runtime/Audio/AudioLoadRequest.cs.meta
Normal file
11
Runtime/Audio/AudioLoadRequest.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b1b1b306b2b64b24aba7fc6740d9df35
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
172
Runtime/Audio/AudioPlayRequest.cs
Normal file
172
Runtime/Audio/AudioPlayRequest.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Runtime/Audio/AudioPlayRequest.cs.meta
Normal file
11
Runtime/Audio/AudioPlayRequest.cs.meta
Normal 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
82
Runtime/Audio/AudioSourceObject.cs
Normal file
82
Runtime/Audio/AudioSourceObject.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Runtime/Audio/AudioSourceObject.cs.meta
Normal file
11
Runtime/Audio/AudioSourceObject.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c79fdc0db53879a4691d4045ecb1e9d4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
8
Runtime/Audio/Components.meta
Normal file
8
Runtime/Audio/Components.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b4583bd26706a0f43a91b4e406ff1a7d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
286
Runtime/Audio/Components/AudioEmitter.cs
Normal file
286
Runtime/Audio/Components/AudioEmitter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Runtime/Audio/Components/AudioEmitter.cs.meta
Normal file
11
Runtime/Audio/Components/AudioEmitter.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f850e2c8d01392a41912893526cebe9e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
41
Runtime/Audio/Components/AudioListenerBinder.cs
Normal file
41
Runtime/Audio/Components/AudioListenerBinder.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Runtime/Audio/Components/AudioListenerBinder.cs.meta
Normal file
11
Runtime/Audio/Components/AudioListenerBinder.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e294cc163e4d4564187a681ba9d76597
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
173
Runtime/Debugger/DebuggerComponent.AudioInformationWindow.cs
Normal file
173
Runtime/Debugger/DebuggerComponent.AudioInformationWindow.cs
Normal 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>";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 611fc331e56d457abe1517f4312aaa44
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user