using System; using System.Collections.Generic; using System.Linq; using Cysharp.Threading.Tasks; using Framework; using PhxhSDK; using UnityEngine; using UnityEngine.Audio; using Constants = Framework.Constants; using Object = UnityEngine.Object; /// /// 音频管理器 - 统一的音频系统管理 /// - 对象池管理 /// - 播放控制 /// - 资源加载 /// - 音量管理 /// - 业务状态管理 /// public class AudioManager: Singlenton, IStart, IInitable, IUpdatable { #region Fields & Properties // Unity GameObject public GameObject AudioRoot; public AudioListener GlobalListener; // 业务状态 public static bool DisableAudio = false; public bool IsPlayingAlbum { get; set; } public bool IsPlayingBGM { get; set; } public bool GlobalIsOpenSpeak; // 对象池限制 public int MusicPoolLimit = 1; public int SoundPoolLimit = 100; public int SpeakPoolLimit = 1; // 音频池 private Dictionary _allAudioDic; private Dictionary _playSoundTimeStampDic; // 播放列表(用于限制播放数量) private List _playingMusicList; private List _playingSoundList; private List _playingSpeakList; // Listener 跟随状态 private bool _isFollowCamera = false; #endregion #region Volume Properties (便捷访问 AudioVolumeSettings) /// /// 全局总音量 /// public float GlobalVolume { get => AudioVolumeSettings.Instance.GlobalVolume; set => AudioVolumeSettings.Instance.GlobalVolume = value; } /// /// 音乐音量 /// public float GlobalMusicVolume { get => AudioVolumeSettings.Instance.MusicVolume; set => AudioVolumeSettings.Instance.MusicVolume = value; } /// /// 音效音量 /// public float GlobalSoundsVolume { get => AudioVolumeSettings.Instance.SoundVolume; set => AudioVolumeSettings.Instance.SoundVolume = value; } /// /// 语音音量 /// public float GlobalSpeakVolume { get => AudioVolumeSettings.Instance.SpeakVolume; set => AudioVolumeSettings.Instance.SpeakVolume = value; } /// /// 设置音乐开关 /// public void SetMusicEnable(bool isEnable) { GlobalMusicVolume = isEnable ? 1 : 0; } /// /// 设置音效开关 /// public void SetSoundEnable(bool isEnable) { GlobalSoundsVolume = isEnable ? 1 : 0; } #endregion #region Lifecycle (Framework) private void AfterSwitchCamera(bool isRegister = true) { if (isRegister) { CameraManager.Instance.AfterSwitchMainCamera += AfterSwitchMainCamera; } else { CameraManager.Instance.AfterSwitchMainCamera -= AfterSwitchMainCamera; } } public void Init() { _allAudioDic = new Dictionary(); _playingMusicList = new List(); _playingSoundList = new List(); _playingSpeakList = new List(); _playSoundTimeStampDic = new(); EventManager.Instance.Register(EventManager.EventName.Audio_ErrorAudioPlay, _OnErrorAudioPlay); AfterSwitchCamera(); } public void Release() { EventManager.Instance.Unregister(EventManager.EventName.Audio_ErrorAudioPlay, _OnErrorAudioPlay); AfterSwitchCamera(false); } public void Start() { if (!AudioRoot) { AudioRoot = new GameObject(); AudioRoot.name = "Audio(Singleton)"; Object.DontDestroyOnLoad(AudioRoot); GlobalListener = AudioRoot.AddComponent(); _InitListenerPos(); } } public void Update(float dt) { _SetListenerPos(); // 遍历所有音频 foreach (var kv in _allAudioDic) { var audio = kv.Value; if (audio.State != PlayState.Stopped) { audio.Update(dt); } } } #endregion #region Asset Loading private string GetFullPath(string key, bool isI18N = false) { var cfg = TableManager.Instance.Tables.AudioSourceConfig.GetOrDefault(key); if (cfg == null) { DebugUtil.LogError($"音频Key:{key}找不到!"); return ""; } var path = cfg.Path; if (isI18N) { if (path.Contains(LanguageManager.I18N)) { var eLanguage = LanguageManager.Instance.GetCurrentLanguage(); var replacePath = LanguageManager.PathDic[eLanguage]; path = path.Replace(LanguageManager.I18N, replacePath); } return path; } return cfg.Path; } private float _GetNormalizeVolume(string key) { var cfg = TableManager.Instance.Tables.AudioSourceConfig.GetOrDefault(key); if (cfg == null) { DebugUtil.LogError($"音频Key:{key}找不到!"); return 0f; } var volume = cfg.VolumeScale; return volume; } private bool _CheckPath(string key) { var cfg = TableManager.Instance.Tables.AudioSourceConfig; if (cfg.GetOrDefault(key) != null) { return true; } return false; } public async UniTask GetClipLength(string key) { var path = GetFullPath(key); var clip = await AssetManager.Instance.LoadAssetAsync(path); if (clip) { return clip.length; } return -1; } #endregion #region Audio Pool Management /// /// 获取可用的Audio /// private AudioPlayer _GetUsableAudio(PhxhSDK.EAudioType eAudioType, bool isLoop) { try { var limit = _CheckIsAudioLimited(eAudioType); if (limit) { return _GetOldAudio(eAudioType); } var freeAudio = _GetFreeAudio(eAudioType); return freeAudio ?? _CreateNewAudio(eAudioType, isLoop); } catch (Exception e) { DebugUtil.LogError($"AudioManger, GetUsableAudio Fail.{e}"); } return null; } private AudioPlayer _GetOldAudio(PhxhSDK.EAudioType eAudioType) { var playingList = _GetPlayingList(eAudioType); if (playingList.Count > 0) { var audio = GetAudioByID(playingList[0]); return audio; } // 如果没有正在播放的音频,创建新的(使用默认不循环) return _CreateNewAudio(eAudioType, isLoop: false); } private int _GetLimit(PhxhSDK.EAudioType eAudioType) { switch (eAudioType) { case PhxhSDK.EAudioType.Music: return MusicPoolLimit; case PhxhSDK.EAudioType.Sound: return SoundPoolLimit; case PhxhSDK.EAudioType.Speak: return SpeakPoolLimit; default: return -1; } } private List _GetPlayingList(PhxhSDK.EAudioType eAudioType) { switch (eAudioType) { case PhxhSDK.EAudioType.Music: return _playingMusicList; case PhxhSDK.EAudioType.Sound: return _playingSoundList; case PhxhSDK.EAudioType.Speak: return _playingSpeakList; default: return _playingSoundList; } } private bool _CheckIsAudioLimited(PhxhSDK.EAudioType eAudioType) { var limit = _GetLimit(eAudioType); var playingList = _GetPlayingList(eAudioType); return playingList.Count >= limit; } private AudioPlayer _GetFreeAudio(PhxhSDK.EAudioType eAudioType) { foreach (var kv in _allAudioDic) { var audio = kv.Value; if (audio.AudioType == eAudioType && audio.State == PlayState.Stopped) { return audio; } } return null; } private AudioPlayer _CreateNewAudio(PhxhSDK.EAudioType eAudioType, bool isLoop) { var rootGo = new GameObject() { name = "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}"; if (isLoop) { rootGo.name += "Loop"; } _allAudioDic.Add(audio.AudioId, audio); return audio; } /// /// 添加到播放列表 /// public void Add2Playing(int id) { var audio = GetAudioByID(id); if (audio == null) return; var list = _GetPlayingList(audio.AudioType); if (!list.Contains(id)) { list.Add(id); } } /// /// 从播放列表移除 /// public void RemovePlaying(int id) { _playingMusicList.Remove(id); _playingSoundList.Remove(id); _playingSpeakList.Remove(id); } #endregion #region Play API (Internal) /// /// 内部播放接口 - GameObject /// private int _PlayAudio(string key, PhxhSDK.EAudioType type, GameObject posObj = null, bool isLoop = false, int priority = AudioConstant.DefaultPriority, bool isFade = false, float fadeTime = 0f) { var settings = AudioVolumeSettings.Instance; switch (type) { case PhxhSDK.EAudioType.Sound: if (settings.GlobalVolume <= 0 || settings.SoundVolume <= 0) return -1; break; case PhxhSDK.EAudioType.Speak: if (settings.GlobalVolume <= 0 || settings.SpeakVolume <= 0) return -1; break; } if (string.IsNullOrEmpty(key)) return -1; switch (type) { case PhxhSDK.EAudioType.Music: return _PlayMusic(key, posObj, isLoop, isFade, fadeTime); case PhxhSDK.EAudioType.Sound: return _PlaySound(key, posObj, isLoop, priority, isFade, fadeTime); case PhxhSDK.EAudioType.Speak: return _PlaySpeakSound(key, posObj, isFade, fadeTime); default: return -1; } } /// /// 内部播放接口 - Vector3 /// private int _PlayAudio(string key, PhxhSDK.EAudioType type, Vector3 pos, bool loop = false, int priority = AudioConstant.DefaultPriority, bool isFade = false, float fadeTime = 0f) { var settings = AudioVolumeSettings.Instance; switch (type) { case PhxhSDK.EAudioType.Sound: if (settings.GlobalVolume <= 0 || settings.SoundVolume <= 0) return -1; break; case PhxhSDK.EAudioType.Speak: if (settings.GlobalVolume <= 0 || settings.SpeakVolume <= 0) return -1; break; } switch (type) { case PhxhSDK.EAudioType.Music: return _PlayMusic(key, pos, loop, isFade, fadeTime); case PhxhSDK.EAudioType.Sound: return _PlaySound(key, pos, loop, priority, isFade, fadeTime); case PhxhSDK.EAudioType.Speak: return _PlaySpeakSound(key, pos, isFade, fadeTime); default: return -1; } } private int _PlayMusic(string key, GameObject posObj = null, bool isLoop = true, bool isFade = false, float fadeTime = 0f, float volume = 1f) { return _PlayMusicCore(key, isLoop, isFade, fadeTime, volume, posObj, null); } private int _PlayMusic(string key, Vector3 pos, bool isLoop, bool isFade = false, float fadeTime = 0f, float volume = 1f) { return _PlayMusicCore(key, isLoop, isFade, fadeTime, volume, null, pos); } private int _PlayMusicCore(string key, bool isLoop, bool isFade, float fadeTime, float volume, GameObject followTarget, Vector3? staticPosition) { var path = GetFullPath(key); var audio = _GetUsableAudio(PhxhSDK.EAudioType.Music, isLoop); if (audio == null) { DebugUtil.LogError($"AudioManager, PlayMusic, Cant GetUsableAudio key:{key}"); return -1; } return ConfigureAndPlay(audio, path, key, isLoop, isFade, fadeTime, volume, followTarget, staticPosition); } private int _PlaySound(string key, GameObject posObj = null, bool isLoop = false, int priority = AudioConstant.DefaultPriority, bool isFade = false, float fadeTime = 0f, float volume = 1f) { return _PlaySoundCore(key, isLoop, priority, isFade, fadeTime, volume, posObj, null); } private int _PlaySound(string key, Vector3 pos, bool isLoop = false, int priority = AudioConstant.DefaultPriority, bool isFade = false, float fadeTime = 0f, float volume = 1f) { return _PlaySoundCore(key, isLoop, priority, isFade, fadeTime, volume, null, pos); } private int _PlaySoundCore(string key, bool isLoop, int priority, bool isFade, float fadeTime, float volume, GameObject followTarget, Vector3? staticPosition) { if (AudioVolumeSettings.Instance.SoundVolume < 0.01f) { return -1; } // 防重播检查(低优先级音效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); } } var audio = _GetUsableAudio(PhxhSDK.EAudioType.Sound, isLoop); if (audio == null) { DebugUtil.LogError($"AudioManager, PlaySound, Cant GetUsableAudio key:{key}"); return -1; } var path = GetFullPath(key); return ConfigureAndPlay(audio, path, key, isLoop, isFade, fadeTime, volume, followTarget, staticPosition); } private int _PlaySpeakSound(string key, GameObject posObj = null, bool isFade = false, float fadeTime = 0f, float volume = 1f) { return _PlaySpeakSoundCore(key, isFade, fadeTime, volume, posObj, null); } private int _PlaySpeakSound(string key, Vector3 pos, bool isFade = false, float fadeTime = 0f, float volume = 1f) { return _PlaySpeakSoundCore(key, isFade, fadeTime, volume, null, pos); } private int _PlaySpeakSoundCore(string key, bool isFade, float fadeTime, float volume, GameObject followTarget, Vector3? staticPosition) { if (AudioVolumeSettings.Instance.SpeakVolume < 0.01f) { return -1; } var audio = _GetUsableAudio(PhxhSDK.EAudioType.Speak, isLoop: false); if (audio == null) { DebugUtil.LogError($"AudioManager, PlaySpeak, Cant GetUsableAudio key:{key}"); return -1; } var path = GetFullPath(key, isI18N: true); return ConfigureAndPlay(audio, path, key, isLoop: false, 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) { // 1. 设置音量缩放系数(来自配置表) var volumeScale = _GetNormalizeVolume(key); audio.SetVolumeScale(volumeScale); // 2. 设置循环 audio.Loop = isLoop; // 3. 设置位置(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); } // 4. 播放音频 if (isFade) { audio.Volume = 0; audio.LoadAndPlayWithFade(path, fadeTime, fadeTime); } else { audio.Volume = volume * volumeScale; audio.LoadAndPlay(path); } return audio.AudioId; } #endregion #region Play API (Public) /// /// 播放音频 - GameObject位置 /// public int PlayAudio(string key, PhxhSDK.EAudioType type = PhxhSDK.EAudioType.Sound, GameObject posObj = null, bool loop = false, int priority = AudioConstant.DefaultPriority, bool isFade = false) { if (DisableAudio) return -1; if (!_CheckPath(key)) { if (DebugUtil.LogEnable) { DebugUtil.LogG($"无法找到对应音频文件,key:{key}"); } return -1; } if (key == Constants.Sound.ERROR_UI || key == Constants.Sound.ERROR_UI_FIGHT) { EventManager.Instance.Send(EventManager.EventName.Audio_ErrorAudioPlay); } if (type == PhxhSDK.EAudioType.Music) { IsPlayingAlbum = false; IsPlayingBGM = false; } return _PlayAudio(key, type, posObj, loop, priority, isFade); } /// /// 播放音频 - Vector3位置 /// public int PlayAudio(string key, Vector3 pos, PhxhSDK.EAudioType type = PhxhSDK.EAudioType.Sound, bool loop = false, int priority = AudioConstant.DefaultPriority, bool isFade = false) { if (DisableAudio) return -1; if (!_CheckPath(key)) { DebugUtil.LogG($"无法找到对应音频文件,key:{key}"); return -1; } if (key == Constants.Sound.ERROR_UI || key == Constants.Sound.ERROR_UI_FIGHT) { EventManager.Instance.Send(EventManager.EventName.Audio_ErrorAudioPlay); } if (type == PhxhSDK.EAudioType.Music) { IsPlayingAlbum = false; IsPlayingBGM = false; } return _PlayAudio(key, type, pos, loop, priority, isFade); } public void PlayByKey(int audioID, string key, bool isFade = false, float fadeOutTime = 4f) { if (_allAudioDic.TryGetValue(audioID, out var audio)) { 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) { if (_allAudioDic.TryGetValue(audioID, out var audio)) { var fullPath = GetFullPath(key); audio.Volume = targetVolume; audio.LoadAndPlayWithFade(fullPath, fadeInTime); } } #endregion #region Control API public void InitLimit(int count0, int count1, int count2) { MusicPoolLimit = count0; SoundPoolLimit = count1; SpeakPoolLimit = count2; } /// /// 根据音频ID获取AudioPlayer实例 /// 注意:即使DisableAudio=true,也会返回实例(因为可能需要查询状态或停止播放) /// public AudioPlayer GetAudioByID(int id) { return _allAudioDic.TryGetValue(id, out var audio) ? audio : null; } /// /// 停止指定ID的音频 /// 注意:即使DisableAudio=true,也应该能停止音频 /// public void StopByID(int audioID) { if (audioID >= 0 && _allAudioDic.TryGetValue(audioID, out var audio)) { audio.Stop(); } } /// /// 淡出停止指定ID的音频 /// 注意:即使DisableAudio=true,也应该能停止音频 /// public void StopWithFadeByID(int id, float fadeOutTime = 1f) { if (id >= 0 && _allAudioDic.TryGetValue(id, out var audio)) { audio.StopWithFadeOut(fadeOutTime); } } public void PauseAllSound() { foreach (var kv in _allAudioDic) { if (kv.Value.AudioType == PhxhSDK.EAudioType.Sound) { kv.Value.Pause(); } } } public void ResumeAllSound() { foreach (var kv in _allAudioDic) { if (kv.Value.AudioType == PhxhSDK.EAudioType.Sound) { kv.Value.Resume(); } } } public void StopAllMusic() { IsPlayingAlbum = false; IsPlayingBGM = false; DebugUtil.LogG("StopAllMusic"); foreach (var kv in _allAudioDic) { if (kv.Value.AudioType == PhxhSDK.EAudioType.Music) { kv.Value.Stop(); } } } public void StopAllSounds() { DebugUtil.LogG("StopAllSounds"); foreach (var kv in _allAudioDic) { if (kv.Value.AudioType == PhxhSDK.EAudioType.Sound) { kv.Value.Stop(); } } } public void StopAllSpeaks() { DebugUtil.LogG("StopAllSpeaks"); foreach (var kv in _allAudioDic) { if (kv.Value.AudioType == PhxhSDK.EAudioType.Speak) { kv.Value.Stop(); } } } public void StopAll() { IsPlayingAlbum = false; IsPlayingBGM = false; DebugUtil.LogG("StopAll"); foreach (var kv in _allAudioDic) { var audio = kv.Value; audio.Stop(); } } public void ReleaseAllAudios() { foreach (var kv in _allAudioDic) { kv.Value.Dispose(); } _allAudioDic.Clear(); _playingMusicList.Clear(); _playingSoundList.Clear(); _playingSpeakList.Clear(); AudioPlayer.ReleaseLoadedAssets(); } #endregion #region Special API /// /// 获取音频频谱数据(处理并归一化) /// public void GetSpectrumData(int id, float[] spectrumsShow, float[] spectrumsData) { AudioPlayer audioInst = GetAudioByID(id); if (null == audioInst) { for (int i = 0; i < spectrumsShow.Length; ++i) { spectrumsShow[i] = 0f; } return; } audioInst.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]; } value = Mathf.Log10(Mathf.Log(i + 2f, 2f) * value * (i * i + 1) * 9 + 1); spectrumsShow[i] = value; } } #endregion #region Listener Management private void _SetListenerPos() { if (AudioRoot && _isFollowCamera) { var camera = CameraManager.Instance.MainCamera; if (camera) { AudioRoot.transform.position = camera.transform.position; } } } private void _InitListenerPos() { AudioRoot.transform.position = new Vector3(0, 0, 0); } public void EnterFollowState() { _isFollowCamera = true; } public void ExitFollowState() { _isFollowCamera = false; _InitListenerPos(); } public void SetGlobalListenerFollow(bool isActive) { if(isActive) EnterFollowState(); else ExitFollowState(); } public void AfterSwitchMainCamera(Camera camera) { var listener = camera.GetComponent(); if (listener) { Object.DestroyImmediate(listener); } } #endregion #region Events private void _OnErrorAudioPlay() { foreach (var kv in _allAudioDic) { var sound = kv.Value; if ((sound.AudioType == PhxhSDK.EAudioType.Sound || sound.AudioType == PhxhSDK.EAudioType.Speak) && sound.IsPlaying) { if (sound.Clip.name == Constants.Sound.COMMON_CLAIM || sound.Clip.name == Constants.Sound.COMMON_CLICK) { sound.Stop(); } } } } #endregion }