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 <noreply@anthropic.com>
This commit is contained in:
DotDebian 2026-10-04 17:51:25 +02:00
parent 2cf901bcff
commit 2452ffcb3f
4 changed files with 218 additions and 6 deletions

View File

@ -32,4 +32,14 @@ if(BUILD_TESTING)
set_tests_properties(audio_out2_pad_mix PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
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:$<TARGET_FILE_DIR:libSceAudioOut>")
endif()
endif()

View File

@ -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<float*>(buf.data());
const auto* src = static_cast<const float*>(data);
const float mixLevel = static_cast<float>(port.mixLevel) / static_cast<float>(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<float>(port.volume[c]) / static_cast<float>(DEFAULT_VOLUME));
(static_cast<float>(port.volume[c]) / static_cast<float>(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<std::int64_t>(src[fr * ch + srcCh]) *
port.volume[c] / DEFAULT_VOLUME;
port.volume[c] * port.mixLevel / (static_cast<std::int64_t>(DEFAULT_VOLUME) * DEFAULT_VOLUME);
s = std::clamp(s,
static_cast<std::int64_t>(std::numeric_limits<std::int16_t>::min()),
static_cast<std::int64_t>(std::numeric_limits<std::int16_t>::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<std::mutex> 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;
}
}

View File

@ -0,0 +1,183 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <stdexcept>
#include <string>
#include <vector>
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<typename TFunction>
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<int>(0x80260003);
constexpr int invalidPortType = static_cast<int>(0x8026000A);
constexpr int invalidMixLevel = static_cast<int>(0x80260014);
template<typename TSample, typename TSetup>
std::vector<TSample> Play(int type, std::uint32_t format, const std::vector<TSample>& 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<int>(frames));
Require(sceAudioOutOutput(handle, nullptr) == static_cast<int>(frames));
Require(sceAudioOutClose(handle) == 0);
std::ifstream file(path, std::ios::binary);
const std::vector<char> bytes{std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
file.close();
std::filesystem::remove(path);
Require(bytes.size() >= block.size() * sizeof(TSample) && bytes.size() % sizeof(TSample) == 0);
const auto* samples = reinterpret_cast<const TSample*>(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<std::int16_t> Ramp() {
std::vector<std::int16_t> block(frames);
for (std::uint32_t frame = 0; frame < frames; frame++) {
block[frame] = static_cast<std::int16_t>((frame % 2 == 0 ? 1 : -1) * 256 * (static_cast<int>(frame % 100) + 1));
}
return block;
}
std::vector<std::int16_t> Scaled(const std::vector<std::int16_t>& block, int level) {
std::vector<std::int16_t> scaled;
for (const std::int16_t sample : block) {
Require(sample * level % unity == 0);
scaled.push_back(static_cast<std::int16_t>(sample * level / unity));
}
return scaled;
}
void TestDefaultLevel() {
std::vector<std::int16_t> 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<float> 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;
}

View File

@ -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