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 }