diff --git a/src/libs/ngs2.cpp b/src/libs/ngs2.cpp index 3c89831ff..f6111454d 100644 --- a/src/libs/ngs2.cpp +++ b/src/libs/ngs2.cpp @@ -344,7 +344,16 @@ enum class Ngs2RackType { CustomSampler, }; -struct Ngs2UserFxContext { +struct Ngs2UserFxProcessContext { + float** channels; + std::array user_data; + uint32_t flags; + uint32_t num_channels; + uint32_t grain_samples; + uint32_t sample_rate; +}; + +struct Ngs2UserFx2Context { void* common; void* param; void* work; @@ -354,7 +363,7 @@ struct Ngs2UserFxContext { uint64_t reserved[4] {}; }; -struct Ngs2UserFxProcessContext { +struct Ngs2UserFx2ProcessContext { float** channels; void* common; const void* param; @@ -370,18 +379,27 @@ struct Ngs2UserFxProcessContext { uint64_t reserved2[4] {}; }; -struct Ngs2UserFxOption { +struct Ngs2UserFx2Option { Ngs2CustomModuleOption header; - int KYTY_SYSV_ABI (*setup)(Ngs2UserFxContext*); - int KYTY_SYSV_ABI (*cleanup)(Ngs2UserFxContext*); + int KYTY_SYSV_ABI (*setup)(Ngs2UserFx2Context*); + int KYTY_SYSV_ABI (*cleanup)(Ngs2UserFx2Context*); uintptr_t control; - int KYTY_SYSV_ABI (*process)(Ngs2UserFxProcessContext*); + int KYTY_SYSV_ABI (*process)(Ngs2UserFx2ProcessContext*); size_t common_size; size_t param_size; size_t work_size; uintptr_t user_data; }; +static bool Ngs2RackIsCustom(Ngs2RackType type) { + switch (type) { + case Ngs2RackType::CustomSubmixer: + case Ngs2RackType::CustomMastering: + case Ngs2RackType::CustomSampler: return true; + default: return false; + } +} + struct Ngs2RackInternal { Ngs2Internal* ngs = nullptr; Ngs2RackInternal* next = nullptr; @@ -390,7 +408,16 @@ struct Ngs2RackInternal { Ngs2ContextBufferInfo buffer_info; Ngs2BufferAllocator allocator; std::array, 24> common; - std::array fx {}; + std::array fx {}; + + [[nodiscard]] uint32_t NumBuffers() const { + return Ngs2RackIsCustom(type) ? option.custom_sampler.custom_rack_option.num_buffers : 1; + } + + [[nodiscard]] uint32_t PortBufferId(size_t port) const { + const auto& custom = option.custom_sampler.custom_rack_option; + return Ngs2RackIsCustom(type) ? custom.port[port].source_buffer_id : 0; + } }; enum class Ngs2VoicePlayState : uint32_t { Empty = 0, Playing = 3, Paused = 5, Stopped = 0xb }; @@ -519,10 +546,12 @@ struct Ngs2VoiceInternal { uint32_t repeated_count = 0; }; struct Module { - std::vector param, work, state; - uint32_t flags = 1; - bool control_warned = false; - bool render_warned = false; + std::vector param, work, state; + void KYTY_SYSV_ABI (*handler)(Ngs2UserFxProcessContext*) = nullptr; + std::array user_data {}; + uint32_t flags = 1; + bool control_warned = false; + bool render_warned = false; }; Ngs2VoicePlayState state = Ngs2VoicePlayState::Empty; uint32_t state_flags = 0; @@ -572,6 +601,15 @@ struct Ngs2VoiceInternal { channels * sizeof(int16_t)); } + // Custom racks keep num_buffers consecutive channel-major buffers; other racks keep one. + void ClearSamples(uint32_t grain) { + samples.assign(size_t(grain) * channels * rack->NumBuffers(), 0.0f); + } + + float* Buffer(uint32_t id, uint32_t grain) { + return samples.data() + size_t(id) * grain * channels; + } + void ResetSetupState() { SetEvent(4); std::ranges::fill(ports, Port {}); @@ -839,9 +877,10 @@ static Ngs2RackInternal* g_racks_list = nullptr; static std::atomic_uint32_t g_next_ngs_uid = 1; static Common::Mutex g_racks_mutex; -static_assert(sizeof(Ngs2UserFxContext) == 72); -static_assert(sizeof(Ngs2UserFxProcessContext) == 104); -static_assert(sizeof(Ngs2UserFxOption) == 72); +static_assert(sizeof(Ngs2UserFxProcessContext) == 48); +static_assert(sizeof(Ngs2UserFx2Context) == 72); +static_assert(sizeof(Ngs2UserFx2ProcessContext) == 104); +static_assert(sizeof(Ngs2UserFx2Option) == 72); static_assert(sizeof(Ngs2SystemOption) == 144); static_assert(sizeof(Ngs2SystemInfo) == 184); static_assert(sizeof(Ngs2RackOption) == 176); @@ -900,15 +939,6 @@ static Ngs2Internal* Ngs2FindSystem(uintptr_t system_handle) { return nullptr; } -static bool Ngs2RackIsCustom(Ngs2RackType type) { - switch (type) { - case Ngs2RackType::CustomSubmixer: - case Ngs2RackType::CustomMastering: - case Ngs2RackType::CustomSampler: return true; - default: return false; - } -} - int KYTY_SYSV_ABI Ngs2SystemQueryBufferSize(const Ngs2SystemOption* option, Ngs2ContextBufferInfo* buffer_info) { PRINT_NAME(); @@ -1188,14 +1218,23 @@ static void Ngs2InitModules(Ngs2RackInternal& rack) { return; } const auto& custom = rack.option.custom_sampler.custom_rack_option; - EXIT_NOT_IMPLEMENTED(custom.num_buffers != 1); + EXIT_NOT_IMPLEMENTED(custom.num_buffers == 0 || custom.num_buffers > 4 || + rack.option.common.max_ports > 16); + for (uint32_t p = 0; p < rack.option.common.max_ports; ++p) { + EXIT_NOT_IMPLEMENTED(custom.port[p].source_buffer_id >= custom.num_buffers); + } auto* voices = reinterpret_cast(&rack + 1); for (uint32_t m = 0; m < custom.num_modules; ++m) { - if (custom.module[m].module_id != 0x1f) { + const auto& info = custom.module[m]; + // Modules process their source buffer in place, so skipped modules pass audio through. + EXIT_NOT_IMPLEMENTED(info.source_buffer_id != info.dest_buffer_id || + std::max(info.source_buffer_id, info.extra_buffer_id) >= + custom.num_buffers); + if (info.module_id != 0x1f) { continue; } auto& fx = rack.fx[m]; - fx = *reinterpret_cast(custom.module[m].option); + fx = *reinterpret_cast(info.option); rack.common[m].resize(fx.common_size); for (uint32_t i = 0; i < rack.option.common.max_voices; ++i) { auto& module = voices[i].modules[m]; @@ -1203,7 +1242,7 @@ static void Ngs2InitModules(Ngs2RackInternal& rack) { module.work.resize(fx.work_size); module.state.resize(custom.module[m].state_size); if (fx.setup != nullptr) { - Ngs2UserFxContext context { + Ngs2UserFx2Context context { rack.common[m].data(), module.param.data(), module.work.data(), fx.user_data, rack.option.common.max_voices, i}; EXIT_NOT_IMPLEMENTED(fx.setup(&context) != OK); @@ -1430,12 +1469,12 @@ int KYTY_SYSV_ABI Ngs2RackDestroy(uintptr_t rack_handle, Ngs2ContextBufferInfo* const auto& fx = rack->fx[m]; if (fx.cleanup != nullptr) { auto& module = voices[i].modules[m]; - Ngs2UserFxContext context {rack->common[m].data(), - module.param.data(), - module.work.data(), - fx.user_data, - rack->option.common.max_voices, - i}; + Ngs2UserFx2Context context {rack->common[m].data(), + module.param.data(), + module.work.data(), + fx.user_data, + rack->option.common.max_voices, + i}; EXIT_NOT_IMPLEMENTED(fx.cleanup(&context) != OK); } } @@ -1729,7 +1768,7 @@ static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) { } if (voice.source_generation != generation) { // A callback replaced the source, possibly changing its channel layout. - voice.samples.assign(size_t(grain) * voice.channels, 0.0f); + voice.ClearSamples(grain); output = 0; } voice.has_samples = output != 0; @@ -1747,7 +1786,7 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vectortype == Ngs2RackType::Sampler || @@ -1770,7 +1809,8 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vectorports) { + for (size_t p = 0; p < source->ports.size(); ++p) { + const auto& port = source->ports[p]; if (port.dest != &voice || port.volume == 0.0f) { continue; } @@ -1778,7 +1818,10 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vectorhas_samples) { continue; } - EXIT_NOT_IMPLEMENTED(port.input != 0); + // Input N of a custom rack voice feeds its buffer N. + EXIT_NOT_IMPLEMENTED(port.input >= voice.rack->NumBuffers()); + const auto* input = source->Buffer(source->rack->PortBufferId(p), grain); + auto* output = voice.Buffer(port.input, grain); const auto* matrix = port.matrix < 0 ? nullptr : &source->matrices.at(port.matrix); const size_t output_channels = matrix == nullptr @@ -1791,8 +1834,7 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vectorchannels + src]); for (uint32_t i = 0; i < grain; ++i) { - voice.samples[dst * grain + i] += - source->samples[src * grain + i] * level; + output[dst * grain + i] += input[src * grain + i] * level; } } } @@ -1801,36 +1843,50 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vectoroption.custom_sampler.custom_rack_option; + const auto rate = voice.rack->ngs->option.sample_rate; for (size_t m = 0; m < voice.modules.size(); ++m) { - if (custom.module[m].module_id != 0x1f) { - if (!voice.modules[m].render_warned) { + const auto& info = custom.module[m]; + auto& module = voice.modules[m]; + if (info.module_id != 0x19 && info.module_id != 0x1f) { + if (!module.render_warned) { Log::WriteToConsoleAndLog(fmt::sprintf( "warning: NGS2 custom module 0x%02x at index %zu is ignored during render\n", - custom.module[m].module_id, m)); - voice.modules[m].render_warned = true; + info.module_id, m)); + module.render_warned = true; } continue; } - EXIT_NOT_IMPLEMENTED(custom.module[m].source_buffer_id != 0 || - custom.module[m].dest_buffer_id != 0); - auto& module = voice.modules[m]; - const auto& fx = voice.rack->fx[m]; - std::vector channels(voice.channels); - for (uint32_t c = 0; c < voice.channels; ++c) { - channels[c] = voice.samples.data() + c * grain; + if (info.module_id == 0x19 && module.handler == nullptr) { + continue; + } + // Handlers receive the source channels followed by the extra buffer's channels. + std::vector channels(2 * size_t(voice.channels)); + auto* source = voice.Buffer(info.source_buffer_id, grain); + auto* extra = voice.Buffer(info.extra_buffer_id, grain); + for (uint32_t c = 0; c < voice.channels; ++c) { + channels[c] = source + c * grain; + channels[voice.channels + c] = extra + c * grain; + } + if (info.module_id == 0x19) { + Ngs2UserFxProcessContext context {channels.data(), module.user_data, + module.flags, voice.channels, + grain, rate}; + module.handler(&context); + } else { + const auto& fx = voice.rack->fx[m]; + Ngs2UserFx2ProcessContext context {channels.data(), + voice.rack->common[m].data(), + module.param.data(), + module.work.data(), + module.state.data(), + fx.user_data, + module.flags, + voice.channels, + voice.channels, + grain, + rate}; + EXIT_NOT_IMPLEMENTED(fx.process(&context) != OK); } - Ngs2UserFxProcessContext context {channels.data(), - voice.rack->common[m].data(), - module.param.data(), - module.work.data(), - module.state.data(), - fx.user_data, - module.flags, - voice.channels, - voice.channels, - grain, - voice.rack->ngs->option.sample_rate}; - EXIT_NOT_IMPLEMENTED(fx.process(&context) != OK); module.flags = 0; } } @@ -2387,24 +2443,39 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH EXIT_NOT_IMPLEMENTED(!Ngs2RackIsCustom(voice->rack->type)); const auto index = param->id & 0x1fu; EXIT_NOT_IMPLEMENTED(index >= voice->modules.size()); - if ((param->id & 0xffffffe0u) != 0x40001f00u || - voice->rack->option.custom_sampler.custom_rack_option.module[index].module_id != - 0x1f) { - if (!voice->modules[index].control_warned) { + const auto module_id = + voice->rack->option.custom_sampler.custom_rack_option.module[index].module_id; + auto& module = voice->modules[index]; + if ((param->id & 0xffffffe0u) != (0x40000000u | module_id << 8u) || + (module_id != 0x19 && module_id != 0x1f)) { + if (!module.control_warned) { Log::WriteToConsoleAndLog(fmt::sprintf( "warning: NGS2 custom control 0x%08" PRIx32 " is ignored\n", param->id)); - voice->modules[index].control_warned = true; + module.control_warned = true; } break; } - struct FxParam { + if (module_id == 0x19) { + struct FxParam { + Ngs2VoiceParamHeader header; + decltype(module.handler) handler; + std::array user_data; + }; + EXIT_NOT_IMPLEMENTED(param->size != sizeof(FxParam)); + const auto& fx = *reinterpret_cast(param); + // Enabling the handler requests a reset; later updates mark user data dirty. + module.flags |= module.handler == nullptr ? 1u : 2u; + module.handler = fx.handler; + module.user_data = fx.user_data; + break; + } + struct Fx2Param { Ngs2VoiceParamHeader header; const void* data; size_t size; }; - const auto& fx = *reinterpret_cast(param); - auto& module = voice->modules[index]; + const auto& fx = *reinterpret_cast(param); EXIT_NOT_IMPLEMENTED(voice->rack->fx[index].control != 0 || fx.size != module.param.size()); std::memcpy(module.param.data(), fx.data, fx.size); diff --git a/tests/Ngs2SamplerTests.cpp b/tests/Ngs2SamplerTests.cpp index 2db528a14..bcb1fd4f7 100644 --- a/tests/Ngs2SamplerTests.cpp +++ b/tests/Ngs2SamplerTests.cpp @@ -47,6 +47,7 @@ struct Fixture { } else { rack.option.custom_sampler.max_waveform_blocks = capacity; rack.option.custom_sampler.custom_rack_option.state_size = sizeof(Ngs2CustomSamplerVoiceState); + rack.option.custom_sampler.custom_rack_option.num_buffers = 1; } } @@ -703,6 +704,53 @@ void TestSubmixerSetupAndRouting() { } } +uint32_t g_side_chain_flags = 0; + +void KYTY_SYSV_ABI MultiplyBySideChain(Ngs2UserFxProcessContext* context) { + Check(context->num_channels == 1 && context->grain_samples == 1 && + context->user_data[0] == 7, + "user FX context did not describe the custom voice"); + g_side_chain_flags = context->flags; + context->channels[0][0] *= context->channels[1][0]; +} + +void TestCustomSideChainUserFx() { + Fixture main(48000), side(48000); + Ngs2RackInternal custom_rack {}; + auto& custom = custom_rack.option.custom_submixer.custom_rack_option; + custom_rack.ngs = &main.system; + custom_rack.type = Ngs2RackType::CustomSubmixer; + custom.num_buffers = 2; + custom.num_modules = 1; + custom.module[0] = {nullptr, 0x19, 0, 1, 0}; + Ngs2VoiceInternal voice; + voice.rack = &custom_rack; + voice.ports.resize(1); + voice.modules.resize(1); + struct { + Ngs2VoiceParamHeader header {24, 0, 0x40020000}; + uint32_t inputs = 1, outputs = 1, flags = 0, reserved = 0; + } setup; + struct { + Ngs2VoiceParamHeader header; + void KYTY_SYSV_ABI (*handler)(Ngs2UserFxProcessContext*); + uintptr_t user_data[3]; + } fx {{40, 0, 0x40001900}, MultiplyBySideChain, {7, 0, 0}}; + Check(Ngs2VoiceControl(reinterpret_cast(&voice), &setup.header) == OK && + Ngs2VoiceControl(reinterpret_cast(&voice), &fx.header) == OK, + "custom submixer user FX setup failed"); + voice.state = Ngs2VoicePlayState::Playing; + main.voice.ports.push_back({&voice, 0, 1.0f, -1}); + side.voice.ports.push_back({&voice, 1, 1.0f, -1}); + const int16_t main_pcm = 8192, side_pcm = 16384; + main.Queue(&main_pcm, 1, 0); + side.Queue(&side_pcm, 1, 0); + Ngs2RenderVoice(voice, {&main.voice, &side.voice, &voice}, 1); + Check(voice.has_samples && voice.samples[0] == 0.125f && voice.samples[1] == 0.5f && + g_side_chain_flags == 1, + "user FX did not receive the side-chain buffer after the source channels"); +} + void TestPcm16RenderOutput() { Ngs2Internal system; system.option = Ngs2DefaultSystemOption(); @@ -1608,6 +1656,7 @@ int main() { TestAtrac9Discrete(); TestPcm16RenderOutput(); TestSubmixerSetupAndRouting(); + TestCustomSideChainUserFx(); TestWaveformBlockGeometry(); TestAtrac9ResampleAfterStarvation(); TestAtrac9ResampleAcrossFrames();