Fix BGM fade ownership across scene startup

This commit is contained in:
gamer147
2026-07-28 13:29:49 -04:00
parent f4bf78d7d1
commit a991d5699c
3 changed files with 60 additions and 6 deletions

View File

@@ -2567,8 +2567,14 @@ enabled=`1` and target=`50` percent; SC0000 writes only control masks `0` and `1
The port therefore retains `0x1cf` as script-owned runtime state, applies the native registered default
attenuation when an unsuppressed voice begins, and restores the saved BGM level when that voice completes
or is stopped by message Skip. This introduces no profile record, boot seed, or persistence backend. A
future unified settings backend can replace the registered defaults without changing the opcode/host seam.
or is stopped by message Skip. Godot also gives an explicit `0xc2` fade exclusive ownership of the BGM
envelope: voice start does not duck during that fade, a new fade cancels its predecessor, and starting a
replacement BGM cancels any still-live prior fade before restoring normal gain. The last rule closes a
main-thread scheduling race at the TITLE-to-SC0000 boundary. The VM's blocking fade deadline begins before
the deferred Godot tween is created, so `BGM005` could start during the tween's final frame and then be
overwritten to `-80 dB`; a later voice temporarily wrote the audible 50-percent duck level and restored
the erroneous silent level afterward. This introduces no profile record, boot seed, or persistence backend.
A future unified settings backend can replace the registered defaults without changing the opcode/host seam.
### Scene-entry state snapshot — auto-seeding single-scene runs (2026-07-09)

View File

@@ -118,6 +118,16 @@ while Godot treated them as active-script manifest ids. The packed resolver maps
through the existing channel players. A synchronized TITLE→GAMESTART→TITLE trace records every load/start
with its filename, and manual validation confirms they are audible; BGM remains unaffected.
**TITLE-to-SC0000 BGM fade ownership corrected (2026-07-28).** SC0000's `0xc2@0x7c1` blocks the VM for
the three-second title-music fade, then starts `BGM005` at `0x7fa`. Godot created the actual gain tween
through a deferred main-thread call, however, so its start could lag the already-running virtual deadline
by one frame. On the unlucky schedule, `BGM005` restored unity gain just before that stale title tween
finished and wrote `-80 dB`. Voice ducking exposed the diagnosis: a voice wrote the configured 50-percent
level, making the live BGM audible, then restored the captured silent level. The Godot backend now owns
exactly one explicit BGM fade, cancels it before replacing the track or starting another fade, and suppresses
voice ducking while the fade owns the envelope as native AGE does. The threaded selftest starts a live
fade and proves a replacement BGM cancels it and retains normal gain.
**Pre-title video sequence implemented (2026-07-20).** SYSTEM4 already owns the native sequence; the
port did not lose an executable-side launcher. Its sole op `0x130` call returns an engine initial-root flag
that is one at context construction and cleared only when op `0x9` resets/reloads root script id zero.

View File

@@ -39,6 +39,7 @@ public partial class Main : Godot.Control
private Label _status = null!;
private Label _locatorHud = null!;
private AudioStreamPlayer _bgm = null!; // looping background music
private Tween? _bgmFadeTween;
private AudioStreamPlayer _voice = null!; // interrupt-on-new voice
private int _voiceQueuedGeneration;
private int _voiceStartedGeneration;
@@ -1631,6 +1632,11 @@ public partial class Main : Godot.Control
var stream = AudioStreamOggVorbis.LoadFromBuffer(oggBytes);
if (stream == null) { GD.Print($"OGG load failed {assetName}"); return; }
stream.Loop = true;
// FadeBgm is marshalled from the VM thread and starts on the next Godot frame, while the VM's
// blocking deadline begins immediately. Scene startup can consequently request the replacement
// track just before the old fade tween reaches zero. Do not let that orphaned tween mute the new
// stream after this method restores its normal gain.
CancelBgmFade();
_bgm.VolumeDb = 0;
_bgm.Stream = stream;
_bgm.Play();
@@ -1691,6 +1697,8 @@ public partial class Main : Godot.Control
private void BeginVoiceBgmDuck(int generation, int targetPercent)
{
// Native voice ducking is suppressed while an explicit 0xc2 BGM fade owns the envelope.
if (_bgmFadeTween != null) return;
if (!_voiceBgmDuckActive)
_voiceBgmDuckRestoreDb = _bgm.VolumeDb;
_voiceBgmDuckActive = true;
@@ -1770,8 +1778,26 @@ public partial class Main : Godot.Control
{
float linear = System.Math.Clamp(targetPercent / 100.0f, 0.0f, 1.0f);
float targetDb = linear <= 0 ? -80.0f : Mathf.LinearToDb(linear);
RestoreVoiceBgmDuck();
CancelBgmFade();
if (realDurationSeconds <= 0) { _bgm.VolumeDb = targetDb; return; }
CreateTween().TweenProperty(_bgm, "volume_db", targetDb, realDurationSeconds);
var tween = CreateTween();
_bgmFadeTween = tween;
tween.Finished += () =>
{
if (ReferenceEquals(_bgmFadeTween, tween))
_bgmFadeTween = null;
};
tween.TweenProperty(_bgm, "volume_db", targetDb, realDurationSeconds);
}
private void CancelBgmFade()
{
var tween = _bgmFadeTween;
_bgmFadeTween = null;
if (tween?.IsValid() == true)
tween.Kill();
}
public bool TryPlayMovie(byte[] mpegBytes, string assetName, long playbackId,
@@ -1947,13 +1973,25 @@ public partial class Main : Godot.Control
byte[] glowGodotWav = RiffWaveSanitizer.RemoveInfoMetadata(glowSfx.Bytes);
bool cp932WavMetadataOk = glowGodotWav.Length == 688_336
&& AudioStreamWav.LoadFromBuffer(glowGodotWav) != null;
ok &= launcherOk && sleepMinimumOk && inputTranslationOk && cp932WavMetadataOk;
AudioPayload bgm = selftestResources.ReadAudio(selftestResources.ResolveBgm(5)!);
FadeBgm(0, 10.0);
bool bgmFadeStarted = _bgmFadeTween?.IsValid() == true;
PlayBgm(bgm.Bytes, bgm.Name);
bool bgmReplacementCancelsFade = bgmFadeStarted
&& _bgmFadeTween == null
&& System.Math.Abs(_bgm.VolumeDb) < 0.001f;
_bgm.Stop();
_bgm.Stream = null;
ok &= launcherOk && sleepMinimumOk && inputTranslationOk && cp932WavMetadataOk
&& bgmReplacementCancelsFade;
if (ok) GD.Print($"SELFTEST OK: threaded host matches headless ({actual.Count} lines, full handling); " +
$"debug launcher catalog/UI smoke ({debugEntries.Count} packed scripts); " +
$"sleep-min=1ms; native-key-translation=ok; cp932-wav-info=ok");
$"sleep-min=1ms; native-key-translation=ok; cp932-wav-info=ok; " +
$"bgm-fade-replacement=ok");
else GD.Print($"SELFTEST FAIL: threaded={actual.Count} vs headless={expected.Count}; " +
$"debug-launcher={launcherOk}; sleep-min={sleepMinimumOk}; " +
$"native-key-translation={inputTranslationOk}; cp932-wav-info={cp932WavMetadataOk}");
$"native-key-translation={inputTranslationOk}; cp932-wav-info={cp932WavMetadataOk}; " +
$"bgm-fade-replacement={bgmReplacementCancelsFade}");
GetTree().Quit(ok ? 0 : 1);
}