重构音频模块 1. 高频、大量音频反复调用时,单帧 CPU 开销与 GC 最优 2. AudioClip / AudioSource 的加载、缓存淘汰、卸载形成完整闭环,避免线性遍历 3. AudioSource 对象池 + 播放请求 struct 全部池化覆盖所有分配点 4. 支持3D环境音并具备距离衰减、遮挡等空间属性 5. 新增音频类型(BGM/SFX/Voice/Ambient) 6. 可调式监控Debug信息 及时跟踪音频缓存 处理 句柄状态
1248 lines
39 KiB
C#
1248 lines
39 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
using AlicizaX.ObjectPool;
|
|
using AlicizaX.Resource.Runtime;
|
|
using UnityEngine;
|
|
using UnityEngine.Audio;
|
|
using YooAsset;
|
|
|
|
namespace AlicizaX.Audio.Runtime
|
|
{
|
|
internal sealed class AudioService : ServiceBase, IAudioService, IAudioDebugService, IServiceTickable
|
|
{
|
|
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<string, AudioClipCacheEntry> _clipCache = new Dictionary<string, AudioClipCacheEntry>(DefaultCacheCapacity, StringComparer.Ordinal);
|
|
private readonly AudioSourceObject[][] _sourceObjects = new AudioSourceObject[(int)AudioType.Max][];
|
|
|
|
private IResourceService _resourceService;
|
|
private IObjectPool<AudioSourceObject> _sourcePool;
|
|
private AudioMixer _audioMixer;
|
|
private Transform _instanceRoot;
|
|
private AudioGroupConfig[] _configs;
|
|
private AudioAgent[] _handleAgents = Array.Empty<AudioAgent>();
|
|
private uint[] _handleGenerations = Array.Empty<uint>();
|
|
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 bool _unityAudioDisabled;
|
|
private bool _initialized;
|
|
private bool _ownsInstanceRoot;
|
|
|
|
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 => _unityAudioDisabled ? 0f : _volume;
|
|
set
|
|
{
|
|
if (_unityAudioDisabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_volume = Mathf.Clamp01(value);
|
|
AudioListener.volume = _enable ? _volume : 0f;
|
|
}
|
|
}
|
|
|
|
public bool Enable
|
|
{
|
|
get => !_unityAudioDisabled && _enable;
|
|
set
|
|
{
|
|
if (_unityAudioDisabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_enable = value;
|
|
AudioListener.volume = _enable ? _volume : 0f;
|
|
}
|
|
}
|
|
|
|
internal float MusicVolume
|
|
{
|
|
get => GetCategoryVolume(AudioType.Music);
|
|
set => SetCategoryVolume(AudioType.Music, value);
|
|
}
|
|
|
|
internal float SoundVolume
|
|
{
|
|
get => GetCategoryVolume(AudioType.Sound);
|
|
set => SetCategoryVolume(AudioType.Sound, value);
|
|
}
|
|
|
|
internal float UISoundVolume
|
|
{
|
|
get => GetCategoryVolume(AudioType.UISound);
|
|
set => SetCategoryVolume(AudioType.UISound, value);
|
|
}
|
|
|
|
internal float VoiceVolume
|
|
{
|
|
get => GetCategoryVolume(AudioType.Voice);
|
|
set => SetCategoryVolume(AudioType.Voice, value);
|
|
}
|
|
|
|
internal float AmbientVolume
|
|
{
|
|
get => GetCategoryVolume(AudioType.Ambient);
|
|
set => SetCategoryVolume(AudioType.Ambient, value);
|
|
}
|
|
|
|
internal bool MusicEnable
|
|
{
|
|
get => GetCategoryEnable(AudioType.Music);
|
|
set => SetCategoryEnable(AudioType.Music, value);
|
|
}
|
|
|
|
internal bool SoundEnable
|
|
{
|
|
get => GetCategoryEnable(AudioType.Sound);
|
|
set => SetCategoryEnable(AudioType.Sound, value);
|
|
}
|
|
|
|
internal bool UISoundEnable
|
|
{
|
|
get => GetCategoryEnable(AudioType.UISound);
|
|
set => SetCategoryEnable(AudioType.UISound, value);
|
|
}
|
|
|
|
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()
|
|
{
|
|
Shutdown(true);
|
|
}
|
|
|
|
public void Initialize(AudioGroupConfig[] audioGroupConfigs, Transform instanceRoot = null, AudioMixer audioMixer = null)
|
|
{
|
|
Shutdown(false);
|
|
|
|
_resourceService = AppServices.Require<IResourceService>();
|
|
IObjectPoolService objectPoolService = AppServices.Require<IObjectPoolService>();
|
|
_sourcePool = objectPoolService.HasObjectPool<AudioSourceObject>(SourcePoolName)
|
|
? objectPoolService.GetObjectPool<AudioSourceObject>(SourcePoolName)
|
|
: objectPoolService.CreatePool<AudioSourceObject>(new ObjectPoolCreateOptions(SourcePoolName, false, 10f, int.MaxValue, float.MaxValue, 10));
|
|
|
|
if (audioGroupConfigs == null || audioGroupConfigs.Length == 0)
|
|
{
|
|
throw new GameFrameworkException("AudioGroupConfig[] is invalid.");
|
|
}
|
|
|
|
_configs = audioGroupConfigs;
|
|
if (instanceRoot != null)
|
|
{
|
|
_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>("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<AudioPlayRequest>(totalAgentCount);
|
|
MemoryPool.Add<AudioLoadRequest>(totalAgentCount);
|
|
MemoryPool.Add<AudioClipCacheEntry>(_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<AudioPlayRequest>();
|
|
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<AudioPlayRequest>();
|
|
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<AudioPlayRequest>();
|
|
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<AudioPlayRequest>();
|
|
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<AudioPlayRequest>();
|
|
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<AudioPlayRequest>();
|
|
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<AudioPlayRequest>();
|
|
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<AudioPlayRequest>();
|
|
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<AudioPlayRequest>();
|
|
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<AudioPlayRequest>();
|
|
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<string> 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<string> 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<AudioLoadRequest>();
|
|
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<AudioClipCacheEntry>();
|
|
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<AudioClip>(entry.Address);
|
|
entry.Handle.Completed += entry.CompletedCallback;
|
|
return;
|
|
}
|
|
|
|
AssetHandle handle = _resourceService.LoadAssetSyncHandle<AudioClip>(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<AudioSource>();
|
|
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<AudioLowPassFilter>();
|
|
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<AudioAgent>();
|
|
_handleGenerations = Array.Empty<uint>();
|
|
_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");
|
|
return propertyInfo != null && (bool)propertyInfo.GetValue(null);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Log.Error(exception.ToString());
|
|
}
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
}
|
|
}
|