From 8cfe1c4d34b5b26b968335a74afda5a27a6ec058 Mon Sep 17 00:00:00 2001 From: nmzik Date: Thu, 29 Oct 2026 02:03:22 +0100 Subject: [PATCH] ngs2: apply custom channel matrices before mastering output Co-authored-by: Frenzied --- src/libs/ngs2.cpp | 45 +++++++++++++++- tests/Ngs2SamplerTests.cpp | 104 ++++++++++++++++++++++++++++++------- 2 files changed, 128 insertions(+), 21 deletions(-) diff --git a/src/libs/ngs2.cpp b/src/libs/ngs2.cpp index 02d697d48..4fac09def 100644 --- a/src/libs/ngs2.cpp +++ b/src/libs/ngs2.cpp @@ -546,6 +546,7 @@ struct Ngs2VoiceInternal { }; struct Module { std::vector param, work, state; + std::vector channel_matrix; void KYTY_SYSV_ABI (*handler)(Ngs2UserFxProcessContext*) = nullptr; std::array user_data {}; uint32_t flags = 1; @@ -563,6 +564,7 @@ struct Ngs2VoiceInternal { std::vector> matrices; std::vector modules; std::vector samples; + std::vector mix_samples; std::vector output_matrix; uint32_t channels = 0; uint32_t output_id = 0; @@ -614,6 +616,27 @@ struct Ngs2VoiceInternal { return samples.data() + size_t(id) * grain * channels; } + void MixChannels(const std::vector& matrix, uint32_t buffer, uint32_t grain) { + if (matrix.empty()) { + return; + } + EXIT_NOT_IMPLEMENTED(channels > 37); + auto* input = Buffer(buffer, grain); + mix_samples.assign(size_t(channels) * grain, 0.0f); + for (uint32_t dst = 0; dst < channels; ++dst) { + for (uint32_t src = 0; src < channels; ++src) { + const float level = matrix[dst * 37 + src]; + if (level == 0.0f) { + continue; + } + for (uint32_t i = 0; i < grain; ++i) { + mix_samples[dst * grain + i] += input[src * grain + i] * level; + } + } + } + std::copy(mix_samples.begin(), mix_samples.end(), input); + } + void ResetSetupState() { SetEvent(4); std::ranges::fill(ports, Port {}); @@ -1850,6 +1873,10 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vectoroutput_id >= num_buffer_info); const auto& output = buffer_info[voice->output_id]; EXIT_NOT_IMPLEMENTED(output.buffer == nullptr); - EXIT_NOT_IMPLEMENTED((output.waveform_type != 0x12 && output.waveform_type != 0x18) || + EXIT_NOT_IMPLEMENTED(output.waveform_type != 0x12 && output.waveform_type != 0x18); + EXIT_NOT_IMPLEMENTED(voice->rack->type == Ngs2RackType::Mastering && output.num_channels != voice->channels); EXIT_NOT_IMPLEMENTED(output.buffer_size < grain * output.num_channels * (output.waveform_type == 0x12 ? sizeof(int16_t) @@ -1954,6 +1982,9 @@ int KYTY_SYSV_ABI Ngs2SystemRender(uintptr_t system_handle, const Ngs2RenderBuff const float level = voice->output_matrix.empty() ? (src == dst ? 1.0f : 0.0f) : voice->output_matrix.at(src * output.num_channels + dst); + if (level == 0.0f) { + continue; + } for (uint32_t i = 0; i < grain; ++i) { pcm[i * output.num_channels + dst] += voice->samples[src * grain + i] * level; } @@ -2452,7 +2483,7 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH 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)) { + (module_id != 0x17 && module_id != 0x19 && module_id != 0x1f)) { if (!module.control_warned) { Log::WriteToConsoleAndLog(fmt::sprintf( "warning: NGS2 custom control 0x%08" PRIx32 " is ignored\n", @@ -2461,6 +2492,16 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH } break; } + if (module_id == 0x17) { + EXIT_NOT_IMPLEMENTED(param->size != sizeof(Ngs2VoiceParamHeader) + + 37 * 37 * sizeof(float)); + const auto* levels = reinterpret_cast(param + 1); + module.channel_matrix.resize(37 * 37); + std::transform(levels, levels + module.channel_matrix.size(), + module.channel_matrix.begin(), + [](float level) { return std::clamp(level, -32.0f, 32.0f); }); + break; + } if (module_id == 0x19) { struct FxParam { Ngs2VoiceParamHeader header; diff --git a/tests/Ngs2SamplerTests.cpp b/tests/Ngs2SamplerTests.cpp index 34761c4cd..1b1f1533c 100644 --- a/tests/Ngs2SamplerTests.cpp +++ b/tests/Ngs2SamplerTests.cpp @@ -751,6 +751,26 @@ void TestCustomSideChainUserFx() { "user FX did not receive the side-chain buffer after the source channels"); } +struct RenderRack { + std::unique_ptr storage; + uintptr_t handle = 0; + + RenderRack(Ngs2Internal& system, uint32_t id, const Ngs2RackOption& option) { + Ngs2ContextBufferInfo buffer {}; + Check(Ngs2RackQueryBufferSize(id, &option, &buffer) == OK, "rack size query failed"); + storage = std::make_unique(buffer.host_buffer_size); + buffer.host_buffer = storage.get(); + Check(Ngs2RackCreate(reinterpret_cast(&system), id, &option, &buffer, + &handle) == OK, "render rack creation failed"); + } + + ~RenderRack() { Check(Ngs2RackDestroy(handle, nullptr) == OK, "render rack cleanup failed"); } + + Ngs2VoiceInternal& Voice() { + return *reinterpret_cast(reinterpret_cast(handle) + 1); + } +}; + void TestPcm16RenderOutput() { Ngs2Internal system; system.option = Ngs2DefaultSystemOption(); @@ -760,23 +780,11 @@ void TestPcm16RenderOutput() { Ngs2FillDefaultRackOption(0x3000, &mastering_option); sampler_option.common.max_voices = 2; sampler_option.common.max_ports = 1; - alignas(Ngs2RackInternal) alignas(Ngs2VoiceInternal) - std::byte sampler_storage[sizeof(Ngs2RackInternal) + 2 * sizeof(Ngs2VoiceInternal)]; - alignas(Ngs2RackInternal) alignas(Ngs2VoiceInternal) - std::byte mastering_storage[sizeof(Ngs2RackInternal) + sizeof(Ngs2VoiceInternal)]; - Ngs2ContextBufferInfo sampler_buffer {sampler_storage, sizeof(sampler_storage)}; - Ngs2ContextBufferInfo mastering_buffer {mastering_storage, sizeof(mastering_storage)}; - uintptr_t sampler_handle = 0, mastering_handle = 0; + RenderRack sampler(system, 0x1000, sampler_option.common); + RenderRack master(system, 0x3000, mastering_option.common); const auto system_handle = reinterpret_cast(&system); - Check(Ngs2RackCreate(system_handle, 0x1000, &sampler_option.common, &sampler_buffer, - &sampler_handle) == OK && - Ngs2RackCreate(system_handle, 0x3000, &mastering_option.common, - &mastering_buffer, &mastering_handle) == OK, - "PCM16 render rack creation failed"); - auto* samplers = reinterpret_cast( - reinterpret_cast(sampler_handle) + 1); - auto& mastering = *reinterpret_cast( - reinterpret_cast(mastering_handle) + 1); + auto* samplers = &sampler.Voice(); + auto& mastering = master.Voice(); mastering.channels = 1; mastering.state = Ngs2VoicePlayState::Playing; const int16_t source[2][4] = {{16384, -16384, 32767, -32768}, @@ -797,9 +805,66 @@ void TestPcm16RenderOutput() { Check(Ngs2SystemRender(system_handle, &output, 1) == OK && std::ranges::all_of(output_samples, [](int16_t sample) { return sample == 0; }), "PCM16 output retained samples from the previous render"); - Check(Ngs2RackDestroy(mastering_handle, nullptr) == OK && - Ngs2RackDestroy(sampler_handle, nullptr) == OK, - "PCM16 render rack cleanup failed"); +} + +void TestCustomMasteringChannelMixer() { + Ngs2Internal system; + system.option = Ngs2DefaultSystemOption(); + system.option.num_grain_samples = 6; + Ngs2RackOptionUnion sampler_option {}; + Ngs2FillDefaultRackOption(0x1000, &sampler_option); + sampler_option.common.max_voices = 1; + sampler_option.common.max_ports = 1; + Ngs2CustomMasteringRackOption master_option {}; + auto& custom = master_option.custom_rack_option; + custom.rack_option.size = sizeof(master_option); + custom.rack_option.max_voices = 1; + custom.rack_option.max_voice_channels = 6; + custom.num_buffers = 1; + custom.num_modules = 1; + custom.module[0].module_id = 0x17; + RenderRack sampler(system, 0x1000, sampler_option.common); + RenderRack master(system, 0x4003, custom.rack_option); + auto& source = sampler.Voice(); + auto& output = master.Voice(); + output.channels = 6; + output.SetEvent(1); + struct { + Ngs2VoiceParamHeader header {5484, 0, 0x40001700}; + float levels[37][37] {}; + } matrix; + constexpr float center = 0.70710677f; + matrix.levels[0][0] = matrix.levels[1][1] = 1.0f; + matrix.levels[0][2] = matrix.levels[1][2] = center; + matrix.levels[0][4] = matrix.levels[1][5] = center; + int16_t impulses[6][6] {}; + for (uint32_t c = 0; c < 6; ++c) impulses[c][c] = 8192; + for (unsigned pass = 0; pass < 2; ++pass) { + if (pass == 1) { + std::memset(matrix.levels, 0, sizeof(matrix.levels)); + matrix.levels[0][1] = 64.0f; + matrix.levels[1][0] = -64.0f; + } + Check(Ngs2VoiceControl(reinterpret_cast(&output), &matrix.header) == OK, + "custom channel mixer control failed"); + source.SetupSampler({0x12, 6, 48000}); + source.blocks.push_back({reinterpret_cast(impulses), + {0, sizeof(impulses), 0, 0, 6}}); + source.ports[0].dest = &output; + source.SetEvent(1); + std::array pcm; + pcm.fill(99.0f); + Ngs2RenderBufferInfo buffer {pcm.data() + 1, 12 * sizeof(float), 0x18, 2}; + Check(Ngs2SystemRender(reinterpret_cast(&system), &buffer, 1) == OK, + "custom mastering rejected stereo output"); + const std::array expected = pass == 0 + ? std::array {0.25f, 0, 0, 0.25f, center / 4, center / 4, + 0, 0, center / 4, 0, 0, center / 4} + : std::array {0, -8, 8, 0}; + Check(pcm.front() == 99 && pcm.back() == 99 && + std::equal(expected.begin(), expected.end(), pcm.begin() + 1), + "custom mixer lost channel routing, in-place input, clamping or output bounds"); + } } void TestPitchLoopAndSkip() { @@ -1700,6 +1765,7 @@ int main() { TestAtrac9Vibration(); TestAtrac9Discrete(); TestPcm16RenderOutput(); + TestCustomMasteringChannelMixer(); TestSubmixerSetupAndRouting(); TestCustomSideChainUserFx(); TestWaveformBlockGeometry();