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

734 lines
24 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 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;
public bool GlobalIsOpenSpeak { get; set; }
// --- Audio Pools ---
private AudioPlayingPool _playingPool; // 固定大小,LRU 复用
private AudioStoppedPool _stoppedPool; // LRU 缓存
private AudioPoolConfig _poolConfig; // 池容量配置
// --- Anti-Replay ---
private Dictionary<string, long> _playSoundTimeStampDic; // Sound 防重播时间戳
#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);
// 初始化停止池
_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;
}
/// <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);
}
#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<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>
/// </summary>
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<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;
// 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
/// <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 (!_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
/// <summary>
/// 应用音频池容量配置
/// <para>用于运行时动态调整播放池和停止池的容量上限</para>
/// <para>建议在 Init() 后、首次播放前调用</para>
/// </summary>
/// <param name="config">池容量配置</param>
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);
}
/// <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();
if (DebugUtil.LogEnable)
DebugUtil.Log($"[AudioManager] Released {playingCount + stoppedCount} audios (Playing: {playingCount}, Stopped: {stoppedCount})");
// 释放静态资源
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
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
/// <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
}