audio: route NGS2 custom rack buffers and run USER_FX side-chain modules

This commit is contained in:
Mr. Big Britches
2026-10-20 06:24:22 +02:00
committed by nmzik
parent 1a053f9e13
commit 6b6274fa5a
2 changed files with 190 additions and 70 deletions
+141 -70
View File
@@ -344,7 +344,16 @@ enum class Ngs2RackType {
CustomSampler,
};
struct Ngs2UserFxContext {
struct Ngs2UserFxProcessContext {
float** channels;
std::array<uintptr_t, 3> 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<std::vector<uint8_t>, 24> common;
std::array<Ngs2UserFxOption, 24> fx {};
std::array<Ngs2UserFx2Option, 24> 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<uint8_t> param, work, state;
uint32_t flags = 1;
bool control_warned = false;
bool render_warned = false;
std::vector<uint8_t> param, work, state;
void KYTY_SYSV_ABI (*handler)(Ngs2UserFxProcessContext*) = nullptr;
std::array<uintptr_t, 3> 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<Ngs2VoiceInternal*>(&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<const Ngs2UserFxOption*>(custom.module[m].option);
fx = *reinterpret_cast<const Ngs2UserFx2Option*>(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::vector<Ngs2Voic
}
EXIT_NOT_IMPLEMENTED(voice.rendering);
voice.rendering = true;
voice.samples.assign(grain * voice.channels, 0.0f);
voice.ClearSamples(grain);
voice.has_samples = false;
if (voice.state == Ngs2VoicePlayState::Playing) {
if (voice.rack->type == Ngs2RackType::Sampler ||
@@ -1770,7 +1809,8 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vector<Ngs2Voic
}
}
for (auto* source: voices) {
for (const auto& port: source->ports) {
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::vector<Ngs2Voic
if (!source->has_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::vector<Ngs2Voic
? (src == dst ? 1.0f : 0.0f)
: (*matrix)[dst * source->channels + 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::vector<Ngs2Voic
}
if (voice.has_samples) {
const auto& custom = voice.rack->option.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<float*> 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<float*> 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<uintptr_t, 3> user_data;
};
EXIT_NOT_IMPLEMENTED(param->size != sizeof(FxParam));
const auto& fx = *reinterpret_cast<const FxParam*>(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<const FxParam*>(param);
auto& module = voice->modules[index];
const auto& fx = *reinterpret_cast<const Fx2Param*>(param);
EXIT_NOT_IMPLEMENTED(voice->rack->fx[index].control != 0 ||
fx.size != module.param.size());
std::memcpy(module.param.data(), fx.data, fx.size);
+49
View File
@@ -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<uintptr_t>(&voice), &setup.header) == OK &&
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&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();