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

511 lines
14 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 PhxhSDK;
using UnityEngine;
using PlayState = PhxhSDK.PlayState;
/// <summary>
/// 音频播放器
/// 功能异步加载、播放控制、3D空间音效、淡入淡出效果
/// 实现接口IAudioPlayer, IAudioSpatial, IAudioFade, IDisposable
/// </summary>
public class AudioPlayer : IAudioPlayer, IAudioSpatial, IAudioFade, IDisposable
{
#region Static - ID Generation & Resource Tracking
private static int _audioIDCounter;
private static Dictionary<string, int> _loadedAssetPaths = new();
/// <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();
}
#endregion
#region Fields
// Unity 组件
private readonly GameObject _rootGo;
private readonly AudioSource _audioSource;
private AudioClip _clip;
// 状态
private PlayState _playState;
private readonly int _audioID;
private EAudioType _eAudioType;
private bool _loop;
// 音量控制
private float _targetVolume = 1f;
private float _normalizeVolume = 1f;
// 效果控制器
private readonly AudioFadeEffect _fadeEffect;
private readonly AudioSpatialController _spatialController;
#endregion
#region Properties
// IAudioPlayer 接口属性
public int AudioId => _audioID;
public PlayState State => _playState;
public bool IsPlaying => _playState == PlayState.Playing;
public AudioClip Clip => _clip;
/// <summary>
/// 音量0-1返回目标音量非实际音量
/// </summary>
public float Volume
{
get => _targetVolume;
set => _targetVolume = Mathf.Clamp01(value);
}
/// <summary>
/// 音频类型Music/Sound/Speak只读在构造时确定
/// </summary>
public EAudioType AudioType => _eAudioType;
/// <summary>
/// 是否循环播放
/// </summary>
public bool Loop
{
get => _loop;
set
{
_loop = value;
if (_audioSource != null)
_audioSource.loop = value;
}
}
// 内部属性(仅供 Manager 层访问)
internal GameObject RootGo => _rootGo;
#endregion
#region Constructor & Initialization
/// <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)
{
_audioID = GenAudioID();
_eAudioType = eAudioType;
_rootGo = rootGo;
_audioSource = GetOrAddAudioSource(rootGo);
_audioSource.volume = AudioVolumeSettings.Instance.GlobalVolume;
// 初始化效果控制器
_fadeEffect = new AudioFadeEffect(
fadeInDuration: AudioConstant.DefaultFadeInDuration,
fadeOutDuration: AudioConstant.DefaultFadeOutDuration);
_spatialController = new AudioSpatialController(_rootGo.transform, _audioSource);
// 如果指定了跟随对象,设置跟随
if (followTarget != null)
{
_spatialController.SetFollowTarget(followTarget);
}
}
private AudioSource GetOrAddAudioSource(GameObject go)
{
var source = go.GetComponent<AudioSource>();
if (!source)
{
source = go.AddComponent<AudioSource>();
}
return source;
}
#endregion
#region Lifecycle & Update
public void Update(float dt)
{
if (_audioSource == null || _clip == null)
return;
// 更新淡入淡出效果
bool shouldStop = _fadeEffect.Update(dt);
// 更新音量
if (_playState == PlayState.Playing || _audioSource.isPlaying)
{
UpdateVolume();
}
// 暂停状态不更新
if (_playState == PlayState.Paused)
return;
// 更新空间位置跟随
if (_playState == PlayState.Playing)
{
_spatialController.Update();
}
// 检查是否应该停止(播放结束或淡出完成)
if (IsPlaybackEnded() || shouldStop || _playState == PlayState.Stopped)
{
if (_playState == PlayState.Loading)
return;
Stop();
}
}
/// <summary>
/// 判断播放是否已结束
/// 只有非循环音频在播放完成后才返回 true
/// </summary>
private bool IsPlaybackEnded()
{
if (_audioSource == null || _clip == null)
return false;
// 循环音频永远不会结束
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;
}
#endregion
#region Playback Control
/// <summary>
/// 播放音频(需要先设置 Clip
/// </summary>
public void Play()
{
if (_clip == null)
{
DebugUtil.LogError("AudioPlayer: Cannot play without clip");
return;
}
_playState = PlayState.Playing;
_fadeEffect.Stop();
_targetVolume = 1f;
_audioSource.Play();
NotifyPlayStarted();
}
/// <summary>
/// 暂停播放
/// </summary>
public void Pause()
{
if (_audioSource != null)
_audioSource.Pause();
_playState = PlayState.Paused;
}
/// <summary>
/// 恢复播放
/// </summary>
public void Resume()
{
if (_audioSource != null)
_audioSource.UnPause();
_playState = PlayState.Playing;
}
/// <summary>
/// 停止播放
/// </summary>
public void Stop()
{
_fadeEffect.Stop();
AudioManager.Instance.RemovePlaying(_audioID);
if (IsPlaybackEnded())
{
_playState = PlayState.Stopped;
return;
}
if (_audioSource != null)
{
_audioSource.Stop();
}
_playState = PlayState.Stopped;
}
/// <summary>
/// 释放资源(销毁 GameObject
/// </summary>
public void Dispose()
{
Stop();
if (_rootGo != null)
{
UnityEngine.Object.Destroy(_rootGo);
}
}
#endregion
#region Async Loading & Play
/// <summary>
/// 异步加载并播放音频(不带淡入淡出)
/// </summary>
/// <param name="path">音频资源路径</param>
public void LoadAndPlay(string path)
{
_playState = PlayState.Loading;
LoadAndPlayInternal(path, useFade: false);
}
/// <summary>
/// 异步加载并播放音频(带淡入淡出效果)
/// </summary>
/// <param name="path">音频资源路径</param>
/// <param name="fadeInTime">淡入时长(秒)</param>
/// <param name="fadeOutTime">淡出时长(秒)</param>
public void LoadAndPlayWithFade(string path,
float fadeInTime = AudioConstant.DefaultFadeInDuration,
float fadeOutTime = AudioConstant.DefaultFadeOutDuration)
{
_playState = PlayState.Loading;
LoadAndPlayInternal(path, useFade: true, fadeInTime, fadeOutTime);
}
/// <summary>
/// 统一的异步加载和播放逻辑
/// </summary>
private async void LoadAndPlayInternal(string path, bool useFade,
float fadeInTime = AudioConstant.DefaultFadeInDuration,
float fadeOutTime = AudioConstant.DefaultFadeOutDuration)
{
// 1. 异步加载音频资源
_clip = await AssetManager.Instance.LoadAssetAsync<AudioClip>(path, null, true);
// 2. 跟踪已加载的资源(用于后续卸载)
TrackLoadedAsset(path);
// 3. 加载失败处理
if (_clip == null)
{
_playState = PlayState.Stopped;
return;
}
// 4. 设置 AudioSource
_audioSource.clip = _clip;
_targetVolume = 1f;
// 5. 根据是否需要淡入淡出设置音量
if (useFade)
{
_fadeEffect.FadeInDuration = fadeInTime;
_fadeEffect.FadeOutDuration = fadeOutTime;
_fadeEffect.StartFadeIn();
_audioSource.volume = 0f;
}
else
{
_fadeEffect.Stop();
}
// 6. 开始播放
_audioSource.Play();
_playState = PlayState.Playing;
// 7. 通知 Manager
NotifyPlayStarted();
}
/// <summary>
/// 跟踪已加载的资源(用于引用计数)
/// </summary>
private void TrackLoadedAsset(string path)
{
if (_loadedAssetPaths.ContainsKey(path))
++_loadedAssetPaths[path];
else
_loadedAssetPaths.TryAdd(path, 1);
}
/// <summary>
/// 通知 Manager 播放已开始
/// </summary>
private void NotifyPlayStarted()
{
AudioManager.Instance.Add2Playing(_audioID);
}
#endregion
#region Volume & Configuration
/// <summary>
/// 设置音量缩放系数(来自配置表的音量调整值)
/// </summary>
/// <param name="scale">音量缩放系数默认1.0,必须为非负数</param>
public void SetVolumeScale(float scale = 1f)
{
_normalizeVolume = Mathf.Max(0f, scale);
}
/// <summary>
/// 设置音频片段(仅供 Manager 层使用)
/// </summary>
internal void SetClip(AudioClip clip)
{
_clip = clip;
if (_audioSource != null)
_audioSource.clip = clip;
}
/// <summary>
/// 统一的音量更新方法
/// 最终音量 = 淡入淡出系数 × 类型音量 × 全局音量 × 目标音量 × 归一化音量
/// </summary>
private void UpdateVolume()
{
var settings = AudioVolumeSettings.Instance;
var fadeValue = _fadeEffect.FadeValue;
// 根据音频类型选择对应的音量系数
float typeVolume = this.AudioType switch
{
EAudioType.Music => settings.MusicVolume,
EAudioType.Sound => settings.SoundVolume,
EAudioType.Speak => settings.SpeakVolume,
_ => 1f
};
_audioSource.volume = fadeValue * typeVolume * settings.GlobalVolume * _targetVolume * _normalizeVolume;
}
#endregion
#region Spatial Audio (3D)
/// <summary>
/// 设置是否启用 3D 空间音效
/// </summary>
public void SetIs3D(bool is3D = false)
{
if (is3D)
_spatialController.Enable3D();
else
_spatialController.Disable3D();
}
/// <summary>
/// 设置跟随对象(自动启用 3D
/// </summary>
public void SetFollowTarget(GameObject target = null)
{
_spatialController.SetFollowTarget(target);
}
/// <summary>
/// 设置静态位置(自动启用 3D
/// </summary>
public void SetPosition(Vector3 position)
{
_spatialController.SetStaticPosition(position, enable3D: true);
}
#endregion
#region Fade Effects
/// <summary>
/// 淡入播放(需要先设置 Clip
/// </summary>
public void PlayWithFadeIn(float duration = AudioConstant.DefaultFadeInDuration)
{
if (_clip == null)
{
DebugUtil.LogError("AudioPlayer: Cannot PlayWithFadeIn without clip");
return;
}
_fadeEffect.FadeInDuration = duration;
_fadeEffect.StartFadeIn();
_playState = PlayState.Playing;
_audioSource.volume = 0f;
_audioSource.Play();
NotifyPlayStarted();
}
/// <summary>
/// 淡出停止(淡出完成后自动调用 Stop
/// </summary>
public void StopWithFadeOut(float duration = AudioConstant.DefaultFadeOutDuration)
{
_fadeEffect.StartFadeOut(duration);
}
#endregion
#region Query & Debug
/// <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
}