diff --git a/core/libs/prx/libSceNgs2.native/CMakeLists.txt b/core/libs/prx/libSceNgs2.native/CMakeLists.txt index f56d7cb300..2f224450cf 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 PitchShift Parse Stream UserFx Report Geom VoiceIndex HandleErrors VoiceHandles VoiceEvents) + foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom PitchShift Sampler 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 42c1549417..35d1dda724 100644 --- a/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake +++ b/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake @@ -10,10 +10,11 @@ function(add_sce_ngs2_library target) ${ngs2Dir}/src/Rack.cpp ${ngs2Dir}/src/Render.cpp ${ngs2Dir}/src/Reverb.cpp + ${ngs2Dir}/src/Sampler.cpp ${ngs2Dir}/src/System.cpp ${ngs2Dir}/src/Voice.cpp ) - set_property(SOURCE ${ngs2Dir}/src/PitchShift.cpp PROPERTY COMPILE_OPTIONS + set_property(SOURCE ${ngs2Dir}/src/PitchShift.cpp ${ngs2Dir}/src/Sampler.cpp PROPERTY COMPILE_OPTIONS "$<$:-ffp-contract=off>;$<$:/fp:strict>") target_include_directories(${target} PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) target_link_libraries(${target} PRIVATE atrac9 libc libkernel) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index 41fa1e4909..93e8e34dc0 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -490,7 +490,10 @@ static_assert(sizeof(Ngs2WaveformBlock) == 40); struct Ngs2SamplerVoiceSetupParam { Ngs2VoiceParamHeader header; Ngs2WaveformFormat format; + std::uint32_t channel_mask; + std::uint32_t reserved; }; +static_assert(sizeof(Ngs2SamplerVoiceSetupParam) == 40); struct Ngs2SamplerVoiceWaveformBlocksParam { Ngs2VoiceParamHeader header; @@ -503,7 +506,9 @@ struct Ngs2SamplerVoiceWaveformBlocksParam { struct Ngs2SamplerVoicePitchParam { Ngs2VoiceParamHeader header; float ratio; + std::uint32_t reserved; }; +static_assert(sizeof(Ngs2SamplerVoicePitchParam) == 16); struct Ngs2SamplerVoiceFilterParam { Ngs2VoiceParamHeader header; diff --git a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp index dda42ea297..80c4872534 100644 --- a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp @@ -202,7 +202,7 @@ static bool DecodeSuperframe(Ngs2Voice& voice) { static bool EnsureFrames(Ngs2Voice& voice, std::size_t count) { auto& atrac9 = voice.atrac9; - while (voice.state == Ngs2PlayState::Playing && BufferedFrames(voice) < count) { + while ((voice.state == Ngs2PlayState::Playing || voice.state == Ngs2PlayState::Paused || voice.outputTransition == Ngs2VoiceTransition::Stop) && BufferedFrames(voice) < count) { if (atrac9.finishOnDrain) { if (BufferedFrames(voice) != 0) return false; atrac9.finishOnDrain = false; @@ -210,47 +210,26 @@ static bool EnsureFrames(Ngs2Voice& voice, std::size_t count) { } if (!DecodeSuperframe(voice)) return false; } - return voice.state == Ngs2PlayState::Playing; + return voice.state == Ngs2PlayState::Playing || voice.state == Ngs2PlayState::Paused || voice.outputTransition == Ngs2VoiceTransition::Stop; } -static bool AdvanceAtrac9(Ngs2Voice& voice) { - while ((voice.phase >> 32) != 0 && voice.state == Ngs2PlayState::Playing) { - if (!EnsureFrames(voice, 1)) return false; - const auto count = std::min(voice.phase >> 32, BufferedFrames(voice)); - voice.atrac9.windowCursor += static_cast(count); - voice.decodedSamples += count; - voice.phase -= count << 32; - if (BufferedFrames(voice) == 0 && voice.atrac9.finishOnDrain) { - voice.atrac9.finishOnDrain = false; - if (!Ngs2FinishBlock(voice)) return false; - } - } - return voice.state == Ngs2PlayState::Playing; -} - -void Ngs2ConsumeAtrac9(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t systemRate) { - constexpr double phaseOne = 4294967296.0; - const auto initialRevision = voice.waveformRevision; - const auto step = static_cast(std::llround(voice.sampleRate * static_cast(voice.pitch) / systemRate * phaseOne)); +std::uint32_t Ngs2ReadSamplerAtrac9(Ngs2Voice& voice, float* dest, std::uint32_t stride, std::uint32_t count) { + const auto revision = voice.waveformRevision; std::uint32_t written = 0; - for (; written < grain; ++written) { - if (!AdvanceAtrac9(voice) || !EnsureFrames(voice, 1)) break; - const auto revision = voice.waveformRevision; - const auto fraction = static_cast(voice.phase / phaseOne); - if (fraction != 0.0f) EnsureFrames(voice, 2); - if (voice.state != Ngs2PlayState::Playing || voice.waveformRevision != revision) break; - const auto* current = voice.atrac9.window.data() + static_cast(voice.atrac9.windowCursor) * voice.channels; - const auto* next = BufferedFrames(voice) > 1 ? current + voice.channels : current; - for (std::uint32_t channel = 0; channel < voice.channels; ++channel) - voice.samples[static_cast(channel) * grain + written] = std::lerp(current[channel], next[channel], fraction); - voice.phase += step; + while (written < count && (voice.state == Ngs2PlayState::Playing || voice.state == Ngs2PlayState::Paused || voice.outputTransition == Ngs2VoiceTransition::Stop) && voice.waveformRevision == revision) { + if (!EnsureFrames(voice, 1) || voice.waveformRevision != revision) break; + const auto available = std::min(count - written, BufferedFrames(voice)); + const auto* source = voice.atrac9.window.data() + static_cast(voice.atrac9.windowCursor) * voice.channels; + for (std::size_t frame = 0; frame < available; ++frame) { + for (std::uint32_t channel = 0; channel < voice.channels; ++channel) { + dest[static_cast(channel) * stride + written + frame] = source[frame * voice.channels + channel]; + } + } + written += static_cast(available); + voice.atrac9.windowCursor += static_cast(available); + voice.decodedSamples += available; } - voice.hasSamples = written != 0; - if (voice.state != Ngs2PlayState::Playing || voice.waveformRevision != initialRevision) return; - AdvanceAtrac9(voice); - if (voice.state != Ngs2PlayState::Playing || voice.waveformRevision != initialRevision) return; - if (!voice.acceptsBlocks && voice.blocks.empty() && BufferedFrames(voice) == 0 && voice.atrac9.remainingSamples != 0) - throw std::invalid_argument("NGS2: the ATRAC9 stream ends before its declared samples"); + return written; } static std::uint16_t ReadLe16(const std::uint8_t* data) { diff --git a/core/libs/prx/libSceNgs2.native/src/Custom.cpp b/core/libs/prx/libSceNgs2.native/src/Custom.cpp index 834f0a562b..6047d21d20 100644 --- a/core/libs/prx/libSceNgs2.native/src/Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Custom.cpp @@ -97,7 +97,6 @@ int Ngs2CheckCustomRack(std::uint32_t rackId, const Ngs2CustomRackOption& option if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_GAIN && option.module[m].option != nullptr) throw std::runtime_error("NGS2: an option for the custom Gain module is not implemented"); if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_GAIN && rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) continue; if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_PITCH_SHIFT) { - if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::runtime_error("NGS2: PitchShift on a custom sampler rack is not implemented"); continue; } if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_GAIN) throw std::runtime_error("NGS2: the custom Gain module on a custom sampler rack is not implemented"); diff --git a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp index d07f0273aa..38e5aec370 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -126,6 +126,10 @@ struct Ngs2Voice { std::unique_ptr reverb; float pitch = 1.0f; std::uint64_t phase = 0; + std::array, NGS2_MAX_CHANNELS> samplerHistory{}; + std::vector samplerScratch; + std::vector samplerReplayBlocks; + bool samplerReplayKnown = true; std::deque blocks; bool acceptsBlocks = true; std::uint64_t decodedSamples = 0; @@ -145,8 +149,7 @@ struct Ngs2Voice { std::vector customUserFx; std::vector customGain; std::vector customPitchShift; - Ngs2PitchShiftTransition pitchShiftTransition = Ngs2PitchShiftTransition::None; - bool pitchShiftPlayed = false; + Ngs2VoiceTransition outputTransition = Ngs2VoiceTransition::None; Ngs2UserFxProcessHandler userFxHandler = nullptr; std::array userFxData{}; std::uint32_t userFxFlags = 0; @@ -157,6 +160,7 @@ struct Ngs2Voice { void SetEvent(std::uint32_t eventId); void ResetSetup(); + bool IsSampler() const; const std::uint8_t* WaveformData() const; std::uint32_t InputChannels() const { return inputChannels != 0 ? inputChannels : channels; } }; @@ -170,6 +174,7 @@ struct Ngs2Rack { std::uint32_t maxFilters = 0; std::uint32_t maxGrainSamples = 0; std::uint32_t maxChannelWorks = 0; + std::uint32_t maxWaveformBlocks = 0; std::uint32_t maxInputs = 0; std::uintptr_t userData = 0; Ngs2ContextBufferInfo bufferInfo{}; @@ -242,10 +247,16 @@ void Ngs2SetupAtrac9(Ngs2Voice& voice, const Ngs2WaveformFormat& format); 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); bool Ngs2FinishBlock(Ngs2Voice& voice); int Ngs2CheckCustomRack(std::uint32_t rackId, const Ngs2CustomRackOption& option, std::uint32_t maxInputs); void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option); +void Ngs2PrepareSampler(Ngs2Voice& voice); +void Ngs2RestartSampler(Ngs2Voice& voice); +void Ngs2ReplaySampler(Ngs2Voice& voice); +void Ngs2SaveSamplerBlock(Ngs2Voice& voice, const Ngs2Block& block); +void Ngs2ConsumeSampler(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate); +std::uint32_t Ngs2ReadSamplerPcm(Ngs2Voice& voice, float* dest, std::uint32_t stride, std::uint32_t count); +std::uint32_t Ngs2ReadSamplerAtrac9(Ngs2Voice& voice, float* dest, std::uint32_t stride, std::uint32_t count); void Ngs2CleanupUserFx(Ngs2Rack& rack); void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param); void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate); diff --git a/core/libs/prx/libSceNgs2.native/src/PitchShift.cpp b/core/libs/prx/libSceNgs2.native/src/PitchShift.cpp index 3823e717e6..b47d0c8e93 100644 --- a/core/libs/prx/libSceNgs2.native/src/PitchShift.cpp +++ b/core/libs/prx/libSceNgs2.native/src/PitchShift.cpp @@ -208,8 +208,7 @@ void Ngs2ResetPitchShift(Ngs2Voice& voice) { state.changed = false; state.initializedChannels = 0; } - voice.pitchShiftTransition = Ngs2PitchShiftTransition::None; - voice.pitchShiftPlayed = false; + voice.outputTransition = Ngs2VoiceTransition::None; } void Ngs2RestartPitchShift(Ngs2Voice& voice) { @@ -241,20 +240,20 @@ void Ngs2ProcessPitchShift(Ngs2Voice& voice, std::size_t module, std::uint32_t g voice.hasSamples = std::any_of(voice.samples.begin(), voice.samples.end(), [](float sample) { return sample != 0.0f; }); } -void Ngs2ApplyPitchShiftGain(Ngs2Voice& voice, std::uint32_t grain) { - if (!Ngs2HasPitchShift(voice)) return; - const auto transition = voice.pitchShiftTransition; - if (transition == Ngs2PitchShiftTransition::None && voice.state != Ngs2PlayState::Paused) return; - if (transition != Ngs2PitchShiftTransition::None && grain < 64) throw std::runtime_error("NGS2: PitchShift voice transitions require at least 64 samples"); +void Ngs2ApplyVoiceGain(Ngs2Voice& voice, std::uint32_t grain) { + if (!Ngs2HasPitchShift(voice) && !voice.IsSampler()) return; + const auto transition = voice.outputTransition; + if (transition == Ngs2VoiceTransition::None && voice.state != Ngs2PlayState::Paused) return; + if (transition != Ngs2VoiceTransition::None && grain < 64) throw std::runtime_error("NGS2: voice transitions require at least 64 samples"); for (std::uint32_t c = 0; c < voice.channels; ++c) { auto* samples = voice.samples.data() + static_cast(c) * grain; for (std::uint32_t i = 0; i < grain; ++i) { float gain = 1.0f; - if (transition == Ngs2PitchShiftTransition::Pause) { + if (transition == Ngs2VoiceTransition::Pause) { if (i >= grain - 64) gain = static_cast(grain - i) / 64.0f; - } else if (transition == Ngs2PitchShiftTransition::Resume) { + } else if (transition == Ngs2VoiceTransition::Resume) { if (i < 64) gain = static_cast(i) / 64.0f; - } else if (transition == Ngs2PitchShiftTransition::Stop) { + } else if (transition == Ngs2VoiceTransition::Stop) { gain = i < 64 ? 1.0f - static_cast(i) / 64.0f : 0.0f; } else if (voice.state == Ngs2PlayState::Paused) { gain = 0.0f; @@ -262,6 +261,6 @@ void Ngs2ApplyPitchShiftGain(Ngs2Voice& voice, std::uint32_t grain) { samples[i] *= gain; } } - voice.pitchShiftTransition = Ngs2PitchShiftTransition::None; + voice.outputTransition = Ngs2VoiceTransition::None; voice.hasSamples = std::any_of(voice.samples.begin(), voice.samples.end(), [](float sample) { return sample != 0.0f; }); } diff --git a/core/libs/prx/libSceNgs2.native/src/PitchShift.hpp b/core/libs/prx/libSceNgs2.native/src/PitchShift.hpp index 655286e473..007ce4eed8 100644 --- a/core/libs/prx/libSceNgs2.native/src/PitchShift.hpp +++ b/core/libs/prx/libSceNgs2.native/src/PitchShift.hpp @@ -34,13 +34,13 @@ struct Ngs2PitchShift { std::uint32_t initializedChannels = 0; }; -enum class Ngs2PitchShiftTransition { None, Pause, Resume, Stop }; +enum class Ngs2VoiceTransition { None, Pause, Resume, Stop }; struct Ngs2Voice; bool Ngs2HasPitchShift(const Ngs2Voice& voice); void Ngs2ResetPitchShift(Ngs2Voice& voice); void Ngs2RestartPitchShift(Ngs2Voice& voice); void Ngs2ProcessPitchShift(Ngs2Voice& voice, std::size_t module, std::uint32_t grain); -void Ngs2ApplyPitchShiftGain(Ngs2Voice& voice, std::uint32_t grain); +void Ngs2ApplyVoiceGain(Ngs2Voice& voice, std::uint32_t grain); #endif diff --git a/core/libs/prx/libSceNgs2.native/src/Rack.cpp b/core/libs/prx/libSceNgs2.native/src/Rack.cpp index 13277128e3..4bce442fad 100644 --- a/core/libs/prx/libSceNgs2.native/src/Rack.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Rack.cpp @@ -137,6 +137,8 @@ static int CreateRack(Ngs2Handle systemHandle, std::uint32_t rackId, const RackO std::memcpy(rack->name, options.common.name, sizeof(rack->name)); rack->name[sizeof(rack->name) - 1] = '\0'; rack->maxGrainSamples = options.common.max_grain_samples; + rack->maxWaveformBlocks = rackId == SCE_NGS2_RACK_ID_SAMPLER ? options.sampler.max_waveform_blocks + : rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER ? options.customSampler.max_waveform_blocks : 0; if (rackId == SCE_NGS2_RACK_ID_SAMPLER) rack->maxChannelWorks = options.sampler.max_channel_works; if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) rack->maxChannelWorks = options.customSampler.max_channel_works; if (rackId == SCE_NGS2_RACK_ID_SUBMIXER) rack->maxInputs = options.submixer.max_inputs; diff --git a/core/libs/prx/libSceNgs2.native/src/Render.cpp b/core/libs/prx/libSceNgs2.native/src/Render.cpp index 7d10b6511c..848b9e41fb 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -11,9 +11,6 @@ #include "prx/libc/include/General.hpp" #include "Ngs2Internal.hpp" -static constexpr double PHASE_ONE = 4294967296.0; -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 * Ngs2PcmSampleBytes(voice.waveformType); if (block.data == nullptr) return nullptr; @@ -30,16 +27,6 @@ static void ReadPcm(const Ngs2Voice& voice, const std::uint8_t* frame, float* ou Ngs2ReadPcmFrame(voice.waveformType, voice.channels, frame, out); } -static void ReadFrames(Ngs2Voice& voice, float* current, float* next) { - auto& block = voice.blocks.front(); - ReadPcm(voice, FrameAt(voice, block, block.cursor), current); - if (block.streaming && block.cursor + 1 >= block.availableFrames) std::memcpy(next, current, voice.channels * sizeof(float)); - else if (block.cursor + 1 < block.info.num_samples) ReadPcm(voice, FrameAt(voice, block, block.cursor + 1), next); - else if (block.info.num_repeats != 0) ReadPcm(voice, FrameAt(voice, block, 0), next); - else if (voice.blocks.size() > 1) ReadPcm(voice, FrameAt(voice, voice.blocks[1], 0), next); - else std::memcpy(next, current, voice.channels * sizeof(float)); -} - bool Ngs2FinishBlock(Ngs2Voice& voice) { auto& block = voice.blocks.front(); const bool repeat = block.info.num_repeats != 0; @@ -74,45 +61,33 @@ bool Ngs2FinishBlock(Ngs2Voice& voice) { const Ngs2Unlocked unlocked; voice.callback(&info); } - return voice.waveformRevision == revision && voice.state == Ngs2PlayState::Playing; + return voice.waveformRevision == revision && (voice.state == Ngs2PlayState::Playing || voice.state == Ngs2PlayState::Paused || voice.outputTransition == Ngs2VoiceTransition::Stop); } -static void Advance(Ngs2Voice& voice, std::uint64_t frames) { - while (frames != 0 && voice.state == Ngs2PlayState::Playing && !voice.blocks.empty()) { +std::uint32_t Ngs2ReadSamplerPcm(Ngs2Voice& voice, float* dest, std::uint32_t stride, std::uint32_t count) { + const auto revision = voice.waveformRevision; + std::uint32_t written = 0; + while (written < count && (voice.state == Ngs2PlayState::Playing || voice.state == Ngs2PlayState::Paused || voice.outputTransition == Ngs2VoiceTransition::Stop) && voice.waveformRevision == revision) { + if (voice.blocks.empty()) break; auto& block = voice.blocks.front(); - 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); - block.cursor += static_cast(step); - voice.decodedSamples += step; - if (voice.waveformType != SCE_NGS2_WAVEFORM_TYPE_ATRAC9) voice.decodedBytes += step * voice.channels * Ngs2PcmSampleBytes(voice.waveformType); - frames -= step; + const auto limit = block.streaming ? std::min(block.info.num_samples, block.availableFrames) : block.info.num_samples; + if (block.cursor >= limit) { + if (block.cursor < block.info.num_samples && !(block.streaming && !voice.acceptsBlocks)) break; + if (!Ngs2FinishBlock(voice)) break; + continue; + } + float frame[NGS2_MAX_CHANNELS]; + ReadPcm(voice, FrameAt(voice, block, block.cursor), frame); + for (std::uint32_t channel = 0; channel < voice.channels; ++channel) dest[static_cast(channel) * stride + written] = frame[channel]; + ++written; + ++block.cursor; + ++voice.decodedSamples; + voice.decodedBytes += voice.channels * Ngs2PcmSampleBytes(voice.waveformType); if (block.streaming) { while (block.pieces.size() > 1 && block.cursor >= block.pieces.front().firstFrame + block.pieces.front().frames) block.pieces.erase(block.pieces.begin()); } - if (block.cursor == block.info.num_samples || (block.streaming && !voice.acceptsBlocks && block.cursor == block.availableFrames)) { - if (!Ngs2FinishBlock(voice)) return; - } } -} - -static void ConsumeSamples(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t systemRate) { - const auto revision = voice.waveformRevision; - const auto step = static_cast(std::llround(voice.sampleRate * static_cast(voice.pitch) / systemRate * PHASE_ONE)); - std::uint32_t written = 0; - float current[NGS2_MAX_CHANNELS]; - float next[NGS2_MAX_CHANNELS]; - for (; written < grain && voice.state == Ngs2PlayState::Playing && !voice.blocks.empty() && voice.waveformRevision == revision; written++) { - ReadFrames(voice, current, next); - const auto fraction = static_cast(static_cast(voice.phase) / PHASE_ONE); - for (std::uint32_t channel = 0; channel < voice.channels; channel++) { - voice.samples[channel * grain + written] = std::lerp(current[channel], next[channel], fraction); - } - voice.phase += step; - Advance(voice, voice.phase >> 32); - voice.phase &= PHASE_FRACTION; - } - voice.hasSamples = written != 0; + return written; } static bool Bypasses(const Ngs2Filter& filter, std::uint32_t channel) { @@ -145,8 +120,7 @@ static void ApplyFilter(Ngs2Voice& voice, Ngs2Filter& filter, std::uint32_t grai static void RenderSampler(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t systemRate) { const auto channels = voice.channels; - if (voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9) Ngs2ConsumeAtrac9(voice, grain, systemRate); - else ConsumeSamples(voice, grain, systemRate); + Ngs2ConsumeSampler(voice, grain, systemRate); if (voice.channels != channels) { voice.samples.assign(static_cast(grain) * voice.channels, 0.0f); voice.hasSamples = false; @@ -246,8 +220,8 @@ static void RenderVoice(Ngs2Voice& voice, const std::vector& voices, } renderingScope{voice.rendering}; voice.samples.assign(static_cast(grain) * voice.InputChannels(), 0.0f); voice.hasSamples = false; - const bool pitchShiftActive = Ngs2HasPitchShift(voice) && - (voice.state == Ngs2PlayState::Paused || voice.pitchShiftTransition == Ngs2PitchShiftTransition::Stop); + const bool pitchShiftActive = (Ngs2HasPitchShift(voice) || voice.IsSampler()) && + (voice.state == Ngs2PlayState::Paused || voice.outputTransition == Ngs2VoiceTransition::Stop); if ((voice.state == Ngs2PlayState::Playing || pitchShiftActive) && voice.channels != 0) { if (voice.rack->rackId == SCE_NGS2_RACK_ID_SAMPLER || voice.rack->rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) { RenderSampler(voice, grain, systemRate); @@ -257,7 +231,7 @@ static void RenderVoice(Ngs2Voice& voice, const std::vector& voices, if (voice.rack->rackId == SCE_NGS2_RACK_ID_REVERB && voice.reverb) voice.hasSamples = Ngs2ProcessReverb(voice, grain, systemRate); Ngs2ProcessLegacyUserFx(voice, grain, systemRate); Ngs2ProcessUserFx(voice, grain, systemRate); - Ngs2ApplyPitchShiftGain(voice, grain); + Ngs2ApplyVoiceGain(voice, grain); } if (voice.samples.size() != static_cast(grain) * voice.channels) voice.samples.assign(static_cast(grain) * voice.channels, 0.0f); voice.rendered = true; @@ -348,7 +322,9 @@ void Ngs2RenderSystem(Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo } for (auto* voice : voices) { if (voice->state == Ngs2PlayState::Stopped) voice->state = Ngs2PlayState::Empty; + const bool wasPaused = (voice->stateFlags & SCE_NGS2_VOICE_STATE_FLAG_PAUSED) != 0; voice->stateFlags = static_cast(voice->state); + if (wasPaused && voice->state == Ngs2PlayState::Paused && (voice->IsSampler() || Ngs2HasPitchShift(*voice))) voice->stateFlags |= SCE_NGS2_VOICE_STATE_FLAG_PLAYING; if (voice->state == Ngs2PlayState::Playing && voice->rack->rackId == SCE_NGS2_RACK_ID_SAMPLER && voice->blocks.empty() && voice->acceptsBlocks) voice->stateFlags |= SCE_NGS2_VOICE_STATE_FLAG_EMPTY; } system.renderCount++; diff --git a/core/libs/prx/libSceNgs2.native/src/Sampler.cpp b/core/libs/prx/libSceNgs2.native/src/Sampler.cpp new file mode 100644 index 0000000000..bf1da64397 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/src/Sampler.cpp @@ -0,0 +1,103 @@ +#include +#include +#include +#include +#include +#include + +#include "Ngs2Internal.hpp" + +static const std::array, 512>& SamplerKernel() { + static const auto table = [] { + std::array, 512> result{}; + for (std::uint32_t phase = 0; phase < 512; ++phase) { + std::array taps{}; + double sum = 0.0; + for (std::uint32_t tap = 0; tap < 4; ++tap) { + const double x = static_cast(tap) - 1.0 - static_cast(phase) / 512.0; + const double sinc = x == 0.0 ? 1.0 : std::sin(std::numbers::pi * x) / (std::numbers::pi * x); + taps[tap] = sinc * (0.5 + 0.5 * std::cos(std::numbers::pi * x / 2.0)); + sum += taps[tap]; + } + for (std::uint32_t tap = 0; tap < 4; ++tap) result[phase][tap] = static_cast(std::round(taps[tap] / sum * 1e10) / 1e10); + } + return result; + }(); + return table; +} + +void Ngs2RestartSampler(Ngs2Voice& voice) { + voice.phase = 0; + voice.samplerHistory = {}; +} + +void Ngs2SaveSamplerBlock(Ngs2Voice& voice, const Ngs2Block& block) { + if (!voice.samplerReplayKnown) return; + if (voice.samplerReplayBlocks.size() >= std::max(1u, voice.rack->maxWaveformBlocks)) { + voice.samplerReplayBlocks.clear(); + voice.samplerReplayKnown = false; + return; + } + voice.samplerReplayBlocks.push_back(block); +} + +void Ngs2ReplaySampler(Ngs2Voice& voice) { + if (!voice.samplerReplayKnown) throw std::runtime_error("NGS2: replay after the retained waveform descriptors were exceeded is not implemented"); + voice.blocks.assign(voice.samplerReplayBlocks.begin(), voice.samplerReplayBlocks.end()); + ++voice.waveformRevision; + voice.decodedSamples = 0; + voice.decodedBytes = 0; + voice.waveformEnd = nullptr; + if (voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9) Ngs2RestartAtrac9(voice); + Ngs2RestartSampler(voice); +} + +void Ngs2PrepareSampler(Ngs2Voice& voice) { + voice.samplerScratch.resize((static_cast(voice.rack->maxGrainSamples) * 4 + 4) * voice.channels); + Ngs2RestartSampler(voice); + SamplerKernel(); +} + +void Ngs2ConsumeSampler(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate) { + const auto revision = voice.waveformRevision; + const auto channels = voice.channels; + const auto stride = grain * 4 + 4; + if (voice.samplerScratch.size() < static_cast(stride) * channels) { + throw std::runtime_error("NGS2: sampler grain exceeds its prepared scratch buffer"); + } + const float ratio = (1.0f / static_cast(sampleRate)) * static_cast(voice.sampleRate) * voice.pitch; + const auto step = std::min(static_cast(std::min(ratio, 4.0f) * 16384.0f), 65535u); + const auto consumed = static_cast((voice.phase + static_cast(step) * grain) >> 14); + for (std::uint32_t channel = 0; channel < channels; ++channel) { + auto* dest = voice.samplerScratch.data() + static_cast(channel) * stride; + std::copy(voice.samplerHistory[channel].begin(), voice.samplerHistory[channel].end(), dest); + std::fill_n(dest + 4, consumed, 0.0f); + } + const auto count = voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9 + ? Ngs2ReadSamplerAtrac9(voice, voice.samplerScratch.data() + 4, stride, consumed) + : Ngs2ReadSamplerPcm(voice, voice.samplerScratch.data() + 4, stride, consumed); + if (voice.waveformRevision != revision) return; + const auto& kernel = SamplerKernel(); + for (std::uint32_t channel = 0; channel < channels; ++channel) { + const auto* source = voice.samplerScratch.data() + static_cast(channel) * stride; + auto* dest = voice.samples.data() + static_cast(channel) * grain; + if (step == 16384 && voice.phase == 0) { + std::copy_n(source + 1, grain, dest); + } else { + auto phase = static_cast(voice.phase); + for (std::uint32_t i = 0; i < grain; ++i) { + const auto index = phase >> 14; + const auto& coefficients = kernel[(phase & 0x3fff) >> 5]; + const float a = source[index] * coefficients[0]; + const float b = source[index + 1] * coefficients[1]; + const float c = source[index + 2] * coefficients[2]; + const float d = source[index + 3] * coefficients[3]; + dest[i] = (a + b) + (c + d); + phase += step; + } + } + std::copy_n(source + consumed, 4, voice.samplerHistory[channel].begin()); + } + voice.phase = (voice.phase + static_cast(step) * grain) & 0x3fff; + voice.hasSamples = count != 0 || std::any_of(voice.samples.begin(), voice.samples.end(), [](float sample) { return sample != 0.0f; }); +} diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index 8621e7b6a0..e1ef5d46f6 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -30,14 +30,18 @@ static void SendEvent(Ngs2Voice& voice, std::uint32_t eventId) { voice.SetEvent(eventId); } +bool Ngs2Voice::IsSampler() const { + return rack != nullptr && (rack->rackId == SCE_NGS2_RACK_ID_SAMPLER || rack->rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER); +} + void Ngs2Voice::SetEvent(std::uint32_t eventId) { switch (eventId) { case SCE_NGS2_VOICE_EVENT_PLAY: if (state == Ngs2PlayState::Empty || state == Ngs2PlayState::Stopped) { - if (Ngs2HasPitchShift(*this)) { + if (IsSampler()) Ngs2ReplaySampler(*this); + if (Ngs2HasPitchShift(*this) || IsSampler()) { Ngs2RestartPitchShift(*this); - pitchShiftTransition = pitchShiftPlayed ? Ngs2PitchShiftTransition::Resume : Ngs2PitchShiftTransition::None; - pitchShiftPlayed = true; + outputTransition = Ngs2VoiceTransition::None; } state = Ngs2PlayState::Playing; stateFlags |= SCE_NGS2_VOICE_STATE_FLAG_INUSE; @@ -47,7 +51,7 @@ void Ngs2Voice::SetEvent(std::uint32_t eventId) { case SCE_NGS2_VOICE_EVENT_STOP: if (state == Ngs2PlayState::Playing || state == Ngs2PlayState::Paused) { state = Ngs2PlayState::Stopped; - if (Ngs2HasPitchShift(*this)) pitchShiftTransition = Ngs2PitchShiftTransition::Stop; + if (Ngs2HasPitchShift(*this) || IsSampler()) outputTransition = Ngs2VoiceTransition::Stop; } break; case SCE_NGS2_VOICE_EVENT_STOP_IMM: @@ -55,18 +59,18 @@ void Ngs2Voice::SetEvent(std::uint32_t eventId) { state = Ngs2PlayState::Empty; Ngs2ClearReverb(*this); Ngs2RestartPitchShift(*this); - pitchShiftTransition = Ngs2PitchShiftTransition::None; + outputTransition = Ngs2VoiceTransition::None; break; case SCE_NGS2_VOICE_EVENT_PAUSE: if (state == Ngs2PlayState::Playing) { state = Ngs2PlayState::Paused; - if (Ngs2HasPitchShift(*this)) pitchShiftTransition = Ngs2PitchShiftTransition::Pause; + if (Ngs2HasPitchShift(*this) || IsSampler()) outputTransition = Ngs2VoiceTransition::Pause; } break; case SCE_NGS2_VOICE_EVENT_RESUME: if (state == Ngs2PlayState::Paused) { state = Ngs2PlayState::Playing; - if (Ngs2HasPitchShift(*this)) pitchShiftTransition = Ngs2PitchShiftTransition::Resume; + if (Ngs2HasPitchShift(*this) || IsSampler()) outputTransition = Ngs2VoiceTransition::Resume; } break; default: throw std::invalid_argument("NGS2: unknown voice event " + Ngs2Hex(eventId)); @@ -90,6 +94,8 @@ void Ngs2Voice::ResetSetup() { pitch = 1.0f; phase = 0; blocks.clear(); + samplerReplayBlocks.clear(); + samplerReplayKnown = true; acceptsBlocks = true; decodedSamples = 0; decodedBytes = 0; @@ -110,6 +116,7 @@ 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 (block.data == nullptr) return nullptr; if (waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9) return block.data + block.dataCursor; if (block.streaming) { for (const auto& piece : block.pieces) { @@ -215,6 +222,7 @@ static void SetupSampler(Ngs2Voice& voice, const Ngs2WaveformFormat& format) { voice.sampleRate = format.sample_rate; voice.waveformType = format.waveform_type; if (format.waveform_type == SCE_NGS2_WAVEFORM_TYPE_ATRAC9) Ngs2SetupAtrac9(voice, format); + Ngs2PrepareSampler(voice); } static void AppendStreamData(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBlocksParam& param, std::size_t frameBytes) { @@ -229,6 +237,7 @@ static void AppendStreamData(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBlo block.num_skip_samples = 0; block.num_repeats = 0; voice.blocks.push_back({static_cast(param.data) + block.data_offset, block}); + Ngs2SaveSamplerBlock(voice, voice.blocks.back()); } return; } @@ -242,6 +251,16 @@ static void AppendStreamData(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBlo } stream.pieces.push_back({static_cast(param.data) + block.data_offset, stream.availableFrames, block.data_size / frameBytes}); stream.availableFrames += block.data_size / frameBytes; + if (voice.samplerReplayKnown && !voice.samplerReplayBlocks.empty()) { + auto& replay = voice.samplerReplayBlocks.back(); + if (replay.pieces.size() >= std::max(1u, voice.rack->maxWaveformBlocks)) { + voice.samplerReplayKnown = false; + voice.samplerReplayBlocks.clear(); + } else { + replay.pieces.push_back(stream.pieces.back()); + replay.availableFrames = stream.availableFrames; + } + } } } @@ -250,13 +269,25 @@ static void AddWaveformBlocks(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBl if ((param.flags & ~knownFlags) != 0) throw std::runtime_error("NGS2: waveform block flags " + Ngs2Hex(param.flags) + " are not implemented"); if ((param.num_blocks != 0 && (voice.channels == 0 || param.blocks == nullptr || param.data == nullptr)) || (voice.channels == 0 && (param.flags & SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND) != 0)) APS5_INVALID_ARG_EX; const bool reset = (param.flags & SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET) != 0; + const auto cursor = reset && !voice.blocks.empty() ? voice.blocks.front().cursor : 0u; + const bool preserveCodec = reset && voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9 && !voice.blocks.empty() && voice.blocks.front().started; + std::uint64_t codecCursor = 0; + if (preserveCodec) { + const auto& old = voice.blocks.front(); + if (param.num_blocks != 1 || param.blocks[0].data_size != old.info.data_size || param.blocks[0].num_samples != old.info.num_samples || + param.blocks[0].num_skip_samples != old.info.num_skip_samples || static_cast(param.data) + param.blocks[0].data_offset != old.data) { + throw std::runtime_error("NGS2: replacing an active ATRAC9 waveform with different data is not implemented"); + } + codecCursor = old.dataCursor; + } if (!voice.acceptsBlocks && !reset) throw std::invalid_argument("NGS2: the voice waveform was already closed"); if (reset) { ++voice.waveformRevision; voice.blocks.clear(); - voice.phase = 0; + voice.samplerReplayBlocks.clear(); + voice.samplerReplayKnown = true; voice.waveformEnd = nullptr; - if (voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9) Ngs2RestartAtrac9(voice); + if (voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9 && !preserveCodec) Ngs2RestartAtrac9(voice); } const std::size_t frameBytes = voice.channels == 0 || voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9 ? 0 : voice.channels * Ngs2PcmSampleBytes(voice.waveformType); if ((param.flags & SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND) != 0) { @@ -272,6 +303,7 @@ static void AddWaveformBlocks(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBl if (allowsSilence && block.data_size == 0) { if (voice.waveformType != SCE_NGS2_WAVEFORM_TYPE_PCM_I16L) throw std::runtime_error("NGS2: a dataless waveform block under the silence flag is not implemented for this format"); voice.blocks.push_back({nullptr, block}); + Ngs2SaveSamplerBlock(voice, voice.blocks.back()); continue; } const std::uint64_t bytes = voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9 ? Ngs2Atrac9BlockBytes(voice, block) @@ -289,6 +321,12 @@ static void AddWaveformBlocks(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBl stream.availableFrames = block.data_size / frameBytes; stream.pieces.push_back({stream.data, 0, stream.availableFrames}); } + Ngs2SaveSamplerBlock(voice, voice.blocks.back()); + } + if (reset && !voice.blocks.empty()) voice.blocks.front().cursor = cursor; + if (preserveCodec && !voice.blocks.empty()) { + voice.blocks.front().dataCursor = codecCursor; + voice.blocks.front().started = true; } } @@ -329,9 +367,12 @@ void Ngs2SetFilter(Ngs2Voice& voice, const Ngs2SamplerVoiceFilterParam& param) { static void ApplySamplerParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { switch (param.id) { case SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP: - case SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_SETUP: - SetupSampler(voice, ParamAs(param).format); + case SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_SETUP: { + const auto& setup = ParamAs(param); + if (setup.channel_mask != 0) throw std::runtime_error("NGS2: sampler setup channel masks are not implemented"); + SetupSampler(voice, setup.format); break; + } case SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS: case SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS: AddWaveformBlocks(voice, ParamAs(param)); @@ -345,8 +386,8 @@ static void ApplySamplerParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& para case SCE_NGS2_SAMPLER_VOICE_PARAM_PITCH: case SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_PITCH: { const float ratio = ParamAs(param).ratio; - if (!std::isfinite(ratio) || ratio < 0.0f) APS5_INVALID_ARG_EX; - voice.pitch = ratio; + if (std::isnan(ratio)) APS5_INVALID_ARG_EX; + voice.pitch = std::clamp(ratio, 0.0f, 4.0f); break; } case SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER: Ngs2SetFilter(voice, ParamAs(param)); break; @@ -488,6 +529,7 @@ int APS5_VABI sceNgs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* state auto& sampler = *reinterpret_cast(state); sampler = {}; sampler.voice_state.state_flags = voice.stateFlags; + if (voice.state == Ngs2PlayState::Empty) return SCE_NGS2_OK; sampler.envelope_height = 1.0f; sampler.num_decoded_samples = voice.decodedSamples; sampler.decoded_data_size = voice.decodedBytes; @@ -513,6 +555,7 @@ int APS5_VABI sceNgs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* state auto& sampler = *reinterpret_cast(state); sampler = {}; sampler.voice_state.state_flags = voice.stateFlags; + if (voice.state == Ngs2PlayState::Empty) return SCE_NGS2_OK; sampler.num_decoded_samples = voice.decodedSamples; return SCE_NGS2_OK; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp index 1c72ffa8d4..ecb8da15f9 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp @@ -80,9 +80,9 @@ static uintptr_t Sampler(uintptr_t system, std::uint32_t skip, std::uint32_t sam return voice; } -static std::vector Render(uintptr_t system, std::uint32_t samples) { +static std::vector Render(uintptr_t system, std::uint32_t samples, std::uint32_t grain = Grain) { std::vector rendered; - std::vector out(Grain); + std::vector out(grain); const Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 1}; while (rendered.size() < samples) { Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); @@ -96,7 +96,7 @@ static void TestSkipAndBlockEnd(const std::vector& reference) { Patch(Sampler(system, 100, 500, 0), Mastering(system, 1)); callbackFlags.clear(); const auto rendered = Render(system, 512); - for (std::uint32_t i = 0; i < 512; i++) Require(rendered[i] == (i < 500 ? reference[100 + i] : 0.0f)); + for (std::uint32_t i = 0; i < 512; i++) Require(rendered[i] == (i >= 3 && i < 503 ? reference[100 + i - 3] : 0.0f)); Require(callbackFlags.size() == 1 && callbackFlags[0] == SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } @@ -112,7 +112,7 @@ static void TestRepeatAndState(const std::vector& reference) { Require(state.num_decoded_samples == 304 && state.decoded_data_size == 2 * SuperframeBytes && state.waveform_data == Superframe + SuperframeBytes); const auto second = Render(system, 304); rendered.insert(rendered.end(), second.begin(), second.end()); - for (std::uint32_t i = 0; i < 608; i++) Require(rendered[i] == (i < 600 ? reference[i % 300] : 0.0f)); + for (std::uint32_t i = 0; i < 608; i++) Require(rendered[i] == (i >= 3 && i < 603 ? reference[(i - 3) % 300] : 0.0f)); Require(callbackFlags.size() == 2 && callbackFlags[0] == SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_REPEAT && callbackFlags[1] == SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END); Require(Flags(sampler) == 0); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); @@ -363,8 +363,9 @@ static void TestParseLoops() { const auto loop = end - begin; const auto rendered = Render(system, end + 2 * loop); for (std::uint32_t i = 0; i < rendered.size(); ++i) { - const auto index = i < end ? i : begin + (i - end) % loop; - Require(rendered[i] == reference[256 + index]); + const auto frame = i >= 3 ? i - 3 : 0; + const auto index = frame < end ? frame : begin + (frame - end) % loop; + Require(rendered[i] == (i < 3 ? 0.0f : reference[256 + index])); } Require(sceNgs2SystemDestroy(system, nullptr) == 0); } @@ -386,8 +387,9 @@ static void TestParseLoops() { const auto tail = Render(system, samples + 600 - 1024 + Grain); rendered.insert(rendered.end(), tail.begin(), tail.end()); for (std::uint32_t i = 0; i < rendered.size(); ++i) { - const auto index = i < 700 ? i : i - 600; - Require(rendered[i] == (i < samples + 600 ? reference[256 + index] : 0.0f)); + const auto frame = i >= 3 ? i - 3 : 0; + const auto index = frame < 700 ? frame : frame - 600; + Require(rendered[i] == (i >= 3 && frame < samples + 600 ? reference[256 + index] : 0.0f)); } Require(Flags(voice) == 0); Require(sceNgs2SystemDestroy(system, nullptr) == 0); @@ -530,20 +532,15 @@ static void TestStreamPartitions() { } const auto output = Render(system, 1664); for (std::size_t i = 0; i < output.size(); ++i) { - const float position = i * pitch; - float expected = 0.0f; - if (position < 800) { - const auto index = static_cast(position); - const auto next = std::min(index + 1, 799u); - const auto fraction = position - index; - expected = reference[skip + index] * (1 - fraction) + reference[skip + next] * fraction; - } + const float expected = SamplerReference(i, pitch, [&](std::uint32_t index) { + return index < 800 ? reference[skip + index] : 0.0f; + }); Require(std::abs(output[i] - expected) < 1e-6f); } Require(callbackFlags.size() == 5 && Flags(voice) == 0); Ngs2SamplerVoiceState state{}; Require(sceNgs2VoiceGetState(voice, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); - Require(state.num_decoded_samples == 800); + Require(state.num_decoded_samples == 0); Require(sceNgs2SystemDestroy(system, nullptr) == 0); } } @@ -566,18 +563,28 @@ static void APS5_VABI Refill(const Ngs2VoiceCallbackInfo* info) { static void TestRefill(const std::vector& reference) { for (int mode = 0; mode < 3; ++mode) { const auto system = CreateSystem(); + Require(sceNgs2SystemSetGrainSamples(system, 64) == SCE_NGS2_OK); const auto voice = EmptySampler(system); RefillState state{0, mode == 1, mode == 2}; Control(voice, SCE_NGS2_VOICE_PARAM_CALLBACK, Ngs2VoiceCallbackParam{{}, Refill, reinterpret_cast(&state), SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0}); Queue(voice, Superframe, 13, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE, 500, 100); if (mode != 0) { - for (float value : Render(system, Grain)) Require(value == 0.0f); + const auto first = Render(system, 64, 64); + for (std::uint32_t i = 0; i < first.size(); ++i) { + const float expected = state.pause && i >= 3 ? reference[100 + i - 3] * static_cast(64 - i) / 64.0f : 0.0f; + Require(first[i] == expected); + } Require(state.calls == 1); if (state.pause) Event(voice, SCE_NGS2_VOICE_EVENT_RESUME); } - const auto output = Render(system, 512); - for (std::size_t i = 0; i < output.size(); ++i) Require(output[i] == (i < 500 ? reference[100 + i] : 0.0f)); + const auto output = Render(system, 512, 64); + for (std::uint32_t i = 0; i < output.size(); ++i) { + const auto frame = i + (state.pause ? 64u : 0u); + float expected = frame >= 3 && frame < 503 ? reference[100 + frame - 3] : 0.0f; + if (state.pause && i < 64) expected *= static_cast(i) / 64.0f; + Require(output[i] == expected); + } Require(state.calls == 2 && Flags(voice) == 0); Require(sceNgs2SystemDestroy(system, nullptr) == 0); } @@ -598,7 +605,7 @@ static void TestStarvationAndTruncation(const std::vector& reference) { } else { Queue(voice, Superframe + 13, SuperframeBytes - 13, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND); const auto output = Render(system, 512); - for (std::size_t i = 0; i < output.size(); ++i) Require(output[i] == (i < 500 ? reference[100 + i] : 0.0f)); + for (std::size_t i = 0; i < output.size(); ++i) Require(output[i] == (i >= 3 && i < 503 ? reference[100 + i - 3] : 0.0f)); Require(Flags(voice) == 0); } Require(sceNgs2SystemDestroy(system, nullptr) == 0); @@ -615,11 +622,11 @@ static void TestAppendBoundaries(const std::vector& reference) { Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, Superframe, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND, 1, &block}); const auto output = Render(system, 512); - for (std::size_t i = 0; i < output.size(); ++i) Require(output[i] == (i < 500 ? reference[100 + i] : 0.0f)); + for (std::size_t i = 0; i < output.size(); ++i) Require(output[i] == (i >= 3 && i < 503 ? reference[100 + i - 3] : 0.0f)); Ngs2SamplerVoiceState state{}; Require(sceNgs2VoiceGetState(voice, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); - Require(state.num_decoded_samples == 500 && state.decoded_data_size == SuperframeBytes); - Require(state.waveform_data == Superframe + SuperframeBytes && Flags(voice) == 0); + Require(state.num_decoded_samples == 0 && state.decoded_data_size == 0); + Require(state.waveform_data == nullptr && Flags(voice) == 0); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp index 7a79b6ffa0..0198f80fc2 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp @@ -176,7 +176,7 @@ static void TestUserFxChain() { Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); auto out = Render(system); - for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.5f && out[i * 2 + 1] == 0.75f); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == (i < 3 ? 0.0f : 0.5f) && out[i * 2 + 1] == (i < 3 ? 0.25f : 0.75f)); Require(calls.processFlags == (std::vector{1}) && calls.order == (std::vector{0, 1})); const float gain = 0.5f; @@ -301,7 +301,7 @@ static void TestCustomSampler() { Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); const auto out = Render(system); - for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.5f && out[i * 2 + 1] == 0.75f); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == (i < 3 ? 0.0f : 0.5f) && out[i * 2 + 1] == (i < 3 ? 0.25f : 0.75f)); Ngs2CustomSamplerVoiceState state{}; Require(sceNgs2VoiceGetState(voice, &state.voice_state, sizeof(Ngs2SamplerVoiceState)) == SCE_NGS2_ERROR_INVALID_OUT_SIZE); @@ -363,7 +363,7 @@ static void TestCustomFilter() { std::vector out(Grain, -1.0f); const Ngs2RenderBufferInfo renderInfo{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 1}; Require(sceNgs2SystemRender(system, &renderInfo, 1) == SCE_NGS2_OK); - Require(out[0] > 0.0f && out[0] < 0.2f && out.back() > out[0]); + Require(out[0] == 0.0f && out[1] == 0.0f && out[2] == 0.0f && out[3] > 0.0f && out[3] < 0.2f && out.back() > out[3]); Ngs2CustomVoiceFilterParam unsupported{}; unsupported.type = 2; @@ -702,7 +702,8 @@ static void TestCustomDelay(std::uint32_t count, const DelayOption& delay) { const auto checkGrain = [&](std::uint32_t grain) { const auto out = Render(system); for (std::uint32_t i = 0; i < Grain * 2; ++i) { - Require(out[i] == static_cast(pcm[grain * Grain * 2 + i]) / 32768.0f); + const auto offset = grain * Grain * 2 + i; + Require(out[i] == (offset < 6 ? 0.0f : static_cast(pcm[offset - 6]) / 32768.0f)); } }; for (std::uint32_t grain = 0; grain < 3; ++grain) checkGrain(grain); @@ -723,7 +724,7 @@ static void TestCustomDelay(std::uint32_t count, const DelayOption& delay) { } checkGrain(3); Require(state.calls == 0); - Event(sampler, SCE_NGS2_VOICE_EVENT_STOP); + Event(sampler, SCE_NGS2_VOICE_EVENT_STOP_IMM); for (const float sample : Render(system)) Require(sample == 0.0f); option.custom_rack_option.module[0].state_size = 4; @@ -883,7 +884,7 @@ static void TestCustomMastering() { auto out = Render(system); Require(out.size() == Grain * 2); - Require(out[0] != 0.0f); + Require(out[0] == 0.0f && out[6] != 0.0f); Require(sceNgs2VoiceGetState(voice, &state, sizeof(state)) == SCE_NGS2_OK); Require((state.state_flags & SCE_NGS2_VOICE_STATE_FLAG_PLAYING) != 0); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Filter.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Filter.cpp index f6dd4600d1..08bd37b75b 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Filter.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Filter.cpp @@ -83,7 +83,7 @@ static void TestLowPassAndTail() { const auto out = RenderGrain(system, 1); for (std::uint32_t i = 0; i < Grain; i++) { const auto frame = grain * Grain + i; - const float expected = reference.Next(frame < pcm.size() ? pcm[frame] / 32768.0 : 0.0); + const float expected = reference.Next(frame >= 3 && frame - 3 < pcm.size() ? pcm[frame - 3] / 32768.0 : 0.0); Require(std::abs(out[i] - expected) <= 1e-7f); } Require(out[Grain - 1] != 0.0f); @@ -112,12 +112,12 @@ static void TestBypassMaskAndGain() { auto out = RenderGrain(system, 2); for (std::uint32_t i = 0; i < Grain; i++) { - Require(out[i * 2] == pcm[i * 2] / 32768.0f); - Require(out[i * 2 + 1] == pcm[i * 2 + 1] / 32768.0f * 0.5f); + Require(out[i * 2] == (i < 3 ? 0.0f : pcm[(i - 3) * 2] / 32768.0f)); + Require(out[i * 2 + 1] == (i < 3 ? 0.0f : pcm[(i - 3) * 2 + 1] / 32768.0f * 0.5f)); } Require(Flags(sampler) == (SCE_NGS2_VOICE_STATE_FLAG_INUSE | SCE_NGS2_VOICE_STATE_FLAG_PLAYING)); out = RenderGrain(system, 2); - for (float sample : out) Require(sample == 0.0f); + for (std::uint32_t i = 0; i < Grain * 2; ++i) Require(out[i] == (i < 6 ? pcm[(Grain - 3) * 2 + i] / 32768.0f * (i % 2 ? 0.5f : 1.0f) : 0.0f)); Require(Flags(sampler) == 0); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } @@ -133,7 +133,7 @@ static void TestSystemRateAndZeroCutoff() { Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); Biquad reference(18000.0, static_cast(0.70710678f), 1.0, 48000.0); const auto out = RenderGrain(system, 1); - for (std::uint32_t i = 0; i < Grain; i++) Require(std::abs(out[i] - reference.Next(0.25)) <= 1e-7f); + for (std::uint32_t i = 0; i < Grain; i++) Require(std::abs(out[i] - reference.Next(SamplerReference(i, 0.5f, [](std::uint32_t) { return 0.25f; }))) <= 1e-7f); SetFilter(sampler, Filter(1, 1, 1, 0, 0.0f, 0.70710678f, 1.0f)); for (float sample : RenderGrain(system, 1)) Require(sample == 0.0f); @@ -143,7 +143,8 @@ static void TestSystemRateAndZeroCutoff() { Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block}); Patch(sampler, master); Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); - for (float sample : RenderGrain(system, 1)) Require(sample == 0.25f); + const auto reset = RenderGrain(system, 1); + for (std::uint32_t i = 0; i < Grain; ++i) Require(reset[i] == (i < 3 ? 0.0f : 0.25f)); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index 3f8ad014ad..d8e996540e 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -32,8 +32,8 @@ static void APS5_VABI OnBlock(const Ngs2VoiceCallbackInfo* info) { callbackFlags.push_back(info->flag); } -static std::vector RenderI16(uintptr_t system) { - std::vector out(Grain, -1); +static std::vector RenderI16(uintptr_t system, std::uint32_t grain = Grain) { + std::vector out(grain, -1); const Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(std::int16_t), SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1}; Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); return out; @@ -135,7 +135,7 @@ static void TestPcmBlockEnd() { callbackFlags.clear(); auto out = RenderI16(system); - for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == pcm[i]); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == (i < 3 ? 0 : pcm[i - 3])); Require(Flags(sampler) == (SCE_NGS2_VOICE_STATE_FLAG_INUSE | SCE_NGS2_VOICE_STATE_FLAG_PLAYING) && callbackFlags.empty()); Ngs2SamplerVoiceState state{}; @@ -145,11 +145,11 @@ static void TestPcmBlockEnd() { Require(state.waveform_data == pcm.data() + Grain); out = RenderI16(system); - for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == (i < 4 ? pcm[Grain + i] : 0)); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == (Grain + i - 3 < pcm.size() ? pcm[Grain + i - 3] : 0)); Require(callbackFlags.size() == 1 && callbackFlags[0] == SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END); Require(Flags(sampler) == 0); Require(sceNgs2VoiceGetState(sampler, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); - Require(state.num_decoded_samples == pcm.size() && state.waveform_data == pcm.data() + pcm.size()); + Require(state.num_decoded_samples == 0 && state.waveform_data == nullptr); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } @@ -170,7 +170,7 @@ static void TestPatchToRackHandle() { Require(port.dest_handle == master && port.dest_input_id == 0); Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); const auto out = RenderI16(system); - Require(out == pcm); + for (std::uint32_t i = 0; i < Grain; ++i) Require(out[i] == (i < 3 ? 0 : pcm[i - 3])); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } @@ -376,14 +376,18 @@ static void TestPitchAndRepeat() { Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); callbackFlags.clear(); - const std::int16_t first[Grain] = {0, 500, 1000, 1500, 2000, 2500, 3000, 1500}; + const std::int16_t first[Grain] = {0, 0, 0, 0, 0, -30, 0, 470}; auto out = RenderI16(system); for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == first[i]); - Require(callbackFlags.size() == 1 && callbackFlags[0] == SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_REPEAT); + Require(callbackFlags.empty()); - const std::int16_t second[Grain] = {0, 500, 1000, 1500, 2000, 2500, 3000, 3000}; + const std::int16_t second[Grain] = {1000, 1500, 2000, 2621, 3000, 1500, 0, 379}; out = RenderI16(system); for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == second[i]); + Require(callbackFlags.size() == 1 && callbackFlags[0] == SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_REPEAT); + out = RenderI16(system); + const std::int16_t tail[Grain] = {1000, 1500, 2000, 2621, 3000, 1530, 0, -91}; + for (std::uint32_t i = 0; i < Grain; ++i) Require(out[i] == tail[i]); Require(callbackFlags.size() == 2 && callbackFlags[1] == SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END); Require(Flags(sampler) == 0); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); @@ -403,7 +407,7 @@ static void TestSetupClearsCallback() { callbackFlags.clear(); auto out = RenderI16(system); - for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == (i < pcm.size() ? pcm[i] : 0)); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == (i >= 3 && i - 3 < pcm.size() ? pcm[i - 3] : 0)); Require(callbackFlags.empty() && Flags(sampler) == 0); Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1, 48000, 0, 0, 0}}); @@ -413,7 +417,7 @@ static void TestSetupClearsCallback() { Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &stream}); Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); out = RenderI16(system); - for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == (i < pcm.size() ? pcm[i] : 0)); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == (i >= 3 && i - 3 < pcm.size() ? pcm[i - 3] : 0)); Require(callbackFlags.size() == 1 && callbackFlags[0] == SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } @@ -454,9 +458,9 @@ static void TestPcmCallbackReset() { Require(sceNgs2VoiceGetState(sampler, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); if (state.num_decoded_samples != 1) std::fprintf(stderr, "PCM callback reset consumed %llu samples before the next grain, expected 1\n", static_cast(state.num_decoded_samples)); Require(state.num_decoded_samples == 1 && state.decoded_data_size == sizeof(std::int16_t) && state.waveform_data == callback.pcm.data()); - for (std::uint32_t frame = 0; frame < Grain; ++frame) Require(first[frame] == (frame == 0 ? 16384 : 0)); + for (std::uint32_t frame = 0; frame < Grain; ++frame) Require(first[frame] == 0); const auto second = RenderI16(system); - for (std::uint32_t frame = 0; frame < Grain; ++frame) Require(second[frame] == callback.pcm[frame * 2]); + for (std::uint32_t frame = 0; frame < Grain; ++frame) Require(second[frame] == (frame < 2 ? 0 : callback.pcm[frame * 2 - 3])); Require(callback.calls == 1); Require(sceNgs2VoiceGetState(sampler, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); Require(state.num_decoded_samples == 1 + Grain * 2 && state.waveform_data == callback.pcm.data() + Grain * 2); @@ -467,6 +471,8 @@ static void TestPcmCallbackReset() { static void TestPcmCallbackRefill() { for (const bool pause : {false, true}) { const auto system = CreateSystem(); + constexpr std::uint32_t RenderGrain = 64; + Require(sceNgs2SystemSetGrainSamples(system, RenderGrain) == SCE_NGS2_OK); const auto sampler = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER)); Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1, 48000, 0, 0, 0}}); Patch(sampler, Mastering(system, 1)); @@ -475,20 +481,27 @@ static void TestPcmCallbackRefill() { Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, &pcm, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE, 1, &block}); PcmResetCallback callback{{}, SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0, 0, pause}; - for (std::uint32_t frame = 0; frame < Grain * 2; ++frame) callback.pcm.push_back(static_cast(1000 + frame * 100)); + for (std::uint32_t frame = 0; frame < RenderGrain * 2; ++frame) callback.pcm.push_back(static_cast(1000 + frame * 100)); Control(sampler, SCE_NGS2_VOICE_PARAM_CALLBACK, Ngs2VoiceCallbackParam{{}, ResetPcm, reinterpret_cast(&callback), callback.flag, 0}); Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); - const auto first = RenderI16(system); - Require(callback.calls == 1 && first.front() == pcm); - for (std::uint32_t frame = 1; frame < Grain; ++frame) Require(first[frame] == (pause ? 0 : callback.pcm[frame - 1])); + const auto first = RenderI16(system, RenderGrain); + Require(callback.calls == 1); + for (std::uint32_t frame = 0; frame < RenderGrain; ++frame) { + const float source = frame < 3 ? 0.0f : frame == 3 ? pcm : callback.pcm[frame - 4]; + const float gain = pause ? static_cast(RenderGrain - frame) / 64.0f : 1.0f; + Require(first[frame] == std::lrint(source * gain)); + } Ngs2SamplerVoiceState state{}; Require(sceNgs2VoiceGetState(sampler, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); - const std::uint32_t consumed = pause ? 0 : Grain - 1; + const std::uint32_t consumed = RenderGrain - 1; Require(state.num_decoded_samples == consumed + 1 && state.waveform_data == callback.pcm.data() + consumed); if (pause) Event(sampler, SCE_NGS2_VOICE_EVENT_RESUME); - const auto second = RenderI16(system); - for (std::uint32_t frame = 0; frame < Grain; ++frame) Require(second[frame] == callback.pcm[consumed + frame]); + const auto second = RenderI16(system, RenderGrain); + for (std::uint32_t frame = 0; frame < RenderGrain; ++frame) { + const float gain = pause ? static_cast(frame) / 64.0f : 1.0f; + Require(second[frame] == std::lrint(callback.pcm[consumed + frame - 3] * gain)); + } Require(callback.calls == 1); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } @@ -537,7 +550,8 @@ static void TestPcmCallbackSetup() { Require(sceNgs2VoiceGetState(sampler, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); Require(state.num_decoded_samples == 0 && state.decoded_data_size == 0 && state.waveform_data == callback.pcm.data()); Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); - Require(callback.calls == 1 && std::equal(output.begin(), output.end(), callback.pcm.begin())); + Require(callback.calls == 1); + for (std::uint32_t i = 0; i < output.size(); ++i) Require(output[i] == (i < channels * 3 ? 0 : callback.pcm[i - channels * 3])); Require(sceNgs2VoiceGetState(sampler, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); Require(state.num_decoded_samples == Grain && state.decoded_data_size == Grain * channels * sizeof(std::int16_t)); Require(state.waveform_data == callback.pcm.data() + Grain * channels); @@ -567,14 +581,14 @@ static void TestSubmixerMatrix() { std::vector out(Grain * 2, -1.0f); const Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2}; Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); - for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.25f && out[i * 2 + 1] == 0.125f); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == (i < 3 ? 0.0f : 0.25f) && out[i * 2 + 1] == (i < 3 ? 0.0f : 0.125f)); Ngs2SubmixerVoiceState state{}; Require(sceNgs2VoiceGetState(submixer, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); Require(state.voice_state.state_flags == (SCE_NGS2_VOICE_STATE_FLAG_INUSE | SCE_NGS2_VOICE_STATE_FLAG_PLAYING)); Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); - for (float sample : out) Require(sample == 0.0f); + for (std::uint32_t i = 0; i < Grain; ++i) Require(out[i * 2] == (i < 3 ? 0.25f : 0.0f) && out[i * 2 + 1] == (i < 3 ? 0.125f : 0.0f)); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } @@ -596,7 +610,7 @@ static std::vector MatrixLevelFrame(const std::vector& levels, boo const Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2}; Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); - return {out[0], out[1]}; + return {out[6], out[7]}; } static void TestMatrixLevelClamp() { @@ -665,9 +679,9 @@ static std::vector RenderReverb(const ReverbCase& reverbCase, std::uint32 std::uint32_t seed = 1; for (std::uint32_t i = 0; reverbCase.noise && i + 3 * channels < pcm.size(); i++) { seed = seed * 1664525u + 1013904223u; - if (i < 3000 * channels) pcm[i + 3 * channels] = static_cast(static_cast((seed >> 16) & 0x3fff) - 0x2000); + if (i < 3000 * channels) pcm[i] = static_cast(static_cast((seed >> 16) & 0x3fff) - 0x2000); } - for (std::uint32_t c = 0; !reverbCase.noise && c < channels; c++) pcm[3 * channels + c] = static_cast((16384 - 4000 * static_cast(c)) * reverbCase.amplitude); + for (std::uint32_t c = 0; !reverbCase.noise && c < channels; c++) pcm[c] = static_cast((16384 - 4000 * static_cast(c)) * reverbCase.amplitude); const auto sampler = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER)); Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, channels, 48000, 0, 0, 0}}); const Ngs2WaveformBlock block{0, pcm.size() * sizeof(std::int16_t), 0, 0, 4096, 0, 0}; @@ -891,7 +905,7 @@ static void TestSampleRate() { const auto sampler = Sampler(system, pcm, 1); Patch(sampler, master); Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); - const std::int16_t expected[Grain] = {0, 500, 1000, 1500, 2000, 2500, 3000, 1500}; + const std::int16_t expected[Grain] = {0, 0, 0, 0, 0, -30, 0, 470}; const auto out = RenderI16(system); for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == expected[i]); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); @@ -924,7 +938,7 @@ static void TestSystemOptionFlags() { Patch(sampler, master); Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); const auto out = RenderI16(system); - Require(std::equal(pcm.begin(), pcm.end(), out.begin())); + for (std::uint32_t i = 0; i < Grain; ++i) Require(out[i] == (i < 3 ? 0 : pcm[(i - 3) % pcm.size()])); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); for (const std::uint32_t flags : {0x1u, 0x3u, 0x4u, 0x80000000u, 0x80000002u}) { @@ -1022,9 +1036,9 @@ static void TestMasteringGain() { }; Require(sceNgs2SystemRender(system, info, 3) == SCE_NGS2_OK); for (std::uint32_t i = 0; i < Grain; i++) { - Require(outStereo[i * 2] == 0.25f && outStereo[i * 2 + 1] == 0.125f); - for (std::uint32_t channel = 0; channel < 6; channel++) Require(outSurround[i * 6 + channel] == (channel == 3 ? 0.125f : 0.25f)); - for (std::uint32_t channel = 0; channel < 4; channel++) Require(outQuad[i * 4 + channel] == 0.25f); + Require(outStereo[i * 2] == (i < 3 ? 0.0f : 0.25f) && outStereo[i * 2 + 1] == (i < 3 ? 0.0f : 0.125f)); + for (std::uint32_t channel = 0; channel < 6; channel++) Require(outSurround[i * 6 + channel] == (i < 3 ? 0.0f : channel == 3 ? 0.125f : 0.25f)); + for (std::uint32_t channel = 0; channel < 4; channel++) Require(outQuad[i * 4 + channel] == (i < 3 ? 0.0f : 0.25f)); } Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); @@ -1036,7 +1050,7 @@ static void TestMasteringGain() { Patch(toLoud, loud); Event(toLoud, SCE_NGS2_VOICE_EVENT_PLAY); auto clipped = RenderI16(loudSystem); - for (std::int16_t sample : clipped) Require(sample == 32767); + for (std::uint32_t i = 0; i < Grain; ++i) Require(clipped[i] == (i < 3 ? 0 : 32767)); Control(loud, SCE_NGS2_MASTERING_VOICE_PARAM_SETUP, Ngs2MasteringVoiceSetupParam{{}, 1, 0}); Event(loud, SCE_NGS2_VOICE_EVENT_PLAY); auto reset = RenderI16(loudSystem); @@ -1104,9 +1118,9 @@ static void TestStereoIntoSurround() { }; Require(sceNgs2SystemRender(system, info, 3) == SCE_NGS2_OK); for (std::uint32_t i = 0; i < Grain; i++) { - Require(outFive[i * 6] == 0.5f && outFive[i * 6 + 1] == 0.25f); + Require(outFive[i * 6] == (i < 3 ? 0.0f : 0.5f) && outFive[i * 6 + 1] == (i < 3 ? 0.0f : 0.25f)); for (std::uint32_t channel = 2; channel < 6; channel++) Require(outFive[i * 6 + channel] == 0.0f); - Require(outSeven[i * 8] == 0.5f && outSeven[i * 8 + 1] == 0.25f); + Require(outSeven[i * 8] == (i < 3 ? 0.0f : 0.5f) && outSeven[i * 8 + 1] == (i < 3 ? 0.0f : 0.25f)); for (std::uint32_t channel = 2; channel < 8; channel++) Require(outSeven[i * 8 + channel] == 0.0f); } @@ -1475,7 +1489,7 @@ static std::vector MixFrame(std::uint32_t sourceChannels, std::uint32_t s Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); usedBuffers = firstBuffer; - return std::vector(out.begin(), out.begin() + destChannels); + return std::vector(out.begin() + 3 * destChannels, out.begin() + 4 * destChannels); } static void TestDefaultChannelMap() { @@ -1544,7 +1558,7 @@ static void TestUnsetMatrix() { const Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2}; Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); const float spread = 0.25f * HalfPower; - for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == spread && out[i * 2 + 1] == spread); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == (i < 3 ? 0.0f : spread) && out[i * 2 + 1] == (i < 3 ? 0.0f : spread)); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); const auto stereoSystem = CreateSystem(); @@ -1562,7 +1576,7 @@ static void TestUnsetMatrix() { Control(stereo, SCE_NGS2_VOICE_PARAM_PORT_MATRIX, Ngs2VoicePortMatrixParam{{}, 0, 0}); Event(stereo, SCE_NGS2_VOICE_EVENT_PLAY); Require(sceNgs2SystemRender(stereoSystem, &info, 1) == SCE_NGS2_OK); - for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.5f && out[i * 2 + 1] == 0.25f); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == (i < 3 ? 0.0f : 0.5f) && out[i * 2 + 1] == (i < 3 ? 0.0f : 0.25f)); Require(sceNgs2SystemDestroy(stereoSystem, nullptr) == SCE_NGS2_OK); } @@ -1584,11 +1598,13 @@ static void TestRenderCapacity(std::uint32_t waveformType, std::uint32_t channel Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); std::fill(out.begin(), out.end(), sentinel); Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); - Require(std::all_of(out.begin(), out.begin() + samples, [sentinel](TSample value) { return value > 0 && value != sentinel; })); + Require(std::all_of(out.begin(), out.begin() + 3 * channels, [](TSample value) { return value == 0; })); + Require(std::all_of(out.begin() + 3 * channels, out.begin() + samples, [sentinel](TSample value) { return value > 0 && value != sentinel; })); Require(std::all_of(out.begin() + samples, out.end(), [sentinel](TSample value) { return value == sentinel; })); Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); - Require(std::all_of(out.begin(), out.begin() + samples, [](TSample value) { return value == 0; })); + Require(std::all_of(out.begin(), out.begin() + 3 * channels, [sentinel](TSample value) { return value > 0 && value != sentinel; })); + Require(std::all_of(out.begin() + 3 * channels, out.begin() + samples, [](TSample value) { return value == 0; })); Require(std::all_of(out.begin() + samples, out.end(), [sentinel](TSample value) { return value == sentinel; })); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Sampler.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Sampler.cpp new file mode 100644 index 0000000000..73491336e4 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Sampler.cpp @@ -0,0 +1,128 @@ +#include "Ngs2Test.hpp" + +#include +#include +#include + +static constexpr std::uint32_t RenderGrain = 256; +static constexpr std::array Checkpoints{8, 12, 14, 20, 24, 30, 32, 40}; + +struct Case { + std::uint32_t rate; + float pitch; + std::int32_t cents; + bool events; + bool bypass; + std::array, 8> expected; +}; + +static const Case Cases[]{ + {48000, 1.0f, 0, false, true, {}}, + {44100, 1.3f, 700, false, false, {{ + {0.137265846f, -0.0252674483f, 0.0799425617f, 0.0359304883f, -0.00174468313f, -0.0710403621f}, + {-0.0386396796f, 0.113722473f, -0.0735076442f, 0.0788545236f, 0.115713701f, -0.0370367542f}, + {-0.0208529234f, 0.131442904f, -0.0557390116f, 0.0966642424f, 0.133591712f, -0.0192373525f}, + {0.0472084023f, -0.108676627f, 0.0123404339f, -0.158158675f, -0.103753135f, 0.0490724295f}, + {0.0901324302f, -0.0657368749f, 0.055264473f, -0.100620545f, -0.0606985465f, 0.0921270251f}, + {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, + {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, + {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, + }}}, + {24000, 0.7f, -700, false, false, {{ + {-0.127651423f, -0.075771533f, -0.0938414633f, -0.0419081151f, 0.0508089289f, 0.102808177f}, + {-0.0176326279f, 0.0342472568f, 0.017153997f, 0.0691795796f, -0.144594505f, -0.092595242f}, + {0.0412285328f, 0.0931084156f, 0.0762526914f, 0.128278285f, -0.0875838622f, -0.0355783924f}, + {-0.0992715359f, -0.0473916531f, -0.06464196f, -0.0126163764f, 0.0828370824f, 0.134836316f}, + {0.014179945f, 0.0660598278f, 0.0491285622f, 0.101154149f, -0.114638358f, -0.062632896f}, + {-0.125253201f, -0.0733733177f, -0.0906971246f, -0.0386715382f, 0.0567951128f, 0.108794361f}, + {-0.0694803074f, -0.0176004246f, -0.0356095247f, 0.0163238123f, 0.111254223f, -0.145683259f}, + {-0.16338414f, -0.100421906f, -0.117817312f, -0.0657917261f, 0.0296836458f, 0.0816829056f}, + }}}, + {48000, 1.3f, 700, true, false, {{ + {-0.0924464986f, -0.0188726112f, -0.0760293677f, 0.0488269478f, 0.156057909f, 0.0191847961f}, + {0.0912231579f, -0.0198750645f, 0.0805369988f, -0.0305612143f, 0.000996570569f, -0.000805972959f}, + {0.0f, -0.0f, 0.035238862f, -0.0581547283f, 0.0200667791f, -0.0917441919f}, + {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, + {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, + {0.130914479f, 0.0156071745f, 0.117588684f, 0.00487004546f, 0.0995834172f, -0.0121165738f}, + {0.0912231579f, -0.0198750645f, 0.0805369988f, -0.0305612143f, 0.0637805164f, -0.0515822694f}, + {0.0705910772f, 0.0424018614f, 0.108651653f, -0.0748682022f, -0.028412085f, 0.0848536044f}, + }}}, +}; + +static void TestSampler(const Case& test) { + const auto firstBuffer = usedBuffers; + const auto system = CreateSystem(); + Require(sceNgs2SystemSetGrainSamples(system, RenderGrain) == SCE_NGS2_OK); + const auto master = Mastering(system, 2); + Ngs2CustomSamplerRackOption option{}; + auto& custom = option.custom_rack_option; + auto& common = custom.rack_option; + common.size = sizeof(option); + common.max_grain_samples = 512; + common.max_voices = 1; + common.max_matrices = 1; + common.max_ports = 1; + custom.num_modules = 1; + custom.num_buffers = 1; + custom.state_size = 48; + custom.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_PITCH_SHIFT; + option.max_channel_works = 2; + option.max_waveform_blocks = 4; + Ngs2ContextBufferInfo query{}; + Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &common, &query) == SCE_NGS2_OK); + const auto storage = Buffer(query); + std::uintptr_t rack = 0; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &common, &storage, &rack) == SCE_NGS2_OK); + const auto voice = Voice(rack); + Control(voice, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_SETUP, + Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, test.rate, 0, 0, 0}}); + if (!test.bypass) Control(voice, SCE_NGS2_CUSTOM_VOICE_PARAM_PITCH_SHIFT, Ngs2CustomVoicePitchShiftParam{{}, test.cents, 0, 0, 0}); + Control(voice, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_PITCH, Ngs2SamplerVoicePitchParam{{}, test.pitch}); + Patch(voice, master); + std::vector pcm(8192 * 2); + for (std::uint32_t i = 0; i < pcm.size(); ++i) pcm[i] = static_cast((static_cast((i / 2 + i % 2 * 17) % 101) - 50) * 100); + const Ngs2WaveformBlock block{0, pcm.size() * 2, 0, 0, 8192, 0, 0x55}; + const auto submit = [&](std::uint32_t flags) { + Control(voice, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, + Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), flags, 1, &block}); + }; + submit(0); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + std::array samples{}; + const Ngs2RenderBufferInfo output{samples.data(), sizeof(samples), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2}; + for (std::uint32_t grain = 0; grain <= Checkpoints.back(); ++grain) { + if (test.events) { + if (grain == 8) submit(SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET); + if (grain == 12) Event(voice, SCE_NGS2_VOICE_EVENT_PAUSE); + if (grain == 14) Event(voice, SCE_NGS2_VOICE_EVENT_RESUME); + if (grain == 20) { + Event(voice, SCE_NGS2_VOICE_EVENT_STOP_IMM); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + } + if (grain == 30) Control(voice, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_PITCH, Ngs2SamplerVoicePitchParam{{}, 0.5f}); + } + Require(sceNgs2SystemRender(system, &output, 1) == SCE_NGS2_OK); + for (const float sample : samples) Require(std::isfinite(sample)); + if (test.bypass) { + for (std::uint32_t i = 0; i < samples.size(); ++i) { + const auto frame = grain * RenderGrain + i / 2; + const float expected = frame >= 3 && frame < 8195 ? pcm[(frame - 3) * 2 + i % 2] / 32768.0f : 0.0f; + Require(samples[i] == expected); + } + continue; + } + for (std::size_t i = 0; i < Checkpoints.size(); ++i) { + if (grain != Checkpoints[i]) continue; + constexpr std::array indices{0, 1, 100, 101, 510, 511}; + for (std::size_t j = 0; j < indices.size(); ++j) Require(std::abs(samples[indices[j]] - test.expected[i][j]) < 1e-6f); + } + } + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + usedBuffers = firstBuffer; +} + +int main() { + for (const auto& test : Cases) TestSampler(test); + return 0; +} diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Stream.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Stream.cpp index fa222ce472..bcd8ad496b 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Stream.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Stream.cpp @@ -51,30 +51,30 @@ static void TestStreamEndsAtDeclaredSamples() { AddBlock(voice, first.data(), first.size() * 2, 20, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE); Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); auto out = RenderI16(system); - for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == (i < 6 ? 16384 : 0)); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == (i >= 3 ? 16384 : 0)); Require(Flags(voice) == (SCE_NGS2_VOICE_STATE_FLAG_INUSE | SCE_NGS2_VOICE_STATE_FLAG_PLAYING)); Ngs2SamplerVoiceState state{}; Require(sceNgs2VoiceGetState(voice, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); Require(state.num_decoded_samples == 6 && state.decoded_data_size == 12 && state.waveform_data == first.data() + 6); out = RenderI16(system); - for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == 0); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == (i == 0 ? 16384 : 0)); Require(sceNgs2VoiceGetState(voice, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); Require(state.num_decoded_samples == 6 && state.decoded_data_size == 12); AddBlock(voice, second.data(), second.size() * 2, 20, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE | SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND); Require(sceNgs2VoiceGetState(voice, &state.voice_state, sizeof(state)) == SCE_NGS2_OK && state.waveform_data == second.data()); out = RenderI16(system); - for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == 8192); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == (i < 3 ? 0 : 8192)); Require(sceNgs2VoiceGetState(voice, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); Require(state.num_decoded_samples == 14 && state.decoded_data_size == 28 && state.waveform_data == second.data() + 8); AddBlock(voice, last.data(), last.size() * 2, 20, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND); out = RenderI16(system); - for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == (i < 6 ? 4096 : 0)); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == (i < 3 ? 8192 : 4096)); Require(Flags(voice) == 0); Require(sceNgs2VoiceGetState(voice, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); - Require(state.num_decoded_samples == 20 && state.decoded_data_size == 40 && state.waveform_data == last.data() + 6); + Require(state.num_decoded_samples == 0 && state.decoded_data_size == 0 && state.waveform_data == nullptr); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } @@ -90,14 +90,14 @@ static void TestStreamClosedBeforeDeclaredSamples() { Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); AddBlock(voice, closing.data(), closing.size() * 2, 1000, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND); auto out = RenderI16(system); - for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == (i < 6 ? 16384 : 8192)); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == (i < 3 ? 0 : 16384)); Require(Flags(voice) == (SCE_NGS2_VOICE_STATE_FLAG_INUSE | SCE_NGS2_VOICE_STATE_FLAG_PLAYING)); out = RenderI16(system); - for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == (i < 2 ? 8192 : 0)); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == (i == 0 ? 16384 : i < 5 ? 8192 : 0)); Require(Flags(voice) == 0); Ngs2SamplerVoiceState state{}; Require(sceNgs2VoiceGetState(voice, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); - Require(state.num_decoded_samples == 10 && state.decoded_data_size == 20); + Require(state.num_decoded_samples == 0 && state.decoded_data_size == 0); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } @@ -140,7 +140,7 @@ static void TestSilenceBlock() { Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); auto out = RenderI16(system); for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == 0); - Require(Flags(voice) == 0); + Require(Flags(voice) == (SCE_NGS2_VOICE_STATE_FLAG_INUSE | SCE_NGS2_VOICE_STATE_FLAG_PLAYING)); Ngs2SamplerVoiceState state{}; Require(sceNgs2VoiceGetState(voice, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); Require(state.num_decoded_samples == Grain && state.decoded_data_size == Grain * 2); @@ -150,7 +150,10 @@ static void TestSilenceBlock() { AddBlock(withData, pcm.data(), pcm.size() * 2, Grain, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_SILENCE); Event(withData, SCE_NGS2_VOICE_EVENT_PLAY); out = RenderI16(system); - for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == 1234); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == (i < 3 ? 0 : 1234)); + Require(Flags(withData) == (SCE_NGS2_VOICE_STATE_FLAG_INUSE | SCE_NGS2_VOICE_STATE_FLAG_PLAYING)); + out = RenderI16(system); + for (std::uint32_t i = 0; i < Grain; ++i) Require(out[i] == (i < 3 ? 1234 : 0)); Require(Flags(withData) == 0); const auto dataless = StreamVoice(rack, 2, master); @@ -192,7 +195,7 @@ static void TestDrainedStreamReportsEmpty() { const std::vector longer(Grain * 2, 8192); AddBlock(voice, longer.data(), longer.size() * 2, Grain * 2, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE); const auto out = RenderI16(system); - Require(out[0] == 8192); + Require(out[0] == 0 && out[3] == 8192); Require(Flags(voice) == playing); AddBlock(voice, pcm.data(), pcm.size() * 2, 4, 0); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp index e1c5907df4..62ac975451 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp @@ -78,6 +78,17 @@ inline bool FrameInfoThrows(const Ngs2WaveformFormat& format) { inline constexpr std::uint32_t Grain = 8; +template +inline float SamplerReference(std::uint32_t frame, float pitch, TSignal signal) { + const float position = frame * pitch; + const auto index = static_cast(position); + const auto source = [&](std::uint32_t offset) { return offset < 4 ? 0.0f : signal(offset - 4); }; + if (position == index) return source(index + 1); + Require(position - index == 0.5f); + return (source(index) * -0.0303300861f + source(index + 1) * 0.530330062f) + + (source(index + 2) * 0.530330062f + source(index + 3) * -0.0303300861f); +} + inline std::vector> buffers; inline std::size_t usedBuffers = 0; diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2UserFx.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2UserFx.cpp index e72e1dc86c..c8f96887f5 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2UserFx.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2UserFx.cpp @@ -103,11 +103,11 @@ static void TestEffectAndIndependentVoices() { Patch(sampler, first); Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); const auto mixed = Render(system, 2); - for (std::size_t i = 0; i < mixed.size(); ++i) Require(mixed[i] == (i % 2 == 0 ? 0.375f : 0.125f)); - Require(firstState.calls == 1 && secondState.calls == 1 && Flags(sampler) == 0); + for (std::size_t i = 0; i < mixed.size(); ++i) Require(mixed[i] == (i < 6 ? (i % 2 == 0 ? 0.125f : 0.25f) : (i % 2 == 0 ? 0.375f : 0.125f))); + Require(firstState.calls == 1 && secondState.calls == 1 && Flags(sampler) == (SCE_NGS2_VOICE_STATE_FLAG_INUSE | SCE_NGS2_VOICE_STATE_FLAG_PLAYING)); const auto tail = Render(system, 2); - for (std::size_t i = 0; i < tail.size(); ++i) Require(tail[i] == (i % 2 == 0 ? 0.125f : 0.25f)); - Require(firstState.calls == 2 && secondState.calls == 2); + for (std::size_t i = 0; i < tail.size(); ++i) Require(tail[i] == (i < 6 ? (i % 2 == 0 ? 0.375f : 0.125f) : (i % 2 == 0 ? 0.125f : 0.25f))); + Require(firstState.calls == 2 && secondState.calls == 2 && Flags(sampler) == 0); Require(sceNgs2SystemDestroy(system, nullptr) == 0); } diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 2f8f3d0fe6..f08a472581 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -31,7 +31,10 @@ - [NGS2 custom Delay](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, 0x1d) - processing and defaults remain unverified on PS5. The [agreed compatibility path](https://github.com/boykopovar/AnyPS5/pull/9337#issuecomment-6351205140) preserves main's PCM pass-through for Void Terrarium (PPSA03061) until any Delay parameter is set; every `0x40001dxx` parameter throws. Only one-buffer custom submixers with no module state and a 20-byte option are accepted. Option type 0/1, taps 1..8, length 1..20000 and reserved 0 follow [fma-rc's 12.70 module reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943). Other shapes and values throw because their console errors are unknown. This preserves host behavior, not console-equivalent Delay processing - [NGS2 custom module ids](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - Reverb (0x1b) and Convolution (0x23) are not processed: a rack that passes their creation checks throws. Gain (0x15) on custom sampler racks also throws. Per [fma-rc's 12.70 module reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A8302` for a wrong non-zero state size, and `0x804A8013`, `0x804A8099` and `0x804A8098` for a bad Reverb option size or word and PitchShift word. Unknown: check order, errors for a Gain option, missing Reverb options, Convolution option sizes, and Convolution's state (64 bytes from SDK 3.0). PPSA04540 uses 0x15, 0x19, 0x1b and 0x23 (#8039), PPSA35169 0x20 (#4246) and PPSA33655 0x15, 0x19 and 0x1b -- [NGS2 custom PitchShift](../../../core/libs/prx/libSceNgs2.native/src/PitchShift.cpp) (module 0x20) accepts absent options or an 8-byte option with quality 0, and processes custom submixer racks in 64-sample blocks, with cents clamped to -1200 through 1200, channel bypass, input filtering, waveform-aligned overlap and resampling. Default and setup-reset parameters bypass the module. Buffered processing continues while paused; pause, resume and stop use 64-sample output fades, and playback restart clears the history. PCM and buffer-state comparisons cover impulses, tones, noise, silence, parameter changes, reset, module indices and independent voices. The reconstructed resampling kernel produces float-rounding differences; sampled comparisons stayed below 1e-6 for input magnitudes up to 0.5. Active processing requires a grain divisible by 64. Pitch-down inputs with no selectable overlap throw instead of stalling. A channel bypassed at initialization cannot be enabled until a playback restart: this throws instead of reproducing the reference's non-terminating filter loop. Custom sampler racks with PitchShift still throw; their upstream sampler resampling path has an unmodeled three-sample startup delay, which changes the effect's waveform alignment. The title stop in [PPSA35169](https://github.com/boykopovar/AnyPS5/issues/4246) uses a custom submixer; no title run has verified progress past it. +- [NGS2 custom PitchShift](../../../core/libs/prx/libSceNgs2.native/src/PitchShift.cpp) (module 0x20) - active processing requires grains divisible by 64. Pitch-down inputs with no selectable overlap throw instead of stalling. A channel bypassed at initialization cannot be enabled until playback restart: this throws instead of reproducing a non-terminating filter loop. Custom sampler and submixer paths support cents -1200..1200, bypass, filtering, waveform alignment and resampling; setup defaults to bypass. Pause/resume/stop fade over 64 samples and restart clears history. Sampled PCM comparisons stayed below 1e-6 and ATRAC9 below 5e-6. The shared sampler uses Q14 phase, four history samples and a 512-phase kernel, producing three initial zero samples at unity rate. No title run has verified progress past the [PPSA35169 stop](https://github.com/boykopovar/AnyPS5/issues/4246). + + +- [NGS2 sampler replay and replacement](../../../core/libs/prx/libSceNgs2.native/src/Sampler.cpp) - playback restart retains waveform descriptors up to the rack's configured block capacity (at least one); exceeding that retention budget makes the next PLAY throw; already-running playback continues. Streaming pieces have the same retention bound. Active ATRAC9 waveform replacement preserves the decoder for the same data descriptor; replacing it with different data or lengths throws because the decoder's cached superframe replacement behavior is unmodeled. Nonzero sampler setup channel masks and transitions on grains below 64 throw. Callback-driven setup/reset still stops the current reader at its revision boundary; exact reference timing for callback replacement and partial compressed input remains unverified. The custom sampler state layout and size validation still have the limitations listed below. - [NGS2 custom Gain module](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - follows [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943): parameter `0x40001500 | module index` holds 37 linear levels, one per channel, default 0, and the module writes source × level into dest, ramping linearly over the grain from the previous level. Unverified: where the ramp starts and ends (here the grain's first sample is one step past the previous level and its last sample is at the new level), whether stopping or setting up the voice again resets the levels, and what the library does with a level outside -32 to 32 (here it throws). On a custom sampler rack, whose parameter index is one higher per fma-rc, Gain throws. A Gain module with an option throws: fma-rc reads Gain as taking none, and the error the library returns for one is unknown. The extra buffer is ignored, as fma-rc reads it - [NGS2 custom mastering rack](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native, rack 0x4003) - follows [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943): option size is 0x508, matching the custom submixer rack layout. Creation returns `0x804A8052` above 37 channels; channels 9 to 37 throw because voices hold at most 8 channels. Mastering racks with modules throw: attached modules and non-default option fields are unverified on PS5. Voice control parameters 0x40030000 (setup), 0x40030004 (gain) and 0x40030005 (output) configure channels, levels and destination buffer. `sceNgs2VoiceGetState` fills common voice state flags. PPSA10278 and PPSA04540 create rack 0x4003 without modules (#8039)