From 2452ffcb3fa789ce9445be716294bfb04131363a Mon Sep 17 00:00:00 2001 From: DotDebian Date: Sun, 4 Oct 2026 17:51:25 +0200 Subject: [PATCH] feat(libSceAudioOut): implement sceAudioOutSetMixLevelPadSpk The mix level is a linear gain on the pad speaker port, 32768 being 0 dB. A pad speaker port opens at 11626 (-9 dB). An unknown handle, a port of another type and a level above 0 dB return the AudioOut error codes; a negative level throws, as its effect is unknown. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAudioOut/CMakeLists.txt | 10 + core/libs/prx/libSceAudioOut/src/AudioOut.cpp | 30 ++- .../tests/AudioOutMixLevelPadSpk.cpp | 183 ++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 218 insertions(+), 6 deletions(-) create mode 100644 core/libs/prx/libSceAudioOut/tests/AudioOutMixLevelPadSpk.cpp diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index 94e484b31..e7ae4fef6 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -32,4 +32,14 @@ if(BUILD_TESTING) set_tests_properties(audio_out2_pad_mix PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(audio_out_mix_level_pad_spk_tests tests/AudioOutMixLevelPadSpk.cpp) + target_include_directories(audio_out_mix_level_pad_spk_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out_mix_level_pad_spk_tests PRIVATE libSceAudioOut libc) + configure_windows_unwind(audio_out_mix_level_pad_spk_tests) + add_test(NAME audio_out_mix_level_pad_spk COMMAND audio_out_mix_level_pad_spk_tests) + if(WIN32) + set_tests_properties(audio_out_mix_level_pad_spk PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index 04a0fc850..8f865806a 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -26,6 +26,7 @@ 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; @@ -86,6 +87,7 @@ struct Port { Format format = Format::Unknown; int channels = 0; int volume[8] = {}; + int mixLevel = DEFAULT_VOLUME; std::uint64_t lastOutputTime = 0; SDL_AudioDeviceID device = 0; SDL_AudioSpec spec = {}; @@ -143,7 +145,7 @@ static const void* prepareBuffer(const Port& port, const void* data, std::vector const auto bps = bytesPerSample(port.format); const auto size = frames * ch * bps; - bool volumeChanged = false; + bool volumeChanged = port.mixLevel != DEFAULT_VOLUME; for (std::uint32_t i = 0; i < ch; i++) { if (port.volume[i] != DEFAULT_VOLUME) { volumeChanged = true; @@ -161,11 +163,12 @@ static const void* prepareBuffer(const Port& port, const void* data, std::vector 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)); + (static_cast(port.volume[c]) / static_cast(DEFAULT_VOLUME)) * mixLevel; } } } else { @@ -175,7 +178,7 @@ static const void* prepareBuffer(const Port& port, const void* data, std::vector 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] / DEFAULT_VOLUME; + 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())); @@ -325,6 +328,7 @@ int APS5_VABI sceAudioOutOpen(int userId, int type, int index, std::uint32_t len port.format = format; port.channels = channelsForFormat(format); port.lastOutputTime = 0; + 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; } @@ -494,9 +498,23 @@ int APS5_VABI sceAudioOutGetPortState(int handle, AudioOutPortState* state) { return 0; } -int APS5_VABI sceAudioOutSetMixLevelPadSpk(void) { - 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; } } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutMixLevelPadSpk.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutMixLevelPadSpk.cpp new file mode 100644 index 000000000..6a889cb8b --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutMixLevelPadSpk.cpp @@ -0,0 +1,183 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAudioOutOpen(int, int, int, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioOutClose(int); +int APS5_VABI sceAudioOutOutput(int, const void*); +int APS5_VABI sceAudioOutSetVolume(int, std::uint32_t, int*); +int APS5_VABI sceAudioOutSetMixLevelPadSpk(int, 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 +} + +template +static bool ThrowsRuntimeError(TFunction function) { + try { + function(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +namespace { + +constexpr int user = 0x10000000; +constexpr int portTypeMain = 0; +constexpr int portTypePadSpeaker = 4; +constexpr std::uint32_t formatS16Mono = 0; +constexpr std::uint32_t formatFloatStereo = 4; +constexpr std::uint32_t frames = 256; +constexpr std::uint32_t frequency = 48000; +constexpr int unity = 32768; +constexpr int defaultMixLevel = 11626; +constexpr int invalidPort = static_cast(0x80260003); +constexpr int invalidPortType = static_cast(0x8026000A); +constexpr int invalidMixLevel = static_cast(0x80260014); + +template +std::vector Play(int type, std::uint32_t format, const std::vector& block, TSetup setup) { + const auto path = std::filesystem::temp_directory_path() / "anyps5_audio_out_mix_level_pad_spk.raw"; + std::filesystem::remove(path); + SetEnvironment("SDL_DISKAUDIOFILE", path.string()); + + const int handle = sceAudioOutOpen(user, type, 0, frames, frequency, format); + Require(handle > 0); + setup(handle); + Require(sceAudioOutOutput(handle, block.data()) == static_cast(frames)); + Require(sceAudioOutOutput(handle, nullptr) == static_cast(frames)); + Require(sceAudioOutClose(handle) == 0); + + std::ifstream file(path, std::ios::binary); + const std::vector bytes{std::istreambuf_iterator(file), std::istreambuf_iterator()}; + file.close(); + std::filesystem::remove(path); + Require(bytes.size() >= block.size() * sizeof(TSample) && bytes.size() % sizeof(TSample) == 0); + + const auto* samples = reinterpret_cast(bytes.data()); + std::size_t first = 0; + std::size_t last = bytes.size() / sizeof(TSample); + while (first < last && samples[first] == TSample{}) first++; + while (last > first && samples[last - 1] == TSample{}) last--; + return {samples + first, samples + last}; +} + +std::vector Ramp() { + std::vector block(frames); + for (std::uint32_t frame = 0; frame < frames; frame++) { + block[frame] = static_cast((frame % 2 == 0 ? 1 : -1) * 256 * (static_cast(frame % 100) + 1)); + } + return block; +} + +std::vector Scaled(const std::vector& block, int level) { + std::vector scaled; + for (const std::int16_t sample : block) { + Require(sample * level % unity == 0); + scaled.push_back(static_cast(sample * level / unity)); + } + return scaled; +} + +void TestDefaultLevel() { + std::vector block(frames); + for (std::uint32_t frame = 0; frame < frames; frame++) block[frame] = frame % 2 == 0 ? 16384 : -16384; + const auto played = Play(portTypePadSpeaker, formatS16Mono, block, [](int) {}); + Require(played == Scaled(block, defaultMixLevel)); + Require(played[0] == 5813 && played[1] == -5813); +} + +void TestLevels() { + const auto block = Ramp(); + Require(Play(portTypePadSpeaker, formatS16Mono, block, [](int handle) { + Require(sceAudioOutSetMixLevelPadSpk(handle, unity) == 0); + }) == block); + Require(Play(portTypePadSpeaker, formatS16Mono, block, [](int handle) { + Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 2) == 0); + }) == Scaled(block, unity / 2)); + Require(Play(portTypePadSpeaker, formatS16Mono, block, [](int handle) { + Require(sceAudioOutSetMixLevelPadSpk(handle, unity) == 0); + Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 4) == 0); + }) == Scaled(block, unity / 4)); + Require(Play(portTypePadSpeaker, formatS16Mono, block, [](int handle) { + Require(sceAudioOutSetMixLevelPadSpk(handle, 0) == 0); + }).empty()); +} + +void TestLevelWithVolume() { + const auto block = Ramp(); + Require(Play(portTypePadSpeaker, formatS16Mono, block, [](int handle) { + int volume = unity / 2; + Require(sceAudioOutSetVolume(handle, 1, &volume) == 0); + Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 2) == 0); + }) == Scaled(block, unity / 4)); + + std::vector stereo(frames * 2); + for (std::uint32_t frame = 0; frame < frames; frame++) { + stereo[frame * 2] = (frame % 2 == 0 ? 0.5f : -0.25f); + stereo[frame * 2 + 1] = (frame % 2 == 0 ? -1.0f : 0.125f); + } + const auto played = Play(portTypePadSpeaker, formatFloatStereo, stereo, [](int handle) { + int volume[2] = {unity, unity / 2}; + Require(sceAudioOutSetVolume(handle, 2, volume) == 0); + Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 4) == 0); + }); + Require(played.size() == stereo.size()); + for (std::uint32_t frame = 0; frame < frames; frame++) { + Require(played[frame * 2] == stereo[frame * 2] * 0.25f); + Require(played[frame * 2 + 1] == stereo[frame * 2 + 1] * 0.125f); + } +} + +void TestOtherPortType() { + const auto block = Ramp(); + Require(Play(portTypeMain, formatS16Mono, block, [](int) {}) == block); + Require(Play(portTypeMain, formatS16Mono, block, [](int handle) { + Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 2) == invalidPortType); + Require(sceAudioOutSetMixLevelPadSpk(handle, unity + 1) == invalidPortType); + }) == block); +} + +void TestRejectedLevels() { + const auto block = Ramp(); + int closed = 0; + Require(Play(portTypePadSpeaker, formatS16Mono, block, [&closed](int handle) { + closed = handle; + Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 2) == 0); + Require(sceAudioOutSetMixLevelPadSpk(handle, unity + 1) == invalidMixLevel); + Require(ThrowsRuntimeError([handle] { sceAudioOutSetMixLevelPadSpk(handle, -1); })); + Require(sceAudioOutSetMixLevelPadSpk(0, unity) == invalidPort); + Require(sceAudioOutSetMixLevelPadSpk(handle + 1, unity) == invalidPort); + }) == Scaled(block, unity / 2)); + Require(sceAudioOutSetMixLevelPadSpk(closed, unity) == invalidPort); +} + +} + +int main() { + SetEnvironment("SDL_AUDIODRIVER", "disk"); + TestDefaultLevel(); + TestLevels(); + TestLevelWithVolume(); + TestOtherPortType(); + TestRejectedLevels(); + return 0; +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index a66170e1c..02b161133 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -41,6 +41,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceSslClose](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature - [sceSslGetSerialNumber](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature - [X+4jdIS75P0](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown name, signature +- [sceAudioOutSetMixLevelPadSpk](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - effect of a negative mix level unknown; only 0 to 32768 (0 dB) is accepted - [AOWqIYsgVHs](../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport) - unknown name, signature - [GQTObcITIXI](../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - unknown name, signature - [BnMAMrsfVWo](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libc) - unknown name, signature