From 46194ddee85a7152df7712d5e951a07eb19398f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=80=9D=E6=B5=B7?= <1464576565@qq.com> Date: Thu, 23 Apr 2026 17:21:36 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E9=9F=B3=E9=A2=91=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=201.=20=E9=AB=98=E9=A2=91=E3=80=81=E5=A4=A7=E9=87=8F?= =?UTF-8?q?=E9=9F=B3=E9=A2=91=E5=8F=8D=E5=A4=8D=E8=B0=83=E7=94=A8=E6=97=B6?= =?UTF-8?q?=EF=BC=8C=E5=8D=95=E5=B8=A7=20CPU=20=E5=BC=80=E9=94=80=E4=B8=8E?= =?UTF-8?q?=20GC=20=E6=9C=80=E4=BC=98=202.=20AudioClip=20/=20AudioSource?= =?UTF-8?q?=20=E7=9A=84=E5=8A=A0=E8=BD=BD=E3=80=81=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E6=B7=98=E6=B1=B0=E3=80=81=E5=8D=B8=E8=BD=BD=E5=BD=A2=E6=88=90?= =?UTF-8?q?=E5=AE=8C=E6=95=B4=E9=97=AD=E7=8E=AF=EF=BC=8C=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E7=BA=BF=E6=80=A7=E9=81=8D=E5=8E=86=203.=20AudioSource=20?= =?UTF-8?q?=E5=AF=B9=E8=B1=A1=E6=B1=A0=20+=20=E6=92=AD=E6=94=BE=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=20struct=20=E5=85=A8=E9=83=A8=E6=B1=A0=E5=8C=96?= =?UTF-8?q?=E8=A6=86=E7=9B=96=E6=89=80=E6=9C=89=E5=88=86=E9=85=8D=E7=82=B9?= =?UTF-8?q?=204.=20=E6=94=AF=E6=8C=813D=E7=8E=AF=E5=A2=83=E9=9F=B3?= =?UTF-8?q?=E5=B9=B6=E5=85=B7=E5=A4=87=E8=B7=9D=E7=A6=BB=E8=A1=B0=E5=87=8F?= =?UTF-8?q?=E3=80=81=E9=81=AE=E6=8C=A1=E7=AD=89=E7=A9=BA=E9=97=B4=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=205.=20=E6=96=B0=E5=A2=9E=E9=9F=B3=E9=A2=91=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=EF=BC=88BGM/SFX/Voice/Ambient=EF=BC=89=206.=20?= =?UTF-8?q?=E5=8F=AF=E8=B0=83=E5=BC=8F=E7=9B=91=E6=8E=A7Debug=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=20=E5=8F=8A=E6=97=B6=E8=B7=9F=E8=B8=AA=E9=9F=B3?= =?UTF-8?q?=E9=A2=91=E7=BC=93=E5=AD=98=20=E5=A4=84=E7=90=86=20=E5=8F=A5?= =?UTF-8?q?=E6=9F=84=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构音频模块 1. 高频、大量音频反复调用时,单帧 CPU 开销与 GC 最优 2. AudioClip / AudioSource 的加载、缓存淘汰、卸载形成完整闭环,避免线性遍历 3. AudioSource 对象池 + 播放请求 struct 全部池化覆盖所有分配点 4. 支持3D环境音并具备距离衰减、遮挡等空间属性 5. 新增音频类型(BGM/SFX/Voice/Ambient) 6. 可调式监控Debug信息 及时跟踪音频缓存 处理 句柄状态 --- Editor/Audio/AudioComponentInspector.cs | 161 +- Editor/Audio/AudioEmitterInspector.cs | 121 ++ Editor/Audio/AudioEmitterInspector.cs.meta | 11 + .../AudioGroupConfigCollectionInspector.cs | 97 + ...udioGroupConfigCollectionInspector.cs.meta | 11 + Entry.prefab | 53 +- Runtime/Audio/Audio.md | 749 ++++++++ Runtime/Audio/Audio.md.meta | 7 + Runtime/Audio/AudioAgent.cs | 753 ++++---- Runtime/Audio/AudioAgentRuntimeState.cs | 40 +- Runtime/Audio/AudioCategory.cs | 455 +++-- Runtime/Audio/AudioClipCacheEntry.cs | 145 ++ Runtime/Audio/AudioClipCacheEntry.cs.meta | 11 + Runtime/Audio/AudioComponent.cs | 61 +- Runtime/Audio/AudioData.cs | 61 - Runtime/Audio/AudioData.cs.meta | 3 - Runtime/Audio/AudioDebugInfo.cs | 148 ++ Runtime/Audio/AudioDebugInfo.cs.meta | 11 + Runtime/Audio/AudioGroupConfig.cs | 80 +- Runtime/Audio/AudioGroupConfigCollection.cs | 99 + .../Audio/AudioGroupConfigCollection.cs.meta | 11 + Runtime/Audio/AudioLoadRequest.cs | 18 + Runtime/Audio/AudioLoadRequest.cs.meta | 11 + Runtime/Audio/AudioPlayRequest.cs | 172 ++ Runtime/Audio/AudioPlayRequest.cs.meta | 11 + Runtime/Audio/AudioService.cs | 1647 ++++++++++++----- Runtime/Audio/AudioSourceObject.cs | 82 + Runtime/Audio/AudioSourceObject.cs.meta | 11 + Runtime/Audio/AudioType.cs | 38 +- Runtime/Audio/Components.meta | 8 + Runtime/Audio/Components/AudioEmitter.cs | 286 +++ Runtime/Audio/Components/AudioEmitter.cs.meta | 11 + .../Audio/Components/AudioListenerBinder.cs | 41 + .../Components/AudioListenerBinder.cs.meta | 11 + Runtime/Audio/IAudioService.cs | 151 +- ...ebuggerComponent.AudioInformationWindow.cs | 173 ++ ...erComponent.AudioInformationWindow.cs.meta | 11 + Runtime/Debugger/DebuggerComponent.cs | 2 + 38 files changed, 4397 insertions(+), 1375 deletions(-) create mode 100644 Editor/Audio/AudioEmitterInspector.cs create mode 100644 Editor/Audio/AudioEmitterInspector.cs.meta create mode 100644 Editor/Audio/AudioGroupConfigCollectionInspector.cs create mode 100644 Editor/Audio/AudioGroupConfigCollectionInspector.cs.meta create mode 100644 Runtime/Audio/Audio.md create mode 100644 Runtime/Audio/Audio.md.meta create mode 100644 Runtime/Audio/AudioClipCacheEntry.cs create mode 100644 Runtime/Audio/AudioClipCacheEntry.cs.meta delete mode 100644 Runtime/Audio/AudioData.cs delete mode 100644 Runtime/Audio/AudioData.cs.meta create mode 100644 Runtime/Audio/AudioDebugInfo.cs create mode 100644 Runtime/Audio/AudioDebugInfo.cs.meta create mode 100644 Runtime/Audio/AudioGroupConfigCollection.cs create mode 100644 Runtime/Audio/AudioGroupConfigCollection.cs.meta create mode 100644 Runtime/Audio/AudioLoadRequest.cs create mode 100644 Runtime/Audio/AudioLoadRequest.cs.meta create mode 100644 Runtime/Audio/AudioPlayRequest.cs create mode 100644 Runtime/Audio/AudioPlayRequest.cs.meta create mode 100644 Runtime/Audio/AudioSourceObject.cs create mode 100644 Runtime/Audio/AudioSourceObject.cs.meta create mode 100644 Runtime/Audio/Components.meta create mode 100644 Runtime/Audio/Components/AudioEmitter.cs create mode 100644 Runtime/Audio/Components/AudioEmitter.cs.meta create mode 100644 Runtime/Audio/Components/AudioListenerBinder.cs create mode 100644 Runtime/Audio/Components/AudioListenerBinder.cs.meta create mode 100644 Runtime/Debugger/DebuggerComponent.AudioInformationWindow.cs create mode 100644 Runtime/Debugger/DebuggerComponent.AudioInformationWindow.cs.meta diff --git a/Editor/Audio/AudioComponentInspector.cs b/Editor/Audio/AudioComponentInspector.cs index 89c2952..b0ec020 100644 --- a/Editor/Audio/AudioComponentInspector.cs +++ b/Editor/Audio/AudioComponentInspector.cs @@ -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(); + 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(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 : ""; + string address = string.IsNullOrEmpty(_agentInfo.Address) ? "" : _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; + } + } } } diff --git a/Editor/Audio/AudioEmitterInspector.cs b/Editor/Audio/AudioEmitterInspector.cs new file mode 100644 index 0000000..fff8132 --- /dev/null +++ b/Editor/Audio/AudioEmitterInspector.cs @@ -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"); + } + } +} diff --git a/Editor/Audio/AudioEmitterInspector.cs.meta b/Editor/Audio/AudioEmitterInspector.cs.meta new file mode 100644 index 0000000..788de32 --- /dev/null +++ b/Editor/Audio/AudioEmitterInspector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 45735882efba4a4c9e8798553bde3a38 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Audio/AudioGroupConfigCollectionInspector.cs b/Editor/Audio/AudioGroupConfigCollectionInspector.cs new file mode 100644 index 0000000..6c37b9a --- /dev/null +++ b/Editor/Audio/AudioGroupConfigCollectionInspector.cs @@ -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(); + } + } +} diff --git a/Editor/Audio/AudioGroupConfigCollectionInspector.cs.meta b/Editor/Audio/AudioGroupConfigCollectionInspector.cs.meta new file mode 100644 index 0000000..2ac2832 --- /dev/null +++ b/Editor/Audio/AudioGroupConfigCollectionInspector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fe9a4f6605709b846b72152265355a75 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Entry.prefab b/Entry.prefab index 6e82e7c..af071e5 100644 --- a/Entry.prefab +++ b/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 diff --git a/Runtime/Audio/Audio.md b/Runtime/Audio/Audio.md new file mode 100644 index 0000000..dd09e6e --- /dev/null +++ b/Runtime/Audio/Audio.md @@ -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(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()` +- 使用 `IResourceService.LoadAssetAsyncHandle()` +- 自己持有 `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 五类音频需求。 diff --git a/Runtime/Audio/Audio.md.meta b/Runtime/Audio/Audio.md.meta new file mode 100644 index 0000000..9ac8a9e --- /dev/null +++ b/Runtime/Audio/Audio.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6a19d8c0bd042124c9adb78b14cbb9c0 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Audio/AudioAgent.cs b/Runtime/Audio/AudioAgent.cs index 6170895..6fae4a3 100644 --- a/Runtime/Audio/AudioAgent.cs +++ b/Runtime/Audio/AudioAgent.cs @@ -1,455 +1,470 @@ -using AlicizaX.Resource.Runtime; -using AlicizaX; using UnityEngine; -using UnityEngine.Audio; -using YooAsset; namespace AlicizaX.Audio.Runtime { - /// - /// 音频代理辅助器。 - /// - 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; - /// - /// 音频代理辅助器运行时状态。 - /// - 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; - /// - /// 音频代理加载请求。 - /// - class LoadRequest + internal void Initialize(AudioService service, AudioCategory category, int index, int globalIndex, AudioSourceObject sourceObject) { - /// - /// 音频代理辅助器加载路径。 - /// - public string Path; - - /// - /// 是否异步。 - /// - public bool BAsync; - - /// - /// 是否池化。 - /// - public bool BInPool; + _service = service; + _category = category; + Index = index; + GlobalIndex = globalIndex; + HeapIndex = -1; + ActiveIndex = -1; + BindSource(sourceObject); + ResetState(); } - /// - /// 音频代理加载请求。 - /// - LoadRequest _pendingLoad = null; - - /// - /// AudioSource实例化Id - /// - public int InstanceId => _instanceId; - - /// - /// 资源操作句柄。 - /// - public AudioData AudioData => _audioData; - - /// - /// 音频代理辅助器音频大小。 - /// - 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; } - /// - /// 音频代理辅助器当前是否空闲。 - /// - 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); } } - /// - /// 音频代理辅助器播放秒数。 - /// - public float Duration => _duration; - - /// - /// 音频代理辅助器当前音频长度。 - /// - 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; - } - } - - /// - /// 音频代理辅助器实例位置。 - /// - public Vector3 Position - { - get => _transform.position; - set => _transform.position = value; - } - - /// - /// 音频代理辅助器是否循环。 - /// - public bool IsLoop - { - get - { - if (_source != null) - { - return _source.loop; - } - else - { - return false; - } - } - set - { - if (_source != null) - { - _source.loop = value; - } - } - } - - /// - /// 音频代理辅助器是否正在播放。 - /// - internal bool IsPlaying - { - get - { - if (_source != null && _source.isPlaying) - { - return true; - } - else - { - return false; - } - } - } - - /// - /// 音频代理辅助器获取当前声源。 - /// - /// - public AudioSource AudioResource() - { - return _source; - } - - /// - /// 创建音频代理辅助器。 - /// - /// 生效路径。 - /// 是否异步。 - /// 音频轨道(类别)。 - /// 是否池化。 - /// 音频代理辅助器。 - 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); } } - /// - /// 初始化音频代理辅助器。 - /// - /// 音频轨道(类别)。 - /// 音频代理辅助器编号。 - public void Init(AudioCategory audioCategory, int index = 0) + private void ReleaseClip() { - _audioService = AppServices.Require(); - _resourceService = AppServices.Require(); - 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(); + 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; - /// - /// 加载音频代理辅助器。 - /// - /// 资源路径。 - /// 是否异步。 - /// 是否池化。 - 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(path); - handle.Completed += OnAssetLoadComplete; - } - else - { - AssetHandle handle = _resourceService.LoadAssetSyncHandle(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); - } - } - } - - /// - /// 停止播放音频代理辅助器。 - /// - /// 是否渐出。 - 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; } } } - /// - /// 暂停音频代理辅助器。 - /// - 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; } - /// - /// 取消暂停音频代理辅助器。 - /// - 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; } - /// - /// 资源加载完成。 - /// - /// 资源操作句柄。 - 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); } - /// - /// 轮询音频代理辅助器。 - /// - /// 逻辑流逝时间,以秒为单位。 - 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); } - /// - /// 销毁音频代理辅助器。 - /// - 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; } } } diff --git a/Runtime/Audio/AudioAgentRuntimeState.cs b/Runtime/Audio/AudioAgentRuntimeState.cs index fe9d325..3867817 100644 --- a/Runtime/Audio/AudioAgentRuntimeState.cs +++ b/Runtime/Audio/AudioAgentRuntimeState.cs @@ -1,33 +1,11 @@ -namespace AlicizaX.Audio.Runtime +namespace AlicizaX.Audio.Runtime { - /// - /// 音频代理辅助器运行时状态枚举。 - /// - public enum AudioAgentRuntimeState + internal enum AudioAgentRuntimeState { - /// - /// 无状态。 - /// - None, - - /// - /// 加载中状态。 - /// - Loading, - - /// - /// 播放中状态。 - /// - Playing, - - /// - /// 渐渐消失状态。 - /// - FadingOut, - - /// - /// 结束状态。 - /// - End, - }; -} \ No newline at end of file + Free = 0, + Loading = 1, + Playing = 2, + Paused = 3, + FadingOut = 4 + } +} diff --git a/Runtime/Audio/AudioCategory.cs b/Runtime/Audio/AudioCategory.cs index 0be4b1d..52d4049 100644 --- a/Runtime/Audio/AudioCategory.cs +++ b/Runtime/Audio/AudioCategory.cs @@ -1,239 +1,334 @@ -using System; -using System.Collections.Generic; using UnityEngine; using UnityEngine.Audio; namespace AlicizaX.Audio.Runtime { - /// - /// 音频轨道(类别)。 - /// - [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 AudioAgents; - private readonly AudioMixerGroup _audioMixerGroup; - private AudioGroupConfig _audioGroupConfig; - private int _maxChannel; - private bool _bEnable = true; - - /// - /// 音频混响器。 - /// - public AudioMixer AudioMixer => audioMixer; - - /// - /// 音频混响器组。 - /// - public AudioMixerGroup AudioMixerGroup => _audioMixerGroup; - - /// - /// 音频组配置。 - /// - public AudioGroupConfig AudioGroupConfig => _audioGroupConfig; - - /// - /// 实例化根节点。 - /// - public Transform InstanceRoot { private set; get; } - - /// - /// 音频轨道是否启用。 - /// - 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); } } } - /// - /// 音频轨道构造函数。 - /// - /// 最大Channel。 - /// 音频混响器。 - /// 音频轨道组配置。 - public AudioCategory(int maxChannel, AudioMixer audioMixer, AudioGroupConfig audioGroupConfig) + internal AudioCategory(AudioService service, AudioMixer audioMixer, AudioGroupConfig config, int globalIndexOffset) { - var audioModule = AppServices.Require(); + _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(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; } } - /// - /// 增加音频。 - /// - /// - 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; } - /// - /// 播放音频。 - /// - /// - /// - /// - /// - 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; } - /// - /// 暂停音频。 - /// - /// 是否渐出 - 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; } - /// - /// 音频轨道轮询。 - /// - /// 逻辑流逝时间,以秒为单位。 - 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; } } } diff --git a/Runtime/Audio/AudioClipCacheEntry.cs b/Runtime/Audio/AudioClipCacheEntry.cs new file mode 100644 index 0000000..dacb151 --- /dev/null +++ b/Runtime/Audio/AudioClipCacheEntry.cs @@ -0,0 +1,145 @@ +using System; +using AlicizaX; +using UnityEngine; +using YooAsset; + +namespace AlicizaX.Audio.Runtime +{ + internal sealed class AudioClipCacheEntry : IMemory + { + private readonly Action _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 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(); + } + } + } +} diff --git a/Runtime/Audio/AudioClipCacheEntry.cs.meta b/Runtime/Audio/AudioClipCacheEntry.cs.meta new file mode 100644 index 0000000..2e462f4 --- /dev/null +++ b/Runtime/Audio/AudioClipCacheEntry.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7e438d922e4ac1e439e5a30a0b9d9ac7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Audio/AudioComponent.cs b/Runtime/Audio/AudioComponent.cs index 24ff0d4..7078377 100644 --- a/Runtime/Audio/AudioComponent.cs +++ b/Runtime/Audio/AudioComponent.cs @@ -4,19 +4,14 @@ using UnityEngine.Audio; namespace AlicizaX.Audio.Runtime { - /// - /// 音效管理,为游戏提供统一的音效播放接口。 - /// - /// 场景3D音效挂到场景物件、技能3D音效挂到技能特效上,并在AudioSource的Output上设置对应分类的AudioMixerGroup [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()); } - /// - /// 初始化音频模块。 - /// - 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"); } - - _audioService.Initialize(m_AudioGroupConfigs, m_InstanceRoot, m_AudioMixer); } + } } - diff --git a/Runtime/Audio/AudioData.cs b/Runtime/Audio/AudioData.cs deleted file mode 100644 index 2691ffd..0000000 --- a/Runtime/Audio/AudioData.cs +++ /dev/null @@ -1,61 +0,0 @@ -using AlicizaX; -using YooAsset; - -namespace AlicizaX.Audio.Runtime -{ - /// - /// 音频数据。 - /// - public class AudioData : IMemory - { - /// - /// 资源句柄。 - /// - public AssetHandle AssetHandle { private set; get; } - - /// - /// 是否使用对象池。 - /// - public bool InPool { private set; get; } = false; - - - - /// - /// 生成音频数据。 - /// - /// 资源操作句柄。 - /// 是否使用对象池。 - /// 音频数据。 - internal static AudioData Alloc(AssetHandle assetHandle, bool inPool) - { - AudioData ret = MemoryPool.Acquire(); - ret.AssetHandle = assetHandle; - ret.InPool = inPool; - return ret; - } - - /// - /// 回收音频数据。 - /// - /// - 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(); - } - } -} diff --git a/Runtime/Audio/AudioData.cs.meta b/Runtime/Audio/AudioData.cs.meta deleted file mode 100644 index 429452f..0000000 --- a/Runtime/Audio/AudioData.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 036a1af4acb84666b73909ba28455cfa -timeCreated: 1742472335 \ No newline at end of file diff --git a/Runtime/Audio/AudioDebugInfo.cs b/Runtime/Audio/AudioDebugInfo.cs new file mode 100644 index 0000000..1689b2e --- /dev/null +++ b/Runtime/Audio/AudioDebugInfo.cs @@ -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; + } + } +} diff --git a/Runtime/Audio/AudioDebugInfo.cs.meta b/Runtime/Audio/AudioDebugInfo.cs.meta new file mode 100644 index 0000000..e509662 --- /dev/null +++ b/Runtime/Audio/AudioDebugInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 81dfdc57e55b43799a3451eeca4e40fa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Audio/AudioGroupConfig.cs b/Runtime/Audio/AudioGroupConfig.cs index 469ae2e..2902ae8 100644 --- a/Runtime/Audio/AudioGroupConfig.cs +++ b/Runtime/Audio/AudioGroupConfig.cs @@ -1,48 +1,72 @@ -using System; +using System; using UnityEngine; namespace AlicizaX.Audio.Runtime { - /// - /// 音频轨道组配置。 - /// [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; } } -} \ No newline at end of file +} diff --git a/Runtime/Audio/AudioGroupConfigCollection.cs b/Runtime/Audio/AudioGroupConfigCollection.cs new file mode 100644 index 0000000..9963c2c --- /dev/null +++ b/Runtime/Audio/AudioGroupConfigCollection.cs @@ -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; + } + } +} diff --git a/Runtime/Audio/AudioGroupConfigCollection.cs.meta b/Runtime/Audio/AudioGroupConfigCollection.cs.meta new file mode 100644 index 0000000..7bf6278 --- /dev/null +++ b/Runtime/Audio/AudioGroupConfigCollection.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 35a9ab9e2f42f744ca3801948d9b8c2e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Audio/AudioLoadRequest.cs b/Runtime/Audio/AudioLoadRequest.cs new file mode 100644 index 0000000..b521eb3 --- /dev/null +++ b/Runtime/Audio/AudioLoadRequest.cs @@ -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; + } + } +} diff --git a/Runtime/Audio/AudioLoadRequest.cs.meta b/Runtime/Audio/AudioLoadRequest.cs.meta new file mode 100644 index 0000000..a07c364 --- /dev/null +++ b/Runtime/Audio/AudioLoadRequest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b1b1b306b2b64b24aba7fc6740d9df35 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Audio/AudioPlayRequest.cs b/Runtime/Audio/AudioPlayRequest.cs new file mode 100644 index 0000000..402fbff --- /dev/null +++ b/Runtime/Audio/AudioPlayRequest.cs @@ -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; + } + } +} diff --git a/Runtime/Audio/AudioPlayRequest.cs.meta b/Runtime/Audio/AudioPlayRequest.cs.meta new file mode 100644 index 0000000..175c6fe --- /dev/null +++ b/Runtime/Audio/AudioPlayRequest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 708b6c6f23eadc7428400b296aae7bb6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Audio/AudioService.cs b/Runtime/Audio/AudioService.cs index ca1b5b9..f7c7a6a 100644 --- a/Runtime/Audio/AudioService.cs +++ b/Runtime/Audio/AudioService.cs @@ -1,90 +1,92 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; +using AlicizaX.ObjectPool; using AlicizaX.Resource.Runtime; -using AlicizaX; using UnityEngine; using UnityEngine.Audio; using YooAsset; -using AudioType = AlicizaX.Audio.Runtime.AudioType; - namespace AlicizaX.Audio.Runtime { - internal class AudioService : ServiceBase, IAudioService, IServiceTickable + internal sealed class AudioService : ServiceBase, IAudioService, IAudioDebugService, IServiceTickable { - public const string MUSIC_VOLUME_NAME = "MusicVolume"; - public const string UI_SOUND_VOLUME_NAME = "UISoundVolume"; - public const string VOICE_VOLUME_NAME = "VoiceVolume"; + private const string SourcePoolName = "Audio Source Pool"; + private const int DefaultCacheCapacity = 128; + private const float DefaultClipTtl = 30f; + private const int HandleIndexBits = 20; + private const ulong HandleIndexMask = (1UL << HandleIndexBits) - 1UL; + private static readonly string[] VolumeParameterNames = + { + "SoundVolume", + "UISoundVolume", + "MusicVolume", + "VoiceVolume", + "AmbientVolume" + }; + + private readonly AudioCategory[] _categories = new AudioCategory[(int)AudioType.Max]; + private readonly float[] _categoryVolumes = new float[(int)AudioType.Max]; + private readonly bool[] _categoryEnables = new bool[(int)AudioType.Max]; + private readonly Dictionary _clipCache = new Dictionary(DefaultCacheCapacity, StringComparer.Ordinal); + private readonly AudioSourceObject[][] _sourceObjects = new AudioSourceObject[(int)AudioType.Max][]; + + private IResourceService _resourceService; + private IObjectPool _sourcePool; private AudioMixer _audioMixer; - private Transform _instanceRoot = null; - private AudioGroupConfig[] _audioGroupConfigs = null; - + private Transform _instanceRoot; + private AudioGroupConfig[] _configs; + private AudioAgent[] _handleAgents = Array.Empty(); + private uint[] _handleGenerations = Array.Empty(); + private AudioListener _listenerCache; + private AudioClipCacheEntry _lruHead; + private AudioClipCacheEntry _lruTail; + private AudioClipCacheEntry _allHead; + private AudioClipCacheEntry _allTail; + private int _clipCacheCapacity = DefaultCacheCapacity; + private float _clipTtl = DefaultClipTtl; private float _volume = 1f; private bool _enable = true; - private readonly AudioCategory[] _audioCategories = new AudioCategory[(int)AudioType.Max]; - private readonly float[] _categoriesVolume = new float[(int)AudioType.Max]; - private bool _bUnityAudioDisabled = false; + private bool _unityAudioDisabled; + private bool _initialized; + private bool _ownsInstanceRoot; - #region Public Propreties - - public Dictionary AudioClipPool { get; set; } = new Dictionary(); - - /// - /// 音频混响器。 - /// public AudioMixer AudioMixer => _audioMixer; - - /// - /// 实例化根节点。 - /// public Transform InstanceRoot => _instanceRoot; + public Transform ListenerTransform => _listenerCache != null && _listenerCache.enabled && _listenerCache.gameObject.activeInHierarchy + ? _listenerCache.transform + : null; + int IAudioDebugService.CategoryCount => _categories.Length; + int IAudioDebugService.ClipCacheCount => _clipCache.Count; + int IAudioDebugService.ClipCacheCapacity => _clipCacheCapacity; + int IAudioDebugService.HandleCapacity => _handleAgents.Length; + bool IAudioDebugService.Initialized => _initialized; + bool IAudioDebugService.UnityAudioDisabled => _unityAudioDisabled; + AudioClipCacheEntry IAudioDebugService.FirstClipCacheEntry => _allHead; + public int Priority => 0; - /// - /// 总音量控制。 - /// public float Volume { - get - { - if (_bUnityAudioDisabled) - { - return 0.0f; - } - - return _volume; - } + get => _unityAudioDisabled ? 0f : _volume; set { - if (_bUnityAudioDisabled) + if (_unityAudioDisabled) { return; } - _volume = value; - AudioListener.volume = _volume; + _volume = Mathf.Clamp01(value); + AudioListener.volume = _enable ? _volume : 0f; } } - /// - /// 总开关。 - /// public bool Enable { - get - { - if (_bUnityAudioDisabled) - { - return false; - } - - return _enable; - } + get => !_unityAudioDisabled && _enable; set { - if (_bUnityAudioDisabled) + if (_unityAudioDisabled) { return; } @@ -94,503 +96,1152 @@ namespace AlicizaX.Audio.Runtime } } - /// - /// 音乐音量。 - /// - public float MusicVolume + internal float MusicVolume { - get - { - if (_bUnityAudioDisabled) - { - return 0.0f; - } - - return _categoriesVolume[(int)AudioType.Music]; - } - set - { - if (_bUnityAudioDisabled) - { - return; - } - - float volume = Mathf.Clamp(value, 0.0001f, 1.0f); - _categoriesVolume[(int)AudioType.Music] = volume; - _audioMixer.SetFloat(MUSIC_VOLUME_NAME, Mathf.Log10(volume) * 20f); - } + get => GetCategoryVolume(AudioType.Music); + set => SetCategoryVolume(AudioType.Music, value); } - /// - /// 音效音量。 - /// - public float SoundVolume + internal float SoundVolume { - get - { - if (_bUnityAudioDisabled) - { - return 0.0f; - } - - return _categoriesVolume[(int)AudioType.Sound]; - } - set - { - if (_bUnityAudioDisabled) - { - return; - } - - float volume = Mathf.Clamp(value, 0.0001f, 1.0f); - _categoriesVolume[(int)AudioType.Sound] = volume; - _audioMixer.SetFloat("SoundVolume", Mathf.Log10(volume) * 20f); - } + get => GetCategoryVolume(AudioType.Sound); + set => SetCategoryVolume(AudioType.Sound, value); } - /// - /// UI音效音量。 - /// - public float UISoundVolume + internal float UISoundVolume { - get - { - if (_bUnityAudioDisabled) - { - return 0.0f; - } - - return _categoriesVolume[(int)AudioType.UISound]; - } - set - { - if (_bUnityAudioDisabled) - { - return; - } - - float volume = Mathf.Clamp(value, 0.0001f, 1.0f); - _categoriesVolume[(int)AudioType.UISound] = volume; - _audioMixer.SetFloat(UI_SOUND_VOLUME_NAME, Mathf.Log10(volume) * 20f); - } + get => GetCategoryVolume(AudioType.UISound); + set => SetCategoryVolume(AudioType.UISound, value); } - /// - /// 语音音量。 - /// - public float VoiceVolume + internal float VoiceVolume { - get - { - if (_bUnityAudioDisabled) - { - return 0.0f; - } - - return _categoriesVolume[(int)AudioType.Voice]; - } - set - { - if (_bUnityAudioDisabled) - { - return; - } - - float volume = Mathf.Clamp(value, 0.0001f, 1.0f); - _categoriesVolume[(int)AudioType.Voice] = volume; - _audioMixer.SetFloat(VOICE_VOLUME_NAME, Mathf.Log10(volume) * 20f); - } + get => GetCategoryVolume(AudioType.Voice); + set => SetCategoryVolume(AudioType.Voice, value); } - /// - /// 音乐开关 - /// - public bool MusicEnable + internal float AmbientVolume { - get - { - if (_bUnityAudioDisabled) - { - return false; - } - - if (_audioMixer.GetFloat(MUSIC_VOLUME_NAME, out var db)) - { - return db > -80f; - } - else - { - return false; - } - } - set - { - if (_bUnityAudioDisabled) - { - return; - } - - _audioCategories[(int)AudioType.Music].Enable = value; - - // 音乐采用0音量方式,避免恢复播放时的复杂逻辑 - if (value) - { - _audioMixer.SetFloat(MUSIC_VOLUME_NAME, Mathf.Log10(_categoriesVolume[(int)AudioType.Music]) * 20f); - } - else - { - _audioMixer.SetFloat(MUSIC_VOLUME_NAME, -80f); - } - } + get => GetCategoryVolume(AudioType.Ambient); + set => SetCategoryVolume(AudioType.Ambient, value); } - /// - /// 音效开关。 - /// - public bool SoundEnable + internal bool MusicEnable { - get - { - if (_bUnityAudioDisabled) - { - return false; - } - - return _audioCategories[(int)AudioType.Sound].Enable; - } - set - { - if (_bUnityAudioDisabled) - { - return; - } - - _audioCategories[(int)AudioType.Sound].Enable = value; - } + get => GetCategoryEnable(AudioType.Music); + set => SetCategoryEnable(AudioType.Music, value); } - /// - /// UI音效开关。 - /// - public bool UISoundEnable + internal bool SoundEnable { - get - { - if (_bUnityAudioDisabled) - { - return false; - } - - return _audioCategories[(int)AudioType.UISound].Enable; - } - set - { - if (_bUnityAudioDisabled) - { - return; - } - - _audioCategories[(int)AudioType.UISound].Enable = value; - } + get => GetCategoryEnable(AudioType.Sound); + set => SetCategoryEnable(AudioType.Sound, value); } - /// - /// 语音开关。 - /// - public bool VoiceEnable + internal bool UISoundEnable { - get - { - if (_bUnityAudioDisabled) - { - return false; - } - - return _audioCategories[(int)AudioType.Voice].Enable; - } - set - { - if (_bUnityAudioDisabled) - { - return; - } - - _audioCategories[(int)AudioType.Voice].Enable = value; - } + get => GetCategoryEnable(AudioType.UISound); + set => SetCategoryEnable(AudioType.UISound, value); } - #endregion - - private IResourceService _resourceService; + internal bool VoiceEnable + { + get => GetCategoryEnable(AudioType.Voice); + set => SetCategoryEnable(AudioType.Voice, value); + } + internal bool AmbientEnable + { + get => GetCategoryEnable(AudioType.Ambient); + set => SetCategoryEnable(AudioType.Ambient, value); + } protected override void OnInitialize() { } protected override void OnDestroyService() { - StopAll(fadeout: false); - CleanSoundPool(); + Shutdown(true); } - /// - /// 初始化音频模块。 - /// - /// 音频轨道组配置。 - /// 实例化根节点。 - /// 音频混响器。 - /// public void Initialize(AudioGroupConfig[] audioGroupConfigs, Transform instanceRoot = null, AudioMixer audioMixer = null) { - _resourceService = AppServices.Require(); - if (_instanceRoot == null) - { - _instanceRoot = instanceRoot; - } + Shutdown(false); - if (audioGroupConfigs == null) + _resourceService = AppServices.Require(); + IObjectPoolService objectPoolService = AppServices.Require(); + _sourcePool = objectPoolService.HasObjectPool(SourcePoolName) + ? objectPoolService.GetObjectPool(SourcePoolName) + : objectPoolService.CreatePool(new ObjectPoolCreateOptions(SourcePoolName, false, 10f, int.MaxValue, float.MaxValue, 10)); + + if (audioGroupConfigs == null || audioGroupConfigs.Length == 0) { throw new GameFrameworkException("AudioGroupConfig[] is invalid."); } - _audioGroupConfigs = audioGroupConfigs; - - if (_instanceRoot == null) + _configs = audioGroupConfigs; + if (instanceRoot != null) { - _instanceRoot = new GameObject("[AudioModule Instances]").transform; - _instanceRoot.localScale = Vector3.one; - UnityEngine.Object.DontDestroyOnLoad(_instanceRoot); + _instanceRoot = instanceRoot; + _ownsInstanceRoot = false; + } + else if (_instanceRoot == null) + { + _instanceRoot = new GameObject("[AudioService Instances]").transform; + _ownsInstanceRoot = true; } + _instanceRoot.localScale = Vector3.one; + + if (_ownsInstanceRoot) + { + UnityEngine.Object.DontDestroyOnLoad(_instanceRoot.gameObject); + } + + _unityAudioDisabled = IsUnityAudioDisabled(); + if (_unityAudioDisabled) + { + _initialized = true; + return; + } + + _audioMixer = audioMixer != null ? audioMixer : Resources.Load("AudioMixer"); + if (_audioMixer == null) + { + throw new GameFrameworkException("AudioMixer is invalid."); + } + + int totalAgentCount = 0; + for (int i = 0; i < (int)AudioType.Max; i++) + { + AudioGroupConfig config = FindConfig((AudioType)i); + totalAgentCount += config != null ? config.AgentHelperCount : 1; + } + + if ((ulong)totalAgentCount > HandleIndexMask) + { + throw new GameFrameworkException("Audio agent count exceeds handle capacity."); + } + + _handleAgents = new AudioAgent[totalAgentCount]; + _handleGenerations = new uint[totalAgentCount]; + MemoryPool.Add(totalAgentCount); + MemoryPool.Add(totalAgentCount); + MemoryPool.Add(_clipCacheCapacity); + + int globalIndexOffset = 0; + for (int i = 0; i < (int)AudioType.Max; i++) + { + AudioGroupConfig config = FindConfig((AudioType)i); + if (config == null) + { + config = CreateRuntimeDefaultConfig((AudioType)i); + } + + _categoryVolumes[i] = Mathf.Clamp(config.Volume, 0.0001f, 1f); + _categoryEnables[i] = !config.Mute; + _sourceObjects[i] = new AudioSourceObject[config.AgentHelperCount]; + _categories[i] = new AudioCategory(this, _audioMixer, config, globalIndexOffset); + globalIndexOffset += config.AgentHelperCount; + ApplyMixerVolume(config, _categoryVolumes[i], _categoryEnables[i]); + } + + _initialized = true; + } + + public void Restart() + { + Initialize(_configs, _instanceRoot, _audioMixer); + } + + public ulong Play(AudioType type, string path, bool loop = false, float volume = 1f, bool async = false, bool cacheClip = true) + { + AudioPlayRequest request = MemoryPool.Acquire(); + request.Set2D(type, path, loop, volume, async, cacheClip); + ulong handle = Play(request); + MemoryPool.Release(request); + return handle; + } + + public ulong Play(AudioType type, AudioClip clip, bool loop = false, float volume = 1f) + { + AudioPlayRequest request = MemoryPool.Acquire(); + request.Set2D(type, clip, loop, volume); + ulong handle = Play(request); + MemoryPool.Release(request); + return handle; + } + + public ulong Play3D(AudioType type, string path, in Vector3 position, bool loop = false, float volume = 1f, bool async = false, bool cacheClip = true) + { + AudioPlayRequest request = MemoryPool.Acquire(); + request.Set3D(type, path, position, loop, volume, async, cacheClip); + ulong handle = Play(request); + MemoryPool.Release(request); + return handle; + } + + public 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) + { + AudioPlayRequest request = MemoryPool.Acquire(); + request.Set3D(type, path, position, minDistance, maxDistance, rolloffMode, spatialBlend, loop, volume, async, cacheClip); + ulong handle = Play(request); + MemoryPool.Release(request); + return handle; + } + + public ulong Play3D(AudioType type, AudioClip clip, in Vector3 position, bool loop = false, float volume = 1f) + { + if (clip == null) + { + return 0UL; + } + + AudioPlayRequest request = MemoryPool.Acquire(); + request.Set3D(type, clip, position, loop, volume); + ulong handle = Play(request); + MemoryPool.Release(request); + return handle; + } + + public ulong Play3D(AudioType type, AudioClip clip, in Vector3 position, float minDistance, float maxDistance, AudioRolloffMode rolloffMode, float spatialBlend = 1f, bool loop = false, float volume = 1f) + { + if (clip == null) + { + return 0UL; + } + + AudioPlayRequest request = MemoryPool.Acquire(); + request.Set3D(type, clip, position, minDistance, maxDistance, rolloffMode, spatialBlend, loop, volume); + ulong handle = Play(request); + MemoryPool.Release(request); + return handle; + } + + public ulong PlayFollow(AudioType type, string path, Transform target, in Vector3 localOffset, bool loop = false, float volume = 1f, bool async = false, bool cacheClip = true) + { + if (target == null) + { + return 0UL; + } + + AudioPlayRequest request = MemoryPool.Acquire(); + request.SetFollow(type, path, target, localOffset, loop, volume, async, cacheClip); + ulong handle = Play(request); + MemoryPool.Release(request); + return handle; + } + + public ulong PlayFollow(AudioType type, AudioClip clip, Transform target, in Vector3 localOffset, bool loop = false, float volume = 1f) + { + if (target == null || clip == null) + { + return 0UL; + } + + AudioPlayRequest request = MemoryPool.Acquire(); + request.SetFollow(type, clip, target, localOffset, loop, volume); + ulong handle = Play(request); + MemoryPool.Release(request); + return handle; + } + + public 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) + { + if (target == null || clip == null) + { + return 0UL; + } + + AudioPlayRequest request = MemoryPool.Acquire(); + request.SetFollow(type, clip, target, localOffset, minDistance, maxDistance, rolloffMode, spatialBlend, loop, volume); + ulong handle = Play(request); + MemoryPool.Release(request); + return handle; + } + + public 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) + { + if (target == null) + { + return 0UL; + } + + AudioPlayRequest request = MemoryPool.Acquire(); + request.SetFollow(type, path, target, localOffset, minDistance, maxDistance, rolloffMode, spatialBlend, loop, volume, async, cacheClip); + ulong handle = Play(request); + MemoryPool.Release(request); + return handle; + } + + internal ulong Play(AudioPlayRequest request) + { + if (!_initialized || _unityAudioDisabled || request == null) + { + return 0UL; + } + + int index = (int)request.Type; + if ((uint)index >= (uint)_categories.Length) + { + return 0UL; + } + + AudioCategory category = _categories[index]; + return category != null ? category.Play(request) : 0UL; + } + + public bool Stop(ulong handle, bool fadeout = false) + { + AudioAgent agent = ResolveHandle(handle); + if (agent == null) + { + return false; + } + + agent.Stop(fadeout); + return true; + } + + public bool IsPlaying(ulong handle) + { + AudioAgent agent = ResolveHandle(handle); + return agent != null && agent.IsPlayingState; + } + + public void Stop(AudioType type, bool fadeout) + { + int index = (int)type; + if ((uint)index < (uint)_categories.Length && _categories[index] != null) + { + _categories[index].Stop(fadeout); + } + } + + public void StopAll(bool fadeout) + { + for (int i = 0; i < _categories.Length; i++) + { + AudioCategory category = _categories[i]; + if (category != null) + { + category.Stop(fadeout); + } + } + } + + public void Pause(ulong handle) + { + AudioAgent agent = ResolveHandle(handle); + if (agent != null) + { + agent.Pause(); + } + } + + public void Resume(ulong handle) + { + AudioAgent agent = ResolveHandle(handle); + if (agent != null) + { + agent.Resume(); + } + } + + public void Preload(IList paths, bool pin = true) + { + if (paths == null || _unityAudioDisabled) + { + return; + } + + for (int i = 0; i < paths.Count; i++) + { + string path = paths[i]; + if (string.IsNullOrEmpty(path)) + { + continue; + } + + AudioClipCacheEntry entry = GetOrCreateClipEntry(path, pin); + entry.Pinned = entry.Pinned || pin; + entry.CacheAfterUse = true; + TouchClip(entry); + if (entry.Handle == null) + { + BeginLoad(entry, true); + } + } + } + + public void Unload(IList paths) + { + if (paths == null) + { + return; + } + + for (int i = 0; i < paths.Count; i++) + { + string path = paths[i]; + if (string.IsNullOrEmpty(path) || !_clipCache.TryGetValue(path, out AudioClipCacheEntry entry)) + { + continue; + } + + entry.Pinned = false; + if (entry.RefCount <= 0 && !entry.Loading && entry.PendingHead == null) + { + RemoveClipEntry(entry); + } + } + } + + public void ClearCache() + { + AudioClipCacheEntry entry = _allHead; + while (entry != null) + { + AudioClipCacheEntry next = entry.AllNext; + if (entry.RefCount <= 0 && !entry.Loading && entry.PendingHead == null) + { + RemoveClipEntry(entry); + } + entry = next; + } + } + + void IServiceTickable.Tick(float deltaTime) + { + if (!_initialized || _unityAudioDisabled) + { + return; + } + + for (int i = 0; i < _categories.Length; i++) + { + AudioCategory category = _categories[i]; + if (category != null) + { + category.Update(deltaTime); + } + } + + TrimClipCache(); + } + + internal AudioSourceObject AcquireSourceObject(AudioCategory category, int index) + { + string name = BuildSourceName(category.TypeIndex, index); + AudioSourceObject sourceObject = _sourcePool.Spawn(name); + if (sourceObject == null) + { + sourceObject = CreateSourceObject(category, index, name); + _sourcePool.Register(sourceObject, true); + } + + _sourceObjects[category.TypeIndex][index] = sourceObject; + return sourceObject; + } + + internal void ReleaseSourceObject(int typeIndex, int index) + { + AudioSourceObject[] sourceObjects = _sourceObjects[typeIndex]; + if (sourceObjects == null) + { + return; + } + + AudioSourceObject sourceObject = sourceObjects[index]; + if (sourceObject == null) + { + return; + } + + sourceObjects[index] = null; + if (sourceObject.Source != null) + { + sourceObject.Source.transform.SetParent(_instanceRoot, false); + } + + _sourcePool.Unspawn(sourceObject); + } + + internal ulong AllocateHandle(AudioAgent agent) + { + int index = agent.GlobalIndex; + if ((uint)index >= (uint)_handleAgents.Length) + { + return 0UL; + } + + uint generation = _handleGenerations[index] + 1U; + if (generation == 0) + { + generation = 1U; + } + + _handleGenerations[index] = generation; + _handleAgents[index] = agent; + return ((ulong)generation << HandleIndexBits) | (uint)(index + 1); + } + + internal void ReleaseHandle(ulong handle, AudioAgent agent) + { + int index = (int)((handle & HandleIndexMask) - 1UL); + if ((uint)index < (uint)_handleAgents.Length && ReferenceEquals(_handleAgents[index], agent)) + { + _handleAgents[index] = null; + } + } + + private AudioAgent ResolveHandle(ulong handle) + { + if (handle == 0) + { + return null; + } + + int index = (int)((handle & HandleIndexMask) - 1UL); + uint generation = (uint)(handle >> HandleIndexBits); + if ((uint)index >= (uint)_handleAgents.Length || _handleGenerations[index] != generation) + { + return null; + } + + return _handleAgents[index]; + } + + internal AudioClipCacheEntry RequestClip(string address, bool async, bool cacheClip, AudioAgent agent, int generation) + { + AudioClipCacheEntry entry = GetOrCreateClipEntry(address, cacheClip); + entry.CacheAfterUse = entry.CacheAfterUse || cacheClip; + TouchClip(entry); + + if (entry.IsLoaded) + { + return entry; + } + + AudioLoadRequest request = MemoryPool.Acquire(); + request.Agent = agent; + request.Generation = generation; + entry.AddPending(request); + + if (entry.Handle == null) + { + BeginLoad(entry, async); + } + + return null; + } + + internal void RetainClip(AudioClipCacheEntry entry) + { + entry.RefCount++; + RemoveFromLru(entry); + TouchClip(entry); + } + + internal void ReleaseClip(AudioClipCacheEntry entry) + { + if (entry.RefCount > 0) + { + entry.RefCount--; + } + + TouchClip(entry); + if (entry.RefCount <= 0) + { + if (entry.CacheAfterUse) + { + AddToLruTail(entry); + } + else + { + RemoveClipEntry(entry); + } + } + } + + internal void OnClipLoadCompleted(AudioClipCacheEntry entry, AssetHandle handle) + { + if (entry == null || !_clipCache.TryGetValue(entry.Address, out AudioClipCacheEntry mapped) || !ReferenceEquals(mapped, entry)) + { + if (handle is { IsValid: true }) + { + handle.Dispose(); + } + + return; + } + + entry.Loading = false; + if (handle != null) + { + handle.Completed -= entry.CompletedCallback; + } + + bool success = handle is { IsValid: true } && handle.AssetObject is AudioClip; + if (success) + { + entry.Handle = handle; + entry.Clip = (AudioClip)handle.AssetObject; + TouchClip(entry); + } + + AudioLoadRequest request = entry.PendingHead; + entry.PendingHead = null; + entry.PendingTail = null; + + while (request != null) + { + AudioLoadRequest next = request.Next; + if (success) + { + request.Agent?.OnClipReady(entry, request.Generation); + } + else + { + request.Agent?.OnClipLoadFailed(request.Generation); + } + + MemoryPool.Release(request); + request = next; + } + + if (!success) + { + RemoveClipEntry(entry); + if (handle is { IsValid: true }) + { + handle.Dispose(); + } + } + else if (entry.RefCount <= 0 && entry.CacheAfterUse) + { + AddToLruTail(entry); + } + else if (success && entry.RefCount <= 0) + { + RemoveClipEntry(entry); + } + } + + public float GetCategoryVolume(AudioType type) + { + if (_unityAudioDisabled) + { + return 0f; + } + + int index = (int)type; + return (uint)index < (uint)_categoryVolumes.Length ? _categoryVolumes[index] : 0f; + } + + public void SetCategoryVolume(AudioType type, float value) + { + if (_unityAudioDisabled) + { + return; + } + + int index = (int)type; + if ((uint)index >= (uint)_categoryVolumes.Length) + { + return; + } + + float volume = Mathf.Clamp(value, 0.0001f, 1f); + _categoryVolumes[index] = volume; + AudioGroupConfig config = GetConfig(type); + ApplyMixerVolume(config, volume, _categoryEnables[index]); + } + + public bool GetCategoryEnable(AudioType type) + { + if (_unityAudioDisabled) + { + return false; + } + + int index = (int)type; + return (uint)index < (uint)_categoryEnables.Length && _categoryEnables[index]; + } + + public void SetCategoryEnable(AudioType type, bool value) + { + if (_unityAudioDisabled) + { + return; + } + + int index = (int)type; + if ((uint)index >= (uint)_categoryEnables.Length) + { + return; + } + + _categoryEnables[index] = value; + AudioCategory category = _categories[index]; + if (category != null) + { + category.Enabled = value; + } + + AudioGroupConfig config = GetConfig(type); + ApplyMixerVolume(config, _categoryVolumes[index], value); + } + + public void RegisterListener(AudioListener listener) + { + if (listener == null) + { + return; + } + + _listenerCache = listener; + } + + public void UnregisterListener(AudioListener listener) + { + if (listener == null) + { + return; + } + + if (ReferenceEquals(_listenerCache, listener)) + { + _listenerCache = null; + } + } + + void IAudioDebugService.FillServiceDebugInfo(AudioServiceDebugInfo info) + { + if (info == null) + { + return; + } + + info.Initialized = _initialized; + info.UnityAudioDisabled = _unityAudioDisabled; + info.Enable = Enable; + info.Volume = Volume; + info.CategoryCount = _categories.Length; + info.ActiveAgentCount = CountActiveAgents(); + info.HandleCapacity = _handleAgents.Length; + info.ClipCacheCount = _clipCache.Count; + info.ClipCacheCapacity = _clipCacheCapacity; + info.Listener = _listenerCache; + info.InstanceRoot = _instanceRoot; + } + + bool IAudioDebugService.FillCategoryDebugInfo(int typeIndex, AudioCategoryDebugInfo info) + { + if (info == null || (uint)typeIndex >= (uint)_categories.Length) + { + return false; + } + + AudioCategory category = _categories[typeIndex]; + if (category == null) + { + info.Clear(); + info.Type = (AudioType)typeIndex; + return false; + } + + float volume = (uint)typeIndex < (uint)_categoryVolumes.Length ? _categoryVolumes[typeIndex] : 0f; + category.FillDebugInfo(volume, info); + return true; + } + + bool IAudioDebugService.FillAgentDebugInfo(int typeIndex, int agentIndex, AudioAgentDebugInfo info) + { + if (info == null || (uint)typeIndex >= (uint)_categories.Length) + { + return false; + } + + AudioCategory category = _categories[typeIndex]; + if (category == null || !category.TryGetAgent(agentIndex, out AudioAgent agent) || agent == null) + { + info.Clear(); + return false; + } + + agent.FillDebugInfo(info); + return true; + } + + bool IAudioDebugService.FillClipCacheDebugInfo(AudioClipCacheEntry entry, AudioClipCacheDebugInfo info) + { + if (entry == null || info == null) + { + return false; + } + + entry.FillDebugInfo(info); + return true; + } + + private void ApplyMixerVolume(AudioGroupConfig config, float volume, bool enabled) + { + if (_audioMixer == null || config == null) + { + return; + } + + string parameter = string.IsNullOrEmpty(config.ExposedVolumeParameter) + ? VolumeParameterNames[(int)config.AudioType] + : config.ExposedVolumeParameter; + _audioMixer.SetFloat(parameter, enabled ? Mathf.Log10(volume) * 20f : -80f); + } + + private AudioClipCacheEntry GetOrCreateClipEntry(string address, bool pinned) + { + if (_clipCache.TryGetValue(address, out AudioClipCacheEntry entry)) + { + return entry; + } + + entry = MemoryPool.Acquire(); + entry.Initialize(this, address, pinned); + _clipCache.Add(address, entry); + AddToAllList(entry); + if (pinned) + { + AddToLruTail(entry); + } + return entry; + } + + private void BeginLoad(AudioClipCacheEntry entry, bool async) + { + entry.Loading = async; + if (async) + { + entry.Handle = _resourceService.LoadAssetAsyncHandle(entry.Address); + entry.Handle.Completed += entry.CompletedCallback; + return; + } + + AssetHandle handle = _resourceService.LoadAssetSyncHandle(entry.Address); + entry.Handle = handle; + OnClipLoadCompleted(entry, handle); + } + + private void TouchClip(AudioClipCacheEntry entry) + { + entry.LastUseTime = Time.realtimeSinceStartup; + if (entry.RefCount <= 0 && entry.IsLoaded) + { + MoveLruToTail(entry); + } + } + + private void TrimClipCache() + { + float now = Time.realtimeSinceStartup; + while (_clipCache.Count > _clipCacheCapacity && _lruHead != null) + { + AudioClipCacheEntry entry = _lruHead; + if (!CanEvict(entry, now, false)) + { + break; + } + + RemoveClipEntry(entry); + } + + AudioClipCacheEntry current = _lruHead; + while (current != null) + { + AudioClipCacheEntry next = current.LruNext; + if (!CanEvict(current, now, true)) + { + break; + } + + RemoveClipEntry(current); + current = next; + } + } + + private bool CanEvict(AudioClipCacheEntry entry, float now, bool requireExpired) + { + if (entry == null || entry.RefCount > 0 || entry.Pinned || entry.Loading) + { + return false; + } + + if (!requireExpired) + { + return true; + } + + return now - entry.LastUseTime >= _clipTtl; + } + + private void RemoveClipEntry(AudioClipCacheEntry entry) + { + if (entry == null) + { + return; + } + + _clipCache.Remove(entry.Address); + RemoveFromLru(entry); + RemoveFromAllList(entry); + MemoryPool.Release(entry); + } + + private void AddToLruTail(AudioClipCacheEntry entry) + { + if (entry.InLru || entry.RefCount > 0 || entry.Loading || !entry.CacheAfterUse) + { + return; + } + + entry.InLru = true; + entry.LruPrev = _lruTail; + entry.LruNext = null; + if (_lruTail != null) + { + _lruTail.LruNext = entry; + } + else + { + _lruHead = entry; + } + + _lruTail = entry; + } + + private void RemoveFromLru(AudioClipCacheEntry entry) + { + if (!entry.InLru) + { + return; + } + + AudioClipCacheEntry prev = entry.LruPrev; + AudioClipCacheEntry next = entry.LruNext; + if (prev != null) + { + prev.LruNext = next; + } + else + { + _lruHead = next; + } + + if (next != null) + { + next.LruPrev = prev; + } + else + { + _lruTail = prev; + } + + entry.LruPrev = null; + entry.LruNext = null; + entry.InLru = false; + } + + private void MoveLruToTail(AudioClipCacheEntry entry) + { + if (!entry.InLru || ReferenceEquals(_lruTail, entry)) + { + return; + } + + RemoveFromLru(entry); + AddToLruTail(entry); + } + + private void AddToAllList(AudioClipCacheEntry entry) + { + entry.AllPrev = _allTail; + entry.AllNext = null; + if (_allTail != null) + { + _allTail.AllNext = entry; + } + else + { + _allHead = entry; + } + + _allTail = entry; + } + + private void RemoveFromAllList(AudioClipCacheEntry entry) + { + AudioClipCacheEntry prev = entry.AllPrev; + AudioClipCacheEntry next = entry.AllNext; + if (prev != null) + { + prev.AllNext = next; + } + else + { + _allHead = next; + } + + if (next != null) + { + next.AllPrev = prev; + } + else + { + _allTail = prev; + } + + entry.AllPrev = null; + entry.AllNext = null; + } + + private AudioSourceObject CreateSourceObject(AudioCategory category, int index, string name) + { + GameObject host = new GameObject(name); + host.transform.SetParent(category.InstanceRoot, false); + AudioSource source = host.AddComponent(); + source.playOnAwake = false; + source.outputAudioMixerGroup = category.MixerGroup; + source.rolloffMode = category.Config.RolloffMode; + source.minDistance = category.Config.MinDistance; + source.maxDistance = category.Config.MaxDistance; + AudioLowPassFilter lowPassFilter = host.AddComponent(); + lowPassFilter.enabled = false; + host.SetActive(true); + return AudioSourceObject.Create(name, source, lowPassFilter); + } + + private static string BuildSourceName(int typeIndex, int index) + { + return "AudioSource_" + typeIndex + "_" + index; + } + + private AudioGroupConfig GetConfig(AudioType type) + { + AudioCategory category = _categories[(int)type]; + if (category != null) + { + return category.Config; + } + + return FindConfig(type); + } + + private AudioGroupConfig FindConfig(AudioType type) + { + if (_configs == null) + { + return null; + } + + for (int i = 0; i < _configs.Length; i++) + { + AudioGroupConfig config = _configs[i]; + if (config != null && config.AudioType == type) + { + return config; + } + } + + return null; + } + + private int CountActiveAgents() + { + int count = 0; + for (int i = 0; i < _categories.Length; i++) + { + AudioCategory category = _categories[i]; + if (category != null) + { + count += category.ActiveCount; + } + } + + return count; + } + + private static AudioGroupConfig CreateRuntimeDefaultConfig(AudioType type) + { + AudioGroupConfig config = new AudioGroupConfig(); + config.SetDefaults(type, type.ToString(), VolumeParameterNames[(int)type], 8, type == AudioType.Music || type == AudioType.UISound ? 0f : 1f, type == AudioType.Voice || type == AudioType.Ambient); + return config; + } + + private void Shutdown(bool destroyRoot) + { + StopAll(false); + + for (int i = 0; i < _categories.Length; i++) + { + AudioCategory category = _categories[i]; + if (category != null) + { + category.Shutdown(); + _categories[i] = null; + } + } + + ClearCache(); + if (_sourcePool != null) + { + _sourcePool.ReleaseAllUnused(); + } + + Array.Clear(_handleAgents, 0, _handleAgents.Length); + Array.Clear(_handleGenerations, 0, _handleGenerations.Length); + _handleAgents = Array.Empty(); + _handleGenerations = Array.Empty(); + _resourceService = null; + _sourcePool = null; + _audioMixer = null; + _listenerCache = null; + _initialized = false; + for (int i = 0; i < _sourceObjects.Length; i++) + { + _sourceObjects[i] = null; + } + + if (destroyRoot) + { + DestroyOwnedRoot(); + } + } + + private void DestroyOwnedRoot() + { + if (_ownsInstanceRoot && _instanceRoot != null) + { + UnityEngine.Object.Destroy(_instanceRoot.gameObject); + } + + _instanceRoot = null; + _ownsInstanceRoot = false; + } + + private static bool IsUnityAudioDisabled() + { #if UNITY_EDITOR try { TypeInfo typeInfo = typeof(AudioSettings).GetTypeInfo(); PropertyInfo propertyInfo = typeInfo.GetDeclaredProperty("unityAudioDisabled"); - _bUnityAudioDisabled = (bool)propertyInfo.GetValue(null); - if (_bUnityAudioDisabled) - { - return; - } + return propertyInfo != null && (bool)propertyInfo.GetValue(null); } - catch (Exception e) + catch (Exception exception) { - Log.Error(e.ToString()); + Log.Error(exception.ToString()); } #endif - - if (audioMixer != null) - { - _audioMixer = audioMixer; - } - - if (_audioMixer == null) - { - _audioMixer = Resources.Load("AudioMixer"); - } - - for (int index = 0; index < (int)AudioType.Max; ++index) - { - AudioType audioType = (AudioType)index; - AudioGroupConfig audioGroupConfig = _audioGroupConfigs.First(t => t.AudioType == audioType); - _audioCategories[index] = new AudioCategory(audioGroupConfig.AgentHelperCount, _audioMixer, audioGroupConfig); - _categoriesVolume[index] = audioGroupConfig.Volume; - } + return false; } - /// - /// 重启音频模块。 - /// - public void Restart() - { - if (_bUnityAudioDisabled) - { - return; - } - - CleanSoundPool(); - - for (int i = 0; i < (int)AudioType.Max; ++i) - { - var audioCategory = _audioCategories[i]; - if (audioCategory != null) - { - for (int j = 0; j < audioCategory.AudioAgents.Count; ++j) - { - var audioAgent = audioCategory.AudioAgents[j]; - if (audioAgent != null) - { - audioAgent.Destroy(); - audioAgent = null; - } - } - } - - audioCategory = null; - } - - Initialize(_audioGroupConfigs); - } - - /// - /// 播放,如果超过最大发声数采用fadeout的方式复用最久播放的AudioSource。 - /// - /// 声音类型 - /// 声音文件路径 - /// 是否循环播放> - /// 音量(0-1.0) - /// 是否异步加载 - /// 是否支持资源池 - public AudioAgent Play(AudioType type, string path, bool bLoop = false, float volume = 1.0f, bool bAsync = false, bool bInPool = false) - { - if (_bUnityAudioDisabled) - { - return null; - } - - AudioAgent audioAgent = _audioCategories[(int)type].Play(path, bAsync, bInPool); - { - if (audioAgent != null) - { - audioAgent.IsLoop = bLoop; - audioAgent.Volume = volume; - } - - return audioAgent; - } - } - - public AudioAgent Play(AudioType type, AudioClip clip, bool bLoop = false, float volume = 1.0f) - { - if (_bUnityAudioDisabled) - { - return null; - } - - AudioAgent audioAgent = _audioCategories[(int)type].Play(clip); - { - if (audioAgent != null) - { - audioAgent.IsLoop = bLoop; - audioAgent.Volume = volume; - } - - return audioAgent; - } - } - - /// - /// 停止某类声音播放。 - /// - /// 声音类型。 - /// 是否渐消。 - public void Stop(AudioType type, bool fadeout) - { - if (_bUnityAudioDisabled) - { - return; - } - - _audioCategories[(int)type].Stop(fadeout); - } - - /// - /// 停止所有声音。 - /// - /// 是否渐消。 - public void StopAll(bool fadeout) - { - if (_bUnityAudioDisabled) - { - return; - } - - for (int i = 0; i < (int)AudioType.Max; ++i) - { - if (_audioCategories[i] != null) - { - _audioCategories[i].Stop(fadeout); - } - } - } - - /// - /// 预先加载AudioClip,并放入对象池。 - /// - /// AudioClip的AssetPath集合。 - public void PutInAudioPool(List list) - { - if (_bUnityAudioDisabled) - { - return; - } - - foreach (string path in list) - { - if (AudioClipPool != null && !AudioClipPool.ContainsKey(path)) - { - AssetHandle assetData = _resourceService.LoadAssetAsyncHandle(path); - assetData.Completed += handle => { AudioClipPool?.Add(path, handle); }; - } - } - } - - /// - /// 将部分AudioClip从对象池移出。 - /// - /// AudioClip的AssetPath集合。 - public void RemoveClipFromPool(List list) - { - if (_bUnityAudioDisabled) - { - return; - } - - foreach (string path in list) - { - if (AudioClipPool.ContainsKey(path)) - { - AudioClipPool[path].Dispose(); - AudioClipPool.Remove(path); - } - } - } - - /// - /// 清空AudioClip的对象池。 - /// - public void CleanSoundPool() - { - if (_bUnityAudioDisabled) - { - return; - } - - foreach (var dic in AudioClipPool) - { - dic.Value.Dispose(); - } - - AudioClipPool.Clear(); - } - - /// - /// 音频模块轮询。 - /// - /// 逻辑流逝时间,以秒为单位。 - /// 真实流逝时间,以秒为单位。 - void IServiceTickable.Tick(float deltaTime) - { - foreach (var audioCategory in _audioCategories) - { - if (audioCategory != null) - { - audioCategory.Update(deltaTime); - } - } - } - - - public int Priority { get; } - } } diff --git a/Runtime/Audio/AudioSourceObject.cs b/Runtime/Audio/AudioSourceObject.cs new file mode 100644 index 0000000..968387e --- /dev/null +++ b/Runtime/Audio/AudioSourceObject.cs @@ -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.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; + } + } + } +} diff --git a/Runtime/Audio/AudioSourceObject.cs.meta b/Runtime/Audio/AudioSourceObject.cs.meta new file mode 100644 index 0000000..fe6efef --- /dev/null +++ b/Runtime/Audio/AudioSourceObject.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c79fdc0db53879a4691d4045ecb1e9d4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Audio/AudioType.cs b/Runtime/Audio/AudioType.cs index 82f0014..6f232ce 100644 --- a/Runtime/Audio/AudioType.cs +++ b/Runtime/Audio/AudioType.cs @@ -1,34 +1,12 @@ -namespace AlicizaX.Audio.Runtime +namespace AlicizaX.Audio.Runtime { - /// - /// 音效分类,可分别关闭/开启对应分类音效。 - /// - /// 命名与AudioMixer中分类名保持一致。 public enum AudioType { - /// - /// 声音音效。 - /// - Sound, - - /// - /// UI声效。 - /// - UISound, - - /// - /// 背景音乐音效。 - /// - Music, - - /// - /// 人声音效。 - /// - Voice, - - /// - /// 最大。 - /// - Max + Sound = 0, + UISound = 1, + Music = 2, + Voice = 3, + Ambient = 4, + Max = 5 } -} \ No newline at end of file +} diff --git a/Runtime/Audio/Components.meta b/Runtime/Audio/Components.meta new file mode 100644 index 0000000..4b2f5aa --- /dev/null +++ b/Runtime/Audio/Components.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b4583bd26706a0f43a91b4e406ff1a7d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Audio/Components/AudioEmitter.cs b/Runtime/Audio/Components/AudioEmitter.cs new file mode 100644 index 0000000..1e99b04 --- /dev/null +++ b/Runtime/Audio/Components/AudioEmitter.cs @@ -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); + } + } +} diff --git a/Runtime/Audio/Components/AudioEmitter.cs.meta b/Runtime/Audio/Components/AudioEmitter.cs.meta new file mode 100644 index 0000000..8901360 --- /dev/null +++ b/Runtime/Audio/Components/AudioEmitter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f850e2c8d01392a41912893526cebe9e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Audio/Components/AudioListenerBinder.cs b/Runtime/Audio/Components/AudioListenerBinder.cs new file mode 100644 index 0000000..099464c --- /dev/null +++ b/Runtime/Audio/Components/AudioListenerBinder.cs @@ -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(); + } + } + + 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); + } + } + } +} diff --git a/Runtime/Audio/Components/AudioListenerBinder.cs.meta b/Runtime/Audio/Components/AudioListenerBinder.cs.meta new file mode 100644 index 0000000..158bc12 --- /dev/null +++ b/Runtime/Audio/Components/AudioListenerBinder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e294cc163e4d4564187a681ba9d76597 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Audio/IAudioService.cs b/Runtime/Audio/IAudioService.cs index 639d9aa..109a094 100644 --- a/Runtime/Audio/IAudioService.cs +++ b/Runtime/Audio/IAudioService.cs @@ -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 { - /// - /// 总音量控制。 - /// - public float Volume { get; set; } + float Volume { get; set; } + bool Enable { get; set; } + AudioMixer AudioMixer { get; } + Transform InstanceRoot { get; } + Transform ListenerTransform { get; } - /// - /// 总开关。 - /// - public bool Enable { get; set; } - - /// - /// 音乐音量 - /// - public float MusicVolume { get; set; } - - /// - /// 音效音量。 - /// - public float SoundVolume { get; set; } - - /// - /// UI音效音量。 - /// - public float UISoundVolume { get; set; } - - /// - /// 语音音量。 - /// - public float VoiceVolume { get; set; } - - /// - /// 音乐开关。 - /// - public bool MusicEnable { get; set; } - - /// - /// 音效开关。 - /// - public bool SoundEnable { get; set; } - - /// - /// UI音效开关。 - /// - public bool UISoundEnable { get; set; } - - /// - /// 语音开关。 - /// - public bool VoiceEnable { get; set; } - - /// - /// 音频混响器。 - /// - public AudioMixer AudioMixer { get;} - - /// - /// 实例化根节点。 - /// - public Transform InstanceRoot { get;} - - public Dictionary AudioClipPool { get; set; } - - /// - /// 初始化音频模块。 - /// - /// 音频轨道组配置。 - /// 实例化根节点。 - /// 音频混响器。 - /// - public void Initialize(AudioGroupConfig[] audioGroupConfigs, Transform instanceRoot = null, AudioMixer audioMixer = null); - - /// - /// 重启音频模块。 - /// - public void Restart(); - - /// - /// 播放音频接口。 - /// - /// 如果超过最大发声数采用fadeout的方式复用最久播放的AudioSource。 - /// 声音类型。 - /// 声音文件路径。 - /// 是否循环播放。> - /// 音量(0-1.0)。 - /// 是否异步加载。 - /// 是否支持资源池。 - 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); - /// - /// 停止某类声音播放。 - /// - /// 声音类型。 - /// 是否渐消。 - public void Stop(AudioType type, bool fadeout); - - /// - /// 停止所有声音。 - /// - /// 是否渐消。 - public void StopAll(bool fadeout); - - /// - /// 预先加载AudioClip,并放入对象池。 - /// - /// AudioClip的AssetPath集合。 - public void PutInAudioPool(List list); - - /// - /// 将部分AudioClip从对象池移出。 - /// - /// AudioClip的AssetPath集合。 - public void RemoveClipFromPool(List list); - - /// - /// 清空AudioClip的对象池。 - /// - 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 paths, bool pin = true); + void Unload(IList paths); + void ClearCache(); } } diff --git a/Runtime/Debugger/DebuggerComponent.AudioInformationWindow.cs b/Runtime/Debugger/DebuggerComponent.AudioInformationWindow.cs new file mode 100644 index 0000000..c99c27e --- /dev/null +++ b/Runtime/Debugger/DebuggerComponent.AudioInformationWindow.cs @@ -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(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 : "")); + card.Add(CreateRow("Instance Root", _serviceInfo.InstanceRoot != null ? _serviceInfo.InstanceRoot.name : "")); + 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) ? "" : _agentInfo.Address; + } + + private static string GetClipName(AudioClip clip) + { + return clip != null ? clip.name : ""; + } + } + } +} diff --git a/Runtime/Debugger/DebuggerComponent.AudioInformationWindow.cs.meta b/Runtime/Debugger/DebuggerComponent.AudioInformationWindow.cs.meta new file mode 100644 index 0000000..2c92175 --- /dev/null +++ b/Runtime/Debugger/DebuggerComponent.AudioInformationWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 611fc331e56d457abe1517f4312aaa44 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Debugger/DebuggerComponent.cs b/Runtime/Debugger/DebuggerComponent.cs index f1336f3..975d27f 100644 --- a/Runtime/Debugger/DebuggerComponent.cs +++ b/Runtime/Debugger/DebuggerComponent.cs @@ -92,6 +92,7 @@ namespace AlicizaX.Debugger.Runtime private RuntimeMemoryInformationWindow m_RuntimeMemoryScriptableObjectInformationWindow = new RuntimeMemoryInformationWindow(); 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); }