From 768da28afb03cfb43c0135e6f9c6763881f3be5a Mon Sep 17 00:00:00 2001 From: tarfouss3 <149015782+tarfouss3@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:25:14 +0000 Subject: [PATCH] feat(audio): implement AJM split decode, resample, control and AudioOut2 mastering Implements 7 exports that called NotImplemented: sceAjmBatchJobDecodeSplit, sceAjmBatchJobSetResampleParametersEx, sceAjmStrError, sceAjmBatchJobControl, sceAudioOut2MasteringInit, sceAudioOut2MasteringSetParam, sceAudioOut2Set3DLatency. Left as stubs, as no reference establishes their signature or behaviour: sceAjmBatchJobEncode, sceAjmDecWVorbisCreateHeaderPacket, sceAjmDecWVorbisCreateSetupPacket, sceAudioOut2GetSpeakerArrayCoefficients, sceAudioOut2MasteringGetState, sceAudioOut2SetSystemDebugState, sceAudioOut2UserGetSupportedAttributes, sceNgs2GeomApply, sceNgs2GeomCalcListener, sceNgs2GeomResetListenerParam, sceNgs2GeomResetSourceParam, sceNgs2PanGetVolumeMatrix, sceNgs2PanInit, sceTextToSpeech2GetSystemStatus, sceTextToSpeech2Cancel, sceTextToSpeech2Close, sceTextToSpeech2GetSpeechStatus, sceTextToSpeech2Open, sceTextToSpeech2Speak, sceTextToSpeech2Terminate. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CM3ZvU1gP32iEs7abAt7N6 --- core/libs/prx/libSceAjm.native/Export.cpp | 29 --- core/libs/prx/libSceAjm.native/src/Ajm.cpp | 77 ++++++- .../libSceAudioOut/src/AudioOut2Mastering.cpp | 6 +- .../libSceAudioOut/src/AudioOut2System.cpp | 4 +- core/libs/tests/GuestAjm.cpp | 192 +++++++++++++++++- docs/dev/TechnicalDebt.md | 9 +- 6 files changed, 273 insertions(+), 44 deletions(-) diff --git a/core/libs/prx/libSceAjm.native/Export.cpp b/core/libs/prx/libSceAjm.native/Export.cpp index 8f5edd6e9fd..cdcc0a8edff 100644 --- a/core/libs/prx/libSceAjm.native/Export.cpp +++ b/core/libs/prx/libSceAjm.native/Export.cpp @@ -14,18 +14,6 @@ static void AjmStub(const char* name) { extern "C" { -int APS5_VABI sceAjmBatchJobDecodeSplit(AjmBatchInfo* info, uint32_t instance, const AjmBuffer* input_buffers, size_t input_buffers_num, const AjmBuffer* output_buffers, size_t output_buffers_num, void* result) { - (void)info; - (void)instance; - (void)input_buffers; - (void)input_buffers_num; - (void)output_buffers; - (void)output_buffers_num; - (void)result; - AjmStub(__func__); - return 0; -} - int APS5_VABI sceAjmBatchJobEncode(AjmBatchInfo* info, uint32_t instance, const void* pcm_input, size_t pcm_input_size, void* bitstream_output, size_t bitstream_output_size, void* result) { (void)info; (void)instance; @@ -38,23 +26,6 @@ int APS5_VABI sceAjmBatchJobEncode(AjmBatchInfo* info, uint32_t instance, const return 0; } -int APS5_VABI sceAjmBatchJobSetResampleParametersEx(AjmBatchInfo* info, uint32_t instance, float ratio_start, float ratio_change_per_sample, uint32_t flags, void* result) { - (void)info; - (void)instance; - (void)ratio_start; - (void)ratio_change_per_sample; - (void)flags; - (void)result; - AjmStub(__func__); - return 0; -} - -const char* APS5_VABI sceAjmStrError(int error) { - (void)error; - AjmStub(__func__); - return nullptr; -} - int APS5_VABI sceAjmDecWVorbisCreateHeaderPacket(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index b7649e1517d..573449fc5c2 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -39,6 +39,33 @@ 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 AJM_ERROR_FIRST = 0x80930001u; +constexpr const char* AJM_ERROR_NAMES[] = { + "SCE_AJM_ERROR_UNKNOWN", + "SCE_AJM_ERROR_INVALID_CONTEXT", + "SCE_AJM_ERROR_INVALID_INSTANCE", + "SCE_AJM_ERROR_INVALID_BATCH", + "SCE_AJM_ERROR_INVALID_PARAMETER", + "SCE_AJM_ERROR_OUT_OF_MEMORY", + "SCE_AJM_ERROR_OUT_OF_RESOURCES", + "SCE_AJM_ERROR_CODEC_NOT_SUPPORTED", + "SCE_AJM_ERROR_CODEC_ALREADY_REGISTERED", + "SCE_AJM_ERROR_CODEC_NOT_REGISTERED", + "SCE_AJM_ERROR_WRONG_REVISION_FLAG", + "SCE_AJM_ERROR_FLAG_NOT_SUPPORTED", + "SCE_AJM_ERROR_BUSY", + "SCE_AJM_ERROR_BAD_PRIORITY", + "SCE_AJM_ERROR_IN_PROGRESS", + "SCE_AJM_ERROR_RETRY", + "SCE_AJM_ERROR_MALFORMED_BATCH", + "SCE_AJM_ERROR_JOB_CREATION", + "SCE_AJM_ERROR_INVALID_OPCODE", + "SCE_AJM_ERROR_PRIORITY_VIOLATION", + "SCE_AJM_ERROR_BUFFER_TOO_BIG", + "SCE_AJM_ERROR_INVALID_ADDRESS", + "SCE_AJM_ERROR_CANCELLED", +}; + constexpr std::uint32_t CODEC_MP3 = 0; constexpr std::uint32_t CODEC_AT9 = 1; constexpr std::uint32_t CODEC_OPUS = 24; @@ -58,6 +85,7 @@ constexpr std::uint64_t RUN_GET_CODEC_INFO = 1ull << 11; constexpr std::uint64_t RUN_MULTIPLE_FRAMES = 1ull << 12; constexpr std::uint64_t CONTROL_RESET = 1ull << 13; constexpr std::uint64_t CONTROL_INITIALIZE = 1ull << 14; +constexpr std::uint64_t CONTROL_RESAMPLE = 1ull << 15; 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; @@ -106,6 +134,11 @@ struct SidebandResampleInfo { std::uint32_t reserved[8]; }; +struct SidebandResampleParameters { + float ratio; + std::uint32_t flags; +}; + struct Resampler { float ratio = 1.0f; std::vector frames; @@ -1080,11 +1113,18 @@ void Control(Instance& instance, const JobHeader& job, const AjmBuffer* inputs) const auto* input = job.inputCount ? static_cast(inputs[0].ptr) : nullptr; const std::size_t inputSize = job.inputCount ? inputs[0].size : 0; const std::size_t gaplessSize = (job.flags & SIDEBAND_GAPLESS_DECODE) ? sizeof(SidebandGaplessDecode) : 0; + const std::size_t resampleSize = (job.flags & CONTROL_RESAMPLE) ? sizeof(SidebandResampleParameters) : 0; const std::size_t initializeSize = (job.flags & CONTROL_INITIALIZE) ? ControlInitializeSize(instance.codec) : 0; - if (inputSize != gaplessSize + initializeSize) NotImplemented_nid_no_patch("sceAjmBatchJobControl (sideband input size other than the gapless decode and the codec's initialize parameters)"); + if (inputSize != gaplessSize + resampleSize + initializeSize) NotImplemented_nid_no_patch("sceAjmBatchJobControl (sideband input size other than the gapless decode, the resample parameters and the codec's initialize parameters)"); + SidebandResampleParameters resample{}; + if (resampleSize) { + std::memcpy(&resample, input + gaplessSize, sizeof(resample)); + if (!std::isfinite(resample.ratio) || resample.ratio <= 0.0f) throw std::runtime_error("AJM: control job resample ratio " + std::to_string(resample.ratio) + " is not a positive finite number"); + } std::int32_t result = 0; if (job.flags & CONTROL_RESET) ClearContext(instance); - if (job.flags & CONTROL_INITIALIZE) result = InitializeInstance(instance, input + gaplessSize, initializeSize); + if (job.flags & CONTROL_INITIALIZE) result = InitializeInstance(instance, input + gaplessSize + resampleSize, initializeSize); + if (resampleSize) instance.resampler.ratio = resample.ratio; if (gaplessSize) { SidebandGaplessDecode gapless{}; std::memcpy(&gapless, input, sizeof(gapless)); @@ -1093,6 +1133,7 @@ void Control(Instance& instance, const JobHeader& job, const AjmBuffer* inputs) } AJM_TRACE("[ajm] instance %u control flags 0x%llx: %zu sideband input bytes -> result 0x%x, gapless total %u skip %u skipped %u\n", job.instance, static_cast(job.flags), inputSize, static_cast(result), instance.gapless.totalSamples, instance.gapless.skipSamples, instance.gapless.skippedSamples); WriteResult(job.sideband, job.sidebandSize, result); + if (gaplessSize && job.sideband && job.sidebandSize >= sizeof(SidebandResult) + sizeof(SidebandGaplessDecode)) std::memcpy(static_cast(job.sideband) + sizeof(SidebandResult), &instance.gapless, sizeof(instance.gapless)); } std::size_t HeldSamples(const Instance& instance) { @@ -1266,6 +1307,16 @@ int APS5_VABI sceAjmDecMp3ParseFrame(const uint8_t* stream, uint32_t streamSize, return ParseMp3Header(stream, streamSize, parseOfl, frame); } +const char* APS5_VABI sceAjmStrError(int error) { + const std::uint32_t index = static_cast(error) - AJM_ERROR_FIRST; + if (index >= std::size(AJM_ERROR_NAMES)) { + char code[16]; + std::snprintf(code, sizeof(code), "0x%08X", static_cast(error)); + throw std::runtime_error(std::string(__func__) + ": " + code + " is not an AJM error code"); + } + return AJM_ERROR_NAMES[index]; +} + int APS5_VABI sceAjmBatchInitialize(void* buffer, size_t size, AjmBatchInfo* info) { if (!buffer || !info) return SCE_AJM_ERROR_INVALID_PARAMETER; info->p_buffer = buffer; @@ -1294,11 +1345,11 @@ int APS5_VABI sceAjmBatchJobSetGaplessDecode(AjmBatchInfo* info, uint32_t instan } int APS5_VABI sceAjmBatchJobControl(AjmBatchInfo* info, uint32_t instance, uint64_t flags, const void* sideband_input, size_t sideband_input_size, void* sideband_output, size_t sideband_output_size) { - constexpr std::uint64_t supported = CONTROL_RESET | CONTROL_INITIALIZE | SIDEBAND_GAPLESS_DECODE; - if (flags == 0 || (flags & ~supported) != 0) NotImplemented_nid_no_patch("sceAjmBatchJobControl (flags other than a combination of RESET, INITIALIZE and SIDEBAND_GAPLESS_DECODE)"); - if ((flags & SIDEBAND_GAPLESS_DECODE) != 0 && (flags & CONTROL_RESET) == 0) NotImplemented_nid_no_patch("sceAjmBatchJobControl (SIDEBAND_GAPLESS_DECODE without RESET)"); - if (sideband_output_size != sizeof(SidebandResult)) NotImplemented_nid_no_patch("sceAjmBatchJobControl (sideband output other than the 8-byte result)"); - if (sideband_input_size != 0 && !sideband_input) NotImplemented_nid_no_patch("sceAjmBatchJobControl (null sideband input)"); + constexpr std::uint64_t supported = CONTROL_RESET | CONTROL_INITIALIZE | CONTROL_RESAMPLE | SIDEBAND_GAPLESS_DECODE; + if ((flags & ~supported) != 0) throw std::runtime_error(std::string(__func__) + ": flags other than a combination of RESET, INITIALIZE, RESAMPLE and SIDEBAND_GAPLESS_DECODE"); + if ((flags & SIDEBAND_GAPLESS_DECODE) != 0 && (flags & CONTROL_RESET) == 0) throw std::runtime_error(std::string(__func__) + ": SIDEBAND_GAPLESS_DECODE without RESET"); + if (sideband_output_size < sizeof(SidebandResult)) throw std::runtime_error(std::string(__func__) + ": a sideband output smaller than the 8-byte result"); + if (sideband_input_size != 0 && !sideband_input) throw std::runtime_error(std::string(__func__) + ": null sideband input"); auto header = MakeHeader(JobKind::Control, instance, sideband_output, sideband_output_size); header.flags = flags; header.inputCount = sideband_input_size != 0 ? 1 : 0; @@ -1307,6 +1358,8 @@ int APS5_VABI sceAjmBatchJobControl(AjmBatchInfo* info, uint32_t instance, uint6 } 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) { + if ((input_buffers_num != 0 && !input_buffers) || (output_buffers_num != 0 && !output_buffers)) throw std::runtime_error(std::string(__func__) + ": a null buffer array with a nonzero count"); + if (input_buffers_num > 0xffu || output_buffers_num > 0xffu) return SCE_AJM_ERROR_INVALID_PARAMETER; auto header = MakeHeader(JobKind::Run, instance, sideband_output, sideband_output_size); header.flags = flags; header.inputCount = static_cast(input_buffers_num); @@ -1329,6 +1382,12 @@ int APS5_VABI sceAjmBatchJobSetResampleParameters(AjmBatchInfo* info, uint32_t i return Append(info, header, nullptr, nullptr); } +int APS5_VABI sceAjmBatchJobSetResampleParametersEx(AjmBatchInfo* info, uint32_t instance, float ratio_start, float ratio_change_per_sample, uint32_t flags, void* result) { + if (!std::isfinite(ratio_start) || ratio_start <= 0.0f) return SCE_AJM_ERROR_INVALID_PARAMETER; + if (ratio_change_per_sample != 0.0f) throw std::runtime_error(std::string(__func__) + ": the effect of a nonzero ratio change per sample is unknown"); + return sceAjmBatchJobSetResampleParameters(info, instance, ratio_start, flags, result); +} + int APS5_VABI sceAjmBatchJobGetResampleInfo(AjmBatchInfo* info, uint32_t instance, void* result) { return Append(info, MakeHeader(JobKind::GetResampleInfo, instance, result, sizeof(SidebandResult) + sizeof(SidebandResampleInfo)), nullptr, nullptr); } @@ -1341,6 +1400,10 @@ int APS5_VABI sceAjmBatchJobDecodeSingle(AjmBatchInfo* info, uint32_t instance, return sceAjmBatchJobRun(info, instance, SIDEBAND_STREAM, bitstream_input, bitstream_input_size, pcm_output, pcm_output_size, result, sizeof(SidebandResult) + sizeof(SidebandStream)); } +int APS5_VABI sceAjmBatchJobDecodeSplit(AjmBatchInfo* info, uint32_t instance, const AjmBuffer* input_buffers, size_t input_buffers_num, const AjmBuffer* output_buffers, size_t output_buffers_num, void* result) { + return sceAjmBatchJobRunSplit(info, instance, SIDEBAND_STREAM | RUN_MULTIPLE_FRAMES, input_buffers, input_buffers_num, output_buffers, output_buffers_num, result, sizeof(SidebandResult) + sizeof(SidebandStream) + sizeof(SidebandMultipleFrames)); +} + int APS5_VABI sceAjmBatchJobGetGaplessDecode(AjmBatchInfo* info, uint32_t instance, void* result) { return sceAjmBatchJobRunSplit(info, instance, SIDEBAND_GAPLESS_DECODE, nullptr, 0, nullptr, 0, result, sizeof(SidebandResult) + sizeof(SidebandGaplessDecode)); } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp index 0d437bbb818..e6f47623f9b 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp @@ -1,11 +1,13 @@ #include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" extern "C" { int APS5_VABI sceAudioOut2MasteringInit(uint32_t flags) { - if (flags != 0) NotImplemented_nid_no_patch(__func__); + if (flags != 0) throw std::runtime_error(std::string(__func__) + ": the meaning of flags " + std::to_string(flags) + " is unknown"); return 0; } @@ -24,7 +26,7 @@ int APS5_VABI sceAudioOut2MasteringGetState(AudioOut2MasteringStatesHeader* stat int APS5_VABI sceAudioOut2MasteringSetParam(const AudioOut2MasteringParamsHeader* param, uint32_t output, uint32_t flags) { (void)output; (void)flags; - if (param == nullptr) NotImplemented_nid_no_patch(__func__); + if (param == nullptr) throw std::runtime_error(std::string(__func__) + ": the result for a null param is unknown"); return 0; } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp index 67d66b44b49..ee5e3f1edd3 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -13,7 +15,7 @@ int APS5_VABI sceAudioOut2Initialize(void) { } int APS5_VABI sceAudioOut2Set3DLatency(int userId, std::uint32_t latency) { - if (userId != USER_ID_SYSTEM || latency < MIN_SUPPORTED_3D_LATENCY || latency > MAX_SUPPORTED_3D_LATENCY) NotImplemented_nid_no_patch(__func__); + if (userId != USER_ID_SYSTEM || latency < MIN_SUPPORTED_3D_LATENCY || latency > MAX_SUPPORTED_3D_LATENCY) throw std::runtime_error(std::string(__func__) + ": the effect of latency " + std::to_string(latency) + " for user " + std::to_string(userId) + " is unknown"); return 0; } diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index 0c88f35d735..2732dbcd0a2 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -31,6 +31,9 @@ int APS5_VABI sceAjmBatchCancel(std::uint32_t, std::uint32_t); int APS5_VABI sceAjmBatchJobClearContext(AjmBatchInfo*, std::uint32_t, void*); int APS5_VABI sceAjmBatchJobSetResampleParameters(AjmBatchInfo*, std::uint32_t, float, std::uint32_t, void*); int APS5_VABI sceAjmBatchJobGetResampleInfo(AjmBatchInfo*, std::uint32_t, void*); +int APS5_VABI sceAjmBatchJobSetResampleParametersEx(AjmBatchInfo*, std::uint32_t, float, float, std::uint32_t, void*); +int APS5_VABI sceAjmBatchJobDecodeSplit(AjmBatchInfo*, std::uint32_t, const AjmBuffer*, std::size_t, const AjmBuffer*, std::size_t, void*); +const char* APS5_VABI sceAjmStrError(int); } static void Require(bool value) { if (!value) std::abort(); } @@ -675,13 +678,23 @@ void TestControlAt9(std::uint32_t context) { Require(ControlRefused(context, instance, CONTROL_INITIALIZE, &start, sizeof(start))); Require(ControlRefused(context, instance, CONTROL_RESET | SIDEBAND_GAPLESS_DECODE, &start, sizeof(start))); + Require(ControlRefused(context, instance, CONTROL_START | (1ull << 15), &start, sizeof(start))); Require(ControlThrows(instance, CONTROL_INITIALIZE | SIDEBAND_GAPLESS_DECODE, &start, sizeof(start), 8)); - Require(ControlThrows(instance, CONTROL_START | (1ull << 15), &start, sizeof(start), 8)); Require(ControlThrows(instance, CONTROL_START | (1ull << 46), &start, sizeof(start), 8)); Require(ControlThrows(instance, CONTROL_START | (1ull << 12), &start, sizeof(start), 8)); - Require(ControlThrows(instance, 0, nullptr, 0, 8)); - Require(ControlThrows(instance, CONTROL_START, &start, sizeof(start), 16)); + Require(ControlThrows(instance, CONTROL_START | 1ull, &start, sizeof(start), 8)); + Require(ControlThrows(instance, CONTROL_START, &start, sizeof(start), 4)); Require(ControlThrows(instance, CONTROL_START, nullptr, sizeof(start), 8)); + + struct { + GaplessSideband gapless; + std::uint32_t guard[2]; + } extended{{-1, -1, 0, 0, 0xffff}, {0xA5A5A5A5u, 0xA5A5A5A5u}}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobControl(&info, instance, CONTROL_START, &start, sizeof(start), &extended, sizeof(extended)) == 0); + Submit(context, info); + Require(extended.gapless.result == 0 && extended.gapless.internalResult == 0 && extended.gapless.totalSamples == 2000 && extended.gapless.skipSamples == 100 && extended.gapless.skippedSamples == 0); + Require(extended.guard[0] == 0xA5A5A5A5u && extended.guard[1] == 0xA5A5A5A5u); Require(sceAjmInstanceDestroy(context, instance) == 0); result[0] = -1; @@ -955,6 +968,175 @@ void TestResampleAt9(std::uint32_t context) { for (const std::uint32_t instance : {plain, faster, slower}) Require(sceAjmInstanceDestroy(context, instance) == 0); } +struct ResampleControl { + float ratio; + std::uint32_t flags; +}; + +void TestControlResample(std::uint32_t context) { + constexpr std::uint64_t controlResample = 1ull << 15; + const std::uint32_t instance = CreateOpus(context); + std::int32_t result[2] = {-1, -1}; + RunControl(context, instance, 0, nullptr, 0, result); + Require(result[0] == 0 && result[1] == 0 && GetResampleInfo(context, instance).ratio == 1.0f); + + const ResampleControl faster{2.0f, 1}; + result[0] = -1; + RunControl(context, instance, controlResample, &faster, sizeof(faster), result); + Require(result[0] == 0 && GetResampleInfo(context, instance).ratio == 2.0f); + for (const float bad : {0.0f, -1.0f, std::nanf(""), INFINITY}) { + const ResampleControl invalid{bad, 0}; + Require(ControlRefused(context, instance, controlResample, &invalid, sizeof(invalid))); + } + Require(GetResampleInfo(context, instance).ratio == 2.0f); + const float rampedLayout[3] = {0.5f, 0.0f, 0.0f}; + Require(ControlRefused(context, instance, controlResample, rampedLayout, sizeof(rampedLayout))); + Require(GetResampleInfo(context, instance).ratio == 2.0f); + + struct { + GaplessDecode gapless; + ResampleControl resample; + std::uint32_t channels; + std::uint32_t sampleRate; + std::uint32_t third; + } start{{0, 0, 0}, {0.5f, 1}, 2, 48000, 0}; + result[0] = -1; + RunControl(context, instance, CONTROL_START | controlResample, &start, sizeof(start), result); + Require(result[0] == 0 && GetResampleInfo(context, instance).ratio == 0.5f); + const auto controlled = Stream(context, instance, OPUS_STEREO, sizeof(OPUS_STEREO), 2, 512, 0); + const std::uint32_t reference = CreateOpus(context); + const auto parameterized = Stream(context, reference, OPUS_STEREO, sizeof(OPUS_STEREO), 2, 512, 0.5f); + Require(!controlled.pcm.empty() && controlled.pcm == parameterized.pcm); + Require(ControlRefused(context, instance, CONTROL_START | controlResample, &start, sizeof(start) - sizeof(start.third))); + + struct { + std::int32_t result[2]; + std::uint32_t guard[2]; + } wide{{-1, -1}, {0xA5A5A5A5u, 0xA5A5A5A5u}}; + std::vector batch(4096); + AjmBatchInfo info{}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobControl(&info, instance, CONTROL_RESET, nullptr, 0, &wide, sizeof(wide)) == 0); + Submit(context, info); + Require(wide.result[0] == 0 && wide.result[1] == 0 && wide.guard[0] == 0xA5A5A5A5u && wide.guard[1] == 0xA5A5A5A5u); + for (const std::uint32_t created : {instance, reference}) Require(sceAjmInstanceDestroy(context, created) == 0); +} + +void TestResampleParametersEx(std::uint32_t context) { + constexpr int invalidParameter = static_cast(0x80930005); + const std::uint32_t ramped = CreateOpus(context); + const std::uint32_t fixed = CreateOpus(context); + std::vector batch(4096); + AjmBatchInfo info{}; + std::int32_t result[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + for (const float bad : {0.0f, -1.0f, std::nanf(""), INFINITY}) { + Require(sceAjmBatchJobSetResampleParametersEx(&info, ramped, bad, 0.0f, 0, result) == invalidParameter); + Require(sceAjmBatchJobSetResampleParametersEx(&info, ramped, bad, 0.25f, 0, result) == invalidParameter); + } + bool threw = false; + try { + static_cast(sceAjmBatchJobSetResampleParametersEx(&info, ramped, 1.0f, 0.001f, 0, result)); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw && info.offset == 0); + + Require(sceAjmBatchJobSetResampleParametersEx(&info, ramped, 2.0f, 0.0f, 1, result) == 0); + Submit(context, info); + Require(result[0] == 0 && result[1] == 0 && GetResampleInfo(context, ramped).ratio == 2.0f); + std::int32_t fixedResult[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobSetResampleParameters(&info, fixed, 2.0f, 1, fixedResult) == 0); + Submit(context, info); + Require(fixedResult[0] == 0); + const auto viaEx = Stream(context, ramped, OPUS_STEREO, sizeof(OPUS_STEREO), 2, 512, 0); + const auto viaFixed = Stream(context, fixed, OPUS_STEREO, sizeof(OPUS_STEREO), 2, 512, 0); + Require(!viaEx.pcm.empty() && viaEx.pcm == viaFixed.pcm); + for (const std::uint32_t instance : {ramped, fixed}) Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +struct SplitDecodeSideband { + DecodeSideband stream; + std::uint32_t frames; + std::uint32_t reserved; + std::uint32_t guard[2]; +}; + +void TestDecodeSplit(std::uint32_t context) { + std::uint32_t plain = 0; + Require(sceAjmInstanceCreate(context, 0, 0, &plain) == 0); + std::vector reference; + std::vector pcm(1152); + for (std::size_t frame = 0; frame < 3; ++frame) { + DecodeSideband decoded{}; + RunDecode(context, plain, MP3_MONO + frame * 96, sizeof(MP3_MONO) - frame * 96, pcm.data(), pcm.size() * sizeof(std::int16_t), decoded); + Require(decoded.result == 0 && decoded.outputWritten == 1152 * 2); + reference.insert(reference.end(), pcm.begin(), pcm.end()); + } + + std::uint32_t split = 0; + Require(sceAjmInstanceCreate(context, 0, 0, &split) == 0); + std::vector first(1000, 0x5A5A); + std::vector second(3 * 1152 - 1000, 0x5A5A); + const AjmBuffer inputs[2] = {{const_cast(MP3_MONO), 150}, {const_cast(MP3_MONO + 150), sizeof(MP3_MONO) - 150}}; + const AjmBuffer outputs[2] = {{first.data(), first.size() * sizeof(std::int16_t)}, {second.data(), second.size() * sizeof(std::int16_t)}}; + SplitDecodeSideband sideband{{-1, -1, -1, -1, 0}, 0xFFFFFFFFu, 0xFFFFFFFFu, {0xA5A5A5A5u, 0xA5A5A5A5u}}; + std::vector batch(4096); + AjmBatchInfo info{}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobDecodeSplit(&info, split, inputs, 2, outputs, 2, &sideband) == 0); + Submit(context, info); + Require(sideband.stream.result == 0 && sideband.stream.internalResult == 0 && sideband.stream.inputConsumed == 3 * 96 && sideband.stream.outputWritten == 3 * 1152 * 2); + Require(sideband.stream.totalDecodedSamples == 3 * 1152 && sideband.frames == 3 && sideband.reserved == 0); + Require(sideband.guard[0] == 0xA5A5A5A5u && sideband.guard[1] == 0xA5A5A5A5u); + std::vector joined(first); + joined.insert(joined.end(), second.begin(), second.end()); + Require(joined == reference); + + std::vector small(100); + const AjmBuffer tooSmall{small.data(), small.size() * sizeof(std::int16_t)}; + const AjmBuffer rest{const_cast(MP3_MONO + 3 * 96), sizeof(MP3_MONO) - 3 * 96}; + sideband.stream = {-1, -1, -1, -1, 0}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobDecodeSplit(&info, split, &rest, 1, &tooSmall, 1, &sideband) == 0); + Submit(context, info); + Require(sideband.stream.result == 0x10 && sideband.stream.inputConsumed == 0 && sideband.stream.outputWritten == 0 && sideband.frames == 0); + + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + for (const bool nullInputs : {true, false}) { + bool threw = false; + try { + static_cast(sceAjmBatchJobDecodeSplit(&info, split, nullInputs ? nullptr : inputs, 2, nullInputs ? outputs : nullptr, 2, &sideband)); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw && info.offset == 0); + } + constexpr int invalidParameter = static_cast(0x80930005); + Require(sceAjmBatchJobDecodeSplit(&info, split, inputs, (std::size_t{1} << 32) + 1, outputs, 2, &sideband) == invalidParameter && info.offset == 0); + Require(sceAjmBatchJobDecodeSplit(&info, split, inputs, 2, outputs, 256, &sideband) == invalidParameter && info.offset == 0); + for (const std::uint32_t instance : {plain, split}) Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +void TestStrError() { + const auto is = [](int error, const char* name) { return std::strcmp(sceAjmStrError(error), name) == 0; }; + Require(is(static_cast(0x80930001), "SCE_AJM_ERROR_UNKNOWN")); + Require(is(static_cast(0x80930005), "SCE_AJM_ERROR_INVALID_PARAMETER")); + Require(is(static_cast(0x8093000A), "SCE_AJM_ERROR_CODEC_NOT_REGISTERED")); + Require(is(static_cast(0x80930017), "SCE_AJM_ERROR_CANCELLED")); + Require(sceAjmStrError(static_cast(0x80930002)) == sceAjmStrError(static_cast(0x80930002))); + for (const int unknown : {0, 4, static_cast(0x80930000), static_cast(0x80930018), static_cast(0x80020016)}) { + bool threw = false; + try { + static_cast(sceAjmStrError(unknown)); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + } +} + } int main() { @@ -988,5 +1170,9 @@ int main() { TestResampleOpus(context); TestResampleMp3(context); TestResampleAt9(context); + TestControlResample(context); + TestResampleParametersEx(context); + TestDecodeSplit(context); + TestStrError(); Require(sceAjmFinalize(context) == 0); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 6079eb2f86a..c4357d84f81 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -151,6 +151,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAudioOut2PortCreate](../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - MAIN (type 0) format validation follows PS5 firmware 13.60 measurements: bits 8-11 accept 1, 2, 8 or 12, bits 0-6 accept 0 or 1, bit 7 requires 8 channels, and bits 12-31 do not affect validation. The sample types are still read as float and int16; bit 7's layout, the PCM effect of bits 12-31 and the port `flags` are unverified. Other port types retain unverified format validation, including 6 channels and rejection of bits 12-31 - [sceAudioOut2PortCreate](../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - 12 channels are assumed to be L R C LFE Ls Rs Lb Rb Ltf Rtf Ltb Rtb as in KytyPS5's 12-to-8 fold, and 6 and 8 channels to be its first 6 and 8; no title or SDK header confirms the order - [sceAudioOut2PortCreate](../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - the stereo fold is assumed, not the console's downmix: C at -3 dB into both sides, Ls/Rs, Lb/Rb and Ltf/Rtf at -3 dB and Ltb/Rtb at -6 dB into their own side, LFE dropped +- [sceAudioOut2MasteringSetParam](../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - the console's result for a null param is unknown; it throws. Any other param is accepted without effect, as there is no mastering stage - [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; only 0x8A810001 (not initialized) is returned - [scePngEncEncode](../../core/libs/prx/libScePngEnc/Export.cpp) (libScePngEnc) - whether rows may stay unfiltered when `filter_type` names a subset of the filters is unknown; as in shadPS4, they may only for 0 and for the all-filters mask - [sceJpegEncEncode](../../core/libs/prx/libSceJpegEnc/Export.cpp) (libSceJpegEnc) - `restart_interval` follows SharpProspero's field doc (0 none, -1 every row of blocks, positive a block count); a positive value is taken as the JPEG DRI interval in MCUs and -1 as one restart per MCU row, which no reference confirms. Other negative values throw @@ -183,9 +184,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: the layer field is not checked and MPEG-2.5 stops at 64 kbps; the original file length (`parse_ofl`) layout (Xing/Info + LAME tag, VBRI, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description - [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title; a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts - [AJM Opus decoder](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout (`u32 channels, u32 sample rate, u32`, seen as `2, 48000, 0`) and the little-endian `u16` byte count before each packet come from one title (The Smurfs Dreams); only 48000 Hz and a zero third word are accepted -- [sceAjmBatchJobControl](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET (bit 13), INITIALIZE (bit 14) and the sideband input order (gapless decode, then initialize parameters) come from [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4); other flags throw - [sceAjmBatchJobControl](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET is assumed to clear the context as sceAjmBatchJobClearContext does -- [sceAjmBatchJobControl](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - initialize parameters are 8 bytes for ATRAC9, 12 for Opus and none for MP3, as one title passes them; other codecs and sizes, and an output other than the 8-byte result, throw - [sceAjmBatchJobControl](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - SIDEBAND_GAPLESS_DECODE with RESET overwrites both counts as in [RPCSX](https://github.com/RPCSX/rpcsx); shadPS4 ignores a zero count when the other is nonzero. Without RESET they differ, so it throws - [NGS2 custom submixer rack](../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - structure layouts from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`; module id 0x1f for UserFx2, the voice parameter id `0x40001f00 | module index`, the process flags (1 on the first process call after setup, 2 after a parameter change, 0 otherwise) and the call order (setup per voice at rack creation, process after the inputs are mixed, cleanup at destruction) come only from [KytyPS5](https://github.com/KytyPS5/KytyPS5). Only UserFx2 modules on a single buffer are accepted: other module ids, more buffers, a module control handler, different input and output channel counts, setup flags and default rack options throw. Each module's state is a separate buffer, not the `state_offset` slice of the voice state, and `sceNgs2VoiceGetState` still throws for this rack. As in KytyPS5, process runs only on grains where an input produced samples, so an effect tail stops with the input and a pending flag 2 waits for the next audible grain - [sceHttpParseResponseHeader](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); parsing and error codes follow [shadPS4's reverse-engineered PS4 library](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. Folded values retain their line breaks and whitespace; an empty first line starts the value scan after its line feed. A consumed byte count above `INT_MAX` throws instead of wrapping. @@ -228,6 +227,12 @@ Throughout the project, every function at every stage either **does exactly what - [Shader registration](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a shader header is accepted at any address: PPSA26344 passes headers 4 and 1 bytes past an 8-byte boundary; whether the console refuses an alignment is unknown - [sceNgs2VoiceControl sampler filter](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown - [sceRtcGetDosTime, sceRtcSetDosTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). A year outside 1980-2107 makes `GetDosTime` return `INVALID_YEAR` after writing 0 for an earlier one and `0xff9fbf7d` (2107-12-31 23:59:58, the largest encodable time) for a later one, and `SetDosTime` decodes the year as the unsigned 7-bit FAT field (1980-2107) without validating the fields, as the disassembly-derived [fpPS4](https://github.com/red-prig/fpPS4) reimplementation of the PS4 library does; not confirmed on a PS5 title, and shadPS4 instead masks out-of-range years into the packed value and sign-extends the 16-bit date +- [sceAjmBatchJobDecodeSplit](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature taken from [SharpProspero](https://github.com/SvenGDK/SharpProspero), behaviour from [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [REmu](https://github.com/xnetcat/REmu), not confirmed on a title: a split run job with `SIDEBAND_STREAM` and `RUN_MULTIPLE_FRAMES` that writes the 32-byte decode result (result, stream, frame count). `sceAjmBatchJobDecode` here still decodes one frame into a 24-byte result, while both references decode multiple frames there as well. A null buffer array with a nonzero count throws in `sceAjmBatchJobRunSplit`: KytyPS5 treats it as no buffers and REmu returns `SCE_AJM_ERROR_INVALID_PARAMETER` +- [sceAjmBatchJobSetResampleParametersEx](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature taken from [SharpProspero](https://github.com/SvenGDK/SharpProspero) and [KytyPS5](https://github.com/KytyPS5/KytyPS5), not confirmed on a title; no reference applies the ratio change (KytyPS5 only logs it). A zero change per sample is taken as `sceAjmBatchJobSetResampleParameters` with `ratio_start` and the same flags; a nonzero change throws +- [sceAjmStrError](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature taken from [orbital](https://github.com/AlexAltea/orbital)'s `db_types.json` and [SharpProspero](https://github.com/SvenGDK/SharpProspero), not confirmed on a title. The console's message text is unknown: it returns the SDK name of each `SCE_AJM_ERROR_*` code from 0x80930001 to 0x80930017, as listed by [fpPS4](https://github.com/red-prig/fpPS4)'s `ajm_error.inc`, and any other value throws +- [sceAjmBatchJobControl](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET (bit 13), INITIALIZE (bit 14), RESAMPLE (bit 15), the sideband input order (gapless decode, resample parameters, initialize parameters) and the output order (result, then the gapless state when `SIDEBAND_GAPLESS_DECODE` is set, reported as by `sceAjmBatchJobGetGaplessDecode`) come from [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4). A job without flags writes result 0 and changes nothing, as in shadPS4. Other flags (revision, codec, run, `SIDEBAND_FORMAT`, `SIDEBAND_STREAM`), an output smaller than 8 bytes and a null input with a nonzero size throw; shadPS4 drops the run and `SIDEBAND_STREAM` bits of a control job +- [sceAjmBatchJobControl](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the RESAMPLE parameters are taken as shadPS4's 8-byte `float ratio, u32 flags` and assumed to set the ratio as `sceAjmBatchJobSetResampleParameters` does, ignoring the flags; shadPS4 stores them without using them. [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s resample job size implies a 12-byte `float, float, u32` layout on PS5; that size throws at batch start, as does a ratio that is not positive and finite +- [sceAjmBatchJobControl](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - initialize parameters are 8 bytes for ATRAC9, 12 for Opus and none for MP3, as one title passes them; other codecs and sizes throw - [sce::Json::InitParameter2](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layout inferred, not known: assumed to start with the allocator, its user data and the file buffer size, in that order, and the whole struct is assumed to fit in 40 bytes; `Initializer::initialize(InitParameter2)` ignores them as `initialize(InitParameter)` does - [sceAjmBatchJobSetResampleParameters, sceAjmBatchJobGetResampleInfo](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm) - the ratio is taken as decoded samples per output sample and num_samples as the decoded samples the resampler holds, both inferred from FMOD (Hades II); the console's interpolation is unknown, a 4-point Catmull-Rom is used. The flags argument is ignored, as in shadPS4 and Kyty - [scePlayerReviewDialogOpen, scePlayerReviewDialogGetResult](../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) - the PS5-only dialog has no public reference: the parameter is not read and the result is assumed to start with the 32-bit common dialog result, as `sceWebBrowserDialogGetResult` assumes