mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-21 03:16:54 +02:00
audio: route NGS2 custom rack buffers and run USER_FX side-chain modules
This commit is contained in:
+141
-70
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user