Implement deferred SFX and voice duck control

This commit is contained in:
gamer147
2026-07-20 11:29:12 -04:00
parent 3fa07c0ae5
commit 4179d90b8f
12 changed files with 182 additions and 30 deletions

View File

@@ -40,6 +40,7 @@ public sealed class GodotAdvHost : IHost
private bool _advTextForceComplete;
private readonly Dictionary<int, AdvWaitIndicatorConfig> _waitIndicators = new();
private volatile bool _messageSkipActive;
private int _voiceBgmDuckControl;
private (AudioPayload Audio, int PlaybackVariant)? _queuedSkippedVoice;
private int _activeWaitLayout;
private long _waitIndicatorStartedMs;
@@ -663,9 +664,16 @@ public sealed class GodotAdvHost : IHost
private void DispatchVoice(AudioPayload audio, int playbackVariant)
{
int generation = _main.QueueVoicePlayback();
bool duckBgm = (System.Threading.Volatile.Read(ref _voiceBgmDuckControl) & 1) == 0;
// Godot's stream player has no matching AGE start-mode control. Retain the native
// variant through dispatch/timeline so that distinction is not erased at the VM seam.
_main.CallDeferred("PlayVoice", audio.Bytes, audio.Name, generation);
_main.CallDeferred("PlayVoice", audio.Bytes, audio.Name, generation, duckBgm, 50);
}
public void SetVoiceBgmDuckControl(long flags)
{
System.Threading.Volatile.Write(ref _voiceBgmDuckControl, unchecked((int)flags));
_timeline?.State("voice-bgm-duck-control", new() { ["flags"] = flags });
}
public void LoadSoundEffect(long resourceId, int channel)
@@ -687,6 +695,16 @@ public sealed class GodotAdvHost : IHost
_main.CallDeferred("StartSoundEffect", channel);
}
public void ScheduleSoundEffectStart(int channel, int startMode, long delayMs)
{
if ((uint)channel >= (uint)_sfxNames.Length || _sfxNames[channel] == null) return;
long ms = System.Math.Clamp(delayMs, 0, 60_000);
double realSeconds = ms / 1000.0 / System.Math.Max(0.05, _clock.Speed);
_timeline?.Event("sfx-start-scheduled", new() { ["channel"] = channel,
["start_mode"] = startMode, ["delay_ms"] = ms, ["file"] = _sfxNames[channel] });
_main.CallDeferred("ScheduleSoundEffectStart", channel, startMode, realSeconds);
}
public void ReleaseSoundEffect(int channel)
{
if ((uint)channel >= (uint)_sfxNames.Length) return;

View File

@@ -38,7 +38,11 @@ public partial class Main : Godot.Control
private int _voiceQueuedGeneration;
private int _voiceStartedGeneration;
private int _voiceCompletedGeneration;
private bool _voiceBgmDuckActive;
private int _voiceBgmDuckGeneration;
private float _voiceBgmDuckRestoreDb;
private readonly AudioStreamPlayer[] _sfx = new AudioStreamPlayer[10]; // SC0000 channels 0..9
private readonly int[] _sfxGenerations = new int[10];
private VirtualMachine _vm = null!;
private GodotAdvHost _host = null!;
private readonly Age.Engine.Hosting.FrameClock _clock = new();
@@ -850,7 +854,7 @@ public partial class Main : Godot.Control
=> System.Threading.Volatile.Read(ref _voiceCompletedGeneration)
< System.Threading.Volatile.Read(ref _voiceQueuedGeneration);
public void PlayVoice(byte[] oggBytes, string assetName, int generation)
public void PlayVoice(byte[] oggBytes, string assetName, int generation, bool duckBgm, int duckTargetPercent)
{
var stream = AudioStreamOggVorbis.LoadFromBuffer(oggBytes);
if (stream == null)
@@ -859,6 +863,10 @@ public partial class Main : Godot.Control
CompleteVoiceGeneration(generation);
return;
}
if (duckBgm)
BeginVoiceBgmDuck(generation, duckTargetPercent);
else
RestoreVoiceBgmDuck();
stream.Loop = false;
_voice.Stream = stream;
System.Threading.Volatile.Write(ref _voiceStartedGeneration, generation);
@@ -888,15 +896,37 @@ public partial class Main : Godot.Control
}
while (System.Threading.Interlocked.CompareExchange(
ref _voiceCompletedGeneration, generation, current) != current);
if (_voiceBgmDuckActive && generation >= _voiceBgmDuckGeneration)
RestoreVoiceBgmDuck();
}
private void BeginVoiceBgmDuck(int generation, int targetPercent)
{
if (!_voiceBgmDuckActive)
_voiceBgmDuckRestoreDb = _bgm.VolumeDb;
_voiceBgmDuckActive = true;
_voiceBgmDuckGeneration = generation;
float linear = System.Math.Clamp(targetPercent / 100.0f, 0.0f, 1.0f);
_bgm.VolumeDb = linear <= 0 ? -80.0f : Mathf.LinearToDb(linear);
}
private void RestoreVoiceBgmDuck()
{
if (!_voiceBgmDuckActive) return;
_bgm.VolumeDb = _voiceBgmDuckRestoreDb;
_voiceBgmDuckActive = false;
_voiceBgmDuckGeneration = 0;
}
public void LoadSoundEffect(byte[] wavBytes, string assetName, int channel)
{
if ((uint)channel >= (uint)_sfx.Length) return;
_sfxGenerations[channel]++;
_sfx[channel].Stop();
_sfx[channel].Stream = null;
var stream = AudioStreamWav.LoadFromBuffer(wavBytes);
if (stream == null) { GD.Print($"WAV load failed {assetName}"); return; }
stream.LoopMode = AudioStreamWav.LoopModeEnum.Disabled;
_sfx[channel].Stop();
_sfx[channel].VolumeDb = 0;
_sfx[channel].Stream = stream;
}
@@ -907,9 +937,31 @@ public partial class Main : Godot.Control
_sfx[channel].Play();
}
public void ScheduleSoundEffectStart(int channel, int startMode, double realDelaySeconds)
{
if ((uint)channel >= (uint)_sfx.Length || _sfx[channel].Stream == null) return;
int generation = _sfxGenerations[channel];
void StartIfCurrent()
{
if (_sfxGenerations[channel] != generation || _sfx[channel].Stream == null) return;
if (_sfx[channel].Stream is AudioStreamWav wav)
wav.LoopMode = startMode == 0
? AudioStreamWav.LoopModeEnum.Disabled
: AudioStreamWav.LoopModeEnum.Forward;
_sfx[channel].Play();
}
if (realDelaySeconds <= 0)
{
StartIfCurrent();
return;
}
GetTree().CreateTimer(realDelaySeconds).Timeout += StartIfCurrent;
}
public void ReleaseSoundEffect(int channel)
{
if ((uint)channel >= (uint)_sfx.Length) return;
_sfxGenerations[channel]++;
_sfx[channel].Stop();
_sfx[channel].Stream = null;
}