diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index c3d0bd85a..d42ddf36a 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -66,6 +66,15 @@ if(BUILD_TESTING) set_tests_properties(audio_out2_port_layouts PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(audio_out2_port_buffers_tests tests/AudioOut2PortBuffers.cpp) + target_include_directories(audio_out2_port_buffers_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out2_port_buffers_tests PRIVATE libSceAudioOut libc) + configure_windows_unwind(audio_out2_port_buffers_tests) + add_test(NAME audio_out2_port_buffers COMMAND audio_out2_port_buffers_tests) + if(WIN32) + set_tests_properties(audio_out2_port_buffers PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(audio_out2_timing_tests tests/AudioOut2Timing.cpp) target_include_directories(audio_out2_timing_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(audio_out2_timing_tests PRIVATE libSceAudioOut libc) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp index 96b9ed331..1a6f8cc7e 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp @@ -63,6 +63,9 @@ struct AudioOut2Port { bool int16 = false; const AudioOut2StereoFold* fold = nullptr; const void* data = nullptr; + std::deque queuedData; + const void* lastBuffer = nullptr; + const void* repeatedData = nullptr; float volume[AUDIO_OUT2_PORT_CHANNELS_MAX] = {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; std::uint64_t dataSets = 0; std::uint64_t attributeTraces = 0; diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp index 051ea932e..0deb7d563 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp @@ -23,6 +23,7 @@ static constexpr std::uint64_t ATTRIBUTE_TRACE_EVERY = 2000; // 8 and 9 (a u32, a u32 and a byte on object and voice ports) are not understood and ignored. static constexpr std::uint32_t ATTRIBUTE_DATA = 0; static constexpr std::uint32_t ATTRIBUTE_VOLUME = 1; +static constexpr std::size_t QUEUED_DATA_MAX = 16; static constexpr std::uint32_t FORMAT_CHANNELS_SHIFT = 8; static constexpr std::uint32_t FORMAT_CHANNELS_MASK = 0xFu; @@ -102,8 +103,14 @@ AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context) { std::lock_guard lock(g_portsLock); AudioOut2Grain grain; for (std::size_t index = 0; index < g_ports.size(); index++) { - const auto& port = g_ports[index]; - if (port.used && port.context == &context && port.data != nullptr && port.channels != 0) grain.push_back({index, port.generation, port.data}); + auto& port = g_ports[index]; + if (!port.used || port.context != &context || port.channels == 0) continue; + const void* data = port.repeatedData; + if (!port.queuedData.empty()) { + data = port.queuedData.front(); + port.queuedData.pop_front(); + } + if (data != nullptr) grain.push_back({index, port.generation, data}); } return grain; } @@ -238,6 +245,10 @@ int APS5_VABI sceAudioOut2PortSetAttributes(AudioOut2PortHandle port, const Audi verdict = "null value"; } else if (attribute.attribute_id == ATTRIBUTE_DATA && attribute.value_size == sizeof(entry->data)) { std::memcpy(&entry->data, attribute.value, sizeof(entry->data)); + entry->queuedData.push_back(entry->data); + if (entry->queuedData.size() > QUEUED_DATA_MAX) entry->queuedData.pop_front(); + entry->repeatedData = entry->data != nullptr && (entry->lastBuffer == nullptr || entry->lastBuffer == entry->data) ? entry->data : nullptr; + if (entry->data != nullptr) entry->lastBuffer = entry->data; entry->dataSets++; verdict = "pcm data pointer"; } else if (attribute.attribute_id == ATTRIBUTE_VOLUME && entry->channels != 0 && attribute.value_size == entry->channels * sizeof(float)) { diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp new file mode 100644 index 000000000..54f467b33 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp @@ -0,0 +1,172 @@ +#include "SceTypes.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAudioOut2ContextResetParam(AudioOut2ContextParam*); +int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam*, void*, std::size_t, AudioOut2ContextHandle*); +int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle); +int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle, std::uint32_t); +int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle, const AudioOut2PortParam*, AudioOut2PortHandle*); +int APS5_VABI sceAudioOut2PortDestroy(AudioOut2PortHandle); +int APS5_VABI sceAudioOut2PortSetAttributes(AudioOut2PortHandle, const AudioOut2Attribute*, std::uint32_t); +} + +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 std::uint32_t grain = 256; +constexpr std::uint32_t channels = 2; +constexpr std::uint32_t frequency = 48000; +constexpr std::uint16_t portTypeMain = 0; +constexpr std::uint32_t attributeData = 0; +constexpr std::uint32_t formatStereoFloat = 2u << 8; +constexpr std::uint32_t silentGrains = 32; +constexpr float masterGain = 0.5f; +constexpr float tolerance = 1e-6f; + +void SetData(AudioOut2PortHandle port, const void* data) { + const AudioOut2Attribute attribute{attributeData, 0, &data, sizeof(data)}; + Require(sceAudioOut2PortSetAttributes(port, &attribute, 1) == 0); +} + +std::vector Play(const std::function& feed) { + const auto path = std::filesystem::temp_directory_path() / ("anyps5_audio_out2_port_buffers-" + std::to_string(std::random_device{}()) + ".raw"); + std::filesystem::remove(path); + SetEnvironment("SDL_DISKAUDIOFILE", path.string()); + + AudioOut2ContextParam params{}; + Require(sceAudioOut2ContextResetParam(¶ms) == 0); + params.num_grains = grain; + params.queue_depth = 1; + AudioOut2ContextHandle context = 0; + Require(sceAudioOut2ContextCreate(¶ms, nullptr, 0, &context) == 0); + AudioOut2PortParam portParams{}; + portParams.port_type = portTypeMain; + portParams.data_format = formatStereoFloat; + portParams.sampling_freq = frequency; + AudioOut2PortHandle port = 0; + Require(sceAudioOut2PortCreate(context, &portParams, &port) == 0); + feed(context, port); + for (std::uint32_t push = 0; push < silentGrains; push++) Require(sceAudioOut2ContextPush(context, 1) == 0); + Require(sceAudioOut2PortDestroy(port) == 0); + Require(sceAudioOut2ContextDestroy(context) == 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() % sizeof(float) == 0); + const auto* samples = reinterpret_cast(bytes.data()); + std::size_t first = 0; + std::size_t last = bytes.size() / sizeof(float); + while (first < last && samples[first] == 0.0f) first++; + while (last > first && samples[last - 1] == 0.0f) last--; + return {samples + first, samples + last}; +} + +std::vector Grain(float level) { + std::vector data(static_cast(grain) * channels); + for (std::uint32_t frame = 0; frame < grain; frame++) { + const float sign = frame % 2 == 0 ? 1.0f : -1.0f; + data[frame * channels] = sign * level * static_cast(1 + frame % 5); + data[frame * channels + 1] = -sign * level * static_cast(1 + frame % 3); + } + return data; +} + +void RequirePlayed(const std::vector& played, const std::vector*>& grains) { + Require(played.size() == grains.size() * grain * channels); + for (std::size_t index = 0; index < grains.size(); index++) { + const auto& data = *grains[index]; + for (std::size_t sample = 0; sample < data.size(); sample++) { + Require(std::fabs(played[index * data.size() + sample] - data[sample] * masterGain) <= tolerance); + } + } +} + +void TestBuffersSetBeforeOnePushPlayInOrder() { + const auto first = Grain(0.01f); + const auto second = Grain(0.02f); + const auto third = Grain(0.03f); + const auto played = Play([&](AudioOut2ContextHandle context, AudioOut2PortHandle port) { + SetData(port, first.data()); + SetData(port, second.data()); + SetData(port, third.data()); + Require(sceAudioOut2ContextPush(context, 1) == 0); + Require(sceAudioOut2ContextPush(context, 1) == 0); + Require(sceAudioOut2ContextPush(context, 1) == 0); + }); + RequirePlayed(played, {&first, &second, &third}); +} + +void TestBurstThenGapKeepsEveryBuffer() { + const auto first = Grain(0.01f); + const auto second = Grain(0.02f); + const auto third = Grain(0.03f); + const auto played = Play([&](AudioOut2ContextHandle context, AudioOut2PortHandle port) { + SetData(port, first.data()); + Require(sceAudioOut2ContextPush(context, 1) == 0); + SetData(port, second.data()); + SetData(port, third.data()); + Require(sceAudioOut2ContextPush(context, 1) == 0); + Require(sceAudioOut2ContextPush(context, 1) == 0); + }); + RequirePlayed(played, {&first, &second, &third}); +} + +void TestAdvancingPortWithoutNewBufferPlaysSilence() { + const auto first = Grain(0.01f); + const auto second = Grain(0.02f); + const auto played = Play([&](AudioOut2ContextHandle context, AudioOut2PortHandle port) { + SetData(port, first.data()); + Require(sceAudioOut2ContextPush(context, 1) == 0); + SetData(port, second.data()); + Require(sceAudioOut2ContextPush(context, 1) == 0); + }); + RequirePlayed(played, {&first, &second}); +} + +void TestRewrittenBufferRepeats() { + auto buffer = Grain(0.01f); + const auto first = buffer; + const auto second = Grain(0.02f); + const auto played = Play([&](AudioOut2ContextHandle context, AudioOut2PortHandle port) { + SetData(port, buffer.data()); + Require(sceAudioOut2ContextPush(context, 1) == 0); + Require(sceAudioOut2ContextPush(context, 1) == 0); + buffer = second; + SetData(port, nullptr); + }); + RequirePlayed(played, {&first, &second}); +} + +} + +int main() { + SetEnvironment("SDL_AUDIODRIVER", "disk"); + TestBuffersSetBeforeOnePushPlayInOrder(); + TestBurstThenGapKeepsEveryBuffer(); + TestAdvancingPortWithoutNewBufferPlaysSilence(); + TestRewrittenBufferRepeats(); + return 0; +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 01b8d21a2..19bbb3236 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -169,6 +169,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAudioOut2PortCreate](../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - MAIN (type 0) format validation follows PS5 firmware 13.60 measurements: bits 8-11 accept 1, 2, 8 or 12, bits 0-6 accept 0 or 1, bit 7 requires 8 channels, and bits 12-31 do not affect validation. The sample types are still read as float and int16; bit 7's layout, the PCM effect of bits 12-31 and the port `flags` are unverified. Other port types retain unverified format validation, including 6 channels and rejection of bits 12-31 - [sceAudioOut2PortCreate](../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - 12 channels are assumed to be L R C LFE Ls Rs Lb Rb Ltf Rtf Ltb Rtb as in KytyPS5's 12-to-8 fold, and 6 and 8 channels to be its first 6 and 8; no title or SDK header confirms the order - [sceAudioOut2PortCreate](../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - the stereo fold is assumed, not the console's downmix: C at -3 dB into both sides, Ls/Rs, Lb/Rb and Ltf/Rtf at -3 dB and Ltb/Rtb at -6 dB into their own side, LFE dropped +- [sceAudioOut2PortSetAttributes](../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - each PCM buffer set (attribute 0) is taken as one grain: buffers set between two pushes are queued and played one per grain, at most 16 queued. A grain with no new buffer repeats the last one only when the port has always been given that same buffer (rewritten in place); a port whose buffers move on plays silence instead. Inferred from PPSA12544, whose object port (type 6) is given one grain of a 64-grain ring per set, often two or three between pushes and then none for a few; the console behaviour for several sets per push and for a push without a new buffer is not documented - [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; only 0x8A810001 (not initialized) is returned - [scePsmlMfsrGetDispatchMfsrPacket1000](../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - unknown signature, imported by PPSA25344; declared without parameters. libScePsml and libScePsml_debug are built from the same source - [scePngEncEncode](../../core/libs/prx/libScePngEnc/Export.cpp) (libScePngEnc) - whether rows may stay unfiltered when `filter_type` names a subset of the filters is unknown; as in shadPS4, they may only for 0 and for the all-filters mask