From d62e8f42eac8c72db8e67f0e327d64d31710be5d Mon Sep 17 00:00:00 2001 From: edubox Date: Mon, 26 Oct 2026 08:26:09 +0100 Subject: [PATCH 1/3] feat(ngs2): implement the sampler and submixer peak meter --- .../libSceNgs2.native/include/Ngs2Types.hpp | 11 +++ .../libSceNgs2.native/src/Ngs2Internal.hpp | 7 ++ core/libs/prx/libSceNgs2.native/src/Rack.cpp | 7 ++ .../libs/prx/libSceNgs2.native/src/Render.cpp | 43 +++++++++ core/libs/prx/libSceNgs2.native/src/Voice.cpp | 22 +++++ .../libSceNgs2.native/tests/Ngs2Render.cpp | 88 +++++++++++++++++++ .../tests/Ngs2VoiceIndex.cpp | 3 + docs/dev/debt/libSceNgs2.native.md | 1 + 8 files changed, 182 insertions(+) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index 0d1c8e282b..784c2c195a 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -30,6 +30,8 @@ static constexpr int SCE_NGS2_ERROR_INVALID_PAN_WORK = static_cast(0x804A84 static constexpr int SCE_NGS2_ERROR_INVALID_PAN_PARAM = static_cast(0x804A8454); static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_HANDLE = static_cast(0x804A8203); static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_ACCESS = static_cast(0x804A8351); +static constexpr int SCE_NGS2_ERROR_INVALID_PEAK_METER_BLOCKS = static_cast(0x804A8092); +static constexpr int SCE_NGS2_ERROR_INVALID_PEAK_METER_MODE = static_cast(0x804A8093); static constexpr int SCE_NGS2_ERROR_NULL_PARAM_LIST = static_cast(0x804A8360); static constexpr int SCE_NGS2_ERROR_INVALID_PARAM_ID = static_cast(0x804A835A); static constexpr int SCE_NGS2_ERROR_INVALID_GEOM_LISTENER_ADDRESS = static_cast(0x804A8461); @@ -85,8 +87,10 @@ static constexpr std::uint32_t SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS static constexpr std::uint32_t SCE_NGS2_SAMPLER_VOICE_PARAM_EXIT_LOOP = 0x10000004; static constexpr std::uint32_t SCE_NGS2_SAMPLER_VOICE_PARAM_PITCH = 0x10000005; static constexpr std::uint32_t SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER = 0x1000000a; +static constexpr std::uint32_t SCE_NGS2_SAMPLER_VOICE_PARAM_PEAK_METER = 0x10000009; static constexpr std::uint32_t SCE_NGS2_SUBMIXER_VOICE_PARAM_SETUP = 0x20000000; static constexpr std::uint32_t SCE_NGS2_SUBMIXER_VOICE_PARAM_USER_FX = 0x20000004; +static constexpr std::uint32_t SCE_NGS2_SUBMIXER_VOICE_PARAM_PEAK_METER = 0x20000005; static constexpr std::uint32_t SCE_NGS2_REVERB_VOICE_PARAM_SETUP = 0x20010000; static constexpr std::uint32_t SCE_NGS2_REVERB_VOICE_PARAM_I3DL2 = 0x20010001; static constexpr std::uint32_t SCE_NGS2_MASTERING_VOICE_PARAM_SETUP = 0x30000000; @@ -626,6 +630,13 @@ static_assert(offsetof(Ngs2CustomVoiceFilterParam, type) == 8); static_assert(offsetof(Ngs2CustomVoiceFilterParam, channel_mask) == 12); static_assert(offsetof(Ngs2CustomVoiceFilterParam, param) == 16); +struct Ngs2VoicePeakMeterParam { + Ngs2VoiceParamHeader header; + std::uint32_t detect_type; + std::uint32_t reserved; +}; +static_assert(sizeof(Ngs2VoicePeakMeterParam) == 16); + struct Ngs2MasteringVoiceSetupParam { Ngs2VoiceParamHeader header; std::uint32_t num_io_channels; diff --git a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp index b73c150e61..ae71bec903 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -151,6 +151,11 @@ struct Ngs2Voice { std::uint32_t userFxFlags = 0; std::vector samples; std::vector> customBuffers; + std::vector> peakMeterHistory; + std::uint32_t peakMeterMode = 0; + std::uint32_t peakMeterCount = 0; + std::uint32_t peakMeterCursor = 0; + float peakHeight = 0.0f; bool rendering = false; bool rendered = false; bool hasSamples = false; @@ -173,6 +178,7 @@ struct Ngs2Rack { std::uint32_t maxGrainSamples = 0; std::uint32_t maxChannelWorks = 0; std::uint32_t maxInputs = 0; + std::uint32_t peakMeterBlocks = 8; std::uintptr_t userData = 0; Ngs2ContextBufferInfo bufferInfo{}; Ngs2BufferAllocator allocator{}; @@ -200,6 +206,7 @@ struct Ngs2System { std::string Ngs2Hex(std::uint32_t value); std::size_t Ngs2PcmSampleBytes(std::uint32_t waveformType); void Ngs2ReadPcmFrame(std::uint32_t waveformType, std::uint32_t channels, const std::uint8_t* frame, float* output); +void Ngs2UpdatePeakMeter(Ngs2Voice& voice, std::uint32_t grain); const std::uint8_t* Ngs2StreamEnd(const Ngs2Voice& voice, const Ngs2Block& block); std::recursive_mutex& Ngs2Mutex(); diff --git a/core/libs/prx/libSceNgs2.native/src/Rack.cpp b/core/libs/prx/libSceNgs2.native/src/Rack.cpp index 13277128e3..89e60d9905 100644 --- a/core/libs/prx/libSceNgs2.native/src/Rack.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Rack.cpp @@ -101,6 +101,10 @@ static int CheckedRackOption(std::uint32_t rackId, const Ngs2RackOption* option, std::memcpy(&options, option, size); const auto maxChannels = RackMaxChannels(rackId, options); if ((rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER || rackId == SCE_NGS2_RACK_ID_CUSTOM_MASTERING) && maxChannels > SCE_NGS2_CUSTOM_SUBMIXER_MAX_CHANNELS) return SCE_NGS2_ERROR_INVALID_NUM_CHANNELS; + if ((rackId == SCE_NGS2_RACK_ID_SAMPLER && (options.sampler.num_peak_meter_blocks < 1 || options.sampler.num_peak_meter_blocks > 256)) || + (rackId == SCE_NGS2_RACK_ID_SUBMIXER && (options.submixer.num_peak_meter_blocks < 1 || options.submixer.num_peak_meter_blocks > 256))) { + return SCE_NGS2_ERROR_INVALID_PEAK_METER_BLOCKS; + } if ((rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER || rackId == SCE_NGS2_RACK_ID_CUSTOM_MASTERING) && (options.customSubmixer.max_inputs == 0 || options.customSubmixer.max_inputs > SCE_NGS2_CUSTOM_SUBMIXER_MAX_INPUTS)) { throw std::runtime_error("NGS2: custom submixer racks with " + std::to_string(options.customSubmixer.max_inputs) + " inputs are not implemented"); } @@ -141,6 +145,9 @@ static int CreateRack(Ngs2Handle systemHandle, std::uint32_t rackId, const RackO 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; if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER || rackId == SCE_NGS2_RACK_ID_CUSTOM_MASTERING) rack->maxInputs = options.customSubmixer.max_inputs; + if (rackId == SCE_NGS2_RACK_ID_SAMPLER) rack->peakMeterBlocks = options.sampler.num_peak_meter_blocks; + if (rackId == SCE_NGS2_RACK_ID_SUBMIXER) rack->peakMeterBlocks = options.submixer.num_peak_meter_blocks; + if (rackId == SCE_NGS2_RACK_ID_MASTERING) rack->peakMeterBlocks = options.mastering.num_peak_meter_blocks; rack->bufferInfo = bufferInfo; rack->allocator = allocator; rack->voices.resize(options.common.max_voices); diff --git a/core/libs/prx/libSceNgs2.native/src/Render.cpp b/core/libs/prx/libSceNgs2.native/src/Render.cpp index 7036b9a377..68ad326770 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -248,6 +248,48 @@ static void MixInputs(Ngs2Voice& voice, const std::vector& voices, s } } +void Ngs2UpdatePeakMeter(Ngs2Voice& voice, std::uint32_t grain) { + if (voice.peakMeterMode == 0 || voice.channels == 0) { + voice.peakHeight = 0.0f; + return; + } + const auto blocks = std::clamp(voice.rack->peakMeterBlocks, 1u, 256u); + if (voice.peakMeterHistory.size() != voice.channels || + (!voice.peakMeterHistory.empty() && voice.peakMeterHistory.front().size() != blocks)) { + voice.peakMeterHistory.assign(voice.channels, std::vector(blocks, 0.0f)); + voice.peakMeterCount = 0; + voice.peakMeterCursor = 0; + } + for (std::uint32_t channel = 0; channel < voice.channels; channel++) { + float value = 0.0f; + if (voice.peakMeterMode == 1) { + for (std::uint32_t i = 0; i < grain; i++) value = std::max(value, std::abs(voice.samples[channel * grain + i])); + } else { + for (std::uint32_t i = 0; i < grain; i++) { + const float sample = voice.samples[channel * grain + i]; + value += sample * sample; + } + } + voice.peakMeterHistory[channel][voice.peakMeterCursor] = value; + } + voice.peakMeterCursor = (voice.peakMeterCursor + 1) % blocks; + voice.peakMeterCount = std::min(voice.peakMeterCount + 1, blocks); + float height = 0.0f; + for (std::uint32_t channel = 0; channel < voice.channels; channel++) { + float channelPeak = 0.0f; + if (voice.peakMeterMode == 1) { + for (std::uint32_t block = 0; block < voice.peakMeterCount; block++) { + channelPeak = std::max(channelPeak, voice.peakMeterHistory[channel][block]); + } + } else { + for (std::uint32_t block = 0; block < voice.peakMeterCount; block++) channelPeak += voice.peakMeterHistory[channel][block]; + channelPeak = std::sqrt(channelPeak / static_cast(voice.peakMeterCount * grain)); + } + height = std::max(height, channelPeak); + } + voice.peakHeight = height; +} + static void RenderVoice(Ngs2Voice& voice, const std::vector& voices, std::uint32_t grain, std::uint32_t systemRate) { if (voice.rendered) return; if (voice.rendering) throw std::invalid_argument("NGS2: the voice patches form a cycle"); @@ -268,6 +310,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); + Ngs2UpdatePeakMeter(voice, grain); } if (voice.samples.size() != static_cast(grain) * voice.channels) voice.samples.assign(static_cast(grain) * voice.channels, 0.0f); voice.rendered = true; diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index 1f61912d77..106ae17fa8 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -89,6 +89,10 @@ void Ngs2Voice::ResetSetup() { callback = nullptr; callbackData = 0; callbackFlags = 0; + peakMeterHistory.clear(); + peakMeterCount = 0; + peakMeterCursor = 0; + peakHeight = 0.0f; } const std::uint8_t* Ngs2StreamEnd(const Ngs2Voice& voice, const Ngs2Block& block) { @@ -322,6 +326,17 @@ void Ngs2SetFilter(Ngs2Voice& voice, const Ngs2SamplerVoiceFilterParam& param) { filter.a2 = (1.0 - alpha) * inverseA0; } +static void SetPeakMeter(Ngs2Voice& voice, std::uint32_t detectType) { + if (detectType > 2) throw Ngs2ReturnCode{SCE_NGS2_ERROR_INVALID_PEAK_METER_MODE}; + if (voice.peakMeterMode != detectType) { + voice.peakMeterHistory.clear(); + voice.peakMeterCount = 0; + voice.peakMeterCursor = 0; + voice.peakHeight = 0.0f; + } + voice.peakMeterMode = detectType; +} + static void ApplySamplerParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { switch (param.id) { case SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP: @@ -346,6 +361,7 @@ static void ApplySamplerParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& para break; } case SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER: Ngs2SetFilter(voice, ParamAs(param)); break; + case SCE_NGS2_SAMPLER_VOICE_PARAM_PEAK_METER: SetPeakMeter(voice, ParamAs(param).detect_type); break; default: throw std::runtime_error("NGS2: sampler voice param " + Ngs2Hex(param.id) + " is not implemented"); } } @@ -371,6 +387,10 @@ static void ApplyParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { case SCE_NGS2_RACK_ID_SAMPLER: case SCE_NGS2_RACK_ID_CUSTOM_SAMPLER: ApplySamplerParam(voice, param); return; case SCE_NGS2_RACK_ID_SUBMIXER: + if (param.id == SCE_NGS2_SUBMIXER_VOICE_PARAM_PEAK_METER) { + SetPeakMeter(voice, ParamAs(param).detect_type); + return; + } if (param.id == SCE_NGS2_SUBMIXER_VOICE_PARAM_USER_FX) { const auto& fx = ParamAs(param); voice.userFxHandler = fx.handler; @@ -489,6 +509,7 @@ int APS5_VABI sceNgs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* state sampler = {}; sampler.voice_state.state_flags = voice.stateFlags; sampler.envelope_height = 1.0f; + sampler.peak_height = voice.peakHeight; sampler.num_decoded_samples = voice.decodedSamples; sampler.decoded_data_size = voice.decodedBytes; sampler.user_data = voice.blocks.empty() ? 0 : voice.blocks.front().info.user_data; @@ -506,6 +527,7 @@ int APS5_VABI sceNgs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* state auto& submixer = *reinterpret_cast(state); submixer = {}; submixer.voice_state.state_flags = voice.stateFlags; + submixer.peak_height = voice.peakHeight; return SCE_NGS2_OK; } case SCE_NGS2_RACK_ID_CUSTOM_SAMPLER: { diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index f053141be3..be3f1fd332 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -70,6 +70,7 @@ static void TestErrorsAndInfo() { named.rack_option.max_ports = 2; named.max_channels = 2; named.max_inputs = 3; + named.num_peak_meter_blocks = 8; Ngs2ContextBufferInfo namedQuery{}; Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_SUBMIXER, &named.rack_option, &namedQuery) == SCE_NGS2_OK); const auto namedBuffer = Buffer(namedQuery); @@ -547,6 +548,92 @@ static void TestPcmCallbackSetup() { } } +static void TestPeakMeter() { + const auto system = CreateSystem(); + const auto master = Mastering(system, 2); + + const auto submixerRack = CreateRack(system, SCE_NGS2_RACK_ID_SUBMIXER); + const auto submixer = Voice(submixerRack); + Control(submixer, SCE_NGS2_SUBMIXER_VOICE_PARAM_SETUP, Ngs2SubmixerVoiceSetupParam{{}, 2, 0}); + Patch(submixer, master); + Event(submixer, SCE_NGS2_VOICE_EVENT_PLAY); + + const std::vector pcm(Grain, 16384); + uintptr_t samplerRack = 0; + const auto sampler = Sampler(system, pcm, 8, &samplerRack); + Patch(sampler, submixer); + const float levels[2] = {1.0f, 1.0f}; + Control(sampler, SCE_NGS2_VOICE_PARAM_MATRIX_LEVELS, Ngs2VoiceMatrixLevelsParam{{}, 0, 2, levels}); + Control(sampler, SCE_NGS2_VOICE_PARAM_PORT_MATRIX, Ngs2VoicePortMatrixParam{{}, 0, 0}); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + + const auto render = [&system] { + 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); + }; + + Ngs2VoicePeakMeterParam peak{{}, 1, 0}; + Control(submixer, SCE_NGS2_SUBMIXER_VOICE_PARAM_PEAK_METER, peak); + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_PEAK_METER, peak); + render(); + Ngs2SubmixerVoiceState submixerState{}; + Require(sceNgs2VoiceGetState(submixer, &submixerState.voice_state, sizeof(submixerState)) == SCE_NGS2_OK); + Require(submixerState.peak_height == 0.5f); + Ngs2SamplerVoiceState samplerState{}; + Require(sceNgs2VoiceGetState(sampler, &samplerState.voice_state, sizeof(samplerState)) == SCE_NGS2_OK); + Require(samplerState.peak_height == 0.5f); + + peak.detect_type = 2; + Control(submixer, SCE_NGS2_SUBMIXER_VOICE_PARAM_PEAK_METER, peak); + render(); + Require(sceNgs2VoiceGetState(submixer, &submixerState.voice_state, sizeof(submixerState)) == SCE_NGS2_OK); + Require(submixerState.peak_height == 0.5f); + + peak.detect_type = 0; + Control(submixer, SCE_NGS2_SUBMIXER_VOICE_PARAM_PEAK_METER, peak); + render(); + Require(sceNgs2VoiceGetState(submixer, &submixerState.voice_state, sizeof(submixerState)) == SCE_NGS2_OK); + Require(submixerState.peak_height == 0.0f); + + peak.detect_type = 3; + peak.header = {sizeof(peak), 0, SCE_NGS2_SUBMIXER_VOICE_PARAM_PEAK_METER}; + Require(sceNgs2VoiceControl(submixer, &peak.header) == SCE_NGS2_ERROR_INVALID_PEAK_METER_MODE); + + Ngs2SubmixerRackOption invalid{}; + invalid.rack_option.size = sizeof(invalid); + invalid.rack_option.max_grain_samples = 256; + invalid.rack_option.max_voices = 1; + invalid.max_channels = 2; + invalid.max_inputs = 1; + std::vector storage(512); + Ngs2ContextBufferInfo buffer{}; + buffer.host_buffer = storage.data(); + buffer.host_buffer_size = storage.size() * sizeof(std::uint64_t); + uintptr_t rejectedRack = 0; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_SUBMIXER, &invalid.rack_option, &buffer, &rejectedRack) == SCE_NGS2_ERROR_INVALID_PEAK_METER_BLOCKS); + invalid.num_peak_meter_blocks = 257; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_SUBMIXER, &invalid.rack_option, &buffer, &rejectedRack) == SCE_NGS2_ERROR_INVALID_PEAK_METER_BLOCKS); + invalid.num_peak_meter_blocks = 8; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_SUBMIXER, &invalid.rack_option, &buffer, &rejectedRack) == SCE_NGS2_OK); + Require(sceNgs2RackDestroy(rejectedRack, nullptr) == SCE_NGS2_OK); + + Ngs2SamplerRackOption invalidSampler{}; + invalidSampler.rack_option.size = sizeof(invalidSampler); + invalidSampler.rack_option.max_grain_samples = 256; + invalidSampler.rack_option.max_voices = 1; + invalidSampler.max_channel_works = 1; + uintptr_t rejectedSampler = 0; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_SAMPLER, &invalidSampler.rack_option, &buffer, &rejectedSampler) == SCE_NGS2_ERROR_INVALID_PEAK_METER_BLOCKS); + invalidSampler.num_peak_meter_blocks = 8; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_SAMPLER, &invalidSampler.rack_option, &buffer, &rejectedSampler) == SCE_NGS2_OK); + Require(sceNgs2RackDestroy(rejectedSampler, nullptr) == SCE_NGS2_OK); + + Require(sceNgs2RackDestroy(samplerRack, nullptr) == SCE_NGS2_OK); + Require(sceNgs2RackDestroy(submixerRack, nullptr) == SCE_NGS2_OK); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + static void TestSubmixerMatrix() { const auto system = CreateSystem(); const auto master = Mastering(system, 2); @@ -1796,6 +1883,7 @@ int main() { TestPan(); TestPanExplicitAnglesAndNanDistance(); TestPitchAndRepeat(); + TestPeakMeter(); TestSubmixerMatrix(); TestReverb(); TestReverbSetup(); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceIndex.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceIndex.cpp index 2b7453a1a0..5013bf81c4 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceIndex.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceIndex.cpp @@ -35,13 +35,16 @@ uintptr_t CreateSingleVoiceRack(uintptr_t system, std::uint32_t rackId) { case SCE_NGS2_RACK_ID_SAMPLER: option.sampler.max_channel_works = 1; option.sampler.max_waveform_blocks = 1; + option.sampler.num_peak_meter_blocks = 8; break; case SCE_NGS2_RACK_ID_SUBMIXER: option.submixer.max_channels = 2; + option.submixer.num_peak_meter_blocks = 8; option.submixer.max_inputs = 1; break; case SCE_NGS2_RACK_ID_MASTERING: option.mastering.max_channels = 2; + option.mastering.num_peak_meter_blocks = 8; break; default: option.reverb.max_channels = 2; diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index e1cf9b0e6a..5dbfb2f4fd 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -31,6 +31,7 @@ - [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), PitchShift (0x20) and Convolution (0x23) are not processed: a rack that passes their creation checks throws. Gain (0x15) on custom sampler racks still throws: its processor initialization and lifecycle are unverified. 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 or PitchShift options, other PitchShift and 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 Gain](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) - PS5 rounding and first-parameter processor state are unverified. The initial previous gain retains main's zero convention; the schema default does not verify processor initialization. Processing before a parameter, after re-setup or after stopping a configured voice throws. The unclamped levels, i/N ramp and equal-gain paths follow [the PS5 12.70 reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6391072574) (0x33ce0, 0x5310); invalid parameter positions return 0x804A835A (0x250b0). Custom sampler Gain and module options throw: their initialization, lifecycle and option errors remain unknown. Integer rendering of nonfinite samples throws because the console conversion is unknown. +- [NGS2 peak meter](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) - [13.42](https://github.com/boykopovar/AnyPS5/pull/11354#issuecomment-6416545468) confirms `peak_height` at offset 12 in `Ngs2SamplerVoiceState` and `Ngs2SubmixerVoiceState` (peak 0.5 and RMS 0.353553 for alternating 16384/0 PCM, no other word in the first 28 bytes changes), and that a sampler SETUP reset clears the detect type until it is enabled again; the submixer reset was not measured. The controls `0x10000009` (sampler) and `0x20000005` (submixer) carry the detect type at offset 8 (0 off, 1 peak, 2 RMS, else `0x804A8093`). Each grain stores one value per channel in a ring of `num_peak_meter_blocks` slots (1 to 256, else `0x804A8092`): the largest absolute sample for peak or the sum of squares for RMS; `peak_height` is the largest channel value, or `sqrt(sum / (blocks * grain))` for RMS, over the filled slots. A detect-type change empties the ring, and measurement is after the voice modules. - [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) - [NGS2 custom buffers](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) - multiple buffers on samplers, submixer channels above 8, inputs beyond 0/1 and multi-buffer chains beyond zero modules or one Gain throw. Default custom module order, UserFx extra-buffer selection and invalid port-buffer errors remain unknown. Input-to-buffer routing and grain clearing follow [the PS5 12.70 reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6391072574) (0x181a0); output-port selection follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/83f8bfd3c9c35fcb135ef6c33dfd88bbfe066ee2/src/libs/ngs2.cpp#L417-L419). These paths have not been compared with console output. Creation validation order and QueryBufferSize validation remain unverified; the query throws for options whose creation returns an error. - [NGS2 reverb rack](../../../core/libs/prx/libSceNgs2.native/src/Reverb.cpp) (libSceNgs2.native, rack 0x2001) - follows the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output at 11.025 to 48 kHz, including parameter changes during playback (differences at float rounding level); not yet compared with the PS5 library. Structure layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_reverb.h` and the param ids 0x20010000 (setup) and 0x20010001 (I3DL2) from PPSA22520. Only the default `reverb_size` of 1 is handled, with its 14400-sample reflection buffer; other sizes and the library's half-rate mode throw. System sample rates below about 7.5 kHz, where an allpass delay would have no samples, throw. Setup only accepts equal input and output channel counts From f20abed467d2efa243566e2c6ee05120673a7d66 Mon Sep 17 00:00:00 2001 From: Eduardo Ruiz Date: Mon, 26 Oct 2026 10:04:03 +0100 Subject: [PATCH 2/3] test(ngs2): cover the peak-meter history and reset --- .../libSceNgs2.native/tests/Ngs2Render.cpp | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index be3f1fd332..637f2ba8c5 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -634,6 +634,109 @@ static void TestPeakMeter() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static void TestPeakMeterHistory() { + const auto samplerFor = [](uintptr_t system, std::uint32_t blocks, const std::vector& pcm) { + Ngs2SamplerRackOption option{}; + option.rack_option.size = sizeof(option); + option.rack_option.max_grain_samples = 512; + option.rack_option.max_voices = 1; + option.rack_option.max_matrices = 1; + option.rack_option.max_ports = 8; + option.max_channel_works = 1; + option.max_codec_caches = 1; + option.max_waveform_blocks = 4; + option.max_envelope_points = 1; + option.max_filters = 1; + option.max_atrac9_decoders = 1; + option.max_atrac9_channel_works = 1; + option.num_peak_meter_blocks = blocks; + Ngs2ContextBufferInfo query{}; + Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_SAMPLER, &option.rack_option, &query) == SCE_NGS2_OK); + const auto buffer = Buffer(query); + uintptr_t rack = 0; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_SAMPLER, &option.rack_option, &buffer, &rack) == SCE_NGS2_OK && rack != 0); + const auto voice = Voice(rack); + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1, 48000, 0, 0, 0}}); + const Ngs2WaveformBlock block{0, pcm.size() * sizeof(std::int16_t), 0, 0, static_cast(pcm.size()), 0, 0x55}; + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block}); + return voice; + }; + + std::vector pcm; + for (int gain = 0; gain < 3; gain++) { + for (std::uint32_t i = 0; i < Grain; i++) pcm.push_back(static_cast(16384 >> gain)); + } + + { + const auto system = CreateSystem(); + const auto master = Mastering(system, 1); + const auto sampler = samplerFor(system, 2, pcm); + Patch(sampler, master); + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_PEAK_METER, Ngs2VoicePeakMeterParam{{}, 2, 0}); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + Ngs2SamplerVoiceState state{}; + const auto height = [&sampler, &state] { + Require(sceNgs2VoiceGetState(sampler, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); + return state.peak_height; + }; + RenderI16(system); + Require(Near(height(), 0.5f)); + RenderI16(system); + Require(Near(height(), std::sqrt(2.5f / 16.0f))); + RenderI16(system); + Require(Near(height(), std::sqrt(0.625f / 16.0f))); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + } + + { + const auto system = CreateSystem(); + const auto master = Mastering(system, 1); + const auto sampler = samplerFor(system, 2, pcm); + Patch(sampler, master); + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_PEAK_METER, Ngs2VoicePeakMeterParam{{}, 1, 0}); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + Ngs2SamplerVoiceState state{}; + const auto height = [&sampler, &state] { + Require(sceNgs2VoiceGetState(sampler, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); + return state.peak_height; + }; + RenderI16(system); + Require(height() == 0.5f); + RenderI16(system); + Require(height() == 0.5f); + RenderI16(system); + Require(height() == 0.25f); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + } + + { + std::vector half(Grain, 0); + for (std::uint32_t i = 0; i < Grain / 2; i++) half[i] = static_cast(16384); + const auto system = CreateSystem(); + const auto master = Mastering(system, 1); + const auto sampler = samplerFor(system, 2, half); + Patch(sampler, master); + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_PEAK_METER, Ngs2VoicePeakMeterParam{{}, 2, 0}); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + Ngs2SamplerVoiceState state{}; + const auto height = [&sampler, &state] { + Require(sceNgs2VoiceGetState(sampler, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); + return state.peak_height; + }; + RenderI16(system); + Require(Near(height(), std::sqrt(0.125f))); + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1, 48000, 0, 0, 0}}); + Require(height() == 0.0f); + const Ngs2WaveformBlock block{0, half.size() * sizeof(std::int16_t), 0, 0, Grain, 0, 0x55}; + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, half.data(), 0, 1, &block}); + Patch(sampler, master); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + RenderI16(system); + Require(Near(height(), std::sqrt(0.125f))); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + } +} + static void TestSubmixerMatrix() { const auto system = CreateSystem(); const auto master = Mastering(system, 2); @@ -1884,6 +1987,7 @@ int main() { TestPanExplicitAnglesAndNanDistance(); TestPitchAndRepeat(); TestPeakMeter(); + TestPeakMeterHistory(); TestSubmixerMatrix(); TestReverb(); TestReverbSetup(); From a2d5b0345635605a7ae64e8db9e5fd11d518e4af Mon Sep 17 00:00:00 2001 From: Eduardo Ruiz Date: Mon, 26 Oct 2026 14:14:32 +0100 Subject: [PATCH 3/3] fix(ngs2): clear the peak-meter mode on a voice reset --- core/libs/prx/libSceNgs2.native/src/Voice.cpp | 1 + core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index 106ae17fa8..48f8e11d76 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -90,6 +90,7 @@ void Ngs2Voice::ResetSetup() { callbackData = 0; callbackFlags = 0; peakMeterHistory.clear(); + peakMeterMode = 0; peakMeterCount = 0; peakMeterCursor = 0; peakHeight = 0.0f; diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index 637f2ba8c5..027777dbc2 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -732,6 +732,13 @@ static void TestPeakMeterHistory() { Patch(sampler, master); Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); RenderI16(system); + Require(height() == 0.0f); + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1, 48000, 0, 0, 0}}); + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, half.data(), 0, 1, &block}); + Patch(sampler, master); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_PEAK_METER, Ngs2VoicePeakMeterParam{{}, 2, 0}); + RenderI16(system); Require(Near(height(), std::sqrt(0.125f))); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); }