1df0666c91
- 新增 统一 `ResourceLoader`类,负责同步/异步加载资源,并对已加载的资源缓存。 - 修改 `MusicTransition`, `AudioSystem`, `LongAudioContainerPlayer`, `MusicSegment`, 'SfxSystem`, `HapticSystem`使用最新的`ResourceLoader`。
433 lines
16 KiB
C#
433 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using UnityEngine;
|
|
using UnityEngine.Audio;
|
|
using DG.Tweening;
|
|
|
|
namespace OCES.Audio
|
|
{
|
|
public 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;
|
|
|
|
const string k_audioConfigPath = "AudioData";
|
|
const string k_audioResourcePath = "Audios";
|
|
|
|
SfxSystem m_sfxSystem;
|
|
MusicSystem m_musicSystem;
|
|
|
|
AudioObjectConfig m_audioObjects;
|
|
AudioGroupConfig m_groups;
|
|
AudioMixer m_mixer;
|
|
Tween m_lowpassTween;
|
|
|
|
|
|
// ─────────────────────────────────────────────
|
|
// 公开接口
|
|
// ─────────────────────────────────────────────
|
|
|
|
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 ? 440f : 22000f;
|
|
|
|
// 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("LowpassFreq", 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("LowpassFreq", value);
|
|
},
|
|
endLog,
|
|
0.2f // duration in seconds
|
|
).SetEase(Ease.OutCubic);
|
|
}
|
|
|
|
/// <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()
|
|
{
|
|
if ((bool)Instance && Instance != this)
|
|
{
|
|
Destroy(gameObject);
|
|
return;
|
|
}
|
|
Instance = this;
|
|
DontDestroyOnLoad(gameObject);
|
|
|
|
this.ResourceLoader = gameObject.AddComponent<ResourceLoader>();
|
|
|
|
this.m_mixer = this.ResourceLoader.LoadSync<AudioMixer>("Audios/Master");
|
|
|
|
// ── SFX 调度器 ──
|
|
// AudioSystem.cs 中
|
|
GameObject sfxPoolRoot = new("SfxSourcePool");
|
|
sfxPoolRoot.transform.SetParent(transform, false);
|
|
AudioSourcePool sfxPool = new(sfxPoolRoot.transform); // 不传 mixer group,让 SfxSystem 自己设置
|
|
this.m_sfxSystem = gameObject.AddComponent<SfxSystem>();
|
|
this.m_audioObjects = AudioConfigLoader.Load<AudioObjectConfig>($"{k_audioConfigPath}/AudioObject");
|
|
this.m_audioObjects.PreParseSwitchMappings();
|
|
this.m_groups = AudioConfigLoader.Load<AudioGroupConfig>($"{k_audioConfigPath}/AudioGroup");
|
|
this.m_sfxSystem.Initialize(this.m_groups, this.m_mixer, sfxPool); // 传入 pool
|
|
|
|
|
|
// ── 音乐与环境音系统 ──
|
|
var segments = AudioConfigLoader.Load<MusicSegmentConfig>($"{k_audioConfigPath}/MusicSegment");
|
|
var containers = AudioConfigLoader.Load<MusicContainerConfig>($"{k_audioConfigPath}/MusicContainer");
|
|
var musicPaths = AudioConfigLoader.Load<MusicPathConfig>($"{k_audioConfigPath}/MusicPath");
|
|
var ambiencePaths = AudioConfigLoader.Load<AmbiencePathConfig>($"{k_audioConfigPath}/AmbiencePath");
|
|
var musicTransitions = AudioConfigLoader.Load<MusicTransitionConfig>($"{k_audioConfigPath}/MusicTransition");
|
|
var ambienceTransitions = AudioConfigLoader.Load<AmbienceTransitionConfig>($"{k_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("Master/Regular/Music")[0];
|
|
AudioSourcePool musicPool = new(musicPoolRoot.transform, musicGroup);
|
|
|
|
GameObject ambiencePoolRoot = new("AmbienceSourcePool");
|
|
ambiencePoolRoot.transform.SetParent(transform, false);
|
|
AudioMixerGroup ambienceGroup = this.m_mixer.FindMatchingGroups("Master/Regular/SFX/Ambience")[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>();
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|