com.alicizax.unity.framework/Runtime/Audio/AudioAgent.cs
陈思海 46194ddee8 重构音频模块 1. 高频、大量音频反复调用时,单帧 CPU 开销与 GC 最优 2. AudioClip / AudioSource 的加载、缓存淘汰、卸载形成完整闭环,避免线性遍历 3. AudioSource 对象池 + 播放请求 struct 全部池化覆盖所有分配点 4. 支持3D环境音并具备距离衰减、遮挡等空间属性 5. 新增音频类型(BGM/SFX/Voice/Ambient) 6. 可调式监控Debug信息 及时跟踪音频缓存 处理 句柄状态
重构音频模块
1. 高频、大量音频反复调用时,单帧 CPU 开销与 GC 最优
2. AudioClip / AudioSource 的加载、缓存淘汰、卸载形成完整闭环,避免线性遍历
3. AudioSource 对象池 + 播放请求 struct 全部池化覆盖所有分配点
4. 支持3D环境音并具备距离衰减、遮挡等空间属性
5. 新增音频类型(BGM/SFX/Voice/Ambient)
6. 可调式监控Debug信息 及时跟踪音频缓存 处理 句柄状态
2026-04-23 17:21:36 +08:00

471 lines
14 KiB
C#

using UnityEngine;
namespace AlicizaX.Audio.Runtime
{
internal sealed class AudioAgent
{
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 AudioLowPassFilter _lowPassFilter;
private AudioClipCacheEntry _clipEntry;
private Transform _transform;
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;
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;
internal void Initialize(AudioService service, AudioCategory category, int index, int globalIndex, AudioSourceObject sourceObject)
{
_service = service;
_category = category;
Index = index;
GlobalIndex = globalIndex;
HeapIndex = -1;
ActiveIndex = -1;
BindSource(sourceObject);
ResetState();
}
internal ulong Play(AudioPlayRequest request)
{
StopImmediate(false);
_generation++;
if (_generation == int.MaxValue)
{
_generation = 1;
}
_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;
}
internal bool OnClipReady(AudioClipCacheEntry entry, int generation)
{
if (_state != AudioAgentRuntimeState.Loading || generation != _generation || entry == null || entry.Clip == null)
{
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);
}
}
internal void Stop(bool fadeout)
{
if (_state == AudioAgentRuntimeState.Free)
{
return;
}
if (!fadeout || _state == AudioAgentRuntimeState.Loading)
{
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;
_source.loop = _loop;
ApplyRuntimeVolume(1f);
_source.Play();
_state = AudioAgentRuntimeState.Playing;
}
private void StopImmediate(bool notifyCategory)
{
if (_state == AudioAgentRuntimeState.Free)
{
return;
}
_generation++;
if (_generation == int.MaxValue)
{
_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);
}
}
private void ReleaseClip()
{
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;
_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;
Transform transform = _source.transform;
if (_followTarget != null)
{
transform.SetParent(_category.InstanceRoot, false);
transform.position = _followTarget.position + _followOffset;
transform.rotation = _followTarget.rotation;
}
else
{
transform.SetParent(_category.InstanceRoot, false);
if (request.UseWorldPosition)
{
transform.position = request.Position;
}
else
{
transform.localPosition = Vector3.zero;
}
}
}
private static float ResolveSpatialBlend(AudioPlayRequest request, AudioGroupConfig config)
{
if (request.SpatialBlend >= 0f)
{
return Mathf.Clamp01(request.SpatialBlend);
}
return config.SpatialBlend;
}
private void UpdateFollowTarget()
{
if (_followTarget == null)
{
return;
}
if (_transform == null)
{
return;
}
if (!_followTarget.gameObject.activeInHierarchy)
{
StopImmediate(true);
return;
}
_transform.position = _followTarget.position + _followOffset;
_transform.rotation = _followTarget.rotation;
}
private void UpdateOcclusion()
{
AudioGroupConfig config = _category.Config;
if (!_spatial || !config.OcclusionEnabled || _transform == null)
{
return;
}
Transform listener = _service.ListenerTransform;
if (listener == null)
{
return;
}
float now = Time.realtimeSinceStartup;
if (now < _nextOcclusionCheckTime)
{
return;
}
_nextOcclusionCheckTime = now + config.OcclusionCheckInterval;
Vector3 origin = _transform.position;
Vector3 target = listener.position;
Vector3 direction = target - origin;
float distance = direction.magnitude;
if (distance <= 0.01f)
{
SetOccluded(false, config);
return;
}
bool occluded = Physics.Raycast(origin, direction / distance, distance, config.OcclusionMask, QueryTriggerInteraction.Ignore);
SetOccluded(occluded, config);
}
private void SetOccluded(bool occluded, AudioGroupConfig config)
{
if (_occluded == occluded)
{
return;
}
_occluded = occluded;
if (_lowPassFilter != null)
{
_lowPassFilter.enabled = occluded;
_lowPassFilter.cutoffFrequency = occluded ? config.OcclusionLowPassCutoff : MaxCutoffFrequency;
}
ApplyRuntimeVolume(_state == AudioAgentRuntimeState.FadingOut ? _fadeTimer / Mathf.Max(_fadeDuration, MinFadeOutSeconds) : 1f);
}
private void ApplyRuntimeVolume(float fadeScale)
{
if (_source == null)
{
return;
}
float occlusionScale = _occluded ? _category.Config.OcclusionVolumeMultiplier : 1f;
_source.volume = _baseVolume * occlusionScale * fadeScale;
}
}
}