Files
aibis-dream/Assets/Scripts/Framework/AudioKit/AudioManager.cs
T
2026-08-05 21:13:43 +08:00

595 lines
19 KiB
C#

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<AudioManager>
{
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<string, EventInstance> _musicDict = new();
private readonly Dictionary<string, EventInstance> _sfxDict = new();
private YarnGlobalParameterRegistry _yarnGlobalParameters;
#endregion
#region 事件索引
private List<IUnRegister> _removeTrigger = new();
private AmbState _currentAmbState = AmbState.Main;
private AmbState? _ambStateBeforePauseMenu;
public AmbState CurrentAmbState => _currentAmbState;
private YarnGlobalParameterRegistry YarnGlobalParameters =>
_yarnGlobalParameters ??= new YarnGlobalParameterRegistry(
new RuntimeFmodGlobalParameterApi());
#endregion
public override void OnSingletonInit()
{
RegisterEvent();
}
private void Start()
{
InitAmb();
}
private void RegisterEvent()
{
// 语音相关
_removeTrigger.Add(EnumEventSystem.Global.Register<DialogEventEnum, LineInfo>(DialogEventEnum.LineStart, OnLineStart));
_removeTrigger.Add(EnumEventSystem.Global.Register(DialogEventEnum.LineShown, OnLineShown));
// 场景切换相关
_removeTrigger.Add(EnumEventSystem.Global.Register<EventEnum, string>(EventEnum.SceneLoad, OnSceneLoad));
_removeTrigger.Add(EnumEventSystem.Global.Register<EventEnum, FixSceneMode>(EventEnum.FixSceneModeChanged, OnFixSceneModeChange));
// 主菜单 / 退出局内
_removeTrigger.Add(EnumEventSystem.Global.Register(GameLifecycleEvent.ApplicationReady, OnAppStart));
_removeTrigger.Add(EnumEventSystem.Global.Register(GameLifecycleEvent.SessionStarted, OnGameStart));
_removeTrigger.Add(EnumEventSystem.Global.Register(GameLifecycleEvent.SessionEnded, OnGameQuit));
_removeTrigger.Add(EnumEventSystem.Global.Register(GameLifecycleEvent.SessionPaused, PauseAll));
_removeTrigger.Add(EnumEventSystem.Global.Register(GameLifecycleEvent.SessionResumed, UnPauseAll));
_removeTrigger.Add(EnumEventSystem.Global.Register<TerminalUIEvent, TerminalMainMenuPhase>(
TerminalUIEvent.MainMenuPhase, OnMainMenuPhase));
_removeTrigger.Add(EnumEventSystem.Global.Register(TerminalUIEvent.PauseMenuAmbEnter, OnPauseMenuAmbEnter));
_removeTrigger.Add(EnumEventSystem.Global.Register<TerminalUIEvent, bool>(
TerminalUIEvent.PauseMenuAmbExit, OnPauseMenuAmbExit));
SingleCastEventSystem.Global.Register<TerminalUIAudioEnum>(TerminalUIAudioEnum.Hover, OnTerminalUIHover);
SingleCastEventSystem.Global.Register<TerminalUIAudioEnum>(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(),
yarnGlobalParameters = YarnGlobalParameters.CaptureFinalValues(Debug.LogWarning)
};
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, Action<string> warn = null)
{
warn ??= Debug.LogWarning;
YarnGlobalParameters.ResetToDefaults(warn);
foreach (var musicEvent in _musicDict.Values)
{
musicEvent.stop(STOP_MODE.IMMEDIATE);
musicEvent.release();
}
foreach (var sfxEvent in _sfxDict.Values)
{
sfxEvent.stop(STOP_MODE.IMMEDIATE);
sfxEvent.release();
}
_musicDict.Clear();
_sfxDict.Clear();
if (!string.IsNullOrEmpty(data.ambState)
&& Enum.TryParse<AmbState>(data.ambState, out var ambState))
{
ChangeAmb(ambState);
}
YarnGlobalParameters.RestoreFinalValues(data.yarnGlobalParameters, warn);
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;
}
}
}
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 SetYarnGlobalParam(string paramName, float value)
{
var result = YarnGlobalParameters.SetFromYarn(paramName, value);
if (result != RESULT.OK)
{
Debug.LogWarning(
$"[AudioManager] Yarn 全局参数 {paramName ?? "(null)"} 设置失败:{result}。");
}
}
public void SetMainMenuStage(int stage)
{
SetGlobalParam(MainMenuStageParamName, stage);
}
private void OnAppStart()
{
ChangeAmb(menuAmbState);
}
private void OnMainMenuPhase(TerminalMainMenuPhase phase)
{
SetMainMenuStage((int)phase);
}
private void OnGameStart()
{
YarnGlobalParameters.ResetToDefaults(Debug.LogWarning);
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();
YarnGlobalParameters.ResetToDefaults(Debug.LogWarning);
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, float pitch = 1f)
{
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);
}
if (!Mathf.Approximately(pitch, 1f))
{
newSfx.setPitch(Mathf.Max(0.01f, pitch));
}
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)
{
normalizedVolume = Mathf.Clamp01(normalizedVolume);
// 最低档需要真正静音;有限的最小分贝值仍会保留可听的线性幅度。
float linearVolume = 0f;
if (normalizedVolume > 0f)
{
// 将非零音量映射到 minDecibels ~ maxDecibels,保留原有的对数听感曲线。
float db = Mathf.Lerp(minDecibels, maxDecibels, normalizedVolume);
linearVolume = Mathf.Pow(10f, db / 20f);
}
var masterBus = RuntimeManager.GetBus("bus:/");
masterBus.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,
FixSceneMode.Sales => AmbState.Sale,
FixSceneMode.AnalysisMode => AmbState.Log,
_ => 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();
}
}