diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index def7785d9..f58476bec 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -389,6 +389,18 @@ struct AjmDecAt9ConfigDataInfo { std::uint32_t superframe_size; }; +struct AjmDecMp3ParseFrame { + std::uint64_t frame_size; + std::uint32_t num_channels; + std::uint32_t samples_per_channel; + std::uint32_t bitrate; + std::uint32_t sample_rate; + std::uint32_t encoder_delay; + std::uint32_t num_frames; + std::uint32_t total_samples; + std::uint32_t ofl_type; +}; + using AudioOut2ContextHandle = std::uint64_t; using AudioOut2PortHandle = std::uint64_t; using AudioOut2UserHandle = std::uintptr_t; diff --git a/core/libs/prx/libSceAjm.native/Export.cpp b/core/libs/prx/libSceAjm.native/Export.cpp index ff03fa3b4..11bc7e1b7 100644 --- a/core/libs/prx/libSceAjm.native/Export.cpp +++ b/core/libs/prx/libSceAjm.native/Export.cpp @@ -129,9 +129,4 @@ const char* APS5_VABI sceAjmStrError(int error) { 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 index 22749392c..5678a519b 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -832,6 +832,36 @@ int APS5_VABI sceAjmDecAt9ParseConfigData(const void* config_data, AjmDecAt9Conf return 0; } +int APS5_VABI sceAjmDecMp3ParseFrame(const uint8_t* stream, uint32_t streamSize, int parseOfl, AjmDecMp3ParseFrame* frame) { + if (!stream || streamSize < 4 || !frame) return SCE_AJM_ERROR_INVALID_PARAMETER; + static constexpr std::uint32_t sampleRates[4][4] = {{11025, 12000, 8000, 0}, {0, 0, 0, 0}, {22050, 24000, 16000, 0}, {44100, 48000, 32000, 0}}; + static constexpr std::uint32_t kbps[4][16] = { + {0, 8, 16, 24, 32, 40, 48, 56, 64, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0}, + {0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0}, + }; + const std::uint32_t header = (std::uint32_t{stream[0]} << 24u) | (std::uint32_t{stream[1]} << 16u) | (std::uint32_t{stream[2]} << 8u) | stream[3]; + const std::uint32_t version = (header >> 19u) & 3u; + const std::uint32_t sampleRate = sampleRates[version][(header >> 10u) & 3u]; + const std::uint32_t bitrate = kbps[version][(header >> 12u) & 15u] * 1000u; + const bool valid = (header >> 21u) == 0x7FFu && sampleRate != 0 && bitrate != 0; + AJM_TRACE("[ajm] parse mp3 frame %02x %02x %02x %02x (%u bytes, parse ofl %d) -> %s, %u Hz, %u bps\n", stream[0], stream[1], stream[2], stream[3], streamSize, parseOfl, valid ? "ok" : "invalid", sampleRate, bitrate); + if (!valid) return SCE_AJM_ERROR_INVALID_PARAMETER; + if (parseOfl) NotImplemented_nid_no_patch("sceAjmDecMp3ParseFrame (original file length lookup)"); + const bool mpeg1 = version == 3; + frame->frame_size = (mpeg1 ? 144u : 72u) * bitrate / sampleRate + ((header >> 9u) & 1u); + frame->num_channels = ((header >> 6u) & 3u) == 3u ? 1u : 2u; + frame->samples_per_channel = mpeg1 ? 1152u : 576u; + frame->bitrate = bitrate; + frame->sample_rate = sampleRate; + frame->encoder_delay = 0; + frame->num_frames = 0; + frame->total_samples = 0; + frame->ofl_type = 0; + 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; diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index f262079b3..590e6b9ab 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -11,6 +11,7 @@ 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 sceAjmDecMp3ParseFrame(const std::uint8_t*, std::uint32_t, int, AjmDecMp3ParseFrame*); 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*); @@ -148,6 +149,52 @@ void TestOpus(std::uint32_t context) { Require(sceAjmInstanceDestroy(context, instance) == 0); } +bool ParsesTo(const std::uint8_t* stream, std::uint32_t streamSize, std::uint64_t frameSize, std::uint32_t channels, std::uint32_t samples, std::uint32_t bitrate, std::uint32_t sampleRate) { + AjmDecMp3ParseFrame frame; + std::memset(&frame, 0xFF, sizeof(frame)); + if (sceAjmDecMp3ParseFrame(stream, streamSize, 0, &frame) != 0) return false; + return frame.frame_size == frameSize && frame.num_channels == channels && frame.samples_per_channel == samples && frame.bitrate == bitrate && frame.sample_rate == sampleRate && + frame.encoder_delay == 0 && frame.num_frames == 0 && frame.total_samples == 0 && frame.ofl_type == 0; +} + +void TestMp3ParseFrame() { + constexpr int invalidParameter = static_cast(0x80930005); + static_assert(sizeof(AjmDecMp3ParseFrame) == 40); + for (std::size_t offset = 0; offset < sizeof(MP3_MONO); offset += 96) { + Require(ParsesTo(MP3_MONO + offset, static_cast(sizeof(MP3_MONO) - offset), 96, 1, 1152, 32000, 48000)); + } + + const std::uint8_t mpeg1StereoPadded[4] = {0xFF, 0xFB, 0x92, 0x00}; + Require(ParsesTo(mpeg1StereoPadded, 4, 418, 2, 1152, 128000, 44100)); + const std::uint8_t mpeg1Protected[4] = {0xFF, 0xFA, 0xE8, 0x40}; + Require(ParsesTo(mpeg1Protected, 4, 1440, 2, 1152, 320000, 32000)); + const std::uint8_t mpeg2Mono[4] = {0xFF, 0xF3, 0x80, 0xC0}; + Require(ParsesTo(mpeg2Mono, 4, 208, 1, 576, 64000, 22050)); + const std::uint8_t mpeg2DualChannel[4] = {0xFF, 0xF3, 0xE8, 0x80}; + Require(ParsesTo(mpeg2DualChannel, 4, 720, 2, 576, 160000, 16000)); + const std::uint8_t mpeg25Mono[4] = {0xFF, 0xE3, 0x88, 0xC0}; + Require(ParsesTo(mpeg25Mono, 4, 576, 1, 576, 64000, 8000)); + const std::uint8_t mpeg25StereoPadded[4] = {0xFF, 0xE3, 0x12, 0x00}; + Require(ParsesTo(mpeg25StereoPadded, 4, 53, 2, 576, 8000, 11025)); + + AjmDecMp3ParseFrame frame{}; + Require(sceAjmDecMp3ParseFrame(nullptr, 4, 0, &frame) == invalidParameter); + Require(sceAjmDecMp3ParseFrame(MP3_MONO, 4, 0, nullptr) == invalidParameter); + Require(sceAjmDecMp3ParseFrame(MP3_MONO, 3, 0, &frame) == invalidParameter); + const std::uint8_t brokenSync[4] = {0xFF, 0xDB, 0x14, 0xC4}; + Require(sceAjmDecMp3ParseFrame(brokenSync, 4, 0, &frame) == invalidParameter); + const std::uint8_t reservedVersion[4] = {0xFF, 0xEB, 0x14, 0xC4}; + Require(sceAjmDecMp3ParseFrame(reservedVersion, 4, 0, &frame) == invalidParameter); + const std::uint8_t freeBitrate[4] = {0xFF, 0xFB, 0x04, 0xC4}; + Require(sceAjmDecMp3ParseFrame(freeBitrate, 4, 0, &frame) == invalidParameter); + const std::uint8_t forbiddenBitrate[4] = {0xFF, 0xFB, 0xF4, 0xC4}; + Require(sceAjmDecMp3ParseFrame(forbiddenBitrate, 4, 0, &frame) == invalidParameter); + const std::uint8_t reservedSampleRate[4] = {0xFF, 0xFB, 0x1C, 0xC4}; + Require(sceAjmDecMp3ParseFrame(reservedSampleRate, 4, 0, &frame) == invalidParameter); + const std::uint8_t mpeg25Above64Kbps[4] = {0xFF, 0xE3, 0x98, 0xC0}; + Require(sceAjmDecMp3ParseFrame(mpeg25Above64Kbps, 4, 0, &frame) == invalidParameter); +} + } int main() { @@ -164,6 +211,7 @@ int main() { const std::uint8_t badHeader[4] = {0xFD, 0x72, 0x1F, 0xF0}; Require(sceAjmDecAt9ParseConfigData(badHeader, &info) == invalidParameter); Require(sceAjmDecAt9ParseConfigData(nullptr, &info) == invalidParameter); + TestMp3ParseFrame(); TestMp3(context); TestOpus(context); Require(sceAjmFinalize(context) == 0); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 9fe00e510..1aea9327d 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -51,6 +51,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAvPlayerStartEx](../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - start info layout unknown; it is ignored and playback starts as with `sceAvPlayerStart` - [sceAvPlayerInit / sceAvPlayerInitEx](../../core/libs/prx/libSceAvPlayer/src/Player.cpp) (libSceAvPlayer) - behaviour without a memory replacement unknown; frame and sample buffers then come from the guest heap - [SceAvPlayerVideoEx](../../core/libs/SceTypes.hpp) (libSceAvPlayer) - frame rate field and encoding unknown; it is left zero in frame and stream info +- [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 - [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 ### Functional @@ -68,5 +69,6 @@ Throughout the project, every function at every stage either **does exactly what - `--to-intel` replaces the approximate VEX.128 register forms of VRSQRTPS and VRCPPS with [correctly rounded](../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). The 256-bit, scalar, legacy and memory forms keep the native instruction, as does a 4-byte site with no movable instruction after it for the jump (5 sites in PPSA21564). - [sceKeyboardGetKey2Char](../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) (libSceKeyboard) translates only the 101-key (US) arrangement and throws for the 106-key (Japanese) one; Ctrl and Alt do not change the character. - [libSceAudiodec](../../core/libs/prx/libSceAudiodec/Export.cpp) throws for the 24-bit PCM word size (`iBwPcm` 0): its sample layout is unknown. ATRAC9 decoding is covered only by configuration and error tests, as no ATRAC9 encoder is available for a fixture. +- [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) throws when the original file length is requested (`parse_ofl` non-zero): reading it from the frame's Xing/Info, VBRI or Fraunhofer data is not implemented. - [libScePngEnc](../../core/libs/prx/libScePngEnc/Export.cpp) honours `filter_type` only as all filters (adaptive) or a single filter: the [PNG encoder](../../core/Decoder/Png/src/Png.cpp) (stb) cannot restrict adaptive filtering to a subset, so the first filter in the mask is used. - [libScePlayerInvitationDialog](../../core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp) simulates dialog completion without displaying UI or sending invitations; its parameter ABI remains unverified.