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 ConsoleLogLevel logLevel; // ReSharper disable once MemberCanBePrivate.Global public IReadOnlyDictionary ActiveStates { get; private set; } public enum ConsoleLogLevel { Off, Log, Warning, Error } //TODO 实现这个功能 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 OnBeat; public event Action OnBar; public event Action 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); } /// /// 更新游戏状态,驱动音乐与环境音系统切换。 /// 调用示例:AudioSystem.Instance.SetState(GameState.Game); /// public void SetState(TEnum state) where TEnum : Enum { this.m_musicSystem.OnStateChanged(state); ActiveStates = this.m_musicSystem.ActiveStates; } public void PlayOnTrigger(uint audioId, CallbackFlags callbackFlags) { Action 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); } // ───────────────────────────────────────────── // 初始化 // ───────────────────────────────────────────── void Awake() { if ((bool)Instance && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); this.m_mixer = Resources.Load("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(); this.m_audioObjects = AudioConfigLoader.Load($"{k_audioConfigPath}/AudioObject"); this.m_audioObjects.PreParseSwitchMappings(); this.m_groups = AudioConfigLoader.Load($"{k_audioConfigPath}/AudioGroup"); this.m_sfxSystem.Initialize(this.m_groups, this.m_mixer, sfxPool); // 传入 pool // ── 音乐与环境音系统 ── var segments = AudioConfigLoader.Load($"{k_audioConfigPath}/MusicSegment"); var containers = AudioConfigLoader.Load($"{k_audioConfigPath}/MusicContainer"); var musicPaths = AudioConfigLoader.Load($"{k_audioConfigPath}/MusicPath"); var ambiencePaths = AudioConfigLoader.Load($"{k_audioConfigPath}/AmbiencePath"); var musicTransitions = AudioConfigLoader.Load($"{k_audioConfigPath}/MusicTransition"); var ambienceTransitions = AudioConfigLoader.Load($"{k_audioConfigPath}/AmbienceTransition"); // MusicSystem 需要运行协程,作为 MonoBehaviour 挂载在同一 GameObject 上 this.m_musicSystem = gameObject.AddComponent(); // 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(); } void Start() { // ── 启动默认音乐与环境音 ── // 触发一次初始状态,让音乐系统从默认状态开始匹配 if (this.startWithMusic) { SetState(Parameters.GameState.Home); } } AudioObject ResolveSwitchContainer(AudioObject switchContainer) { // 遍历 ActiveStates 找到 TypeId 匹配的枚举类型 Enum currentStateValue = null; bool foundGroup = false; foreach (KeyValuePair 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(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 T(); 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(string tableName) where T : IBinarySerializable, new() { TextAsset bytes = Resources.Load(tableName); if (!bytes) Debug.LogError($"未找到表 {tableName}"); IBinarySerializable data = new T(); bool readOk = FileManager.ReadBinaryDataFromBytes(bytes.bytes, ref data); if (readOk) return (T)data; Debug.LogError($"{tableName} 解析出错,类型 {typeof(T)}"); return default; } class AudioObjectArrayWrapper { public T[] AudioObjects; } } }