Files

471 lines
18 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Audio;
using DG.Tweening;
namespace OCES.Audio
{
public partial class AudioSystem : MonoBehaviour
{
public static AudioSystem Instance { get; private set; }
public bool startWithMusic;
public Parameters.GameState startMusicWith;
public ConsoleLogLevel logLevel;
// ReSharper disable once MemberCanBePrivate.Global
public IReadOnlyDictionary<Type, Enum> ActiveStates { get; private set; }
public enum ConsoleLogLevel { Off, Log, Warning, Error } //TODO 实现这个功能
internal ResourceLoader ResourceLoader;
[SerializeField] AudioExtendSettings extendSettings;
SfxSystem m_sfxSystem;
MusicSystem m_musicSystem;
AudioObjectConfig m_audioObjects;
AudioGroupConfig m_groups;
AudioMixer m_mixer;
Tween m_lowpassTween;
partial void OnAwakeComplete();
// ─────────────────────────────────────────────
// 公开接口
// ─────────────────────────────────────────────
public event Action<uint> OnBeat;
public event Action<uint> OnBar;
public event Action<uint> OnGrid;
public void Play(uint audioId, Action onPlay = null)
{
AudioObject obj = this.m_audioObjects.QueryById(audioId);
if (obj != null)
{
Play(obj, onPlay);
}
}
public void Play(AudioObject audioObject, Action onPlay = null)
{
if (audioObject.ContainerType == ContainerType.Switch)
{
audioObject = ResolveSwitchContainer(audioObject);
if (audioObject == null)
{
Debug.Log("[AudioSystem] 无法解析Switch Container,检查配置表!");
return;
}
}
this.m_sfxSystem.TryPlay(audioObject, onPlay);
}
public void Play(int audioId)
{
Play((uint)audioId);
}
[Obsolete("Use Play(uint) instead")]
public void Play(string audioName)
{
if (!NameDictionaries.NameToId.TryGetValue(audioName, out uint id))
{
Debug.LogWarning($"[Audio] Name '{audioName}' not found.");
return;
}
if (NameDictionaries.AmbiguousNames.Contains(audioName))
{
Debug.LogWarning(
$"[AudioSystem] Name '{audioName}' is ambiguous. Using first matched ID: {id}. " +
"Use ID instead to avoid unexpected behavior."
);
}
if (NameDictionaries.SharedIdNames.Contains(audioName))
{
Debug.LogWarning(
$"[AudioSystem] Name '{audioName}' is a item of a container AudioObject (ID: {id}). " +
"Use ID instead to avoid unexpected behavior."
);
}
Play(id);
}
public void SetLowpass(bool enable)
{
float target = enable
? AudioExtendSettings.Instance.lowpassEnabledCutoff
: AudioExtendSettings.Instance.lowpassDisabledCutoff;
// Kill existing tween to avoid stacking
if (this.m_lowpassTween != null && this.m_lowpassTween.IsActive())
this.m_lowpassTween.Kill();
// Get current value as start
if (!this.m_mixer.GetFloat(AudioExtendSettings.Instance.lowpassParamName, out float current))
current = target;
float startLog = Mathf.Log10(current);
float endLog = Mathf.Log10(target);
this.m_lowpassTween = DOTween.To(
() => startLog,
x =>
{
startLog = x;
float value = Mathf.Pow(10f, startLog);
this.m_mixer.SetFloat(AudioExtendSettings.Instance.lowpassParamName, value);
},
endLog,
AudioExtendSettings.Instance.lowpassTweenDuration // duration in seconds
).SetEase(Ease.OutCubic);
}
public void SetMusicVolume(float targetVolume)
{
this.m_mixer.SetFloat("MusicVolume", targetVolume);
}
public void SetSFXVolume(float targetVolume)
{
this.m_mixer.SetFloat("SFXVolume", targetVolume);
this.m_mixer.SetFloat("SFXAccentVolume", targetVolume);
}
public void SetMusicState(bool enable)
{
SetMusicVolume(enable ? 0 : -80);
}
public void SetSFXState(bool enable)
{
SetSFXVolume(enable ? 0 : -80);
}
/// <summary>
/// 更新游戏状态,驱动音乐与环境音系统切换。
/// 调用示例:AudioSystem.Instance.SetState(GameState.Game);
/// </summary>
public void SetState<TEnum>(TEnum state) where TEnum : Enum
{
this.m_musicSystem.OnStateChanged(state);
ActiveStates = this.m_musicSystem.ActiveStates;
}
public void PlayOnTrigger(uint audioId, CallbackFlags callbackFlags)
{
Action<uint> callback = null;
callback = (id) =>
{
// 播放音效
Play(audioId);
// 取消订阅,确保只触发一次
switch (callbackFlags)
{
case CallbackFlags.MusicSyncBeat:
OnBeat -= callback;
break;
case CallbackFlags.MusicSyncBar:
OnBar -= callback;
break;
case CallbackFlags.MusicSyncGrid:
OnGrid -= callback;
break;
}
};
// 订阅对应的事件
switch (callbackFlags)
{
case CallbackFlags.MusicSyncBeat:
OnBeat += callback;
break;
case CallbackFlags.MusicSyncBar:
OnBar += callback;
break;
case CallbackFlags.MusicSyncGrid:
OnGrid += callback;
break;
default:
Debug.LogWarning($"[AudioSystem] Unknown callback flag '{callbackFlags}'." +
$"Audio Container with ID {audioId} will be ignored.");
break;
}
}
public void Stop(uint audioId)
{
this.m_sfxSystem.Stop(audioId);
}
/// <summary>
/// 设置指定音频ID的音量
/// </summary>
/// <param name="audioId">音频ID</param>
/// <param name="targetVolume">目标音量 (0.0 - 1.0)</param>
public void SetVolume(uint audioId, float targetVolume)
{
if (targetVolume is < 0 or > 1)
{
Debug.LogWarning($"[AudioSystem] Volume '{targetVolume}' is out of range [0, 1].");
return;
}
this.m_sfxSystem.SetVolume(audioId, targetVolume);
}
/// <summary>
/// 设置指定音频ID的音量
/// </summary>
/// <param name="audioId">音频ID</param>
/// <param name="targetVolume">目标音量 (0.0 - 1.0)</param>
public void SetVolume(int audioId, float targetVolume)
{
SetVolume((uint)audioId, targetVolume);
}
/// <summary>
/// 设置指定音频ID的音高
/// </summary>
/// <param name="audioId">音频ID</param>
/// <param name="targetPitch">目标音高 (通常 1.0 为正常音高, -3 ~ 3)</param>
public void SetPitch(uint audioId, float targetPitch)
{
if (targetPitch is < -3 or > 3)
{
Debug.LogWarning($"[AudioSystem] Pitch '{targetPitch}' is out of range [-3, 3].");
return;
}
this.m_sfxSystem.SetPitch(audioId, targetPitch);
}
/// <summary>
/// 设置指定音频ID的音高
/// </summary>
/// <param name="audioId">音频ID</param>
/// <param name="targetPitch">目标音高 (通常 1.0 为正常音高)</param>
public void SetPitch(int audioId, float targetPitch)
{
SetPitch((uint)audioId, targetPitch);
}
/// <summary>
/// 重置指定音频ID的音量为默认值
/// </summary>
/// <param name="audioId">音频ID</param>
public void ResetVolume(uint audioId)
{
this.m_sfxSystem.ResetVolume(audioId);
}
/// <summary>
/// 重置指定音频ID的音量为默认值
/// </summary>
/// <param name="audioId">音频ID</param>
public void ResetVolume(int audioId)
{
ResetVolume((uint)audioId);
}
/// <summary>
/// 重置指定音频ID的音高为默认值
/// </summary>
/// <param name="audioId">音频ID</param>
public void ResetPitch(uint audioId)
{
this.m_sfxSystem.ResetPitch(audioId);
}
/// <summary>
/// 重置指定音频ID的音高为默认值
/// </summary>
/// <param name="audioId">音频ID</param>
public void ResetPitch(int audioId)
{
ResetPitch((uint)audioId);
}
// ─────────────────────────────────────────────
// 初始化
// ─────────────────────────────────────────────
void Awake()
{
AudioExtendSettings.Instance = this.extendSettings;
if ((bool)Instance && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
this.ResourceLoader = gameObject.AddComponent<ResourceLoader>();
if (AudioExtendSettings.Instance.useAssetBundle)
{
string bundlePath = Path.Combine(Application.streamingAssetsPath,
AudioExtendSettings.Instance.audioBundlePath);
this.ResourceLoader.SetProvider(new AssetBundleAssetProvider(bundlePath));
}
else
{
this.ResourceLoader.SetProvider(new ResourcesAssetProvider());
}
string audioConfigPath = AudioExtendSettings.Instance.audioConfigPath;
this.m_mixer = this.ResourceLoader.LoadSync<AudioMixer>(AudioExtendSettings.Instance.audioMixerPath);
// ── SFX 调度器 ──
// AudioSystem.cs 中
GameObject sfxPoolRoot = new("SfxSourcePool");
sfxPoolRoot.transform.SetParent(transform, false);
AudioSourcePool sfxPool = new(sfxPoolRoot.transform);
this.m_sfxSystem = gameObject.AddComponent<SfxSystem>();
this.m_audioObjects = AudioConfigLoader.Load<AudioObjectConfig>($"{audioConfigPath}/AudioObject");
this.m_audioObjects.PreParseSwitchMappings();
this.m_groups = AudioConfigLoader.Load<AudioGroupConfig> ($"{audioConfigPath}/AudioGroup");
this.m_sfxSystem.Initialize(this.m_groups, this.m_mixer, sfxPool);
// ── 音乐与环境音系统 ──
var segments = AudioConfigLoader.Load<MusicSegmentConfig> ($"{audioConfigPath}/MusicSegment");
var containers = AudioConfigLoader.Load<MusicContainerConfig> ($"{audioConfigPath}/MusicContainer");
var musicPaths = AudioConfigLoader.Load<MusicPathConfig> ($"{audioConfigPath}/MusicPath");
var ambiencePaths = AudioConfigLoader.Load<AmbiencePathConfig> ($"{audioConfigPath}/AmbiencePath");
var musicTransitions = AudioConfigLoader.Load<MusicTransitionConfig> ($"{audioConfigPath}/MusicTransition");
var ambienceTransitions = AudioConfigLoader.Load<AmbienceTransitionConfig> ($"{audioConfigPath}/AmbienceTransition");
//运行时数据验证
segments.Validate();
containers.Validate(segments);
// MusicSystem 需要运行协程,作为 MonoBehaviour 挂载在同一 GameObject 上
this.m_musicSystem = gameObject.AddComponent<MusicSystem>();
// AudioSourcePool 由 MusicSystem 独占一个子节点,与 SFX pool 隔离
GameObject musicPoolRoot = new("MusicSourcePool");
musicPoolRoot.transform.SetParent(transform, false);
AudioMixerGroup musicGroup = this.m_mixer.FindMatchingGroups(AudioExtendSettings.Instance.musicGroupPath)[0];
AudioSourcePool musicPool = new(musicPoolRoot.transform, musicGroup);
GameObject ambiencePoolRoot = new("AmbienceSourcePool");
ambiencePoolRoot.transform.SetParent(transform, false);
AudioMixerGroup ambienceGroup = this.m_mixer.FindMatchingGroups(AudioExtendSettings.Instance.ambienceGroupPath)[0];
AudioSourcePool ambiencePool = new(ambiencePoolRoot.transform, ambienceGroup);
this.m_musicSystem.Initialize(
segments,
containers,
musicPaths,
ambiencePaths,
musicTransitions,
ambienceTransitions,
musicPool,
ambiencePool);
this.m_musicSystem.OnBeat += id => this.OnBeat?.Invoke(id);
this.m_musicSystem.OnBar += id => this.OnBar?.Invoke(id);
this.m_musicSystem.OnGrid += id => this.OnGrid?.Invoke(id);
// ── 注册 StateGroup ──
Parameters.EnumIds.RegisterAllGameState();
ActiveStates = new Dictionary<Type, Enum>();
//引入HapticSystem
OnAwakeComplete();
}
void Start()
{
// Debug.Log("[AudioSystem] Start");
// ── 启动默认音乐与环境音 ──
// 触发一次初始状态,让音乐系统从默认状态开始匹配
if (this.startWithMusic)
{
SetState(this.startMusicWith);
}
}
AudioObject ResolveSwitchContainer(AudioObject switchContainer)
{
// 遍历 ActiveStates 找到 TypeId 匹配的枚举类型
Enum currentStateValue = null;
bool foundGroup = false;
foreach (KeyValuePair<Type, Enum> keyValuePair in ActiveStates)
{
if (StateGroupRegistry.GetTypeId(keyValuePair.Key) != switchContainer.SwitchGroupId) continue;
currentStateValue = keyValuePair.Value;
foundGroup = true;
break;
}
if (!foundGroup)
{
Debug.LogWarning($"[AudioSystem] Switch Container {switchContainer.Id} 找不到 TypeId={switchContainer.SwitchGroupId} 对应的状态组。");
return this.m_audioObjects.GetDefaultSwitchOrFallback(switchContainer);
}
// 解析AudioObject对象
AudioObject childContainer = this.m_audioObjects.GetMappingResult(switchContainer.Id, currentStateValue);
return childContainer ?? this.m_audioObjects.GetDefaultSwitchOrFallback(switchContainer);
}
}
public static class AudioConfigLoader
{
public static T Load<T>(string configPath, string fileName)
where T : IBinarySerializable, new()
{
string path = Path.Combine(
Application.dataPath, "Resources", configPath, fileName);
if (!File.Exists(path))
{
Debug.LogError($"[AudioImportTool] 找不到配置文件: {path}");
return default;
}
try
{
T config = new();
using MemoryStream ms = new(File.ReadAllBytes(path));
using BinaryReader reader = new(ms);
config.DeSerialize(reader);
return config;
}
catch (Exception e)
{
Debug.LogError($"[AudioImportTool] 配置表反序列化失败 {fileName}: {e.Message}");
return default;
}
}
public static T Load<T>(string tableName) where T : IBinarySerializable, new()
{
TextAsset bytes = AudioSystem.Instance.ResourceLoader.LoadSync<TextAsset>(tableName);
if (!bytes)
{
Debug.LogError($"未找到表 {tableName}");
return default;
}
IBinarySerializable data = new T();
bool readOk = FileManager.ReadBinaryDataFromBytes(bytes.bytes, ref data);
if (readOk)
return (T)data;
Debug.LogError($"{tableName} 解析出错,类型 {typeof(T)}");
return default;
}
}
}