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.