From 20366977584c28e753b79674ec00a8a014840d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 2 Oct 2026 09:20:45 +0200 Subject: [PATCH] fix(ajm): convert planar 16-bit MP3 output from FFmpeg Co-Authored-By: Claude --- core/libs/prx/libSceAjm.native/CMakeLists.txt | 18 +++ core/libs/prx/libSceAjm.native/src/Ajm.cpp | 6 +- core/libs/tests/GuestAjm.cpp | 103 ++++++++++++++++++ 3 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 core/libs/tests/GuestAjm.cpp diff --git a/core/libs/prx/libSceAjm.native/CMakeLists.txt b/core/libs/prx/libSceAjm.native/CMakeLists.txt index 6623b3914..87d1e3cc9 100644 --- a/core/libs/prx/libSceAjm.native/CMakeLists.txt +++ b/core/libs/prx/libSceAjm.native/CMakeLists.txt @@ -29,3 +29,21 @@ target_include_directories(libSceAjm.native PRIVATE ) target_link_libraries(libSceAjm.native PRIVATE atrac9 ffmpeg libc libkernel) configure_windows_unwind(libSceAjm.native) + +if(MINGW) + foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) + string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + endforeach() +endif() + +if(BUILD_TESTING) + add_test_executable(guest_ajm_tests ${LIBS_INCLUDE_DIR}/tests/GuestAjm.cpp) + target_include_directories(guest_ajm_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ajm_tests PRIVATE libSceAjm.native libc) + configure_windows_unwind(guest_ajm_tests) + add_test(NAME guest_ajm COMMAND guest_ajm_tests) + if(WIN32) + set_tests_properties(guest_ajm PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 4d6207075..65c6748e1 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -469,13 +469,15 @@ void RunMp3(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c instance.mp3Bitrate = frame.bitrate; int status = 0; while ((status = avcodec_receive_frame(instance.mp3, decoded.get())) == 0) { - if (decoded->format != AV_SAMPLE_FMT_FLTP) throw std::runtime_error("AJM MP3: FFmpeg sample format " + std::to_string(decoded->format) + " is not converted"); + const auto format = static_cast(decoded->format); + if (format != AV_SAMPLE_FMT_FLTP && format != AV_SAMPLE_FMT_S16P) throw std::runtime_error("AJM MP3: FFmpeg sample format " + std::to_string(decoded->format) + " is not converted"); const auto channels = static_cast(decoded->ch_layout.nb_channels); const auto samples = static_cast(decoded->nb_samples); pcm.resize(samples * channels * sampleBytes); for (std::size_t sample = 0; sample < samples; ++sample) { for (std::size_t channel = 0; channel < channels; ++channel) { - const float value = reinterpret_cast(decoded->extended_data[channel])[sample]; + const float value = format == AV_SAMPLE_FMT_FLTP ? reinterpret_cast(decoded->extended_data[channel])[sample] + : reinterpret_cast(decoded->extended_data[channel])[sample] / 32768.0f; auto* out = pcm.data() + (sample * channels + channel) * sampleBytes; if (encoding == 0) { const auto converted = static_cast(std::clamp(std::lrint(value * 32768.0), -32768L, 32767L)); diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp new file mode 100644 index 000000000..da4407746 --- /dev/null +++ b/core/libs/tests/GuestAjm.cpp @@ -0,0 +1,103 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAjmInitialize(std::int64_t, std::uint32_t*); +int APS5_VABI sceAjmFinalize(std::uint32_t); +int APS5_VABI sceAjmDecAt9ParseConfigData(const void*, AjmDecAt9ConfigDataInfo*); +int APS5_VABI sceAjmInstanceCreate(std::uint32_t, std::uint32_t, std::uint64_t, std::uint32_t*); +int APS5_VABI sceAjmInstanceDestroy(std::uint32_t, std::uint32_t); +int APS5_VABI sceAjmBatchInitialize(void*, std::size_t, AjmBatchInfo*); +int APS5_VABI sceAjmBatchJobDecode(AjmBatchInfo*, std::uint32_t, const void*, std::size_t, void*, std::size_t, void*); +int APS5_VABI sceAjmBatchStart(std::uint32_t, const AjmBatchInfo*, int, AjmBatchError*, std::uint32_t*); +int APS5_VABI sceAjmBatchWait(std::uint32_t, std::uint32_t, std::uint32_t, AjmBatchError*); +} + +static void Require(bool value) { if (!value) std::abort(); } + +namespace { + +const std::uint8_t MP3_MONO[] = { + 0xFF, 0xFB, 0x14, 0xC4, 0x00, 0x00, 0x03, 0xB0, 0x21, 0x5E, 0xF4, 0x30, 0x80, 0x30, 0x98, 0x88, 0xA9, 0x83, 0x34, 0x70, 0x00, 0x18, 0x89, 0xCB, + 0x20, 0x00, 0x3B, 0xBB, 0xB9, 0x9F, 0xD7, 0x0E, 0x06, 0x06, 0x06, 0x2C, 0x3E, 0xEF, 0x83, 0xE7, 0xF1, 0x00, 0x63, 0x83, 0xFD, 0x4E, 0xFC, 0xFF, + 0x29, 0xE1, 0xF7, 0xD9, 0x87, 0x7E, 0xFF, 0x65, 0x9E, 0x21, 0xAF, 0x46, 0x58, 0xBB, 0x46, 0x58, 0xBA, 0x64, 0x40, 0x21, 0xB2, 0x6C, 0xFA, 0x6C, + 0xF8, 0x42, 0x0E, 0x7C, 0x1E, 0x8F, 0x0D, 0xBF, 0x1B, 0x02, 0xBC, 0x24, 0x0D, 0x37, 0xCA, 0x9D, 0x52, 0xFD, 0x77, 0xE0, 0x16, 0xBA, 0x02, 0x04, + 0xFF, 0xFB, 0x14, 0xC4, 0x03, 0x83, 0xC4, 0xE0, 0x2B, 0x28, 0x1D, 0xE4, 0x80, 0x20, 0x5D, 0x95, 0x60, 0x41, 0x70, 0x16, 0xC8, 0xD3, 0x02, 0x00, + 0x2F, 0x12, 0x02, 0xD0, 0x40, 0x1D, 0x98, 0x4D, 0x06, 0x99, 0x84, 0xF2, 0x68, 0x98, 0xBF, 0x86, 0xD1, 0x84, 0xC8, 0x18, 0x2A, 0xA9, 0x79, 0x50, + 0x85, 0xDE, 0x14, 0x88, 0x9A, 0xF8, 0x44, 0x02, 0xB0, 0x60, 0x62, 0x5E, 0x11, 0xE9, 0xDC, 0x18, 0x65, 0x8F, 0xFF, 0x7F, 0xFC, 0xB3, 0xF8, 0x58, + 0xEF, 0xFF, 0xFF, 0xE1, 0x95, 0xF8, 0x44, 0x02, 0xB0, 0x60, 0x62, 0x5E, 0x11, 0xE9, 0xDC, 0x18, 0x65, 0x8F, 0xFF, 0x7F, 0xFC, 0xB3, 0xF8, 0x58, + 0xFF, 0xFB, 0x14, 0xC4, 0x09, 0x83, 0xC2, 0xEC, 0xAB, 0x02, 0x0B, 0x80, 0xB6, 0x40, 0x5D, 0x95, 0x60, 0x41, 0x70, 0x16, 0xC8, 0xEF, 0xFF, 0xFF, + 0xE1, 0x9F, 0x08, 0x80, 0x56, 0x0C, 0x0C, 0x4B, 0xC2, 0x3D, 0x3B, 0x83, 0x0C, 0xB1, 0xFF, 0xEF, 0xFF, 0x96, 0x7F, 0x0B, 0x1D, 0xFF, 0xFF, 0xFC, + 0x31, 0xF8, 0x44, 0x02, 0xB0, 0x60, 0x62, 0x5E, 0x11, 0xE9, 0xDC, 0x18, 0x65, 0x8F, 0xFF, 0x7F, 0xFC, 0xB3, 0xF8, 0x58, 0xEF, 0xFF, 0xFF, 0xE1, + 0x9F, 0x08, 0x80, 0x56, 0x0C, 0x0C, 0x4B, 0xC2, 0x3D, 0x3B, 0x83, 0x0C, 0xB1, 0xFF, 0xEF, 0xFF, 0x96, 0x7F, 0x0B, 0x1D, 0xFF, 0xFF, 0xFC, 0x31, + 0xFF, 0xFB, 0x14, 0xC4, 0x17, 0x83, 0xC2, 0xEC, 0xAB, 0x02, 0x0B, 0x80, 0xB6, 0x40, 0x5D, 0x95, 0x60, 0x41, 0x70, 0x16, 0xC8, 0x96, 0x55, 0x8D, + 0xBF, 0xE9, 0xD8, 0x50, 0x01, 0xA6, 0x17, 0x80, 0x34, 0x61, 0x00, 0x17, 0xC6, 0x15, 0x40, 0xD4, 0x61, 0x56, 0x25, 0x66, 0x3E, 0xA4, 0x92, 0x62, + 0xC3, 0xED, 0x26, 0x23, 0xA5, 0x5C, 0x63, 0x10, 0x01, 0xC6, 0x0F, 0x20, 0x9E, 0x60, 0x62, 0x08, 0x26, 0x06, 0xE0, 0x12, 0xD6, 0x22, 0x02, 0x6C, + 0x93, 0xC7, 0x04, 0x3E, 0xBF, 0xFE, 0xBF, 0xFF, 0x72, 0x62, 0x28, 0x22, 0xD1, 0x2F, 0xEF, 0xFD, 0xF1, 0x15, 0x09, 0x83, 0x08, 0x8C, 0x00, 0xFE, + 0xFF, 0xFB, 0x14, 0xC4, 0x25, 0x80, 0x07, 0x74, 0x2D, 0x16, 0x15, 0xE7, 0x80, 0x00, 0xF2, 0x0C, 0xE8, 0x83, 0x36, 0xB0, 0x00, 0xC4, 0x42, 0x82, + 0xC0, 0x86, 0x24, 0x2A, 0x02, 0x11, 0xFF, 0x02, 0x70, 0xF6, 0x07, 0x80, 0x9B, 0xCB, 0xCD, 0x0D, 0x00, 0x7C, 0x49, 0x1D, 0xBF, 0xD3, 0xDE, 0x3B, + 0x52, 0x36, 0xFE, 0xFA, 0xF3, 0x66, 0xB5, 0x13, 0xDF, 0x06, 0x84, 0xA1, 0x2F, 0xCB, 0x05, 0x41, 0x5F, 0xF5, 0x2A, 0x4C, 0x41, 0x4D, 0x45, 0x34, + 0x2E, 0x30, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, + 0xFF, 0xFB, 0x14, 0xC4, 0x0E, 0x83, 0xC0, 0x00, 0x01, 0xA4, 0x1C, 0x00, 0x00, 0x20, 0x00, 0x00, 0x34, 0x80, 0x00, 0x00, 0x04, 0xAA, 0xAA, 0xAA, + 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, + 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, + 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, +}; + +struct DecodeSideband { + std::int32_t result; + std::int32_t internalResult; + std::int32_t inputConsumed; + std::int32_t outputWritten; + std::uint64_t totalDecodedSamples; +}; + +void TestMp3(std::uint32_t context) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 0, 0, &instance) == 0); + std::vector batch(4096); + std::vector pcm(1152); + std::size_t offset = 0; + std::int16_t peak = 0; + for (int frame = 0; frame < 6; ++frame) { + AjmBatchInfo info{}; + DecodeSideband sideband{}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobDecode(&info, instance, MP3_MONO + offset, sizeof(MP3_MONO) - offset, pcm.data(), pcm.size() * sizeof(std::int16_t), &sideband) == 0); + std::uint32_t id = 0; + AjmBatchError error{}; + Require(sceAjmBatchStart(context, &info, 0, &error, &id) == 0 && sceAjmBatchWait(context, id, 0, &error) == 0); + Require(sideband.result == 0 && sideband.inputConsumed == 96 && sideband.outputWritten == 1152 * 2); + Require(sideband.totalDecodedSamples == static_cast(frame + 1) * 1152); + for (const std::int16_t sample : pcm) peak = std::max(peak, static_cast(std::abs(sample))); + offset += static_cast(sideband.inputConsumed); + } + Require(offset == sizeof(MP3_MONO) && peak > 3276 && peak < 4915); + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +} + +int main() { + constexpr int invalidParameter = static_cast(0x80930005); + std::uint32_t context = 0; + Require(sceAjmInitialize(0, nullptr) == invalidParameter); + Require(sceAjmInitialize(0, &context) == 0 && context != 0); + + const std::uint8_t stereo48k[4] = {0xFE, 0x72, 0x1F, 0xF0}; + AjmDecAt9ConfigDataInfo info{}; + Require(sceAjmDecAt9ParseConfigData(stereo48k, &info) == 0); + Require(info.channels == 2 && info.sample_rate == 48000 && info.frame_samples_per_channel == 256); + Require(info.superframe_samples_per_channel == 1024 && info.superframe_size == 1024); + const std::uint8_t badHeader[4] = {0xFD, 0x72, 0x1F, 0xF0}; + Require(sceAjmDecAt9ParseConfigData(badHeader, &info) == invalidParameter); + Require(sceAjmDecAt9ParseConfigData(nullptr, &info) == invalidParameter); + TestMp3(context); + Require(sceAjmFinalize(context) == 0); +}