apu: guard the XMA loop-end frame test against degenerate loop metadata

The loop-end test runs before UpdateLoopStatus, so the degenerate-loop guard there never reaches it. With loop_count > 0 and loop_start >= loop_end, the clamped loop end equals the start offset of the stream at packet 0, arming the frame output limit and silently truncating 384 samples from its first frame. Cvar audio_xma_loop_end_guard, default on.
This commit is contained in:
Dipshet
2026-08-05 20:29:34 +02:00
parent ad7be62321
commit 5ca3b24c62
2 changed files with 30 additions and 0 deletions
@@ -84,6 +84,19 @@ REXCVAR_DEFINE_BOOL(audio_xma_header_straddle_fix, true, "Audio",
"combs on the stereo fold - the long-reported doubled/"
"robotic cutscene dialogue. Off restores the legacy "
"walk for A/B.");
REXCVAR_DEFINE_BOOL(audio_xma_loop_end_guard, true, "Audio",
"Apply the degenerate-loop guard (see "
"audio_xma_loop_guard) to the loop-end frame test as "
"well. That test is evaluated before the loop-status "
"update, so the guard there never reaches it: with "
"loop_count > 0 and loop_start >= loop_end (metadata "
"AC6's streamed voices carry), the clamped loop_end "
"equals a voice's starting read offset for the stream "
"that begins at packet 0, arming the frame output limit "
"and silently discarding 384 samples of that stream's "
"first frame - permanently offsetting one context of a "
"multi-stream voice from its siblings. Off restores the "
"legacy behavior for A/B.");
REXCVAR_DEFINE_BOOL(audio_xma_preserve_timeline, true, "Audio",
"When the XMA decoder backend rejects a damaged frame, emit that "
"frame's worth of silence instead of dropping it. The input read "
+17
View File
@@ -20,6 +20,7 @@ REXCVAR_DECLARE(bool, audio_deep_trace);
REXCVAR_DECLARE(bool, audio_xma_loop_guard);
REXCVAR_DECLARE(bool, audio_xma_preserve_timeline);
REXCVAR_DECLARE(bool, audio_xma_header_straddle_fix);
REXCVAR_DECLARE(bool, audio_xma_loop_end_guard);
namespace rex::audio {
@@ -726,6 +727,22 @@ void XmaContext::Decode(XMA_CONTEXT_DATA* data) {
if (data->loop_count > 0) {
const uint32_t loop_end = std::max(kBitsPerPacketHeader, data->loop_end);
is_loop_end_frame = (data->input_buffer_read_offset == loop_end);
if (is_loop_end_frame && REXCVAR_GET(audio_xma_loop_end_guard) &&
data->loop_start >= data->loop_end) {
// This test is evaluated before UpdateLoopStatus, so the degenerate-loop
// guard there (audio_xma_loop_guard) never reaches it. With
// loop_count > 0 and loop_start >= loop_end the clamp above turns
// loop_end into kBitsPerPacketHeader - which is exactly a voice's
// starting read offset for the stream that begins at packet 0, and
// exactly what SwapInputBuffer resets the read offset to - so the
// frame output limit armed on the first frame of that stream (and on
// any post-swap pass that lands at the raw offset), and Consume()
// silently discarded the tail of the frame: 384 samples lost on ONE
// context of a multi-stream voice, which permanently offsets it from
// its siblings. Apply the same real-window requirement here that
// UpdateLoopStatus applies.
is_loop_end_frame = false;
}
}
UpdateLoopStatus(data);