959 lines
34 KiB
C#
959 lines
34 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using Cysharp.Threading.Tasks;
|
||
using Framework;
|
||
using PhxhSDK;
|
||
using UnityEngine;
|
||
using UnityEngine.SceneManagement;
|
||
using Constants = Framework.Constants;
|
||
using Object = UnityEngine.Object;
|
||
|
||
/// <summary>
|
||
/// 音频管理器 - 统一的音频系统管理
|
||
/// <para>职责:</para>
|
||
/// <para>- 协调播放池和停止池</para>
|
||
/// <para>- 音频播放控制</para>
|
||
/// <para>- 配置表查询和资源加载</para>
|
||
/// <para>- 音量管理(委托给 AudioVolumeSettings)</para>
|
||
/// <para>- 业务状态管理(Album、BGM 等)</para>
|
||
/// </summary>
|
||
public class AudioManager: Singlenton<AudioManager>, IStart, IInitable, IUpdatable
|
||
{
|
||
#region Fields & Properties
|
||
|
||
// --- Unity Components ---
|
||
private GameObject _audioRoot;
|
||
private AudioListener _globalListener;
|
||
|
||
/// <summary>
|
||
/// 音频根节点(供框架内部使用,如 StoryAudioManager 设置父节点)
|
||
/// </summary>
|
||
public GameObject AudioRoot => _audioRoot;
|
||
|
||
// --- Business State ---
|
||
public static bool DisableAudio { get; set; } = false;
|
||
|
||
// --- Audio Pools ---
|
||
private AudioPlayingPool _playingPool; // 固定大小,LRU 复用
|
||
private AudioStoppedPool _stoppedPool; // LRU 缓存
|
||
private AudioPoolConfig _poolConfig; // 池容量配置
|
||
|
||
// --- Anti-Replay ---
|
||
private Dictionary<string, long> _playSoundTimeStampDic; // Sound 防重播时间戳
|
||
|
||
// --- External Players ---
|
||
private readonly Dictionary<int, AudioPlayer> _externalAudios = new();
|
||
private readonly List<AudioPlayer> _externalTempList = new(); // 复用列表,避免 GC
|
||
|
||
#endregion
|
||
|
||
#region Volume Control
|
||
|
||
/// <summary>
|
||
/// 音量设置(委托给 AudioVolumeSettings)
|
||
/// </summary>
|
||
public AudioVolumeSettings VolumeSettings => AudioVolumeSettings.Instance;
|
||
|
||
#endregion
|
||
|
||
#region Lifecycle
|
||
|
||
/// <summary>
|
||
/// 初始化数据结构和逻辑(IInitable 接口)
|
||
/// <para>职责:创建对象池、注册事件、应用默认配置</para>
|
||
/// <para>不依赖 Unity GameObject,可独立测试</para>
|
||
/// </summary>
|
||
public void Init()
|
||
{
|
||
// 使用默认配置
|
||
_poolConfig = AudioPoolConfig.Default;
|
||
|
||
// 初始化播放池
|
||
_playingPool = new AudioPlayingPool();
|
||
_playingPool.SetLimit(PhxhSDK.EAudioType.Music, _poolConfig.MusicPlayingLimit);
|
||
_playingPool.SetLimit(PhxhSDK.EAudioType.Sound, _poolConfig.SoundPlayingLimit);
|
||
_playingPool.SetLimit(PhxhSDK.EAudioType.Speak, _poolConfig.SpeakPlayingLimit);
|
||
|
||
// 订阅播放池的停止回调(自动将已停止的音频移到停止池)
|
||
_playingPool.OnAudioStopped = OnAudioStoppedInPlayingPool;
|
||
|
||
// 初始化停止池
|
||
_stoppedPool = new AudioStoppedPool();
|
||
_stoppedPool.SetLimit(PhxhSDK.EAudioType.Music, _poolConfig.MusicStoppedLimit);
|
||
_stoppedPool.SetLimit(PhxhSDK.EAudioType.Sound, _poolConfig.SoundStoppedLimit);
|
||
_stoppedPool.SetLimit(PhxhSDK.EAudioType.Speak, _poolConfig.SpeakStoppedLimit);
|
||
|
||
// 初始化停止池超时时间
|
||
_stoppedPool.SetTimeout(PhxhSDK.EAudioType.Music, _poolConfig.MusicStoppedTimeout);
|
||
_stoppedPool.SetTimeout(PhxhSDK.EAudioType.Sound, _poolConfig.SoundStoppedTimeout);
|
||
_stoppedPool.SetTimeout(PhxhSDK.EAudioType.Speak, _poolConfig.SpeakStoppedTimeout);
|
||
|
||
// 初始化其他
|
||
_playSoundTimeStampDic = new();
|
||
|
||
// 注册事件
|
||
EventManager.Instance.Register(EventManager.EventName.Audio_ErrorAudioPlay, _OnErrorAudioPlay);
|
||
|
||
// 订阅场景加载事件(自动清理场景中的 AudioListener)
|
||
SceneManager.sceneLoaded += OnSceneLoaded;
|
||
}
|
||
|
||
public void Release()
|
||
{
|
||
EventManager.Instance.Unregister(EventManager.EventName.Audio_ErrorAudioPlay, _OnErrorAudioPlay);
|
||
|
||
// 取消订阅场景加载事件
|
||
SceneManager.sceneLoaded -= OnSceneLoaded;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化 Unity 相关对象(IStart 接口)
|
||
/// <para>职责:创建 AudioRoot GameObject 和 AudioListener</para>
|
||
/// <para>依赖 Unity Runtime,幂等操作</para>
|
||
/// </summary>
|
||
public void Start()
|
||
{
|
||
if (!_audioRoot)
|
||
{
|
||
_audioRoot = new GameObject("Audio(Singleton)");
|
||
Object.DontDestroyOnLoad(_audioRoot);
|
||
_globalListener = _audioRoot.AddComponent<AudioListener>();
|
||
_audioRoot.transform.position = Vector3.zero;
|
||
|
||
// 初始清理(处理启动场景中的 AudioListener)
|
||
RemoveRedundantAudioListeners().Forget();
|
||
}
|
||
}
|
||
|
||
public void Update(float dt)
|
||
{
|
||
_playingPool.Update(dt);
|
||
UpdateExternalPlayers(dt);
|
||
|
||
// 更新停止池,释放超时的音频
|
||
var expiredAudios = _stoppedPool.Update(dt);
|
||
if (expiredAudios != null)
|
||
{
|
||
foreach (var audio in expiredAudios)
|
||
{
|
||
audio.Dispose();
|
||
|
||
if (DebugUtil.LogEnable)
|
||
{
|
||
DebugUtil.Log($"[AudioManager] ⏰ 停止池音频超时释放: Key={audio.CurrentPlayingKey}, ID={audio.AudioId}, Type={audio.AudioType}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private void UpdateExternalPlayers(float dt)
|
||
{
|
||
if (_externalAudios.Count == 0)
|
||
return;
|
||
|
||
_externalTempList.Clear();
|
||
_externalTempList.AddRange(_externalAudios.Values);
|
||
|
||
foreach (var audio in _externalTempList)
|
||
{
|
||
audio?.Update(dt);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Config & Asset
|
||
|
||
/// <summary>
|
||
/// 获取音频配置(统一入口,避免重复查询)
|
||
/// </summary>
|
||
private cfg.AudioCfg.DataAudioSource _GetConfig(string key)
|
||
{
|
||
var cfg = TableManager.Instance.Tables.AudioSourceConfig.GetOrDefault(key);
|
||
if (cfg == null)
|
||
{
|
||
DebugUtil.LogError($"[AudioManager] 音频配置不存在: {key}");
|
||
}
|
||
return cfg;
|
||
}
|
||
|
||
private bool _CheckPath(string key)
|
||
{
|
||
return _GetConfig(key) != null;
|
||
}
|
||
|
||
private string GetFullPath(string key, bool isI18N = false)
|
||
{
|
||
var cfg = _GetConfig(key);
|
||
if (cfg == null)
|
||
return "";
|
||
|
||
var path = cfg.Path;
|
||
if (isI18N && path.Contains(LanguageManager.I18N))
|
||
{
|
||
var eLanguage = LanguageManager.Instance.GetCurrentLanguage();
|
||
var replacePath = LanguageManager.PathDic[eLanguage];
|
||
path = path.Replace(LanguageManager.I18N, replacePath);
|
||
}
|
||
|
||
return path;
|
||
}
|
||
|
||
private float _GetVolumeScale(string key)
|
||
{
|
||
var cfg = _GetConfig(key);
|
||
if (cfg == null)
|
||
return 0f;
|
||
|
||
return cfg.VolumeScale;
|
||
}
|
||
|
||
private bool _ShouldSkipPlay(PhxhSDK.EAudioType type)
|
||
{
|
||
var settings = AudioVolumeSettings.Instance;
|
||
if (settings.GlobalVolume <= 0)
|
||
return true;
|
||
|
||
return type switch
|
||
{
|
||
PhxhSDK.EAudioType.Sound => settings.SoundVolume <= 0,
|
||
PhxhSDK.EAudioType.Speak => settings.SpeakVolume <= 0,
|
||
_ => false
|
||
};
|
||
}
|
||
|
||
public async UniTask<float> GetClipLength(string key)
|
||
{
|
||
var path = GetFullPath(key);
|
||
var clip = await AssetManager.Instance.LoadAssetAsync<AudioClip>(path);
|
||
return clip ? clip.length : -1;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Pool Management
|
||
|
||
/// <summary>
|
||
/// 获取可用的 AudioPlayer
|
||
/// <para>策略:</para>
|
||
/// <para>- 播放池未满 → 从停止池精确复用或创建新的</para>
|
||
/// <para>- 播放池已满 → 优先从停止池精确复用,否则复用播放池最旧的</para>
|
||
/// </summary>
|
||
private AudioPlayer _GetUsableAudio(PhxhSDK.EAudioType eAudioType, string key, bool isLoop)
|
||
{
|
||
try
|
||
{
|
||
AudioPlayer audio = null;
|
||
bool fromStoppedPool = false;
|
||
bool poolWasFull = false;
|
||
string evictedKey = null;
|
||
|
||
// 尝试从停止池获取(精确匹配 key)
|
||
audio = _stoppedPool.TryGetCached(eAudioType, key);
|
||
|
||
if (audio != null)
|
||
{
|
||
// 从停止池移除
|
||
_stoppedPool.RemoveById(audio.AudioId);
|
||
fromStoppedPool = true;
|
||
|
||
// 启用 GameObject(从停止池复用)
|
||
audio.SetActive(true);
|
||
}
|
||
else
|
||
{
|
||
// 创建新的(默认已启用)
|
||
audio = _CreateNewAudio(eAudioType, isLoop);
|
||
}
|
||
|
||
if (_playingPool.IsFull(eAudioType))
|
||
{
|
||
poolWasFull = true;
|
||
// 播放池已满,需要腾出位置:停止最旧的并移到停止池
|
||
var oldest = _playingPool.GetOldest(eAudioType);
|
||
if (oldest != null)
|
||
{
|
||
evictedKey = oldest.CurrentPlayingKey;
|
||
oldest.Stop();
|
||
_playingPool.Remove(oldest.AudioId);
|
||
|
||
if (!string.IsNullOrEmpty(oldest.CurrentPlayingKey))
|
||
{
|
||
var evicted = _stoppedPool.AddStopped(oldest.AudioType, oldest.CurrentPlayingKey, oldest);
|
||
DisposeEvictedAudios(evicted);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 加入播放池
|
||
_playingPool.Add(audio);
|
||
|
||
// 统一日志(放在外面,显示获取音频的完整信息)
|
||
if (DebugUtil.LogEnable)
|
||
{
|
||
var (playingMusic, playingSound, playingSpeak) = _playingPool.GetStats();
|
||
var (stoppedMusic, stoppedSound, stoppedSpeak) = _stoppedPool.GetStats();
|
||
|
||
if (poolWasFull)
|
||
{
|
||
DebugUtil.Log($"[AudioManager] 🔄 获取音频(池已满): Key={key}, ID={audio.AudioId}, Type={audio.AudioType}, FromStopped={fromStoppedPool}, EvictedKey={evictedKey} | " +
|
||
$"播放池(M:{playingMusic} S:{playingSound} Sp:{playingSpeak}) | " +
|
||
$"停止池(M:{stoppedMusic} S:{stoppedSound} Sp:{stoppedSpeak})");
|
||
}
|
||
else
|
||
{
|
||
DebugUtil.Log($"[AudioManager] ➕ 获取音频(池未满): Key={key}, ID={audio.AudioId}, Type={audio.AudioType}, FromStopped={fromStoppedPool} | " +
|
||
$"播放池(M:{playingMusic} S:{playingSound} Sp:{playingSpeak}) | " +
|
||
$"停止池(M:{stoppedMusic} S:{stoppedSound} Sp:{stoppedSpeak})");
|
||
}
|
||
}
|
||
|
||
return audio;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
DebugUtil.LogError($"[AudioManager] GetUsableAudio failed: {e}");
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private AudioPlayer _CreateNewAudio(PhxhSDK.EAudioType eAudioType, bool isLoop)
|
||
{
|
||
var rootGo = new GameObject("Audio");
|
||
rootGo.transform.SetParent(_audioRoot.transform);
|
||
|
||
var audio = new AudioPlayer(eAudioType, rootGo);
|
||
|
||
// 设置 GameObject 名称
|
||
string typeName = eAudioType switch
|
||
{
|
||
PhxhSDK.EAudioType.Music => "Music",
|
||
PhxhSDK.EAudioType.Sound => "Sound",
|
||
PhxhSDK.EAudioType.Speak => "Speak",
|
||
_ => "Audio"
|
||
};
|
||
rootGo.name = $"{typeName}{audio.AudioId}{(isLoop ? "Loop" : "")}";
|
||
|
||
return audio;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 播放池音频停止回调处理
|
||
/// <para>当音频在播放池中自然停止(播放完成)时触发</para>
|
||
/// <para>职责:从播放池移除 → 加入停止池(用于复用)</para>
|
||
/// </summary>
|
||
private void OnAudioStoppedInPlayingPool(AudioPlayer audio)
|
||
{
|
||
if (audio == null)
|
||
return;
|
||
|
||
// 从播放池移除
|
||
_playingPool.Remove(audio.AudioId);
|
||
|
||
// 加入停止池(用于后续复用)
|
||
if (!string.IsNullOrEmpty(audio.CurrentPlayingKey))
|
||
{
|
||
var evictedAudios = _stoppedPool.AddStopped(audio.AudioType, audio.CurrentPlayingKey, audio);
|
||
DisposeEvictedAudios(evictedAudios);
|
||
|
||
// 停止日志
|
||
if (DebugUtil.LogEnable)
|
||
{
|
||
var (playingMusic, playingSound, playingSpeak) = _playingPool.GetStats();
|
||
var (stoppedMusic, stoppedSound, stoppedSpeak) = _stoppedPool.GetStats();
|
||
DebugUtil.Log($"[AudioManager] ⏹️ 音频自然停止并移入停止池: Key={audio.CurrentPlayingKey}, ID={audio.AudioId}, Type={audio.AudioType} | " +
|
||
$"播放池(M:{playingMusic} S:{playingSound} Sp:{playingSpeak}) | " +
|
||
$"停止池(M:{stoppedMusic} S:{stoppedSound} Sp:{stoppedSpeak})");
|
||
}
|
||
}
|
||
}
|
||
|
||
private void DisposeEvictedAudios(List<AudioPlayer> evictedAudios)
|
||
{
|
||
foreach (var audio in evictedAudios)
|
||
{
|
||
audio.Dispose();
|
||
if (DebugUtil.LogEnable)
|
||
DebugUtil.Log($"[LRU] Evicted: ID={audio.AudioId}, Type={audio.AudioType}");
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Play Core
|
||
|
||
private int _PlayAudioCore(string key, PhxhSDK.EAudioType audioType, bool isLoop,
|
||
int priority, bool isFade, float fadeTime, float volume,
|
||
GameObject followTarget, Vector3? staticPosition)
|
||
{
|
||
|
||
// 音量检查
|
||
if (_ShouldSkipPlay(audioType))
|
||
return -1;
|
||
|
||
// 防重播策略(根据音频类型)
|
||
switch (audioType)
|
||
{
|
||
case PhxhSDK.EAudioType.Music:
|
||
case PhxhSDK.EAudioType.Speak:
|
||
// Music/Speak: 如果相同 key 的音频已在播放,不重新播放
|
||
var existingAudio = _playingPool.FindPlayingByKey(audioType, key);
|
||
if (existingAudio != null)
|
||
{
|
||
if (DebugUtil.LogEnable)
|
||
{
|
||
DebugUtil.Log($"[AudioManager] ⏭️ {audioType} 已在播放,跳过重复播放: Key={key}, ID={existingAudio.AudioId}");
|
||
}
|
||
return existingAudio.AudioId;
|
||
}
|
||
break;
|
||
|
||
case PhxhSDK.EAudioType.Sound:
|
||
// Sound: 时间戳防重播(低优先级 100ms 内防重)
|
||
if (priority >= AudioConstant.LowPriority)
|
||
{
|
||
var curTimeStamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||
if (_playSoundTimeStampDic.TryGetValue(key, out var timeStamp))
|
||
{
|
||
if (curTimeStamp - timeStamp < 100)
|
||
return -1;
|
||
_playSoundTimeStampDic[key] = curTimeStamp;
|
||
}
|
||
else
|
||
{
|
||
_playSoundTimeStampDic.TryAdd(key, curTimeStamp);
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
|
||
// Speak 强制不循环
|
||
if (audioType == PhxhSDK.EAudioType.Speak)
|
||
isLoop = false;
|
||
|
||
// 获取配置(只查询一次)
|
||
var config = _GetConfig(key);
|
||
if (config == null)
|
||
{
|
||
DebugUtil.LogError($"[AudioManager] 音频配置不存在,无法播放: key={key}");
|
||
return -1;
|
||
}
|
||
|
||
// 获取路径(Speak 使用 I18N)
|
||
var path = GetFullPath(key, isI18N: audioType == PhxhSDK.EAudioType.Speak);
|
||
|
||
// 获取可用的 AudioPlayer
|
||
var audio = _GetUsableAudio(audioType, key, isLoop);
|
||
if (audio == null)
|
||
{
|
||
DebugUtil.LogError($"[AudioManager] GetUsableAudio failed: key={key}, type={audioType}");
|
||
return -1;
|
||
}
|
||
|
||
// 配置并播放
|
||
return _ConfigureAndPlay(audio, path, key, config.VolumeScale, isLoop, isFade, fadeTime, volume,
|
||
followTarget, staticPosition);
|
||
}
|
||
|
||
private int _ConfigureAndPlay(AudioPlayer audio, string path, string key, float volumeScale,
|
||
bool isLoop, bool isFade, float fadeTime, float volume, GameObject followTarget, Vector3? staticPosition)
|
||
{
|
||
audio.CurrentPlayingKey = key;
|
||
audio.SetVolumeScale(volumeScale);
|
||
audio.Loop = isLoop;
|
||
|
||
// 3D 音效设置
|
||
if (followTarget != null)
|
||
{
|
||
audio.SetFollowTarget(followTarget);
|
||
audio.SetIs3D(true);
|
||
}
|
||
else if (staticPosition.HasValue)
|
||
{
|
||
audio.SetPosition(staticPosition.Value);
|
||
audio.SetIs3D(true);
|
||
}
|
||
else
|
||
{
|
||
audio.SetIs3D(false);
|
||
}
|
||
|
||
// 播放
|
||
if (isFade)
|
||
{
|
||
audio.Volume = 0;
|
||
audio.LoadAndPlayWithFade(path, fadeTime, fadeTime);
|
||
}
|
||
else
|
||
{
|
||
audio.Volume = volume * volumeScale;
|
||
audio.LoadAndPlay(path);
|
||
}
|
||
|
||
// 播放日志
|
||
if (DebugUtil.LogEnable)
|
||
{
|
||
var (playingMusic, playingSound, playingSpeak) = _playingPool.GetStats();
|
||
var (stoppedMusic, stoppedSound, stoppedSpeak) = _stoppedPool.GetStats();
|
||
DebugUtil.Log($"[AudioManager] ▶️ 播放音频: Key={key}, ID={audio.AudioId}, Type={audio.AudioType} | " +
|
||
$"播放池(M:{playingMusic} S:{playingSound} Sp:{playingSpeak}) | " +
|
||
$"停止池(M:{stoppedMusic} S:{stoppedSound} Sp:{stoppedSpeak})");
|
||
}
|
||
|
||
return audio.AudioId;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Public API - Play
|
||
|
||
/// <summary>
|
||
/// 播放音频(统一入口)
|
||
/// </summary>
|
||
/// <param name="key">音频配置 Key</param>
|
||
/// <param name="type">音频类型</param>
|
||
/// <param name="posObj">跟随对象(3D 音效)</param>
|
||
/// <param name="pos">静态位置(3D 音效)</param>
|
||
/// <param name="loop">是否循环</param>
|
||
/// <param name="priority">优先级(Sound 防重播)</param>
|
||
/// <param name="isFade">是否淡入淡出</param>
|
||
/// <returns>音频 ID,失败返回 -1</returns>
|
||
public int PlayAudio(string key, PhxhSDK.EAudioType type = PhxhSDK.EAudioType.Sound,
|
||
GameObject posObj = null, Vector3? pos = null, bool loop = false,
|
||
int priority = AudioConstant.DefaultPriority, bool isFade = false)
|
||
{
|
||
|
||
if (DisableAudio)
|
||
return -1;
|
||
|
||
// if (key != "SE_ui_common_click")
|
||
// {
|
||
// return -1;
|
||
// }
|
||
|
||
if (!_CheckPath(key))
|
||
{
|
||
if (DebugUtil.LogEnable)
|
||
DebugUtil.LogG($"[AudioManager] 音频配置不存在: {key}");
|
||
return -1;
|
||
}
|
||
|
||
// 错误音频事件
|
||
if (key == Constants.Sound.ERROR_UI || key == Constants.Sound.ERROR_UI_FIGHT)
|
||
EventManager.Instance.Send(EventManager.EventName.Audio_ErrorAudioPlay);
|
||
|
||
return _PlayAudioCore(key, type, loop, priority, isFade, fadeTime: 0f, volume: 1f,
|
||
followTarget: posObj, staticPosition: pos);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据音频 ID 播放新的音频配置
|
||
/// </summary>
|
||
/// <param name="audioID">音频 ID</param>
|
||
/// <param name="key">新的音频配置 Key</param>
|
||
/// <param name="isFade">是否使用淡入淡出</param>
|
||
/// <param name="fadeTime">淡入淡出时间(秒)</param>
|
||
/// <param name="targetVolume">目标音量(可选,不设置则使用配置表中的音量)</param>
|
||
public void PlayByKey(int audioID, string key, bool isFade = false, float fadeTime = 4f, float? targetVolume = null)
|
||
{
|
||
var audio = GetAudioByID(audioID);
|
||
if (audio == null)
|
||
return;
|
||
|
||
audio.CurrentPlayingKey = key;
|
||
var fullPath = GetFullPath(key);
|
||
|
||
// 设置目标音量(如果指定)
|
||
if (targetVolume.HasValue)
|
||
audio.Volume = targetVolume.Value;
|
||
|
||
// 播放
|
||
if (isFade)
|
||
audio.LoadAndPlayWithFade(fullPath, fadeTime);
|
||
else
|
||
audio.LoadAndPlay(fullPath);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Public API - Control
|
||
|
||
/// <summary>
|
||
/// 应用音频池容量配置
|
||
/// <para>用于运行时动态调整播放池和停止池的容量上限</para>
|
||
/// <para>建议在 Init() 后、首次播放前调用</para>
|
||
/// </summary>
|
||
/// <param name="config">池容量配置</param>
|
||
public void ApplyPoolConfig(AudioPoolConfig config)
|
||
{
|
||
// 应用各类型的限制
|
||
ApplyLimitForType(PhxhSDK.EAudioType.Music, config.MusicPlayingLimit, config.MusicStoppedLimit, config.MusicStoppedTimeout);
|
||
ApplyLimitForType(PhxhSDK.EAudioType.Sound, config.SoundPlayingLimit, config.SoundStoppedLimit,config.SoundStoppedTimeout);
|
||
ApplyLimitForType(PhxhSDK.EAudioType.Speak, config.SpeakPlayingLimit, config.SpeakStoppedLimit,config.SpeakStoppedTimeout);
|
||
|
||
|
||
// 更新配置
|
||
_poolConfig = config;
|
||
|
||
// 日志
|
||
if (DebugUtil.LogEnable)
|
||
{
|
||
DebugUtil.Log($"[AudioManager] 📊 音频池配置已应用:\n" +
|
||
$" Music: 播放池={_poolConfig.MusicPlayingLimit}, 停止池={_poolConfig.MusicStoppedLimit}, 超时={_poolConfig.MusicStoppedTimeout}s\n" +
|
||
$" Sound: 播放池={_poolConfig.SoundPlayingLimit}, 停止池={_poolConfig.SoundStoppedLimit}, 超时={_poolConfig.SoundStoppedTimeout}s\n" +
|
||
$" Speak: 播放池={_poolConfig.SpeakPlayingLimit}, 停止池={_poolConfig.SpeakStoppedLimit}, 超时={_poolConfig.SpeakStoppedTimeout}s");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 应用指定类型的播放池和停止池限制
|
||
/// </summary>
|
||
private void ApplyLimitForType(PhxhSDK.EAudioType type, int playingLimit, int stoppedLimit, float stoppedTimeout)
|
||
{
|
||
// 验证并应用播放池限制
|
||
if (playingLimit > 0)
|
||
{
|
||
_playingPool.SetLimit(type, playingLimit);
|
||
|
||
|
||
// 更新配置
|
||
switch (type)
|
||
{
|
||
case PhxhSDK.EAudioType.Music:
|
||
_poolConfig.MusicPlayingLimit = playingLimit;
|
||
break;
|
||
case PhxhSDK.EAudioType.Sound:
|
||
_poolConfig.SoundPlayingLimit = playingLimit;
|
||
break;
|
||
case PhxhSDK.EAudioType.Speak:
|
||
_poolConfig.SpeakPlayingLimit = playingLimit;
|
||
break;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
DebugUtil.LogWarning($"[AudioManager] Invalid {type}PlayingLimit ({playingLimit}), must be > 0");
|
||
}
|
||
|
||
// 验证并应用停止池限制
|
||
if (stoppedLimit >= 0)
|
||
{
|
||
_stoppedPool.SetLimit(type, stoppedLimit);
|
||
_stoppedPool.SetTimeout(type, stoppedTimeout);
|
||
|
||
// 更新配置
|
||
switch (type)
|
||
{
|
||
case PhxhSDK.EAudioType.Music:
|
||
_poolConfig.MusicStoppedLimit = stoppedLimit;
|
||
break;
|
||
case PhxhSDK.EAudioType.Sound:
|
||
_poolConfig.SoundStoppedLimit = stoppedLimit;
|
||
break;
|
||
case PhxhSDK.EAudioType.Speak:
|
||
_poolConfig.SpeakStoppedLimit = stoppedLimit;
|
||
break;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
DebugUtil.LogWarning($"[AudioManager] Invalid {type}StoppedLimit ({stoppedLimit}), must be >= 0");
|
||
}
|
||
}
|
||
|
||
public AudioPlayer GetAudioByID(int id)
|
||
{
|
||
var audio = _playingPool.GetById(id);
|
||
if (audio != null)
|
||
return audio;
|
||
|
||
audio = _stoppedPool.GetById(id);
|
||
if (audio != null)
|
||
return audio;
|
||
|
||
_externalAudios.TryGetValue(id, out audio);
|
||
return audio;
|
||
}
|
||
|
||
#region External Players
|
||
|
||
/// <summary>
|
||
/// 注册外部创建的 AudioPlayer(例如 Story 系统自建播放器)
|
||
/// </summary>
|
||
public void RegisterExternalAudioPlayer(AudioPlayer player)
|
||
{
|
||
if (player == null)
|
||
return;
|
||
|
||
_externalAudios[player.AudioId] = player;
|
||
|
||
if (DebugUtil.LogEnable)
|
||
DebugUtil.Log($"[AudioManager] 📥 注册外部音频: ID={player.AudioId}, Type={player.AudioType}");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 注销外部 AudioPlayer,避免持有无效引用
|
||
/// </summary>
|
||
public void UnregisterExternalAudioPlayer(int audioId)
|
||
{
|
||
if (_externalAudios.Remove(audioId) && DebugUtil.LogEnable)
|
||
DebugUtil.Log($"[AudioManager] 📤 注销外部音频: ID={audioId}");
|
||
}
|
||
|
||
#endregion
|
||
|
||
public void StopByID(int audioID)
|
||
{
|
||
if (audioID >= 0)
|
||
{
|
||
var audio = GetAudioByID(audioID);
|
||
if (audio != null)
|
||
{
|
||
audio.Stop();
|
||
|
||
// 停止日志
|
||
if (DebugUtil.LogEnable)
|
||
{
|
||
var (playingMusic, playingSound, playingSpeak) = _playingPool.GetStats();
|
||
var (stoppedMusic, stoppedSound, stoppedSpeak) = _stoppedPool.GetStats();
|
||
DebugUtil.Log($"[AudioManager] ⏹️ 停止音频: Key={audio.CurrentPlayingKey}, ID={audioID}, Type={audio.AudioType} | " +
|
||
$"播放池(M:{playingMusic} S:{playingSound} Sp:{playingSpeak}) | " +
|
||
$"停止池(M:{stoppedMusic} S:{stoppedSound} Sp:{stoppedSpeak})");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
public void StopWithFadeByID(int id, float fadeOutTime = 1f)
|
||
{
|
||
if (id >= 0)
|
||
{
|
||
var audio = GetAudioByID(id);
|
||
if (audio != null)
|
||
{
|
||
audio.StopWithFadeOut(fadeOutTime);
|
||
|
||
// 淡出停止日志
|
||
if (DebugUtil.LogEnable)
|
||
{
|
||
var (playingMusic, playingSound, playingSpeak) = _playingPool.GetStats();
|
||
var (stoppedMusic, stoppedSound, stoppedSpeak) = _stoppedPool.GetStats();
|
||
DebugUtil.Log($"[AudioManager] ⏹️ 淡出停止音频: Key={audio.CurrentPlayingKey}, ID={id}, Type={audio.AudioType}, FadeTime={fadeOutTime}s | " +
|
||
$"播放池(M:{playingMusic} S:{playingSound} Sp:{playingSpeak}) | " +
|
||
$"停止池(M:{stoppedMusic} S:{stoppedSound} Sp:{stoppedSpeak})");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 停止指定类型的所有音频
|
||
/// </summary>
|
||
/// <param name="type">音频类型</param>
|
||
public void StopAllOfType(PhxhSDK.EAudioType type)
|
||
{
|
||
DebugUtil.LogG($"[AudioManager] StopAllOfType: {type}");
|
||
_playingPool.StopAllOfType(type);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 暂停指定类型的所有音频
|
||
/// </summary>
|
||
/// <param name="type">音频类型</param>
|
||
public void PauseAllOfType(PhxhSDK.EAudioType type)
|
||
{
|
||
DebugUtil.LogG($"[AudioManager] PauseAllOfType: {type}");
|
||
_playingPool.PauseAllOfType(type);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 恢复指定类型的所有音频
|
||
/// </summary>
|
||
/// <param name="type">音频类型</param>
|
||
public void ResumeAllOfType(PhxhSDK.EAudioType type)
|
||
{
|
||
DebugUtil.LogG($"[AudioManager] ResumeAllOfType: {type}");
|
||
_playingPool.ResumeAllOfType(type);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 停止所有音频
|
||
/// </summary>
|
||
public void StopAll()
|
||
{
|
||
DebugUtil.LogG("[AudioManager] StopAll");
|
||
_playingPool.StopAll();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 释放所有音频资源(清空池并销毁所有 AudioPlayer)
|
||
/// <para>自动停止所有播放中的音频,适用于场景卸载、关卡退出等场景</para>
|
||
/// </summary>
|
||
public void ReleaseAllAudios()
|
||
{
|
||
// 先停止所有播放
|
||
_playingPool.StopAll();
|
||
|
||
// 释放播放池和停止池
|
||
int playingCount = _playingPool.DisposeAll();
|
||
int stoppedCount = _stoppedPool.DisposeAll();
|
||
int externalCount = 0;
|
||
foreach (var external in _externalAudios.Values)
|
||
{
|
||
external.Dispose();
|
||
externalCount++;
|
||
}
|
||
_externalAudios.Clear();
|
||
|
||
if (DebugUtil.LogEnable)
|
||
DebugUtil.Log($"[AudioManager] Released {playingCount + stoppedCount + externalCount} audios (Playing: {playingCount}, Stopped: {stoppedCount}, External: {externalCount})");
|
||
|
||
// 释放静态资源
|
||
AudioPlayer.ReleaseLoadedAssets();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Public API - Spectrum
|
||
|
||
/// <summary>
|
||
/// 获取音频频谱数据(归一化处理)
|
||
/// </summary>
|
||
public void GetSpectrumData(int id, float[] spectrumsShow, float[] spectrumsData)
|
||
{
|
||
var audio = GetAudioByID(id);
|
||
if (audio == null)
|
||
{
|
||
Array.Clear(spectrumsShow, 0, spectrumsShow.Length);
|
||
return;
|
||
}
|
||
|
||
audio.GetSpectrumData(spectrumsData);
|
||
int n = spectrumsData.Length / spectrumsShow.Length;
|
||
for (int i = 0; i < spectrumsShow.Length; i++)
|
||
{
|
||
float value = 0f;
|
||
for (int j = 0; j < n; j++)
|
||
value += spectrumsData[i * n + j];
|
||
|
||
spectrumsShow[i] = Mathf.Log10(Mathf.Log(i + 2f, 2f) * value * (i * i + 1) * 9 + 1);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Listener
|
||
|
||
/// <summary>
|
||
/// 场景加载完成后的处理
|
||
/// <para>自动移除新场景中的冗余 AudioListener,确保全局只有一个</para>
|
||
/// </summary>
|
||
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
||
{
|
||
RemoveRedundantAudioListeners().Forget();
|
||
|
||
if (DebugUtil.LogEnable)
|
||
DebugUtil.Log($"[AudioManager] 场景加载完成: {scene.name},已清理冗余 AudioListener");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 移除场景中所有冗余的 AudioListener(保留 AudioManager 管理的全局 AudioListener)
|
||
/// <para>确保全局只有一个 AudioListener,避免 Unity 警告和音频问题</para>
|
||
/// <para>等待一帧后执行,确保场景完全加载</para>
|
||
/// </summary>
|
||
private async UniTask RemoveRedundantAudioListeners()
|
||
{
|
||
// 等待一帧,确保场景完全加载
|
||
await UniTask.NextFrame();
|
||
|
||
//雪松场景:用 Object.FindObjectsOfType 和 Camera.main 查找 AudioListener都有问题
|
||
|
||
// 获取活跃场景
|
||
var activeScene = SceneManager.GetActiveScene();
|
||
if (!activeScene.isLoaded)
|
||
return;
|
||
|
||
// 从活跃场景的根对象中查找 AudioListener
|
||
var rootObjects = activeScene.GetRootGameObjects();
|
||
int removedCount = 0;
|
||
|
||
foreach (var rootObject in rootObjects)
|
||
{
|
||
// 查找该根对象及其子对象中的所有 AudioListener
|
||
var listeners = rootObject.GetComponentsInChildren<AudioListener>(true);
|
||
|
||
foreach (var listener in listeners)
|
||
{
|
||
// 保留 AudioManager 管理的全局 AudioListener
|
||
if (listener == _globalListener)
|
||
continue;
|
||
|
||
if (DebugUtil.LogEnable)
|
||
DebugUtil.LogWarning($"[AudioManager] 移除冗余 AudioListener: {listener.gameObject.name} (Scene: {activeScene.name})");
|
||
|
||
Object.Destroy(listener);
|
||
removedCount++;
|
||
}
|
||
}
|
||
|
||
if (removedCount > 0 && DebugUtil.LogEnable)
|
||
DebugUtil.Log($"[AudioManager] 场景 {activeScene.name} 共移除 {removedCount} 个冗余 AudioListener");
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Events
|
||
|
||
/// <summary>
|
||
/// 错误音频播放时的处理
|
||
/// <para>停止特定的 Sound/Speak 音频(COMMON_CLAIM, COMMON_CLICK)</para>
|
||
/// </summary>
|
||
private void _OnErrorAudioPlay()
|
||
{
|
||
StopSpecificClips(PhxhSDK.EAudioType.Sound);
|
||
StopSpecificClips(PhxhSDK.EAudioType.Speak);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 停止指定类型中特定名称的音频片段
|
||
/// </summary>
|
||
private void StopSpecificClips(PhxhSDK.EAudioType type)
|
||
{
|
||
foreach (var audio in _playingPool.GetAllOfType(type))
|
||
{
|
||
if (audio.IsPlaying && audio.Clip != null &&
|
||
(audio.Clip.name == Constants.Sound.COMMON_CLAIM ||
|
||
audio.Clip.name == Constants.Sound.COMMON_CLICK))
|
||
{
|
||
audio.Stop();
|
||
}
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Debug
|
||
|
||
/// <summary>
|
||
/// 获取音频池统计信息
|
||
/// </summary>
|
||
public string GetPoolStats()
|
||
{
|
||
(int playingMusic, int playingSound, int playingSpeak) = _playingPool.GetStats();
|
||
(int stoppedMusic, int stoppedSound, int stoppedSpeak) = _stoppedPool.GetStats();
|
||
|
||
int totalPlaying = playingMusic + playingSound + playingSpeak;
|
||
int totalStopped = stoppedMusic + stoppedSound + stoppedSpeak;
|
||
|
||
return $"Audio Pool Stats:\n" +
|
||
$"Playing - Music: {playingMusic}/{_playingPool.GetLimit(PhxhSDK.EAudioType.Music)}, " +
|
||
$"Sound: {playingSound}/{_playingPool.GetLimit(PhxhSDK.EAudioType.Sound)}, " +
|
||
$"Speak: {playingSpeak}/{_playingPool.GetLimit(PhxhSDK.EAudioType.Speak)} " +
|
||
$"(Total: {totalPlaying})\n" +
|
||
$"Stopped - Music: {stoppedMusic}, Sound: {stoppedSound}, Speak: {stoppedSpeak} (Total: {totalStopped})\n" +
|
||
$"Total - {totalPlaying + totalStopped} AudioPlayers";
|
||
}
|
||
|
||
#endregion
|
||
} |