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..48f8e11d76 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -89,6 +89,11 @@ void Ngs2Voice::ResetSetup() { callback = nullptr; callbackData = 0; callbackFlags = 0; + peakMeterHistory.clear(); + peakMeterMode = 0; + peakMeterCount = 0; + peakMeterCursor = 0; + peakHeight = 0.0f; } const std::uint8_t* Ngs2StreamEnd(const Ngs2Voice& voice, const Ngs2Block& block) { @@ -322,6 +327,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 +362,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 +388,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 +510,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 +528,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..027777dbc2 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,202 @@ 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 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(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); + } +} + static void TestSubmixerMatrix() { const auto system = CreateSystem(); const auto master = Mastering(system, 2); @@ -1796,6 +1993,8 @@ int main() { TestPan(); TestPanExplicitAnglesAndNanDistance(); TestPitchAndRepeat(); + TestPeakMeter(); + TestPeakMeterHistory(); 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