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

788 lines
21 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 System.Linq;
using Cysharp.Threading.Tasks;
using Framework;
using PhxhSDK;
using UnityEngine;
using UnityEngine.Audio;
using Constants = Framework.Constants;
using Object = UnityEngine.Object;
/// <summary>
/// 音频管理器 - 统一的音频系统管理
/// - 对象池管理
/// - 播放控制
/// - 资源加载
/// - 音量管理
/// - 业务状态管理
/// </summary>
public class AudioManager: Singlenton<AudioManager>, IStart, IInitable, IUpdatable
{
#region Fields & Properties
// Unity GameObject (internal)
private GameObject _audioRoot;
private AudioListener _globalListener;
/// <summary>
/// 音频根节点(仅供框架内部使用,如 StoryAudioManager 需要设置父节点)
/// 注意:请勿在外部修改此对象,只用于层级设置
/// </summary>
public GameObject AudioRoot => _audioRoot;
// 业务状态
public static bool DisableAudio { get; set; } = false;
public bool IsPlayingAlbum { get; set; }
public bool IsPlayingBGM { get; set; }
public bool GlobalIsOpenSpeak { get; set; }
// 对象池限制 (internal)
private int _musicPoolLimit = 1;
private int _soundPoolLimit = 100;
private int _speakPoolLimit = 1;
// 音频池
private Dictionary<int, AudioPlayer> _allAudioDic;
private Dictionary<string, long> _playSoundTimeStampDic;
// 播放列表(用于限制播放数量)
private List<int> _playingMusicList;
private List<int> _playingSoundList;
private List<int> _playingSpeakList;
// Listener 跟随状态
private bool _isFollowCamera = false;
#endregion
#region Volume Control (Simplified - Direct access to AudioVolumeSettings)
/// <summary>
/// 音量设置(直接访问 AudioVolumeSettings.Instance)
/// 推荐:AudioVolumeSettings.Instance.MusicVolume = 0.5f;
/// </summary>
public AudioVolumeSettings VolumeSettings => AudioVolumeSettings.Instance;
/// <summary>
/// 设置音乐开关(简化版,会重置为默认音量)
/// </summary>
public void SetMusicEnable(bool isEnable)
{
AudioVolumeSettings.Instance.MusicVolume = isEnable ? 1 : 0;
}
/// <summary>
/// 设置音效开关(简化版,会重置为默认音量)
/// </summary>
public void SetSoundEnable(bool isEnable)
{
AudioVolumeSettings.Instance.SoundVolume = 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<int, AudioPlayer>();
_playingMusicList = new List<int>();
_playingSoundList = new List<int>();
_playingSpeakList = new List<int>();
_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<AudioListener>();
_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 Helper Methods
/// <summary>
/// 检查是否应该跳过播放(根据音量设置)
/// </summary>
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
};
}
#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<float> GetClipLength(string key)
{
var path = GetFullPath(key);
var clip = await AssetManager.Instance.LoadAssetAsync<AudioClip>(path);
if (clip)
{
return clip.length;
}
return -1;
}
#endregion
#region Audio Pool Management
/// <summary>
/// 获取可用的Audio
/// </summary>
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<int> _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;
}
/// <summary>
/// 添加到播放列表
/// </summary>
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);
}
}
/// <summary>
/// 从播放列表移除
/// </summary>
public void RemovePlaying(int id)
{
_playingMusicList.Remove(id);
_playingSoundList.Remove(id);
_playingSpeakList.Remove(id);
}
#endregion
#region Play API (Internal)
/// <summary>
/// 统一的播放核心方法(合并了 Music/Sound/Speak 的逻辑)
/// </summary>
private int _PlayAudioCore(string key, PhxhSDK.EAudioType audioType, bool isLoop,
int priority, bool isFade, float fadeTime, float volume,
GameObject followTarget, Vector3? staticPosition)
{
// 1. 音量检查
if (_ShouldSkipPlay(audioType))
return -1;
// 2. 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);
}
}
// 3. Speak 类型强制不循环
if (audioType == PhxhSDK.EAudioType.Speak)
{
isLoop = false;
}
// 4. 获取音频路径(Speak 使用 I18N 路径)
var path = GetFullPath(key, isI18N: audioType == PhxhSDK.EAudioType.Speak);
// 5. 获取可用的 AudioPlayer
var audio = _GetUsableAudio(audioType, isLoop);
if (audio == null)
{
DebugUtil.LogError($"AudioManager, PlayAudio, Cant GetUsableAudio key:{key}, type:{audioType}");
return -1;
}
// 6. 配置并播放
return ConfigureAndPlay(audio, path, key, isLoop, isFade, fadeTime, volume,
followTarget, staticPosition);
}
/// <summary>
/// 统一的配置和播放核心方法
/// </summary>
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)
/// <summary>
/// 播放音频(统一入口)
/// </summary>
/// <param name="key">音频配置Key</param>
/// <param name="type">音频类型(Music/Sound/Speak)</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)
{
// 1. 前置检查
if (DisableAudio)
return -1;
if (!_CheckPath(key))
{
if (DebugUtil.LogEnable)
{
DebugUtil.LogG($"无法找到对应音频文件,key:{key}");
}
return -1;
}
// 2. 特殊事件处理
if (key == Constants.Sound.ERROR_UI || key == Constants.Sound.ERROR_UI_FIGHT)
{
EventManager.Instance.Send(EventManager.EventName.Audio_ErrorAudioPlay);
}
// 3. Music 类型特殊处理
if (type == PhxhSDK.EAudioType.Music)
{
IsPlayingAlbum = false;
IsPlayingBGM = false;
}
// 4. 调用核心播放方法
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)
{
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;
}
/// <summary>
/// 根据音频ID获取AudioPlayer实例
/// 注意:即使DisableAudio=true,也会返回实例(因为可能需要查询状态或停止播放)
/// </summary>
public AudioPlayer GetAudioByID(int id)
{
return _allAudioDic.TryGetValue(id, out var audio) ? audio : null;
}
/// <summary>
/// 停止指定ID的音频
/// 注意:即使DisableAudio=true,也应该能停止音频
/// </summary>
public void StopByID(int audioID)
{
if (audioID >= 0 && _allAudioDic.TryGetValue(audioID, out var audio))
{
audio.Stop();
}
}
/// <summary>
/// 淡出停止指定ID的音频
/// 注意:即使DisableAudio=true,也应该能停止音频
/// </summary>
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
/// <summary>
/// 获取音频频谱数据(处理并归一化)
/// </summary>
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<AudioListener>();
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
}