diff --git a/core/libs/prx/libSceAudioIn/CMakeLists.txt b/core/libs/prx/libSceAudioIn/CMakeLists.txt new file mode 100644 index 000000000..5d980124a --- /dev/null +++ b/core/libs/prx/libSceAudioIn/CMakeLists.txt @@ -0,0 +1,24 @@ +add_library(libSceAudioIn SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceAudioIn PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include) +target_link_libraries(libSceAudioIn PRIVATE SDL2-static) +target_link_options(libSceAudioIn PRIVATE LINKER:--exclude-libs,ALL) +configure_windows_unwind(libSceAudioIn) + +if(MINGW) + foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) + string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + endforeach() +endif() + +if(BUILD_TESTING) + add_test_executable(guest_audio_in_tests ${LIBS_INCLUDE_DIR}/tests/GuestAudioIn.cpp) + target_include_directories(guest_audio_in_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_audio_in_tests PRIVATE libSceAudioIn libc) + configure_windows_unwind(guest_audio_in_tests) + add_test(NAME guest_audio_in_capture COMMAND guest_audio_in_tests capture) + add_test(NAME guest_audio_in_no_device COMMAND guest_audio_in_tests none) + if(WIN32) + set_tests_properties(guest_audio_in_capture guest_audio_in_no_device PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceAudioIn/Export.cpp b/core/libs/prx/libSceAudioIn/Export.cpp index b706c38e1..3c31aaa40 100644 --- a/core/libs/prx/libSceAudioIn/Export.cpp +++ b/core/libs/prx/libSceAudioIn/Export.cpp @@ -2,37 +2,165 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "SDL.h" +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr int AUDIO_IN_ERROR_INVALID_HANDLE = static_cast(0x80260101); +constexpr int AUDIO_IN_ERROR_INVALID_SIZE = static_cast(0x80260102); +constexpr int AUDIO_IN_ERROR_INVALID_FREQ = static_cast(0x80260103); +constexpr int AUDIO_IN_ERROR_INVALID_TYPE = static_cast(0x80260104); +constexpr int AUDIO_IN_ERROR_INVALID_PARAM = static_cast(0x80260106); +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; + +using Clock = std::chrono::steady_clock; + +struct Format { + SDL_AudioFormat format; + std::uint8_t channels; +}; + +struct Port { + bool used = false; + bool busy = false; + std::uint32_t samples = 0; + std::uint32_t frameBytes = 0; + SDL_AudioDeviceID device = 0; + Clock::duration grain{}; + Clock::time_point next{}; +}; + +std::mutex g_mutex; +std::array g_ports{}; +bool g_sdlAudio = false; + +bool formatOf(std::uint32_t param, Format& format) { + switch (param) { + case 1: format = {AUDIO_S16SYS, 1}; return true; + case 2: format = {AUDIO_S16SYS, 2}; return true; + case 0x11: format = {AUDIO_F32SYS, 1}; return true; + case 0x12: format = {AUDIO_F32SYS, 2}; return true; + default: return false; + } +} + +SDL_AudioDeviceID openDevice(std::uint32_t freq, std::uint32_t samples, const Format& format) { + if (!g_sdlAudio) { + if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) return 0; + g_sdlAudio = true; + } + SDL_AudioSpec desired{}; + desired.freq = static_cast(freq); + desired.format = format.format; + desired.channels = format.channels; + desired.samples = static_cast(samples); + SDL_AudioSpec obtained{}; + const SDL_AudioDeviceID device = SDL_OpenAudioDevice(nullptr, 1, &desired, &obtained, 0); + if (device != 0) SDL_PauseAudioDevice(device, 0); + return device; +} + +Port* find(int handle) { + if (handle <= 0 || static_cast(handle) > g_ports.size()) return nullptr; + Port& port = g_ports[static_cast(handle - 1)]; + return port.used ? &port : nullptr; +} + +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) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + const std::size_t queued = SDL_GetQueuedAudioSize(device); + std::array discard; + for (std::size_t excess = queued > bytes * MAX_QUEUED_BLOCKS ? queued - bytes * MAX_QUEUED_BLOCKS : 0; excess > 0;) { + const auto chunk = static_cast(std::min(excess, discard.size())); + excess -= SDL_DequeueAudio(device, discard.data(), chunk); + } + return SDL_DequeueAudio(device, dest, static_cast(bytes)); +} + +} extern "C" { int APS5_VABI sceAudioInGetSilentState(int handle) { - (void)handle; - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_mutex); + const Port* port = find(handle); + if (!port) return AUDIO_IN_ERROR_INVALID_HANDLE; + return port->device != 0 ? 0 : AUDIO_IN_SILENT_STATE_DEVICE_NONE; } int APS5_VABI sceAudioInInput(int handle, void* dest) { - (void)handle; - (void)dest; - NotImplemented_nid_no_patch(__func__); - return 0; + std::unique_lock lock(g_mutex); + Port* port = find(handle); + if (!port) return AUDIO_IN_ERROR_INVALID_HANDLE; + if (port->busy) return AUDIO_IN_ERROR_BUSY; + if (!dest) { + if (port->device != 0) SDL_ClearQueuedAudio(port->device); + port->next = {}; + return 0; + } + const std::size_t bytes = static_cast(port->samples) * port->frameBytes; + const SDL_AudioDeviceID device = port->device; + const auto now = Clock::now(); + const auto wake = port->next > now ? port->next : now; + port->next = wake + port->grain; + const auto deadline = now + port->grain * MAX_QUEUED_BLOCKS; + const int samples = static_cast(port->samples); + port->busy = true; + lock.unlock(); + + auto* out = static_cast(dest); + std::size_t captured = 0; + if (device != 0) { + captured = capture(device, out, bytes, deadline); + } else { + std::this_thread::sleep_until(wake); + } + std::memset(out + captured, 0, bytes - captured); + lock.lock(); + port->busy = false; + return samples; } 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; - (void)type; - (void)index; - (void)len; - (void)freq; - (void)param; - NotImplemented_nid_no_patch(__func__); - return 0; + (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; } int32_t APS5_VABI sceAudioInClose(int32_t handle) { - (void)handle; - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_mutex); + Port* port = find(handle); + if (!port) return AUDIO_IN_ERROR_INVALID_HANDLE; + if (port->busy) return AUDIO_IN_ERROR_BUSY; + if (port->device != 0) SDL_CloseAudioDevice(port->device); + *port = {}; + return 0; } int32_t APS5_VABI sceAudioInHqOpen(int32_t user_id, uint32_t type, uint32_t index, uint32_t len, uint32_t freq, uint32_t param) { diff --git a/core/libs/tests/GuestAudioIn.cpp b/core/libs/tests/GuestAudioIn.cpp new file mode 100644 index 000000000..27c717f5c --- /dev/null +++ b/core/libs/tests/GuestAudioIn.cpp @@ -0,0 +1,102 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAudioInOpen(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); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static void SetEnvironment(const char* name, const std::string& value) { +#ifdef _WIN32 + _putenv_s(name, value.c_str()); +#else + setenv(name, value.c_str(), 1); +#endif +} + +namespace { + +constexpr int user = 0x10000000; + +void TestValidation() { + constexpr int invalidSize = static_cast(0x80260102); + constexpr int invalidFreq = static_cast(0x80260103); + constexpr int invalidType = static_cast(0x80260104); + constexpr int invalidParam = static_cast(0x80260106); + Require(sceAudioInOpen(user, 2, 0, 256, 48000, 2) == invalidType); + Require(sceAudioInOpen(user, 0, 1, 256, 48000, 2) == invalidParam); + Require(sceAudioInOpen(user, 0, 0, 512, 48000, 2) == invalidSize); + 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); +} + +void TestCapture() { + const auto path = std::filesystem::temp_directory_path() / "anyps5_audio_in_capture.raw"; + std::vector recorded(256 * 2 * 4); + for (std::size_t i = 0; i < recorded.size(); ++i) recorded[i] = static_cast(i * 7 - 3000); + std::ofstream(path, std::ios::binary).write(reinterpret_cast(recorded.data()), static_cast(recorded.size() * 2)); + SetEnvironment("SDL_AUDIODRIVER", "disk"); + SetEnvironment("SDL_DISKAUDIOFILEIN", path.string()); + + TestValidation(); + const int handle = sceAudioInOpen(user, 0, 0, 256, 48000, 2); + Require(handle > 0 && sceAudioInGetSilentState(handle) == 0); + std::vector block(256 * 2 + 1, 0x5555); + std::vector captured; + for (int i = 0; i < 4; ++i) { + Require(sceAudioInInput(handle, block.data()) == 256); + captured.insert(captured.end(), block.begin(), block.end() - 1); + Require(block.back() == 0x5555); + } + Require(captured == recorded); + Require(sceAudioInInput(handle, block.data()) == 256); + for (std::size_t i = 0; i < 256 * 2; ++i) Require(block[i] == 0); + Require(sceAudioInClose(handle) == 0); + std::filesystem::remove(path); +} + +void TestNoDevice() { + constexpr int invalidHandle = static_cast(0x80260101); + constexpr int portFull = static_cast(0x80260107); + SetEnvironment("SDL_AUDIODRIVER", "none"); + + TestValidation(); + const int handle = sceAudioInOpen(user, 0, 0, 256, 16000, 0x12); + Require(handle > 0 && sceAudioInGetSilentState(handle) == 1); + Require(sceAudioInGetSilentState(handle + 1) == invalidHandle); + Require(sceAudioInInput(handle + 1, nullptr) == invalidHandle); + std::vector buffer(256 * 8 + 1, 0xCC); + const auto start = std::chrono::steady_clock::now(); + for (int block = 0; block < 5; ++block) Require(sceAudioInInput(handle, buffer.data()) == 256); + const auto elapsed = std::chrono::steady_clock::now() - start; + Require(elapsed >= std::chrono::milliseconds(4 * 16) && elapsed < std::chrono::milliseconds(1000)); + 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); + Require(sceAudioInOpen(user, 1, 0, 128, 48000, 1) == portFull); + Require(sceAudioInClose(handle) == 0); + Require(sceAudioInClose(handle) == invalidHandle); + Require(sceAudioInInput(handle, buffer.data()) == invalidHandle); + Require(sceAudioInOpen(user, 1, 0, 128, 48000, 0x11) == handle); +} + +} + +int main(int argc, char** argv) { + Require(argc == 2); + if (std::strcmp(argv[1], "capture") == 0) TestCapture(); + else TestNoDevice(); +}