mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-28 13:56:59 +01: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,
|
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* common;
|
||||||
void* param;
|
void* param;
|
||||||
void* work;
|
void* work;
|
||||||
@@ -354,7 +363,7 @@ struct Ngs2UserFxContext {
|
|||||||
uint64_t reserved[4] {};
|
uint64_t reserved[4] {};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Ngs2UserFxProcessContext {
|
struct Ngs2UserFx2ProcessContext {
|
||||||
float** channels;
|
float** channels;
|
||||||
void* common;
|
void* common;
|
||||||
const void* param;
|
const void* param;
|
||||||
@@ -370,18 +379,27 @@ struct Ngs2UserFxProcessContext {
|
|||||||
uint64_t reserved2[4] {};
|
uint64_t reserved2[4] {};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Ngs2UserFxOption {
|
struct Ngs2UserFx2Option {
|
||||||
Ngs2CustomModuleOption header;
|
Ngs2CustomModuleOption header;
|
||||||
int KYTY_SYSV_ABI (*setup)(Ngs2UserFxContext*);
|
int KYTY_SYSV_ABI (*setup)(Ngs2UserFx2Context*);
|
||||||
int KYTY_SYSV_ABI (*cleanup)(Ngs2UserFxContext*);
|
int KYTY_SYSV_ABI (*cleanup)(Ngs2UserFx2Context*);
|
||||||
uintptr_t control;
|
uintptr_t control;
|
||||||
int KYTY_SYSV_ABI (*process)(Ngs2UserFxProcessContext*);
|
int KYTY_SYSV_ABI (*process)(Ngs2UserFx2ProcessContext*);
|
||||||
size_t common_size;
|
size_t common_size;
|
||||||
size_t param_size;
|
size_t param_size;
|
||||||
size_t work_size;
|
size_t work_size;
|
||||||
uintptr_t user_data;
|
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 {
|
struct Ngs2RackInternal {
|
||||||
Ngs2Internal* ngs = nullptr;
|
Ngs2Internal* ngs = nullptr;
|
||||||
Ngs2RackInternal* next = nullptr;
|
Ngs2RackInternal* next = nullptr;
|
||||||
@@ -390,7 +408,16 @@ struct Ngs2RackInternal {
|
|||||||
Ngs2ContextBufferInfo buffer_info;
|
Ngs2ContextBufferInfo buffer_info;
|
||||||
Ngs2BufferAllocator allocator;
|
Ngs2BufferAllocator allocator;
|
||||||
std::array<std::vector<uint8_t>, 24> common;
|
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 };
|
enum class Ngs2VoicePlayState : uint32_t { Empty = 0, Playing = 3, Paused = 5, Stopped = 0xb };
|
||||||
@@ -519,10 +546,12 @@ struct Ngs2VoiceInternal {
|
|||||||
uint32_t repeated_count = 0;
|
uint32_t repeated_count = 0;
|
||||||
};
|
};
|
||||||
struct Module {
|
struct Module {
|
||||||
std::vector<uint8_t> param, work, state;
|
std::vector<uint8_t> param, work, state;
|
||||||
uint32_t flags = 1;
|
void KYTY_SYSV_ABI (*handler)(Ngs2UserFxProcessContext*) = nullptr;
|
||||||
bool control_warned = false;
|
std::array<uintptr_t, 3> user_data {};
|
||||||
bool render_warned = false;
|
uint32_t flags = 1;
|
||||||
|
bool control_warned = false;
|
||||||
|
bool render_warned = false;
|
||||||
};
|
};
|
||||||
Ngs2VoicePlayState state = Ngs2VoicePlayState::Empty;
|
Ngs2VoicePlayState state = Ngs2VoicePlayState::Empty;
|
||||||
uint32_t state_flags = 0;
|
uint32_t state_flags = 0;
|
||||||
@@ -572,6 +601,15 @@ struct Ngs2VoiceInternal {
|
|||||||
channels * sizeof(int16_t));
|
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() {
|
void ResetSetupState() {
|
||||||
SetEvent(4);
|
SetEvent(4);
|
||||||
std::ranges::fill(ports, Port {});
|
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 std::atomic_uint32_t g_next_ngs_uid = 1;
|
||||||
static Common::Mutex g_racks_mutex;
|
static Common::Mutex g_racks_mutex;
|
||||||
|
|
||||||
static_assert(sizeof(Ngs2UserFxContext) == 72);
|
static_assert(sizeof(Ngs2UserFxProcessContext) == 48);
|
||||||
static_assert(sizeof(Ngs2UserFxProcessContext) == 104);
|
static_assert(sizeof(Ngs2UserFx2Context) == 72);
|
||||||
static_assert(sizeof(Ngs2UserFxOption) == 72);
|
static_assert(sizeof(Ngs2UserFx2ProcessContext) == 104);
|
||||||
|
static_assert(sizeof(Ngs2UserFx2Option) == 72);
|
||||||
static_assert(sizeof(Ngs2SystemOption) == 144);
|
static_assert(sizeof(Ngs2SystemOption) == 144);
|
||||||
static_assert(sizeof(Ngs2SystemInfo) == 184);
|
static_assert(sizeof(Ngs2SystemInfo) == 184);
|
||||||
static_assert(sizeof(Ngs2RackOption) == 176);
|
static_assert(sizeof(Ngs2RackOption) == 176);
|
||||||
@@ -900,15 +939,6 @@ static Ngs2Internal* Ngs2FindSystem(uintptr_t system_handle) {
|
|||||||
return nullptr;
|
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,
|
int KYTY_SYSV_ABI Ngs2SystemQueryBufferSize(const Ngs2SystemOption* option,
|
||||||
Ngs2ContextBufferInfo* buffer_info) {
|
Ngs2ContextBufferInfo* buffer_info) {
|
||||||
PRINT_NAME();
|
PRINT_NAME();
|
||||||
@@ -1188,14 +1218,23 @@ static void Ngs2InitModules(Ngs2RackInternal& rack) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto& custom = rack.option.custom_sampler.custom_rack_option;
|
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);
|
auto* voices = reinterpret_cast<Ngs2VoiceInternal*>(&rack + 1);
|
||||||
for (uint32_t m = 0; m < custom.num_modules; ++m) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
auto& fx = rack.fx[m];
|
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);
|
rack.common[m].resize(fx.common_size);
|
||||||
for (uint32_t i = 0; i < rack.option.common.max_voices; ++i) {
|
for (uint32_t i = 0; i < rack.option.common.max_voices; ++i) {
|
||||||
auto& module = voices[i].modules[m];
|
auto& module = voices[i].modules[m];
|
||||||
@@ -1203,7 +1242,7 @@ static void Ngs2InitModules(Ngs2RackInternal& rack) {
|
|||||||
module.work.resize(fx.work_size);
|
module.work.resize(fx.work_size);
|
||||||
module.state.resize(custom.module[m].state_size);
|
module.state.resize(custom.module[m].state_size);
|
||||||
if (fx.setup != nullptr) {
|
if (fx.setup != nullptr) {
|
||||||
Ngs2UserFxContext context {
|
Ngs2UserFx2Context context {
|
||||||
rack.common[m].data(), module.param.data(), module.work.data(),
|
rack.common[m].data(), module.param.data(), module.work.data(),
|
||||||
fx.user_data, rack.option.common.max_voices, i};
|
fx.user_data, rack.option.common.max_voices, i};
|
||||||
EXIT_NOT_IMPLEMENTED(fx.setup(&context) != OK);
|
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];
|
const auto& fx = rack->fx[m];
|
||||||
if (fx.cleanup != nullptr) {
|
if (fx.cleanup != nullptr) {
|
||||||
auto& module = voices[i].modules[m];
|
auto& module = voices[i].modules[m];
|
||||||
Ngs2UserFxContext context {rack->common[m].data(),
|
Ngs2UserFx2Context context {rack->common[m].data(),
|
||||||
module.param.data(),
|
module.param.data(),
|
||||||
module.work.data(),
|
module.work.data(),
|
||||||
fx.user_data,
|
fx.user_data,
|
||||||
rack->option.common.max_voices,
|
rack->option.common.max_voices,
|
||||||
i};
|
i};
|
||||||
EXIT_NOT_IMPLEMENTED(fx.cleanup(&context) != OK);
|
EXIT_NOT_IMPLEMENTED(fx.cleanup(&context) != OK);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1729,7 +1768,7 @@ static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) {
|
|||||||
}
|
}
|
||||||
if (voice.source_generation != generation) {
|
if (voice.source_generation != generation) {
|
||||||
// A callback replaced the source, possibly changing its channel layout.
|
// 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;
|
output = 0;
|
||||||
}
|
}
|
||||||
voice.has_samples = 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);
|
EXIT_NOT_IMPLEMENTED(voice.rendering);
|
||||||
voice.rendering = true;
|
voice.rendering = true;
|
||||||
voice.samples.assign(grain * voice.channels, 0.0f);
|
voice.ClearSamples(grain);
|
||||||
voice.has_samples = false;
|
voice.has_samples = false;
|
||||||
if (voice.state == Ngs2VoicePlayState::Playing) {
|
if (voice.state == Ngs2VoicePlayState::Playing) {
|
||||||
if (voice.rack->type == Ngs2RackType::Sampler ||
|
if (voice.rack->type == Ngs2RackType::Sampler ||
|
||||||
@@ -1770,7 +1809,8 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vector<Ngs2Voic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (auto* source: voices) {
|
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) {
|
if (port.dest != &voice || port.volume == 0.0f) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1778,7 +1818,10 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vector<Ngs2Voic
|
|||||||
if (!source->has_samples) {
|
if (!source->has_samples) {
|
||||||
continue;
|
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 auto* matrix = port.matrix < 0 ? nullptr : &source->matrices.at(port.matrix);
|
||||||
const size_t output_channels =
|
const size_t output_channels =
|
||||||
matrix == nullptr
|
matrix == nullptr
|
||||||
@@ -1791,8 +1834,7 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vector<Ngs2Voic
|
|||||||
? (src == dst ? 1.0f : 0.0f)
|
? (src == dst ? 1.0f : 0.0f)
|
||||||
: (*matrix)[dst * source->channels + src]);
|
: (*matrix)[dst * source->channels + src]);
|
||||||
for (uint32_t i = 0; i < grain; ++i) {
|
for (uint32_t i = 0; i < grain; ++i) {
|
||||||
voice.samples[dst * grain + i] +=
|
output[dst * grain + i] += input[src * grain + i] * level;
|
||||||
source->samples[src * grain + i] * level;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1801,36 +1843,50 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vector<Ngs2Voic
|
|||||||
}
|
}
|
||||||
if (voice.has_samples) {
|
if (voice.has_samples) {
|
||||||
const auto& custom = voice.rack->option.custom_sampler.custom_rack_option;
|
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) {
|
for (size_t m = 0; m < voice.modules.size(); ++m) {
|
||||||
if (custom.module[m].module_id != 0x1f) {
|
const auto& info = custom.module[m];
|
||||||
if (!voice.modules[m].render_warned) {
|
auto& module = voice.modules[m];
|
||||||
|
if (info.module_id != 0x19 && info.module_id != 0x1f) {
|
||||||
|
if (!module.render_warned) {
|
||||||
Log::WriteToConsoleAndLog(fmt::sprintf(
|
Log::WriteToConsoleAndLog(fmt::sprintf(
|
||||||
"warning: NGS2 custom module 0x%02x at index %zu is ignored during render\n",
|
"warning: NGS2 custom module 0x%02x at index %zu is ignored during render\n",
|
||||||
custom.module[m].module_id, m));
|
info.module_id, m));
|
||||||
voice.modules[m].render_warned = true;
|
module.render_warned = true;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
EXIT_NOT_IMPLEMENTED(custom.module[m].source_buffer_id != 0 ||
|
if (info.module_id == 0x19 && module.handler == nullptr) {
|
||||||
custom.module[m].dest_buffer_id != 0);
|
continue;
|
||||||
auto& module = voice.modules[m];
|
}
|
||||||
const auto& fx = voice.rack->fx[m];
|
// Handlers receive the source channels followed by the extra buffer's channels.
|
||||||
std::vector<float*> channels(voice.channels);
|
std::vector<float*> channels(2 * size_t(voice.channels));
|
||||||
for (uint32_t c = 0; c < voice.channels; ++c) {
|
auto* source = voice.Buffer(info.source_buffer_id, grain);
|
||||||
channels[c] = voice.samples.data() + c * 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;
|
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));
|
EXIT_NOT_IMPLEMENTED(!Ngs2RackIsCustom(voice->rack->type));
|
||||||
const auto index = param->id & 0x1fu;
|
const auto index = param->id & 0x1fu;
|
||||||
EXIT_NOT_IMPLEMENTED(index >= voice->modules.size());
|
EXIT_NOT_IMPLEMENTED(index >= voice->modules.size());
|
||||||
if ((param->id & 0xffffffe0u) != 0x40001f00u ||
|
const auto module_id =
|
||||||
voice->rack->option.custom_sampler.custom_rack_option.module[index].module_id !=
|
voice->rack->option.custom_sampler.custom_rack_option.module[index].module_id;
|
||||||
0x1f) {
|
auto& module = voice->modules[index];
|
||||||
if (!voice->modules[index].control_warned) {
|
if ((param->id & 0xffffffe0u) != (0x40000000u | module_id << 8u) ||
|
||||||
|
(module_id != 0x19 && module_id != 0x1f)) {
|
||||||
|
if (!module.control_warned) {
|
||||||
Log::WriteToConsoleAndLog(fmt::sprintf(
|
Log::WriteToConsoleAndLog(fmt::sprintf(
|
||||||
"warning: NGS2 custom control 0x%08" PRIx32 " is ignored\n",
|
"warning: NGS2 custom control 0x%08" PRIx32 " is ignored\n",
|
||||||
param->id));
|
param->id));
|
||||||
voice->modules[index].control_warned = true;
|
module.control_warned = true;
|
||||||
}
|
}
|
||||||
break;
|
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;
|
Ngs2VoiceParamHeader header;
|
||||||
const void* data;
|
const void* data;
|
||||||
size_t size;
|
size_t size;
|
||||||
};
|
};
|
||||||
const auto& fx = *reinterpret_cast<const FxParam*>(param);
|
const auto& fx = *reinterpret_cast<const Fx2Param*>(param);
|
||||||
auto& module = voice->modules[index];
|
|
||||||
EXIT_NOT_IMPLEMENTED(voice->rack->fx[index].control != 0 ||
|
EXIT_NOT_IMPLEMENTED(voice->rack->fx[index].control != 0 ||
|
||||||
fx.size != module.param.size());
|
fx.size != module.param.size());
|
||||||
std::memcpy(module.param.data(), fx.data, fx.size);
|
std::memcpy(module.param.data(), fx.data, fx.size);
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ struct Fixture {
|
|||||||
} else {
|
} else {
|
||||||
rack.option.custom_sampler.max_waveform_blocks = capacity;
|
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.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() {
|
void TestPcm16RenderOutput() {
|
||||||
Ngs2Internal system;
|
Ngs2Internal system;
|
||||||
system.option = Ngs2DefaultSystemOption();
|
system.option = Ngs2DefaultSystemOption();
|
||||||
@@ -1608,6 +1656,7 @@ int main() {
|
|||||||
TestAtrac9Discrete();
|
TestAtrac9Discrete();
|
||||||
TestPcm16RenderOutput();
|
TestPcm16RenderOutput();
|
||||||
TestSubmixerSetupAndRouting();
|
TestSubmixerSetupAndRouting();
|
||||||
|
TestCustomSideChainUserFx();
|
||||||
TestWaveformBlockGeometry();
|
TestWaveformBlockGeometry();
|
||||||
TestAtrac9ResampleAfterStarvation();
|
TestAtrac9ResampleAfterStarvation();
|
||||||
TestAtrac9ResampleAcrossFrames();
|
TestAtrac9ResampleAcrossFrames();
|
||||||
|
|||||||
Reference in New Issue
Block a user