912 lines
32 KiB
C++
912 lines
32 KiB
C++
/**
|
|
******************************************************************************
|
|
* Xenia : Xbox 360 Emulator Research Project *
|
|
******************************************************************************
|
|
* Copyright 2024 Xenia Canary. All rights reserved. *
|
|
* Released under the BSD license - see LICENSE in the root for more details. *
|
|
******************************************************************************
|
|
*/
|
|
|
|
#include "xenia/apu/xma_context_new.h"
|
|
#include "xenia/apu/xma_helpers.h"
|
|
|
|
#include "xenia/base/logging.h"
|
|
#include "xenia/base/platform.h"
|
|
#include "xenia/base/profiling.h"
|
|
|
|
extern "C" {
|
|
#if XE_COMPILER_MSVC
|
|
#pragma warning(push)
|
|
#pragma warning(disable : 4101 4244 5033)
|
|
#endif
|
|
#include "third_party/FFmpeg/libavcodec/avcodec.h"
|
|
#include "third_party/FFmpeg/libavutil/channel_layout.h"
|
|
#include "third_party/FFmpeg/libavutil/error.h"
|
|
#if XE_COMPILER_MSVC
|
|
#pragma warning(pop)
|
|
#endif
|
|
} // extern "C"
|
|
|
|
// Credits for most of this code goes to:
|
|
// https://github.com/koolkdev/libertyv/blob/master/libav_wrapper/xma2dec.c
|
|
|
|
namespace xe {
|
|
namespace apu {
|
|
|
|
XmaContextNew::XmaContextNew() = default;
|
|
|
|
XmaContextNew::~XmaContextNew() {
|
|
if (av_context_) {
|
|
avcodec_free_context(&av_context_);
|
|
}
|
|
if (av_frame_) {
|
|
av_frame_free(&av_frame_);
|
|
}
|
|
}
|
|
|
|
int XmaContextNew::Setup(uint32_t id, Memory* memory, uint32_t guest_ptr) {
|
|
id_ = id;
|
|
memory_ = memory;
|
|
guest_ptr_ = guest_ptr;
|
|
|
|
// Allocate ffmpeg stuff:
|
|
av_packet_ = av_packet_alloc();
|
|
assert_not_null(av_packet_);
|
|
av_packet_->buf = av_buffer_alloc(128 * 1024);
|
|
|
|
// find the XMA2 audio decoder
|
|
av_codec_ = avcodec_find_decoder(AV_CODEC_ID_XMAFRAMES);
|
|
if (!av_codec_) {
|
|
XELOGE("XmaContext {}: Codec not found", id);
|
|
return 1;
|
|
}
|
|
|
|
av_context_ = avcodec_alloc_context3(av_codec_);
|
|
if (!av_context_) {
|
|
XELOGE("XmaContext {}: Couldn't allocate context", id);
|
|
return 1;
|
|
}
|
|
|
|
// Initialize these to 0. They'll actually be set later.
|
|
av_context_->ch_layout = AVChannelLayout{};
|
|
av_context_->sample_rate = 0;
|
|
|
|
av_frame_ = av_frame_alloc();
|
|
if (!av_frame_) {
|
|
XELOGE("XmaContext {}: Couldn't allocate frame", id);
|
|
return 1;
|
|
}
|
|
|
|
// FYI: We're purposely not opening the codec here. That is done later.
|
|
return 0;
|
|
}
|
|
|
|
RingBuffer XmaContextNew::PrepareOutputRingBuffer(XMA_CONTEXT_DATA* data) {
|
|
const uint32_t output_capacity =
|
|
data->output_buffer_block_count * kOutputBytesPerBlock;
|
|
const uint32_t output_read_offset =
|
|
data->output_buffer_read_offset * kOutputBytesPerBlock;
|
|
const uint32_t output_write_offset =
|
|
data->output_buffer_write_offset * kOutputBytesPerBlock;
|
|
|
|
if (output_capacity > kOutputMaxSizeBytes) {
|
|
XELOGW(
|
|
"XmaContext {}: Output buffer uses more space than expected! "
|
|
"(Actual: {} Max: {})",
|
|
id(), output_capacity, kOutputMaxSizeBytes);
|
|
}
|
|
|
|
uint8_t* output_buffer = memory()->TranslatePhysical(data->output_buffer_ptr);
|
|
|
|
// Output buffers are in raw PCM samples, 256 bytes per block.
|
|
// Output buffer is a ring buffer. We need to write from the write offset
|
|
// to the read offset.
|
|
RingBuffer output_rb(output_buffer, output_capacity);
|
|
output_rb.set_read_offset(output_read_offset);
|
|
output_rb.set_write_offset(output_write_offset);
|
|
remaining_subframe_blocks_in_output_buffer_ =
|
|
(int32_t)output_rb.write_count() / kOutputBytesPerBlock;
|
|
|
|
return output_rb;
|
|
}
|
|
|
|
bool XmaContextNew::Work() {
|
|
if (!is_enabled() || !is_allocated()) {
|
|
return false;
|
|
}
|
|
|
|
std::lock_guard<xe_mutex> lock(lock_);
|
|
set_is_enabled(false);
|
|
|
|
auto context_ptr = memory()->TranslateVirtual(guest_ptr());
|
|
XMA_CONTEXT_DATA data(context_ptr);
|
|
const XMA_CONTEXT_DATA initial_data = data;
|
|
|
|
if (!data.output_buffer_valid) {
|
|
return true;
|
|
}
|
|
|
|
RingBuffer output_rb = PrepareOutputRingBuffer(&data);
|
|
|
|
if (data.IsConsumeOnlyContext()) {
|
|
// Nothing to drain — don't touch the context or we'll reset the
|
|
// game's output buffer offsets, causing stale PCM to be re-read.
|
|
if (current_frame_remaining_subframes_ == 0) {
|
|
return true;
|
|
}
|
|
XELOGAPU("XmaContext {}: Consume-only context, draining subframes", id());
|
|
Consume(&output_rb, &data);
|
|
data.output_buffer_write_offset =
|
|
output_rb.write_offset() / kOutputBytesPerBlock;
|
|
StoreContextMerged(data, initial_data, context_ptr);
|
|
return true;
|
|
}
|
|
|
|
// Minimum free blocks needed before attempting a decode.
|
|
// Use the number of subframes Consume() will actually write per iteration
|
|
// (= subframe_decode_count, clamped to 1) plus any headroom requested by
|
|
// output_buffer_padding. Using a full-frame worth of space (the old
|
|
// formula) was far too restrictive: games like TGM Ace use
|
|
// subframe_decode_count=2 on a small ring buffer and never had 8 free
|
|
// blocks available, causing the decoder to permanently stall.
|
|
const uint32_t effective_sdc =
|
|
std::max(static_cast<uint32_t>(1), data.subframe_decode_count);
|
|
const int32_t minimum_subframe_decode_count =
|
|
static_cast<int32_t>(effective_sdc) + data.output_buffer_padding;
|
|
|
|
// We don't have enough space to even make one pass
|
|
// Waiting for decoder to return more space.
|
|
if (minimum_subframe_decode_count >
|
|
remaining_subframe_blocks_in_output_buffer_) {
|
|
XELOGD("XmaContext {}: No space for subframe decoding {}/{}!", id(),
|
|
minimum_subframe_decode_count,
|
|
remaining_subframe_blocks_in_output_buffer_);
|
|
StoreContextMerged(data, initial_data, context_ptr);
|
|
return true;
|
|
}
|
|
|
|
while (remaining_subframe_blocks_in_output_buffer_ >=
|
|
minimum_subframe_decode_count) {
|
|
XELOGAPU(
|
|
"XmaContext {}: Write Count: {}, Capacity: {} - {} {} Subframes: {} "
|
|
"Padding: {}",
|
|
id(), (uint32_t)output_rb.write_count(),
|
|
remaining_subframe_blocks_in_output_buffer_,
|
|
data.input_buffer_0_valid + (data.input_buffer_1_valid << 1),
|
|
data.output_buffer_valid, data.subframe_decode_count,
|
|
data.output_buffer_padding);
|
|
|
|
Decode(&data);
|
|
Consume(&output_rb, &data);
|
|
|
|
if (!data.IsAnyInputBufferValid() || data.error_status == 4) {
|
|
XELOGAPU(
|
|
"XmaContext {}: Work loop exit - buffers_valid={} error_status={}",
|
|
id(), data.IsAnyInputBufferValid(), data.error_status);
|
|
break;
|
|
}
|
|
}
|
|
|
|
data.output_buffer_write_offset =
|
|
output_rb.write_offset() / kOutputBytesPerBlock;
|
|
|
|
XELOGAPU("XmaContext {}: Read Output: {} Write Output: {}", id(),
|
|
data.output_buffer_read_offset, data.output_buffer_write_offset);
|
|
|
|
// That's a bit misleading due to nature of ringbuffer
|
|
// when write and read offset matches it might mean that we wrote nothing
|
|
// or we fully saturated allocated space.
|
|
if (output_rb.empty()) {
|
|
XELOGAPU("XmaContext {}: Output ring buffer empty, invalidating output",
|
|
id());
|
|
data.output_buffer_valid = 0;
|
|
}
|
|
|
|
StoreContextMerged(data, initial_data, context_ptr);
|
|
return true;
|
|
}
|
|
|
|
void XmaContextNew::Enable() { set_is_enabled(true); }
|
|
|
|
void XmaContextNew::Clear() {
|
|
std::lock_guard<xe_mutex> lock(lock_);
|
|
|
|
auto context_ptr = memory()->TranslateVirtual(guest_ptr());
|
|
XMA_CONTEXT_DATA data(context_ptr);
|
|
ClearLocked(&data);
|
|
data.Store(context_ptr);
|
|
}
|
|
|
|
void XmaContextNew::ClearLocked(XMA_CONTEXT_DATA* data) {
|
|
XELOGAPU("XmaContext: reset context {}", id());
|
|
|
|
data->input_buffer_0_valid = 0;
|
|
data->input_buffer_1_valid = 0;
|
|
data->output_buffer_valid = 0;
|
|
|
|
data->input_buffer_read_offset = kBitsPerPacketHeader;
|
|
data->output_buffer_read_offset = 0;
|
|
data->output_buffer_write_offset = 0;
|
|
|
|
current_frame_remaining_subframes_ = 0;
|
|
loop_frame_output_limit_ = 0;
|
|
loop_start_skip_pending_ = false;
|
|
}
|
|
|
|
void XmaContextNew::Disable() { set_is_enabled(false); }
|
|
|
|
void XmaContextNew::Release() {
|
|
// Lock it in case the decoder thread is working on it now.
|
|
std::lock_guard<xe_mutex> lock(lock_);
|
|
assert_true(is_allocated());
|
|
|
|
set_is_allocated(false);
|
|
auto context_ptr = memory()->TranslateVirtual(guest_ptr());
|
|
std::memset(context_ptr, 0, sizeof(XMA_CONTEXT_DATA)); // Zero it.
|
|
}
|
|
|
|
int XmaContextNew::GetSampleRate(int id) {
|
|
return kIdToSampleRate[std::min(id, 3)];
|
|
}
|
|
|
|
void XmaContextNew::SwapInputBuffer(XMA_CONTEXT_DATA* data) {
|
|
// No more frames.
|
|
XELOGAPU("XmaContext: SwapInputBuffer from buffer {} to {}",
|
|
data->current_buffer, data->current_buffer ^ 1);
|
|
if (data->current_buffer == 0) {
|
|
data->input_buffer_0_valid = 0;
|
|
} else {
|
|
data->input_buffer_1_valid = 0;
|
|
}
|
|
data->current_buffer ^= 1;
|
|
data->input_buffer_read_offset = kBitsPerPacketHeader;
|
|
}
|
|
|
|
void XmaContextNew::Consume(RingBuffer* XE_RESTRICT output_rb,
|
|
const XMA_CONTEXT_DATA* const XE_RESTRICT data) {
|
|
if (!current_frame_remaining_subframes_) {
|
|
return;
|
|
}
|
|
|
|
// Check if the loop end truncation limit has been reached.
|
|
// Total subframes for this frame minus remaining gives how many have already
|
|
// been consumed. If that reaches the limit, discard the rest.
|
|
if (loop_frame_output_limit_ > 0) {
|
|
const uint8_t total_subframes =
|
|
(kBytesPerFrameChannel / kOutputBytesPerBlock) << data->is_stereo;
|
|
const uint8_t consumed =
|
|
total_subframes - current_frame_remaining_subframes_;
|
|
if (consumed >= loop_frame_output_limit_) {
|
|
// Charge headroom as if the frame completed normally so the Work()
|
|
// loop doesn't overestimate available output space.
|
|
XELOGAPU(
|
|
"XmaContext {}: Loop end truncation: discarding {} remaining "
|
|
"subframes (limit {})",
|
|
id(), current_frame_remaining_subframes_, loop_frame_output_limit_);
|
|
remaining_subframe_blocks_in_output_buffer_ -=
|
|
data->output_buffer_padding;
|
|
current_frame_remaining_subframes_ = 0;
|
|
loop_frame_output_limit_ = 0;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Guard against subframe_decode_count == 0 which would cause zero progress
|
|
// and an infinite loop in Work(). Treat 0 as 1 (minimum progress).
|
|
const uint8_t effective_sdc =
|
|
std::max(static_cast<uint32_t>(1), data->subframe_decode_count);
|
|
int8_t subframes_to_write = std::min(
|
|
(int8_t)current_frame_remaining_subframes_, (int8_t)effective_sdc);
|
|
|
|
// Clamp to loop end limit if active.
|
|
if (loop_frame_output_limit_ > 0) {
|
|
const uint8_t total_subframes =
|
|
(kBytesPerFrameChannel / kOutputBytesPerBlock) << data->is_stereo;
|
|
const uint8_t consumed =
|
|
total_subframes - current_frame_remaining_subframes_;
|
|
const int8_t remaining_until_limit =
|
|
(int8_t)(loop_frame_output_limit_ - consumed);
|
|
if (subframes_to_write > remaining_until_limit) {
|
|
subframes_to_write = remaining_until_limit;
|
|
}
|
|
}
|
|
|
|
const int8_t raw_frame_read_offset =
|
|
((kBytesPerFrameChannel / kOutputBytesPerBlock) << data->is_stereo) -
|
|
current_frame_remaining_subframes_;
|
|
|
|
output_rb->Write(
|
|
raw_frame_.data() + (kOutputBytesPerBlock * raw_frame_read_offset),
|
|
subframes_to_write * kOutputBytesPerBlock);
|
|
|
|
// Reserve extra blocks as headroom when unk_skip_decode is set.
|
|
// Only apply when the frame is fully consumed to avoid double-counting.
|
|
const int8_t headroom =
|
|
(current_frame_remaining_subframes_ - subframes_to_write == 0)
|
|
? data->output_buffer_padding
|
|
: 0;
|
|
|
|
remaining_subframe_blocks_in_output_buffer_ -= subframes_to_write + headroom;
|
|
current_frame_remaining_subframes_ -= subframes_to_write;
|
|
|
|
XELOGAPU("XmaContext {}: Consume: {} - {} - {} - {} - {}", id(),
|
|
remaining_subframe_blocks_in_output_buffer_,
|
|
data->output_buffer_write_offset, data->output_buffer_read_offset,
|
|
output_rb->write_offset(), current_frame_remaining_subframes_);
|
|
}
|
|
|
|
void XmaContextNew::Decode(XMA_CONTEXT_DATA* data) {
|
|
SCOPE_profile_cpu_f("apu");
|
|
|
|
// No available data.
|
|
if (!data->IsAnyInputBufferValid()) {
|
|
XELOGAPU("XmaContext {}: Decode skipped - no valid input buffers", id());
|
|
// data->error_status = 4;
|
|
return;
|
|
}
|
|
|
|
if (current_frame_remaining_subframes_ > 0) {
|
|
return;
|
|
}
|
|
|
|
if (!data->IsCurrentInputBufferValid()) {
|
|
XELOGAPU("XmaContext {}: Current buffer {} invalid, swapping to other",
|
|
id(), data->current_buffer);
|
|
SwapInputBuffer(data);
|
|
if (!data->IsCurrentInputBufferValid()) {
|
|
XELOGAPU("XmaContext {}: Both buffers invalid after swap, aborting",
|
|
id());
|
|
return;
|
|
}
|
|
}
|
|
|
|
uint8_t* current_input_buffer = GetCurrentInputBuffer(data);
|
|
|
|
input_buffer_.fill(0);
|
|
|
|
// Detect if we're about to decode the loop end frame (before
|
|
// UpdateLoopStatus may reset the offset).
|
|
bool is_loop_end_frame = false;
|
|
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);
|
|
}
|
|
|
|
UpdateLoopStatus(data);
|
|
|
|
if (!data->output_buffer_block_count) {
|
|
XELOGE("XmaContext {}: Error - Received 0 for output_buffer_block_count!",
|
|
id());
|
|
return;
|
|
}
|
|
|
|
XELOGAPU(
|
|
"Processing context {} (offset {}, buffer {}, ptr {:p}, output buffer "
|
|
"{:08X}, output buffer count {})",
|
|
id(), data->input_buffer_read_offset, data->current_buffer,
|
|
static_cast<void*>(current_input_buffer), data->output_buffer_ptr,
|
|
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) {
|
|
XELOGW(
|
|
"XmaContext {}: Read offset {} is inside packet header, clamping to {}",
|
|
id(), 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_packet_count =
|
|
current_input_size / kBytesPerPacket;
|
|
|
|
const int16_t packet_index =
|
|
GetPacketNumber(current_input_size, data->input_buffer_read_offset);
|
|
|
|
if (packet_index == -1) {
|
|
XELOGE("XmaContext {}: Invalid packet index. Input read offset: {}", id(),
|
|
data->input_buffer_read_offset);
|
|
return;
|
|
}
|
|
|
|
uint8_t* packet = current_input_buffer + (packet_index * kBytesPerPacket);
|
|
const uint32_t packet_first_frame_offset = xma::GetPacketFrameOffset(packet);
|
|
uint32_t relative_offset = data->input_buffer_read_offset % kBitsPerPacket;
|
|
|
|
// If the read offset is before the first frame in this packet we're in the
|
|
// tail of a split frame from the previous packet. We don't have the
|
|
// beginning of that frame so skip ahead to the first complete frame.
|
|
// This also guards against games that kick the decoder with an offset
|
|
// pointing into the packet header (e.g. Dirt 2).
|
|
if (relative_offset < packet_first_frame_offset) {
|
|
XELOGAPU(
|
|
"XmaContext {}: Skipping split frame tail in packet {} "
|
|
"(offset {} -> first frame {})",
|
|
id(), packet_index, relative_offset, packet_first_frame_offset);
|
|
data->input_buffer_read_offset =
|
|
(packet_index * kBitsPerPacket) + packet_first_frame_offset;
|
|
relative_offset = packet_first_frame_offset;
|
|
}
|
|
const uint8_t skip_count = xma::GetPacketSkipCount(packet);
|
|
|
|
// Full packet skip — no new frames begin in this packet (XMA2: 0xFF,
|
|
// XMA1: lower 8 bits of 0x7FF also reads as 0xFF). Advance to the
|
|
// next sequential packet instead of trying to parse frames.
|
|
if (skip_count == 0xFF) {
|
|
XELOGAPU("XmaContext {}: Full packet skip (0xFF) at packet {}/{}", id(),
|
|
packet_index, current_input_packet_count);
|
|
uint32_t next_input_offset = GetNextPacketReadOffset(
|
|
current_input_buffer, packet_index + 1, current_input_packet_count);
|
|
if (next_input_offset == kBitsPerPacketHeader) {
|
|
SwapInputBuffer(data);
|
|
}
|
|
data->input_buffer_read_offset = next_input_offset;
|
|
return;
|
|
}
|
|
|
|
kPacketInfo packet_info = GetPacketInfo(packet, relative_offset);
|
|
const uint32_t packet_to_skip = skip_count + 1;
|
|
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) {
|
|
XELOGAPU(
|
|
"XmaContext {}: Split frame header at packet {} boundary, "
|
|
"combining with next packet {}",
|
|
id(), packet_index, next_packet_index);
|
|
const uint8_t* next_packet =
|
|
GetNextPacket(data, next_packet_index, current_input_packet_count);
|
|
if (!next_packet) {
|
|
// Next buffer not available yet. We can't resolve the split header
|
|
// without it, so consume (swap) the current buffer and move on.
|
|
XELOGAPU(
|
|
"XmaContext {}: Split frame header at packet {}, next buffer "
|
|
"unavailable — swapping input buffer",
|
|
id(), packet_index);
|
|
SwapInputBuffer(data);
|
|
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) {
|
|
XELOGW(
|
|
"XmaContext {}: Split header resolved to kMaxFrameLength (0x7FFF), "
|
|
"setting error_status=4",
|
|
id());
|
|
// 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(current_input_buffer, (packet_index + 1) * kBitsPerPacket);
|
|
stream.SetOffset(data->input_buffer_read_offset);
|
|
|
|
const uint64_t bits_to_copy = GetAmountOfBitsToRead(
|
|
(uint32_t)stream.BitsRemaining(), packet_info.current_frame_size_);
|
|
|
|
if (bits_to_copy == 0) {
|
|
XELOGE("XmaContext {}: There is no bits to copy!", id());
|
|
SwapInputBuffer(data);
|
|
return;
|
|
}
|
|
|
|
if (packet_info.isLastFrameInPacket()) {
|
|
// Frame is a splitted frame
|
|
if (stream.BitsRemaining() < packet_info.current_frame_size_) {
|
|
const uint8_t* next_packet =
|
|
GetNextPacket(data, next_packet_index, current_input_packet_count);
|
|
|
|
if (!next_packet) {
|
|
// Error path
|
|
// Decoder probably should return error here
|
|
// Not sure what error code should be returned
|
|
data->error_status = 4;
|
|
return;
|
|
}
|
|
// Copy next packet to buffer
|
|
std::memcpy(input_buffer_.data() + kBytesPerPacketData,
|
|
next_packet + kBytesPerPacketHeader, kBytesPerPacketData);
|
|
}
|
|
}
|
|
|
|
// Copy current packet to buffer
|
|
std::memcpy(input_buffer_.data(), packet + kBytesPerPacketHeader,
|
|
kBytesPerPacketData);
|
|
|
|
stream = BitStream(input_buffer_.data(),
|
|
(kBitsPerPacket - kBitsPerPacketHeader) * 2);
|
|
stream.SetOffset(relative_offset - kBitsPerPacketHeader);
|
|
|
|
xma_frame_.fill(0);
|
|
|
|
XELOGAPU(
|
|
"XmaContext {}: Reading Frame {}/{} (size: {}) From Packet "
|
|
"{}/{}",
|
|
id(), (int32_t)packet_info.current_frame_, packet_info.frame_count_,
|
|
packet_info.current_frame_size_, packet_index,
|
|
current_input_packet_count);
|
|
|
|
const uint32_t padding_start = static_cast<uint8_t>(
|
|
stream.Copy(xma_frame_.data() + 1, packet_info.current_frame_size_));
|
|
|
|
raw_frame_.fill(0);
|
|
|
|
PrepareDecoder(data->sample_rate, bool(data->is_stereo));
|
|
PreparePacket(packet_info.current_frame_size_, padding_start);
|
|
if (DecodePacket(av_context_, av_packet_, av_frame_)) {
|
|
// dump_raw(av_frame_, id());
|
|
ConvertFrame(reinterpret_cast<const uint8_t**>(&av_frame_->data),
|
|
bool(data->is_stereo), raw_frame_.data());
|
|
current_frame_remaining_subframes_ = 4 << data->is_stereo;
|
|
|
|
// Loop end: limit output to subframes 0..loop_subframe_end.
|
|
if (is_loop_end_frame) {
|
|
loop_frame_output_limit_ = (data->loop_subframe_end + 1)
|
|
<< data->is_stereo;
|
|
XELOGAPU(
|
|
"XmaContext {}: Loop end frame - limiting output to {} subframes "
|
|
"(loop_subframe_end={})",
|
|
id(), loop_frame_output_limit_, data->loop_subframe_end);
|
|
} else {
|
|
loop_frame_output_limit_ = 0;
|
|
}
|
|
|
|
// Loop start: skip leading subframes per loop_subframe_skip.
|
|
// Reducing remaining shifts the read offset forward in Consume().
|
|
if (loop_start_skip_pending_) {
|
|
const uint8_t skip = data->loop_subframe_skip << data->is_stereo;
|
|
if (skip < current_frame_remaining_subframes_) {
|
|
XELOGAPU(
|
|
"XmaContext {}: Loop start - skipping {} leading subframes "
|
|
"(loop_subframe_skip={})",
|
|
id(), skip, data->loop_subframe_skip);
|
|
current_frame_remaining_subframes_ -= skip;
|
|
}
|
|
loop_start_skip_pending_ = false;
|
|
}
|
|
}
|
|
|
|
// Compute where to go next.
|
|
if (!packet_info.isLastFrameInPacket()) {
|
|
const uint32_t next_frame_offset =
|
|
(data->input_buffer_read_offset + bits_to_copy) % kBitsPerPacket;
|
|
|
|
XELOGAPU("XmaContext {}: Index: {}/{} - Next frame offset: {}", id(),
|
|
(int32_t)packet_info.current_frame_, packet_info.frame_count_,
|
|
next_frame_offset);
|
|
|
|
data->input_buffer_read_offset =
|
|
(packet_index * kBitsPerPacket) + next_frame_offset;
|
|
return;
|
|
}
|
|
|
|
const uint8_t* next_packet =
|
|
GetNextPacket(data, next_packet_index, current_input_packet_count);
|
|
|
|
if (!next_packet) {
|
|
XELOGAPU(
|
|
"XmaContext {}: Last frame in packet {}, next packet {} unavailable "
|
|
"(end of buffer)",
|
|
id(), packet_index, next_packet_index);
|
|
}
|
|
|
|
uint32_t next_input_offset = GetNextPacketReadOffset(
|
|
current_input_buffer, next_packet_index, current_input_packet_count);
|
|
|
|
if (next_input_offset == kBitsPerPacketHeader) {
|
|
SwapInputBuffer(data);
|
|
// We're at start of next buffer
|
|
// If it have any frame in this packet decoder should go to first frame in
|
|
// packet If it doesn't have any frame then it should immediatelly go to
|
|
// next packet
|
|
if (data->IsAnyInputBufferValid()) {
|
|
next_input_offset = xma::GetPacketFrameOffset(
|
|
memory()->TranslatePhysical(data->GetCurrentInputBufferAddress()));
|
|
|
|
if (next_input_offset > kMaxFrameSizeinBits) {
|
|
XELOGAPU(
|
|
"XmaContext {}: Next buffer contains no frames in packet! Frame "
|
|
"offset: {}",
|
|
id(), next_input_offset);
|
|
SwapInputBuffer(data);
|
|
return;
|
|
}
|
|
XELOGAPU("XmaContext {}: Next buffer first frame starts at: {}", id(),
|
|
next_input_offset);
|
|
}
|
|
}
|
|
data->input_buffer_read_offset = next_input_offset;
|
|
return;
|
|
}
|
|
|
|
// Frame & Packet searching methods
|
|
|
|
void XmaContextNew::UpdateLoopStatus(XMA_CONTEXT_DATA* data) {
|
|
if (data->loop_count == 0) {
|
|
return;
|
|
}
|
|
|
|
const uint32_t loop_start = std::max(kBitsPerPacketHeader, data->loop_start);
|
|
const uint32_t loop_end = std::max(kBitsPerPacketHeader, data->loop_end);
|
|
|
|
XELOGAPU("XmaContext {}: Looped Data: {} < {} (Start: {}) Remaining: {}",
|
|
id(), data->input_buffer_read_offset, data->loop_end,
|
|
data->loop_start, data->loop_count);
|
|
|
|
if (data->input_buffer_read_offset != loop_end) {
|
|
return;
|
|
}
|
|
|
|
data->input_buffer_read_offset = loop_start;
|
|
loop_start_skip_pending_ = true;
|
|
|
|
if (data->loop_count != 255) {
|
|
data->loop_count--;
|
|
}
|
|
}
|
|
|
|
const uint8_t* XmaContextNew::GetNextPacket(
|
|
XMA_CONTEXT_DATA* data, uint32_t next_packet_index,
|
|
uint32_t current_input_packet_count) {
|
|
if (next_packet_index < current_input_packet_count) {
|
|
return memory()->TranslatePhysical(data->GetCurrentInputBufferAddress()) +
|
|
next_packet_index * kBytesPerPacket;
|
|
}
|
|
|
|
const uint8_t next_buffer_index = data->current_buffer ^ 1;
|
|
|
|
if (!data->IsInputBufferValid(next_buffer_index)) {
|
|
return nullptr;
|
|
}
|
|
|
|
const uint32_t next_buffer_address =
|
|
data->GetInputBufferAddress(next_buffer_index);
|
|
|
|
if (!next_buffer_address) {
|
|
// This should never occur but there is always a chance
|
|
XELOGE(
|
|
"XmaContext {}: Buffer is marked as valid, but doesn't have valid "
|
|
"pointer!",
|
|
id());
|
|
return nullptr;
|
|
}
|
|
|
|
return memory()->TranslatePhysical(next_buffer_address);
|
|
}
|
|
|
|
const uint32_t XmaContextNew::GetNextPacketReadOffset(
|
|
uint8_t* buffer, uint32_t next_packet_index,
|
|
uint32_t current_input_packet_count) {
|
|
// Scan forward for the next packet that contains a new frame.
|
|
while (next_packet_index < current_input_packet_count) {
|
|
uint8_t* next_packet = buffer + (next_packet_index * kBytesPerPacket);
|
|
const uint32_t packet_frame_offset = xma::GetPacketFrameOffset(next_packet);
|
|
|
|
if (packet_frame_offset <= kMaxFrameSizeinBits) {
|
|
const uint32_t new_input_buffer_offset =
|
|
(next_packet_index * kBitsPerPacket) + packet_frame_offset;
|
|
|
|
XELOGAPU("XmaContext {}: new offset: {} packet_offset: {} packet: {}/{}",
|
|
id(), new_input_buffer_offset, packet_frame_offset,
|
|
next_packet_index, current_input_packet_count);
|
|
return new_input_buffer_offset;
|
|
}
|
|
next_packet_index++;
|
|
}
|
|
|
|
return kBitsPerPacketHeader;
|
|
}
|
|
|
|
const uint32_t XmaContextNew::GetAmountOfBitsToRead(
|
|
const uint32_t remaining_stream_bits, const uint32_t frame_size) {
|
|
return std::min(remaining_stream_bits, frame_size);
|
|
}
|
|
|
|
uint32_t XmaContextNew::GetCurrentInputBufferSize(XMA_CONTEXT_DATA* data) {
|
|
return data->GetCurrentInputBufferPacketCount() * kBytesPerPacket;
|
|
}
|
|
|
|
uint8_t* XmaContextNew::GetCurrentInputBuffer(XMA_CONTEXT_DATA* data) {
|
|
return memory()->TranslatePhysical(data->GetCurrentInputBufferAddress());
|
|
}
|
|
|
|
const kPacketInfo XmaContextNew::GetPacketInfo(uint8_t* packet,
|
|
uint32_t frame_offset) {
|
|
kPacketInfo packet_info = {};
|
|
|
|
const uint32_t first_frame_offset = xma::GetPacketFrameOffset(packet);
|
|
BitStream stream(packet, kBitsPerPacket);
|
|
stream.SetOffset(first_frame_offset);
|
|
|
|
// Handling of splitted frame
|
|
if (frame_offset < first_frame_offset) {
|
|
packet_info.current_frame_ = 0;
|
|
packet_info.current_frame_size_ = first_frame_offset - frame_offset;
|
|
}
|
|
|
|
while (true) {
|
|
if (stream.BitsRemaining() < kBitsPerFrameHeader) {
|
|
break;
|
|
}
|
|
|
|
const uint64_t frame_size = stream.Peek(kBitsPerFrameHeader);
|
|
if (frame_size == 0 || frame_size == xma::kMaxFrameLength) {
|
|
break;
|
|
}
|
|
|
|
if (stream.offset_bits() == frame_offset) {
|
|
packet_info.current_frame_ = packet_info.frame_count_;
|
|
packet_info.current_frame_size_ = (uint32_t)frame_size;
|
|
}
|
|
|
|
packet_info.frame_count_++;
|
|
|
|
if (frame_size > stream.BitsRemaining()) {
|
|
// Last frame.
|
|
break;
|
|
}
|
|
|
|
stream.Advance(frame_size - 1);
|
|
|
|
// Read the trailing bit to see if frames follow
|
|
if (stream.Read(1) == 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (xma::IsPacketXma2Type(packet)) {
|
|
const uint8_t xma2_frame_count = xma::GetPacketFrameCount(packet);
|
|
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(
|
|
"XmaContext {}: XMA2 packet header defines different amount of "
|
|
"frames than internally found! (Header: {} Found: {})",
|
|
id(), xma2_frame_count, packet_info.frame_count_);
|
|
}
|
|
}
|
|
return packet_info;
|
|
}
|
|
|
|
int16_t XmaContextNew::GetPacketNumber(size_t size, size_t bit_offset) {
|
|
if (bit_offset < kBitsPerPacketHeader) {
|
|
assert_always();
|
|
return -1;
|
|
}
|
|
|
|
if (bit_offset >= (size << 3)) {
|
|
assert_always();
|
|
return -1;
|
|
}
|
|
|
|
size_t byte_offset = bit_offset >> 3;
|
|
size_t packet_number = byte_offset / kBytesPerPacket;
|
|
|
|
return (int16_t)packet_number;
|
|
}
|
|
|
|
int XmaContextNew::PrepareDecoder(int sample_rate, bool is_two_channel) {
|
|
sample_rate = GetSampleRate(sample_rate);
|
|
|
|
// Re-initialize the context with new sample rate and channels.
|
|
uint32_t channels = is_two_channel ? 2 : 1;
|
|
if (av_context_->sample_rate != sample_rate ||
|
|
av_context_->ch_layout.nb_channels != (int)channels) {
|
|
XELOGAPU("XmaContext {}: Codec reinit: rate {} -> {}, channels {} -> {}",
|
|
id(), av_context_->sample_rate, sample_rate,
|
|
av_context_->ch_layout.nb_channels, channels);
|
|
// We have to recreate the codec context so it'll realloc whatever data it
|
|
// needs.
|
|
avcodec_free_context(&av_context_);
|
|
av_context_ = avcodec_alloc_context3(av_codec_);
|
|
|
|
av_context_->sample_rate = sample_rate;
|
|
av_channel_layout_default(&av_context_->ch_layout, channels);
|
|
av_context_->flags2 |= AV_CODEC_FLAG2_SKIP_MANUAL;
|
|
|
|
if (avcodec_open2(av_context_, av_codec_, NULL) < 0) {
|
|
XELOGE("XmaContext: Failed to reopen FFmpeg context");
|
|
return -1;
|
|
}
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void XmaContextNew::PreparePacket(const uint32_t frame_size,
|
|
const uint32_t frame_padding) {
|
|
av_packet_->data = xma_frame_.data();
|
|
av_packet_->size =
|
|
static_cast<int>(1 + ((frame_padding + frame_size) / 8) +
|
|
(((frame_padding + frame_size) % 8) ? 1 : 0));
|
|
|
|
auto padding_end = av_packet_->size * 8 - (8 + frame_padding + frame_size);
|
|
assert_true(padding_end < 8);
|
|
xma_frame_[0] = ((frame_padding & 7) << 5) | ((padding_end & 7) << 2);
|
|
}
|
|
|
|
bool XmaContextNew::DecodePacket(AVCodecContext* av_context,
|
|
const AVPacket* av_packet, AVFrame* av_frame) {
|
|
auto ret = avcodec_send_packet(av_context, av_packet);
|
|
if (ret < 0) {
|
|
char errbuf[AV_ERROR_MAX_STRING_SIZE];
|
|
av_strerror(ret, errbuf, sizeof(errbuf));
|
|
XELOGE("XmaContext {}: Error sending packet for decoding: {} ({})", id(),
|
|
errbuf, ret);
|
|
return false;
|
|
}
|
|
ret = avcodec_receive_frame(av_context, av_frame);
|
|
|
|
if (ret == AVERROR(EAGAIN)) {
|
|
// Codec needs more input before producing output (e.g. first frame warmup).
|
|
return false;
|
|
}
|
|
if (ret < 0) {
|
|
char errbuf[AV_ERROR_MAX_STRING_SIZE];
|
|
av_strerror(ret, errbuf, sizeof(errbuf));
|
|
XELOGE("XmaContext {}: Error during decoding: {} ({})", id(), errbuf, ret);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void XmaContextNew::StoreContextMerged(const XMA_CONTEXT_DATA& data,
|
|
const XMA_CONTEXT_DATA& initial_data,
|
|
uint8_t* context_ptr) {
|
|
XMA_CONTEXT_DATA fresh(context_ptr);
|
|
|
|
// DWORD 0: decoder owns loop_count, output_buffer_write_offset.
|
|
// Only clear valid flags the decoder actually consumed (was 1, now 0).
|
|
fresh.loop_count = data.loop_count;
|
|
fresh.output_buffer_write_offset = data.output_buffer_write_offset;
|
|
if (initial_data.input_buffer_0_valid && !data.input_buffer_0_valid) {
|
|
fresh.input_buffer_0_valid = 0;
|
|
}
|
|
if (initial_data.input_buffer_1_valid && !data.input_buffer_1_valid) {
|
|
fresh.input_buffer_1_valid = 0;
|
|
}
|
|
|
|
// DWORD 1: decoder conditionally clears output_buffer_valid
|
|
if (initial_data.output_buffer_valid && !data.output_buffer_valid) {
|
|
fresh.output_buffer_valid = 0;
|
|
}
|
|
|
|
// DWORD 2: decoder owns input_buffer_read_offset, error_status
|
|
fresh.input_buffer_read_offset = data.input_buffer_read_offset;
|
|
fresh.error_status = data.error_status;
|
|
|
|
// DWORD 4: decoder owns current_buffer
|
|
fresh.current_buffer = data.current_buffer;
|
|
|
|
// DWORD 9: decoder owns output_buffer_read_offset (reset by ClearLocked)
|
|
fresh.output_buffer_read_offset = data.output_buffer_read_offset;
|
|
|
|
fresh.Store(context_ptr);
|
|
}
|
|
|
|
} // namespace apu
|
|
} // namespace xe
|