diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index d7fd0273e..7a4be6256 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -38,6 +38,7 @@ constexpr std::int32_t AJM_RESULT_CODEC_ERROR = 0x40000000; constexpr std::uint32_t CODEC_MP3 = 0; constexpr std::uint32_t CODEC_AT9 = 1; +constexpr std::uint32_t CODEC_OPUS = 24; // APS5_TRACE_AJM=1 logs every job the title submits and what the library writes back. bool TraceEnabled() { @@ -101,10 +102,15 @@ struct Instance { std::uint32_t mp3Channels = 0; std::uint32_t mp3SampleRate = 0; std::uint32_t mp3Bitrate = 0; + AVCodecContext* opus = nullptr; + std::uint32_t opusChannels = 0; + std::uint32_t opusSampleRate = 0; + std::vector opusPending; ~Instance() { if (decoder) Atrac9ReleaseHandle(decoder); if (mp3) avcodec_free_context(&mp3); + if (opus) avcodec_free_context(&opus); } }; @@ -212,7 +218,25 @@ bool ResetDecoder(Instance& instance) { return Atrac9InitDecoder(instance.decoder, config) == 0; } +void OpenOpus(Instance& instance); + std::int32_t InitializeInstance(Instance& instance, const std::uint8_t* parameters, std::uint64_t size) { + if (instance.codec == CODEC_OPUS) { + if (size < 8) return AJM_RESULT_INVALID_PARAMETER; + std::uint32_t channels = 0; + std::uint32_t sampleRate = 0; + 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; + instance.opusChannels = channels; + instance.opusSampleRate = sampleRate; + OpenOpus(instance); + instance.opusPending.clear(); + instance.initialized = true; + instance.totalDecodedSamples = 0; + instance.gapless = {}; + return 0; + } if (instance.codec != CODEC_AT9) { instance.initialized = true; return 0; @@ -521,6 +545,143 @@ void RunMp3(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c instance.mp3Channels, instance.mp3SampleRate, instance.mp3Bitrate, static_cast(instance.totalDecodedSamples)); WriteRunSideband(job, instance, result, consumed, pcmOutputs.produced, frames, SidebandFormat{instance.mp3Channels, ChannelMask(instance.mp3Channels), instance.mp3SampleRate, encoding, instance.mp3Bitrate, 0}); } +void OpenOpus(Instance& instance) { + static const bool quiet = (av_log_set_level(TraceEnabled() ? AV_LOG_WARNING : AV_LOG_QUIET), true); + (void)quiet; + const AVCodec* codec = avcodec_find_decoder(AV_CODEC_ID_OPUS); + if (instance.opus) avcodec_free_context(&instance.opus); + instance.opus = codec ? avcodec_alloc_context3(codec) : nullptr; + 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 (avcodec_open2(instance.opus, codec, nullptr) < 0) throw std::runtime_error("AJM: cannot open the FFmpeg Opus decoder"); +} + +std::size_t OpusPacketSamples(const std::uint8_t* packet, std::size_t size) { + if (size < 1) return 0; + const std::uint32_t config = packet[0] >> 3u; + std::size_t frameSamples = 0; + if (config < 12) frameSamples = (config & 3u) == 0 ? 480 : (config & 3u) == 1 ? 960 : (config & 3u) == 2 ? 1920 : 2880; + else if (config < 16) frameSamples = (config & 1u) == 0 ? 480 : 960; + else frameSamples = std::size_t{120} << (config & 3u); + std::size_t frames = 0; + switch (packet[0] & 3u) { + case 0: frames = 1; break; + case 1: + case 2: frames = 2; break; + default: + if (size < 2) return 0; + frames = packet[1] & 0x3fu; + break; + } + return frames * frameSamples; +} + +void RunOpus(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { + const auto input = JoinInputs(job, inputs); + PcmOutputs pcmOutputs(outputs, job.outputCount); + const auto encoding = PcmEncoding(instance); + if (encoding > 2) throw std::runtime_error("AJM Opus: PCM encoding " + std::to_string(encoding) + " is not implemented"); + const std::size_t sampleBytes = encoding == 0 ? sizeof(std::int16_t) : sizeof(std::int32_t); + std::unique_ptr packet(av_packet_alloc(), [](AVPacket* p) { av_packet_free(&p); }); + std::unique_ptr decoded(av_frame_alloc(), [](AVFrame* f) { av_frame_free(&f); }); + if (!packet || !decoded) throw std::bad_alloc(); + std::vector pcm; + const std::size_t channels = instance.opusChannels; + + std::int32_t result = 0; + std::size_t consumed = 0; + std::uint32_t frames = 0; + if (!instance.opusPending.empty()) { + const std::size_t bytes = std::min(instance.opusPending.size(), pcmOutputs.Room()); + pcmOutputs.Emit(instance.opusPending.data(), bytes); + instance.opusPending.erase(instance.opusPending.begin(), instance.opusPending.begin() + static_cast(bytes)); + } + const std::size_t frameBytes = channels * sampleBytes; + while (consumed < input.size() && instance.opusPending.empty()) { + if (input.size() - consumed < 2) { + result |= AJM_RESULT_PARTIAL_INPUT; + break; + } + const std::size_t bytes = input[consumed] | (std::size_t{input[consumed + 1]} << 8u); + if (input.size() - consumed - 2 < bytes) { + result |= AJM_RESULT_PARTIAL_INPUT; + break; + } + const auto* data = input.data() + consumed + 2; + const std::size_t samples = OpusPacketSamples(data, bytes); + if (bytes == 0 || samples == 0) { + result |= AJM_RESULT_INVALID_DATA; + break; + } + if (pcmOutputs.Room() < frameBytes) { + if (frames == 0 && pcmOutputs.produced == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; + break; + } + packet->data = const_cast(data); + packet->size = static_cast(bytes); + if (avcodec_send_packet(instance.opus, packet.get()) < 0) { + result |= AJM_RESULT_INVALID_DATA; + break; + } + consumed += 2 + bytes; + ++frames; + int status = 0; + while ((status = avcodec_receive_frame(instance.opus, decoded.get())) == 0) { + const auto format = static_cast(decoded->format); + if (format != AV_SAMPLE_FMT_FLTP && format != AV_SAMPLE_FMT_FLT) throw std::runtime_error("AJM Opus: FFmpeg sample format " + std::to_string(decoded->format) + " is not converted"); + const auto frameChannels = static_cast(decoded->ch_layout.nb_channels); + const auto frameSamples = static_cast(decoded->nb_samples); + pcm.assign(frameSamples * channels * sampleBytes, 0); + for (std::size_t sample = 0; sample < frameSamples; ++sample) { + for (std::size_t channel = 0; channel < channels; ++channel) { + const std::size_t sourceChannel = std::min(channel, frameChannels - 1); + const float value = format == AV_SAMPLE_FMT_FLTP ? reinterpret_cast(decoded->extended_data[sourceChannel])[sample] + : reinterpret_cast(decoded->data[0])[sample * frameChannels + sourceChannel]; + 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)); + std::memcpy(out, &converted, sizeof(converted)); + } else if (encoding == 1) { + const auto converted = static_cast(std::clamp(std::llrint(value * 2147483648.0), -2147483648LL, 2147483647LL)); + std::memcpy(out, &converted, sizeof(converted)); + } else { + std::memcpy(out, &value, sizeof(value)); + } + } + } + std::size_t first = 0; + std::size_t count = frameSamples; + if (instance.gapless.skippedSamples < instance.gapless.skipSamples) { + const std::size_t skip = std::min(count, instance.gapless.skipSamples - instance.gapless.skippedSamples); + instance.gapless.skippedSamples = static_cast(instance.gapless.skippedSamples + skip); + first += skip; + count -= skip; + } + if (instance.gapless.totalSamples != 0) { + const std::uint64_t remaining = instance.gapless.totalSamples > instance.totalDecodedSamples ? instance.gapless.totalSamples - instance.totalDecodedSamples : 0; + count = static_cast(std::min(count, remaining)); + } + const auto* start = pcm.data() + first * frameBytes; + const std::size_t total = count * frameBytes; + const std::size_t fits = std::min(total, pcmOutputs.Room() / frameBytes * frameBytes); + pcmOutputs.Emit(start, fits); + instance.opusPending.insert(instance.opusPending.end(), start + fits, start + total); + instance.totalDecodedSamples += count; + av_frame_unref(decoded.get()); + } + if (status != AVERROR(EAGAIN)) { + result |= AJM_RESULT_INVALID_DATA; + break; + } + if ((job.flags & RUN_MULTIPLE_FRAMES) == 0) break; + } + if (consumed == input.size() && frames == 0 && pcmOutputs.produced == 0 && result == 0) result |= AJM_RESULT_PARTIAL_INPUT; + + AJM_TRACE("[ajm] instance %u opus run flags 0x%llx: %zu input bytes, %zu output bytes -> result 0x%x, %u frames, consumed %zu, produced %zu, total samples %llu\n", job.instance, static_cast(job.flags), input.size(), pcmOutputs.capacity, static_cast(result), frames, consumed, pcmOutputs.produced, static_cast(instance.totalDecodedSamples)); + WriteRunSideband(job, instance, result, consumed, pcmOutputs.produced, frames, SidebandFormat{instance.opusChannels, ChannelMask(instance.opusChannels), instance.opusSampleRate, encoding, 0, 0}); +} + void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { if (job.kind == JobKind::GetStatistics) { AJM_TRACE("[ajm] statistics job: sideband %llu bytes\n", static_cast(job.sidebandSize)); @@ -547,6 +708,8 @@ void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* out instance->gapless.skippedSamples = 0; if (instance->decoder && instance->initialized) ResetDecoder(*instance); if (instance->mp3) avcodec_flush_buffers(instance->mp3); + if (instance->opus) avcodec_flush_buffers(instance->opus); + instance->opusPending.clear(); WriteResult(job.sideband, job.sidebandSize, 0); break; case JobKind::SetGaplessDecode: { @@ -567,6 +730,8 @@ void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* out RunAt9(*instance, job, inputs, outputs); } else if (instance->codec == CODEC_MP3) { RunMp3(*instance, job, inputs, outputs); + } else if (instance->codec == CODEC_OPUS) { + RunOpus(*instance, job, inputs, outputs); } else { throw std::runtime_error("AJM: decoding codec " + std::to_string(instance->codec) + " is not implemented"); } diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index aad2ed219..f262079b3 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -14,6 +14,7 @@ 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 sceAjmBatchJobInitialize(AjmBatchInfo*, std::uint32_t, const void*, std::size_t, void*); 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*); @@ -50,6 +51,21 @@ const std::uint8_t MP3_MONO[] = { 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, }; +const std::uint8_t OPUS_STEREO[] = { + 0x46, 0x00, 0x7C, 0x87, 0xFC, 0xB1, 0x1D, 0xC0, 0xE3, 0x07, 0xD5, 0x9C, 0x4D, 0x91, 0x62, 0x6B, 0x73, 0x86, 0x05, 0xE8, 0xDB, 0xC6, 0x23, 0x6D, + 0x5E, 0x6F, 0x73, 0xF8, 0xF2, 0x47, 0xD9, 0x5E, 0xEA, 0xB3, 0xD2, 0x74, 0x6C, 0xE0, 0xCE, 0xE2, 0x3C, 0xFA, 0x59, 0x4A, 0xBA, 0x8F, 0x76, 0x0F, + 0x15, 0xE1, 0x3E, 0x60, 0xDF, 0x80, 0xC5, 0x44, 0xB1, 0x1B, 0xEF, 0x43, 0x03, 0x4F, 0xF2, 0xDE, 0xE3, 0xAD, 0x8B, 0x20, 0xFF, 0x68, 0x0C, 0x0A, + 0x3F, 0x00, 0x7C, 0x87, 0xFD, 0x45, 0xBD, 0x12, 0x00, 0xA5, 0xB1, 0xA0, 0x0A, 0x62, 0x24, 0x6F, 0x63, 0x70, 0xA5, 0x35, 0x13, 0x03, 0x74, 0x0D, + 0x83, 0xC9, 0x74, 0x1B, 0x79, 0xED, 0xA3, 0x09, 0x83, 0xA5, 0x02, 0xC4, 0x60, 0x49, 0xC9, 0xB6, 0x9A, 0x60, 0xAD, 0x6C, 0x77, 0x84, 0xE5, 0xC2, + 0xCE, 0x2E, 0x71, 0x07, 0x7E, 0x40, 0xCB, 0xDC, 0x13, 0xDF, 0xCF, 0x84, 0x5D, 0x55, 0x3B, 0x6B, 0x6F, 0x44, 0x00, 0x7C, 0x88, 0x01, 0xE8, 0xA1, + 0x2A, 0x12, 0x7D, 0x5E, 0xF6, 0x74, 0x78, 0xB1, 0x27, 0x2B, 0x8C, 0xF0, 0x7F, 0x95, 0x51, 0x71, 0x28, 0x18, 0xEA, 0x66, 0xEB, 0xCA, 0xAC, 0xDF, + 0x6C, 0x9C, 0xFD, 0xED, 0x88, 0x8E, 0x66, 0xD7, 0xDA, 0x36, 0xD7, 0xEB, 0x5F, 0xA6, 0x05, 0x72, 0xDA, 0x52, 0x14, 0x7F, 0xF5, 0x84, 0x54, 0xA1, + 0xA9, 0xF1, 0xAA, 0xA8, 0x73, 0x9A, 0xCC, 0x91, 0x83, 0x92, 0xD0, 0x7F, 0x3B, 0x6B, 0x65, 0x43, 0x00, 0x7C, 0x88, 0x01, 0xE8, 0xA1, 0x2A, 0x12, + 0x7D, 0x5F, 0x04, 0xF8, 0xF5, 0x1F, 0xA0, 0x85, 0x89, 0xED, 0xBA, 0x7C, 0x5F, 0x63, 0x2A, 0x9E, 0x05, 0xE8, 0x63, 0xD1, 0x73, 0x0C, 0xAF, 0xC8, + 0xC9, 0x22, 0xF7, 0x11, 0x1D, 0x8B, 0xA9, 0x9A, 0x90, 0x77, 0xF2, 0x86, 0x49, 0x15, 0x6E, 0xD6, 0x34, 0x1F, 0x50, 0x15, 0xEC, 0x85, 0xC7, 0xF5, + 0xA7, 0xFA, 0x99, 0xF9, 0x47, 0x32, 0x07, 0xAF, 0x24, 0xBF, 0x14, 0xC5, +}; + struct DecodeSideband { std::int32_t result; std::int32_t internalResult; @@ -82,6 +98,56 @@ void TestMp3(std::uint32_t context) { Require(sceAjmInstanceDestroy(context, instance) == 0); } +void RunDecode(std::uint32_t context, std::uint32_t instance, const std::uint8_t* input, std::size_t inputSize, void* output, std::size_t outputSize, DecodeSideband& sideband) { + std::vector batch(4096); + AjmBatchInfo info{}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobDecode(&info, instance, input, inputSize, output, outputSize, &sideband) == 0); + std::uint32_t id = 0; + AjmBatchError error{}; + Require(sceAjmBatchStart(context, &info, 0, &error, &id) == 0 && sceAjmBatchWait(context, id, 0, &error) == 0); +} + +void TestOpus(std::uint32_t context) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0); + const std::uint32_t parameters[3] = {2, 48000, 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); + 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); + + std::vector pcm(960 * 2); + std::size_t offset = 0; + std::int16_t 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); + DecodeSideband sideband{}; + RunDecode(context, instance, OPUS_STEREO + offset, sizeof(OPUS_STEREO) - offset, pcm.data(), pcm.size() * sizeof(std::int16_t), sideband); + Require(sideband.result == 0 && static_cast(sideband.inputConsumed) == 2 + bytes && sideband.outputWritten == 960 * 2 * 2); + Require(sideband.totalDecodedSamples == static_cast(packet + 1) * 960); + if (packet > 0) { + for (const std::int16_t sample : pcm) peak = std::max(peak, static_cast(std::abs(sample))); + } + offset += static_cast(sideband.inputConsumed); + } + Require(peak > 900 && peak < 1200); + + std::vector small(512 * 2); + DecodeSideband first{}; + RunDecode(context, instance, OPUS_STEREO + offset, sizeof(OPUS_STEREO) - offset, small.data(), small.size() * sizeof(std::int16_t), first); + Require(first.result == 0 && static_cast(first.inputConsumed) == sizeof(OPUS_STEREO) - offset && first.outputWritten == 512 * 2 * 2); + DecodeSideband rest{}; + RunDecode(context, instance, OPUS_STEREO + sizeof(OPUS_STEREO), 0, small.data(), small.size() * sizeof(std::int16_t), rest); + Require(rest.result == 0 && rest.inputConsumed == 0 && rest.outputWritten == (960 - 512) * 2 * 2); + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + } int main() { @@ -99,5 +165,6 @@ int main() { Require(sceAjmDecAt9ParseConfigData(badHeader, &info) == invalidParameter); Require(sceAjmDecAt9ParseConfigData(nullptr, &info) == invalidParameter); TestMp3(context); + TestOpus(context); Require(sceAjmFinalize(context) == 0); }