From 9ac583bdddd0f97beaf7b212e91a4ed7f5e1208e Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 09:46:29 +0200 Subject: [PATCH] feat(libSceAudioIn): implement sceAudioInHqOpen and sceAudioInAsyncOpen Both open the same SDL capture port as sceAudioInOpen, so Input, GetSilentState and Close work on their handles. HqOpen accepts only a 128-sample grain at 48 kHz (SharpProspero, KytyPS5); AsyncOpen returns INVALID_SIZE above 384 samples (SharpProspero). Values the sources disagree on throw. Format 0x10 still throws: only KytyPS5 accepts it. Imported by FMOD-based titles, The Smurfs Dreams (PPSA21607) and Astro Bot (PPSA21564); Ghost of Yotei (PPSA26344) calls sceAudioInOpen. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 16bfdf928d0a2da0b4473db2633a2e270538cbf5) --- core/libs/prx/libSceAudioIn/Export.cpp | 79 ++++++++++++++------------ core/libs/tests/GuestAudioIn.cpp | 59 ++++++++++++++++++- docs/dev/TechnicalDebt.md | 1 + 3 files changed, 103 insertions(+), 36 deletions(-) diff --git a/core/libs/prx/libSceAudioIn/Export.cpp b/core/libs/prx/libSceAudioIn/Export.cpp index 50e917ee5..b3ae47b9f 100644 --- a/core/libs/prx/libSceAudioIn/Export.cpp +++ b/core/libs/prx/libSceAudioIn/Export.cpp @@ -21,6 +21,7 @@ constexpr int AUDIO_IN_ERROR_PORT_FULL = static_cast(0x80260107); constexpr int AUDIO_IN_ERROR_BUSY = static_cast(0x8026010A); constexpr int AUDIO_IN_SILENT_STATE_DEVICE_NONE = 1; constexpr std::uint32_t MAX_QUEUED_BLOCKS = 4; +constexpr std::uint32_t MAX_ASYNC_LEN = 384; using Clock = std::chrono::steady_clock; @@ -75,6 +76,32 @@ Port* find(int handle) { return port.used ? &port : nullptr; } +int checkArguments(std::uint32_t type, std::uint32_t index, std::uint32_t len, std::uint32_t freq) { + if (type != 0 && type != 1) return AUDIO_IN_ERROR_INVALID_TYPE; + if (index != 0) return AUDIO_IN_ERROR_INVALID_PARAM; + if (len != 128 && len != 256) return AUDIO_IN_ERROR_INVALID_SIZE; + if (freq != 48000 && freq != 16000) return AUDIO_IN_ERROR_INVALID_FREQ; + return 0; +} + +int openPort(std::uint32_t len, std::uint32_t freq, std::uint32_t param, const char* function) { + if (param == 0x10) { + NotImplemented_nid_no_patch(function); + return 0; + } + Format format{}; + if (!formatOf(param, format)) return AUDIO_IN_ERROR_INVALID_PARAM; + std::lock_guard lock(g_mutex); + for (std::size_t i = 0; i < g_ports.size(); ++i) { + if (g_ports[i].used) continue; + const auto frameBytes = static_cast(SDL_AUDIO_BITSIZE(format.format) / 8 * format.channels); + const auto grain = std::chrono::duration_cast(std::chrono::microseconds(1000000ull * len / freq)); + g_ports[i] = {true, false, len, frameBytes, openDevice(freq, len, format), grain, {}}; + return static_cast(i + 1); + } + return AUDIO_IN_ERROR_PORT_FULL; +} + std::size_t capture(SDL_AudioDeviceID device, std::uint8_t* dest, std::size_t bytes, Clock::time_point deadline) { while (SDL_GetQueuedAudioSize(device) < bytes && Clock::now() < deadline) PreciseSleepUs(1000); const std::size_t queued = SDL_GetQueuedAudioSize(device); @@ -136,25 +163,8 @@ int APS5_VABI sceAudioInInput(int handle, void* dest) { int APS5_VABI sceAudioInOpen(int user_id, uint32_t type, uint32_t index, uint32_t len, uint32_t freq, uint32_t param) { (void)user_id; - if (type != 0 && type != 1) return AUDIO_IN_ERROR_INVALID_TYPE; - if (index != 0) return AUDIO_IN_ERROR_INVALID_PARAM; - if (len != 128 && len != 256) return AUDIO_IN_ERROR_INVALID_SIZE; - if (freq != 48000 && freq != 16000) return AUDIO_IN_ERROR_INVALID_FREQ; - if (param == 0x10) { - NotImplemented_nid_no_patch(__func__); - return 0; - } - Format format{}; - if (!formatOf(param, format)) return AUDIO_IN_ERROR_INVALID_PARAM; - std::lock_guard lock(g_mutex); - for (std::size_t i = 0; i < g_ports.size(); ++i) { - if (g_ports[i].used) continue; - const auto frameBytes = static_cast(SDL_AUDIO_BITSIZE(format.format) / 8 * format.channels); - const auto grain = std::chrono::duration_cast(std::chrono::microseconds(1000000ull * len / freq)); - g_ports[i] = {true, false, len, frameBytes, openDevice(freq, len, format), grain, {}}; - return static_cast(i + 1); - } - return AUDIO_IN_ERROR_PORT_FULL; + if (const int error = checkArguments(type, index, len, freq); error != 0) return error; + return openPort(len, freq, param, __func__); } int32_t APS5_VABI sceAudioInClose(int32_t handle) { @@ -168,25 +178,24 @@ int32_t APS5_VABI sceAudioInClose(int32_t handle) { } int32_t APS5_VABI sceAudioInHqOpen(int32_t user_id, uint32_t type, uint32_t index, uint32_t len, uint32_t freq, uint32_t param) { - (void)user_id; - (void)type; - (void)index; - (void)len; - (void)freq; - (void)param; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)user_id; + if (const int error = checkArguments(type, index, len, freq); error != 0) return error; + if (len != 128 || freq != 48000) { + NotImplemented_nid_no_patch(__func__); + return 0; + } + return openPort(len, freq, param, __func__); } int32_t APS5_VABI sceAudioInAsyncOpen(int32_t user_id, uint32_t type, uint32_t index, uint32_t len, uint32_t freq, uint32_t param) { - (void)user_id; - (void)type; - (void)index; - (void)len; - (void)freq; - (void)param; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)user_id; + const int error = checkArguments(type, index, len, freq); + if (error == AUDIO_IN_ERROR_INVALID_SIZE && len <= MAX_ASYNC_LEN) { + NotImplemented_nid_no_patch(__func__); + return 0; + } + if (error != 0) return error; + return openPort(len, freq, param, __func__); } APS5_EXPORT("X+4jdIS75P0", sceAudioInUnknown_X4jdIS75P0); diff --git a/core/libs/tests/GuestAudioIn.cpp b/core/libs/tests/GuestAudioIn.cpp index f3b3d3ff4..d9e957911 100644 --- a/core/libs/tests/GuestAudioIn.cpp +++ b/core/libs/tests/GuestAudioIn.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -6,12 +7,15 @@ #include #include #include +#include #include #include extern "C" { int APS5_VABI sceAudioInInit(); int APS5_VABI sceAudioInOpen(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioInHqOpen(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioInAsyncOpen(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); int APS5_VABI sceAudioInInput(int, void*); int APS5_VABI sceAudioInGetSilentState(int); int APS5_VABI sceAudioInClose(int); @@ -19,6 +23,16 @@ int APS5_VABI sceAudioInClose(int); static void Require(bool value) { if (!value) std::abort(); } +template +static bool ReportsNotImplemented(const char* name, TCall call) { + try { + call(); + } catch (const std::runtime_error& error) { + return std::string(error.what()) == std::string(name) + " not implemented"; + } + return false; +} + static void SetEnvironment(const char* name, const std::string& value) { #ifdef _WIN32 _putenv_s(name, value.c_str()); @@ -42,6 +56,22 @@ void TestValidation() { Require(sceAudioInOpen(user, 0, 0, 256, 44100, 2) == invalidFreq); Require(sceAudioInOpen(user, 0, 0, 256, 48000, 3) == invalidParam); Require(sceAudioInOpen(user, 0, 0, 256, 48000, 0) == invalidParam); + Require(sceAudioInHqOpen(user, 2, 0, 128, 48000, 2) == invalidType); + Require(sceAudioInHqOpen(user, 0, 1, 128, 48000, 2) == invalidParam); + Require(sceAudioInHqOpen(user, 0, 0, 512, 48000, 2) == invalidSize); + Require(sceAudioInHqOpen(user, 0, 0, 128, 44100, 2) == invalidFreq); + Require(sceAudioInHqOpen(user, 0, 0, 128, 48000, 3) == invalidParam); + Require(sceAudioInAsyncOpen(user, 2, 0, 128, 48000, 2) == invalidType); + Require(sceAudioInAsyncOpen(user, 0, 1, 128, 48000, 2) == invalidParam); + Require(sceAudioInAsyncOpen(user, 0, 0, 385, 48000, 2) == invalidSize); + Require(sceAudioInAsyncOpen(user, 0, 0, 256, 44100, 2) == invalidFreq); + Require(sceAudioInAsyncOpen(user, 0, 0, 256, 48000, 0) == invalidParam); + Require(ReportsNotImplemented("sceAudioInHqOpen", [] { sceAudioInHqOpen(user, 0, 0, 256, 48000, 2); })); + Require(ReportsNotImplemented("sceAudioInHqOpen", [] { sceAudioInHqOpen(user, 0, 0, 128, 16000, 2); })); + Require(ReportsNotImplemented("sceAudioInAsyncOpen", [] { sceAudioInAsyncOpen(user, 0, 0, 384, 48000, 2); })); + Require(ReportsNotImplemented("sceAudioInOpen", [] { sceAudioInOpen(user, 0, 0, 256, 48000, 0x10); })); + Require(ReportsNotImplemented("sceAudioInHqOpen", [] { sceAudioInHqOpen(user, 0, 0, 128, 48000, 0x10); })); + Require(ReportsNotImplemented("sceAudioInAsyncOpen", [] { sceAudioInAsyncOpen(user, 0, 0, 128, 48000, 0x10); })); } void TestCapture() { @@ -66,6 +96,11 @@ void TestCapture() { Require(sceAudioInInput(handle, block.data()) == 256); for (std::size_t i = 0; i < 256 * 2; ++i) Require(block[i] == 0); Require(sceAudioInClose(handle) == 0); + const int async = sceAudioInAsyncOpen(user, 0, 0, 256, 48000, 2); + Require(async == handle && sceAudioInGetSilentState(async) == 0); + Require(sceAudioInInput(async, block.data()) == 256); + Require(std::equal(block.begin(), block.end() - 1, recorded.begin()) && block.back() == 0x5555); + Require(sceAudioInClose(async) == 0); std::filesystem::remove(path); } @@ -87,8 +122,30 @@ void TestNoDevice() { for (std::size_t i = 0; i < 256 * 8; ++i) Require(buffer[i] == 0); Require(buffer[256 * 8] == 0xCC); Require(sceAudioInInput(handle, nullptr) == 0); - for (int port = 1; port < 8; ++port) Require(sceAudioInOpen(user, 1, 0, 128, 48000, 1) > 0); + const int hq = sceAudioInHqOpen(user, 0, 0, 128, 48000, 2); + const int async = sceAudioInAsyncOpen(user, 1, 0, 256, 16000, 0x11); + Require(hq > 0 && async > 0 && hq != handle && async != handle && hq != async); + Require(sceAudioInGetSilentState(hq) == 1 && sceAudioInGetSilentState(async) == 1); + std::fill(buffer.begin(), buffer.end(), 0xCC); + Require(sceAudioInInput(hq, buffer.data()) == 128); + for (std::size_t i = 0; i < 128 * 4; ++i) Require(buffer[i] == 0); + Require(buffer[128 * 4] == 0xCC); + std::fill(buffer.begin(), buffer.end(), 0xCC); + Require(sceAudioInInput(async, buffer.data()) == 256); + for (std::size_t i = 0; i < 256 * 4; ++i) Require(buffer[i] == 0); + Require(buffer[256 * 4] == 0xCC); + Require(sceAudioInInput(async, nullptr) == 0); + for (int port = 3; port < 8; ++port) Require(sceAudioInOpen(user, 1, 0, 128, 48000, 1) > 0); Require(sceAudioInOpen(user, 1, 0, 128, 48000, 1) == portFull); + Require(sceAudioInHqOpen(user, 1, 0, 128, 48000, 1) == portFull); + Require(sceAudioInAsyncOpen(user, 1, 0, 128, 48000, 1) == portFull); + Require(sceAudioInClose(hq) == 0); + Require(sceAudioInClose(hq) == invalidHandle); + Require(sceAudioInGetSilentState(hq) == invalidHandle); + Require(sceAudioInAsyncOpen(user, 0, 0, 128, 48000, 2) == hq); + Require(sceAudioInClose(async) == 0); + Require(sceAudioInInput(async, buffer.data()) == invalidHandle); + Require(sceAudioInHqOpen(user, 1, 0, 128, 48000, 0x12) == async); Require(sceAudioInClose(handle) == 0); Require(sceAudioInClose(handle) == invalidHandle); Require(sceAudioInInput(handle, buffer.data()) == invalidHandle); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..ad4ce77b0 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -345,6 +345,7 @@ Throughout the project, every function at every stage either **does exactly what ### Functional +- [sceAudioInHqOpen, sceAudioInAsyncOpen](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) open the same port as `sceAudioInOpen`, so `sceAudioInInput`, `sceAudioInGetSilentState` and `sceAudioInClose` accept their handles. The signatures agree across [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/fc5d2cc219c43c8c8f354153e7b3d0c04a5d506e/src/core/libraries/audio/audioin.cpp), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/c86c8b4d9333a766cc8c4495f89f893bf35e6199/src/libs/audio.cpp). HqOpen is limited to 128 samples at 48 kHz; AsyncOpen rejects sizes above 384 and throws for currently unverified inputs. Host capture pacing and console behavior remain unverified. - [getc_unlocked, putc_unlocked](../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. - [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.