Fix BGM fade ownership across scene startup
This commit is contained in:
@@ -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
|
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
|
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
|
or is stopped by message Skip. Godot also gives an explicit `0xc2` fade exclusive ownership of the BGM
|
||||||
future unified settings backend can replace the registered defaults without changing the opcode/host seam.
|
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)
|
### Scene-entry state snapshot — auto-seeding single-scene runs (2026-07-09)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
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.
|
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
|
**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
|
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.
|
that is one at context construction and cleared only when op `0x9` resets/reloads root script id zero.
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ public partial class Main : Godot.Control
|
|||||||
private Label _status = null!;
|
private Label _status = null!;
|
||||||
private Label _locatorHud = null!;
|
private Label _locatorHud = null!;
|
||||||
private AudioStreamPlayer _bgm = null!; // looping background music
|
private AudioStreamPlayer _bgm = null!; // looping background music
|
||||||
|
private Tween? _bgmFadeTween;
|
||||||
private AudioStreamPlayer _voice = null!; // interrupt-on-new voice
|
private AudioStreamPlayer _voice = null!; // interrupt-on-new voice
|
||||||
private int _voiceQueuedGeneration;
|
private int _voiceQueuedGeneration;
|
||||||
private int _voiceStartedGeneration;
|
private int _voiceStartedGeneration;
|
||||||
@@ -1631,6 +1632,11 @@ public partial class Main : Godot.Control
|
|||||||
var stream = AudioStreamOggVorbis.LoadFromBuffer(oggBytes);
|
var stream = AudioStreamOggVorbis.LoadFromBuffer(oggBytes);
|
||||||
if (stream == null) { GD.Print($"OGG load failed {assetName}"); return; }
|
if (stream == null) { GD.Print($"OGG load failed {assetName}"); return; }
|
||||||
stream.Loop = true;
|
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.VolumeDb = 0;
|
||||||
_bgm.Stream = stream;
|
_bgm.Stream = stream;
|
||||||
_bgm.Play();
|
_bgm.Play();
|
||||||
@@ -1691,6 +1697,8 @@ public partial class Main : Godot.Control
|
|||||||
|
|
||||||
private void BeginVoiceBgmDuck(int generation, int targetPercent)
|
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)
|
if (!_voiceBgmDuckActive)
|
||||||
_voiceBgmDuckRestoreDb = _bgm.VolumeDb;
|
_voiceBgmDuckRestoreDb = _bgm.VolumeDb;
|
||||||
_voiceBgmDuckActive = true;
|
_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 linear = System.Math.Clamp(targetPercent / 100.0f, 0.0f, 1.0f);
|
||||||
float targetDb = linear <= 0 ? -80.0f : Mathf.LinearToDb(linear);
|
float targetDb = linear <= 0 ? -80.0f : Mathf.LinearToDb(linear);
|
||||||
|
RestoreVoiceBgmDuck();
|
||||||
|
CancelBgmFade();
|
||||||
if (realDurationSeconds <= 0) { _bgm.VolumeDb = targetDb; return; }
|
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,
|
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);
|
byte[] glowGodotWav = RiffWaveSanitizer.RemoveInfoMetadata(glowSfx.Bytes);
|
||||||
bool cp932WavMetadataOk = glowGodotWav.Length == 688_336
|
bool cp932WavMetadataOk = glowGodotWav.Length == 688_336
|
||||||
&& AudioStreamWav.LoadFromBuffer(glowGodotWav) != null;
|
&& 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); " +
|
if (ok) GD.Print($"SELFTEST OK: threaded host matches headless ({actual.Count} lines, full handling); " +
|
||||||
$"debug launcher catalog/UI smoke ({debugEntries.Count} packed scripts); " +
|
$"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}; " +
|
else GD.Print($"SELFTEST FAIL: threaded={actual.Count} vs headless={expected.Count}; " +
|
||||||
$"debug-launcher={launcherOk}; sleep-min={sleepMinimumOk}; " +
|
$"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);
|
GetTree().Quit(ok ? 0 : 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user