From 485f638a530e828109f623f4920fb976b462f449 Mon Sep 17 00:00:00 2001 From: SandstromPL Date: Tue, 13 Oct 2026 16:03:16 +0530 Subject: [PATCH 1/2] fix(ajm): accept Opus mapping family 255 for a mono stream Co-authored-by: poonasor <12569806+poonasor@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAjm.native/src/Ajm.cpp | 2 +- core/libs/tests/GuestAjm.cpp | 48 ++++++++++++++++++++++ docs/dev/debt/libSceAjm.native.md | 2 +- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 8dc5f2ad78..15cc83db67 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -374,7 +374,7 @@ std::int32_t InitializeInstance(Instance& instance, const std::uint8_t* paramete 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 > 1 && (mappingFamily != 255 || channels != 1)) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (Opus mapping family other than 0, 1 or a mono stream's 255)"); if (mappingFamily == 0 && channels > 2) return AJM_RESULT_INVALID_PARAMETER; instance.opusChannels = channels; instance.opusMappingFamily = mappingFamily; diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index b03e0a14fb..5d0dc75362 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -496,6 +496,53 @@ bool Refused(std::uint32_t context, const AjmBatchInfo& info) { return false; } +void TestOpusMonoFamily255(std::uint32_t context) { + std::uint32_t instances[2] = {}; + for (std::size_t index = 0; index < 2; ++index) { + Require(sceAjmInstanceCreate(context, 24, 0, &instances[index]) == 0); + const std::uint32_t parameters[3] = {1, 48000, index == 0 ? 0u : 255u}; + std::vector batch(4096); + AjmBatchInfo info{}; + std::int64_t initResult[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instances[index], parameters, sizeof(parameters), initResult) == 0); + Submit(context, info); + Require(initResult[0] == 0); + } + std::size_t offset = 0; + int peak = 0; + for (int packet = 0; packet < 3; ++packet) { + const std::size_t bytes = OPUS_STEREO[offset] | (std::size_t{OPUS_STEREO[offset + 1]} << 8u); + std::vector pcm[2] = {std::vector(960, -12345), std::vector(960, -12345)}; + for (std::size_t index = 0; index < 2; ++index) { + DecodeSideband sideband{}; + RunDecode(context, instances[index], OPUS_STEREO + offset, 2 + bytes, pcm[index].data(), pcm[index].size() * sizeof(std::int16_t), sideband); + Require(sideband.result == 0 && static_cast(sideband.inputConsumed) == 2 + bytes && sideband.outputWritten == 960 * 2); + Require(sideband.totalDecodedSamples == static_cast(packet + 1) * 960); + } + Require(pcm[0] == pcm[1]); + if (packet > 0) { + for (const std::int16_t sample : pcm[1]) peak = std::max(peak, std::abs(static_cast(sample))); + } + offset += 2 + bytes; + } + Require(peak > 900 && peak < 1200); + + const std::uint32_t refusedParameters[2][3] = {{2, 48000, 255}, {1, 48000, 2}}; + for (const auto& parameters : refusedParameters) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0); + 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); + Require(Refused(context, info)); + Require(sceAjmInstanceDestroy(context, instance) == 0); + } + for (const std::uint32_t instance : instances) Require(sceAjmInstanceDestroy(context, instance) == 0); +} + void TestDecodeSingle(std::uint32_t context) { std::uint32_t instance = 0; Require(sceAjmInstanceCreate(context, 0, 0, &instance) == 0); @@ -1245,6 +1292,7 @@ int main() { TestMp3(context); TestOpus(context); TestOpus(context, 1); + TestOpusMonoFamily255(context); TestOpusFamilyZeroChannels(context); TestOpusSurround(context, 3, 0); for (std::size_t stream = 0; stream < 2; ++stream) { diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index 3a3e8f5009..d9a2f57845 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -6,7 +6,7 @@ - [sceAjmBatchJobGetInfo](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - writes the result and format sideband of a run job without buffers with the `SIDEBAND_FORMAT` flag (0x400000000000), as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does, not confirmed on a PS5 title; the format reports bitrate 0 for ATRAC9 as in KytyPS5, while [shadPS4](https://github.com/shadps4-emu/shadPS4) derives it from the superframe size; before the instance is initialized it returns the not-initialized result (1) and no format, like the other run jobs here, while KytyPS5 returns result 0 with the format of the uninitialized decoder - [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 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 +- [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. NHL 27 (PPSA34063) passes `1, 48000, 255`. Family 255 with one channel is decoded as one mono stream, the same as family 0; this is assumed, since the stream count, coupled stream count and channel mapping that a family-255 Opus header carries ([RFC 7845 section 5.1.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1.1)) are not passed. Only 48000 Hz, mapping families 0 and 1, and family 255 with one channel are implemented; other rates and families, and family 255 with more channels, 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 From 911e6c83baf7a57dc682d1014daca7cea2e8190a Mon Sep 17 00:00:00 2001 From: ps54all Date: Wed, 14 Oct 2026 00:03:21 +0200 Subject: [PATCH 2/2] feat(ajm): decode Opus mapping family 255 with several channels A family 255 instance with more than one channel carries no stream count or mapping in its initialization words, so its decoder now opens on the first packet: the multistream packet's self-delimited streams are counted (RFC 6716 appendix B), each stream's TOC stereo flag marks it as coupled, and the channels map in order. A packet whose streams do not make the channel count throws. Metal Gear Solid 4 Master Collection (PPSA18194) initializes a 4-channel family 255 decoder for its intro movie. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAjm.native/src/Ajm.cpp | 98 +++++++++++++++++++++- core/libs/tests/GuestAjm.cpp | 66 ++++++++++++++- docs/dev/debt/libSceAjm.native.md | 1 + 3 files changed, 162 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 15cc83db67..1a7da71af6 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -136,6 +136,9 @@ struct Instance { AVCodecContext* opus = nullptr; std::uint32_t opusChannels = 0; std::uint32_t opusMappingFamily = 0; + std::uint32_t opusStreams = 0; + std::uint32_t opusCoupled = 0; + bool opusLayoutPending = false; std::uint32_t opusSampleRate = 0; std::vector opusPending; Resampler resampler; @@ -374,12 +377,13 @@ std::int32_t InitializeInstance(Instance& instance, const std::uint8_t* paramete 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 && (mappingFamily != 255 || channels != 1)) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (Opus mapping family other than 0, 1 or a mono stream's 255)"); + if (mappingFamily > 1 && mappingFamily != 255) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (Opus mapping family other than 0, 1 or 255)"); if (mappingFamily == 0 && channels > 2) return AJM_RESULT_INVALID_PARAMETER; instance.opusChannels = channels; instance.opusMappingFamily = mappingFamily; instance.opusSampleRate = sampleRate; - OpenOpus(instance); + instance.opusLayoutPending = mappingFamily == 255 && channels > 1; + if (!instance.opusLayoutPending) OpenOpus(instance); instance.opusPending.clear(); instance.initialized = true; instance.totalDecodedSamples = 0; @@ -953,6 +957,21 @@ 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 == 255 && 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] = 255; + header[19] = static_cast(instance.opusStreams); + header[20] = static_cast(instance.opusCoupled); + for (std::uint32_t channel = 0; channel < instance.opusChannels; ++channel) header[21 + channel] = static_cast(channel); + } 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}; @@ -999,6 +1018,80 @@ std::size_t OpusPacketSamples(const std::uint8_t* packet, std::size_t size) { return frames * frameSamples; } + +std::size_t OpusLength(const std::uint8_t* data, std::size_t size, std::size_t& at) { + if (at >= size) return SIZE_MAX; + const std::size_t first = data[at++]; + if (first < 252) return first; + if (at >= size) return SIZE_MAX; + return first + 4u * data[at++]; +} + +std::size_t OpusSelfDelimitedBytes(const std::uint8_t* data, std::size_t size) { + if (size < 1) return SIZE_MAX; + const std::uint32_t code = data[0] & 3u; + std::size_t at = 1; + if (code == 0) { + const std::size_t frame = OpusLength(data, size, at); + return frame == SIZE_MAX ? SIZE_MAX : at + frame; + } + if (code == 1) { + const std::size_t frame = OpusLength(data, size, at); + return frame == SIZE_MAX ? SIZE_MAX : at + 2 * frame; + } + if (code == 2) { + const std::size_t first = OpusLength(data, size, at); + const std::size_t second = OpusLength(data, size, at); + return first == SIZE_MAX || second == SIZE_MAX ? SIZE_MAX : at + first + second; + } + if (at >= size) return SIZE_MAX; + const std::uint8_t count = data[at++]; + const std::size_t frames = count & 0x3fu; + if (frames == 0) return SIZE_MAX; + std::size_t padding = 0; + if ((count & 0x40u) != 0) { + for (;;) { + if (at >= size) return SIZE_MAX; + const std::uint8_t value = data[at++]; + padding += value == 255 ? 254 : value; + if (value != 255) break; + } + } + std::size_t total = 0; + if ((count & 0x80u) != 0) { + for (std::size_t frame = 0; frame < frames; ++frame) { + const std::size_t bytes = OpusLength(data, size, at); + if (bytes == SIZE_MAX) return SIZE_MAX; + total += bytes; + } + } else { + const std::size_t bytes = OpusLength(data, size, at); + if (bytes == SIZE_MAX) return SIZE_MAX; + total = bytes * frames; + } + return at + total + padding; +} + +void OpenOpusFromPacket(Instance& instance, const std::uint8_t* data, std::size_t size) { + std::uint32_t streams = 0; + std::uint32_t coupled = 0; + std::size_t offset = 0; + while (offset < size) { + if ((data[offset] & 0x04u) != 0) ++coupled; + ++streams; + const std::uint32_t channels = coupled * 2 + (streams - coupled); + if (channels >= instance.opusChannels) break; + const std::size_t bytes = OpusSelfDelimitedBytes(data + offset, size - offset); + if (bytes == SIZE_MAX || bytes >= size - offset) break; + offset += bytes; + } + if (coupled * 2 + (streams - coupled) != instance.opusChannels) throw std::runtime_error("AJM Opus: a mapping family 255 packet's " + std::to_string(streams) + " streams (" + std::to_string(coupled) + " stereo) do not make its " + std::to_string(instance.opusChannels) + " channels"); + instance.opusStreams = streams; + instance.opusCoupled = coupled; + instance.opusLayoutPending = false; + OpenOpus(instance); +} + void RunOpus(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { const JobInput input(job, inputs); PcmOutputs pcmOutputs(outputs, job.outputCount); @@ -1048,6 +1141,7 @@ void RunOpus(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, if (frames == 0 && pcmOutputs.produced == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; break; } + if (instance.opusLayoutPending) OpenOpusFromPacket(instance, data, bytes); packet->data = const_cast(data); packet->size = static_cast(bytes); if (avcodec_send_packet(instance.opus, packet.get()) < 0) { diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index 5d0dc75362..9fdfdb3d1c 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -496,6 +496,69 @@ bool Refused(std::uint32_t context, const AjmBatchInfo& info) { return false; } +void AppendOpusLength(std::vector& out, std::size_t length) { + if (length < 252) { + out.push_back(static_cast(length)); + return; + } + out.push_back(static_cast(252 + (length - 252) % 4)); + out.push_back(static_cast((length - 252) / 4)); +} + +std::vector SelfDelimited(const std::uint8_t* packet, std::size_t bytes) { + const std::uint32_t code = packet[0] & 3u; + Require(code <= 1); + std::vector out{packet[0]}; + AppendOpusLength(out, code == 0 ? bytes - 1 : (bytes - 1) / 2); + out.insert(out.end(), packet + 1, packet + bytes); + return out; +} + +void TestOpusMultistreamFamily255(std::uint32_t context) { + const bool stereo = (OPUS_STEREO[2] & 0x04u) != 0; + const std::uint32_t channels = stereo ? 4 : 2; + std::uint32_t mono = 0; + std::uint32_t multi = 0; + const std::uint32_t parameters[2][3] = {{stereo ? 2u : 1u, 48000, 0}, {channels, 48000, 255}}; + std::uint32_t* instances[2] = {&mono, &multi}; + for (std::size_t index = 0; index < 2; ++index) { + Require(sceAjmInstanceCreate(context, 24, 0, instances[index]) == 0); + std::vector batch(4096); + AjmBatchInfo info{}; + std::int64_t initResult[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, *instances[index], parameters[index], sizeof(parameters[index]), initResult) == 0); + Submit(context, info); + Require(initResult[0] == 0); + } + const std::uint32_t streamChannels = channels / 2; + std::size_t offset = 0; + for (int packet = 0; packet < 3; ++packet) { + const std::size_t bytes = OPUS_STEREO[offset] | (std::size_t{OPUS_STEREO[offset + 1]} << 8u); + const std::uint8_t* data = OPUS_STEREO + offset + 2; + std::vector joined = SelfDelimited(data, bytes); + joined.insert(joined.end(), data, data + bytes); + std::vector framed{static_cast(joined.size()), static_cast(joined.size() >> 8u)}; + framed.insert(framed.end(), joined.begin(), joined.end()); + std::vector single(960 * streamChannels, -12345); + std::vector both(960 * channels, -12345); + DecodeSideband sideband{}; + RunDecode(context, mono, OPUS_STEREO + offset, 2 + bytes, single.data(), single.size() * sizeof(std::int16_t), sideband); + Require(sideband.result == 0); + RunDecode(context, multi, framed.data(), framed.size(), both.data(), both.size() * sizeof(std::int16_t), sideband); + Require(sideband.result == 0 && static_cast(sideband.inputConsumed) == framed.size() && sideband.outputWritten == 960 * channels * 2); + for (std::size_t sample = 0; sample < 960; ++sample) { + for (std::uint32_t channel = 0; channel < streamChannels; ++channel) { + Require(both[sample * channels + channel] == single[sample * streamChannels + channel]); + Require(both[sample * channels + streamChannels + channel] == single[sample * streamChannels + channel]); + } + } + offset += 2 + bytes; + } + Require(sceAjmInstanceDestroy(context, mono) == 0); + Require(sceAjmInstanceDestroy(context, multi) == 0); +} + void TestOpusMonoFamily255(std::uint32_t context) { std::uint32_t instances[2] = {}; for (std::size_t index = 0; index < 2; ++index) { @@ -528,7 +591,7 @@ void TestOpusMonoFamily255(std::uint32_t context) { } Require(peak > 900 && peak < 1200); - const std::uint32_t refusedParameters[2][3] = {{2, 48000, 255}, {1, 48000, 2}}; + const std::uint32_t refusedParameters[1][3] = {{1, 48000, 2}}; for (const auto& parameters : refusedParameters) { std::uint32_t instance = 0; Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0); @@ -1293,6 +1356,7 @@ int main() { TestOpus(context); TestOpus(context, 1); TestOpusMonoFamily255(context); + TestOpusMultistreamFamily255(context); TestOpusFamilyZeroChannels(context); TestOpusSurround(context, 3, 0); for (std::size_t stream = 0; stream < 2; ++stream) { diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index d9a2f57845..5d3be631e2 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -19,3 +19,4 @@ - [sceAjmDecMp3ParseFrame](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) throws when the original file length is requested (`parse_ofl` non-zero) for a layer other than III, for a CRC-protected frame, or for a VBRI header outside MPEG-1 stereo: shadPS4 looks for the tag after the CRC and after the side information, while LAME and FFmpeg place it without the CRC and VBRI always at byte 36. - [ATRAC9 gapless segments](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a run job stops after the gapless total of output samples counted from the start of the segment, leaving the rest of its input unconsumed, and the next run starts the next segment with the decoder as it is and without a second skip. FMOD in PPSA29141 sets a total shorter than a sound's frames and reads the frames after it on later runs; with the output held at the total its stream reader waited for them forever and the last buffer repeated. The console's handling of the frames after the total is not documented; a reset (`SetGaplessDecode` with the reset flag, `ClearContext`, initialization) starts a new segment. An ATRAC9 run job without `RUN_MULTIPLE_FRAMES` decodes its whole input as one with it: FMOD in PPSA29141 puts two run jobs in one batch with the second's input right after the whole of the first's. Whether the console stops after one frame there is not verified; MP3 and Opus run jobs still stop after one frame - [AJM](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp), [APR](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) and [NGS2](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) state is allocated with `new` and never freed on purpose: title threads still call into these libraries while `std::exit` runs static destructors, so ordinary statics would be destroyed under them. Keep them leaked; `guest_ajm_teardown` covers the AJM case on Unix. +- [AJM Opus mapping family 255 with several channels](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): the initialization words carry no stream count or channel mapping, so the decoder opens on the first packet. It counts the self-delimited streams of that multistream packet ([RFC 6716 appendix B](https://datatracker.ietf.org/doc/html/rfc6716#appendix-B)) and takes each stream's stereo flag from its TOC byte as coupled, then decodes with an identity channel mapping. A packet whose streams do not add up to the channel count throws. The layout is inferred, not read from the console; unverified on hardware.