Files
OpenMaidEngine/godot/GodotAdvHost.Audio.cs
2026-08-02 21:02:27 -04:00

197 lines
7.5 KiB
C#

using System;
using Age.Engine.Hosting;
using Age.Engine.Model;
using Age.Engine.Sys4;
public sealed partial class GodotAdvHost
{
private readonly string?[] _sfxNames = new string?[10]; // SC0000 native channel subset
private int _voiceBgmDuckControl;
private (AudioPayload Audio, int PlaybackVariant)? _queuedSkippedVoice;
private readonly object _scheduledVoiceLock = new();
private (AudioPayload Audio, int PlaybackVariant, uint DelayMs, uint? StartMs)? _scheduledVoice;
public void PlayBgm(long id)
=> DispatchBgm(id, 1, false);
public void RestartBgm(long id, int startMode)
=> DispatchBgm(id, startMode, true);
private void DispatchBgm(long id, int startMode, bool forceRestart)
{
var asset = _res.ResolveBgm(id);
var audio = asset != null ? LoadAudio(asset) : null;
_timeline?.Event("bgm", new()
{
["id"] = id,
["file"] = audio?.Name,
["start_mode"] = startMode,
["force_restart"] = forceRestart,
});
if (audio == null) return;
if (forceRestart)
_main.CallDeferred("RestartBgm", audio.Bytes, audio.Name, startMode);
else
_main.CallDeferred("PlayBgm", audio.Bytes, audio.Name);
}
public void StopBgm()
{
_timeline?.Event("bgm-stop", new());
_main.CallDeferred("StopBgm");
}
public void PlayVoice(long id) => PlayVoice(id, 0);
public void PlayVoice(long id, int playbackVariant)
{
var asset = _res.ResolveVoice(id);
var audio = asset != null ? LoadAudio(asset) : null;
_timeline?.Event("voice", new() { ["id"] = id, ["file"] = audio?.Name,
["playback_variant"] = playbackVariant });
if (audio == null) return;
bool queuedForSkip;
bool firstQueued = false;
lock (_messageSkipLock)
{
queuedForSkip = _messageSkipActive;
if (queuedForSkip)
{
firstQueued = _queuedSkippedVoice == null;
_queuedSkippedVoice = (audio, playbackVariant);
}
}
if (queuedForSkip)
{
if (firstQueued) _main.CallDeferred("StopVoiceForMessageSkip");
return;
}
DispatchVoice(audio, playbackVariant);
}
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, 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 ScheduleVoicePlayback(long id, int playbackVariant, long delayMs)
{
var asset = _res.ResolveVoice(id);
var audio = asset != null ? LoadAudio(asset) : null;
_timeline?.Event("voice-scheduled", new()
{
["id"] = id, ["file"] = audio?.Name, ["playback_variant"] = playbackVariant,
["delay_ms"] = unchecked((uint)delayMs),
});
lock (_scheduledVoiceLock)
_scheduledVoice = audio == null
? null
: (audio, playbackVariant, unchecked((uint)delayMs), null);
}
public void LoadSoundEffect(long resourceId, int channel)
{
if ((uint)channel >= (uint)_sfxNames.Length) return;
var asset = _res.ResolveSoundEffect(resourceId);
var audio = asset != null ? LoadAudio(asset) : null;
_sfxNames[channel] = audio?.Name;
_timeline?.Event("sfx-load", new() { ["resource"] = resourceId, ["channel"] = channel,
["file"] = audio?.Name });
if (audio != null) _main.CallDeferred("LoadSoundEffect", audio.Bytes, audio.Name, channel);
}
public void StartSoundEffect(int channel) => StartSoundEffect(channel, 0);
public void StartSoundEffect(int channel, int startMode)
{
if ((uint)channel >= (uint)_sfxNames.Length || _sfxNames[channel] == null) return;
_timeline?.Event("sfx-start", new() { ["channel"] = channel,
["start_mode"] = startMode, ["file"] = _sfxNames[channel] });
_main.CallDeferred("StartSoundEffect", channel, startMode);
}
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;
_timeline?.Event("sfx-release", new() { ["channel"] = channel,
["file"] = _sfxNames[channel] });
_sfxNames[channel] = null;
_main.CallDeferred("ReleaseSoundEffect", channel);
}
private AudioPayload? LoadAudio(AssetEntry asset)
{
try { return _res.ReadAudio(asset); }
catch (System.Exception e)
{
Godot.GD.Print($"audio read failed {asset.Name}: {e.Message}");
return null;
}
}
public void FadeBgm(int targetPercent, long durationMs)
{
long ms = System.Math.Clamp(durationMs, 0, 60_000);
double realSeconds = ms / 1000.0 / System.Math.Max(0.05, _clock.Speed);
_timeline?.State("bgm-fade", new() { ["target_percent"] = targetPercent, ["duration_ms"] = ms });
_main.CallDeferred("FadeBgm", targetPercent, realSeconds);
long deadline = _clock.NowMs + ms;
IsSleeping = true;
bool scriptSuspended = SuspendScriptForPresentation();
try
{
// Native parks the interpreter in its audio service while the main render loop continues.
// Publish scene changes accumulated before the fade (notably GAMESTART -> SC0000's black
// frame), then leave presentation ownership with the compositor for the timed wait.
RequestSynchronizedPresentation();
while (_clock.NowMs < deadline && !_stopping) _frameSignal.WaitOne(50);
}
finally
{
ResumeScriptAfterPresentation(scriptSuspended);
IsSleeping = false;
}
_timeline?.State("running", new() { ["bgm_fade_complete"] = true });
}
public void ApplyAudioVolume(int category, int basisPoints)
{
_timeline?.State("audio-volume", new()
{
["category"] = category,
["basis_points"] = basisPoints,
});
_main.CallDeferred("ApplyAudioVolume", category, basisPoints);
}
public void ApplyAudioRouteEnabled(int category, bool enabled)
{
_timeline?.State("audio-route", new()
{
["category"] = category,
["enabled"] = enabled,
});
_main.CallDeferred("ApplyAudioRouteEnabled", category, enabled);
}
}