Merge pull request #3831 from eljackjoestar-cmd/codex/opus-surround
feat(ajm): decode Opus surround mapping family
This commit is contained in:
commit
84377033a5
|
|
@ -6,6 +6,7 @@
|
|||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavutil/log.h>
|
||||
#include <libavutil/mem.h>
|
||||
}
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
|
|
@ -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<std::uint8_t> 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<int>(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<int>(21 + instance.opusChannels);
|
||||
instance.opus->extradata = static_cast<std::uint8_t*>(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<std::uint8_t>(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");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<std::uint8_t> 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<std::uint8_t> 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<std::uint8_t> 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<std::uint8_t>(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<std::uint8_t>(payload);
|
||||
packet[1] = static_cast<std::uint8_t>(payload >> 8u);
|
||||
std::vector<std::int16_t> 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<std::size_t>(first.inputConsumed) == packet.size() && first.outputWritten == static_cast<std::int32_t>(pcm.size() * sizeof(std::int16_t)));
|
||||
std::vector<std::int16_t> 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<std::int32_t>(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<int>(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<std::uint8_t> 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);
|
||||
|
|
|
|||
|
|
@ -275,7 +275,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
|
||||
|
|
|
|||
Loading…
Reference in New Issue