using System; using System.Collections; using System.Collections.Generic; using AibisDream; using AibisDream.FixSystem; using AibisDream.Framework; using AibisDream.SaveSystem; using AibisDream.UI; using FMOD; using FMOD.Studio; using FMODUnity; using UnityEngine; using STOP_MODE = FMOD.Studio.STOP_MODE; using Debug = UnityEngine.Debug; namespace AibisDream.Kit { public class AudioManager : Singleton { private const string AmbStateName = "scene"; private const string MainMenuStageParamName = "mainMenuStage"; public EventReference ambEvent; [Header("AMB Settings")] [SerializeField] private AmbState menuAmbState = AmbState.Outside; [Header("Volume Settings")] [Tooltip("Minimum volume in dB when slider is at 0")] [SerializeField] private float minDecibels = -60f; [Tooltip("Maximum volume in dB when slider is at 1")] [SerializeField] private float maxDecibels = 0f; [Header("Terminal UI")] [SerializeField] private string terminalHoverSfx = "event:/ActionFB/menu_hover"; [SerializeField] private string terminalClickSfx = "event:/ActionFB/menu_click"; #region Event实例 private EventInstance _ambInstance; private EventInstance _voiceInstance; private readonly Dictionary _musicDict = new(); private readonly Dictionary _sfxDict = new(); #endregion #region 事件索引 private List _removeTrigger = new(); private AmbState _currentAmbState = AmbState.Main; private AmbState? _ambStateBeforePauseMenu; public AmbState CurrentAmbState => _currentAmbState; #endregion public override void OnSingletonInit() { RegisterEvent(); } private void Start() { InitAmb(); } private void RegisterEvent() { // 语音相关 _removeTrigger.Add(EnumEventSystem.Global.Register(DialogEventEnum.LineStart, OnLineStart)); _removeTrigger.Add(EnumEventSystem.Global.Register(DialogEventEnum.LineShown, OnLineShown)); // 场景切换相关 _removeTrigger.Add(EnumEventSystem.Global.Register(EventEnum.SceneLoad, OnSceneLoad)); _removeTrigger.Add(EnumEventSystem.Global.Register(EventEnum.FixSceneModeChanged, OnFixSceneModeChange)); // 主菜单 / 退出局内 _removeTrigger.Add(EnumEventSystem.Global.Register(GameLoopEnum.AppStart, OnAppStart)); _removeTrigger.Add(EnumEventSystem.Global.Register(GameLoopEnum.GameStart, OnGameStart)); _removeTrigger.Add(EnumEventSystem.Global.Register(GameLoopEnum.GameQuit, OnGameQuit)); _removeTrigger.Add(EnumEventSystem.Global.Register( TerminalUIEvent.MainMenuPhase, OnMainMenuPhase)); _removeTrigger.Add(EnumEventSystem.Global.Register(TerminalUIEvent.PauseMenuAmbEnter, OnPauseMenuAmbEnter)); _removeTrigger.Add(EnumEventSystem.Global.Register( TerminalUIEvent.PauseMenuAmbExit, OnPauseMenuAmbExit)); SingleCastEventSystem.Global.Register(TerminalUIAudioEnum.Hover, OnTerminalUIHover); SingleCastEventSystem.Global.Register(TerminalUIAudioEnum.Click, OnTerminalUIClick); } public override void OnSingletonDestroy() { UnRegisterEvent(); } private void UnRegisterEvent() { foreach (var trigger in _removeTrigger) { trigger.UnRegister(); } _removeTrigger.Clear(); SingleCastEventSystem.Global.Unregister(TerminalUIAudioEnum.Hover); SingleCastEventSystem.Global.Unregister(TerminalUIAudioEnum.Click); } private void OnTerminalUIHover() { PlaySfx(terminalHoverSfx); } private void OnTerminalUIClick() { PlaySfx(terminalClickSfx); } public AudioSnapshotDto CaptureSnapshot() { var ambState = _currentAmbState == AmbState.PauseMenu && _ambStateBeforePauseMenu.HasValue ? _ambStateBeforePauseMenu.Value : _currentAmbState; var dto = new AudioSnapshotDto { ambState = ambState.ToString() }; foreach (var pair in _musicDict) { pair.Value.GetDescription().getPath(out var path); dto.musicPaths[pair.Key] = path; } foreach (var pair in _sfxDict) { pair.Value.GetDescription().getPath(out var path); dto.sfxPaths[pair.Key] = path; } return dto; } public void RestoreSnapshot(AudioSnapshotDto data) { foreach (var musicEvent in _musicDict.Values) { musicEvent.release(); } foreach (var sfxEvent in _sfxDict.Values) { sfxEvent.release(); } _musicDict.Clear(); _sfxDict.Clear(); if (data.musicPaths != null) { foreach (var music in data.musicPaths) { var instance = RuntimeManager.CreateInstance(music.Value); instance.start(); _musicDict[music.Key] = instance; } } if (data.sfxPaths != null) { foreach (var sfx in data.sfxPaths) { var instance = RuntimeManager.CreateInstance(sfx.Value); instance.start(); _sfxDict[sfx.Key] = instance; } } if (!string.IsNullOrEmpty(data.ambState) && Enum.TryParse(data.ambState, out var ambState)) { ChangeAmb(ambState); } } public void PauseAll() { foreach (var sfx in _sfxDict.Values) { sfx.setPaused(true); } _voiceInstance.setPaused(true); } public void UnPauseAll() { foreach (var sfx in _sfxDict.Values) { sfx.setPaused(false); } _voiceInstance.setPaused(false); } #region AMB相关 private void InitAmb() { _ambInstance = RuntimeManager.CreateInstance(ambEvent); _ambInstance.start(); } public void ChangeAmb(AmbState state) { _currentAmbState = state; RuntimeManager.StudioSystem.setParameterByName(AmbStateName, (float)state); } private void OnPauseMenuAmbEnter() { if (_currentAmbState == AmbState.PauseMenu) { return; } _ambStateBeforePauseMenu = _currentAmbState; ChangeAmb(AmbState.PauseMenu); } private void OnPauseMenuAmbExit(bool restorePrevious) { if (!_ambStateBeforePauseMenu.HasValue) { return; } if (restorePrevious) { ChangeAmb(_ambStateBeforePauseMenu.Value); } _ambStateBeforePauseMenu = null; } public void SetGlobalParam(string paramName, float value) { RuntimeManager.StudioSystem.setParameterByName(paramName, value); } public void SetMainMenuStage(int stage) { SetGlobalParam(MainMenuStageParamName, stage); } private void OnAppStart() { ChangeAmb(menuAmbState); } private void OnMainMenuPhase(TerminalMainMenuPhase phase) { SetMainMenuStage((int)phase); } private void OnGameStart() { SetMainMenuStage(3); } private void OnSceneLoad(string loadingScene) { // 场景加载时切换AMB var state = MatchAmbState(loadingScene); ChangeAmb(state); } private void OnGameQuit() { // 清除当前播放的所有东西 foreach (var music in _musicDict.Values) { music.stop(STOP_MODE.IMMEDIATE); music.release(); } foreach (var sfx in _sfxDict.Values) { sfx.stop(STOP_MODE.IMMEDIATE); sfx.release(); } if (_voiceInstance.isValid()) { _voiceInstance.stop(STOP_MODE.IMMEDIATE); _voiceInstance.release(); } _musicDict.Clear(); _sfxDict.Clear(); ChangeAmb(menuAmbState); } private void OnFixSceneModeChange(FixSceneMode nextState) { // 状态切换时加载 var state = MatchAmbState(nextState); Debug.Log("OnFixSceneModeChange: " + state); ChangeAmb(state); } #endregion #region 音乐相关 public void PlayMusic(string eventName, string audioKey = null) { var eventPath = FormatEventPath(eventName); var key = string.IsNullOrEmpty(audioKey) ? eventName : audioKey; if (_musicDict.TryGetValue(key, out var oldMusic)) { oldMusic.stop(STOP_MODE.IMMEDIATE); oldMusic.release(); RemoveMusic(key); } var newMusic = RuntimeManager.CreateInstance(eventPath); AddMusic(key, newMusic); newMusic.start(); } public void StopMusic(string key, STOP_MODE stopMode) { if (_musicDict.TryGetValue(key, out var oldMusic)) { oldMusic.stop(stopMode); oldMusic.release(); RemoveMusic(key); } } public void SetMusicParam(string key, string param, float paramValue) { if (_musicDict.TryGetValue(key, out var music)) { music.setParameterByName(param, paramValue); } } private void RemoveMusic(string key) { _musicDict.Remove(key); } private void AddMusic(string key, EventInstance eventInstance) { _musicDict[key] = eventInstance; } #endregion public void OnLineStart(LineInfo dialogText) { if (_voiceInstance.IsPlaying()) { _voiceInstance.stop(STOP_MODE.IMMEDIATE); _voiceInstance.release(); } if (string.IsNullOrEmpty(dialogText.character.voicePath) || !FModEx.IsEventExist(dialogText.character.VoicePath)) { return; } _voiceInstance = RuntimeManager.CreateInstance(dialogText.character.VoicePath); _voiceInstance.start(); } public void OnLineShown() { if (_voiceInstance.isValid()) { _voiceInstance.stop(STOP_MODE.IMMEDIATE); _voiceInstance.release(); } } public void PlaySfx(string eventName, string audioKey = null) { var eventPath = FormatEventPath(eventName); var key = string.IsNullOrEmpty(audioKey) ? eventName : audioKey; if (_sfxDict.TryGetValue(key, out var oldSfx)) { oldSfx.stop(STOP_MODE.IMMEDIATE); oldSfx.release(); RemoveSfx(key); } var newSfx = RuntimeManager.CreateInstance(eventPath); if (newSfx.GetDescription().isOneshot(out var isOneshot) != RESULT.OK || !isOneshot) { AddSfx(key, newSfx); } newSfx.start(); } public void StopSfx(string key, STOP_MODE stopMode=STOP_MODE.IMMEDIATE) { if (_sfxDict.TryGetValue(key, out var oldSfx)) { oldSfx.stop(stopMode); oldSfx.release(); RemoveSfx(key); } } public void SetSfxParam(string key, string paramName, float paramValue) { if (_sfxDict.TryGetValue(key, out var sfx)) { sfx.setParameterByName(paramName, paramValue); } } private void RemoveSfx(string key) { _sfxDict.Remove(key); } private void AddSfx(string key, EventInstance eventInstance) { _sfxDict[key] = eventInstance; } #region 调整音量 public void SetVolume(float normalizedVolume) { // 将 0~1 映射到 minDecibels ~ maxDecibels,再转换为线性幅度 // 使用对数曲线匹配人耳听觉特性 float db = Mathf.Lerp(minDecibels, maxDecibels, normalizedVolume); float linearVolume = Mathf.Pow(10f, db / 20f); var masterVca = RuntimeManager.GetBus("bus:/"); masterVca.setVolume(linearVolume); } #endregion #region 一些静态方法 private static string FormatEventPath(string eventName) { return eventName.StartsWith("event:/") ? eventName : $"event:/{eventName}"; } private static AmbState MatchAmbState(string key) { return key switch { "Scene/ExpressFixScene" => AmbState.Clinic, "Scene/PeipeiFixScene" => AmbState.Clinic, "Scene/OpenFixScene" => AmbState.Empty, "Scene/HuoShanFixScene" => AmbState.Clinic, "Scene/Bridge" => AmbState.Footbridge, "Scene/SubWay" => AmbState.Subway, "Scene/ClinicOut" => AmbState.Outside, "Scene/梦境" => AmbState.Dream, "Scene/酒吧场景" => AmbState.Bar, _ => AmbState.Main }; } private static AmbState MatchAmbState(FixSceneMode fixState) { return fixState switch { FixSceneMode.Clinic => AmbState.Clinic, FixSceneMode.BodyModule => AmbState.Body, FixSceneMode.EmoCut => AmbState.Head, FixSceneMode.MemoryCut => AmbState.Head, FixSceneMode.LogicCut => AmbState.Head, FixSceneMode.UF => AmbState.UF, FixSceneMode.Eye => AmbState.Eye, FixSceneMode.Memory => AmbState.Memory, _ => AmbState.Main }; } #endregion public void StopSfxWithDelay(string key, string paramName, float paramValue, float delay, STOP_MODE stopMode = STOP_MODE.ALLOWFADEOUT) { if (_sfxDict.TryGetValue(key, out var sfx)) { StartCoroutine(DelayedStop(sfx, key, paramName, paramValue, delay, stopMode)); } } private IEnumerator DelayedStop(EventInstance instance, string key, string paramName, float paramValue, float delay, STOP_MODE stopMode) { instance.setParameterByName(paramName, paramValue); yield return new WaitForSeconds(delay); instance.stop(stopMode); instance.release(); RemoveSfx(key); } public void StopMusicWithDelay(string key, string paramName, float paramValue, float delay, STOP_MODE stopMode = STOP_MODE.ALLOWFADEOUT) { if (_musicDict.TryGetValue(key, out var music)) { StartCoroutine(DelayedStopMusic(music, key, paramName, paramValue, delay, stopMode)); } } private IEnumerator DelayedStopMusic(EventInstance instance, string key, string paramName, float paramValue, float delay, STOP_MODE stopMode) { instance.setParameterByName(paramName, paramValue); yield return new WaitForSeconds(delay); instance.stop(stopMode); instance.release(); RemoveMusic(key); } } public interface IAudioManager { // Yarn音频接口 public void PlayMusic(string eventName, string audioKey); public void StopMusic(string eventName, STOP_MODE stopMode); public void SetMusicParam(string eventName, string paramName, float paramValue); // AMB参数变化 public void ChangeAmb(AmbState state); public void SetGlobalParam(string paramName, float value); // 语音相关 public void OnLineStart(DialogText dialogText); public void OnLineShown(); // 交互相关 public void PlaySfx(string eventName, string audioKey); public void StopSfx(string eventName, STOP_MODE stopMode); public void SetSfxParam(string eventName, string paramName, float paramValue); // 暂停 public void PauseAll(); public void UnPauseAll(); } }