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:
commit
95fe14235a
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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(¶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<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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue