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)