Merge pull request #2536 from orbz22/fix/audioout2-queue-port-buffers

fix(audioout): play every AudioOut2 port buffer set between pushes, one per grain
This commit is contained in:
Adrià 2026-10-10 11:17:19 +02:00 committed by GitHub
commit 95fe14235a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 198 additions and 2 deletions

View File

@ -66,6 +66,15 @@ if(BUILD_TESTING)
set_tests_properties(audio_out2_port_layouts PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceAudioOut>")
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:$<TARGET_FILE_DIR:libSceAudioOut>")
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)

View File

@ -63,6 +63,9 @@ struct AudioOut2Port {
bool int16 = false;
const AudioOut2StereoFold* fold = nullptr;
const void* data = nullptr;
std::deque<const void*> 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;

View File

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

View File

@ -0,0 +1,172 @@
#include "SceTypes.hpp"
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <functional>
#include <iterator>
#include <random>
#include <string>
#include <vector>
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<float> Play(const std::function<void(AudioOut2ContextHandle, AudioOut2PortHandle)>& 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(&params) == 0);
params.num_grains = grain;
params.queue_depth = 1;
AudioOut2ContextHandle context = 0;
Require(sceAudioOut2ContextCreate(&params, 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<char> bytes{std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
file.close();
std::filesystem::remove(path);
Require(bytes.size() % sizeof(float) == 0);
const auto* samples = reinterpret_cast<const float*>(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<float> Grain(float level) {
std::vector<float> data(static_cast<std::size_t>(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<float>(1 + frame % 5);
data[frame * channels + 1] = -sign * level * static_cast<float>(1 + frame % 3);
}
return data;
}
void RequirePlayed(const std::vector<float>& played, const std::vector<const std::vector<float>*>& 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;
}

View File

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