From 8c922abd7747387bf3a864b9781cc164acd08934 Mon Sep 17 00:00:00 2001 From: Dipshet <264011288+Dipshet@users.noreply.github.com> Date: Fri, 31 Jul 2026 04:23:20 +0200 Subject: [PATCH] Preserve the XMA stream timeline across undecodable frames A frame the decoder backend rejects advances the read offset but emitted nothing, silently shortening that stream by 512 samples, permanent desync for multi-stream sources (5.1 premixes as parallel stereo XMA). Emit the frame as silence instead so the timeline holds. Robustness: no in-game trigger known in AC6. audio_xma_preserve_timeline (default on). --- .../src/native/audio/audio_system.cpp | 10 +++++++ .../src/native/audio/xma/context.cpp | 28 +++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/thirdparty/rexglue-sdk/src/native/audio/audio_system.cpp b/thirdparty/rexglue-sdk/src/native/audio/audio_system.cpp index bb1b29e6..10c5f301 100644 --- a/thirdparty/rexglue-sdk/src/native/audio/audio_system.cpp +++ b/thirdparty/rexglue-sdk/src/native/audio/audio_system.cpp @@ -19,6 +19,16 @@ REXCVAR_DEFINE_BOOL(audio_trace_render_driver_verbose, false, "Audio", "Trace render-driver activity"); REXCVAR_DEFINE_BOOL(audio_deep_trace, false, "Audio", "Enable verbose runtime audio tracing"); +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 " + "offset advances past the frame either way, so dropping the output " + "silently shortens the stream's timeline by 512 samples per damaged " + "frame - which would permanently desynchronize multi-stream sources " + "(5.1 premixes carried as parallel stereo streams, e.g. AC6's " + "cutscene mixes). Robustness fix - no in-game trigger is known in " + "AC6 (instrumented runs decode every frame). On by default; off " + "restores the legacy drop behavior."); namespace rex::audio { diff --git a/thirdparty/rexglue-sdk/src/native/audio/xma/context.cpp b/thirdparty/rexglue-sdk/src/native/audio/xma/context.cpp index 73994824..6550bc44 100644 --- a/thirdparty/rexglue-sdk/src/native/audio/xma/context.cpp +++ b/thirdparty/rexglue-sdk/src/native/audio/xma/context.cpp @@ -17,6 +17,7 @@ #include REXCVAR_DECLARE(bool, audio_deep_trace); +REXCVAR_DECLARE(bool, audio_xma_preserve_timeline); namespace rex::audio { @@ -853,11 +854,32 @@ void XmaContext::Decode(XMA_CONTEXT_DATA* data) { .sample_rate = static_cast(GetSampleRate(data->sample_rate)), .is_two_channel = bool(data->is_stereo), }; - if (decoder_backend_ && + const bool frame_decoded = + decoder_backend_ && decoder_backend_->DecodePacket( - decode_request, std::span(raw_frame_.data(), raw_frame_.size()))) { + decode_request, std::span(raw_frame_.data(), raw_frame_.size())); + if (!frame_decoded && REXCVAR_GET(audio_xma_preserve_timeline)) { + // A frame the decoder rejects still occupies exactly kSamplesPerFrame + // samples of the stream's timeline, and the read offset advances past it + // below either way. Dropping the output entirely (the legacy behavior) + // silently shortens the stream by one frame - a latent correctness bug + // for multi-stream sources that must stay sample-locked, e.g. 5.1 + // premixes carried as parallel stereo streams (AC6's cutscene mixes). + // Instrumented AC6 runs decode every cutscene frame successfully, so no + // in-game trigger is known - this is robustness against decode errors, + // not a fix for an audible symptom. Deliver the frame as silence + // instead - raw_frame_ is zero-filled above, so falling through emits + // one frame of silence in this stream (~10 ms, masked by the other + // streams) and the timeline holds. + REXAPU_DEBUG( + "XmaContext {}: undecodable frame (packet={} offset={} frame_bits={}) - " + "emitting silence to preserve the stream timeline", + id(), last_packet_index_, last_input_read_offset_before_, + packet_info.current_frame_size_); + } + if (frame_decoded || REXCVAR_GET(audio_xma_preserve_timeline)) { current_frame_remaining_subframes_ = 4 << data->is_stereo; - last_decode_succeeded_ = true; + last_decode_succeeded_ = frame_decoded; if (is_loop_end_frame) { loop_frame_output_limit_ = (data->loop_subframe_end + 1) << data->is_stereo;