重构音频模块 1. 高频、大量音频反复调用时,单帧 CPU 开销与 GC 最优 2. AudioClip / AudioSource 的加载、缓存淘汰、卸载形成完整闭环,避免线性遍历 3. AudioSource 对象池 + 播放请求 struct 全部池化覆盖所有分配点 4. 支持3D环境音并具备距离衰减、遮挡等空间属性 5. 新增音频类型(BGM/SFX/Voice/Ambient) 6. 可调式监控Debug信息 及时跟踪音频缓存 处理 句柄状态
471 lines
14 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|