[XMA] Fix multiple audio issues in new decoder
- Handle XMA2 frames with headers split across packet boundaries (fixes audio cutoff in Capcom games) - Output buffer minimum space check using actual frame size instead of SDC-based formula, allowing tight-buffer contexts to decode (fixes PGR4 engine sounds) - Gate consume-only context clearing with tight-buffer heuristic to prevent disrupting playback while still allowing stream completion signaling (fixes Borderlands 2 dialog) - Clamp read offset 0 to packet header size to prevent decode rejection loop (fixes Dirt 2 crashes)
This commit is contained in:
@@ -154,6 +154,11 @@ struct XMA_CONTEXT_DATA {
|
|||||||
const bool IsConsumeOnlyContext() const {
|
const bool IsConsumeOnlyContext() const {
|
||||||
return (input_buffer_0_packet_count | input_buffer_1_packet_count) == 0;
|
return (input_buffer_0_packet_count | input_buffer_1_packet_count) == 0;
|
||||||
}
|
}
|
||||||
|
// Whether the SDC-based minimum exceeds the output buffer size.
|
||||||
|
const bool HasTightOutputBuffer() const {
|
||||||
|
return (int32_t)((subframe_decode_count * 2) - 1) >
|
||||||
|
(int32_t)output_buffer_block_count;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
static_assert_size(XMA_CONTEXT_DATA, 64);
|
static_assert_size(XMA_CONTEXT_DATA, 64);
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,16 @@ bool XmaContextNew::Work() {
|
|||||||
|
|
||||||
if (data.IsConsumeOnlyContext()) {
|
if (data.IsConsumeOnlyContext()) {
|
||||||
Consume(&output_rb, &data);
|
Consume(&output_rb, &data);
|
||||||
if (data.output_buffer_read_offset == data.output_buffer_write_offset) {
|
// Clearing contexts that match TightBufferOutput heuristic can disrupt
|
||||||
|
// playback (e.g. audio noise during races in PGR4), so we only clear
|
||||||
|
// contexts with enough output buffer headroom where empty input reliably
|
||||||
|
// indicates the stream is finished (e.g. needed for dialog completion in
|
||||||
|
// Borderlands 2 startup).
|
||||||
|
// NOTE(has207): this feels wrong to have such a heuristic but it seems to
|
||||||
|
// work for the known cases. May need to be revisited if failing cases are
|
||||||
|
// found.
|
||||||
|
if (!data.HasTightOutputBuffer() &&
|
||||||
|
data.output_buffer_read_offset == data.output_buffer_write_offset) {
|
||||||
ClearLocked(&data);
|
ClearLocked(&data);
|
||||||
}
|
}
|
||||||
data.Store(context_ptr);
|
data.Store(context_ptr);
|
||||||
@@ -138,7 +147,7 @@ bool XmaContextNew::Work() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const int32_t minimum_subframe_decode_count =
|
const int32_t minimum_subframe_decode_count =
|
||||||
(data.subframe_decode_count * 2) - 1;
|
(kBytesPerFrameChannel / kOutputBytesPerBlock) << data.is_stereo;
|
||||||
|
|
||||||
// We don't have enough space to even make one pass
|
// We don't have enough space to even make one pass
|
||||||
// Waiting for decoder to return more space.
|
// Waiting for decoder to return more space.
|
||||||
@@ -337,6 +346,13 @@ void XmaContextNew::Decode(XMA_CONTEXT_DATA* data) {
|
|||||||
static_cast<void*>(current_input_buffer), data->output_buffer_ptr,
|
static_cast<void*>(current_input_buffer), data->output_buffer_ptr,
|
||||||
data->output_buffer_block_count);
|
data->output_buffer_block_count);
|
||||||
|
|
||||||
|
// Games like Dirt 2 can kick the decoder with read offset 0 (pointing into
|
||||||
|
// the packet header) before filling in a valid offset. Clamp to the first
|
||||||
|
// valid data position to avoid rejecting the packet entirely.
|
||||||
|
if (data->input_buffer_read_offset < kBitsPerPacketHeader) {
|
||||||
|
data->input_buffer_read_offset = kBitsPerPacketHeader;
|
||||||
|
}
|
||||||
|
|
||||||
const uint32_t current_input_size = GetCurrentInputBufferSize(data);
|
const uint32_t current_input_size = GetCurrentInputBufferSize(data);
|
||||||
const uint32_t current_input_packet_count =
|
const uint32_t current_input_packet_count =
|
||||||
current_input_size / kBytesPerPacket;
|
current_input_size / kBytesPerPacket;
|
||||||
@@ -360,10 +376,42 @@ void XmaContextNew::Decode(XMA_CONTEXT_DATA* data) {
|
|||||||
|
|
||||||
const uint32_t relative_offset =
|
const uint32_t relative_offset =
|
||||||
data->input_buffer_read_offset % kBitsPerPacket;
|
data->input_buffer_read_offset % kBitsPerPacket;
|
||||||
const kPacketInfo packet_info = GetPacketInfo(packet, relative_offset);
|
kPacketInfo packet_info = GetPacketInfo(packet, relative_offset);
|
||||||
const uint32_t packet_to_skip = xma::GetPacketSkipCount(packet) + 1;
|
const uint32_t packet_to_skip = xma::GetPacketSkipCount(packet) + 1;
|
||||||
const uint32_t next_packet_index = packet_index + packet_to_skip;
|
const uint32_t next_packet_index = packet_index + packet_to_skip;
|
||||||
|
|
||||||
|
// Frame header split across packet boundary — combine packets to read
|
||||||
|
// the full 15-bit header and resolve the real frame size.
|
||||||
|
// Only detected for XMA2 packets where the header provides an authoritative
|
||||||
|
// frame count. XMA1 packets lack a frame count field so split headers
|
||||||
|
// cannot be detected — if XMA1 encoders can produce them, those frames
|
||||||
|
// will still be silently lost.
|
||||||
|
if (packet_info.current_frame_size_ == 0) {
|
||||||
|
const uint8_t* next_packet =
|
||||||
|
GetNextPacket(data, next_packet_index, current_input_packet_count);
|
||||||
|
if (!next_packet) {
|
||||||
|
// Matching split-body error handling below; correct error code unknown.
|
||||||
|
data->error_status = 4;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::memcpy(input_buffer_.data(), packet + kBytesPerPacketHeader,
|
||||||
|
kBytesPerPacketData);
|
||||||
|
std::memcpy(input_buffer_.data() + kBytesPerPacketData,
|
||||||
|
next_packet + kBytesPerPacketHeader, kBytesPerPacketData);
|
||||||
|
|
||||||
|
BitStream combined(input_buffer_.data(),
|
||||||
|
(kBitsPerPacket - kBitsPerPacketHeader) * 2);
|
||||||
|
combined.SetOffset(relative_offset - kBitsPerPacketHeader);
|
||||||
|
|
||||||
|
uint64_t frame_size = combined.Peek(kBitsPerFrameHeader);
|
||||||
|
if (frame_size == xma::kMaxFrameLength) {
|
||||||
|
// Matching split-body error handling below; correct error code unknown.
|
||||||
|
data->error_status = 4;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
packet_info.current_frame_size_ = (uint32_t)frame_size;
|
||||||
|
}
|
||||||
|
|
||||||
BitStream stream =
|
BitStream stream =
|
||||||
BitStream(current_input_buffer, (packet_index + 1) * kBitsPerPacket);
|
BitStream(current_input_buffer, (packet_index + 1) * kBitsPerPacket);
|
||||||
stream.SetOffset(data->input_buffer_read_offset);
|
stream.SetOffset(data->input_buffer_read_offset);
|
||||||
@@ -623,7 +671,15 @@ const kPacketInfo XmaContextNew::GetPacketInfo(uint8_t* packet,
|
|||||||
|
|
||||||
if (xma::IsPacketXma2Type(packet)) {
|
if (xma::IsPacketXma2Type(packet)) {
|
||||||
const uint8_t xma2_frame_count = xma::GetPacketFrameCount(packet);
|
const uint8_t xma2_frame_count = xma::GetPacketFrameCount(packet);
|
||||||
if (xma2_frame_count != packet_info.frame_count_) {
|
if (xma2_frame_count > packet_info.frame_count_) {
|
||||||
|
// Frame header split across packet boundary — scanner couldn't
|
||||||
|
// peek the full 15-bit header. Trust the XMA2 header count.
|
||||||
|
if (packet_info.current_frame_size_ == 0) {
|
||||||
|
// Current frame is the split-header frame
|
||||||
|
packet_info.current_frame_ = packet_info.frame_count_;
|
||||||
|
}
|
||||||
|
packet_info.frame_count_ = xma2_frame_count;
|
||||||
|
} else if (xma2_frame_count != packet_info.frame_count_) {
|
||||||
XELOGE(
|
XELOGE(
|
||||||
"XmaContext {}: XMA2 packet header defines different amount of "
|
"XmaContext {}: XMA2 packet header defines different amount of "
|
||||||
"frames than internally found! (Header: {} Found: {})",
|
"frames than internally found! (Header: {} Found: {})",
|
||||||
|
|||||||
Reference in New Issue
Block a user