feat(libSceNgs2.native): add custom voice filter support and related tests for 0x13 issue case

This commit is contained in:
Jnbla6 2026-10-12 16:34:27 +03:00 committed by Dean Galvin
parent 855823e85e
commit ef52b16920
6 changed files with 138 additions and 8 deletions

View File

@ -63,10 +63,12 @@ static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_
static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_EXIT_LOOP = 0x40010004;
static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_PITCH = 0x40010005;
static constexpr std::uint32_t SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP = 0x40020000;
static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER = 0x40001300;
static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 = 0x40001f00;
static constexpr std::uint32_t SCE_NGS2_CUSTOM_MAX_MODULES = 24;
static constexpr std::uint32_t SCE_NGS2_CUSTOM_MAX_PORTS = 16;
static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_FILTER = 0x13;
static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2 = 0x1f;
static constexpr std::uint32_t SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE = 1;
@ -507,6 +509,33 @@ struct Ngs2CustomVoiceUserFx2Param {
};
static_assert(sizeof(Ngs2CustomVoiceUserFx2Param) == 24);
struct Ngs2CustomVoiceFilterParam {
Ngs2VoiceParamHeader header;
std::uint32_t type;
std::uint32_t channel_mask;
union {
struct {
float i0;
float i1;
float i2;
float o1;
float o2;
} direct;
struct {
float frequency;
float q;
float level;
std::uint32_t reserved;
std::uint32_t reserved2;
} fcq;
} param;
std::uint32_t reserved3;
};
static_assert(sizeof(Ngs2CustomVoiceFilterParam) == 40);
static_assert(offsetof(Ngs2CustomVoiceFilterParam, type) == 8);
static_assert(offsetof(Ngs2CustomVoiceFilterParam, channel_mask) == 12);
static_assert(offsetof(Ngs2CustomVoiceFilterParam, param) == 16);
struct Ngs2MasteringVoiceSetupParam {
Ngs2VoiceParamHeader header;
std::uint32_t num_io_channels;

View File

@ -17,10 +17,16 @@ void Ngs2CheckCustomRack(const Ngs2CustomRackOption& option) {
if (option.num_modules > SCE_NGS2_CUSTOM_MAX_MODULES || option.rack_option.max_ports > SCE_NGS2_CUSTOM_MAX_PORTS) APS5_INVALID_ARG_EX;
for (std::uint32_t m = 0; m < option.num_modules; m++) {
const auto& info = option.module[m];
if (info.module_id != SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) throw std::runtime_error("NGS2: custom module id " + Ngs2Hex(info.module_id) + " is not implemented");
if (info.option == nullptr || info.option->size != sizeof(Ngs2CustomUserFx2ModuleOption)) APS5_INVALID_ARG_EX;
if (info.source_buffer_id != 0 || info.dest_buffer_id != 0) APS5_INVALID_ARG_EX;
if (reinterpret_cast<const Ngs2CustomUserFx2ModuleOption*>(info.option)->process_handler == nullptr) APS5_INVALID_ARG_EX;
if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) {
if (info.option == nullptr || info.option->size != sizeof(Ngs2CustomUserFx2ModuleOption)) APS5_INVALID_ARG_EX;
if (info.source_buffer_id != 0 || info.dest_buffer_id != 0) APS5_INVALID_ARG_EX;
if (reinterpret_cast<const Ngs2CustomUserFx2ModuleOption*>(info.option)->process_handler == nullptr) APS5_INVALID_ARG_EX;
} else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_FILTER) {
if (info.option != nullptr || info.state_size != 0 || info.state_offset != 0) APS5_INVALID_ARG_EX;
if (info.source_buffer_id != 0 || info.extra_buffer_id != 0 || info.dest_buffer_id != 0) APS5_INVALID_ARG_EX;
} else {
throw std::runtime_error("NGS2: custom module id " + Ngs2Hex(info.module_id) + " is not implemented");
}
}
for (std::uint32_t p = 0; p < option.rack_option.max_ports; p++) {
if (option.port[p].source_buffer_id != 0) APS5_INVALID_ARG_EX;
@ -34,10 +40,13 @@ static Ngs2UserFx2SetupContext UserFxContext(Ngs2Rack& rack, std::size_t module,
}
void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option) {
rack.customModuleIds.resize(option.num_modules);
rack.userFx.resize(option.num_modules);
rack.userFxCommon.resize(option.num_modules);
for (auto& voice : rack.voices) voice.userFx.resize(option.num_modules);
for (std::uint32_t m = 0; m < option.num_modules; m++) {
rack.customModuleIds[m] = option.module[m].module_id;
if (option.module[m].module_id == SCE_NGS2_CUSTOM_MODULE_ID_FILTER) continue;
const auto& module = *reinterpret_cast<const Ngs2CustomUserFx2ModuleOption*>(option.module[m].option);
rack.userFx[m] = module;
rack.userFxCommon[m].resize(module.common_size);
@ -58,6 +67,7 @@ void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option) {
void Ngs2CleanupUserFx(Ngs2Rack& rack) {
for (std::uint32_t i = 0; i < rack.voices.size(); i++) {
for (std::size_t m = 0; m < rack.userFx.size(); m++) {
if (rack.customModuleIds[m] != SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) continue;
if (rack.userFx[m].cleanup_handler == nullptr) continue;
auto context = UserFxContext(rack, m, i);
const int result = rack.userFx[m].cleanup_handler(&context);
@ -90,6 +100,27 @@ void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) {
SetupCustomSubmixer(voice, ParamAs<Ngs2CustomSubmixerVoiceSetupParam>(param));
return;
}
if ((param.id & ~0x1fu) == SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER) {
if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SAMPLER && rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) {
throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack");
}
const auto index = param.id & 0x1f;
if (index >= voice.rack->customModuleIds.size() || voice.rack->customModuleIds[index] != SCE_NGS2_CUSTOM_MODULE_ID_FILTER) APS5_INVALID_ARG_EX;
const auto& custom = ParamAs<Ngs2CustomVoiceFilterParam>(param);
Ngs2SamplerVoiceFilterParam filter{};
filter.header = custom.header;
filter.index = index;
filter.location = 1;
filter.type = custom.type;
filter.channel_mask = custom.channel_mask;
if (custom.type == 1) {
filter.frequency = custom.param.fcq.frequency;
filter.q = custom.param.fcq.q;
filter.level = custom.param.fcq.level;
}
Ngs2SetFilter(voice, filter);
return;
}
if ((param.id & ~0x1fu) == SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2) {
if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SAMPLER && rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) {
throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack");
@ -102,10 +133,12 @@ void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) {
}
void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate) {
if (voice.userFx.empty()) return;
if (voice.userFx.empty() || std::none_of(voice.rack->customModuleIds.begin(), voice.rack->customModuleIds.end(),
[](std::uint32_t id) { return id == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2; })) return;
std::vector<float*> channels(voice.channels);
for (std::uint32_t c = 0; c < voice.channels; c++) channels[c] = voice.samples.data() + static_cast<std::size_t>(c) * grain;
for (std::size_t m = 0; m < voice.userFx.size(); m++) {
if (voice.rack->customModuleIds[m] != SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) continue;
auto& fx = voice.userFx[m];
const auto& module = voice.rack->userFx[m];
Ngs2UserFx2ProcessContext context{channels.data(), voice.rack->userFxCommon[m].data(), fx.param.data(), fx.work.data(), fx.state.data(),

View File

@ -156,6 +156,7 @@ struct Ngs2Rack {
std::vector<Ngs2Voice> voices;
std::vector<Ngs2CustomUserFx2ModuleOption> userFx;
std::vector<std::vector<std::uint8_t>> userFxCommon;
std::vector<std::uint32_t> customModuleIds;
};
struct Ngs2System {
@ -185,6 +186,7 @@ void* Ngs2Place(const Ngs2ContextBufferInfo* bufferInfo, std::size_t size, std::
int Ngs2DestroyRack(Ngs2Rack& rack, Ngs2ContextBufferInfo* outBufferInfo);
int Ngs2ReleaseBuffer(const Ngs2BufferAllocator& allocator, Ngs2ContextBufferInfo bufferInfo, Ngs2ContextBufferInfo* outBufferInfo);
void Ngs2SetupAtrac9(Ngs2Voice& voice, const Ngs2WaveformFormat& format);
void Ngs2SetFilter(Ngs2Voice& voice, const Ngs2SamplerVoiceFilterParam& param);
std::size_t Ngs2Atrac9BlockBytes(const Ngs2Voice& voice, const Ngs2WaveformBlock& block);
void Ngs2RestartAtrac9(Ngs2Voice& voice);
void Ngs2ConsumeAtrac9(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t systemRate);

View File

@ -114,7 +114,9 @@ static int CreateRack(Ngs2Handle systemHandle, std::uint32_t rackId, const RackO
rack->system = system;
rack->rackId = rackId;
rack->maxChannels = RackMaxChannels(rackId, options);
rack->maxFilters = rackId == SCE_NGS2_RACK_ID_SAMPLER ? options.sampler.max_filters : 0;
rack->maxFilters = rackId == SCE_NGS2_RACK_ID_SAMPLER ? options.sampler.max_filters
: rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER ? options.customSampler.custom_rack_option.num_modules
: rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER ? options.customSubmixer.custom_rack_option.num_modules : 0;
rack->uid = nextUid++;
std::memcpy(rack->name, options.common.name, sizeof(rack->name));
rack->name[sizeof(rack->name) - 1] = '\0';

View File

@ -248,7 +248,7 @@ static void AddWaveformBlocks(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBl
}
}
static void SetFilter(Ngs2Voice& voice, const Ngs2SamplerVoiceFilterParam& param) {
void Ngs2SetFilter(Ngs2Voice& voice, const Ngs2SamplerVoiceFilterParam& param) {
if (param.index >= voice.rack->maxFilters) APS5_INVALID_ARG_EX;
if (voice.filters.size() <= param.index) voice.filters.resize(param.index + 1);
auto& filter = voice.filters[param.index];
@ -305,7 +305,7 @@ static void ApplySamplerParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& para
voice.pitch = ratio;
break;
}
case SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER: SetFilter(voice, ParamAs<Ngs2SamplerVoiceFilterParam>(param)); break;
case SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER: Ngs2SetFilter(voice, ParamAs<Ngs2SamplerVoiceFilterParam>(param)); break;
default: throw std::runtime_error("NGS2: sampler voice param " + Ngs2Hex(param.id) + " is not implemented");
}
}

View File

@ -90,6 +90,17 @@ static bool CreateThrows(const Ngs2CustomSubmixerRackOption& option) {
return false;
}
template <typename TException>
static bool CreateSamplerThrows(const Ngs2CustomSamplerRackOption& option) {
Ngs2ContextBufferInfo query{};
try {
sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &option.custom_rack_option.rack_option, &query);
} catch (const TException&) {
return true;
}
return false;
}
template <typename TException, typename TParam>
static bool ControlThrows(uintptr_t voice, std::uint32_t id, TParam param) {
param.header = {static_cast<std::uint16_t>(sizeof(TParam)), 0, id};
@ -274,9 +285,62 @@ static void TestCustomSampler() {
Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK);
}
static void TestCustomFilter() {
const auto system = CreateSystem();
const auto master = Mastering(system, 1);
Ngs2CustomSamplerRackOption option{};
option.custom_rack_option.rack_option.size = sizeof(option);
option.custom_rack_option.rack_option.max_grain_samples = 512;
option.custom_rack_option.rack_option.max_voices = 1;
option.custom_rack_option.num_buffers = 1;
option.custom_rack_option.num_modules = 1;
option.custom_rack_option.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_FILTER;
option.max_channel_works = 1;
auto invalid = option;
invalid.custom_rack_option.module[0].state_size = 4;
Require(CreateSamplerThrows<std::invalid_argument>(invalid));
invalid = option;
invalid.custom_rack_option.module[0].option = reinterpret_cast<const Ngs2CustomModuleOption*>(1);
Require(CreateSamplerThrows<std::invalid_argument>(invalid));
Ngs2ContextBufferInfo query{};
Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &option.custom_rack_option.rack_option, &query) == SCE_NGS2_OK);
const auto info = Buffer(query);
uintptr_t rack = 0;
Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &option.custom_rack_option.rack_option, &info, &rack) == SCE_NGS2_OK);
const auto voice = Voice(rack);
Control(voice, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_SETUP,
Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1, 48000, 0, 0, 0}});
const std::vector<std::int16_t> pcm(Grain, 16384);
const Ngs2WaveformBlock block{0, pcm.size() * sizeof(std::int16_t), 0, 0, static_cast<std::uint32_t>(pcm.size()), 0, 0};
Control(voice, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS,
Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block});
Ngs2CustomVoiceFilterParam filter{};
filter.type = 1;
filter.param.fcq.frequency = 10000.0f;
filter.param.fcq.q = 0.70710678f;
filter.param.fcq.level = 1.0f;
Control(voice, SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER, filter);
Require(ControlThrows<std::invalid_argument>(voice, SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER | 1, filter));
Patch(voice, master);
Event(voice, SCE_NGS2_VOICE_EVENT_PLAY);
std::vector<float> out(Grain, -1.0f);
const Ngs2RenderBufferInfo renderInfo{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 1};
Require(sceNgs2SystemRender(system, &renderInfo, 1) == SCE_NGS2_OK);
Require(out[0] > 0.0f && out[0] < 0.1f && out.back() > out[0]);
Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK);
Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK);
}
int main() {
TestUserFxChain();
TestRackOptions();
TestCustomSampler();
TestCustomFilter();
return 0;
}