feat(opus): implement 48 kHz libSceOpusDec

Decode raw mono and stereo packets for missing system-library imports. Add behavioral tests and document unverified firmware semantics.
This commit is contained in:
Mouhand-Kaddo 2026-10-09 00:34:38 +04:00
parent d70b899894
commit 4c7e823294
4 changed files with 269 additions and 0 deletions

View File

@ -0,0 +1,16 @@
add_library(libSceOpusDec SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp)
target_include_directories(libSceOpusDec PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(libSceOpusDec PRIVATE ffmpeg)
configure_windows_unwind(libSceOpusDec)
if(BUILD_TESTING)
add_test_executable(guest_opus_dec_tests ${LIBS_INCLUDE_DIR}/tests/GuestOpusDec.cpp)
target_include_directories(guest_opus_dec_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_opus_dec_tests PRIVATE libSceOpusDec libc)
configure_windows_unwind(guest_opus_dec_tests)
add_test(NAME guest_opus_dec COMMAND guest_opus_dec_tests)
if(WIN32)
set_tests_properties(guest_opus_dec PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceOpusDec>")
endif()
endif()

View File

@ -0,0 +1,138 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <limits>
#include <memory>
#include <mutex>
#include <stdexcept>
#include <unordered_map>
extern "C" {
#include <libavcodec/avcodec.h>
}
namespace {
struct Decoder {
std::uint32_t context;
int channels;
AVCodecContext* codec = nullptr;
~Decoder() { avcodec_free_context(&codec); }
};
std::mutex mutex;
std::unordered_map<std::uint32_t, std::uint32_t*> contexts;
std::unordered_map<void*, std::unique_ptr<Decoder>> decoders;
std::uint32_t nextContext = 1;
int PacketSamples(const std::uint8_t* packet, int size) {
const unsigned config = packet[0] >> 3;
const int frameSamples = config < 12 ? ((config & 3) == 3 ? 2880 : 480 << (config & 3))
: config < 16 ? 480 << (config & 1) : 120 << (config & 3);
int frames = 1;
switch (packet[0] & 3) {
case 1:
case 2: frames = 2; break;
case 3:
if (size < 2) throw std::runtime_error("OpusDec: truncated packet frame count");
frames = packet[1] & 63;
break;
}
if (frames == 0 || frames * frameSamples > 5760)
throw std::runtime_error("OpusDec: invalid packet duration");
return frames * frameSamples;
}
} // namespace
extern "C" {
int APS5_VABI sceOpusDecInitialize(std::uint32_t* context) {
if (!context) throw std::runtime_error("OpusDec: null context output");
std::lock_guard lock(mutex);
for (const auto& [id, owner] : contexts)
if (owner == context) throw std::runtime_error("OpusDec: context already initialized");
if (nextContext == 0) throw std::runtime_error("OpusDec: context identifiers exhausted");
const auto id = nextContext++;
contexts.emplace(id, context);
*context = id;
return 0;
}
int APS5_VABI sceOpusDecTerminate(std::uint32_t* context) {
if (!context) throw std::runtime_error("OpusDec: null context");
std::lock_guard lock(mutex);
if (!contexts.contains(*context)) throw std::runtime_error("OpusDec: unknown context");
for (const auto& [state, decoder] : decoders)
if (decoder->context == *context) throw std::runtime_error("OpusDec: termination with live decoders is not implemented");
contexts.erase(*context);
return 0;
}
int APS5_VABI sceOpusDecGetSize(int channels) {
if (channels != 1 && channels != 2) throw std::runtime_error("OpusDec: only mono and stereo are supported");
return 640;
}
int APS5_VABI sceOpusDecCreateEx(std::uint32_t* context, void* state, int sampleRate, int channels) {
if (!context || !state) throw std::runtime_error("OpusDec: null context or state");
sceOpusDecGetSize(channels);
if (sampleRate != 48000) throw std::runtime_error("OpusDec: sample rates other than 48000 Hz are not implemented");
std::lock_guard lock(mutex);
if (!contexts.contains(*context)) throw std::runtime_error("OpusDec: unknown context");
if (decoders.contains(state)) throw std::runtime_error("OpusDec: state already owns a decoder");
auto decoder = std::make_unique<Decoder>();
decoder->context = *context;
decoder->channels = channels;
const auto* codec = avcodec_find_decoder(AV_CODEC_ID_OPUS);
if (!codec) throw std::runtime_error("OpusDec: FFmpeg Opus decoder unavailable");
decoder->codec = avcodec_alloc_context3(codec);
if (!decoder->codec) throw std::bad_alloc();
av_channel_layout_default(&decoder->codec->ch_layout, channels);
decoder->codec->sample_rate = 48000;
if (avcodec_open2(decoder->codec, codec, nullptr) < 0)
throw std::runtime_error("OpusDec: cannot open FFmpeg decoder");
decoders.emplace(state, std::move(decoder));
return 0;
}
int APS5_VABI sceOpusDecDecode(void* state, const std::uint8_t* data, int bytes, std::int16_t* pcm, int capacity) {
if (!data || bytes <= 0 || bytes > std::numeric_limits<int>::max() - AV_INPUT_BUFFER_PADDING_SIZE || !pcm || capacity < 0)
throw std::runtime_error("OpusDec: invalid packet or PCM buffer; packet loss concealment is not implemented");
std::lock_guard lock(mutex);
const auto found = decoders.find(state);
if (found == decoders.end()) throw std::runtime_error("OpusDec: unknown decoder state");
auto& decoder = *found->second;
const int samples = PacketSamples(data, bytes);
const int outputBytes = samples * decoder.channels * 2;
if (capacity / 2 * 2 < outputBytes) throw std::runtime_error("OpusDec: PCM buffer is too small");
std::unique_ptr<AVPacket, void (*)(AVPacket*)> packet(av_packet_alloc(), [](AVPacket* p) { av_packet_free(&p); });
std::unique_ptr<AVFrame, void (*)(AVFrame*)> frame(av_frame_alloc(), [](AVFrame* f) { av_frame_free(&f); });
if (!packet || !frame || av_new_packet(packet.get(), bytes) < 0) throw std::bad_alloc();
std::memcpy(packet->data, data, bytes);
if (avcodec_send_packet(decoder.codec, packet.get()) < 0 || avcodec_receive_frame(decoder.codec, frame.get()) < 0)
throw std::runtime_error("OpusDec: FFmpeg rejected the packet");
const auto format = static_cast<AVSampleFormat>(frame->format);
if (frame->nb_samples != samples || frame->ch_layout.nb_channels != decoder.channels || frame->sample_rate != 48000 ||
(format != AV_SAMPLE_FMT_FLTP && format != AV_SAMPLE_FMT_FLT))
throw std::runtime_error("OpusDec: unexpected FFmpeg PCM format");
for (int sample = 0; sample < samples; ++sample) {
for (int channel = 0; channel < decoder.channels; ++channel) {
const auto* source = reinterpret_cast<const float*>(frame->extended_data[format == AV_SAMPLE_FMT_FLTP ? channel : 0]);
const float value = source[format == AV_SAMPLE_FMT_FLTP ? sample : sample * decoder.channels + channel];
const auto converted = static_cast<std::int16_t>(std::lrint(std::clamp(value * 32768.0f, -32768.0f, 32767.0f)));
std::memcpy(reinterpret_cast<std::uint8_t*>(pcm) + (sample * decoder.channels + channel) * 2, &converted, 2);
}
}
return outputBytes;
}
int APS5_VABI sceOpusDecDestroy(void* state) {
std::lock_guard lock(mutex);
if (decoders.erase(state) == 0) throw std::runtime_error("OpusDec: unknown decoder state");
return 0;
}
}

View File

@ -0,0 +1,114 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <stdexcept>
extern "C" {
int APS5_VABI sceOpusDecInitialize(std::uint32_t*);
int APS5_VABI sceOpusDecTerminate(std::uint32_t*);
int APS5_VABI sceOpusDecGetSize(int);
int APS5_VABI sceOpusDecCreateEx(std::uint32_t*, void*, int, int);
int APS5_VABI sceOpusDecDecode(void*, const std::uint8_t*, int, std::int16_t*, int);
int APS5_VABI sceOpusDecDestroy(void*);
}
namespace {
void Require(bool value) { if (!value) std::abort(); }
template <typename TFunction>
void RequireThrows(TFunction function) {
try { function(); } catch (const std::runtime_error&) { return; }
std::abort();
}
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,
};
} // namespace
int main() {
std::uint32_t context = 0, otherContext = 0;
Require(sceOpusDecInitialize(&context) == 0);
Require(sceOpusDecInitialize(&otherContext) == 0 && context != otherContext);
RequireThrows([&] { sceOpusDecInitialize(&context); });
Require(sceOpusDecGetSize(1) == 640 && sceOpusDecGetSize(2) == 640);
std::array<std::uint8_t, 640> state{}, otherState{}, monoState{};
Require(sceOpusDecCreateEx(&context, state.data(), 48000, 2) == 0);
Require(sceOpusDecCreateEx(&otherContext, otherState.data(), 48000, 2) == 0);
Require(sceOpusDecCreateEx(&context, monoState.data(), 48000, 1) == 0);
RequireThrows([&] { sceOpusDecTerminate(&context); });
RequireThrows([&] { sceOpusDecCreateEx(&context, state.data(), 48000, 2); });
RequireThrows([&] { sceOpusDecGetSize(3); });
std::array<std::uint8_t, 640> unsupported{};
for (int rate : {8000, 12000, 16000, 24000})
RequireThrows([&] { sceOpusDecCreateEx(&context, unsupported.data(), rate, 2); });
std::array<std::int16_t, 1922> pcm{}, reference{};
pcm.fill(12345);
RequireThrows([&] { sceOpusDecDecode(state.data(), OPUS_STEREO + 2, 70, pcm.data() + 1, 3839); });
Require(std::all_of(pcm.begin(), pcm.end(), [](auto value) { return value == 12345; }));
const std::uint8_t malformed[] = {0x7f};
RequireThrows([&] { sceOpusDecDecode(state.data(), malformed, 1, pcm.data() + 1, 3840); });
RequireThrows([&] { sceOpusDecDecode(state.data(), nullptr, 0, pcm.data() + 1, 3840); });
Require(std::all_of(pcm.begin(), pcm.end(), [](auto value) { return value == 12345; }));
std::size_t offset = 0;
for (int frame = 0; frame < 3; ++frame) {
const int bytes = OPUS_STEREO[offset] | (OPUS_STEREO[offset + 1] << 8);
const auto* packet = OPUS_STEREO + offset + 2;
Require(sceOpusDecDecode(state.data(), packet, bytes, pcm.data() + 1, 3841) == 3840);
Require(pcm.front() == 12345 && pcm.back() == 12345);
Require(sceOpusDecDecode(otherState.data(), packet, bytes, reference.data() + 1, 3840) == 3840);
Require(std::equal(pcm.begin() + 1, pcm.end() - 1, reference.begin() + 1));
if (frame == 1) {
Require(sceOpusDecCreateEx(&context, unsupported.data(), 48000, 2) == 0);
std::array<std::int16_t, 1920> fresh{};
Require(sceOpusDecDecode(unsupported.data(), packet, bytes, fresh.data(), 3840) == 3840);
Require(!std::equal(fresh.begin(), fresh.end(), pcm.begin() + 1));
Require(sceOpusDecDestroy(unsupported.data()) == 0);
}
if (frame > 0) {
int peak = 0;
for (auto it = pcm.begin() + 1; it != pcm.end() - 1; ++it) peak = std::max(peak, std::abs(static_cast<int>(*it)));
Require(peak > 900 && peak < 1200);
}
std::array<std::int16_t, 962> mono{};
mono.front() = mono.back() = 12345;
Require(sceOpusDecDecode(monoState.data(), packet, bytes, mono.data() + 1, 1921) == 1920);
Require(mono.front() == 12345 && mono.back() == 12345);
Require(std::any_of(mono.begin() + 1, mono.end() - 1, [](auto value) { return value != 0; }));
offset += bytes + 2;
}
Require(sceOpusDecDestroy(state.data()) == 0);
RequireThrows([&] { sceOpusDecDestroy(state.data()); });
RequireThrows([&] { sceOpusDecDecode(state.data(), OPUS_STEREO + 2, 70, pcm.data(), 3840); });
Require(sceOpusDecCreateEx(&context, state.data(), 48000, 2) == 0);
Require(sceOpusDecDecode(state.data(), OPUS_STEREO + 2, 70, pcm.data(), 3840) == 3840);
std::array<std::int16_t, 3842> rejected;
rejected.fill(12345);
const std::uint8_t invalidFraming[] = {0x7d, 1};
RequireThrows([&] { sceOpusDecDecode(state.data(), invalidFraming, 2, rejected.data() + 1, 7680); });
Require(std::all_of(rejected.begin(), rejected.end(), [](auto value) { return value == 12345; }));
Require(sceOpusDecDestroy(state.data()) == 0);
Require(sceOpusDecDestroy(monoState.data()) == 0);
Require(sceOpusDecTerminate(&context) == 0);
RequireThrows([&] { sceOpusDecTerminate(&context); });
RequireThrows([&] { sceOpusDecCreateEx(&context, state.data(), 48000, 2); });
Require(sceOpusDecDecode(otherState.data(), OPUS_STEREO + offset + 2, OPUS_STEREO[offset], reference.data(), 3840) == 3840);
Require(sceOpusDecDestroy(otherState.data()) == 0);
Require(sceOpusDecTerminate(&otherContext) == 0);
}

View File

@ -242,6 +242,7 @@ Throughout the project, every function at every stage either **does exactly what
### Functional
- [libSceOpusDec](../../core/libs/prx/libSceOpusDec/Export.cpp) uses FFmpeg for 48000 Hz mono/stereo signed-16 decoding. The six signatures, 640-byte opaque state allocation and byte-count return follow [public PS5 decoder research](https://github.com/blackbearreloaded/ps5-hardware-audio-decoding/blob/26be5a12f92b2cdc67f8a9e082fd69ae0e9a35a2/docs/DECODING.md#direct-opus-abi) and [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). Console PCM equivalence and error codes are unverified. Other sample rates, packet loss concealment, invalid inputs and termination with live decoders throw. Context identifiers and host decoder state are private registries; the firmware's opaque state layout and error recovery are unknown.
- [libSceHttp2 asynchronous requests](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. A kqueue option or user event that is missing, an extra option, and a wait with no completed operation throw
- [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown
- [Wide-character conversions](../../core/libs/prx/libc/src/Strings.cpp) (libc) - `wcsrtombs` and [`swprintf`](../../core/libs/prx/libc/src/Formatting.cpp) still pass guest strings to the host's `wchar_t` functions. The guest's `wchar_t` is 16 bits, so on Linux, where the host's is 32 bits, they misread and miswrite guest strings. The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits