NLDClient-yudde/ProjectNLD/Assets/Code/Scripts/Framework/Audio/AudioManager.cs

959 lines
34 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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
}