Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions core/libs/prx/libSceAudioOut/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
15 changes: 13 additions & 2 deletions core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -234,6 +241,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)) {
Expand Down
172 changes: 172 additions & 0 deletions core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp
Original file line number Diff line number Diff line change
@@ -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;
}
1 change: 1 addition & 0 deletions docs/dev/TechnicalDebt.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,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
- [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
- [sceJpegEncEncode](../../core/libs/prx/libSceJpegEnc/Export.cpp) (libSceJpegEnc) - `restart_interval` follows SharpProspero's field doc (0 none, -1 every row of blocks, positive a block count); a positive value is taken as the JPEG DRI interval in MCUs and -1 as one restart per MCU row, which no reference confirms. Other negative values throw
Expand Down
Loading