mirror of
https://github.com/sal063/AC6_recomp
synced 2026-08-21 23:00:53 -04:00
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).
This commit is contained in:
@@ -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 {
|
||||
|
||||
|
||||
+25
-3
@@ -17,6 +17,7 @@
|
||||
#include <native/stream.h>
|
||||
|
||||
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<uint32_t>(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<uint8_t>(raw_frame_.data(), raw_frame_.size()))) {
|
||||
decode_request, std::span<uint8_t>(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;
|
||||
|
||||
Reference in New Issue
Block a user