Merge pull request #243 from adriadam10/feat/audio-in-capture

feat(libSceAudioIn): capture from the host's recording device
This commit is contained in:
Dean 2026-10-04 19:55:15 +13:00 committed by GitHub
commit 3357199fce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 272 additions and 18 deletions

View File

@ -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:$<TARGET_FILE_DIR:libSceAudioIn>")
endif()
endif()

View File

@ -2,37 +2,165 @@
#include <cstddef>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include "SDL.h"
#include <algorithm>
#include <array>
#include <chrono>
#include <cstring>
#include <mutex>
#include <thread>
namespace {
constexpr int AUDIO_IN_ERROR_INVALID_HANDLE = static_cast<int>(0x80260101);
constexpr int AUDIO_IN_ERROR_INVALID_SIZE = static_cast<int>(0x80260102);
constexpr int AUDIO_IN_ERROR_INVALID_FREQ = static_cast<int>(0x80260103);
constexpr int AUDIO_IN_ERROR_INVALID_TYPE = static_cast<int>(0x80260104);
constexpr int AUDIO_IN_ERROR_INVALID_PARAM = static_cast<int>(0x80260106);
constexpr int AUDIO_IN_ERROR_PORT_FULL = static_cast<int>(0x80260107);
constexpr int AUDIO_IN_ERROR_BUSY = static_cast<int>(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<Port, 8> 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<int>(freq);
desired.format = format.format;
desired.channels = format.channels;
desired.samples = static_cast<Uint16>(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<std::size_t>(handle) > g_ports.size()) return nullptr;
Port& port = g_ports[static_cast<std::size_t>(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<std::uint8_t, 4096> discard;
for (std::size_t excess = queued > bytes * MAX_QUEUED_BLOCKS ? queued - bytes * MAX_QUEUED_BLOCKS : 0; excess > 0;) {
const auto chunk = static_cast<Uint32>(std::min(excess, discard.size()));
excess -= SDL_DequeueAudio(device, discard.data(), chunk);
}
return SDL_DequeueAudio(device, dest, static_cast<Uint32>(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<std::size_t>(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<int>(port->samples);
port->busy = true;
lock.unlock();
auto* out = static_cast<std::uint8_t*>(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<std::uint32_t>(SDL_AUDIO_BITSIZE(format.format) / 8 * format.channels);
const auto grain = std::chrono::duration_cast<Clock::duration>(std::chrono::microseconds(1000000ull * len / freq));
g_ports[i] = {true, false, len, frameBytes, openDevice(freq, len, format), grain, {}};
return static_cast<int>(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) {

View File

@ -0,0 +1,102 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
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<int>(0x80260102);
constexpr int invalidFreq = static_cast<int>(0x80260103);
constexpr int invalidType = static_cast<int>(0x80260104);
constexpr int invalidParam = static_cast<int>(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<std::int16_t> recorded(256 * 2 * 4);
for (std::size_t i = 0; i < recorded.size(); ++i) recorded[i] = static_cast<std::int16_t>(i * 7 - 3000);
std::ofstream(path, std::ios::binary).write(reinterpret_cast<const char*>(recorded.data()), static_cast<std::streamsize>(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<std::int16_t> block(256 * 2 + 1, 0x5555);
std::vector<std::int16_t> 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<int>(0x80260101);
constexpr int portFull = static_cast<int>(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<std::uint8_t> 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();
}