diff --git a/core/libs/prx/libSceAudioIn/Export.cpp b/core/libs/prx/libSceAudioIn/Export.cpp index 86fbd70d5..b07ad2150 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__); } int32_t APS5_VABI sceAudioInIsPadEmbeddedMic(int32_t handle) { 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 5937709cd..6bd99388a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -354,6 +354,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. - [ftrylockfile](../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - tries the host CRT stream lock that `flockfile`, `funlockfile` and each locking host stdio call on the stream take, so it returns -1 while another thread holds the stream through `flockfile` or is inside such a call, and 0 when it acquires the lock or already owns it (recursively, released by as many `funlockfile` calls), as FreeBSD 11 [`_ftrylockfile`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/_flock_stub.c#L89-L104) does. A libc export holds the lock only for each host call it makes, not across several host calls or while it updates the guest `FILE` prefix. Linux uses glibc `ftrylockfile`. UCRT has no try variant of `_lock_file`, so Windows calls `TryEnterCriticalSection` on the `CRITICAL_SECTION` that `_lock_file` enters, found through the private UCRT stream layout (`__crt_stdio_stream_data` in `ucrt/inc/corecrt_internal_stdio.h`, unchanged from Windows SDK 10.0.19041 to 10.0.26100); another CRT or a change of that layout breaks it. A null or closed stream throws where FreeBSD dereferences it