diff --git a/src/libs/ngs2.cpp b/src/libs/ngs2.cpp index a9e6c0884..5344dbc94 100644 --- a/src/libs/ngs2.cpp +++ b/src/libs/ngs2.cpp @@ -568,6 +568,8 @@ struct Ngs2VoiceInternal { std::vector output_matrix; uint32_t channels = 0; uint32_t output_id = 0; + float fbw_level = 1.0f; + float lfe_level = 1.0f; bool rendering = false; bool rendered = false; bool has_samples = false; @@ -1978,10 +1980,13 @@ int KYTY_SYSV_ABI Ngs2SystemRender(uintptr_t system_handle, const Ngs2RenderBuff pcm = static_cast(output.buffer); } for (uint32_t dst = 0; dst < output.num_channels; ++dst) { + // Output channel 3 is LFE; every other channel is full band. + const float gain = dst == 3 ? voice->lfe_level : voice->fbw_level; for (uint32_t src = 0; src < voice->channels; ++src) { - const float level = voice->output_matrix.empty() - ? (src == dst ? 1.0f : 0.0f) - : voice->output_matrix.at(src * output.num_channels + dst); + const float level = + gain * (voice->output_matrix.empty() + ? static_cast(src == dst) + : voice->output_matrix.at(src * output.num_channels + dst)); if (level == 0.0f) { continue; } @@ -2481,6 +2486,12 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH matrix->levels + matrix->num_levels); break; } + case 4: { + const auto* levels = reinterpret_cast(values); + voice->fbw_level = std::clamp(levels[0], 0.0f, 4.0f); + voice->lfe_level = std::clamp(levels[1], 0.0f, 4.0f); + break; + } case 5: voice->output_id = values[0]; break; default: EXIT("unsupported mastering control: 0x%08" PRIx32 "\n", param->id); } diff --git a/tests/Ngs2SamplerTests.cpp b/tests/Ngs2SamplerTests.cpp index e3cf79ad2..9dad0b446 100644 --- a/tests/Ngs2SamplerTests.cpp +++ b/tests/Ngs2SamplerTests.cpp @@ -822,6 +822,38 @@ void TestPatchToRackHandle() { "patch to a rack handle did not connect to its voice #0"); } +void TestMasteringGain() { + Ngs2Internal system; + system.option = Ngs2DefaultSystemOption(); + system.option.num_grain_samples = 1; + Ngs2RackOptionUnion sampler_option {}, mastering_option {}; + Ngs2FillDefaultRackOption(0x1000, &sampler_option); + Ngs2FillDefaultRackOption(0x3000, &mastering_option); + sampler_option.common.max_voices = 1; + RenderRack sampler(system, 0x1000, sampler_option.common); + RenderRack master(system, 0x3000, mastering_option.common); + auto& source = sampler.Voice(); + auto& mastering = master.Voice(); + mastering.channels = 8; + mastering.state = Ngs2VoicePlayState::Playing; + const int16_t frame[8] = {8192, 8192, 8192, 8192, 8192, 8192, 8192, 8192}; + source.SetupSampler({0x12, 8, 48000}); + source.blocks.push_back({reinterpret_cast(frame), {0, sizeof(frame), 0, 0, 1}}); + source.ports[0].dest = &mastering; + source.SetEvent(1); + struct { + Ngs2VoiceParamHeader header {16, 0, 0x30000004}; + float fbw = 5.0f, lfe = 0.5f; + } gain; + float pcm[8] = {}; + Ngs2RenderBufferInfo output {pcm, sizeof(pcm), 0x18, 8}; + const float expected[8] = {1.0f, 1.0f, 1.0f, 0.125f, 1.0f, 1.0f, 1.0f, 1.0f}; + Check(Ngs2VoiceControl(reinterpret_cast(&mastering), &gain.header) == OK && + Ngs2SystemRender(reinterpret_cast(&system), &output, 1) == OK && + std::ranges::equal(pcm, expected), + "mastering gain was not clamped or applied per full band and LFE channel"); +} + void TestCustomMasteringChannelMixer() { Ngs2Internal system; system.option = Ngs2DefaultSystemOption(); @@ -1819,6 +1851,7 @@ int main() { TestAtrac9Discrete(); TestPcm16RenderOutput(); TestPatchToRackHandle(); + TestMasteringGain(); TestCustomMasteringChannelMixer(); TestCustomVoiceStateCapacity(); TestSubmixerSetupAndRouting();