Merge pull request #11354 from edubox/feat/ngs2-peak-meter

feat(ngs2): implement the sampler and submixer peak meter
This commit is contained in:
Adrià authored and GitHub committed 2026-10-28 15:16:05 +01:00
commit acd13871af
8 files changed
+294

No files matched your search

@@ -30,6 +30,8 @@ static constexpr int SCE_NGS2_ERROR_INVALID_PAN_WORK = static_cast<int>(0x804A84
static constexpr int SCE_NGS2_ERROR_INVALID_PAN_PARAM = static_cast<int>(0x804A8454);
static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_HANDLE = static_cast<int>(0x804A8203);
static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_ACCESS = static_cast<int>(0x804A8351);
static constexpr int SCE_NGS2_ERROR_INVALID_PEAK_METER_BLOCKS = static_cast<int>(0x804A8092);
static constexpr int SCE_NGS2_ERROR_INVALID_PEAK_METER_MODE = static_cast<int>(0x804A8093);
static constexpr int SCE_NGS2_ERROR_NULL_PARAM_LIST = static_cast<int>(0x804A8360);
static constexpr int SCE_NGS2_ERROR_INVALID_PARAM_ID = static_cast<int>(0x804A835A);
static constexpr int SCE_NGS2_ERROR_INVALID_GEOM_LISTENER_ADDRESS = static_cast<int>(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;
@@ -151,6 +151,11 @@ struct Ngs2Voice {
std::uint32_t userFxFlags = 0;
std::vector<float> samples;
std::vector<std::vector<float>> customBuffers;
std::vector<std::vector<float>> 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();
@@ -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);
@@ -248,6 +248,48 @@ static void MixInputs(Ngs2Voice& voice, const std::vector<Ngs2Voice*>& 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<float>(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<std::uint32_t>(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<float>(voice.peakMeterCount * grain));
}
height = std::max(height, channelPeak);
}
voice.peakHeight = height;
}
static void RenderVoice(Ngs2Voice& voice, const std::vector<Ngs2Voice*>& 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<Ngs2Voice*>& 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<std::size_t>(grain) * voice.channels) voice.samples.assign(static_cast<std::size_t>(grain) * voice.channels, 0.0f);
voice.rendered = true;
@@ -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<Ngs2SamplerVoiceFilterParam>(param)); break;
case SCE_NGS2_SAMPLER_VOICE_PARAM_PEAK_METER: SetPeakMeter(voice, ParamAs<Ngs2VoicePeakMeterParam>(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<Ngs2VoicePeakMeterParam>(param).detect_type);
return;
}
if (param.id == SCE_NGS2_SUBMIXER_VOICE_PARAM_USER_FX) {
const auto& fx = ParamAs<Ngs2SubmixerVoiceUserFxParam>(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<Ngs2SubmixerVoiceState*>(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: {
@@ -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<std::int16_t> 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<float> 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<std::uint64_t> 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<std::int16_t>& 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<std::uint32_t>(pcm.size()), 0, 0x55};
Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block});
return voice;
};
std::vector<std::int16_t> pcm;
for (int gain = 0; gain < 3; gain++) {
for (std::uint32_t i = 0; i < Grain; i++) pcm.push_back(static_cast<std::int16_t>(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<std::int16_t> half(Grain, 0);
for (std::uint32_t i = 0; i < Grain / 2; i++) half[i] = static_cast<std::int16_t>(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();
@@ -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;
+1
View File
@@ -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