From ccc5cc111f9941dc9483bf3fd143f876a87e5474 Mon Sep 17 00:00:00 2001 From: ElysiaAlarien <10030248+ElysiaAlarien@users.noreply.github.com> Date: Sun, 25 Oct 2026 08:24:56 -0400 Subject: [PATCH] feat(ngs2): decode complete HE-VAG waveform blocks Recognize VAGp waveforms in sceNgs2ParseWaveformData and sceNgs2ParseWaveformFile and let the sampler play them. Complete blocks decode to PCM16 with vgmstream's psv_decoder.c, from the vgmstream submodule already in the tree, before the existing renderer applies pitch, repeats and mixing. Embedded loops, streaming append, dataless blocks and nonzero configuration data throw. Rebased onto current main from #5797: the submodule and packaging changes are dropped because main already carries them, and the debt entry no longer says VAG block calculation throws. Co-Authored-By: Mohd Faour Co-Authored-By: Claude Sonnet 5.5 --- .../libs/prx/libSceNgs2.native/CMakeLists.txt | 2 +- .../prx/libSceNgs2.native/Ngs2Library.cmake | 11 +- .../libSceNgs2.native/include/Ngs2Types.hpp | 1 + .../libs/prx/libSceNgs2.native/src/Atrac9.cpp | 13 +- core/libs/prx/libSceNgs2.native/src/Hevag.cpp | 83 ++++++++++ .../libSceNgs2.native/src/Ngs2Internal.hpp | 3 + .../libs/prx/libSceNgs2.native/src/Render.cpp | 12 +- core/libs/prx/libSceNgs2.native/src/Voice.cpp | 18 ++- .../prx/libSceNgs2.native/tests/Ngs2Hevag.cpp | 151 ++++++++++++++++++ docs/dev/debt/libSceNgs2.native.md | 1 + 10 files changed, 285 insertions(+), 10 deletions(-) create mode 100644 core/libs/prx/libSceNgs2.native/src/Hevag.cpp create mode 100644 core/libs/prx/libSceNgs2.native/tests/Ngs2Hevag.cpp diff --git a/core/libs/prx/libSceNgs2.native/CMakeLists.txt b/core/libs/prx/libSceNgs2.native/CMakeLists.txt index fddd6277abad..576778b22baa 100644 --- a/core/libs/prx/libSceNgs2.native/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2.native/CMakeLists.txt @@ -2,7 +2,7 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/Ngs2Library.cmake) add_sce_ngs2_library(libSceNgs2.native) if(BUILD_TESTING) - foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Parse Stream UserFx Report Geom VoiceIndex HandleErrors VoiceHandles VoiceEvents) + foreach(ngs2Test IN ITEMS Render Atrac9 Hevag Filter Custom Parse Stream UserFx Report Geom VoiceIndex HandleErrors VoiceHandles VoiceEvents) 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/Ngs2Library.cmake b/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake index b89226f992af..3985b7720c0a 100644 --- a/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake +++ b/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake @@ -1,7 +1,14 @@ +enable_language(C) + function(add_sce_ngs2_library target) + if(NOT TARGET ngs2_hevag) + add_library(ngs2_hevag STATIC ${CMAKE_SOURCE_DIR}/3rdparty/vgmstream/src/coding/psv_decoder.c) + set_target_properties(ngs2_hevag PROPERTIES POSITION_INDEPENDENT_CODE ON C_VISIBILITY_PRESET hidden) + endif() set(ngs2Dir ${CMAKE_CURRENT_FUNCTION_LIST_DIR}) add_library(${target} SHARED EXCLUDE_FROM_ALL ${ngs2Dir}/src/Atrac9.cpp + ${ngs2Dir}/src/Hevag.cpp ${ngs2Dir}/src/Custom.cpp ${ngs2Dir}/src/Geom.cpp ${ngs2Dir}/src/Pan.cpp @@ -11,8 +18,8 @@ function(add_sce_ngs2_library target) ${ngs2Dir}/src/System.cpp ${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 libc libkernel) + target_include_directories(${target} PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src ${CMAKE_SOURCE_DIR}/3rdparty/vgmstream/src) + target_link_libraries(${target} PRIVATE atrac9 ngs2_hevag 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/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index 04b86aa717d2..f2759fe1860b 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -50,6 +50,7 @@ static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_I16L = 0x12; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_I16B = 0x13; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_F32L = 0x18; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_F32B = 0x19; +static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_VAG = 0x1c; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_ATRAC9 = 0x40; static constexpr std::uint32_t SCE_NGS2_VOICE_EVENT_PLAY = 1; diff --git a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp index ecebdfed40f8..8e6a291f3bd3 100644 --- a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp @@ -26,7 +26,6 @@ static constexpr std::size_t FACT_ATRAC9_SIZE = 12; static constexpr std::uint32_t MAX_SAMPLE_RATE = 192000; static constexpr std::uint32_t WAVEFORM_TYPE_PCM_FIRST = 0x10; static constexpr std::uint32_t WAVEFORM_TYPE_PCM_LAST = 0x1b; -static constexpr std::uint32_t WAVEFORM_TYPE_VAG = 0x1c; static constexpr std::uint32_t VAG_FRAME_SAMPLES = 28; static constexpr std::uint32_t ATRAC9_FRAME_SAMPLES[16] = {0, 64, 0, 0, 128, 0, 0, 256, 0, 0, 0, 0, 0, 0, 0, 0}; static constexpr std::uint32_t PCM_BYTES_PER_SAMPLE[] = {1, 1, 2, 2, 3, 3, 4, 4, 4, 4, 8, 8}; @@ -411,6 +410,7 @@ int APS5_VABI sceNgs2ParseWaveformData(const void* data, size_t data_size, Ngs2W *info = {}; if (data == nullptr) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA; const auto* bytes = static_cast(data); + if (data_size >= 4 && std::memcmp(bytes, "VAGp", 4) == 0) return Ngs2ParseHevag(bytes, data_size, *info); RiffChunks chunks; const int result = ReadChunks(data_size, [bytes](std::size_t at, std::size_t) { return bytes + at; }, chunks); if (result != SCE_NGS2_OK) return result; @@ -436,8 +436,13 @@ int APS5_VABI sceNgs2ParseWaveformFile(const char* path, uint32_t offset, Ngs2Wa return static_cast(piece.data()); }; RiffChunks chunks; - int result = ReadChunks(length, readBytes, chunks); - if (result == SCE_NGS2_OK) result = ParseWaveform(chunks, length, *info); + int result; + if (length >= 4 && std::memcmp(readBytes(0, 4), "VAGp", 4) == 0) { + result = length < 0x30 ? SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA : Ngs2ParseHevag(readBytes(0, 0x30), length, *info); + } else { + result = ReadChunks(length, readBytes, chunks); + if (result == SCE_NGS2_OK) result = ParseWaveform(chunks, length, *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; @@ -453,7 +458,7 @@ int APS5_VABI sceNgs2CalcWaveformBlock(const Ngs2WaveformFormat* format, uint32_ *block = PcmBlock(type, format->num_channels, sample_pos, num_samples); return SCE_NGS2_OK; } - if (type == WAVEFORM_TYPE_VAG) { + if (type == SCE_NGS2_WAVEFORM_TYPE_VAG) { *block = VagBlock(format->num_channels, sample_pos, num_samples); return SCE_NGS2_OK; } diff --git a/core/libs/prx/libSceNgs2.native/src/Hevag.cpp b/core/libs/prx/libSceNgs2.native/src/Hevag.cpp new file mode 100644 index 000000000000..60aa13d31676 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/src/Hevag.cpp @@ -0,0 +1,83 @@ +#include +#include +#include +#include + +#include "Ngs2Internal.hpp" + +extern "C" { +#include "coding/coding.h" +} + +namespace { + +struct FrameStream { + STREAMFILE stream{}; + const std::uint8_t* frame; +}; + +std::size_t ReadFrame(STREAMFILE* stream, std::uint8_t* destination, offv_t offset, std::size_t size) { + if (offset < 0 || offset >= 16) return 0; + size = std::min(size, 16 - static_cast(offset)); + std::memcpy(destination, reinterpret_cast(stream)->frame + offset, size); + return size; +} + +std::uint32_t ReadBe32(const std::uint8_t* data) { + return (static_cast(data[0]) << 24) | (static_cast(data[1]) << 16) + | (static_cast(data[2]) << 8) | data[3]; +} + +} + +int Ngs2ParseHevag(const std::uint8_t* header, std::size_t size, Ngs2WaveformInfo& info) { + if (size < 0x30) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA; + const auto version = ReadBe32(header + 4); + if (version != 0x20001 && version != 0x30000) return SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT; + if (ReadBe32(header + 0x18) != 0 || (ReadBe32(header + 0x1c) & 0xffff00ffu) != 0) + throw std::runtime_error("NGS2: alternate VAG header layout is not implemented"); + const std::uint32_t channels = header[0x1e] == 0 ? 1 : header[0x1e]; + const auto rate = ReadBe32(header + 0x10); + if (channels > NGS2_MAX_CHANNELS || rate == 0 || rate > 192000) return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; + const auto bytes = ReadBe32(header + 0xc); + const auto frameBytes = 16 * channels; + if (bytes > size - 0x30 || bytes % frameBytes != 0) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA; + const std::uint64_t samples = static_cast(bytes / frameBytes) * 28; + if (samples > std::numeric_limits::max()) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA; + info.format = {SCE_NGS2_WAVEFORM_TYPE_VAG, channels, rate, 0, 0, 0}; + info.data_offset = 0x30; + info.data_size = bytes; + info.num_samples = static_cast(samples); + info.audio_unit_size = frameBytes; + info.num_audio_unit_samples = 28; + info.num_audio_unit_per_frame = 1; + info.audio_frame_size = frameBytes; + info.num_audio_frame_samples = 28; + info.num_blocks = 1; + info.block[0] = {0x30, bytes, 0, 0, info.num_samples, 0, 0}; + return SCE_NGS2_OK; +} + +void Ngs2DecodeHevag(Ngs2Block& block, std::uint32_t channels) { + const std::uint64_t samples = static_cast(block.info.num_skip_samples) + block.info.num_samples; + const auto frames = (samples + 27) / 28; + if (frames > block.info.data_size / (16 * channels)) throw std::invalid_argument("NGS2: truncated HE-VAG block"); + block.decoded.resize(static_cast(frames * 28 * channels)); + std::array histories{}; + FrameStream input{}; + input.stream.read = ReadFrame; + for (std::uint64_t frame = 0; frame < frames; ++frame) { + for (std::uint32_t channel = 0; channel < channels; ++channel) { + input.frame = block.data + (frame * channels + channel) * 16; + const auto predictor = (input.frame[1] & 0xf0) | (input.frame[0] >> 4); + const auto flags = input.frame[1] & 0xf; + if (predictor >= 128 || (input.frame[0] & 0xf) > 12 || flags > 7) + throw std::invalid_argument("NGS2: invalid HE-VAG frame"); + if (flags != 0 && flags != 1 && flags != 7) + throw std::runtime_error("NGS2: HE-VAG embedded loops are not implemented"); + auto& history = histories[channel]; + history.streamfile = &input.stream; + decode_hevag(&history, block.decoded.data() + frame * 28 * channels + channel, static_cast(channels), 0, 28); + } + } +} diff --git a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp index 73270afab9de..9e7305db4112 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -43,6 +43,7 @@ struct Ngs2Block { std::uint64_t availableFrames = 0; std::vector pieces; bool started = false; + std::vector decoded; }; struct Ngs2Atrac9DecoderDeleter { @@ -237,6 +238,8 @@ void Ngs2SetFilter(Ngs2Voice& voice, const Ngs2SamplerVoiceFilterParam& param); std::size_t Ngs2Atrac9BlockBytes(const Ngs2Voice& voice, const Ngs2WaveformBlock& block); void Ngs2RestartAtrac9(Ngs2Voice& voice); void Ngs2ConsumeAtrac9(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t systemRate); +void Ngs2DecodeHevag(Ngs2Block& block, std::uint32_t channels); +int Ngs2ParseHevag(const std::uint8_t* header, std::size_t size, Ngs2WaveformInfo& info); bool Ngs2FinishBlock(Ngs2Voice& voice); int Ngs2CheckCustomRack(std::uint32_t rackId, const Ngs2CustomRackOption& option, std::uint32_t maxInputs); void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option); diff --git a/core/libs/prx/libSceNgs2.native/src/Render.cpp b/core/libs/prx/libSceNgs2.native/src/Render.cpp index 3fb3689e2344..a631a1e25c9e 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -17,6 +17,8 @@ static constexpr std::uint64_t PHASE_FRACTION = 0xffffffff; static const std::uint8_t* FrameAt(const Ngs2Voice& voice, const Ngs2Block& block, std::uint32_t frame) { const std::size_t frameBytes = voice.channels * sizeof(std::int16_t); if (block.data == nullptr) return nullptr; + if (voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_VAG) + return reinterpret_cast(block.decoded.data()) + (static_cast(block.info.num_skip_samples) + frame) * frameBytes; if (block.streaming) { for (const auto& piece : block.pieces) { if (frame >= piece.firstFrame && frame - piece.firstFrame < piece.frames) return piece.data + (frame - piece.firstFrame) * frameBytes; @@ -74,7 +76,8 @@ bool Ngs2FinishBlock(Ngs2Voice& voice) { 0, }; if (!repeat) { - voice.waveformEnd = voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9 ? block.data + block.dataCursor : block.streaming ? Ngs2StreamEnd(voice, block) : FrameAt(voice, block, block.info.num_samples); + voice.waveformEnd = voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_VAG ? (block.data == nullptr ? nullptr : block.data + block.info.data_size) + : voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9 ? block.data + block.dataCursor : block.streaming ? Ngs2StreamEnd(voice, block) : FrameAt(voice, block, block.info.num_samples); voice.blocks.pop_front(); } const auto revision = voice.waveformRevision; @@ -91,9 +94,14 @@ static void Advance(Ngs2Voice& voice, std::uint64_t frames) { const std::uint64_t limit = block.streaming ? std::min(block.info.num_samples, block.availableFrames) : block.info.num_samples; if (block.cursor >= limit && block.cursor != block.info.num_samples && !(block.streaming && !voice.acceptsBlocks)) break; const auto step = std::min(frames, limit - block.cursor); + if (voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_VAG && block.data != nullptr) { + const auto before = block.cursor == 0 ? 0 : (static_cast(block.info.num_skip_samples) + block.cursor + 27) / 28; + const auto after = (static_cast(block.info.num_skip_samples) + block.cursor + step + 27) / 28; + voice.decodedBytes += (after - before) * 16 * voice.channels; + } block.cursor += static_cast(step); voice.decodedSamples += step; - if (voice.waveformType != SCE_NGS2_WAVEFORM_TYPE_ATRAC9) voice.decodedBytes += step * voice.channels * sizeof(std::int16_t); + if (voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_PCM_I16L) voice.decodedBytes += step * voice.channels * sizeof(std::int16_t); frames -= step; if (block.streaming) { while (block.pieces.size() > 1 && block.cursor >= block.pieces.front().firstFrame + block.pieces.front().frames) block.pieces.erase(block.pieces.begin()); diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index add38b4d3f64..23bca68d938a 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -93,6 +93,8 @@ const std::uint8_t* Ngs2StreamEnd(const Ngs2Voice& voice, const Ngs2Block& block const std::uint8_t* Ngs2Voice::WaveformData() const { if (blocks.empty()) return waveformEnd; const auto& block = blocks.front(); + if (waveformType == SCE_NGS2_WAVEFORM_TYPE_VAG) + return block.data + ((static_cast(block.info.num_skip_samples) + block.cursor) / 28) * channels * 16; if (waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9) return block.data + block.dataCursor; if (block.streaming) { for (const auto& piece : block.pieces) { @@ -187,10 +189,13 @@ static void SetupSampler(Ngs2Voice& voice, const Ngs2WaveformFormat& format) { return; } voice.ResetSetup(); - if (format.waveform_type != SCE_NGS2_WAVEFORM_TYPE_PCM_I16L && format.waveform_type != SCE_NGS2_WAVEFORM_TYPE_ATRAC9) { + if (format.waveform_type != SCE_NGS2_WAVEFORM_TYPE_PCM_I16L && format.waveform_type != SCE_NGS2_WAVEFORM_TYPE_ATRAC9 && + format.waveform_type != SCE_NGS2_WAVEFORM_TYPE_VAG) { throw std::runtime_error("NGS2: waveform type " + Ngs2Hex(format.waveform_type) + " is not implemented"); } if (format.frame_offset != 0 || format.frame_margin != 0) throw std::runtime_error("NGS2: waveform frame offset and margin are not implemented"); + if (format.waveform_type == SCE_NGS2_WAVEFORM_TYPE_VAG && format.config_data != 0) + throw std::runtime_error("NGS2: HE-VAG configuration data is not implemented"); if (format.num_channels == 0 || format.num_channels > NGS2_MAX_CHANNELS || format.sample_rate == 0) APS5_INVALID_ARG_EX; voice.channels = format.num_channels; voice.sampleRate = format.sample_rate; @@ -199,6 +204,7 @@ static void SetupSampler(Ngs2Voice& voice, const Ngs2WaveformFormat& format) { } static void AppendStreamData(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBlocksParam& param, std::size_t frameBytes) { + if (voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_VAG) throw std::runtime_error("NGS2: streaming HE-VAG blocks are not implemented"); if (voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9) { const bool pendingWaveform = voice.atrac9.remainingSamples != 0 || std::any_of(voice.blocks.begin(), voice.blocks.end(), [](const Ngs2Block& block) { return block.info.num_samples != 0; }); @@ -251,11 +257,13 @@ static void AddWaveformBlocks(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBl const auto& block = param.blocks[i]; if (block.num_samples == 0 && block.data_size == 0) continue; if (allowsSilence && block.data_size == 0) { + if (voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_VAG) throw std::runtime_error("NGS2: dataless HE-VAG blocks are not implemented"); if (voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9) throw std::runtime_error("NGS2: a dataless waveform block under the silence flag is not implemented for ATRAC9 voices"); voice.blocks.push_back({nullptr, block}); continue; } const std::uint64_t bytes = voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9 ? Ngs2Atrac9BlockBytes(voice, block) + : voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_VAG ? ((static_cast(block.num_skip_samples) + block.num_samples + 27) / 28) * 16 * voice.channels : (static_cast(block.num_skip_samples) + block.num_samples) * frameBytes; const bool streaming = block.num_samples != 0 && bytes > block.data_size && voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_PCM_I16L && block.num_skip_samples == 0 && block.num_repeats == 0 && block.data_size >= frameBytes && block.data_size % frameBytes == 0; @@ -264,6 +272,14 @@ static void AddWaveformBlocks(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBl throw std::invalid_argument("NGS2: waveform block " + std::to_string(i) + " does not fit its data"); } voice.blocks.push_back({static_cast(param.data) + block.data_offset, block}); + if (voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_VAG) { + try { + Ngs2DecodeHevag(voice.blocks.back(), voice.channels); + } catch (...) { + voice.blocks.pop_back(); + throw; + } + } if (streaming) { auto& stream = voice.blocks.back(); stream.streaming = true; diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Hevag.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Hevag.cpp new file mode 100644 index 000000000000..e19ecbfa5afd --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Hevag.cpp @@ -0,0 +1,151 @@ +#include "Ngs2Test.hpp" +#include "prx/libc/include/General.hpp" + +#include +#include +#include +#include +#include + +static void PutBe32(std::vector& data, std::size_t offset, std::uint32_t value) { + for (unsigned i = 0; i < 4; ++i) data[offset + i] = static_cast(value >> (24 - 8 * i)); +} + +static std::vector Wave(std::uint32_t channels, std::uint32_t frames) { + std::vector data(0x30 + channels * frames * 16); + std::memcpy(data.data(), "VAGp", 4); + PutBe32(data, 4, 0x20001); + PutBe32(data, 0xc, channels * frames * 16); + PutBe32(data, 0x10, 48000); + data[0x1e] = static_cast(channels); + for (std::uint32_t frame = 0; frame < frames; ++frame) { + for (std::uint32_t channel = 0; channel < channels; ++channel) { + auto* unit = data.data() + 0x30 + (frame * channels + channel) * 16; + unit[0] = 12; + std::memset(unit + 2, channel == 0 ? 0x21 : 0xfe, 14); + } + } + return data; +} + +static void Parse() { + auto data = Wave(2, 2); + Ngs2WaveformInfo info{}; + Require(sceNgs2ParseWaveformData(data.data(), data.size(), &info) == SCE_NGS2_OK); + Require(info.format.waveform_type == SCE_NGS2_WAVEFORM_TYPE_VAG && info.format.num_channels == 2 && info.format.sample_rate == 48000); + Require(info.data_offset == 0x30 && info.data_size == 64 && info.num_samples == 56); + Require(info.audio_unit_size == 32 && info.num_audio_unit_samples == 28 && info.num_audio_unit_per_frame == 1); + Require(info.audio_frame_size == 32 && info.num_audio_frame_samples == 28 && info.num_blocks == 1); + Require(info.block[0].data_offset == 0x30 && info.block[0].data_size == 64 && info.block[0].num_samples == 56); + for (std::size_t size = 4; size < 0x30; ++size) + Require(sceNgs2ParseWaveformData(data.data(), size, &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); + Require(sceNgs2ParseWaveformData(data.data(), data.size() - 1, &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); + PutBe32(data, 0xc, 63); + Require(sceNgs2ParseWaveformData(data.data(), data.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); + PutBe32(data, 0xc, 64); + data[0x1e] = 9; + Require(sceNgs2ParseWaveformData(data.data(), data.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); + data[0x1e] = 2; + PutBe32(data, 4, 0x30000); + Require(sceNgs2ParseWaveformData(data.data(), data.size(), &info) == SCE_NGS2_OK); + PutBe32(data, 4, 0x20); + Require(sceNgs2ParseWaveformData(data.data(), data.size(), &info) == SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT); + + data = Wave(1, 2); + data[0x1e] = 0; + Require(sceNgs2ParseWaveformData(data.data(), data.size(), &info) == SCE_NGS2_OK && info.format.num_channels == 1); + const char* guestPath = "/aps5_ngs2_hevag.vag"; + const auto path = ResolvePath_nid_no_patch(guestPath); + { + std::ofstream out(path, std::ios::binary); + out.write("pad", 3); + out.write(reinterpret_cast(data.data()), static_cast(data.size())); + } + Require(sceNgs2ParseWaveformFile(guestPath, 3, &info) == SCE_NGS2_OK); + Require(info.data_offset == 0x33 && info.block[0].data_offset == 0x33 && info.num_samples == 56); + std::filesystem::remove(path); +} + +static void Playback() { + auto data = Wave(2, 2); + data[0x51] = 7; + data[0x61] = 7; + Ngs2WaveformInfo parsed{}; + Require(sceNgs2ParseWaveformData(data.data(), data.size(), &parsed) == SCE_NGS2_OK); + const auto system = CreateSystem(); + const auto master = Mastering(system, 2); + const auto sampler = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER)); + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, parsed.format}); + parsed.block[0].num_skip_samples = 3; + parsed.block[0].num_samples = 53; + parsed.block[0].num_repeats = 1; + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, data.data(), 0, 1, parsed.block}); + Patch(sampler, master); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + for (unsigned grain = 0; grain < 14; ++grain) { + std::array output{}; + const Ngs2RenderBufferInfo render{output.data(), sizeof(output), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2}; + Require(sceNgs2SystemRender(system, &render, 1) == SCE_NGS2_OK); + for (unsigned i = 0; i < Grain; ++i) { + const unsigned sample = grain * Grain + i; + const unsigned position = sample % 53 + 3; + Require(output[i * 2] == (sample >= 106 || position >= 28 ? 0.0f : (position % 2 == 0 ? 1.0f : 2.0f) / 32768)); + Require(output[i * 2 + 1] == (sample >= 106 || position >= 28 ? 0.0f : (position % 2 == 0 ? -2.0f : -1.0f) / 32768)); + } + } + Ngs2SamplerVoiceState state{}; + Require(sceNgs2VoiceGetState(sampler, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); + Require(state.num_decoded_samples == 106 && state.decoded_data_size == 128); + Require(state.waveform_data == data.data() + data.size()); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void HistoryAndErrors() { + auto data = Wave(1, 2); + data[0x30] = 0xdc; + data[0x31] = 0x10; + std::memset(data.data() + 0x32, 0x77, 14); + data[0x40] = 0xdc; + data[0x41] = 0x10; + std::memset(data.data() + 0x42, 0x77, 14); + const auto system = CreateSystem(); + const auto master = Mastering(system, 1); + const auto sampler = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER)); + bool configurationThrew = false; + try { + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_VAG, 1, 48000, 1, 0, 0}}); + } catch (const std::exception&) { configurationThrew = true; } + Require(configurationThrew); + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_VAG, 1, 48000, 0, 0, 0}}); + const Ngs2WaveformBlock block{0x30, 32, 0, 0, 56, 0, 0}; + for (const std::uint8_t flags : {std::uint8_t{0x80}, std::uint8_t{0x16}}) { + data[0x31] = flags; + bool threw = false; + try { + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, data.data(), SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET, 1, &block}); + } catch (const std::exception&) { threw = true; } + Require(threw); + } + data[0x31] = 0x10; + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, data.data(), SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET, 1, &block}); + Patch(sampler, master); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + constexpr std::array expected{7, 11, 7, 3, 3, 5, 6, 6}; + std::array output{}; + const Ngs2RenderBufferInfo render{output.data(), sizeof(output), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 1}; + Require(sceNgs2SystemRender(system, &render, 1) == SCE_NGS2_OK); + for (std::size_t i = 0; i < expected.size(); ++i) Require(output[i] == expected[i] / 32768.0f); + for (unsigned grain = 1; grain < 7; ++grain) { + Require(sceNgs2SystemRender(system, &render, 1) == SCE_NGS2_OK); + if (grain >= 2) { + for (float sample : output) Require(sample == 5.0f / 32768); + } + } + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +int main() { + Parse(); + Playback(); + HistoryAndErrors(); +} diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 062ccbe526a7..d56dc852acce 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -2,6 +2,7 @@ ### Unknown function info +- [NGS2 HE-VAG](../../../core/libs/prx/libSceNgs2.native/src/Hevag.cpp) is unverified against a console: playback in PPSA36591 ([#5677](https://github.com/boykopovar/AnyPS5/issues/5677)), decoder rounding, waveform-info fields and byte-position reporting. `VAGp` versions `0x20001` and `0x30000` follow [vgmstream](https://github.com/vgmstream/vgmstream/blob/7c520e8f9b078dfde2f84f1b5bcbad0c6c415806/src/meta/vag.c), type `0x1c` follows [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Ngs2/Ngs2WaveformParser.cs). Complete blocks decode to cached PCM16 with `psv_decoder.c`; history restarts at zero for each submitted block. Embedded loops, streaming append, dataless blocks, nonzero configuration data and alternate headers throw; other versions return `SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT`. Cached samples ignore later guest edits. - [NGS2 voice validation](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) returns `0x804A8203` for an invalid voice in Control, RunCommands, GetState, GetStateFlags, GetPortInfo and QueryInfo, following the type-4 handle error in [prosper's PS5 module analysis](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp#L4478). Control checks a null parameter list first (`0x804A8360`); GetState resolves the voice before output validation, and GetState rejects sizes below 8 before a null output. Existing rack-specific exact state sizes, other argument precedence and command layouts are retained; arbitrary-sized state copies and full console behavior remain unverified. These values follow module analysis, not new console execution. - [NGS2 system and rack handle errors](../../../core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp) use `0x804A8201` and `0x804A8202`, following the native handle-type error helper recorded in [prosper's PS5 module analysis](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp). [sceNgs2RackGetVoiceHandle](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) resolves the rack before checking the output pointer, matching the export in firmware 1.00 through 12.70 reported in [#5916](https://github.com/boykopovar/AnyPS5/pull/5916#issuecomment-6172832590). Other exports retain their existing validation order; complete invalid-input precedence and console execution remain unverified.