feat: 音频参数保存功能

This commit is contained in:
2026-07-28 01:12:22 +08:00
parent 2902c554a6
commit 22a0d7377d
8 changed files with 455 additions and 13 deletions
@@ -42,6 +42,7 @@ namespace AibisDream.Kit
private EventInstance _voiceInstance;
private readonly Dictionary<string, EventInstance> _musicDict = new();
private readonly Dictionary<string, EventInstance> _sfxDict = new();
private YarnGlobalParameterRegistry _yarnGlobalParameters;
#endregion
@@ -52,6 +53,9 @@ namespace AibisDream.Kit
private AmbState? _ambStateBeforePauseMenu;
public AmbState CurrentAmbState => _currentAmbState;
private YarnGlobalParameterRegistry YarnGlobalParameters =>
_yarnGlobalParameters ??= new YarnGlobalParameterRegistry(
new RuntimeFmodGlobalParameterApi());
#endregion
@@ -124,7 +128,8 @@ namespace AibisDream.Kit
var dto = new AudioSnapshotDto
{
ambState = ambState.ToString()
ambState = ambState.ToString(),
yarnGlobalParameters = YarnGlobalParameters.CaptureFinalValues(Debug.LogWarning)
};
foreach (var pair in _musicDict)
@@ -142,21 +147,34 @@ namespace AibisDream.Kit
return dto;
}
public void RestoreSnapshot(AudioSnapshotDto data)
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)
@@ -176,13 +194,6 @@ namespace AibisDream.Kit
_sfxDict[sfx.Key] = instance;
}
}
if (!string.IsNullOrEmpty(data.ambState)
&& Enum.TryParse<AmbState>(data.ambState, out var ambState))
{
ChangeAmb(ambState);
}
}
public void PauseAll()
@@ -250,6 +261,16 @@ namespace AibisDream.Kit
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);
@@ -267,6 +288,7 @@ namespace AibisDream.Kit
private void OnGameStart()
{
YarnGlobalParameters.ResetToDefaults(Debug.LogWarning);
SetMainMenuStage(3);
}
@@ -300,6 +322,7 @@ namespace AibisDream.Kit
_musicDict.Clear();
_sfxDict.Clear();
YarnGlobalParameters.ResetToDefaults(Debug.LogWarning);
ChangeAmb(menuAmbState);
}
@@ -66,7 +66,7 @@ namespace AibisDream.Kit
[YarnCommand("set_global_param")]
public static void SetGlobalParam(string paramName, float value)
{
AudioManager.Instance.SetGlobalParam(paramName, value);
AudioManager.Instance.SetYarnGlobalParam(paramName, value);
}
}
}
}
@@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using FMOD;
using FMOD.Studio;
using FMODUnity;
namespace AibisDream.Kit
{
internal interface IFmodGlobalParameterApi
{
RESULT SetParameterByName(string name, float value, bool ignoreSeekSpeed);
RESULT GetParameterByName(string name, out float value, out float finalValue);
RESULT GetParameterDescriptionByName(string name, out PARAMETER_DESCRIPTION description);
RESULT SetParameterById(PARAMETER_ID id, float value, bool ignoreSeekSpeed);
}
internal sealed class RuntimeFmodGlobalParameterApi : IFmodGlobalParameterApi
{
public RESULT SetParameterByName(string name, float value, bool ignoreSeekSpeed)
{
return RuntimeManager.StudioSystem.setParameterByName(name, value, ignoreSeekSpeed);
}
public RESULT GetParameterByName(string name, out float value, out float finalValue)
{
return RuntimeManager.StudioSystem.getParameterByName(name, out value, out finalValue);
}
public RESULT GetParameterDescriptionByName(
string name,
out PARAMETER_DESCRIPTION description)
{
return RuntimeManager.StudioSystem.getParameterDescriptionByName(name, out description);
}
public RESULT SetParameterById(PARAMETER_ID id, float value, bool ignoreSeekSpeed)
{
return RuntimeManager.StudioSystem.setParameterByID(id, value, ignoreSeekSpeed);
}
}
/// <summary>
/// 只记录曾被 Yarn set_global_param 成功修改的参数名。
/// 参数值与默认值始终以 FMOD Studio System 为唯一来源。
/// </summary>
internal sealed class YarnGlobalParameterRegistry
{
private readonly IFmodGlobalParameterApi _api;
private readonly HashSet<string> _trackedNames =
new(StringComparer.OrdinalIgnoreCase);
internal YarnGlobalParameterRegistry(IFmodGlobalParameterApi api)
{
_api = api ?? throw new ArgumentNullException(nameof(api));
}
internal IReadOnlyCollection<string> TrackedNames => _trackedNames;
internal RESULT SetFromYarn(string name, float value)
{
if (string.IsNullOrWhiteSpace(name))
{
return RESULT.ERR_INVALID_PARAM;
}
var result = _api.SetParameterByName(name, value, ignoreSeekSpeed: false);
if (result == RESULT.OK)
{
_trackedNames.Add(name);
}
return result;
}
internal Dictionary<string, float> CaptureFinalValues(Action<string> warn)
{
var values = new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase);
foreach (var name in _trackedNames)
{
var result = _api.GetParameterByName(name, out _, out var finalValue);
if (result == RESULT.OK)
{
values[name] = finalValue;
continue;
}
warn?.Invoke(
$"[AudioManager] 捕获 Yarn 全局参数 {name} 失败:{result}。本次存档跳过该参数。");
}
return values;
}
internal void ResetToDefaults(Action<string> warn)
{
var names = new List<string>(_trackedNames);
foreach (var name in names)
{
var descriptionResult =
_api.GetParameterDescriptionByName(name, out var description);
if (descriptionResult != RESULT.OK)
{
warn?.Invoke(
$"[AudioManager] 查询 Yarn 全局参数 {name} 默认值失败:{descriptionResult}。保留追踪以便稍后重试。");
continue;
}
var resetResult = _api.SetParameterById(
description.id,
description.defaultvalue,
ignoreSeekSpeed: true);
if (resetResult != RESULT.OK)
{
warn?.Invoke(
$"[AudioManager] 重置 Yarn 全局参数 {name} 失败:{resetResult}。保留追踪以便稍后重试。");
continue;
}
_trackedNames.Remove(name);
}
}
internal void RestoreFinalValues(
IReadOnlyDictionary<string, float> values,
Action<string> warn)
{
if (values == null)
{
return;
}
foreach (var pair in values)
{
var result = _api.SetParameterByName(
pair.Key,
pair.Value,
ignoreSeekSpeed: true);
if (result == RESULT.OK)
{
_trackedNames.Add(pair.Key);
continue;
}
warn?.Invoke(
$"[AudioManager] 恢复 Yarn 全局参数 {pair.Key} 失败:{result}。跳过该参数。");
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fe8472981b434b1d94e494cca8e3801d
@@ -37,7 +37,7 @@ namespace AibisDream.SaveSystem
return;
}
AudioManager.Instance.RestoreSnapshot(audio);
AudioManager.Instance.RestoreSnapshot(audio, context.Warn);
}
}
}
+7 -1
View File
@@ -123,7 +123,10 @@ namespace AibisDream.SaveSystem
public bool isTalking;
}
/// <summary>sections["audio"]FMOD 音乐/SFX 终态与 AMB 状态(不记播放进度)。</summary>
/// <summary>
/// sections["audio"]FMOD 音乐/SFX 终态、AMB 状态与 Yarn 全局参数 finalValue
/// (不记 Event 播放进度)。
/// </summary>
[Serializable]
public class AudioSnapshotDto
{
@@ -134,6 +137,9 @@ namespace AibisDream.SaveSystem
/// <summary><see cref="AmbState"/> 枚举名。</summary>
public string ambState;
/// <summary>由 Yarn set_global_param 修改过的参数名 → 捕获时的 FMOD finalValue。</summary>
public Dictionary<string, float> yarnGlobalParameters = new();
}
/// <summary>