using System;
using System.Collections.Generic;
using System.Linq;
using Cysharp.Threading.Tasks;
using Framework;
using PhxhSDK;
using UnityEngine;
using UnityEngine.Audio;
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;
public bool GlobalIsOpenSpeak { get; set; }
// --- Audio Pools ---
private AudioPlayingPool _playingPool; // 固定大小,LRU 复用
private AudioStoppedPool _stoppedPool; // LRU 缓存
private AudioPoolConfig _poolConfig; // 池容量配置
// --- Anti-Replay ---
private Dictionary _playSoundTimeStampDic; // Sound 防重播时间戳
#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);
// 初始化停止池
_stoppedPool = new AudioStoppedPool();
_stoppedPool.SetLimit(PhxhSDK.EAudioType.Music, _poolConfig.MusicStoppedLimit);
_stoppedPool.SetLimit(PhxhSDK.EAudioType.Sound, _poolConfig.SoundStoppedLimit);
_stoppedPool.SetLimit(PhxhSDK.EAudioType.Speak, _poolConfig.SpeakStoppedLimit);
// 初始化其他
_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);
}
#endregion
#region Config & Asset
private bool _CheckPath(string key)
{
return TableManager.Instance.Tables.AudioSourceConfig.GetOrDefault(key) != null;
}
private string GetFullPath(string key, bool isI18N = false)
{
var cfg = TableManager.Instance.Tables.AudioSourceConfig.GetOrDefault(key);
if (cfg == null)
{
DebugUtil.LogError($"[AudioManager] 音频配置不存在: {key}");
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 = TableManager.Instance.Tables.AudioSourceConfig.GetOrDefault(key);
if (cfg == null)
{
DebugUtil.LogError($"[AudioManager] 音频配置不存在: {key}");
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;
// 1. 播放池未满:尝试从停止池获取或创建新的
if (!_playingPool.IsFull(eAudioType))
{
// 尝试从停止池获取(精确匹配 key)
audio = _stoppedPool.GetFree(eAudioType, key);
if (audio != null)
{
// 从停止池移除
_stoppedPool.RemoveById(audio.AudioId);
}
else
{
// 创建新的
audio = _CreateNewAudio(eAudioType, isLoop);
}
// 加入播放池
_playingPool.Add(audio);
return audio;
}
// 2. 播放池已满:复用最旧的
audio = _playingPool.GetOldest(eAudioType);
if (audio != null)
{
// 停止音频
audio.Stop();
// 从播放池移除
_playingPool.Remove(audio.AudioId);
// 如果有 key,加入停止池
if (!string.IsNullOrEmpty(audio.CurrentPlayingKey))
{
var evictedAudios = _stoppedPool.AddStopped(audio.AudioType, audio.CurrentPlayingKey, audio);
DisposeEvictedAudios(evictedAudios);
}
// 从停止池移除(如果刚加入了)
_stoppedPool.RemoveById(audio.AudioId);
// 重新加入播放池
_playingPool.Add(audio);
}
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 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;
// Sound 防重播(低优先级 100ms 内防重)
if (audioType == PhxhSDK.EAudioType.Sound && 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);
}
}
// Speak 强制不循环
if (audioType == PhxhSDK.EAudioType.Speak)
isLoop = false;
// 获取路径(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, isLoop, isFade, fadeTime, volume,
followTarget, staticPosition);
}
private int _ConfigureAndPlay(AudioPlayer audio, string path, string key, bool isLoop, bool isFade,
float fadeTime, float volume, GameObject followTarget, Vector3? staticPosition)
{
audio.CurrentPlayingKey = key;
audio.SetVolumeScale(_GetVolumeScale(key));
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 * _GetVolumeScale(key);
audio.LoadAndPlay(path);
}
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 (!_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);
}
public void PlayByKey(int audioID, string key, bool isFade = false, float fadeOutTime = 4f)
{
var audio = GetAudioByID(audioID);
if (audio == null)
return;
audio.CurrentPlayingKey = key;
var fullPath = GetFullPath(key);
if (isFade)
audio.LoadAndPlayWithFade(fullPath, fadeOutTime);
else
audio.LoadAndPlay(fullPath);
}
public void PlayByKey(int audioID, string key, float fadeInTime, float targetVolume)
{
var audio = GetAudioByID(audioID);
if (audio == null)
return;
audio.CurrentPlayingKey = key;
audio.Volume = targetVolume;
audio.LoadAndPlayWithFade(GetFullPath(key), fadeInTime);
}
#endregion
#region Public API - Control
///
/// 应用音频池容量配置
/// 用于运行时动态调整播放池和停止池的容量上限
/// 建议在 Init() 后、首次播放前调用
///
/// 池容量配置
public void ApplyPoolConfig(AudioPoolConfig config)
{
// 验证并应用播放池限制
if (config.MusicPlayingLimit > 0)
{
_poolConfig.MusicPlayingLimit = config.MusicPlayingLimit;
_playingPool.SetLimit(PhxhSDK.EAudioType.Music, config.MusicPlayingLimit);
}
else
{
DebugUtil.LogWarning("[AudioManager] Invalid MusicPlayingLimit, must be > 0");
}
if (config.SoundPlayingLimit > 0)
{
_poolConfig.SoundPlayingLimit = config.SoundPlayingLimit;
_playingPool.SetLimit(PhxhSDK.EAudioType.Sound, config.SoundPlayingLimit);
}
else
{
DebugUtil.LogWarning("[AudioManager] Invalid SoundPlayingLimit, must be > 0");
}
if (config.SpeakPlayingLimit > 0)
{
_poolConfig.SpeakPlayingLimit = config.SpeakPlayingLimit;
_playingPool.SetLimit(PhxhSDK.EAudioType.Speak, config.SpeakPlayingLimit);
}
else
{
DebugUtil.LogWarning("[AudioManager] Invalid SpeakPlayingLimit, must be > 0");
}
// 验证并应用停止池限制
if (config.MusicStoppedLimit >= 0)
{
_poolConfig.MusicStoppedLimit = config.MusicStoppedLimit;
_stoppedPool.SetLimit(PhxhSDK.EAudioType.Music, config.MusicStoppedLimit);
}
else
{
DebugUtil.LogWarning("[AudioManager] Invalid MusicStoppedLimit, must be >= 0");
}
if (config.SoundStoppedLimit >= 0)
{
_poolConfig.SoundStoppedLimit = config.SoundStoppedLimit;
_stoppedPool.SetLimit(PhxhSDK.EAudioType.Sound, config.SoundStoppedLimit);
}
else
{
DebugUtil.LogWarning("[AudioManager] Invalid SoundStoppedLimit, must be >= 0");
}
if (config.SpeakStoppedLimit >= 0)
{
_poolConfig.SpeakStoppedLimit = config.SpeakStoppedLimit;
_stoppedPool.SetLimit(PhxhSDK.EAudioType.Speak, config.SpeakStoppedLimit);
}
else
{
DebugUtil.LogWarning("[AudioManager] Invalid SpeakStoppedLimit, must be >= 0");
}
if (DebugUtil.LogEnable)
{
DebugUtil.Log($"[AudioManager] Pool config applied: " +
$"Playing(M:{_poolConfig.MusicPlayingLimit}, S:{_poolConfig.SoundPlayingLimit}, Sp:{_poolConfig.SpeakPlayingLimit}) " +
$"Stopped(M:{_poolConfig.MusicStoppedLimit}, S:{_poolConfig.SoundStoppedLimit}, Sp:{_poolConfig.SpeakStoppedLimit})");
}
}
public AudioPlayer GetAudioByID(int id)
{
var audio = _playingPool.GetById(id);
return audio ?? _stoppedPool.GetById(id);
}
public void StopByID(int audioID)
{
if (audioID >= 0)
GetAudioByID(audioID)?.Stop();
}
public void StopWithFadeByID(int id, float fadeOutTime = 1f)
{
if (id >= 0)
GetAudioByID(id)?.StopWithFadeOut(fadeOutTime);
}
///
/// 停止指定类型的所有音频
///
/// 音频类型
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();
if (DebugUtil.LogEnable)
DebugUtil.Log($"[AudioManager] Released {playingCount + stoppedCount} audios (Playing: {playingCount}, Stopped: {stoppedCount})");
// 释放静态资源
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
private void _OnErrorAudioPlay()
{
// 停止特定的 Sound/Speak(COMMON_CLAIM, COMMON_CLICK)
foreach (var sound in _playingPool.GetAllOfType(PhxhSDK.EAudioType.Sound))
{
if (sound.IsPlaying && sound.Clip != null &&
(sound.Clip.name == Constants.Sound.COMMON_CLAIM || sound.Clip.name == Constants.Sound.COMMON_CLICK))
{
sound.Stop();
}
}
foreach (var speak in _playingPool.GetAllOfType(PhxhSDK.EAudioType.Speak))
{
if (speak.IsPlaying && speak.Clip != null &&
(speak.Clip.name == Constants.Sound.COMMON_CLAIM || speak.Clip.name == Constants.Sound.COMMON_CLICK))
{
speak.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
}