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;
///
/// 音频管理器 - 统一的音频系统管理
/// 职责:
/// - 协调播放池和停止池
/// - 音频播放控制
/// - 配置表查询和资源加载
/// - 音量管理(委托给 AudioVolumeSettings)
/// - 业务状态管理(Album、BGM 等)
///
public class AudioManager: Singlenton, IStart, IInitable, IUpdatable
{
#region Fields & Properties
// --- Unity Components ---
private GameObject _audioRoot;
private AudioListener _globalListener;
///
/// 音频根节点(供框架内部使用,如 StoryAudioManager 设置父节点)
///
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 _playSoundTimeStampDic; // Sound 防重播时间戳
// --- External Players ---
private readonly Dictionary _externalAudios = new();
private readonly List _externalTempList = new(); // 复用列表,避免 GC
#endregion
#region Volume Control
///
/// 音量设置(委托给 AudioVolumeSettings)
///
public AudioVolumeSettings VolumeSettings => AudioVolumeSettings.Instance;
#endregion
#region Lifecycle
///
/// 初始化数据结构和逻辑(IInitable 接口)
/// 职责:创建对象池、注册事件、应用默认配置
/// 不依赖 Unity GameObject,可独立测试
///
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;
}
///
/// 初始化 Unity 相关对象(IStart 接口)
/// 职责:创建 AudioRoot GameObject 和 AudioListener
/// 依赖 Unity Runtime,幂等操作
///
public void Start()
{
if (!_audioRoot)
{
_audioRoot = new GameObject("Audio(Singleton)");
Object.DontDestroyOnLoad(_audioRoot);
_globalListener = _audioRoot.AddComponent();
_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
///
/// 获取音频配置(统一入口,避免重复查询)
///
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 GetClipLength(string key)
{
var path = GetFullPath(key);
var clip = await AssetManager.Instance.LoadAssetAsync(path);
return clip ? clip.length : -1;
}
#endregion
#region Pool Management
///
/// 获取可用的 AudioPlayer
/// 策略:
/// - 播放池未满 → 从停止池精确复用或创建新的
/// - 播放池已满 → 优先从停止池精确复用,否则复用播放池最旧的
///
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;
}
///
/// 播放池音频停止回调处理
/// 当音频在播放池中自然停止(播放完成)时触发
/// 职责:从播放池移除 → 加入停止池(用于复用)
///
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 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
///
/// 播放音频(统一入口)
///
/// 音频配置 Key
/// 音频类型
/// 跟随对象(3D 音效)
/// 静态位置(3D 音效)
/// 是否循环
/// 优先级(Sound 防重播)
/// 是否淡入淡出
/// 音频 ID,失败返回 -1
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);
}
///
/// 根据音频 ID 播放新的音频配置
///
/// 音频 ID
/// 新的音频配置 Key
/// 是否使用淡入淡出
/// 淡入淡出时间(秒)
/// 目标音量(可选,不设置则使用配置表中的音量)
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
///
/// 应用音频池容量配置
/// 用于运行时动态调整播放池和停止池的容量上限
/// 建议在 Init() 后、首次播放前调用
///
/// 池容量配置
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");
}
}
///
/// 应用指定类型的播放池和停止池限制
///
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
///
/// 注册外部创建的 AudioPlayer(例如 Story 系统自建播放器)
///
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}");
}
///
/// 注销外部 AudioPlayer,避免持有无效引用
///
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})");
}
}
}
}
///
/// 停止指定类型的所有音频
///
/// 音频类型
public void StopAllOfType(PhxhSDK.EAudioType type)
{
DebugUtil.LogG($"[AudioManager] StopAllOfType: {type}");
_playingPool.StopAllOfType(type);
}
///
/// 暂停指定类型的所有音频
///
/// 音频类型
public void PauseAllOfType(PhxhSDK.EAudioType type)
{
DebugUtil.LogG($"[AudioManager] PauseAllOfType: {type}");
_playingPool.PauseAllOfType(type);
}
///
/// 恢复指定类型的所有音频
///
/// 音频类型
public void ResumeAllOfType(PhxhSDK.EAudioType type)
{
DebugUtil.LogG($"[AudioManager] ResumeAllOfType: {type}");
_playingPool.ResumeAllOfType(type);
}
///
/// 停止所有音频
///
public void StopAll()
{
DebugUtil.LogG("[AudioManager] StopAll");
_playingPool.StopAll();
}
///
/// 释放所有音频资源(清空池并销毁所有 AudioPlayer)
/// 自动停止所有播放中的音频,适用于场景卸载、关卡退出等场景
///
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
///
/// 获取音频频谱数据(归一化处理)
///
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
///
/// 场景加载完成后的处理
/// 自动移除新场景中的冗余 AudioListener,确保全局只有一个
///
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
RemoveRedundantAudioListeners().Forget();
if (DebugUtil.LogEnable)
DebugUtil.Log($"[AudioManager] 场景加载完成: {scene.name},已清理冗余 AudioListener");
}
///
/// 移除场景中所有冗余的 AudioListener(保留 AudioManager 管理的全局 AudioListener)
/// 确保全局只有一个 AudioListener,避免 Unity 警告和音频问题
/// 等待一帧后执行,确保场景完全加载
///
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(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
///
/// 错误音频播放时的处理
/// 停止特定的 Sound/Speak 音频(COMMON_CLAIM, COMMON_CLICK)
///
private void _OnErrorAudioPlay()
{
StopSpecificClips(PhxhSDK.EAudioType.Sound);
StopSpecificClips(PhxhSDK.EAudioType.Speak);
}
///
/// 停止指定类型中特定名称的音频片段
///
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
///
/// 获取音频池统计信息
///
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
}