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

517 lines
14 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
2025-03-10 13:12:05 +08:00
using PhxhSDK;
using UnityEngine;
2025-11-14 19:35:19 +08:00
using PlayState = PhxhSDK.PlayState;
2025-03-10 13:12:05 +08:00
2025-11-14 15:57:36 +08:00
/// <summary>
/// 音频播放器
2025-11-17 14:37:45 +08:00
/// 功能:异步加载、播放控制、3D空间音效、淡入淡出效果
2025-11-20 12:02:14 +08:00
/// 实现接口:IAudioPlayer, IAudioSpatial, IAudioFade, IDisposable
2025-11-14 15:57:36 +08:00
/// </summary>
2025-11-20 12:02:14 +08:00
public class AudioPlayer : IAudioPlayer, IAudioSpatial, IAudioFade, IDisposable
2025-03-10 13:12:05 +08:00
{
2025-11-17 14:37:45 +08:00
#region Static - ID Generation & Resource Tracking
2025-11-14 19:10:22 +08:00
2025-11-17 14:37:45 +08:00
private static int _audioIDCounter;
2025-11-14 19:35:19 +08:00
private static Dictionary<string, int> _loadedAssetPaths = new();
2025-11-14 19:10:22 +08:00
2025-11-17 14:37:45 +08:00
/// <summary>
/// 生成唯一的 AudioID
/// </summary>
public static int GenAudioID() => ++_audioIDCounter;
/// <summary>
/// 重置 AudioID 计数器
/// </summary>
public static void ReleaseAudioIDs() => _audioIDCounter = 0;
/// <summary>
/// 释放所有已加载的资源
/// </summary>
public static void ReleaseLoadedAssets()
{
foreach (var kv in _loadedAssetPaths)
{
var key = kv.Key;
var count = kv.Value;
while (count > 0)
{
AssetManager.Instance.Unload(key, false, true);
--count;
}
}
_loadedAssetPaths.Clear();
}
2025-11-14 19:10:22 +08:00
#endregion
2025-11-17 14:37:45 +08:00
#region Fields
2025-11-14 19:10:22 +08:00
2025-11-14 19:35:19 +08:00
// Unity 组件
2025-11-17 14:37:45 +08:00
private readonly GameObject _rootGo;
private readonly AudioSource _audioSource;
2025-11-14 19:35:19 +08:00
private AudioClip _clip;
2025-11-17 14:37:45 +08:00
// 状态
2025-11-14 19:35:19 +08:00
private PlayState _playState;
2025-11-17 14:37:45 +08:00
private readonly int _audioID;
2025-11-17 19:37:24 +08:00
private EAudioType _eAudioType;
2025-11-14 19:35:19 +08:00
private bool _loop;
2025-11-24 12:08:57 +08:00
/// <summary>
/// 当前播放的音频配置 key(用于停止池复用)
/// 由 AudioManager 在播放前设置
/// </summary>
public string CurrentPlayingKey { get; set; }
2025-11-17 14:37:45 +08:00
// 音量控制
private float _targetVolume = 1f;
2025-11-14 19:35:19 +08:00
private float _normalizeVolume = 1f;
2025-11-14 19:10:22 +08:00
2025-11-17 14:37:45 +08:00
// 效果控制器
private readonly AudioFadeEffect _fadeEffect;
private readonly AudioSpatialController _spatialController;
2025-11-14 19:10:22 +08:00
#endregion
2025-11-17 14:37:45 +08:00
#region Properties
2025-11-14 19:10:22 +08:00
2025-11-17 14:37:45 +08:00
// IAudioPlayer 接口属性
2025-11-14 19:35:19 +08:00
public int AudioId => _audioID;
public PlayState State => _playState;
public bool IsPlaying => _playState == PlayState.Playing;
public AudioClip Clip => _clip;
2025-11-14 19:10:22 +08:00
2025-11-19 19:19:51 +08:00
/// <summary>
2025-11-20 12:02:14 +08:00
/// 音量(0-1),返回目标音量(非实际音量)
2025-11-19 19:19:51 +08:00
/// </summary>
2025-11-20 12:02:14 +08:00
public float Volume
2025-11-14 19:35:19 +08:00
{
2025-11-20 12:02:14 +08:00
get => _targetVolume;
set => _targetVolume = Mathf.Clamp01(value);
2025-11-14 19:35:19 +08:00
}
2025-11-14 19:10:22 +08:00
2025-11-20 12:02:14 +08:00
/// <summary>
/// 音频类型(Music/Sound/Speak),只读,在构造时确定
/// </summary>
public EAudioType AudioType => _eAudioType;
2025-11-19 19:19:51 +08:00
/// <summary>
/// 是否循环播放
/// </summary>
public bool Loop
2025-11-14 19:35:19 +08:00
{
get => _loop;
2025-11-19 19:19:51 +08:00
set
{
_loop = value;
if (_audioSource != null)
_audioSource.loop = value;
}
2025-11-14 19:35:19 +08:00
}
2025-11-20 12:02:14 +08:00
// 内部属性(仅供 Manager 层访问)
internal GameObject RootGo => _rootGo;
2025-11-14 19:35:19 +08:00
#endregion
2025-11-17 14:37:45 +08:00
#region Constructor & Initialization
2025-03-10 13:12:05 +08:00
2025-11-19 19:19:51 +08:00
/// <summary>
/// 构造音频播放器
/// </summary>
/// <param name="eAudioType">音频类型</param>
/// <param name="rootGo">挂载的 GameObject</param>
/// <param name="followTarget">可选的跟随目标(自动启用3D音效)</param>
public AudioPlayer(EAudioType eAudioType, GameObject rootGo, GameObject followTarget = null)
2025-03-10 13:12:05 +08:00
{
2025-11-17 14:37:45 +08:00
_audioID = GenAudioID();
2025-11-17 19:37:24 +08:00
_eAudioType = eAudioType;
2025-03-10 13:12:05 +08:00
_rootGo = rootGo;
2025-11-17 14:37:45 +08:00
_audioSource = GetOrAddAudioSource(rootGo);
2025-11-13 18:36:05 +08:00
_audioSource.volume = AudioVolumeSettings.Instance.GlobalVolume;
2025-11-13 18:55:54 +08:00
2025-11-17 14:37:45 +08:00
// 初始化效果控制器
2025-11-14 19:35:19 +08:00
_fadeEffect = new AudioFadeEffect(
fadeInDuration: AudioConstant.DefaultFadeInDuration,
fadeOutDuration: AudioConstant.DefaultFadeOutDuration);
_spatialController = new AudioSpatialController(_rootGo.transform, _audioSource);
2025-11-19 19:19:51 +08:00
// 如果指定了跟随对象,设置跟随
if (followTarget != null)
{
2025-11-19 19:19:51 +08:00
_spatialController.SetFollowTarget(followTarget);
}
2025-03-10 13:12:05 +08:00
}
2025-11-17 14:37:45 +08:00
private AudioSource GetOrAddAudioSource(GameObject go)
2025-03-10 13:12:05 +08:00
{
2025-11-17 14:37:45 +08:00
var source = go.GetComponent<AudioSource>();
2025-03-10 13:12:05 +08:00
if (!source)
{
2025-11-17 14:37:45 +08:00
source = go.AddComponent<AudioSource>();
2025-03-10 13:12:05 +08:00
}
return source;
}
2025-11-24 12:08:57 +08:00
#endregion
2025-11-17 14:37:45 +08:00
#region Lifecycle & Update
2025-03-10 13:12:05 +08:00
2025-11-14 19:10:22 +08:00
public void Update(float dt)
2025-03-10 13:12:05 +08:00
{
2025-11-17 14:37:45 +08:00
if (_audioSource == null || _clip == null)
2025-03-10 13:12:05 +08:00
return;
2025-11-13 18:55:54 +08:00
// 更新淡入淡出效果
bool shouldStop = _fadeEffect.Update(dt);
2025-11-17 14:37:45 +08:00
// 更新音量
2025-11-14 19:35:19 +08:00
if (_playState == PlayState.Playing || _audioSource.isPlaying)
2025-03-10 13:12:05 +08:00
{
2025-11-17 14:37:45 +08:00
UpdateVolume();
2025-03-10 13:12:05 +08:00
}
2025-11-17 14:37:45 +08:00
// 暂停状态不更新
2025-11-14 19:35:19 +08:00
if (_playState == PlayState.Paused)
return;
2025-11-13 18:55:54 +08:00
// 更新空间位置跟随
2025-11-14 19:35:19 +08:00
if (_playState == PlayState.Playing)
2025-03-10 13:12:05 +08:00
{
_spatialController.Update();
2025-03-10 13:12:05 +08:00
}
2025-11-13 18:55:54 +08:00
// 检查是否应该停止(播放结束或淡出完成)
2025-11-17 14:37:45 +08:00
if (IsPlaybackEnded() || shouldStop || _playState == PlayState.Stopped)
2025-03-10 13:12:05 +08:00
{
2025-11-14 19:35:19 +08:00
if (_playState == PlayState.Loading)
2025-08-20 14:39:40 +08:00
return;
2025-03-10 13:12:05 +08:00
Stop();
}
}
2025-11-24 12:08:57 +08:00
2025-11-17 14:37:45 +08:00
/// <summary>
/// 判断播放是否已结束
2025-11-20 12:02:14 +08:00
/// 只有非循环音频在播放完成后才返回 true
2025-11-17 14:37:45 +08:00
/// </summary>
private bool IsPlaybackEnded()
2025-03-10 13:12:05 +08:00
{
2025-11-17 14:37:45 +08:00
if (_audioSource == null || _clip == null)
return false;
2025-11-20 12:02:14 +08:00
// 循环音频永远不会结束
if (_loop)
return false;
// 只有同时满足以下条件才算结束:
// 1. 不在播放状态(!isPlaying)
// 2. 播放时间已到末尾(time >= length)
// 3. 状态不是 Paused(暂停不算结束)
// 4. 状态不是 Loading(加载中不算结束)
return !_audioSource.isPlaying
&& _audioSource.time >= _clip.length
&& _playState != PlayState.Paused
&& _playState != PlayState.Loading;
2025-03-10 13:12:05 +08:00
}
2025-11-17 14:37:45 +08:00
#endregion
#region Playback Control
2025-03-10 13:12:05 +08:00
2025-11-17 14:37:45 +08:00
/// <summary>
/// 播放音频(需要先设置 Clip)
/// </summary>
2025-11-14 19:10:22 +08:00
public void Play()
2025-03-10 13:12:05 +08:00
{
2025-11-17 14:37:45 +08:00
if (_clip == null)
{
DebugUtil.LogError("AudioPlayer: Cannot play without clip");
return;
}
2025-11-14 19:35:19 +08:00
_playState = PlayState.Playing;
2025-11-17 14:37:45 +08:00
_fadeEffect.Stop();
2025-11-14 19:35:19 +08:00
_targetVolume = 1f;
2025-03-10 13:12:05 +08:00
_audioSource.Play();
2025-11-17 14:37:45 +08:00
NotifyPlayStarted();
}
/// <summary>
/// 暂停播放
/// </summary>
public void Pause()
{
if (_audioSource != null)
2025-11-24 12:08:57 +08:00
_audioSource.Pause();
2025-11-17 14:37:45 +08:00
_playState = PlayState.Paused;
2025-03-10 13:12:05 +08:00
}
2025-11-17 14:37:45 +08:00
/// <summary>
/// 恢复播放
/// </summary>
public void Resume()
{
if (_audioSource != null)
2025-11-24 12:08:57 +08:00
_audioSource.UnPause();
2025-11-17 14:37:45 +08:00
_playState = PlayState.Playing;
}
/// <summary>
/// 停止播放
/// </summary>
2025-11-14 19:10:22 +08:00
public void Stop()
2025-03-10 13:12:05 +08:00
{
2025-11-17 14:37:45 +08:00
_fadeEffect.Stop();
2025-11-13 18:55:54 +08:00
2025-11-17 14:37:45 +08:00
if (IsPlaybackEnded())
2025-03-10 13:12:05 +08:00
{
2025-11-14 19:35:19 +08:00
_playState = PlayState.Stopped;
2025-03-10 13:12:05 +08:00
return;
}
2025-03-24 15:59:50 +08:00
2025-11-17 14:37:45 +08:00
if (_audioSource != null)
2025-03-24 15:59:50 +08:00
{
_audioSource.Stop();
}
2025-11-14 19:35:19 +08:00
_playState = PlayState.Stopped;
2025-03-10 13:12:05 +08:00
}
2025-11-20 12:02:14 +08:00
/// <summary>
/// 释放资源(销毁 GameObject)
/// </summary>
public void Dispose()
{
Stop();
2025-11-24 12:08:57 +08:00
2025-11-20 12:02:14 +08:00
if (_rootGo != null)
{
UnityEngine.Object.Destroy(_rootGo);
}
}
2025-11-24 12:08:57 +08:00
#endregion
2025-11-17 14:37:45 +08:00
#region Async Loading & Play
2025-03-10 13:12:05 +08:00
2025-11-14 18:21:45 +08:00
/// <summary>
/// 异步加载并播放音频(不带淡入淡出)
/// </summary>
2025-11-20 12:02:14 +08:00
/// <param name="path">音频资源路径</param>
public void LoadAndPlay(string path)
2025-08-19 18:12:10 +08:00
{
2025-11-14 19:35:19 +08:00
_playState = PlayState.Loading;
2025-11-17 14:37:45 +08:00
LoadAndPlayInternal(path, useFade: false);
2025-11-14 18:21:45 +08:00
}
/// <summary>
2025-11-20 12:02:14 +08:00
/// 异步加载并播放音频(带淡入淡出效果)
2025-11-14 18:21:45 +08:00
/// </summary>
2025-11-20 12:02:14 +08:00
/// <param name="path">音频资源路径</param>
/// <param name="fadeInTime">淡入时长(秒)</param>
/// <param name="fadeOutTime">淡出时长(秒)</param>
public void LoadAndPlayWithFade(string path,
2025-11-14 19:35:19 +08:00
float fadeInTime = AudioConstant.DefaultFadeInDuration,
float fadeOutTime = AudioConstant.DefaultFadeOutDuration)
2025-11-14 18:21:45 +08:00
{
2025-11-14 19:35:19 +08:00
_playState = PlayState.Loading;
2025-11-17 14:37:45 +08:00
LoadAndPlayInternal(path, useFade: true, fadeInTime, fadeOutTime);
2025-08-19 18:12:10 +08:00
}
2025-11-14 18:21:45 +08:00
/// <summary>
2025-11-17 14:37:45 +08:00
/// 统一的异步加载和播放逻辑
2025-11-14 18:21:45 +08:00
/// </summary>
2025-11-17 14:37:45 +08:00
private async void LoadAndPlayInternal(string path, bool useFade,
2025-11-14 19:35:19 +08:00
float fadeInTime = AudioConstant.DefaultFadeInDuration,
float fadeOutTime = AudioConstant.DefaultFadeOutDuration)
2025-03-10 13:12:05 +08:00
{
2025-11-14 18:21:45 +08:00
// 1. 异步加载音频资源
_clip = await AssetManager.Instance.LoadAssetAsync<AudioClip>(path, null, true);
2025-11-14 18:21:45 +08:00
// 2. 跟踪已加载的资源(用于后续卸载)
2025-11-17 14:37:45 +08:00
TrackLoadedAsset(path);
2025-11-14 18:21:45 +08:00
// 3. 加载失败处理
if (_clip == null)
2025-03-10 13:12:05 +08:00
{
2025-11-14 19:35:19 +08:00
_playState = PlayState.Stopped;
2025-03-10 13:12:05 +08:00
return;
}
2025-11-14 18:21:45 +08:00
2025-11-17 14:37:45 +08:00
// 4. 设置 AudioSource
2025-03-10 13:12:05 +08:00
_audioSource.clip = _clip;
2025-11-14 19:35:19 +08:00
_targetVolume = 1f;
2025-11-14 18:21:45 +08:00
// 5. 根据是否需要淡入淡出设置音量
if (useFade)
{
_fadeEffect.FadeInDuration = fadeInTime;
_fadeEffect.FadeOutDuration = fadeOutTime;
_fadeEffect.StartFadeIn();
_audioSource.volume = 0f;
}
else
{
_fadeEffect.Stop();
}
// 6. 开始播放
2025-03-10 13:12:05 +08:00
_audioSource.Play();
2025-11-14 19:35:19 +08:00
_playState = PlayState.Playing;
2025-11-14 18:21:45 +08:00
2025-11-17 14:37:45 +08:00
// 7. 通知 Manager
NotifyPlayStarted();
2025-03-10 13:12:05 +08:00
}
2025-11-14 18:21:45 +08:00
/// <summary>
/// 跟踪已加载的资源(用于引用计数)
/// </summary>
2025-11-17 14:37:45 +08:00
private void TrackLoadedAsset(string path)
2025-03-10 13:12:05 +08:00
{
2025-11-13 20:14:42 +08:00
if (_loadedAssetPaths.ContainsKey(path))
++_loadedAssetPaths[path];
else
2025-11-13 20:14:42 +08:00
_loadedAssetPaths.TryAdd(path, 1);
2025-11-14 18:21:45 +08:00
}
/// <summary>
2025-11-24 12:08:57 +08:00
/// 通知外部播放已开始(触发事件)
2025-11-14 18:21:45 +08:00
/// </summary>
2025-11-17 14:37:45 +08:00
private void NotifyPlayStarted()
2025-11-14 18:21:45 +08:00
{
// 空方法,保留接口以避免破坏现有调用
2025-03-10 13:12:05 +08:00
}
2025-11-17 14:37:45 +08:00
#endregion
#region Volume & Configuration
2025-03-10 13:12:05 +08:00
/// <summary>
2025-11-19 19:19:51 +08:00
/// 设置音量缩放系数(来自配置表的音量调整值)
/// </summary>
2025-11-20 12:02:14 +08:00
/// <param name="scale">音量缩放系数,默认1.0,必须为非负数</param>
2025-11-19 19:19:51 +08:00
public void SetVolumeScale(float scale = 1f)
2025-03-10 13:12:05 +08:00
{
2025-11-20 12:02:14 +08:00
_normalizeVolume = Mathf.Max(0f, scale);
2025-03-10 13:12:05 +08:00
}
2025-11-14 15:57:36 +08:00
/// <summary>
2025-11-20 12:02:14 +08:00
/// 设置音频片段(仅供 Manager 层使用)
2025-11-14 15:57:36 +08:00
/// </summary>
2025-11-20 12:02:14 +08:00
internal void SetClip(AudioClip clip)
2025-11-17 14:37:45 +08:00
{
_clip = clip;
if (_audioSource != null)
_audioSource.clip = clip;
}
2025-11-14 15:57:36 +08:00
/// <summary>
2025-11-17 14:37:45 +08:00
/// 统一的音量更新方法
/// 最终音量 = 淡入淡出系数 × 类型音量 × 全局音量 × 目标音量 × 归一化音量
2025-11-14 15:57:36 +08:00
/// </summary>
2025-11-17 14:37:45 +08:00
private void UpdateVolume()
{
var settings = AudioVolumeSettings.Instance;
var fadeValue = _fadeEffect.FadeValue;
2025-11-19 17:57:39 +08:00
// 根据音频类型选择对应的音量系数
2025-11-19 19:19:51 +08:00
float typeVolume = this.AudioType switch
2025-11-17 14:37:45 +08:00
{
2025-11-17 19:37:24 +08:00
EAudioType.Music => settings.MusicVolume,
2025-11-19 17:57:39 +08:00
EAudioType.Sound => settings.SoundVolume,
EAudioType.Speak => settings.SpeakVolume,
2025-11-17 14:37:45 +08:00
_ => 1f
};
_audioSource.volume = fadeValue * typeVolume * settings.GlobalVolume * _targetVolume * _normalizeVolume;
}
2025-03-10 13:12:05 +08:00
2025-11-17 14:37:45 +08:00
#endregion
2025-11-13 16:59:52 +08:00
2025-11-17 14:37:45 +08:00
#region Spatial Audio (3D)
2025-11-13 16:59:52 +08:00
/// <summary>
2025-11-17 14:37:45 +08:00
/// 设置是否启用 3D 空间音效
2025-11-13 16:59:52 +08:00
/// </summary>
2025-11-17 14:37:45 +08:00
public void SetIs3D(bool is3D = false)
2025-11-13 16:59:52 +08:00
{
2025-11-17 14:37:45 +08:00
if (is3D)
_spatialController.Enable3D();
else
_spatialController.Disable3D();
2025-11-13 16:59:52 +08:00
}
2025-11-14 15:57:36 +08:00
2025-11-17 14:37:45 +08:00
/// <summary>
/// 设置跟随对象(自动启用 3D)
/// </summary>
public void SetFollowTarget(GameObject target = null)
{
_spatialController.SetFollowTarget(target);
}
2025-11-14 15:57:36 +08:00
/// <summary>
2025-11-17 14:37:45 +08:00
/// 设置静态位置(自动启用 3D)
2025-11-14 15:57:36 +08:00
/// </summary>
2025-11-17 14:37:45 +08:00
public void SetPosition(Vector3 position)
2025-11-14 15:57:36 +08:00
{
2025-11-17 14:37:45 +08:00
_spatialController.SetStaticPosition(position, enable3D: true);
2025-11-14 15:57:36 +08:00
}
2025-11-17 14:37:45 +08:00
2025-11-24 12:08:57 +08:00
#endregion
#region Fade Effects
2025-11-14 15:57:36 +08:00
/// <summary>
2025-11-17 14:37:45 +08:00
/// 淡入播放(需要先设置 Clip)
2025-11-14 15:57:36 +08:00
/// </summary>
2025-11-14 19:35:19 +08:00
public void PlayWithFadeIn(float duration = AudioConstant.DefaultFadeInDuration)
2025-11-14 15:57:36 +08:00
{
if (_clip == null)
{
DebugUtil.LogError("AudioPlayer: Cannot PlayWithFadeIn without clip");
return;
}
_fadeEffect.FadeInDuration = duration;
_fadeEffect.StartFadeIn();
2025-11-14 19:35:19 +08:00
_playState = PlayState.Playing;
2025-11-14 15:57:36 +08:00
_audioSource.volume = 0f;
_audioSource.Play();
2025-11-17 14:37:45 +08:00
NotifyPlayStarted();
2025-11-14 15:57:36 +08:00
}
/// <summary>
2025-11-17 14:37:45 +08:00
/// 淡出停止(淡出完成后自动调用 Stop)
2025-11-14 15:57:36 +08:00
/// </summary>
2025-11-14 19:35:19 +08:00
public void StopWithFadeOut(float duration = AudioConstant.DefaultFadeOutDuration)
{
_fadeEffect.StartFadeOut(duration);
}
2025-11-24 12:08:57 +08:00
#endregion
2025-11-17 14:37:45 +08:00
#region Query & Debug
2025-11-14 15:57:36 +08:00
2025-11-17 14:37:45 +08:00
/// <summary>
/// 获取当前播放时间(秒)
/// </summary>
public float GetPlaybackTime() => _audioSource ? _audioSource.time : -1f;
/// <summary>
/// 获取音频频谱数据(用于可视化)
/// </summary>
public void GetSpectrumData(float[] spectrums)
{
if (_audioSource != null)
{
_audioSource.GetSpectrumData(spectrums, 0, FFTWindow.Blackman);
}
}
#endregion
}