Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 40 additions & 26 deletions core/libs/prx/libSceNgs2.native/src/Voice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<const Ngs2SamplerVoiceFilterParam&>(param);
}
if (param.size != sizeof(Ngs2Ps4SamplerVoiceFilterParam)) throw Ngs2ReturnCode{SCE_NGS2_ERROR_PS4_INVALID_VOICE_CONTROL_SIZE};
const auto& ps4 = reinterpret_cast<const Ngs2Ps4SamplerVoiceFilterParam&>(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<Ngs2VoiceEventParam>(param).event_id;
Expand Down Expand Up @@ -612,6 +642,14 @@ static Ngs2PendingControl CaptureParamControl(Ngs2Voice& voice, const Ngs2VoiceP
ParamAs<Ngs2VoiceCallbackParam>(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;
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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<const Ngs2Ps4SamplerVoiceFilterParam&>(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<const Ngs2SamplerVoiceFilterParam&>(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<Ngs2VoicePeakMeterParam>(param).detect_type); break;
default: throw std::runtime_error("NGS2: sampler voice param " + Ngs2Hex(param.id) + " is not implemented");
}
Expand Down
68 changes: 66 additions & 2 deletions core/libs/prx/libSceNgs2.native/tests/Ngs2JobBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<float, 64> 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<std::int16_t, 1024> 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<std::uint32_t>(sizeof(pcm)), 0, 0, static_cast<std::uint32_t>(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<const Ngs2VoiceParamHeader*, 4> rejected{&badType.header, &badIndex.header, &badLocation.header, &truncated};
std::array<int, 4> 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<std::int16_t, 64> pcm{};
Expand Down Expand Up @@ -748,7 +811,7 @@ static void TestPortVolumeOver() {

int main(int argc, char** argv) {
if (argc > 1) {
const std::pair<const char*, void (*)()> scenarios[] = {{"over", TestPortVolumeOver}, {"validation", TestCaptureValidation}, {"drained", TestBufferedAppendAfterDrain},
const std::pair<const char*, void (*)()> 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;
Expand Down Expand Up @@ -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;
}
Loading