diff --git a/core/libs/prx/libSceAjm.native/CMakeLists.txt b/core/libs/prx/libSceAjm.native/CMakeLists.txt new file mode 100644 index 000000000..83bd7327e --- /dev/null +++ b/core/libs/prx/libSceAjm.native/CMakeLists.txt @@ -0,0 +1,31 @@ +enable_language(C) + +set(LIBATRAC9_DIR ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) +add_library(atrac9 STATIC EXCLUDE_FROM_ALL + ${LIBATRAC9_DIR}/band_extension.c + ${LIBATRAC9_DIR}/bit_allocation.c + ${LIBATRAC9_DIR}/bit_reader.c + ${LIBATRAC9_DIR}/decinit.c + ${LIBATRAC9_DIR}/decoder.c + ${LIBATRAC9_DIR}/huffCodes.c + ${LIBATRAC9_DIR}/imdct.c + ${LIBATRAC9_DIR}/libatrac9.c + ${LIBATRAC9_DIR}/quantization.c + ${LIBATRAC9_DIR}/scale_factors.c + ${LIBATRAC9_DIR}/tables.c + ${LIBATRAC9_DIR}/unpack.c + ${LIBATRAC9_DIR}/utility.c +) +set_target_properties(atrac9 PROPERTIES POSITION_INDEPENDENT_CODE ON) + +add_library(libSceAjm.native SHARED EXCLUDE_FROM_ALL + ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Ajm.cpp +) + +target_include_directories(libSceAjm.native PRIVATE + ${LIBS_INCLUDE_DIR} + ${LIBATRAC9_DIR} +) +target_link_libraries(libSceAjm.native PRIVATE atrac9 libc libkernel) +configure_windows_unwind(libSceAjm.native) diff --git a/core/libs/prx/libSceAjm.native/Export.cpp b/core/libs/prx/libSceAjm.native/Export.cpp index 66117f2b5..c2150a64a 100644 --- a/core/libs/prx/libSceAjm.native/Export.cpp +++ b/core/libs/prx/libSceAjm.native/Export.cpp @@ -2,36 +2,22 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include +#include + +// Entry points this library does not implement: reported before the unimplemented-call exception so an +// APS5_TRACE_AJM run shows which one a title reached. +static void AjmStub(const char* name) { + std::fprintf(stderr, "[ajm] unimplemented %s called\n", name); + NotImplemented_nid_no_patch(name); +} extern "C" { int APS5_VABI sceAjmBatchCancel(uint32_t context, uint32_t batch) { (void)context; (void)batch; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAjmBatchErrorDump(const AjmBatchInfo* info, AjmBatchError* error) { - (void)info; - (void)error; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAjmBatchInitialize(void* buffer, size_t size, AjmBatchInfo* info) { - (void)buffer; - (void)size; - (void)info; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAjmBatchJobClearContext(AjmBatchInfo* info, uint32_t instance, void* result) { - (void)info; - (void)instance; - (void)result; - NotImplemented_nid_no_patch(__func__); + AjmStub(__func__); return 0; } @@ -43,7 +29,7 @@ int APS5_VABI sceAjmBatchJobControl(AjmBatchInfo* info, uint32_t instance, uint6 (void)sideband_input_size; (void)sideband_output; (void)sideband_output_size; - NotImplemented_nid_no_patch(__func__); + AjmStub(__func__); return 0; } @@ -55,7 +41,7 @@ int APS5_VABI sceAjmBatchJobDecode(AjmBatchInfo* info, uint32_t instance, const (void)pcm_output; (void)pcm_output_size; (void)result; - NotImplemented_nid_no_patch(__func__); + AjmStub(__func__); return 0; } @@ -67,7 +53,7 @@ int APS5_VABI sceAjmBatchJobDecodeSingle(AjmBatchInfo* info, uint32_t instance, (void)pcm_output; (void)pcm_output_size; (void)result; - NotImplemented_nid_no_patch(__func__); + AjmStub(__func__); return 0; } @@ -79,7 +65,7 @@ int APS5_VABI sceAjmBatchJobDecodeSplit(AjmBatchInfo* info, uint32_t instance, c (void)output_buffers; (void)output_buffers_num; (void)result; - NotImplemented_nid_no_patch(__func__); + AjmStub(__func__); return 0; } @@ -91,7 +77,7 @@ int APS5_VABI sceAjmBatchJobEncode(AjmBatchInfo* info, uint32_t instance, const (void)bitstream_output; (void)bitstream_output_size; (void)result; - NotImplemented_nid_no_patch(__func__); + AjmStub(__func__); return 0; } @@ -100,7 +86,7 @@ int APS5_VABI sceAjmBatchJobGetCodecInfo(AjmBatchInfo* info, uint32_t instance, (void)instance; (void)result; (void)result_size; - NotImplemented_nid_no_patch(__func__); + AjmStub(__func__); return 0; } @@ -108,7 +94,7 @@ int APS5_VABI sceAjmBatchJobGetGaplessDecode(AjmBatchInfo* info, uint32_t instan (void)info; (void)instance; (void)result; - NotImplemented_nid_no_patch(__func__); + AjmStub(__func__); return 0; } @@ -116,7 +102,7 @@ int APS5_VABI sceAjmBatchJobGetInfo(AjmBatchInfo* info, uint32_t instance, void* (void)info; (void)instance; (void)result; - NotImplemented_nid_no_patch(__func__); + AjmStub(__func__); return 0; } @@ -124,25 +110,7 @@ int APS5_VABI sceAjmBatchJobGetResampleInfo(AjmBatchInfo* info, uint32_t instanc (void)info; (void)instance; (void)result; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAjmBatchJobGetStatistics(AjmBatchInfo* info, float interval, void* result) { - (void)info; - (void)interval; - (void)result; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAjmBatchJobInitialize(AjmBatchInfo* info, uint32_t instance, const void* codec_parameters, size_t codec_parameters_size, void* result) { - (void)info; - (void)instance; - (void)codec_parameters; - (void)codec_parameters_size; - (void)result; - NotImplemented_nid_no_patch(__func__); + AjmStub(__func__); return 0; } @@ -156,31 +124,7 @@ int APS5_VABI sceAjmBatchJobRun(AjmBatchInfo* info, uint32_t instance, uint64_t (void)data_output_size; (void)sideband_output; (void)sideband_output_size; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAjmBatchJobRunSplit(AjmBatchInfo* info, uint32_t instance, uint64_t flags, const AjmBuffer* input_buffers, size_t input_buffers_num, const AjmBuffer* output_buffers, size_t output_buffers_num, void* sideband_output, size_t sideband_output_size) { - (void)info; - (void)instance; - (void)flags; - (void)input_buffers; - (void)input_buffers_num; - (void)output_buffers; - (void)output_buffers_num; - (void)sideband_output; - (void)sideband_output_size; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAjmBatchJobSetGaplessDecode(AjmBatchInfo* info, uint32_t instance, const void* gapless_decode, int reset, void* result) { - (void)info; - (void)instance; - (void)gapless_decode; - (void)reset; - (void)result; - NotImplemented_nid_no_patch(__func__); + AjmStub(__func__); return 0; } @@ -190,7 +134,7 @@ int APS5_VABI sceAjmBatchJobSetResampleParameters(AjmBatchInfo* info, uint32_t i (void)ratio; (void)flags; (void)result; - NotImplemented_nid_no_patch(__func__); + AjmStub(__func__); return 0; } @@ -201,99 +145,19 @@ int APS5_VABI sceAjmBatchJobSetResampleParametersEx(AjmBatchInfo* info, uint32_t (void)ratio_change_per_sample; (void)flags; (void)result; - NotImplemented_nid_no_patch(__func__); + AjmStub(__func__); return 0; } -int APS5_VABI sceAjmBatchStart(uint32_t context, const AjmBatchInfo* info, int priority, AjmBatchError* error, uint32_t* batch) { - (void)context; - (void)info; - (void)priority; +const char* APS5_VABI sceAjmStrError(int error) { (void)error; - (void)batch; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAjmBatchWait(uint32_t context, uint32_t batch, uint32_t timeout, AjmBatchError* error) { - (void)context; - (void)batch; - (void)timeout; - (void)error; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAjmDecAt9ParseConfigData(const void* config_data, AjmDecAt9ConfigDataInfo* config_info) { - (void)config_data; - (void)config_info; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAjmFinalize(uint32_t context) { - (void)context; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAjmInitialize(int64_t reserved, uint32_t* context) { - (void)reserved; - (void)context; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAjmInstanceCreate(uint32_t context, uint32_t codec, uint64_t flags, uint32_t* instance) { - (void)context; - (void)codec; - (void)flags; - (void)instance; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAjmInstanceDestroy(uint32_t context, uint32_t instance) { - (void)context; - (void)instance; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAjmMemoryRegister(uint32_t context, void* ptr, size_t pages) { - (void)context; - (void)ptr; - (void)pages; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAjmMemoryUnregister(uint32_t context, void* ptr) { - (void)context; - (void)ptr; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAjmModuleRegister(uint32_t context, uint32_t codec, int64_t reserved) { - (void)context; - (void)codec; - (void)reserved; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAjmModuleUnregister(uint32_t context, uint32_t codec) { - (void)context; - (void)codec; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -const char* sceAjmStrError(int error) { - (void)error; - NotImplemented_nid_no_patch(__func__); + AjmStub(__func__); return nullptr; } +int APS5_VABI sceAjmDecMp3ParseFrame() { + AjmStub(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp new file mode 100644 index 000000000..1bfc8f760 --- /dev/null +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -0,0 +1,573 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include "libatrac9.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// The batch buffer and its job records are private to this library: the guest only reserves the +// memory and reads the "used bytes" field of AjmBatchInfo. Sideband results written back to guest +// memory follow the SDK layouts. +namespace { + +constexpr int SCE_AJM_ERROR_INVALID_INSTANCE = static_cast(0x80930003); +constexpr int SCE_AJM_ERROR_INVALID_PARAMETER = static_cast(0x80930005); +constexpr int SCE_AJM_ERROR_OUT_OF_RESOURCES = static_cast(0x80930007); + +constexpr std::int32_t AJM_RESULT_NOT_INITIALIZED = 0x00000001; +constexpr std::int32_t AJM_RESULT_INVALID_DATA = 0x00000002; +constexpr std::int32_t AJM_RESULT_INVALID_PARAMETER = 0x00000004; +constexpr std::int32_t AJM_RESULT_PARTIAL_INPUT = 0x00000008; +constexpr std::int32_t AJM_RESULT_NOT_ENOUGH_ROOM = 0x00000010; +constexpr std::int32_t AJM_RESULT_CODEC_ERROR = 0x40000000; + +constexpr std::uint32_t CODEC_AT9 = 1; + +// APS5_TRACE_AJM=1 logs every job the title submits and what the library writes back. +bool TraceEnabled() { + static const bool enabled = std::getenv("APS5_TRACE_AJM") != nullptr; + return enabled; +} + +#define AJM_TRACE(...) \ + do { \ + if (TraceEnabled()) std::fprintf(stderr, __VA_ARGS__); \ + } while (0) + +constexpr std::uint64_t RUN_MULTIPLE_FRAMES = 1ull << 12; +constexpr std::uint64_t SIDEBAND_GAPLESS_DECODE = 1ull << 45; +constexpr std::uint64_t SIDEBAND_FORMAT = 1ull << 46; +constexpr std::uint64_t SIDEBAND_STREAM = 1ull << 47; + +struct SidebandResult { + std::int32_t result; + std::int32_t internalResult; +}; + +struct SidebandStream { + std::int32_t inputConsumed; + std::int32_t outputWritten; + std::uint64_t totalDecodedSamples; +}; + +struct SidebandFormat { + std::uint32_t numChannels; + std::uint32_t channelMask; + std::uint32_t sampleRate; + std::uint32_t sampleFormat; + std::uint32_t bitrate; + std::uint32_t reserved; +}; + +struct SidebandGaplessDecode { + std::uint32_t totalSamples; + std::uint16_t skipSamples; + std::uint16_t skippedSamples; +}; + +struct SidebandMultipleFrames { + std::uint32_t numFrames; + std::uint32_t reserved; +}; + +struct Instance { + std::uint32_t codec = 0; + std::uint64_t flags = 0; + void* decoder = nullptr; + Atrac9CodecInfo info{}; + bool initialized = false; + std::uint32_t superframeRemaining = 0; + std::uint32_t frameInSuperframe = 0; + std::uint64_t totalDecodedSamples = 0; + SidebandGaplessDecode gapless{}; + bool flagsReported = false; + + ~Instance() { + if (decoder) Atrac9ReleaseHandle(decoder); + } +}; + +std::mutex g_lock; +std::map> g_instances; +std::atomic g_nextContext{1}; +std::atomic g_nextInstance{1}; +std::atomic g_nextBatch{1}; + +enum class JobKind : std::uint32_t { + Initialize = 1, + ClearContext = 2, + SetGaplessDecode = 3, + Run = 4, + GetStatistics = 5, +}; + +struct JobHeader { + JobKind kind; + std::uint32_t bytes; + std::uint32_t instance; + std::uint32_t reserved; + std::uint64_t flags; + void* sideband; + std::uint64_t sidebandSize; + std::uint64_t parameterSize; + std::uint32_t inputCount; + std::uint32_t outputCount; + std::uint8_t parameters[16]; +}; + +int Append(AjmBatchInfo* info, const JobHeader& header, const AjmBuffer* inputs, const AjmBuffer* outputs) { + if (!info || !info->p_buffer) return SCE_AJM_ERROR_INVALID_PARAMETER; + const std::size_t bytes = sizeof(JobHeader) + (header.inputCount + header.outputCount) * sizeof(AjmBuffer); + if (bytes > info->size - info->offset) return SCE_AJM_ERROR_OUT_OF_RESOURCES; + auto* cursor = static_cast(info->p_buffer) + info->offset; + JobHeader record = header; + record.bytes = static_cast(bytes); + std::memcpy(cursor, &record, sizeof(record)); + cursor += sizeof(record); + if (header.inputCount) std::memcpy(cursor, inputs, header.inputCount * sizeof(AjmBuffer)); + cursor += header.inputCount * sizeof(AjmBuffer); + if (header.outputCount) std::memcpy(cursor, outputs, header.outputCount * sizeof(AjmBuffer)); + info->offset += bytes; + return 0; +} + +JobHeader MakeHeader(JobKind kind, std::uint32_t instance, void* sideband, std::uint64_t sidebandSize) { + JobHeader header{}; + header.kind = kind; + header.instance = instance; + header.sideband = sideband; + header.sidebandSize = sidebandSize; + return header; +} + +// The SDK's speaker masks (SCE_AJM_CHANNELMASK_*) for a decoded channel count. +std::uint32_t ChannelMask(std::size_t channels) { + switch (channels) { + case 1: return 0x4; + case 2: return 0x3; + case 4: return 0x33; + case 6: return 0x3F; + case 8: return 0x63F; + default: return 0; + } +} + +void WriteResult(void* sideband, std::uint64_t size, std::int32_t result) { + if (!sideband || size < sizeof(SidebandResult)) return; + const SidebandResult value{result, 0}; + std::memcpy(sideband, &value, sizeof(value)); +} + +Instance* Find(std::uint32_t id) { + const auto found = g_instances.find(id); + return found == g_instances.end() ? nullptr : found->second.get(); +} + +// Replaces the instance's decoder with a fresh one for its configuration, at a superframe boundary. The +// decoder handle keeps its position within the superframe, which a re-initialization does not clear. +bool ResetDecoder(Instance& instance) { + if (instance.decoder) Atrac9ReleaseHandle(instance.decoder); + instance.decoder = Atrac9GetHandle(); + instance.superframeRemaining = 0; + instance.frameInSuperframe = 0; + unsigned char config[ATRAC9_CONFIG_DATA_SIZE]; + std::memcpy(config, instance.info.configData, sizeof(config)); + return Atrac9InitDecoder(instance.decoder, config) == 0; +} + +std::int32_t InitializeInstance(Instance& instance, const std::uint8_t* parameters, std::uint64_t size) { + if (instance.codec != CODEC_AT9) { + instance.initialized = true; + return 0; + } + if (size < ATRAC9_CONFIG_DATA_SIZE) return AJM_RESULT_INVALID_PARAMETER; + std::memcpy(instance.info.configData, parameters, ATRAC9_CONFIG_DATA_SIZE); + if (!ResetDecoder(instance)) { + instance.initialized = false; + return AJM_RESULT_INVALID_PARAMETER; + } + Atrac9GetCodecInfo(instance.decoder, &instance.info); + instance.initialized = true; + instance.totalDecodedSamples = 0; + instance.gapless = {}; + return 0; +} + +// Titles may hand the decoder a whole .at9 file. Like the console's decoder, a RIFF/WAVE header at a +// frame boundary is skipped up to the data chunk's payload and counted as consumed input. Returns the +// payload offset, or 0 when the bytes do not start a complete RIFF header. +std::size_t RiffDataOffset(const std::uint8_t* data, std::size_t size) { + if (size < 12 || std::memcmp(data, "RIFF", 4) != 0 || std::memcmp(data + 8, "WAVE", 4) != 0) return 0; + std::size_t cursor = 12; + while (cursor + 8 <= size) { + std::uint32_t chunkBytes = 0; + std::memcpy(&chunkBytes, data + cursor + 4, sizeof(chunkBytes)); + if (std::memcmp(data + cursor, "data", 4) == 0) return cursor + 8; + cursor += 8 + static_cast(chunkBytes) + (chunkBytes & 1u); + } + return 0; +} + +// Whether this is the instance's first run since it was (re)initialized; used to report its flags once. +bool consumedFirstRun(Instance& instance) { + if (instance.flagsReported) return false; + instance.flagsReported = true; + return true; +} + +// Decodes AT9 frames from the concatenated inputs into the concatenated outputs in the instance's PCM +// encoding. +void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { + std::vector input; + for (std::uint32_t index = 0; index < job.inputCount; ++index) { + const auto* data = static_cast(inputs[index].ptr); + input.insert(input.end(), data, data + inputs[index].size); + } + std::size_t outputCapacity = 0; + for (std::uint32_t index = 0; index < job.outputCount; ++index) outputCapacity += outputs[index].size; + + const auto channels = static_cast(instance.info.channels); + const auto frameSamples = static_cast(instance.info.frameSamples); + // Instance flags bits 7..9 select the PCM encoding: 0 16-bit, 1 32-bit integer, 2 float. + const auto encoding = static_cast((instance.flags >> 7u) & 7u); + const std::size_t sampleBytes = encoding == 0 ? sizeof(std::int16_t) : sizeof(std::int32_t); + const std::size_t frameBytes = frameSamples * channels * sampleBytes; + std::vector pcm(frameBytes); + static const bool traceFlags = std::getenv("APS5_TRACE_AJM") != nullptr; + if (traceFlags && instance.totalDecodedSamples == 0 && consumedFirstRun(instance)) std::fprintf(stderr, "[ajm] instance %u flags 0x%llx (encoding %u)\n", job.instance, static_cast(instance.flags), encoding); + + std::int32_t result = 0; + int decodeStatus = 0; + std::size_t consumed = 0; + std::size_t produced = 0; + std::uint32_t frames = 0; + std::uint32_t outputIndex = 0; + std::size_t outputOffset = 0; + const auto emit = [&](const std::uint8_t* source, std::size_t bytes) { + while (bytes > 0 && outputIndex < job.outputCount) { + const std::size_t room = outputs[outputIndex].size - outputOffset; + const std::size_t chunk = std::min(room, bytes); + std::memcpy(static_cast(outputs[outputIndex].ptr) + outputOffset, source, chunk); + source += chunk; + bytes -= chunk; + produced += chunk; + outputOffset += chunk; + if (outputOffset == outputs[outputIndex].size) { + ++outputIndex; + outputOffset = 0; + } + } + }; + + // A superframe is a fixed superframeSize bytes holding framesInSuperframe frames packed back to back, + // each byte aligned but of its own size (the encoder moves bits between the frames of a superframe), + // with padding after the last one. The decoder reports each frame's size, so frames are walked with + // it and the padding is skipped after the last frame. Like the console's decoder, a superframe is + // decoded only once it is wholly present: a trailing partial superframe stays unconsumed and is + // reported as partial input, and the title resubmits it from the consumed offset. + const auto superframeSize = static_cast(instance.info.superframeSize); + const auto framesInSuperframe = static_cast(std::max(1, instance.info.framesInSuperframe)); + for (;;) { + if (instance.superframeRemaining == 0) consumed += RiffDataOffset(input.data() + consumed, input.size() - consumed); + const std::size_t needed = instance.superframeRemaining == 0 ? superframeSize : instance.superframeRemaining; + if (input.size() - consumed < needed) { + if (input.size() != consumed) result |= AJM_RESULT_PARTIAL_INPUT; + break; + } + if (outputCapacity - produced < frameBytes) { + if (frames == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; + break; + } + if (instance.superframeRemaining == 0) { + instance.superframeRemaining = static_cast(superframeSize); + instance.frameInSuperframe = 0; + } + int used = 0; + switch (encoding) { + case 0: decodeStatus = Atrac9Decode(instance.decoder, input.data() + consumed, reinterpret_cast(pcm.data()), &used, 0); break; + case 1: decodeStatus = Atrac9DecodeS32(instance.decoder, input.data() + consumed, reinterpret_cast(pcm.data()), &used, 0); break; + default: decodeStatus = Atrac9DecodeF32(instance.decoder, input.data() + consumed, reinterpret_cast(pcm.data()), &used, 0); break; + } + if (decodeStatus != 0 || used <= 0 || static_cast(used) > instance.superframeRemaining) { + result |= AJM_RESULT_INVALID_DATA; + // The decoder tracks its position in the superframe; a fresh one starts the next superframe. + ResetDecoder(instance); + break; + } + consumed += static_cast(used); + instance.superframeRemaining -= static_cast(used); + if (++instance.frameInSuperframe == framesInSuperframe) { + consumed += instance.superframeRemaining; + instance.superframeRemaining = 0; + } + ++frames; + + std::size_t first = 0; + std::size_t count = frameSamples; + if (instance.gapless.skippedSamples < instance.gapless.skipSamples) { + const std::size_t skip = std::min(count, instance.gapless.skipSamples - instance.gapless.skippedSamples); + instance.gapless.skippedSamples = static_cast(instance.gapless.skippedSamples + skip); + first += skip; + count -= skip; + } + if (instance.gapless.totalSamples != 0) { + const std::uint64_t remaining = instance.gapless.totalSamples > instance.totalDecodedSamples ? instance.gapless.totalSamples - instance.totalDecodedSamples : 0; + count = static_cast(std::min(count, remaining)); + } + emit(pcm.data() + first * channels * sampleBytes, count * channels * sampleBytes); + instance.totalDecodedSamples += count; + if ((job.flags & RUN_MULTIPLE_FRAMES) == 0) break; + } + + if (TraceEnabled()) { + std::fprintf(stderr, "[ajm] instance %u run flags 0x%llx: %zu input bytes in %u buffers, %zu output bytes in %u buffers, sideband %llu bytes; superframe %d (%d frames), %d channels, %d samples/frame, %d Hz, config %02x %02x %02x %02x -> result 0x%x, decoder status 0x%x, %u frames, consumed %zu, produced %zu, total samples %llu, gapless total %u skip %u skipped %u, input", + job.instance, static_cast(job.flags), input.size(), job.inputCount, outputCapacity, job.outputCount, static_cast(job.sidebandSize), instance.info.superframeSize, instance.info.framesInSuperframe, instance.info.channels, instance.info.frameSamples, instance.info.samplingRate, + instance.info.configData[0], instance.info.configData[1], instance.info.configData[2], instance.info.configData[3], static_cast(result), static_cast(decodeStatus), frames, consumed, produced, static_cast(instance.totalDecodedSamples), instance.gapless.totalSamples, instance.gapless.skipSamples, instance.gapless.skippedSamples); + for (std::size_t i = 0; i < input.size() && i < 16; ++i) std::fprintf(stderr, " %02x", input[i]); + std::fprintf(stderr, "\n"); + } + auto* sideband = static_cast(job.sideband); + std::size_t offset = 0; + const auto write = [&](const void* value, std::size_t size) { + if (!sideband || offset + size > job.sidebandSize) return; + std::memcpy(sideband + offset, value, size); + offset += size; + }; + const SidebandResult status{result, 0}; + write(&status, sizeof(status)); + if (job.flags & SIDEBAND_STREAM) { + const SidebandStream stream{static_cast(consumed), static_cast(produced), instance.totalDecodedSamples}; + write(&stream, sizeof(stream)); + } + if (job.flags & SIDEBAND_FORMAT) { + const SidebandFormat format{static_cast(channels), ChannelMask(channels), static_cast(instance.info.samplingRate), encoding, 0, 0}; + write(&format, sizeof(format)); + } + if (job.flags & SIDEBAND_GAPLESS_DECODE) write(&instance.gapless, sizeof(instance.gapless)); + if (job.flags & RUN_MULTIPLE_FRAMES) { + const SidebandMultipleFrames multiple{frames, 0}; + write(&multiple, sizeof(multiple)); + } +} + +void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { + if (job.kind == JobKind::GetStatistics) { + AJM_TRACE("[ajm] statistics job: sideband %llu bytes\n", static_cast(job.sidebandSize)); + if (job.sideband && job.sidebandSize) std::memset(job.sideband, 0, std::min(job.sidebandSize, 24)); + return; + } + std::lock_guard lock(g_lock); + auto* instance = Find(job.instance); + if (!instance) { + AJM_TRACE("[ajm] job kind %u on unknown instance %u\n", static_cast(job.kind), job.instance); + WriteResult(job.sideband, job.sidebandSize, AJM_RESULT_INVALID_PARAMETER); + return; + } + switch (job.kind) { + case JobKind::Initialize: { + const std::int32_t result = InitializeInstance(*instance, job.parameters, job.parameterSize); + AJM_TRACE("[ajm] instance %u initialize: %llu parameter bytes (%02x %02x %02x %02x %02x %02x %02x %02x), sideband %llu bytes -> result 0x%x, %d channels, %d Hz, superframe %d bytes (%d frames of %d samples)\n", job.instance, static_cast(job.parameterSize), job.parameters[0], job.parameters[1], job.parameters[2], job.parameters[3], job.parameters[4], job.parameters[5], job.parameters[6], job.parameters[7], static_cast(job.sidebandSize), static_cast(result), instance->info.channels, instance->info.samplingRate, instance->info.superframeSize, instance->info.framesInSuperframe, instance->info.frameSamples); + WriteResult(job.sideband, job.sidebandSize, result); + break; + } + case JobKind::ClearContext: + AJM_TRACE("[ajm] instance %u clear context (sideband %llu bytes)\n", job.instance, static_cast(job.sidebandSize)); + instance->totalDecodedSamples = 0; + instance->gapless.skippedSamples = 0; + if (instance->decoder && instance->initialized) ResetDecoder(*instance); + WriteResult(job.sideband, job.sidebandSize, 0); + break; + case JobKind::SetGaplessDecode: { + SidebandGaplessDecode gapless{}; + std::memcpy(&gapless, job.parameters, sizeof(gapless)); + AJM_TRACE("[ajm] instance %u set gapless decode: total %u, skip %u, reset %llu (sideband %llu bytes)\n", job.instance, gapless.totalSamples, gapless.skipSamples, static_cast(job.flags), static_cast(job.sidebandSize)); + instance->gapless.totalSamples = gapless.totalSamples; + instance->gapless.skipSamples = gapless.skipSamples; + if (job.flags) instance->gapless.skippedSamples = 0; + WriteResult(job.sideband, job.sidebandSize, 0); + break; + } + case JobKind::Run: + if (!instance->initialized) { + AJM_TRACE("[ajm] instance %u run before initialize\n", job.instance); + WriteResult(job.sideband, job.sidebandSize, AJM_RESULT_NOT_INITIALIZED); + } else if (instance->codec == CODEC_AT9) { + RunAt9(*instance, job, inputs, outputs); + } else { + AJM_TRACE("[ajm] instance %u run on codec %u without a decoder\n", job.instance, instance->codec); + WriteResult(job.sideband, job.sidebandSize, AJM_RESULT_CODEC_ERROR); + } + break; + default: + AJM_TRACE("[ajm] instance %u unknown job kind %u\n", job.instance, static_cast(job.kind)); + WriteResult(job.sideband, job.sidebandSize, AJM_RESULT_INVALID_PARAMETER); + break; + } +} + +} + +extern "C" { + +int APS5_VABI sceAjmInitialize(int64_t reserved, uint32_t* context) { + (void)reserved; + if (!context) return SCE_AJM_ERROR_INVALID_PARAMETER; + *context = g_nextContext.fetch_add(1, std::memory_order_relaxed); + return 0; +} + +int APS5_VABI sceAjmFinalize(uint32_t context) { + (void)context; + return 0; +} + +int APS5_VABI sceAjmModuleRegister(uint32_t context, uint32_t codec, int64_t reserved) { + (void)context; + (void)codec; + (void)reserved; + return 0; +} + +int APS5_VABI sceAjmModuleUnregister(uint32_t context, uint32_t codec) { + (void)context; + (void)codec; + return 0; +} + +int APS5_VABI sceAjmMemoryRegister(uint32_t context, void* ptr, size_t pages) { + (void)context; + (void)ptr; + (void)pages; + return 0; +} + +int APS5_VABI sceAjmMemoryUnregister(uint32_t context, void* ptr) { + (void)context; + (void)ptr; + return 0; +} + +int APS5_VABI sceAjmInstanceCreate(uint32_t context, uint32_t codec, uint64_t flags, uint32_t* instance) { + (void)context; + if (!instance) return SCE_AJM_ERROR_INVALID_PARAMETER; + static std::atomic reportedCodecs{0}; + if (codec != CODEC_AT9 && codec < 32 && (reportedCodecs.fetch_or(1u << codec) & (1u << codec)) == 0) + std::fprintf(stderr, "[ajm] codec %u has no decoder; its jobs report codec errors\n", codec); + auto created = std::make_unique(); + created->codec = codec; + created->flags = flags; + const std::uint32_t id = (codec << 14) | (g_nextInstance.fetch_add(1, std::memory_order_relaxed) & 0x3FFF); + std::lock_guard lock(g_lock); + g_instances[id] = std::move(created); + *instance = id; + AJM_TRACE("[ajm] instance create: context %u, codec %u, flags 0x%llx -> instance %u\n", context, codec, static_cast(flags), id); + return 0; +} + +int APS5_VABI sceAjmInstanceDestroy(uint32_t context, uint32_t instance) { + (void)context; + AJM_TRACE("[ajm] instance %u destroy\n", instance); + std::lock_guard lock(g_lock); + return g_instances.erase(instance) ? 0 : SCE_AJM_ERROR_INVALID_INSTANCE; +} + +int APS5_VABI sceAjmDecAt9ParseConfigData(const void* config_data, AjmDecAt9ConfigDataInfo* config_info) { + if (!config_data || !config_info) return SCE_AJM_ERROR_INVALID_PARAMETER; + void* decoder = Atrac9GetHandle(); + unsigned char config[ATRAC9_CONFIG_DATA_SIZE]; + std::memcpy(config, config_data, sizeof(config)); + const bool valid = Atrac9InitDecoder(decoder, config) == 0; + Atrac9CodecInfo info{}; + if (valid) Atrac9GetCodecInfo(decoder, &info); + Atrac9ReleaseHandle(decoder); + AJM_TRACE("[ajm] parse config %02x %02x %02x %02x -> %s, %d channels, %d Hz, superframe %d bytes (%d frames of %d samples)\n", config[0], config[1], config[2], config[3], valid ? "ok" : "invalid", info.channels, info.samplingRate, info.superframeSize, info.framesInSuperframe, info.frameSamples); + if (!valid) return SCE_AJM_ERROR_INVALID_PARAMETER; + config_info->channels = static_cast(info.channels); + config_info->sample_rate = static_cast(info.samplingRate); + config_info->frame_samples_per_channel = static_cast(info.frameSamples); + config_info->superframe_samples_per_channel = static_cast(info.frameSamples * info.framesInSuperframe); + config_info->superframe_size = static_cast(info.superframeSize); + return 0; +} + +int APS5_VABI sceAjmBatchInitialize(void* buffer, size_t size, AjmBatchInfo* info) { + if (!buffer || !info) return SCE_AJM_ERROR_INVALID_PARAMETER; + info->p_buffer = buffer; + info->offset = 0; + info->size = size; + return 0; +} + +int APS5_VABI sceAjmBatchJobInitialize(AjmBatchInfo* info, uint32_t instance, const void* codec_parameters, size_t codec_parameters_size, void* result) { + auto header = MakeHeader(JobKind::Initialize, instance, result, sizeof(SidebandResult)); + header.parameterSize = std::min(codec_parameters_size, sizeof(header.parameters)); + if (codec_parameters && header.parameterSize) std::memcpy(header.parameters, codec_parameters, header.parameterSize); + return Append(info, header, nullptr, nullptr); +} + +int APS5_VABI sceAjmBatchJobClearContext(AjmBatchInfo* info, uint32_t instance, void* result) { + return Append(info, MakeHeader(JobKind::ClearContext, instance, result, sizeof(SidebandResult)), nullptr, nullptr); +} + +int APS5_VABI sceAjmBatchJobSetGaplessDecode(AjmBatchInfo* info, uint32_t instance, const void* gapless_decode, int reset, void* result) { + auto header = MakeHeader(JobKind::SetGaplessDecode, instance, result, sizeof(SidebandResult)); + if (gapless_decode) std::memcpy(header.parameters, gapless_decode, sizeof(SidebandGaplessDecode)); + header.flags = reset ? 1 : 0; + return Append(info, header, nullptr, nullptr); +} + +int APS5_VABI sceAjmBatchJobRunSplit(AjmBatchInfo* info, uint32_t instance, uint64_t flags, const AjmBuffer* input_buffers, size_t input_buffers_num, const AjmBuffer* output_buffers, size_t output_buffers_num, void* sideband_output, size_t sideband_output_size) { + auto header = MakeHeader(JobKind::Run, instance, sideband_output, sideband_output_size); + header.flags = flags; + header.inputCount = static_cast(input_buffers_num); + header.outputCount = static_cast(output_buffers_num); + return Append(info, header, input_buffers, output_buffers); +} + +int APS5_VABI sceAjmBatchJobGetStatistics(AjmBatchInfo* info, float interval, void* result) { + (void)interval; + return Append(info, MakeHeader(JobKind::GetStatistics, 0, result, 24), nullptr, nullptr); +} + +int APS5_VABI sceAjmBatchStart(uint32_t context, const AjmBatchInfo* info, int priority, AjmBatchError* error, uint32_t* batch) { + (void)context; + (void)priority; + if (!info || !batch) return SCE_AJM_ERROR_INVALID_PARAMETER; + AJM_TRACE("[ajm] batch start: context %u, priority %d, %llu of %llu buffer bytes used\n", context, priority, static_cast(info->offset), static_cast(info->size)); + const auto* cursor = static_cast(info->p_buffer); + const auto* end = cursor + info->offset; + while (cursor < end) { + JobHeader job; + std::memcpy(&job, cursor, sizeof(job)); + const auto* buffers = reinterpret_cast(cursor + sizeof(JobHeader)); + Execute(job, buffers, buffers + job.inputCount); + cursor += job.bytes; + } + if (error) std::memset(error, 0, sizeof(*error)); + *batch = g_nextBatch.fetch_add(1, std::memory_order_relaxed); + return 0; +} + +int APS5_VABI sceAjmBatchWait(uint32_t context, uint32_t batch, uint32_t timeout, AjmBatchError* error) { + (void)context; + (void)batch; + (void)timeout; + if (error) std::memset(error, 0, sizeof(*error)); + return 0; +} + +int APS5_VABI sceAjmBatchErrorDump(const AjmBatchInfo* info, AjmBatchError* error) { + (void)info; + if (error) std::memset(error, 0, sizeof(*error)); + return 0; +} + +} diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index 64478c4b4..b9ecce49e 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -185,8 +185,23 @@ static const void* prepareBuffer(const Port& port, const void* data, std::vector } static void queueAudio(Port& port, const void* data) { - if (port.device == 0 || data == nullptr) { - APS5_INVALID_ARG_EX; + // Without an SDL device there is nothing to queue: the callers already sleep for the block's + // duration so the game's timing holds. A null pointer is the documented way to wait until the + // port's queued audio has been output (sceAudioOutOutput(handle, NULL)); it queues nothing. + if (port.device == 0) return; + if (data == nullptr) { + const std::uint64_t waitStart = sceKernelGetProcessTime(); + while (SDL_GetQueuedAudioSize(port.device) > 0) { + if (sceKernelGetProcessTime() - waitStart > DRAIN_TIMEOUT_US) { + SDL_ClearQueuedAudio(port.device); + break; + } + struct timespec req{}; + req.tv_sec = 0; + req.tv_nsec = static_cast(DRAIN_SLEEP_US * 1000ULL); + nanosleep(&req, nullptr); + } + return; } std::vector prepareBuf; @@ -391,12 +406,12 @@ int APS5_VABI sceAudioOutOutputs(AudioOutOutputParam* param, std::uint32_t num) } for (std::uint32_t i = 0; i < num; i++) { - queueAudio(*getPort(param[i].handle), param[i].ptr); + if (auto* port = getPort(param[i].handle)) queueAudio(*port, param[i].ptr); } const std::uint64_t done = sceKernelGetProcessTime(); for (std::uint32_t i = 0; i < num; i++) { - getPort(param[i].handle)->lastOutputTime = done; + if (auto* port = getPort(param[i].handle)) port->lastOutputTime = done; } return static_cast(first.samplesNum); diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp index 8720752dd..85de18a57 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp @@ -1,64 +1,277 @@ +#include +#include +#include #include #include +#include +#include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "AudioOut2Internal.hpp" + +// An AudioOut2 context is the hardware output queue: every push mixes the ports' current grain +// (num_grains samples) and appends it to a queue of queue_depth grains that plays in real time. The +// title paces its mixer on that queue (Demon's Souls polls GetQueueLevel and pushes non-blocking +// whenever a slot is free), so the level must follow the output clock: with an SDL device open the +// device's own queue is the hardware queue, otherwise a wall-clock model of it stands in. + +using Clock = std::chrono::steady_clock; + +static constexpr std::size_t CONTEXT_MEMORY = 0x10000; +static constexpr std::uint32_t DEFAULT_MAX_PORTS = 16; +// Pushes, polls and advances traced individually before the trace falls back to the per-second summary. +static constexpr std::uint64_t CALL_TRACE_FULL = 16; +// Silence queued ahead of the first grain (and again whenever the SDL queue ran dry) so scheduling +// jitter of the pushing thread does not starve the device; the queue level reported to the title +// counts only what lies beyond it. +static constexpr std::uint32_t CUSHION_MS = 40; +// The SDL queue is not allowed to run further ahead than this; grains beyond it are dropped. +static constexpr std::uint32_t MAX_QUEUED_MS = 250; +// A blocking push on a full queue gives up after this long. +static constexpr std::chrono::milliseconds FULL_WAIT_TIMEOUT{200}; +static constexpr std::chrono::milliseconds FULL_WAIT_STEP{1}; +static constexpr std::uint16_t DEVICE_SAMPLES = 512; +// The summed ports (a 7.1 bed folded to stereo plus the object ports) peak above full scale in the +// intro cutscene (2.1 measured); the device takes float and clips hard, so the mix is attenuated +// and clamped. +static constexpr float MASTER_GAIN = 0.5f; +static constexpr std::size_t OUTPUT_FRAME_BYTES = AUDIO_OUT2_OUTPUT_CHANNELS * sizeof(float); +static constexpr std::uint32_t OUTPUT_BYTES_PER_MS = AUDIO_OUT2_SAMPLE_RATE * OUTPUT_FRAME_BYTES / 1000; + +bool AudioOut2TraceEnabled() { + static const bool enabled = std::getenv("APS5_TRACE_AUDIOOUT2") != nullptr; + return enabled; +} + +double AudioOut2TraceSeconds() { + static const auto start = Clock::now(); + return std::chrono::duration(Clock::now() - start).count(); +} + +static AudioOut2Context* FromHandle(AudioOut2ContextHandle ctx) { + return reinterpret_cast(ctx); +} + +static Clock::duration GrainDuration(const AudioOut2Context& context) { + return std::chrono::duration_cast(std::chrono::duration(static_cast(context.grain) / AUDIO_OUT2_SAMPLE_RATE)); +} + +static std::uint32_t GrainBytes(const AudioOut2Context& context) { + return static_cast(context.grain * OUTPUT_FRAME_BYTES); +} + +static std::uint32_t SdlQueuedMs(const AudioOut2Context& context) { + return context.device ? SDL_GetQueuedAudioSize(context.device) / OUTPUT_BYTES_PER_MS : 0; +} + +// Retires the modelled grains whose playback finished by now. The caller holds the context lock. +static void Drain(AudioOut2Context& context, Clock::time_point now) { + const auto duration = GrainDuration(context); + while (context.queued > 0) { + const auto oldestEnd = context.playHead - duration * (context.queued - 1); + if (oldestEnd > now) break; + context.queued--; + } + if (context.queued == 0 && context.playHead < now) context.playHead = now; +} + +// Grains queued and not yet played, as the title sees them. The caller holds the context lock. +static std::uint32_t QueueLevel(AudioOut2Context& context, Clock::time_point now) { + Drain(context, now); + if (context.device == 0) return context.queued; + const auto queuedBytes = SDL_GetQueuedAudioSize(context.device); + const auto cushionBytes = CUSHION_MS * OUTPUT_BYTES_PER_MS; + const auto pending = queuedBytes > cushionBytes ? queuedBytes - cushionBytes : 0; + const auto grainBytes = GrainBytes(context); + return std::min(context.queueDepth, (pending + grainBytes - 1) / grainBytes); +} + +static void OpenDevice(AudioOut2Context& context) { + if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { + AUDIOOUT2_TRACE("SDL audio init failed: %s\n", SDL_GetError()); + return; + } + SDL_AudioSpec desired{}; + desired.freq = static_cast(AUDIO_OUT2_SAMPLE_RATE); + desired.format = AUDIO_F32SYS; + desired.channels = AUDIO_OUT2_OUTPUT_CHANNELS; + desired.samples = DEVICE_SAMPLES; + desired.callback = nullptr; + SDL_AudioSpec obtained{}; + // No format change is allowed: SDL converts to the device's native format itself, so the queue + // always takes stereo float at 48 kHz. + context.device = SDL_OpenAudioDevice(nullptr, 0, &desired, &obtained, 0); + if (context.device == 0) { + AUDIOOUT2_TRACE("SDL_OpenAudioDevice failed: %s\n", SDL_GetError()); + return; + } + SDL_PauseAudioDevice(context.device, 0); + AUDIOOUT2_TRACE("SDL device %u opened: %d Hz, format 0x%x, %u channels, %u samples\n", context.device, obtained.freq, obtained.format, obtained.channels, obtained.samples); +} + +static void CloseDevice(AudioOut2Context& context) { + if (context.device != 0 && SDL_WasInit(SDL_INIT_AUDIO) != 0) { + SDL_ClearQueuedAudio(context.device); + SDL_CloseAudioDevice(context.device); + } + context.device = 0; +} + +// Mixes the ports' current grain and queues it on the SDL device. The caller holds the lock. +static std::uint32_t Render(AudioOut2Context& context) { + std::fill(context.mix.begin(), context.mix.end(), 0.0f); + const auto mixed = AudioOut2MixPorts(context, context.mix.data(), context.grain); + for (float& sample : context.mix) { + sample = std::clamp(sample * MASTER_GAIN, -1.0f, 1.0f); + if (AudioOut2TraceEnabled()) context.summaryPeak = std::max(context.summaryPeak, std::abs(sample)); + } + if (context.device == 0) return mixed; + const auto queuedBytes = SDL_GetQueuedAudioSize(context.device); + if (queuedBytes > MAX_QUEUED_MS * OUTPUT_BYTES_PER_MS) { + context.dropped++; + return mixed; + } + if (queuedBytes == 0) { + static const std::vector silence(static_cast(CUSHION_MS) * AUDIO_OUT2_SAMPLE_RATE / 1000 * AUDIO_OUT2_OUTPUT_CHANNELS, 0.0f); + context.primes++; + SDL_QueueAudio(context.device, silence.data(), static_cast(silence.size() * sizeof(float))); + } + SDL_QueueAudio(context.device, context.mix.data(), GrainBytes(context)); + return mixed; +} + +static void TraceSummary(AudioOut2Context& context, Clock::time_point now) { + if (!AudioOut2TraceEnabled()) return; + if (context.summaryStart == Clock::time_point{}) { + context.summaryStart = now; + return; + } + const auto elapsed = std::chrono::duration(now - context.summaryStart).count(); + if (elapsed < 1.0) return; + std::fprintf(stderr, "[audioout2] t=%.3f ctx %p: %.1f pushes/s, %.1f advances/s, %.1f queue polls/s, hw queue %u/%u, sdl queue %u ms, mix peak %.3f; totals: pushes %llu (%llu blocking, %llu queue-full rejects), primes %llu, dropped %llu\n", + AudioOut2TraceSeconds(), static_cast(&context), static_cast(context.summaryPushes) / elapsed, static_cast(context.summaryAdvances) / elapsed, + static_cast(context.summaryPolls) / elapsed, QueueLevel(context, now), context.queueDepth, SdlQueuedMs(context), static_cast(context.summaryPeak), + static_cast(context.pushes), static_cast(context.blockingPushes), static_cast(context.fullRejects), + static_cast(context.primes), static_cast(context.dropped)); + context.summaryStart = now; + context.summaryPushes = 0; + context.summaryAdvances = 0; + context.summaryPolls = 0; + context.summaryPeak = 0.0f; +} extern "C" { +// The title's per-tick step between setting the ports' data and pushing. The ports are mixed at push +// time from the buffers they currently point at, and the push paces the clock, so nothing is due here. int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle ctx) { - (void)ctx; - NotImplemented_nid_no_patch(__func__); + auto* context = FromHandle(ctx); + if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; + std::lock_guard lock(context->lock); + context->advances++; + context->summaryAdvances++; + if (context->advances <= CALL_TRACE_FULL) AUDIOOUT2_TRACE("t=%.3f Advance ctx %p (pushes so far %llu)\n", AudioOut2TraceSeconds(), static_cast(context), static_cast(context->pushes)); return 0; } int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam* params, void* buffer, size_t buffer_size, AudioOut2ContextHandle* ctx) { - (void)params; - (void)buffer; - (void)buffer_size; - (void)ctx; - NotImplemented_nid_no_patch(__func__); + if (!params || !ctx) return SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT; + auto* context = new AudioOut2Context(); + context->grain = params->num_grains ? params->num_grains : AUDIO_OUT2_DEFAULT_GRAIN; + context->queueDepth = params->queue_depth ? params->queue_depth : 1; + context->playHead = Clock::now(); + context->mix.assign(static_cast(context->grain) * AUDIO_OUT2_OUTPUT_CHANNELS, 0.0f); + AUDIOOUT2_TRACE("t=%.3f ContextCreate: max_ports=%u max_object_ports=%u guarantee_object_ports=%u queue_depth=%u num_grains=%u flags=0x%x buffer=%p size=%zu -> ctx %p: %u-sample grains (%.2f ms), %u queued, %u Hz stereo float output\n", + AudioOut2TraceSeconds(), params->max_ports, params->max_object_ports, params->guarantee_object_ports, params->queue_depth, params->num_grains, params->flags, buffer, buffer_size, + static_cast(context), context->grain, 1000.0 * context->grain / AUDIO_OUT2_SAMPLE_RATE, context->queueDepth, AUDIO_OUT2_SAMPLE_RATE); + OpenDevice(*context); + *ctx = reinterpret_cast(context); return 0; } int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle ctx) { - (void)ctx; - NotImplemented_nid_no_patch(__func__); + auto* context = FromHandle(ctx); + if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; + AUDIOOUT2_TRACE("t=%.3f ContextDestroy ctx %p after %llu pushes\n", AudioOut2TraceSeconds(), static_cast(context), static_cast(context->pushes)); + { + std::lock_guard lock(context->lock); + CloseDevice(*context); + } + AudioOut2ReleasePorts(*context); + delete context; return 0; } -int APS5_VABI sceAudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint32_t* queue_level, uint32_t* available_queues) { - (void)ctx; - (void)queue_level; - (void)available_queues; - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint32_t* queue_level, uint32_t* available_queue) { + auto* context = FromHandle(ctx); + if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; + std::lock_guard lock(context->lock); + const auto level = QueueLevel(*context, Clock::now()); + if (queue_level) *queue_level = level; + if (available_queue) *available_queue = context->queueDepth - level; + context->queueLevelPolls++; + context->summaryPolls++; + if (context->queueLevelPolls <= CALL_TRACE_FULL) AUDIOOUT2_TRACE("t=%.3f GetQueueLevel ctx %p -> level %u, available %u\n", AudioOut2TraceSeconds(), static_cast(context), level, context->queueDepth - level); return 0; } int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t blocking) { - (void)ctx; - (void)blocking; - NotImplemented_nid_no_patch(__func__); + auto* context = FromHandle(ctx); + if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; + std::unique_lock lock(context->lock); + auto now = Clock::now(); + const auto waitStart = now; + while (QueueLevel(*context, now) >= context->queueDepth) { + if (!blocking) { + context->fullRejects++; + if (context->fullRejects <= CALL_TRACE_FULL) AUDIOOUT2_TRACE("t=%.3f Push ctx %p non-blocking on a full queue (%u): rejected\n", AudioOut2TraceSeconds(), static_cast(context), context->queueDepth); + return SCE_AUDIO_OUT2_ERROR_QUEUE_FULL; + } + if (now - waitStart > FULL_WAIT_TIMEOUT) break; + lock.unlock(); + std::this_thread::sleep_for(FULL_WAIT_STEP); + lock.lock(); + now = Clock::now(); + } + context->queued++; + context->playHead += GrainDuration(*context); + const auto mixed = Render(*context); + context->pushes++; + context->summaryPushes++; + if (blocking) context->blockingPushes++; + if (context->pushes <= CALL_TRACE_FULL) { + AUDIOOUT2_TRACE("t=%.3f Push ctx %p blocking=%u: grain from %u ports, hw queue %u/%u, sdl queue %u ms\n", AudioOut2TraceSeconds(), static_cast(context), blocking, + mixed, QueueLevel(*context, now), context->queueDepth, SdlQueuedMs(*context)); + } + TraceSummary(*context, now); return 0; } int APS5_VABI sceAudioOut2ContextQueryMemory(const AudioOut2ContextParam* params, size_t* memory_size) { - (void)params; - (void)memory_size; - NotImplemented_nid_no_patch(__func__); + if (!params || !memory_size) return SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT; + *memory_size = CONTEXT_MEMORY; return 0; } int APS5_VABI sceAudioOut2ContextResetParam(AudioOut2ContextParam* params) { - (void)params; - NotImplemented_nid_no_patch(__func__); + if (!params) return SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT; + std::memset(params, 0, sizeof(*params)); + params->max_ports = DEFAULT_MAX_PORTS; + params->queue_depth = 1; + params->num_grains = AUDIO_OUT2_DEFAULT_GRAIN; return 0; } int APS5_VABI sceAudioOut2ContextSetAttributes(AudioOut2ContextHandle ctx, const AudioOut2Attribute* attributes, uint32_t num) { - (void)ctx; - (void)attributes; - (void)num; - NotImplemented_nid_no_patch(__func__); + auto* context = FromHandle(ctx); + if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; + if (!attributes && num != 0) return SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT; + for (uint32_t index = 0; index < num; index++) { + AUDIOOUT2_TRACE("t=%.3f ContextSetAttributes ctx %p: id=0x%x size=%zu value=%p (ignored)\n", AudioOut2TraceSeconds(), static_cast(context), attributes[index].attribute_id, attributes[index].value_size, attributes[index].value); + } return 0; } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp new file mode 100644 index 000000000..611d3e726 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp @@ -0,0 +1,95 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAUDIOOUT_SRC_AUDIOOUT2INTERNAL_HPP +#define CORE_LIBS_PRX_LIBSCEAUDIOOUT_SRC_AUDIOOUT2INTERNAL_HPP + +#include +#include +#include +#include +#include +#include + +#include "SDL.h" +#include "SceTypes.hpp" + +// Shared by the AudioOut2 context and port files. +// +// APS5_TRACE_AUDIOOUT2=1 prints the title's use of the API to stderr: context and port parameters, every +// attribute the title sets (first few per port, then sampled), the first pushes and queue polls, and a +// once-per-second summary with the push rate and the SDL queue level. +bool AudioOut2TraceEnabled(); +// Seconds since the first AudioOut2 call, for the trace. +double AudioOut2TraceSeconds(); + +#define AUDIOOUT2_TRACE(...) \ + do { \ + if (AudioOut2TraceEnabled()) std::fprintf(stderr, "[audioout2] " __VA_ARGS__); \ + } while (0) + +static constexpr int SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT = static_cast(0x80260502); +static constexpr int SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE = static_cast(0x80260503); +// Not recovered from a title or SDK header: follows the 0x802605xx pattern of the codes above. Returned +// by a non-blocking push when the modelled hardware queue is full. +static constexpr int SCE_AUDIO_OUT2_ERROR_QUEUE_FULL = static_cast(0x80260507); + +static constexpr std::uint32_t AUDIO_OUT2_SAMPLE_RATE = 48000; +// The grain a push carries, in samples per channel: a context's num_grains (Demon's Souls: 256, 5.33 ms). +static constexpr std::uint32_t AUDIO_OUT2_DEFAULT_GRAIN = 256; +static constexpr std::uint32_t AUDIO_OUT2_OUTPUT_CHANNELS = 2; +static constexpr std::uint32_t AUDIO_OUT2_PORT_CHANNELS_MAX = 8; + +struct AudioOut2Context; + +struct AudioOut2Port { + bool used = false; + AudioOut2Context* context = nullptr; + std::uint16_t type = 0; + std::uint32_t dataFormat = 0; + std::uint32_t samplingFreq = 0; + std::uint32_t flags = 0; + // Channel count decoded from dataFormat; 0 when the format is not understood (the port is then + // not rendered). Samples are float. + std::uint32_t channels = 0; + // The PCM buffer (one grain, float, interleaved) the title last handed over through the data + // attribute. It is guest memory the title rewrites every tick, so it is read when the context + // mixes, not when it is set. + const float* data = nullptr; + float volume[AUDIO_OUT2_PORT_CHANNELS_MAX] = {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; + std::uint64_t dataSets = 0; + std::uint64_t attributeTraces = 0; +}; + +struct AudioOut2Context { + std::mutex lock; + std::uint32_t grain = AUDIO_OUT2_DEFAULT_GRAIN; + std::uint32_t queueDepth = 1; + // Fallback hardware queue model for a context without an SDL device: pushes that have not + // finished playing, and the time the last of them finishes; playback is real time, so the queue + // drains as the wall clock advances. With a device the SDL queue is the hardware queue. + std::uint32_t queued = 0; + std::chrono::steady_clock::time_point playHead; + SDL_AudioDeviceID device = 0; + // Stereo float mix of the ports for one push. + std::vector mix; + // Trace counters. + std::uint64_t pushes = 0; + std::uint64_t blockingPushes = 0; + std::uint64_t fullRejects = 0; + std::uint64_t advances = 0; + std::uint64_t queueLevelPolls = 0; + std::uint64_t primes = 0; + std::uint64_t dropped = 0; + std::uint64_t summaryPushes = 0; + std::uint64_t summaryAdvances = 0; + std::uint64_t summaryPolls = 0; + // Largest sample magnitude of the grains mixed since the last summary: the mixed signal's level. + float summaryPeak = 0.0f; + std::chrono::steady_clock::time_point summaryStart; +}; + +// Mixes every port of the context that carries PCM data into out (stereo float, frames frames), summing +// onto the zeroed buffer. Returns the number of ports mixed. +std::uint32_t AudioOut2MixPorts(const AudioOut2Context& context, float* out, std::uint32_t frames); +// Forgets the ports of a context being destroyed. +void AudioOut2ReleasePorts(const AudioOut2Context& context); + +#endif diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp index d90c5b40a..546ac704e 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp @@ -1,35 +1,171 @@ +#include #include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "AudioOut2Internal.hpp" + +static constexpr std::uint16_t OUTPUT_MAIN = 1; +static constexpr std::int16_t VOLUME_MAX = 127; +// Attribute sets are traced in full for the first few calls of a port, then once per this many. +static constexpr std::uint64_t ATTRIBUTE_TRACE_FULL = 8; +static constexpr std::uint64_t ATTRIBUTE_TRACE_EVERY = 2000; + +// Port attributes as Demon's Souls sets them every tick (APS5_TRACE_AUDIOOUT2): id 0 carries the +// grain's PCM as an 8-byte value holding the buffer pointer, id 1 one float gain per channel. Ids 5, +// 8 and 9 (a u32, a u32 and a byte on object and voice ports) are not understood and ignored. +static constexpr std::uint32_t ATTRIBUTE_DATA = 0; +static constexpr std::uint32_t ATTRIBUTE_VOLUME = 1; + +// data_format bits 8..11 carry the channel count: the title opens 0x100 (mono object ports), +// 0x200 (stereo) and 0x880 (7.1 bed). The buffers behind them are float, as their spacing shows +// (1024 bytes per 256-sample mono grain). The low byte's meaning is not known. +static constexpr std::uint32_t FORMAT_CHANNELS_SHIFT = 8; +static constexpr std::uint32_t FORMAT_CHANNELS_MASK = 0xFu; + +static std::mutex g_portsLock; +// Grows on demand: the title opens its bed ports plus max_object_ports object ports at once. +static std::vector g_ports; + +static AudioOut2Port* FromHandle(AudioOut2PortHandle handle) { + const auto index = handle - 1; + if (handle == 0 || index >= g_ports.size() || !g_ports[index].used) return nullptr; + return &g_ports[index]; +} + +// 8-channel order FL FR C LFE RL RR SL SR (a swapped rear/side pair order sums the same): the rear +// and side pairs fold into the front at -3 dB, the centre into both sides, and the LFE is dropped. +static constexpr float DOWNMIX_GAIN = 0.7071f; + +static void AccumulatePort(const AudioOut2Port& port, float* out, std::uint32_t frames) { + const auto ch = port.channels; + const float* volume = port.volume; + for (std::uint32_t frame = 0; frame < frames; frame++) { + const float* in = port.data + static_cast(frame) * ch; + float left = 0.0f; + float right = 0.0f; + if (ch == 1) { + left = right = in[0] * volume[0]; + } else if (ch < 8) { + left = in[0] * volume[0]; + right = in[1] * volume[1]; + } else { + const float centre = in[2] * volume[2] * DOWNMIX_GAIN; + left = in[0] * volume[0] + centre + (in[4] * volume[4] + in[6] * volume[6]) * DOWNMIX_GAIN; + right = in[1] * volume[1] + centre + (in[5] * volume[5] + in[7] * volume[7]) * DOWNMIX_GAIN; + } + out[frame * AUDIO_OUT2_OUTPUT_CHANNELS] += left; + out[frame * AUDIO_OUT2_OUTPUT_CHANNELS + 1] += right; + } +} + +std::uint32_t AudioOut2MixPorts(const AudioOut2Context& context, float* out, std::uint32_t frames) { + std::lock_guard lock(g_portsLock); + std::uint32_t mixed = 0; + for (const auto& port : g_ports) { + if (!port.used || port.context != &context || port.data == nullptr || port.channels == 0) continue; + AccumulatePort(port, out, frames); + mixed++; + } + return mixed; +} + +void AudioOut2ReleasePorts(const AudioOut2Context& context) { + std::lock_guard lock(g_portsLock); + for (auto& port : g_ports) { + if (port.used && port.context == &context) port = AudioOut2Port{}; + } +} + +static void TraceAttribute(AudioOut2PortHandle handle, const AudioOut2Port& port, const AudioOut2Attribute& attribute, const char* verdict) { + if (!AudioOut2TraceEnabled()) return; + char values[128] = ""; + if (attribute.value != nullptr) { + std::size_t used = 0; + const auto words = std::min(attribute.value_size / sizeof(std::uint32_t), 4); + for (std::size_t index = 0; index < words && used < sizeof(values); index++) { + std::uint32_t bits = 0; + float value = 0.0f; + std::memcpy(&bits, static_cast(attribute.value) + index * sizeof(bits), sizeof(bits)); + std::memcpy(&value, &bits, sizeof(value)); + used += static_cast(std::snprintf(values + used, sizeof(values) - used, " %08x(%g)", bits, static_cast(value))); + } + } + std::fprintf(stderr, "[audioout2] t=%.3f port %llu set attribute id=0x%x size=%zu value=%p%s: %s (data sets so far %llu)\n", + AudioOut2TraceSeconds(), static_cast(handle), attribute.attribute_id, attribute.value_size, attribute.value, values, verdict, + static_cast(port.dataSets)); +} extern "C" { int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut2PortParam* params, AudioOut2PortHandle* port) { - (void)ctx; - (void)params; - (void)port; - NotImplemented_nid_no_patch(__func__); + if (!ctx) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; + if (!params || !port) return SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_portsLock); + std::size_t index = 0; + while (index < g_ports.size() && g_ports[index].used) index++; + if (index == g_ports.size()) g_ports.emplace_back(); + auto& entry = g_ports[index]; + entry = AudioOut2Port{}; + entry.used = true; + entry.context = reinterpret_cast(ctx); + entry.type = params->port_type; + entry.dataFormat = params->data_format; + entry.samplingFreq = params->sampling_freq; + entry.flags = params->flags; + entry.channels = (params->data_format >> FORMAT_CHANNELS_SHIFT) & FORMAT_CHANNELS_MASK; + if (entry.channels > AUDIO_OUT2_PORT_CHANNELS_MAX) entry.channels = 0; + *port = static_cast(index) + 1; + AUDIOOUT2_TRACE("t=%.3f PortCreate ctx=%llx -> port %llu: type=0x%x data_format=0x%x (%u float ch) sampling_freq=%u flags=0x%x user=%llx\n", + AudioOut2TraceSeconds(), static_cast(ctx), static_cast(*port), params->port_type, params->data_format, + entry.channels, params->sampling_freq, params->flags, static_cast(params->user_handle)); return 0; } int APS5_VABI sceAudioOut2PortDestroy(AudioOut2PortHandle port) { - (void)port; - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(g_portsLock); + auto* entry = FromHandle(port); + if (!entry) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; + AUDIOOUT2_TRACE("t=%.3f PortDestroy port %llu (data sets %llu)\n", AudioOut2TraceSeconds(), static_cast(port), static_cast(entry->dataSets)); + *entry = AudioOut2Port{}; return 0; } int APS5_VABI sceAudioOut2PortGetState(AudioOut2PortHandle port, AudioOut2PortState* state) { - (void)port; - (void)state; - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(g_portsLock); + if (!FromHandle(port)) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; + if (!state) return SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT; + state->output = OUTPUT_MAIN; + state->num_channels = AUDIO_OUT2_OUTPUT_CHANNELS; + state->volume = VOLUME_MAX; + state->reroute_counter = 0; + state->flags = 0; return 0; } int APS5_VABI sceAudioOut2PortSetAttributes(AudioOut2PortHandle port, const AudioOut2Attribute* attributes, uint32_t num) { - (void)port; - (void)attributes; - (void)num; - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(g_portsLock); + auto* entry = FromHandle(port); + if (!entry) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; + if (!attributes && num != 0) return SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT; + for (uint32_t index = 0; index < num; index++) { + const auto& attribute = attributes[index]; + const char* verdict = "ignored"; + if (attribute.value == nullptr) { + verdict = "null value"; + } else if (attribute.attribute_id == ATTRIBUTE_DATA && attribute.value_size == sizeof(entry->data)) { + std::memcpy(&entry->data, attribute.value, sizeof(entry->data)); + entry->dataSets++; + verdict = "pcm data pointer"; + } else if (attribute.attribute_id == ATTRIBUTE_VOLUME && entry->channels != 0 && attribute.value_size == entry->channels * sizeof(float)) { + std::memcpy(entry->volume, attribute.value, attribute.value_size); + verdict = "volume"; + } + const auto traces = entry->attributeTraces++; + if (traces < ATTRIBUTE_TRACE_FULL || traces % ATTRIBUTE_TRACE_EVERY == 0) TraceAttribute(port, *entry, attribute, verdict); + } return 0; } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp index 8e3275ad3..5f9850726 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp @@ -49,9 +49,15 @@ size_t APS5_VABI sceAudioOut2GetSpeakerArrayMemorySize(uint32_t num_speakers, ui } int APS5_VABI sceAudioOut2GetSpeakerInfo(AudioOut2SpeakerInfo* info, uint32_t flags) { - (void)info; (void)flags; - NotImplemented_nid_no_patch(__func__); + if (!info) return static_cast(0x80260502); + constexpr std::uint8_t SpeakerTypeStereo = 0; + constexpr std::uint32_t FrontLeftAndRight = 0x3; + *info = AudioOut2SpeakerInfo{}; + info->type = SpeakerTypeStereo; + info->available_bits = FrontLeftAndRight; + info->speaker_angle[0] = {-30, 0}; + info->speaker_angle[1] = {30, 0}; return 0; } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp index 021ddc497..b690814de 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp @@ -4,13 +4,12 @@ extern "C" { int APS5_VABI sceAudioOut2Initialize(void) { - NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceAudioOut2GetSystemState(AudioOut2SystemState* state) { - (void)state; - NotImplemented_nid_no_patch(__func__); + if (!state) return static_cast(0x80260502); + state->loudness = 0.0f; return 0; } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp index 8373b6838..a139d2262 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp @@ -1,19 +1,23 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include + +static constexpr int SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT = static_cast(0x80260502); + +static std::atomic g_nextUser{1}; extern "C" { int APS5_VABI sceAudioOut2UserCreate(uint32_t user_id, AudioOut2UserHandle* handle) { (void)user_id; - (void)handle; - NotImplemented_nid_no_patch(__func__); + if (!handle) return SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT; + *handle = g_nextUser.fetch_add(1, std::memory_order_relaxed); return 0; } int APS5_VABI sceAudioOut2UserDestroy(AudioOut2UserHandle handle) { (void)handle; - NotImplemented_nid_no_patch(__func__); return 0; }