diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index a207721f0f0f..cdde97af3dd6 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -540,6 +540,36 @@ static int ControlUserFx2(Ngs2Voice& voice, std::uint32_t index, const Ngs2Custo static Ngs2Voice* PatchDest(const Ngs2Voice& voice, const Ngs2VoicePatchParam& patch); +static void CheckFilter(const Ngs2Voice& voice, const Ngs2SamplerVoiceFilterParam& param) { + if (param.header.size < sizeof(Ngs2SamplerVoiceFilterParam)) throw Ngs2ReturnCode{SCE_NGS2_ERROR_INVALID_VOICE_CONTROL_SIZE}; + if (param.index >= voice.rack->maxFilters) throw Ngs2ReturnCode{PS4_LIBRARY ? SCE_NGS2_ERROR_PS4_INVALID_FILTER_INDEX : SCE_NGS2_ERROR_INVALID_PARAM_ID}; + if (!PS4_LIBRARY && param.type >= 12) throw Ngs2ReturnCode{SCE_NGS2_ERROR_INVALID_FILTER_TYPE}; + if (PS4_LIBRARY ? param.location >= PS4_MAX_FILTER_LOCATIONS : param.location >= std::uint64_t{2} + voice.ports.size()) { + throw Ngs2ReturnCode{PS4_LIBRARY ? SCE_NGS2_ERROR_PS4_INVALID_FILTER_LOCATION : SCE_NGS2_ERROR_INVALID_MODULE_LOCATION}; + } +} + +static Ngs2SamplerVoiceFilterParam SamplerFilterParam(const Ngs2VoiceParamHeader& param) { + if (!PS4_LIBRARY) { + if (param.size < sizeof(Ngs2SamplerVoiceFilterParam)) throw Ngs2ReturnCode{SCE_NGS2_ERROR_INVALID_VOICE_CONTROL_SIZE}; + return reinterpret_cast(param); + } + if (param.size != sizeof(Ngs2Ps4SamplerVoiceFilterParam)) throw Ngs2ReturnCode{SCE_NGS2_ERROR_PS4_INVALID_VOICE_CONTROL_SIZE}; + const auto& ps4 = reinterpret_cast(param); + Ngs2SamplerVoiceFilterParam filter{}; + filter.header = {sizeof(filter), 0, param.id}; + filter.index = ps4.index; + filter.location = ps4.location; + filter.channel_mask = ps4.channel_mask; + filter.type = ps4.type; + filter.frequency = ps4.fcq.frequency; + filter.q = ps4.fcq.q; + filter.level = ps4.fcq.level; + filter.reserved[0] = ps4.fcq.reserved; + filter.reserved[1] = ps4.fcq.reserved2; + return filter; +} + static Ngs2PendingControl CaptureParamControl(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { if (param.id == SCE_NGS2_VOICE_PARAM_EVENT) { const auto event = ParamAs(param).event_id; @@ -612,6 +642,14 @@ static Ngs2PendingControl CaptureParamControl(Ngs2Voice& voice, const Ngs2VoiceP ParamAs(param); return CopyParam(param); } + if (param.id == SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER) { + if (voice.rack->rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) { + throw Ngs2ReturnCode{PS4_LIBRARY ? SCE_NGS2_ERROR_PS4_INVALID_VOICE_CONTROL_ID : SCE_NGS2_ERROR_INVALID_PARAM_ID}; + } + if (voice.rack->rackId != SCE_NGS2_RACK_ID_SAMPLER) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); + CheckFilter(voice, SamplerFilterParam(param)); + return CopyParam(param); + } if (voice.rack->rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER && (param.id & ~0x1fu) == SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX) { const auto index = param.id & 0x1fu; if (index >= voice.customUserFx.size() || voice.rack->customModuleIds[index] != SCE_NGS2_CUSTOM_MODULE_ID_USER_FX) APS5_INVALID_ARG_EX; @@ -888,12 +926,7 @@ static void AddWaveformBlocks(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBl } void Ngs2SetFilter(Ngs2Voice& voice, const Ngs2SamplerVoiceFilterParam& param) { - if (param.header.size < sizeof(Ngs2SamplerVoiceFilterParam)) throw Ngs2ReturnCode{SCE_NGS2_ERROR_INVALID_VOICE_CONTROL_SIZE}; - if (param.index >= voice.rack->maxFilters) throw Ngs2ReturnCode{PS4_LIBRARY ? SCE_NGS2_ERROR_PS4_INVALID_FILTER_INDEX : SCE_NGS2_ERROR_INVALID_PARAM_ID}; - if (!PS4_LIBRARY && param.type >= 12) throw Ngs2ReturnCode{SCE_NGS2_ERROR_INVALID_FILTER_TYPE}; - if (PS4_LIBRARY ? param.location >= PS4_MAX_FILTER_LOCATIONS : param.location >= std::uint64_t{2} + voice.ports.size()) { - throw Ngs2ReturnCode{PS4_LIBRARY ? SCE_NGS2_ERROR_PS4_INVALID_FILTER_LOCATION : SCE_NGS2_ERROR_INVALID_MODULE_LOCATION}; - } + CheckFilter(voice, param); if (voice.filters.size() <= param.index) voice.filters.resize(param.index + 1); auto& filter = voice.filters[param.index]; if (param.type == 0 || (PS4_LIBRARY && param.type >= PS4_FIRST_UNFILTERED_TYPE)) { @@ -959,26 +992,7 @@ static void ApplySamplerParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& para voice.pitch = std::clamp(ratio, 0.0f, 4.0f); break; } - case SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER: - if (PS4_LIBRARY) { - if (param.size != sizeof(Ngs2Ps4SamplerVoiceFilterParam)) throw Ngs2ReturnCode{SCE_NGS2_ERROR_PS4_INVALID_VOICE_CONTROL_SIZE}; - const auto& ps4 = reinterpret_cast(param); - Ngs2SamplerVoiceFilterParam filter{}; - filter.header = {sizeof(filter), 0, param.id}; - filter.index = ps4.index; - filter.location = ps4.location; - filter.channel_mask = ps4.channel_mask; - filter.type = ps4.type; - filter.frequency = ps4.fcq.frequency; - filter.q = ps4.fcq.q; - filter.level = ps4.fcq.level; - filter.reserved[0] = ps4.fcq.reserved; - filter.reserved[1] = ps4.fcq.reserved2; - Ngs2SetFilter(voice, filter); - } else { - Ngs2SetFilter(voice, reinterpret_cast(param)); - } - break; + case SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER: Ngs2SetFilter(voice, SamplerFilterParam(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"); } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2JobBuffer.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2JobBuffer.cpp index f87b73305154..75b1e1ab6fdb 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2JobBuffer.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2JobBuffer.cpp @@ -572,6 +572,69 @@ static void TestBufferedWaveformCodes() { ResetBuffers(firstBuffer); } +static Ngs2SamplerVoiceFilterParam LowPass(std::uint32_t index, std::uint32_t location, std::uint32_t type) { + Ngs2SamplerVoiceFilterParam param{}; + param.header = {sizeof(param), 0, SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER}; + param.index = index; + param.location = location; + param.type = type; + param.frequency = 1000.0f; + param.q = 0.707f; + param.level = 1.0f; + return param; +} + +static float RenderEnergy(std::uintptr_t system) { + std::array samples{}; + const Ngs2RenderBufferInfo output{samples.data(), sizeof(samples), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 1}; + Require(sceNgs2SystemRender(system, &output, 1) == SCE_NGS2_OK); + float energy = 0.0f; + for (const float sample : samples) energy += sample * sample; + return energy; +} + +static void TestBufferedSamplerFilter() { + const auto firstBuffer = usedBuffers; + std::array pcm{}; + for (std::size_t i = 0; i < pcm.size(); ++i) pcm[i] = i % 2 == 0 ? 8000 : -8000; + PeerAction peer; + const auto setup = CreatePeerSystem(peer); + const auto voice = MonoSampler(setup.system); + Require(AddBlocks(voice, pcm.data(), 0, {0, static_cast(sizeof(pcm)), 0, 0, static_cast(pcm.size()), 0, 0}) == SCE_NGS2_OK); + Patch(voice, setup.master); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + const auto direct = MonoSampler(setup.system); + const auto custom = Sampler(setup.system, true); + const auto badType = LowPass(0, 1, 12); + const auto badIndex = LowPass(8, 1, 1); + const auto badLocation = LowPass(0, 1000, 1); + const Ngs2VoiceParamHeader truncated{sizeof(Ngs2VoiceParamHeader), 0, SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER}; + const auto filter = LowPass(0, 1, 1); + const std::array rejected{&badType.header, &badIndex.header, &badLocation.header, &truncated}; + std::array immediate{}; + for (std::size_t i = 0; i < rejected.size(); ++i) { + immediate[i] = sceNgs2VoiceControl(direct, rejected[i]); + Require(immediate[i] != SCE_NGS2_OK); + } + const int customImmediate = sceNgs2VoiceControl(custom, &filter.header); + Require(customImmediate != SCE_NGS2_OK); + unsigned checked = 0; + peer.action = [&](unsigned tick) { + if (tick != 0) return; + for (std::size_t i = 0; i < rejected.size(); ++i) Require(sceNgs2VoiceControl(voice, rejected[i]) == immediate[i]); + Require(sceNgs2VoiceControl(custom, &filter.header) == customImmediate); + Require(sceNgs2VoiceControl(voice, &filter.header) == SCE_NGS2_OK); + ++checked; + }; + const float unfiltered = RenderEnergy(setup.system); + Require(unfiltered > 1.0f); + float filtered = unfiltered; + for (unsigned grain = 0; grain < 6; ++grain) filtered = RenderEnergy(setup.system); + Require(checked == 1 && filtered < unfiltered * 1.0e-3f); + Require(sceNgs2SystemDestroy(setup.system, nullptr) == SCE_NGS2_OK); + ResetBuffers(firstBuffer); +} + static void TestBufferedAppendAfterDrain() { const auto firstBuffer = usedBuffers; std::array pcm{}; @@ -748,7 +811,7 @@ static void TestPortVolumeOver() { int main(int argc, char** argv) { if (argc > 1) { - const std::pair scenarios[] = {{"over", TestPortVolumeOver}, {"validation", TestCaptureValidation}, {"drained", TestBufferedAppendAfterDrain}, + const std::pair scenarios[] = {{"over", TestPortVolumeOver}, {"validation", TestCaptureValidation}, {"filter", TestBufferedSamplerFilter}, {"drained", TestBufferedAppendAfterDrain}, {"emptysetup", TestEmptySetupPort}, {"destroyed", TestPendingPatchDestroyedDest}, {"race", TestBlockQueueRace}}; for (const auto& [name, run] : scenarios) { if (std::strcmp(argv[1], name) != 0) continue; @@ -797,11 +860,12 @@ int main(int argc, char** argv) { ++cases; TestCaptureValidation(); TestBufferedWaveformCodes(); + TestBufferedSamplerFilter(); TestBufferedAppendAfterDrain(); TestEmptySetupPort(); TestPendingPatchDestroyedDest(); TestBlockQueueRace(); - cases += 6; + cases += 7; std::printf("NGS2 worker buffering: %u scenarios passed\n", cases + 1); return 0; }