diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 4d7cd26fa..8dc5f2ad7 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -6,6 +6,7 @@ extern "C" { #include #include +#include } #include #include @@ -134,6 +135,7 @@ struct Instance { std::uint32_t mp3Bitrate = 0; AVCodecContext* opus = nullptr; std::uint32_t opusChannels = 0; + std::uint32_t opusMappingFamily = 0; std::uint32_t opusSampleRate = 0; std::vector opusPending; Resampler resampler; @@ -369,10 +371,13 @@ std::int32_t InitializeInstance(Instance& instance, const std::uint8_t* paramete std::memcpy(&channels, parameters, sizeof(channels)); std::memcpy(&sampleRate, parameters + 4, sizeof(sampleRate)); if (channels == 0 || channels > 8 || sampleRate == 0) return AJM_RESULT_INVALID_PARAMETER; - std::uint32_t third = 0; - if (size >= 12) std::memcpy(&third, parameters + 8, sizeof(third)); - if (sampleRate != 48000 || third != 0) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (Opus sample rate other than 48000 or nonzero third parameter word)"); + std::uint32_t mappingFamily = 0; + if (size >= 12) std::memcpy(&mappingFamily, parameters + 8, sizeof(mappingFamily)); + if (sampleRate != 48000) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (Opus sample rate other than 48000)"); + if (mappingFamily > 1) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (Opus mapping family other than 0 or 1)"); + if (mappingFamily == 0 && channels > 2) return AJM_RESULT_INVALID_PARAMETER; instance.opusChannels = channels; + instance.opusMappingFamily = mappingFamily; instance.opusSampleRate = sampleRate; OpenOpus(instance); instance.opusPending.clear(); @@ -948,6 +953,29 @@ void OpenOpus(Instance& instance) { if (!instance.opus) throw std::runtime_error("AJM: cannot allocate the FFmpeg Opus decoder"); av_channel_layout_default(&instance.opus->ch_layout, static_cast(instance.opusChannels)); instance.opus->sample_rate = 48000; + if (instance.opusMappingFamily == 1) { + static constexpr std::uint8_t streams[8] = {1, 1, 2, 2, 3, 4, 4, 5}; + static constexpr std::uint8_t coupled[8] = {0, 1, 1, 2, 2, 2, 3, 3}; + static constexpr std::uint8_t mapping[8][8] = { + {0}, {0, 1}, {0, 2, 1}, {0, 1, 2, 3}, + {0, 4, 1, 2, 3}, {0, 4, 1, 2, 3, 5}, + {0, 4, 1, 2, 3, 5, 6}, {0, 6, 1, 2, 3, 4, 5, 7} + }; + const std::size_t index = instance.opusChannels - 1; + instance.opus->extradata_size = static_cast(21 + instance.opusChannels); + instance.opus->extradata = static_cast(av_mallocz(instance.opus->extradata_size + AV_INPUT_BUFFER_PADDING_SIZE)); + if (!instance.opus->extradata) throw std::bad_alloc(); + auto* header = instance.opus->extradata; + std::memcpy(header, "OpusHead", 8); + header[8] = 1; + header[9] = static_cast(instance.opusChannels); + header[12] = 0x80; + header[13] = 0xbb; + header[18] = 1; + header[19] = streams[index]; + header[20] = coupled[index]; + std::memcpy(header + 21, mapping[index], instance.opusChannels); + } if (avcodec_open2(instance.opus, codec, nullptr) < 0) throw std::runtime_error("AJM: cannot open the FFmpeg Opus decoder"); } diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index c35ca5e78..b03e0a14f 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -131,10 +131,10 @@ void RunDecode(std::uint32_t context, std::uint32_t instance, const std::uint8_t Require(sceAjmBatchStart(context, &info, 0, &error, &id) == 0 && sceAjmBatchWait(context, id, 0, &error) == 0); } -void TestOpus(std::uint32_t context) { +void TestOpus(std::uint32_t context, std::uint32_t mappingFamily = 0) { std::uint32_t instance = 0; Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0); - const std::uint32_t parameters[3] = {2, 48000, 0}; + const std::uint32_t parameters[3] = {2, 48000, mappingFamily}; std::vector batch(4096); AjmBatchInfo info{}; std::int64_t initResult[2] = {-1, -1}; @@ -171,6 +171,80 @@ void TestOpus(std::uint32_t context) { Require(sceAjmInstanceDestroy(context, instance) == 0); } +void TestOpusSurround(std::uint32_t context, std::uint32_t channels, std::size_t activeStream) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0); + const std::uint32_t parameters[3] = {channels, 48000, 1}; + std::vector batch(4096); + AjmBatchInfo info{}; + std::int64_t initResult[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instance, parameters, sizeof(parameters), initResult) == 0); + std::uint32_t id = 0; + AjmBatchError error{}; + Require(sceAjmBatchStart(context, &info, 0, &error, &id) == 0 && sceAjmBatchWait(context, id, 0, &error) == 0); + Require(initResult[0] == 0); + const std::size_t streamCount = channels == 3 || channels == 4 ? 2 : channels == 5 ? 3 : channels == 8 ? 5 : 4; + const std::size_t coupledCount = channels == 3 ? 1 : channels >= 7 ? 3 : 2; + std::size_t offset = 0; + for (std::size_t packetIndex = 0; packetIndex < 3; ++packetIndex) { + const std::size_t bytes = OPUS_STEREO[offset] | (std::size_t{OPUS_STEREO[offset + 1]} << 8u); + Require((OPUS_STEREO[offset + 2] & 3u) == 0 && bytes < 252); + std::vector packet{0, 0}; + for (std::size_t stream = 0; stream < streamCount; ++stream) { + if (stream == activeStream) { + packet.push_back(OPUS_STEREO[offset + 2]); + if (stream + 1 < streamCount) packet.push_back(static_cast(bytes - 1)); + packet.insert(packet.end(), OPUS_STEREO + offset + 3, OPUS_STEREO + offset + 2 + bytes); + } else { + packet.push_back(stream < coupledCount ? 0xfc : 0xf8); + if (stream + 1 < streamCount) packet.push_back(2); + packet.push_back(0xff); + packet.push_back(0xfe); + } + } + const std::size_t payload = packet.size() - 2; + packet[0] = static_cast(payload); + packet[1] = static_cast(payload >> 8u); + std::vector pcm(512 * channels, -12345); + DecodeSideband first{}; + RunDecode(context, instance, packet.data(), packet.size(), pcm.data(), pcm.size() * sizeof(std::int16_t), first); + Require(first.result == 0 && static_cast(first.inputConsumed) == packet.size() && first.outputWritten == static_cast(pcm.size() * sizeof(std::int16_t))); + std::vector rest(448 * channels, -12345); + DecodeSideband remaining{}; + RunDecode(context, instance, packet.data() + packet.size(), 0, rest.data(), rest.size() * sizeof(std::int16_t), remaining); + Require(remaining.result == 0 && remaining.inputConsumed == 0 && remaining.outputWritten == static_cast(rest.size() * sizeof(std::int16_t))); + Require(remaining.totalDecodedSamples == (packetIndex + 1) * 960); + pcm.insert(pcm.end(), rest.begin(), rest.end()); + for (std::size_t channel = 0; channel < channels; ++channel) { + int peak = 0; + for (std::size_t sample = channel; sample < pcm.size(); sample += channels) peak = std::max(peak, std::abs(static_cast(pcm[sample]))); + const std::size_t activeChannel = activeStream == 0 ? 0 : channels == 4 ? 2 : channels == 5 ? 3 : channels == 6 ? 4 : channels == 7 ? activeStream == 1 ? 5 : 2 : activeStream == 1 ? 6 : 4; + const std::size_t secondChannel = channels == 7 && activeStream == 2 ? 4 : activeChannel + 1; + const bool active = channel == activeChannel || channel == secondChannel; + Require(active ? packetIndex == 0 || peak > 900 : peak <= 2); + } + offset += 2 + bytes; + } + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +void TestOpusFamilyZeroChannels(std::uint32_t context) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0); + const std::uint32_t parameters[3] = {4, 48000, 0}; + std::vector batch(4096); + AjmBatchInfo info{}; + std::int32_t result[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instance, parameters, sizeof(parameters), result) == 0); + std::uint32_t id = 0; + AjmBatchError error{}; + Require(sceAjmBatchStart(context, &info, 0, &error, &id) == 0 && sceAjmBatchWait(context, id, 0, &error) == 0); + Require(result[0] == 4 && result[1] == 0); + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + void TestOpusInputBeyondStatedData(std::uint32_t context, bool multipleFrames) { #ifdef _WIN32 SYSTEM_INFO system{}; @@ -1170,6 +1244,18 @@ int main() { TestMp3ParseOfl(); TestMp3(context); TestOpus(context); + TestOpus(context, 1); + TestOpusFamilyZeroChannels(context); + TestOpusSurround(context, 3, 0); + for (std::size_t stream = 0; stream < 2; ++stream) { + TestOpusSurround(context, 4, stream); + TestOpusSurround(context, 5, stream); + TestOpusSurround(context, 6, stream); + } + for (std::size_t stream = 0; stream < 3; ++stream) { + TestOpusSurround(context, 7, stream); + TestOpusSurround(context, 8, stream); + } TestOpusInputBeyondStatedData(context, false); TestOpusInputBeyondStatedData(context, true); TestDecodeSingle(context); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index face0cf0e..3724f0fa0 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -269,7 +269,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 - [NGS2 port matrix](../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a port without a matrix, or with a selected matrix whose levels were never set, mixes through a default channel map: mono reaches both stereo channels at -3 dB and the 5.1 and 7.1 centre, stereo folds to mono at -3 dB per side, and wider sources fold down with 0.5 and -3 dB gains. The map and the equivalence of an unset matrix with no matrix were measured from the PS4 firmware 11.00 `libSceNgs2.sprx`, not yet from the PS5 library. That library only accepts submixer and mastering voices with 1, 2, 6 or 8 channels, so the map into 3, 4, 5 or 7 channels throws. A matrix set with fewer levels than the destination needs still mixes only the rows it has - [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 +- [AJM Opus decoder](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout and little-endian `u16` packet byte count come from The Smurfs Dreams (`2, 48000, 0`) and Lost Soul Aside (`4, 48000, 1`). The third word is interpreted as the mapping family, as in [KytyPS5's initialize structure](https://github.com/KytyPS5/KytyPS5/blob/984cfcebd141b211aa9e0a8cde63b0ebe652d07a/src/libs/ajm/opus_decoder.h#L11-L15). Family 1 uses the conventional stream counts, coupled counts and channel map from [libopus 1.5.2's surround layout](https://github.com/xiph/opus/blob/v1.5.2/src/opus_multistream_encoder.c#L47-L55), passed to FFmpeg through an [RFC 7845 section 5.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1) header with zero pre-skip and gain; gapless skipping remains controlled by AJM sideband parameters. FFmpeg returns PCM in its native speaker order. The implicit stream layout, PCM channel order and invalid family-0 channel count result are not verified on a console. Only 48000 Hz and mapping families 0 and 1 are implemented; other rates and families throw - [sceAjmBatchJobSetResampleParametersEx](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `(info, instance, float ratio_start, float ratio_change_per_sample, flags, result)` signature is inferred from the name and from `sceAjmBatchJobSetResampleParameters`; with a zero change per sample it sets the ratio as `sceAjmBatchJobSetResampleParameters` does (Watch Dogs: Legion, PPSA01500, calls it so). How a non-zero change ramps the ratio is unknown and throws - [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