#include #include #include #include #include #include #include #include #include #include #include #include #include "SDL.h" #include "SceTypes.hpp" #include "prx/libc/include/PreciseWait.hpp" #include "prx/libkernel/Time/include/Time.hpp" static constexpr int PORT_TYPE_MAIN = 0; static constexpr int PORT_TYPE_BGM = 1; static constexpr int PORT_TYPE_VOICE = 2; static constexpr int PORT_TYPE_PERSONAL = 3; static constexpr int PORT_TYPE_PADSPK = 4; static constexpr int PORT_TYPE_VIBRATION = 10; static constexpr int PORT_TYPE_AUDIO3D = 126; static constexpr int PORT_TYPE_AUX = 127; static constexpr int PORTS_MAX = 32; static constexpr int DEFAULT_VOLUME = 32768; static constexpr int DEFAULT_PADSPK_MIX_LEVEL = 11626; static constexpr std::uint32_t FORMAT_MASK = 0xFFu; static constexpr std::uint64_t TARGET_LATENCY_US = 40000; static constexpr std::uint64_t DRAIN_TIMEOUT_US = 200000; static constexpr std::uint64_t DRAIN_SLEEP_US = 1000; enum class Format { Unknown, S16Mono, S16Stereo, S16_8Ch, F32Mono, F32Stereo, F32_8Ch, S16_8ChStd, F32_8ChStd, }; static bool formatIsFloat(Format f) { return f == Format::F32Mono || f == Format::F32Stereo || f == Format::F32_8Ch || f == Format::F32_8ChStd; } static bool formatIsStd(Format f) { return f == Format::S16_8ChStd || f == Format::F32_8ChStd; } static int channelsForFormat(Format f) { switch (f) { case Format::S16Mono: case Format::F32Mono: return 1; case Format::S16Stereo: case Format::F32Stereo: return 2; case Format::S16_8Ch: case Format::F32_8Ch: case Format::S16_8ChStd: case Format::F32_8ChStd: return 8; default: throw std::runtime_error("channelsForFormat: unknown format"); } } static SDL_AudioFormat sdlFormat(Format f) { return formatIsFloat(f) ? AUDIO_F32SYS : AUDIO_S16SYS; } static std::uint32_t bytesPerSample(Format f) { return formatIsFloat(f) ? sizeof(float) : sizeof(std::int16_t); } struct Port { bool used = false; int type = 0; std::uint32_t samplesNum = 0; std::uint32_t freq = 0; Format format = Format::Unknown; int channels = 0; int volume[8] = {}; int mixLevel = DEFAULT_VOLUME; std::uint64_t lastDataOutputTime = 0; std::uint64_t virtualQueueEnd = 0; SDL_AudioDeviceID device = 0; SDL_AudioSpec spec = {}; }; static std::mutex g_mutex; static std::mutex g_outputMutex; static Port g_ports[PORTS_MAX]; static bool g_sdlInitialized = false; static bool ensureSdlAudio() { if (g_sdlInitialized) { return true; } if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { return false; } g_sdlInitialized = true; return true; } static bool openDevice(Port& port) { if (!ensureSdlAudio()) { return false; } SDL_AudioSpec desired{}; desired.freq = static_cast(port.freq); desired.format = sdlFormat(port.format); desired.channels = static_cast(port.channels); desired.samples = static_cast(port.samplesNum); desired.callback = nullptr; SDL_AudioSpec obtained{}; port.device = SDL_OpenAudioDevice(nullptr, 0, &desired, &obtained, SDL_AUDIO_ALLOW_ANY_CHANGE); if (port.device == 0) { return false; } port.spec = obtained; SDL_PauseAudioDevice(port.device, 0); return true; } static void closeDevice(Port& port) { if (port.device != 0 && SDL_WasInit(SDL_INIT_AUDIO) != 0) { SDL_ClearQueuedAudio(port.device); SDL_CloseAudioDevice(port.device); } port.device = 0; port.spec = {}; } static constexpr std::uint32_t STD_8CH_MAP[8] = {0, 1, 2, 3, 6, 7, 4, 5}; static const void* prepareBuffer(const Port& port, const void* data, std::vector& buf) { const auto frames = port.samplesNum; const auto ch = static_cast(port.channels); const auto bps = bytesPerSample(port.format); const auto size = frames * ch * bps; bool volumeChanged = port.mixLevel != DEFAULT_VOLUME; for (std::uint32_t i = 0; i < ch; i++) { if (port.volume[i] != DEFAULT_VOLUME) { volumeChanged = true; break; } } if (!volumeChanged && !formatIsStd(port.format)) { return data; } buf.resize(size); const bool isStd = formatIsStd(port.format) && ch == 8; if (formatIsFloat(port.format)) { auto* dst = reinterpret_cast(buf.data()); const auto* src = static_cast(data); const float mixLevel = static_cast(port.mixLevel) / static_cast(DEFAULT_VOLUME); for (std::uint32_t fr = 0; fr < frames; fr++) { for (std::uint32_t c = 0; c < ch; c++) { const auto srcCh = isStd ? STD_8CH_MAP[c] : c; dst[fr * ch + c] = src[fr * ch + srcCh] * (static_cast(port.volume[c]) / static_cast(DEFAULT_VOLUME)) * mixLevel; } } } else { auto* dst = reinterpret_cast(buf.data()); const auto* src = static_cast(data); for (std::uint32_t fr = 0; fr < frames; fr++) { for (std::uint32_t c = 0; c < ch; c++) { const auto srcCh = isStd ? STD_8CH_MAP[c] : c; std::int64_t s = static_cast(src[fr * ch + srcCh]) * port.volume[c] * port.mixLevel / (static_cast(DEFAULT_VOLUME) * DEFAULT_VOLUME); s = std::clamp(s, static_cast(std::numeric_limits::min()), static_cast(std::numeric_limits::max())); dst[fr * ch + c] = static_cast(s); } } } return buf.data(); } static void sleepUs(std::unique_lock& lock, std::uint64_t us) { lock.unlock(); PreciseSleepUs(us); lock.lock(); } static void queueAudio(Port& port, const void* data, std::unique_lock& lock) { if (data == nullptr) { const std::uint64_t waitStart = sceKernelGetProcessTime(); while (SDL_GetQueuedAudioSize(port.device) > 0) { if (sceKernelGetProcessTime() - waitStart > DRAIN_TIMEOUT_US) { SDL_ClearQueuedAudio(port.device); break; } sleepUs(lock, DRAIN_SLEEP_US); } return; } std::vector prepareBuf; const void* prepared = prepareBuffer(port, data, prepareBuf); const std::uint32_t preparedSize = port.samplesNum * static_cast(port.channels) * bytesPerSample(port.format); SDL_AudioCVT cvt{}; const int cvtResult = SDL_BuildAudioCVT( &cvt, sdlFormat(port.format), static_cast(port.channels), static_cast(port.freq), port.spec.format, port.spec.channels, port.spec.freq); if (cvtResult < 0) { throw std::runtime_error(std::string("SDL_BuildAudioCVT: ") + SDL_GetError()); } const void* queueData = prepared; std::uint32_t queueSize = preparedSize; std::vector convertBuf; if (cvtResult > 0) { convertBuf.resize(static_cast(preparedSize) * cvt.len_mult); std::memcpy(convertBuf.data(), prepared, preparedSize); cvt.buf = convertBuf.data(); cvt.len = static_cast(preparedSize); if (SDL_ConvertAudio(&cvt) < 0) { throw std::runtime_error(std::string("SDL_ConvertAudio: ") + SDL_GetError()); } queueData = cvt.buf; queueSize = static_cast(cvt.len_cvt); } const std::uint64_t bufferUs = port.freq != 0 ? (1000000ULL * port.samplesNum) / port.freq : 0; const std::uint32_t buffers = bufferUs != 0 ? static_cast((TARGET_LATENCY_US + bufferUs - 1) / bufferUs) : 2u; const std::uint32_t minQueued = queueSize * std::clamp(buffers, 2u, 16u); const std::uint64_t waitStart = sceKernelGetProcessTime(); while (SDL_GetQueuedAudioSize(port.device) > minQueued) { if (sceKernelGetProcessTime() - waitStart > DRAIN_TIMEOUT_US) { SDL_ClearQueuedAudio(port.device); break; } sleepUs(lock, DRAIN_SLEEP_US); } if (SDL_QueueAudio(port.device, queueData, queueSize) < 0) { throw std::runtime_error(std::string("SDL_QueueAudio: ") + SDL_GetError()); } // APS5_LOG_OUT("device=%u type=%d bytes=%u queued=%u", port.device, port.type, queueSize, SDL_GetQueuedAudioSize(port.device)); } static void paceVirtualPort(Port& port, bool hasData, std::unique_lock& lock) { const std::uint64_t now = sceKernelGetProcessTime(); const auto queued = static_cast(port.virtualQueueEnd - now); if (queued <= 0) port.virtualQueueEnd = now; if (!hasData) { if (queued > 0) sleepUs(lock, static_cast(queued)); return; } if (queued > static_cast(TARGET_LATENCY_US)) sleepUs(lock, static_cast(queued) - TARGET_LATENCY_US); port.virtualQueueEnd += (1000000ULL * port.samplesNum) / port.freq; } static bool portTypeValid(int type) { return (type >= PORT_TYPE_MAIN && type <= PORT_TYPE_PADSPK) || type == PORT_TYPE_VIBRATION || type == PORT_TYPE_AUDIO3D || type == PORT_TYPE_AUX; } static Port* getPort(int handle) { const int idx = handle - 1; if (idx < 0 || idx >= PORTS_MAX || !g_ports[idx].used) { return nullptr; } return &g_ports[idx]; } extern "C" { int APS5_VABI sceAudioOutInit() { return 0; } int APS5_VABI sceAudioOutOpen(int userId, int type, int index, std::uint32_t len, std::uint32_t freq, std::uint32_t param) { (void)userId; if (!portTypeValid(type)) { return -2144993270; } if (len == 0) { return -2144993274; } if (freq == 0) { return -2144993272; } if (index != 0) { throw std::runtime_error("sceAudioOutOpen: index != 0 not supported"); } Format format = Format::Unknown; switch (param & FORMAT_MASK) { case 0: format = Format::S16Mono; break; case 1: format = Format::S16Stereo; break; case 2: format = Format::S16_8Ch; break; case 3: format = Format::F32Mono; break; case 4: format = Format::F32Stereo; break; case 5: format = Format::F32_8Ch; break; case 6: format = Format::S16_8ChStd; break; case 7: format = Format::F32_8ChStd; break; default: throw std::runtime_error("sceAudioOutOpen: unknown format param"); } std::lock_guard lock(g_mutex); for (int i = 0; i < PORTS_MAX; i++) { if (!g_ports[i].used) { Port& port = g_ports[i]; port.used = true; port.type = type; port.samplesNum = len; port.freq = freq; port.format = format; port.channels = channelsForFormat(format); port.lastDataOutputTime = 0; port.virtualQueueEnd = sceKernelGetProcessTime(); port.mixLevel = type == PORT_TYPE_PADSPK ? DEFAULT_PADSPK_MIX_LEVEL : DEFAULT_VOLUME; for (int c = 0; c < port.channels; c++) { port.volume[c] = DEFAULT_VOLUME; } if (type != PORT_TYPE_VIBRATION) { openDevice(port); } static const bool trace = std::getenv("APS5_TRACE_AUDIOOUT") != nullptr; if (trace) std::fprintf(stderr, "[audioout] port %d: type %d, %u samples at %u Hz, format %d, device %u\n", i + 1, type, len, freq, static_cast(format), static_cast(port.device)); return i + 1; } } return -2144993275; } int APS5_VABI sceAudioOutClose(int handle) { std::lock_guard outputLock(g_outputMutex); std::lock_guard lock(g_mutex); Port* port = getPort(handle); if (port == nullptr) { return -2144993277; } closeDevice(*port); *port = Port{}; return 0; } int APS5_VABI sceAudioOutOutput(int handle, const void* ptr) { std::lock_guard outputLock(g_outputMutex); std::unique_lock lock(g_mutex); Port* port = getPort(handle); if (port == nullptr) { return -2144993277; } if (port->device == 0) { paceVirtualPort(*port, ptr != nullptr, lock); } else { queueAudio(*port, ptr, lock); } if (ptr != nullptr) port->lastDataOutputTime = sceKernelGetProcessTime(); return static_cast(port->samplesNum); } int APS5_VABI sceAudioOutOutputs(AudioOutOutputParam* param, std::uint32_t num) { if (param == nullptr || num == 0) { return -2144993276; } std::lock_guard outputLock(g_outputMutex); std::unique_lock lock(g_mutex); for (std::uint32_t i = 0; i < num; i++) { if (getPort(param[i].handle) == nullptr) { return -2144993277; } } Port& first = *getPort(param[0].handle); for (std::uint32_t i = 0; i < num; i++) { Port& port = *getPort(param[i].handle); if (port.device == 0) { paceVirtualPort(port, param[i].ptr != nullptr, lock); } else { queueAudio(port, param[i].ptr, lock); } } const std::uint64_t done = sceKernelGetProcessTime(); for (std::uint32_t i = 0; i < num; i++) { if (param[i].ptr != nullptr) getPort(param[i].handle)->lastDataOutputTime = done; } return static_cast(first.samplesNum); } int APS5_VABI sceAudioOutSetVolume(int handle, std::uint32_t flag, int* vol) { if (vol == nullptr) { return -2144993276; } std::lock_guard lock(g_mutex); Port* port = getPort(handle); if (port == nullptr) { return -2144993277; } const bool isStd = formatIsStd(port->format); for (int i = 0; i < port->channels; i++, flag >>= 1u) { if ((flag & 1u) == 0) { continue; } int srcIdx = i; if (isStd) { if (i == 4) srcIdx = 6; else if (i == 5) srcIdx = 7; else if (i == 6) srcIdx = 4; else if (i == 7) srcIdx = 5; } port->volume[i] = vol[srcIdx]; } return 0; } int APS5_VABI sceAudioOutGetLastOutputTime(int handle, std::uint64_t* outputTime) { if (outputTime == nullptr) { return -2144993276; } std::lock_guard lock(g_mutex); Port* port = getPort(handle); if (port == nullptr) { return -2144993277; } *outputTime = port->lastDataOutputTime; return 0; } int APS5_VABI sceAudioOutGetPortState(int handle, AudioOutPortState* state) { if (state == nullptr) { return -2144993276; } std::lock_guard lock(g_mutex); Port* port = getPort(handle); if (port == nullptr) { return -2144993277; } state->rerouteCounter = 0; state->volume = 127; state->flag = 0; state->activeState = 0; state->reserved[0] = 0; switch (port->type) { case PORT_TYPE_MAIN: case PORT_TYPE_BGM: case PORT_TYPE_AUDIO3D: state->output = 1; state->channel = static_cast(port->channels > 2 ? 2 : port->channels); break; case PORT_TYPE_VOICE: case PORT_TYPE_PERSONAL: state->output = 0x40; state->channel = 1; break; case PORT_TYPE_PADSPK: case PORT_TYPE_VIBRATION: state->output = 4; state->channel = 1; break; case PORT_TYPE_AUX: state->output = 0x80; state->channel = 0; break; default: throw std::runtime_error("sceAudioOutGetPortState: unknown port type"); } return 0; } int APS5_VABI sceAudioOutSysClose() { NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceAudioOutSysConfigureOutput() { NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceAudioOutSysGetHdmiMonitorInfo() { NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceAudioOutSysOpen() { NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceAudioOutSetMixLevelPadSpk(int handle, int mixLevel) { std::lock_guard lock(g_mutex); Port* port = getPort(handle); if (port == nullptr) { return -2144993277; } if (port->type != PORT_TYPE_PADSPK) { return -2144993270; } if (mixLevel > DEFAULT_VOLUME) { return -2144993260; } if (mixLevel < 0) { throw std::runtime_error("sceAudioOutSetMixLevelPadSpk: negative mix level not supported"); } port->mixLevel = mixLevel; return 0; } int APS5_VABI sceAudioOutExClose() { NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceAudioOutExConfigureOutput() { NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceAudioOutExGetMonitorInfo() { NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceAudioOutExOpen() { NotImplemented_nid_no_patch(__func__); return 0; } }