Merge pull request #1020 from gjonn/feat/ngs2-parse-waveform-file

feat(libSceNgs2.native): implement sceNgs2ParseWaveformFile and parse 16-bit PCM waveforms
This commit is contained in:
Dean 2026-10-08 09:21:16 +13:00 committed by GitHub
commit 76543c2e89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 151 additions and 2 deletions

View File

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

View File

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

View File

@ -2,15 +2,19 @@
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <limits>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
#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,34 @@ 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");
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<std::uint32_t>(chunks.dataOffset);
info.data_size = static_cast<std::uint32_t>(dataSize);
info.num_samples = static_cast<std::uint32_t>(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 +229,30 @@ 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, 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<std::uint64_t>(file.tellg());
if (offset > fileSize) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA;
const std::uint64_t length = fileSize - offset;
std::vector<std::uint8_t> bytes(static_cast<std::size_t>(length));
file.seekg(offset);
if (!file.read(reinterpret_cast<char*>(bytes.data()), static_cast<std::streamsize>(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 = {};

View File

@ -0,0 +1,94 @@
#include "Ngs2Test.hpp"
#include "prx/libc/include/General.hpp"
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <vector>
static void Put16(std::vector<std::uint8_t>& out, std::uint32_t value) {
out.push_back(static_cast<std::uint8_t>(value));
out.push_back(static_cast<std::uint8_t>(value >> 8));
}
static void Put32(std::vector<std::uint8_t>& out, std::uint32_t value) {
Put16(out, value & 0xffff);
Put16(out, value >> 16);
}
static void PutTag(std::vector<std::uint8_t>& out, const char* tag) {
out.insert(out.end(), tag, tag + 4);
}
static std::vector<std::uint8_t> 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<std::uint8_t> 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<const char*>(wave.data()), static_cast<std::streamsize>(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<std::uint32_t>(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;
}

View File

@ -10,6 +10,7 @@
extern "C" {
int APS5_VABI sceNgs2ParseWaveformData(const void*, size_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*);

View File

@ -172,6 +172,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 `(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
- [sceTextToSpeech2GetSystemStatus](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) (libSceTextToSpeech2) - unknown signature