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 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 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 ? 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); } /// /// 更新游戏状态,驱动音乐与环境音系统切换。 /// 调用示例: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); } /// /// 设置指定音频ID的音量 /// /// 音频ID /// 目标音量 (0.0 - 1.0) 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); } /// /// 设置指定音频ID的音量 /// /// 音频ID /// 目标音量 (0.0 - 1.0) public void SetVolume(int audioId, float targetVolume) { SetVolume((uint)audioId, targetVolume); } /// /// 设置指定音频ID的音高 /// /// 音频ID /// 目标音高 (通常 1.0 为正常音高, -3 ~ 3) 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); } /// /// 设置指定音频ID的音高 /// /// 音频ID /// 目标音高 (通常 1.0 为正常音高) public void SetPitch(int audioId, float targetPitch) { SetPitch((uint)audioId, targetPitch); } /// /// 重置指定音频ID的音量为默认值 /// /// 音频ID public void ResetVolume(uint audioId) { this.m_sfxSystem.ResetVolume(audioId); } /// /// 重置指定音频ID的音量为默认值 /// /// 音频ID public void ResetVolume(int audioId) { ResetVolume((uint)audioId); } /// /// 重置指定音频ID的音高为默认值 /// /// 音频ID public void ResetPitch(uint audioId) { this.m_sfxSystem.ResetPitch(audioId); } /// /// 重置指定音频ID的音高为默认值 /// /// 音频ID 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(); 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(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(); this.m_audioObjects = AudioConfigLoader.Load($"{audioConfigPath}/AudioObject"); this.m_audioObjects.PreParseSwitchMappings(); this.m_groups = AudioConfigLoader.Load ($"{audioConfigPath}/AudioGroup"); this.m_sfxSystem.Initialize(this.m_groups, this.m_mixer, sfxPool); // ── 音乐与环境音系统 ── var segments = AudioConfigLoader.Load ($"{audioConfigPath}/MusicSegment"); var containers = AudioConfigLoader.Load ($"{audioConfigPath}/MusicContainer"); var musicPaths = AudioConfigLoader.Load ($"{audioConfigPath}/MusicPath"); var ambiencePaths = AudioConfigLoader.Load ($"{audioConfigPath}/AmbiencePath"); var musicTransitions = AudioConfigLoader.Load ($"{audioConfigPath}/MusicTransition"); var ambienceTransitions = AudioConfigLoader.Load ($"{audioConfigPath}/AmbienceTransition"); //运行时数据验证 segments.Validate(); containers.Validate(segments); // 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(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(); //引入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 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(); 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 = AudioSystem.Instance.ResourceLoader.LoadSync(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; } } }