From 64230cf95ab2e331b55fbccde9ac671e0972588d Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Tue, 6 Oct 2026 20:37:42 -0400 Subject: [PATCH 1/5] feat(libSceNgs2.native): implement sceNgs2ParseWaveformFile and parse 16-bit PCM waveforms --- .../prx/libSceNgs2.native/Ngs2Library.cmake | 2 +- .../libs/prx/libSceNgs2.native/src/Atrac9.cpp | 53 +++++++++++++++ .../libSceNgs2.native/tests/Ngs2Atrac9.cpp | 65 +++++++++++++++++++ .../prx/libSceNgs2.native/tests/Ngs2Test.hpp | 1 + docs/dev/TechnicalDebt.md | 1 + 5 files changed, 121 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake b/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake index 5f8a9df33..453e4d6a1 100644 --- a/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake +++ b/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake @@ -10,7 +10,7 @@ function(add_sce_ngs2_library target) ${ngs2Dir}/src/Voice.cpp ) target_include_directories(${target} PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) - target_link_libraries(${target} PRIVATE atrac9) + target_link_libraries(${target} PRIVATE atrac9 libc libkernel) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_options(${target} PRIVATE LINKER:--exclude-libs,ALL) endif() diff --git a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp index 3a59cf433..7fc7691d8 100644 --- a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp @@ -2,15 +2,19 @@ #include #include #include +#include +#include #include #include #include #include +#include #include "libatrac9.h" #include "prx/libc/include/General.hpp" #include "Ngs2Internal.hpp" +static constexpr std::uint16_t WAVE_FORMAT_PCM = 0x1; static constexpr std::uint16_t WAVE_FORMAT_EXTENSIBLE = 0xfffe; static constexpr std::uint8_t ATRAC9_GUID[16] = {0xd2, 0x42, 0xe1, 0x47, 0xba, 0x36, 0x8d, 0x4d, 0x88, 0xfc, 0x61, 0x65, 0x4f, 0x8c, 0x83, 0x6c}; static constexpr std::size_t FMT_ATRAC9_SIZE = 52; @@ -181,6 +185,33 @@ static int ParseAtrac9(const RiffChunks& chunks, Ngs2WaveformInfo& info) { return SCE_NGS2_OK; } +static int ParsePcm16(const RiffChunks& chunks, std::size_t size, Ngs2WaveformInfo& info) { + const auto* format = chunks.format; + if (chunks.formatSize < 16) return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; + const std::uint32_t channels = ReadLe16(format + 2); + const std::uint32_t sampleRate = ReadLe32(format + 4); + const std::uint32_t bytesPerFrame = channels * sizeof(std::int16_t); + if (ReadLe16(format + 14) != 16 || channels == 0 || channels > NGS2_MAX_CHANNELS || sampleRate == 0 || sampleRate > MAX_SAMPLE_RATE || ReadLe16(format + 12) != bytesPerFrame) { + return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; + } + if (chunks.sampler != nullptr && chunks.samplerSize >= 32 && ReadLe32(chunks.sampler + 28) != 0) throw std::runtime_error("NGS2: parsing looped waveforms is not implemented"); + const std::size_t dataSize = std::min(chunks.dataSize, size - chunks.dataOffset) / bytesPerFrame * bytesPerFrame; + info.format = {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, channels, sampleRate, 0, 0, 0}; + info.data_offset = static_cast(chunks.dataOffset); + info.data_size = static_cast(dataSize); + info.num_samples = static_cast(dataSize / bytesPerFrame); + info.audio_unit_size = bytesPerFrame; + info.num_audio_unit_samples = 1; + info.num_audio_unit_per_frame = 1; + info.audio_frame_size = bytesPerFrame; + info.num_audio_frame_samples = 1; + info.num_blocks = 1; + info.block[0].data_offset = chunks.dataOffset; + info.block[0].data_size = dataSize; + info.block[0].num_samples = info.num_samples; + return SCE_NGS2_OK; +} + #pragma GCC visibility push(default) extern "C" { @@ -197,9 +228,31 @@ int APS5_VABI sceNgs2ParseWaveformData(const void* data, size_t data_size, Ngs2W std::memcmp(chunks.format + FMT_GUID_OFFSET, ATRAC9_GUID, sizeof(ATRAC9_GUID)) == 0) { return ParseAtrac9(chunks, *info); } + if (tag == WAVE_FORMAT_PCM) return ParsePcm16(chunks, data_size, *info); throw std::runtime_error("NGS2: parsing waveform format tag " + Ngs2Hex(tag) + " is not implemented"); } +int APS5_VABI sceNgs2ParseWaveformFile(const char* path, uint32_t offset, uint32_t size, Ngs2WaveformInfo* info) { + if (info == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + *info = {}; + if (path == nullptr) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA; + const auto host = ResolvePath_nid_no_patch(path); + std::ifstream file(host, std::ios::binary | std::ios::ate); + if (!file) throw std::runtime_error(std::string(__func__) + ": cannot open " + host.string()); + const auto fileSize = static_cast(file.tellg()); + if (offset > fileSize) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA; + const std::uint64_t length = size != 0 ? size : fileSize - offset; + if (length > fileSize - offset) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA; + std::vector bytes(static_cast(length)); + file.seekg(offset); + if (!file.read(reinterpret_cast(bytes.data()), static_cast(bytes.size()))) throw std::runtime_error(std::string(__func__) + ": read failed for " + host.string()); + const int result = sceNgs2ParseWaveformData(bytes.data(), bytes.size(), info); + if (result != SCE_NGS2_OK) return result; + info->data_offset += offset; + for (std::uint32_t i = 0; i < info->num_blocks; i++) info->block[i].data_offset += offset; + return SCE_NGS2_OK; +} + int APS5_VABI sceNgs2CalcWaveformBlock(const Ngs2WaveformFormat* format, uint32_t sample_pos, uint32_t num_samples, Ngs2WaveformBlock* block) { if (block == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; *block = {}; diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp index 739d8ae3c..97e168908 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp @@ -4,6 +4,8 @@ #include #include +#include +#include #include #include #include @@ -176,6 +178,67 @@ static void TestParse() { Require(sceNgs2ParseWaveformData(wrongRate.data(), wrongRate.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); } +static std::vector PcmFile(std::uint32_t channels, std::uint32_t sampleRate, std::uint32_t bits, std::uint32_t frames) { + const std::uint32_t frameBytes = channels * bits / 8; + std::vector file; + PutTag(file, "RIFF"); + Put32(file, 36 + frames * frameBytes); + PutTag(file, "WAVE"); + PutTag(file, "fmt "); + Put32(file, 16); + Put16(file, 1); + Put16(file, channels); + Put32(file, sampleRate); + Put32(file, sampleRate * frameBytes); + Put16(file, frameBytes); + Put16(file, bits); + PutTag(file, "data"); + Put32(file, frames * frameBytes); + file.resize(file.size() + frames * frameBytes, 0x11); + return file; +} + +static void TestParsePcm() { + const auto file = PcmFile(2, 44100, 16, 100); + Ngs2WaveformInfo info{}; + Require(sceNgs2ParseWaveformData(file.data(), file.size(), &info) == SCE_NGS2_OK); + Require(info.format.waveform_type == SCE_NGS2_WAVEFORM_TYPE_PCM_I16L && info.format.num_channels == 2 && info.format.sample_rate == 44100); + Require(info.data_offset == 44 && info.data_size == 400 && info.num_samples == 100); + Require(info.num_blocks == 1 && info.block[0].data_offset == 44 && info.block[0].data_size == 400 && info.block[0].num_samples == 100); + + auto truncated = file; + truncated.resize(44 + 202); + Require(sceNgs2ParseWaveformData(truncated.data(), truncated.size(), &info) == SCE_NGS2_OK); + Require(info.data_size == 200 && info.num_samples == 50); + + const auto eightBit = PcmFile(1, 44100, 8, 10); + Require(sceNgs2ParseWaveformData(eightBit.data(), eightBit.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); + const auto noChannels = PcmFile(0, 44100, 16, 10); + Require(sceNgs2ParseWaveformData(noChannels.data(), noChannels.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); +} + +static void TestParseFile() { + const auto wave = PcmFile(1, 22050, 16, 64); + const auto path = std::filesystem::temp_directory_path() / "aps5_ngs2_parse_file.wav"; + { + std::ofstream out(path, std::ios::binary); + out.write("", 3); + out.write(reinterpret_cast(wave.data()), static_cast(wave.size())); + } + const auto name = path.string(); + Ngs2WaveformInfo info{}; + Require(sceNgs2ParseWaveformFile(name.c_str(), 3, 0, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2ParseWaveformFile(nullptr, 0, 0, &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); + Require(sceNgs2ParseWaveformFile(name.c_str(), 3, 0, &info) == SCE_NGS2_OK); + Require(info.format.num_channels == 1 && info.format.sample_rate == 22050 && info.num_samples == 64); + Require(info.data_offset == 3 + 44 && info.block[0].data_offset == 3 + 44 && info.data_size == 128); + Require(sceNgs2ParseWaveformFile(name.c_str(), 3, static_cast(wave.size()), &info) == SCE_NGS2_OK); + Require(sceNgs2ParseWaveformFile(name.c_str(), 3, static_cast(wave.size()) + 1, &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); + Require(sceNgs2ParseWaveformFile(name.c_str(), static_cast(wave.size()) + 4, 0, &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); + Require(sceNgs2ParseWaveformFile(name.c_str(), 0, 0, &info) == SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT); + std::filesystem::remove(path); +} + static void TestCalcBlock() { const Ngs2WaveformFormat format{SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 1, 48000, Config, 0, 0}; Ngs2WaveformBlock block{}; @@ -235,6 +298,8 @@ int main() { TestSkipAndBlockEnd(reference); TestRepeatAndState(reference); TestParse(); + TestParsePcm(); + TestParseFile(); TestCalcBlock(); TestCorruptSuperframeAtPageEnd(); return 0; diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp index ecfa52ce3..3018a2f4a 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp @@ -10,6 +10,7 @@ extern "C" { int APS5_VABI sceNgs2ParseWaveformData(const void*, size_t, Ngs2WaveformInfo*); +int APS5_VABI sceNgs2ParseWaveformFile(const char*, uint32_t, uint32_t, Ngs2WaveformInfo*); int APS5_VABI sceNgs2CalcWaveformBlock(const Ngs2WaveformFormat*, uint32_t, uint32_t, Ngs2WaveformBlock*); int APS5_VABI sceNgs2SystemResetOption(Ngs2SystemOption*); int APS5_VABI sceNgs2SystemQueryBufferSize(const Ngs2SystemOption*, Ngs2ContextBufferInfo*); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 23d063145..94c2af7bc 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -150,6 +150,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceNgs2RackGetInfo](../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - field order from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2RackInfo` with the 64-byte name of the other info structures (216 bytes); `type` is assumed to be the rack id divided by 0x1000, `min_grain_samples` 64 (the system minimum) and `state_flags` 1; `uid` comes from a per-process rack counter, `render_count` is the owning system's render count and `active_voice_count` counts voices that are not empty (paused and stopped voices included). The last process ratio and tick and `active_channel_work_count` are left zero - [sceNgs2VoiceQueryInfo](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - only info id 0x4001 is known: PPSA22520 passes an 8-byte buffer and branches on the first word being 1 or 2, so it is assumed to be the voice channel count (0 before the voice is set up); the second word is zeroed and other ids throw - [sceNgs2VoiceGetPortInfo](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - layout from shadPS4; the delay and destination input are zero because ports have neither yet +- [sceNgs2ParseWaveformFile](../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature is assumed to be `(path, offset, size, info)` with a zero size meaning the rest of the file, and the data and block offsets are taken to be relative to the start of the file, not of the requested range; an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame, and the info of looped PCM and of other PCM formats throw - [sceSaveDataInitialize3](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a repeated initialize succeeds: PPSA12544's executable and its Unity SaveData plugin both initialize, and the plugin fails on any error. Initializations are counted and `sceSaveDataTerminate` ends the session at the last one; how the console pairs them is unknown - [sceVoiceSetMuteFlag](../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - only shadPS4 (PS4) has bodies. A null tick returns `INVALID_POINTER` like the existing `sceRtcFormatRFC3339`, where shadPS4 formats the current time instead; offsets outside ±1439 minutes return `INVALID_VALUE` like `sceRtcFormatRFC3339`; the local-time variants use the host's offset at the given instant (shadPS4 uses `sceKernelGettimezone`, the current offset) From 74a7a1d9f1ac1d195780851a884ae4b346683f07 Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Tue, 6 Oct 2026 21:53:59 -0400 Subject: [PATCH 2/5] fix(libSceNgs2.native): take (path, offset, info) in sceNgs2ParseWaveformFile and reject PCM data that does not fit The call site of PPSA21402 sets only three argument registers, so the function has no size parameter. A data chunk that runs past the buffer or ends inside a frame returns INVALID_WAVEFORM_DATA instead of being shortened. The test no longer writes padding from a string literal. --- .../libs/prx/libSceNgs2.native/src/Atrac9.cpp | 8 +++---- .../libSceNgs2.native/tests/Ngs2Atrac9.cpp | 24 +++++++++++-------- .../prx/libSceNgs2.native/tests/Ngs2Test.hpp | 2 +- docs/dev/TechnicalDebt.md | 2 +- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp index 7fc7691d8..af2b13da2 100644 --- a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp @@ -195,7 +195,8 @@ static int ParsePcm16(const RiffChunks& chunks, std::size_t size, Ngs2WaveformIn return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; } if (chunks.sampler != nullptr && chunks.samplerSize >= 32 && ReadLe32(chunks.sampler + 28) != 0) throw std::runtime_error("NGS2: parsing looped waveforms is not implemented"); - const std::size_t dataSize = std::min(chunks.dataSize, size - chunks.dataOffset) / bytesPerFrame * bytesPerFrame; + if (chunks.dataSize > size - chunks.dataOffset || chunks.dataSize % bytesPerFrame != 0) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA; + const std::size_t dataSize = chunks.dataSize; info.format = {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, channels, sampleRate, 0, 0, 0}; info.data_offset = static_cast(chunks.dataOffset); info.data_size = static_cast(dataSize); @@ -232,7 +233,7 @@ int APS5_VABI sceNgs2ParseWaveformData(const void* data, size_t data_size, Ngs2W throw std::runtime_error("NGS2: parsing waveform format tag " + Ngs2Hex(tag) + " is not implemented"); } -int APS5_VABI sceNgs2ParseWaveformFile(const char* path, uint32_t offset, uint32_t size, Ngs2WaveformInfo* info) { +int APS5_VABI sceNgs2ParseWaveformFile(const char* path, uint32_t offset, Ngs2WaveformInfo* info) { if (info == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; *info = {}; if (path == nullptr) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA; @@ -241,8 +242,7 @@ int APS5_VABI sceNgs2ParseWaveformFile(const char* path, uint32_t offset, uint32 if (!file) throw std::runtime_error(std::string(__func__) + ": cannot open " + host.string()); const auto fileSize = static_cast(file.tellg()); if (offset > fileSize) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA; - const std::uint64_t length = size != 0 ? size : fileSize - offset; - if (length > fileSize - offset) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA; + const std::uint64_t length = fileSize - offset; std::vector bytes(static_cast(length)); file.seekg(offset); if (!file.read(reinterpret_cast(bytes.data()), static_cast(bytes.size()))) throw std::runtime_error(std::string(__func__) + ": read failed for " + host.string()); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp index 97e168908..6b1a69fbb 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp @@ -208,8 +208,13 @@ static void TestParsePcm() { auto truncated = file; truncated.resize(44 + 202); - Require(sceNgs2ParseWaveformData(truncated.data(), truncated.size(), &info) == SCE_NGS2_OK); - Require(info.data_size == 200 && info.num_samples == 50); + Require(sceNgs2ParseWaveformData(truncated.data(), truncated.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); + + auto partialFrame = PcmFile(2, 44100, 16, 100); + partialFrame.resize(44 + 402, 0x11); + const std::uint32_t partialSize = 402; + std::memcpy(partialFrame.data() + 40, &partialSize, sizeof(partialSize)); + Require(sceNgs2ParseWaveformData(partialFrame.data(), partialFrame.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); const auto eightBit = PcmFile(1, 44100, 8, 10); Require(sceNgs2ParseWaveformData(eightBit.data(), eightBit.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); @@ -222,20 +227,19 @@ static void TestParseFile() { const auto path = std::filesystem::temp_directory_path() / "aps5_ngs2_parse_file.wav"; { std::ofstream out(path, std::ios::binary); - out.write("", 3); + const char pad[3]{}; + out.write(pad, sizeof(pad)); out.write(reinterpret_cast(wave.data()), static_cast(wave.size())); } const auto name = path.string(); Ngs2WaveformInfo info{}; - Require(sceNgs2ParseWaveformFile(name.c_str(), 3, 0, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); - Require(sceNgs2ParseWaveformFile(nullptr, 0, 0, &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); - Require(sceNgs2ParseWaveformFile(name.c_str(), 3, 0, &info) == SCE_NGS2_OK); + Require(sceNgs2ParseWaveformFile(name.c_str(), 3, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2ParseWaveformFile(nullptr, 0, &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); + Require(sceNgs2ParseWaveformFile(name.c_str(), 3, &info) == SCE_NGS2_OK); Require(info.format.num_channels == 1 && info.format.sample_rate == 22050 && info.num_samples == 64); Require(info.data_offset == 3 + 44 && info.block[0].data_offset == 3 + 44 && info.data_size == 128); - Require(sceNgs2ParseWaveformFile(name.c_str(), 3, static_cast(wave.size()), &info) == SCE_NGS2_OK); - Require(sceNgs2ParseWaveformFile(name.c_str(), 3, static_cast(wave.size()) + 1, &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); - Require(sceNgs2ParseWaveformFile(name.c_str(), static_cast(wave.size()) + 4, 0, &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); - Require(sceNgs2ParseWaveformFile(name.c_str(), 0, 0, &info) == SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT); + Require(sceNgs2ParseWaveformFile(name.c_str(), static_cast(wave.size()) + 4, &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); + Require(sceNgs2ParseWaveformFile(name.c_str(), 0, &info) == SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT); std::filesystem::remove(path); } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp index 3018a2f4a..3f48fb8c9 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp @@ -10,7 +10,7 @@ extern "C" { int APS5_VABI sceNgs2ParseWaveformData(const void*, size_t, Ngs2WaveformInfo*); -int APS5_VABI sceNgs2ParseWaveformFile(const char*, uint32_t, uint32_t, Ngs2WaveformInfo*); +int APS5_VABI sceNgs2ParseWaveformFile(const char*, uint32_t, Ngs2WaveformInfo*); int APS5_VABI sceNgs2CalcWaveformBlock(const Ngs2WaveformFormat*, uint32_t, uint32_t, Ngs2WaveformBlock*); int APS5_VABI sceNgs2SystemResetOption(Ngs2SystemOption*); int APS5_VABI sceNgs2SystemQueryBufferSize(const Ngs2SystemOption*, Ngs2ContextBufferInfo*); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 94c2af7bc..08fa27d5c 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -150,7 +150,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceNgs2RackGetInfo](../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - field order from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2RackInfo` with the 64-byte name of the other info structures (216 bytes); `type` is assumed to be the rack id divided by 0x1000, `min_grain_samples` 64 (the system minimum) and `state_flags` 1; `uid` comes from a per-process rack counter, `render_count` is the owning system's render count and `active_voice_count` counts voices that are not empty (paused and stopped voices included). The last process ratio and tick and `active_channel_work_count` are left zero - [sceNgs2VoiceQueryInfo](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - only info id 0x4001 is known: PPSA22520 passes an 8-byte buffer and branches on the first word being 1 or 2, so it is assumed to be the voice channel count (0 before the voice is set up); the second word is zeroed and other ids throw - [sceNgs2VoiceGetPortInfo](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - layout from shadPS4; the delay and destination input are zero because ports have neither yet -- [sceNgs2ParseWaveformFile](../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature is assumed to be `(path, offset, size, info)` with a zero size meaning the rest of the file, and the data and block offsets are taken to be relative to the start of the file, not of the requested range; an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame, and the info of looped PCM and of other PCM formats throw +- [sceNgs2ParseWaveformFile](../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature `(path, offset, info)` is taken from the call site of PPSA21402, which sets only three argument registers; the file is read from `offset` to its end, the data and block offsets are taken to be relative to the start of the file, not of `offset`, and an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame; `INVALID_WAVEFORM_DATA` for a `data` chunk that runs past the buffer or ends inside a frame is assumed, and looped PCM and other PCM formats throw - [sceSaveDataInitialize3](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a repeated initialize succeeds: PPSA12544's executable and its Unity SaveData plugin both initialize, and the plugin fails on any error. Initializations are counted and `sceSaveDataTerminate` ends the session at the last one; how the console pairs them is unknown - [sceVoiceSetMuteFlag](../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - only shadPS4 (PS4) has bodies. A null tick returns `INVALID_POINTER` like the existing `sceRtcFormatRFC3339`, where shadPS4 formats the current time instead; offsets outside ±1439 minutes return `INVALID_VALUE` like `sceRtcFormatRFC3339`; the local-time variants use the host's offset at the given instant (shadPS4 uses `sceKernelGettimezone`, the current offset) From 1bdc37bb86e28598f448edc64e57680efc8b7636 Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Tue, 6 Oct 2026 22:07:24 -0400 Subject: [PATCH 3/5] test(libSceNgs2.native): write the ParseWaveformFile test file where the guest path resolves On Linux every absolute guest path is resolved under the working directory, so the temporary file in the host temp directory was not found. The test now asks ResolvePath_nid_no_patch for the host path of a guest path and writes the file there. --- .../libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp index 6b1a69fbb..27c255202 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp @@ -1,6 +1,7 @@ #include "Ngs2Test.hpp" #include "libatrac9.h" +#include "prx/libc/include/General.hpp" #include #include @@ -224,22 +225,22 @@ static void TestParsePcm() { static void TestParseFile() { const auto wave = PcmFile(1, 22050, 16, 64); - const auto path = std::filesystem::temp_directory_path() / "aps5_ngs2_parse_file.wav"; + const char* const guestPath = "/aps5_ngs2_parse_file.wav"; + const auto path = ResolvePath_nid_no_patch(guestPath); { std::ofstream out(path, std::ios::binary); const char pad[3]{}; out.write(pad, sizeof(pad)); out.write(reinterpret_cast(wave.data()), static_cast(wave.size())); } - const auto name = path.string(); Ngs2WaveformInfo info{}; - Require(sceNgs2ParseWaveformFile(name.c_str(), 3, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2ParseWaveformFile(guestPath, 3, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); Require(sceNgs2ParseWaveformFile(nullptr, 0, &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); - Require(sceNgs2ParseWaveformFile(name.c_str(), 3, &info) == SCE_NGS2_OK); + Require(sceNgs2ParseWaveformFile(guestPath, 3, &info) == SCE_NGS2_OK); Require(info.format.num_channels == 1 && info.format.sample_rate == 22050 && info.num_samples == 64); Require(info.data_offset == 3 + 44 && info.block[0].data_offset == 3 + 44 && info.data_size == 128); - Require(sceNgs2ParseWaveformFile(name.c_str(), static_cast(wave.size()) + 4, &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); - Require(sceNgs2ParseWaveformFile(name.c_str(), 0, &info) == SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT); + Require(sceNgs2ParseWaveformFile(guestPath, static_cast(wave.size()) + 4, &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); + Require(sceNgs2ParseWaveformFile(guestPath, 0, &info) == SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT); std::filesystem::remove(path); } From 02166268ea3623736afc99f67591316d1b23ffd2 Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Wed, 7 Oct 2026 09:49:31 -0400 Subject: [PATCH 4/5] test(libSceNgs2.native): run the waveform parse tests in their own executable On Linux ngs2_atrac9 aborted without any output once TestParseFile was part of its main(). The exception that TestCorruptSuperframeAtPageEnd throws in the Ngs2 module and catches in main() has to resume through a cleanup landing pad inside main(). That resume goes through the unwinder statically linked into the test executable, which then fails with _URC_FATAL_PHASE2_ERROR and aborts silently. Whether main() gets such a pad depends on what the compiler inlines into it, so adding any function with non-trivial cleanups to that main() is enough to trigger it. Move the PCM and file parse tests into a new ngs2_parse executable and restore Ngs2Atrac9.cpp to its upstream content, so the test that catches exceptions is unchanged. The new executable has no catch and never relies on that path. --- .../libs/prx/libSceNgs2.native/CMakeLists.txt | 2 +- .../libSceNgs2.native/tests/Ngs2Atrac9.cpp | 70 -------------- .../prx/libSceNgs2.native/tests/Ngs2Parse.cpp | 94 +++++++++++++++++++ 3 files changed, 95 insertions(+), 71 deletions(-) create mode 100644 core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp diff --git a/core/libs/prx/libSceNgs2.native/CMakeLists.txt b/core/libs/prx/libSceNgs2.native/CMakeLists.txt index b9ed2af05..b212fc733 100644 --- a/core/libs/prx/libSceNgs2.native/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2.native/CMakeLists.txt @@ -8,7 +8,7 @@ if(MINGW) endif() if(BUILD_TESTING) - foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom) + foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Parse) string(TOLOWER ${ngs2Test} testName) add_test_executable(ngs2_${testName}_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Ngs2${ngs2Test}.cpp) target_include_directories(ngs2_${testName}_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp index 27c255202..739d8ae3c 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp @@ -1,12 +1,9 @@ #include "Ngs2Test.hpp" #include "libatrac9.h" -#include "prx/libc/include/General.hpp" #include #include -#include -#include #include #include #include @@ -179,71 +176,6 @@ static void TestParse() { Require(sceNgs2ParseWaveformData(wrongRate.data(), wrongRate.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); } -static std::vector PcmFile(std::uint32_t channels, std::uint32_t sampleRate, std::uint32_t bits, std::uint32_t frames) { - const std::uint32_t frameBytes = channels * bits / 8; - std::vector file; - PutTag(file, "RIFF"); - Put32(file, 36 + frames * frameBytes); - PutTag(file, "WAVE"); - PutTag(file, "fmt "); - Put32(file, 16); - Put16(file, 1); - Put16(file, channels); - Put32(file, sampleRate); - Put32(file, sampleRate * frameBytes); - Put16(file, frameBytes); - Put16(file, bits); - PutTag(file, "data"); - Put32(file, frames * frameBytes); - file.resize(file.size() + frames * frameBytes, 0x11); - return file; -} - -static void TestParsePcm() { - const auto file = PcmFile(2, 44100, 16, 100); - Ngs2WaveformInfo info{}; - Require(sceNgs2ParseWaveformData(file.data(), file.size(), &info) == SCE_NGS2_OK); - Require(info.format.waveform_type == SCE_NGS2_WAVEFORM_TYPE_PCM_I16L && info.format.num_channels == 2 && info.format.sample_rate == 44100); - Require(info.data_offset == 44 && info.data_size == 400 && info.num_samples == 100); - Require(info.num_blocks == 1 && info.block[0].data_offset == 44 && info.block[0].data_size == 400 && info.block[0].num_samples == 100); - - auto truncated = file; - truncated.resize(44 + 202); - Require(sceNgs2ParseWaveformData(truncated.data(), truncated.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); - - auto partialFrame = PcmFile(2, 44100, 16, 100); - partialFrame.resize(44 + 402, 0x11); - const std::uint32_t partialSize = 402; - std::memcpy(partialFrame.data() + 40, &partialSize, sizeof(partialSize)); - Require(sceNgs2ParseWaveformData(partialFrame.data(), partialFrame.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); - - const auto eightBit = PcmFile(1, 44100, 8, 10); - Require(sceNgs2ParseWaveformData(eightBit.data(), eightBit.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); - const auto noChannels = PcmFile(0, 44100, 16, 10); - Require(sceNgs2ParseWaveformData(noChannels.data(), noChannels.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); -} - -static void TestParseFile() { - const auto wave = PcmFile(1, 22050, 16, 64); - const char* const guestPath = "/aps5_ngs2_parse_file.wav"; - const auto path = ResolvePath_nid_no_patch(guestPath); - { - std::ofstream out(path, std::ios::binary); - const char pad[3]{}; - out.write(pad, sizeof(pad)); - out.write(reinterpret_cast(wave.data()), static_cast(wave.size())); - } - Ngs2WaveformInfo info{}; - Require(sceNgs2ParseWaveformFile(guestPath, 3, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); - Require(sceNgs2ParseWaveformFile(nullptr, 0, &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); - Require(sceNgs2ParseWaveformFile(guestPath, 3, &info) == SCE_NGS2_OK); - Require(info.format.num_channels == 1 && info.format.sample_rate == 22050 && info.num_samples == 64); - Require(info.data_offset == 3 + 44 && info.block[0].data_offset == 3 + 44 && info.data_size == 128); - Require(sceNgs2ParseWaveformFile(guestPath, static_cast(wave.size()) + 4, &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); - Require(sceNgs2ParseWaveformFile(guestPath, 0, &info) == SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT); - std::filesystem::remove(path); -} - static void TestCalcBlock() { const Ngs2WaveformFormat format{SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 1, 48000, Config, 0, 0}; Ngs2WaveformBlock block{}; @@ -303,8 +235,6 @@ int main() { TestSkipAndBlockEnd(reference); TestRepeatAndState(reference); TestParse(); - TestParsePcm(); - TestParseFile(); TestCalcBlock(); TestCorruptSuperframeAtPageEnd(); return 0; diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp new file mode 100644 index 000000000..396881d46 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp @@ -0,0 +1,94 @@ +#include "Ngs2Test.hpp" + +#include "prx/libc/include/General.hpp" + +#include +#include +#include +#include +#include + +static void Put16(std::vector& out, std::uint32_t value) { + out.push_back(static_cast(value)); + out.push_back(static_cast(value >> 8)); +} + +static void Put32(std::vector& out, std::uint32_t value) { + Put16(out, value & 0xffff); + Put16(out, value >> 16); +} + +static void PutTag(std::vector& out, const char* tag) { + out.insert(out.end(), tag, tag + 4); +} + +static std::vector PcmFile(std::uint32_t channels, std::uint32_t sampleRate, std::uint32_t bits, std::uint32_t frames) { + const std::uint32_t frameBytes = channels * bits / 8; + std::vector file; + PutTag(file, "RIFF"); + Put32(file, 36 + frames * frameBytes); + PutTag(file, "WAVE"); + PutTag(file, "fmt "); + Put32(file, 16); + Put16(file, 1); + Put16(file, channels); + Put32(file, sampleRate); + Put32(file, sampleRate * frameBytes); + Put16(file, frameBytes); + Put16(file, bits); + PutTag(file, "data"); + Put32(file, frames * frameBytes); + file.resize(file.size() + frames * frameBytes, 0x11); + return file; +} + +static void TestParsePcm() { + const auto file = PcmFile(2, 44100, 16, 100); + Ngs2WaveformInfo info{}; + Require(sceNgs2ParseWaveformData(file.data(), file.size(), &info) == SCE_NGS2_OK); + Require(info.format.waveform_type == SCE_NGS2_WAVEFORM_TYPE_PCM_I16L && info.format.num_channels == 2 && info.format.sample_rate == 44100); + Require(info.data_offset == 44 && info.data_size == 400 && info.num_samples == 100); + Require(info.num_blocks == 1 && info.block[0].data_offset == 44 && info.block[0].data_size == 400 && info.block[0].num_samples == 100); + + auto truncated = file; + truncated.resize(44 + 202); + Require(sceNgs2ParseWaveformData(truncated.data(), truncated.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); + + auto partialFrame = PcmFile(2, 44100, 16, 100); + partialFrame.resize(44 + 402, 0x11); + const std::uint32_t partialSize = 402; + std::memcpy(partialFrame.data() + 40, &partialSize, sizeof(partialSize)); + Require(sceNgs2ParseWaveformData(partialFrame.data(), partialFrame.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); + + const auto eightBit = PcmFile(1, 44100, 8, 10); + Require(sceNgs2ParseWaveformData(eightBit.data(), eightBit.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); + const auto noChannels = PcmFile(0, 44100, 16, 10); + Require(sceNgs2ParseWaveformData(noChannels.data(), noChannels.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); +} + +static void TestParseFile() { + const auto wave = PcmFile(1, 22050, 16, 64); + const char* const guestPath = "/aps5_ngs2_parse_file.wav"; + const auto path = ResolvePath_nid_no_patch(guestPath); + { + std::ofstream out(path, std::ios::binary); + const char pad[3]{}; + out.write(pad, sizeof(pad)); + out.write(reinterpret_cast(wave.data()), static_cast(wave.size())); + } + Ngs2WaveformInfo info{}; + Require(sceNgs2ParseWaveformFile(guestPath, 3, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2ParseWaveformFile(nullptr, 0, &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); + Require(sceNgs2ParseWaveformFile(guestPath, 3, &info) == SCE_NGS2_OK); + Require(info.format.num_channels == 1 && info.format.sample_rate == 22050 && info.num_samples == 64); + Require(info.data_offset == 3 + 44 && info.block[0].data_offset == 3 + 44 && info.data_size == 128); + Require(sceNgs2ParseWaveformFile(guestPath, static_cast(wave.size()) + 4, &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); + Require(sceNgs2ParseWaveformFile(guestPath, 0, &info) == SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT); + std::filesystem::remove(path); +} + +int main() { + TestParsePcm(); + TestParseFile(); + return 0; +}