Fix repeated FIELD BGM restarts
This commit is contained in:
@@ -2421,6 +2421,15 @@ callback if that channel is loaded again or released before its deadline.
|
|||||||
Normal-speed windowed validation reached `wait-for-input@0x1a58` after 45.6 seconds without an audio stall;
|
Normal-speed windowed validation reached `wait-for-input@0x1a58` after 45.6 seconds without an audio stall;
|
||||||
the user confirmed the opening effects were audible and sounded good.
|
the user confirmed the opening effects were audible and sounded good.
|
||||||
|
|
||||||
|
FIELD deliberately reasserts `play-bgm(stage_bgm_id)` from its common post-action/event cleanup at
|
||||||
|
`0x884a`; ordinary unit movement reaches that cleanup once per acting unit. The BGM facade already retains
|
||||||
|
the selected direct-name track id for op `0xc0` and numbered-save state. An identical `0xbf` request must
|
||||||
|
therefore preserve the current stream and playback position rather than reopen the OGG. The former host path
|
||||||
|
decoded and assigned a fresh Godot stream on every request, resetting the stage music after every player
|
||||||
|
move and repeatedly throughout enemy turns. The VM now treats a current-track reassertion as idempotent.
|
||||||
|
A different id still starts a new track, and `0xc2` target zero clears the retained id so a later request
|
||||||
|
can start that track normally.
|
||||||
|
|
||||||
`0xc2` is BGM rather than SFX: `op_0xc2_bgm_fade@0x4204c0` sets run-state `0x200`, arms the service timer,
|
`0xc2` is BGM rather than SFX: `op_0xc2_bgm_fade@0x4204c0` sets run-state `0x200`, arms the service timer,
|
||||||
and calls `bgm_fade_arm@0x464830`. `bgm_fade_tick@0x464960` linearly interpolates current to target percent;
|
and calls `bgm_fade_arm@0x464830`. `bgm_fade_tick@0x464960` linearly interpolates current to target percent;
|
||||||
durations at least 1000 ms take 100 steps, shorter durations take 10, and target zero releases the source.
|
durations at least 1000 ms take 100 steps, shorter durations take 10, and target zero releases the source.
|
||||||
|
|||||||
@@ -163,9 +163,9 @@ Port status (2026-07-24): after the blocking host releases this wait, the VM que
|
|||||||
- **evidence:** Ghidra op 0xb6 handler 0x420250 -> sfx_channel_release@0x482600 -> sound_buffer_destroy@0x4831a0, which releases the per-channel object under its critical section and clears the slot. Native trace captured SC0000's channels 0..9 release sweep in consecutive calls.
|
- **evidence:** Ghidra op 0xb6 handler 0x420250 -> sfx_channel_release@0x482600 -> sound_buffer_destroy@0x4831a0, which releases the per-channel object under its critical section and clears the slot. Native trace captured SC0000's channels 0..9 release sweep in consecutive calls.
|
||||||
|
|
||||||
### 0xbf `play-bgm` (play-bgm, argc 1)
|
### 0xbf `play-bgm` (play-bgm, argc 1)
|
||||||
- **summary:** Play background music by id. BGM is addressed by DIRECT LITERAL NAME: id -> BGM{id:03d}.OGG (in DATA3), unlike ordinary resource operands, which are universal packed SYS4INI/AAI ids. E.g. play-bgm 5 -> BGM005.
|
- **summary:** Play background music by id. BGM is addressed by DIRECT LITERAL NAME: id -> BGM{id:03d}.OGG (in DATA3), unlike ordinary resource operands, which are universal packed SYS4INI/AAI ids. E.g. play-bgm 5 -> BGM005. Reasserting the facade's current track id is idempotent and preserves playback position; a different id starts the new track, while fade-to-zero clears the current id so a later request can start it again.
|
||||||
- **grounding:** source=investigation, confidence=high
|
- **grounding:** source=investigation, confidence=high
|
||||||
- **evidence:** By-ear confirmed (2026-07-06): SC0000 real game plays BGM005 for play-bgm 0x5 and BGM008 for play-bgm 0x8 (we initially mis-played BGM006/BGM009 via the disproven scene-section model). Direct-name proven by play-bgm 0x23 -> BGM035.OGG, a real standalone track (the BGM set skips 030-034). Ghidra /v2 op_0xbf_handler@0x420390 forwards the numeric track to the BGM facade rather than asset_open_indexed_entry. Diagnostic: `Age.Cli audio SC0000.BIN`.
|
- **evidence:** By-ear confirmed (2026-07-06): SC0000 real game plays BGM005 for play-bgm 0x5 and BGM008 for play-bgm 0x8 (we initially mis-played BGM006/BGM009 via the disproven scene-section model). Direct-name proven by play-bgm 0x23 -> BGM035.OGG, a real standalone track (the BGM set skips 030-034). Ghidra /v2 op_0xbf_handler@0x420390 forwards the numeric track to the BGM facade rather than asset_open_indexed_entry; the facade retains that id at EngineCtx.current_bgm_track_id for op 0xc0 and numbered saves. FIELD intentionally reasserts stage_bgm_id from its common post-action/event cleanup at 0x884a, reached after unit movement; restarting an identical host stream there resets the music once per unit during enemy turns. Op 0xc2 target zero releases the source and the VM clears the retained id. Diagnostic: `Age.Cli audio SC0000.BIN`.
|
||||||
|
|
||||||
### 0xc0 `get-current-bgm-track` (get-current-bgm-track, argc 1)
|
### 0xc0 `get-current-bgm-track` (get-current-bgm-track, argc 1)
|
||||||
- **summary:** (track_out) - return the direct-name BGM track id retained by the music facade; the same value is restored from numbered saves.
|
- **summary:** (track_out) - return the direct-name BGM track id retained by the music facade; the same value is restored from numbered saves.
|
||||||
|
|||||||
@@ -7,6 +7,31 @@ using Xunit;
|
|||||||
|
|
||||||
public class SfxOpsTests
|
public class SfxOpsTests
|
||||||
{
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ReassertingCurrentBgmPreservesPlaybackUntilTrackChangesOrStops()
|
||||||
|
{
|
||||||
|
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||||
|
var script = ScriptAssembler.Assemble(table, "BGM",
|
||||||
|
new List<(int, Operand[])>
|
||||||
|
{
|
||||||
|
(0xbf, new[] { new Operand(0, 24) }),
|
||||||
|
(0xbf, new[] { new Operand(0, 24) }), // FIELD action cleanup: keep current playback position
|
||||||
|
(0xbf, new[] { new Operand(0, 25) }),
|
||||||
|
(0xbf, new[] { new Operand(0, 25) }),
|
||||||
|
(0xc2, new[] { new Operand(0, 0), new Operand(0, 0) }),
|
||||||
|
(0xbf, new[] { new Operand(0, 25) }), // fade-to-zero cleared the current track
|
||||||
|
(0x2, Array.Empty<Operand>()),
|
||||||
|
}, Array.Empty<string>());
|
||||||
|
var host = new RecordingHost();
|
||||||
|
var vm = new VirtualMachine(script, table, host);
|
||||||
|
|
||||||
|
vm.Run();
|
||||||
|
|
||||||
|
Assert.Equal("exit", vm.HaltReason);
|
||||||
|
Assert.Equal([24L, 25L, 25L], host.BgmTracks);
|
||||||
|
Assert.Equal((0, 0L), Assert.Single(host.BgmFades));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Sc0000AudioLifecycleDelayAndDuckControlReachHost()
|
public void Sc0000AudioLifecycleDelayAndDuckControlReachHost()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2330,9 +2330,18 @@ public sealed class VirtualMachine
|
|||||||
Gfx.ReleaseSurfaceRange(42, 1000 - 42);
|
Gfx.ReleaseSurfaceRange(42, 1000 - 42);
|
||||||
_host.ReleaseSurfaceRange(42, 1000 - 42); return pc + 1;
|
_host.ReleaseSurfaceRange(42, 1000 - 42); return pc + 1;
|
||||||
case "play-bgm":
|
case "play-bgm":
|
||||||
_currentBgmTrackId = Read(a[0]);
|
{
|
||||||
_host.PlayBgm(_currentBgmTrackId);
|
long requestedTrackId = Read(a[0]);
|
||||||
|
// Native scripts freely reassert the stage BGM after FIELD action/event cleanup.
|
||||||
|
// The music facade retains the current track id, so an identical request keeps the
|
||||||
|
// existing stream position instead of reopening the OGG from the beginning.
|
||||||
|
if (requestedTrackId != _currentBgmTrackId)
|
||||||
|
{
|
||||||
|
_currentBgmTrackId = requestedTrackId;
|
||||||
|
_host.PlayBgm(_currentBgmTrackId);
|
||||||
|
}
|
||||||
return pc + 1;
|
return pc + 1;
|
||||||
|
}
|
||||||
case "get-current-bgm-track":
|
case "get-current-bgm-track":
|
||||||
Write(a[0], _currentBgmTrackId);
|
Write(a[0], _currentBgmTrackId);
|
||||||
return pc + 1;
|
return pc + 1;
|
||||||
|
|||||||
@@ -1887,12 +1887,12 @@ abi_source = "kelebek+decode-validated"
|
|||||||
[opcode.semantics]
|
[opcode.semantics]
|
||||||
name = "play-bgm"
|
name = "play-bgm"
|
||||||
category = "audio"
|
category = "audio"
|
||||||
summary = "Play background music by id. BGM is addressed by DIRECT LITERAL NAME: id -> BGM{id:03d}.OGG (in DATA3), unlike ordinary resource operands, which are universal packed SYS4INI/AAI ids. E.g. play-bgm 5 -> BGM005."
|
summary = "Play background music by id. BGM is addressed by DIRECT LITERAL NAME: id -> BGM{id:03d}.OGG (in DATA3), unlike ordinary resource operands, which are universal packed SYS4INI/AAI ids. E.g. play-bgm 5 -> BGM005. Reasserting the facade's current track id is idempotent and preserves playback position; a different id starts the new track, while fade-to-zero clears the current id so a later request can start it again."
|
||||||
noop_headless = false
|
noop_headless = false
|
||||||
source = "investigation"
|
source = "investigation"
|
||||||
confidence = "high"
|
confidence = "high"
|
||||||
depends_on = []
|
depends_on = []
|
||||||
evidence = "By-ear confirmed (2026-07-06): SC0000 real game plays BGM005 for play-bgm 0x5 and BGM008 for play-bgm 0x8 (we initially mis-played BGM006/BGM009 via the disproven scene-section model). Direct-name proven by play-bgm 0x23 -> BGM035.OGG, a real standalone track (the BGM set skips 030-034). Ghidra /v2 op_0xbf_handler@0x420390 forwards the numeric track to the BGM facade rather than asset_open_indexed_entry. Diagnostic: `Age.Cli audio SC0000.BIN`."
|
evidence = "By-ear confirmed (2026-07-06): SC0000 real game plays BGM005 for play-bgm 0x5 and BGM008 for play-bgm 0x8 (we initially mis-played BGM006/BGM009 via the disproven scene-section model). Direct-name proven by play-bgm 0x23 -> BGM035.OGG, a real standalone track (the BGM set skips 030-034). Ghidra /v2 op_0xbf_handler@0x420390 forwards the numeric track to the BGM facade rather than asset_open_indexed_entry; the facade retains that id at EngineCtx.current_bgm_track_id for op 0xc0 and numbered saves. FIELD intentionally reasserts stage_bgm_id from its common post-action/event cleanup at 0x884a, reached after unit movement; restarting an identical host stream there resets the music once per unit during enemy turns. Op 0xc2 target zero releases the source and the VM clears the retained id. Diagnostic: `Age.Cli audio SC0000.BIN`."
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 1
|
i = 1
|
||||||
|
|||||||
Reference in New Issue
Block a user