mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-27 00:28:14 +00:00
Merge pull request #10851 from Felipebmaitan/feat/ngs2-audio-engine
feat(ngs2): route custom submixer buffers through Gain
This commit is contained in:
12 files changed
+762
-48
No files matched your search
@@ -2,7 +2,19 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/Ngs2Library.cmake)
|
||||
add_sce_ngs2_library(libSceNgs2.native)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Parse Stream UserFx Report Geom VoiceIndex HandleErrors VoiceHandles VoiceEvents)
|
||||
add_test_executable(ngs2_custom_safety_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Ngs2CustomSafety.cpp)
|
||||
target_include_directories(ngs2_custom_safety_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(ngs2_custom_safety_tests PRIVATE libSceNgs2.native libkernel libc)
|
||||
configure_windows_unwind(ngs2_custom_safety_tests)
|
||||
foreach(safetyCase IN ITEMS custom_callback upstream_callback configured_gain routing_one_buffer routing_two_buffers)
|
||||
add_test(NAME ngs2_custom_safety_${safetyCase} COMMAND ngs2_custom_safety_tests ${safetyCase})
|
||||
set_tests_properties(ngs2_custom_safety_${safetyCase} PROPERTIES TIMEOUT 10)
|
||||
if(WIN32)
|
||||
set_tests_properties(ngs2_custom_safety_${safetyCase} PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNgs2.native>")
|
||||
endif()
|
||||
endforeach()
|
||||
foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Gain Routing Parse Stream UserFx Report Geom VoiceIndex HandleErrors VoiceHandles VoiceEvents)
|
||||
string(TOLOWER ${ngs2Test} testName)
|
||||
add_test_executable(ngs2_${testName}_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Ngs2${ngs2Test}.cpp)
|
||||
target_include_directories(ngs2_${testName}_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src)
|
||||
|
||||
@@ -31,6 +31,7 @@ static constexpr int SCE_NGS2_ERROR_INVALID_PAN_PARAM = static_cast<int>(0x804A8
|
||||
static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_HANDLE = static_cast<int>(0x804A8203);
|
||||
static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_ACCESS = static_cast<int>(0x804A8351);
|
||||
static constexpr int SCE_NGS2_ERROR_NULL_PARAM_LIST = static_cast<int>(0x804A8360);
|
||||
static constexpr int SCE_NGS2_ERROR_INVALID_PARAM_ID = static_cast<int>(0x804A835A);
|
||||
static constexpr int SCE_NGS2_ERROR_INVALID_GEOM_LISTENER_ADDRESS = static_cast<int>(0x804A8461);
|
||||
static constexpr int SCE_NGS2_ERROR_INVALID_GEOM_FLAG = static_cast<int>(0x804A8463);
|
||||
static constexpr int SCE_NGS2_ERROR_INVALID_OPTION_SIZE = static_cast<int>(0x804A8013);
|
||||
|
||||
@@ -89,17 +89,23 @@ int Ngs2CheckCustomRack(std::uint32_t rackId, const Ngs2CustomRackOption& option
|
||||
const int result = CheckBuiltInModule(info);
|
||||
if (result != SCE_NGS2_OK) return result;
|
||||
}
|
||||
if (option.num_buffers != 1) throw std::runtime_error("NGS2: custom racks with " + std::to_string(option.num_buffers) + " buffers are not implemented");
|
||||
if (option.num_buffers != 1 &&
|
||||
(rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER || option.num_modules > 1 ||
|
||||
(option.num_modules == 1 && option.module[0].module_id != SCE_NGS2_CUSTOM_MODULE_ID_GAIN))) {
|
||||
throw std::runtime_error("NGS2: multiple custom buffers require a submixer with at most one Gain module");
|
||||
}
|
||||
for (std::uint32_t m = 0; m < option.num_modules; m++) {
|
||||
const auto moduleId = option.module[m].module_id;
|
||||
if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_GAIN && option.module[m].option != nullptr) throw std::runtime_error("NGS2: an option for the custom Gain module is not implemented");
|
||||
if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_GAIN && rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) continue;
|
||||
if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_GAIN) throw std::runtime_error("NGS2: the custom Gain module on a custom sampler rack is not implemented");
|
||||
if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_GAIN && rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) {
|
||||
throw std::runtime_error("NGS2: custom sampler Gain initialization and lifecycle are not implemented");
|
||||
}
|
||||
if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_GAIN) continue;
|
||||
const char* name = BuiltInModuleName(moduleId);
|
||||
if (name != nullptr) throw std::runtime_error(std::string("NGS2: the custom ") + name + " module is not implemented");
|
||||
}
|
||||
for (std::uint32_t p = 0; p < option.rack_option.max_ports; p++) {
|
||||
if (option.port[p].source_buffer_id != 0) APS5_INVALID_ARG_EX;
|
||||
if (option.port[p].source_buffer_id >= option.num_buffers) APS5_INVALID_ARG_EX;
|
||||
}
|
||||
return SCE_NGS2_OK;
|
||||
}
|
||||
@@ -111,11 +117,14 @@ static Ngs2UserFx2SetupContext UserFxContext(Ngs2Rack& rack, std::size_t module,
|
||||
}
|
||||
|
||||
void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option) {
|
||||
rack.customPortBuffers.resize(option.rack_option.max_ports);
|
||||
for (std::size_t p = 0; p < rack.customPortBuffers.size(); p++) rack.customPortBuffers[p] = option.port[p].source_buffer_id;
|
||||
rack.customModuleIds.resize(option.num_modules);
|
||||
rack.customModules.resize(option.num_modules);
|
||||
rack.userFx.resize(option.num_modules);
|
||||
rack.userFxCommon.resize(option.num_modules);
|
||||
for (auto& voice : rack.voices) {
|
||||
voice.customBuffers.resize(option.num_buffers - 1);
|
||||
voice.userFx.resize(option.num_modules);
|
||||
voice.customUserFx.resize(option.num_modules);
|
||||
voice.customGain.assign(option.num_modules, {});
|
||||
@@ -164,6 +173,8 @@ static void SetupCustomSubmixer(Ngs2Voice& voice, const Ngs2CustomSubmixerVoiceS
|
||||
if (setup.flags != 0) throw std::runtime_error("NGS2: custom submixer setup flags are not implemented");
|
||||
if (setup.num_input_channels != setup.num_output_channels) throw std::runtime_error("NGS2: custom submixer channel conversion is not implemented");
|
||||
if (setup.num_input_channels == 0 || setup.num_input_channels > voice.rack->maxChannels) APS5_INVALID_ARG_EX;
|
||||
for (auto& gain : voice.customGain) if (gain.configured) gain.lifecycleUnknown = true;
|
||||
++voice.waveformRevision;
|
||||
voice.channels = setup.num_input_channels;
|
||||
}
|
||||
|
||||
@@ -188,22 +199,41 @@ static void SetCustomUserFxParam(Ngs2Voice& voice, std::uint32_t index, const Ng
|
||||
fx.flags = 1;
|
||||
}
|
||||
|
||||
static void SetGainParam(Ngs2Voice& voice, std::uint32_t index, const Ngs2CustomVoiceGainParam& param) {
|
||||
if (index >= voice.customGain.size() || voice.rack->customModuleIds[index] != SCE_NGS2_CUSTOM_MODULE_ID_GAIN) APS5_INVALID_ARG_EX;
|
||||
for (const float gain : param.gain) {
|
||||
if (!(gain >= -32.0f && gain <= 32.0f)) throw std::runtime_error("NGS2: a custom Gain level outside -32 to 32 is not implemented");
|
||||
}
|
||||
int Ngs2ApplyCustomGainParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& header) {
|
||||
const auto index = header.id & 0xff;
|
||||
if (index >= voice.rack->customModuleIds.size()) return SCE_NGS2_ERROR_INVALID_PARAM_ID;
|
||||
if (voice.rack->customModuleIds[index] != SCE_NGS2_CUSTOM_MODULE_ID_GAIN) return SCE_NGS2_ERROR_INVALID_PARAM_ID;
|
||||
const auto& param = ParamAs<Ngs2CustomVoiceGainParam>(header);
|
||||
std::copy(std::begin(param.gain), std::end(param.gain), voice.customGain[index].target.begin());
|
||||
voice.customGain[index].configured = true;
|
||||
return SCE_NGS2_OK;
|
||||
}
|
||||
|
||||
static void ProcessGain(Ngs2Voice& voice, Ngs2VoiceCustomGain& gain, const std::vector<float*>& channels, std::uint32_t grain) {
|
||||
if (voice.hasSamples) {
|
||||
for (std::uint32_t c = 0; c < voice.channels; c++) {
|
||||
const float from = gain.current[c];
|
||||
const float step = (gain.target[c] - from) / static_cast<float>(grain);
|
||||
for (std::uint32_t i = 0; i < grain; i++) channels[c][i] *= from + step * static_cast<float>(i + 1);
|
||||
static void ProcessGain(Ngs2Voice& voice, std::size_t module, std::uint32_t grain) {
|
||||
auto& gain = voice.customGain[module];
|
||||
if (!gain.configured) throw std::runtime_error("NGS2: custom Gain processing before its first parameter set is not verified");
|
||||
if (gain.lifecycleUnknown) throw std::runtime_error("NGS2: custom Gain processing after setup or stop is not verified");
|
||||
const auto& info = voice.rack->customModules[module];
|
||||
const auto& source = voice.Samples(info.source_buffer_id);
|
||||
auto& dest = voice.Samples(info.dest_buffer_id);
|
||||
for (std::uint32_t c = 0; c < voice.channels; c++) {
|
||||
const auto offset = static_cast<std::size_t>(c) * grain;
|
||||
const float from = gain.current[c];
|
||||
const float to = gain.target[c];
|
||||
if (from == to) {
|
||||
if (to == 0.0f) std::fill_n(dest.data() + offset, grain, 0.0f);
|
||||
else if (to == 1.0f) {
|
||||
if (source.data() != dest.data()) std::copy_n(source.data() + offset, grain, dest.data() + offset);
|
||||
}
|
||||
else for (std::uint32_t i = 0; i < grain; i++) dest[offset + i] = source[offset + i] * to;
|
||||
} else {
|
||||
const float step = (to - from) / static_cast<float>(grain);
|
||||
for (std::uint32_t i = 0; i < grain; i++) dest[offset + i] = source[offset + i] * (from + step * static_cast<float>(i));
|
||||
}
|
||||
voice.hasSamples = std::any_of(voice.samples.begin(), voice.samples.end(), [](float sample) { return sample != 0.0f; });
|
||||
}
|
||||
voice.hasSamples = std::any_of(voice.samples.begin(), voice.samples.end(), [](float sample) { return sample != 0.0f; });
|
||||
for (const auto& buffer : voice.customBuffers) {
|
||||
voice.hasSamples |= std::any_of(buffer.begin(), buffer.end(), [](float sample) { return sample != 0.0f; });
|
||||
}
|
||||
gain.current = gain.target;
|
||||
}
|
||||
@@ -243,11 +273,6 @@ void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) {
|
||||
Ngs2SetFilter(voice, filter);
|
||||
return;
|
||||
}
|
||||
if ((param.id & ~0x1fu) == SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN) {
|
||||
if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack");
|
||||
SetGainParam(voice, param.id & 0x1f, ParamAs<Ngs2CustomVoiceGainParam>(param));
|
||||
return;
|
||||
}
|
||||
if ((param.id & ~0x1fu) == SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2) {
|
||||
if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SAMPLER && rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) {
|
||||
throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack");
|
||||
@@ -266,12 +291,20 @@ void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) {
|
||||
|
||||
void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate) {
|
||||
if (voice.userFx.empty() && voice.customUserFx.empty()) return;
|
||||
const auto revision = voice.waveformRevision;
|
||||
const auto channelCount = voice.channels;
|
||||
const auto checkLayout = [&] {
|
||||
if (voice.waveformRevision != revision || voice.channels != channelCount || voice.samples.size() != static_cast<std::size_t>(channelCount) * grain) {
|
||||
throw std::runtime_error("NGS2: voice setup changes during custom processing are not implemented");
|
||||
}
|
||||
};
|
||||
checkLayout();
|
||||
std::vector<float*> channels(voice.channels);
|
||||
for (std::uint32_t c = 0; c < voice.channels; c++) channels[c] = voice.samples.data() + static_cast<std::size_t>(c) * grain;
|
||||
for (std::size_t m = 0; m < voice.rack->customModuleIds.size(); m++) {
|
||||
if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_DELAY) continue;
|
||||
if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_GAIN) {
|
||||
ProcessGain(voice, voice.customGain[m], channels, grain);
|
||||
ProcessGain(voice, m, grain);
|
||||
} else if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) {
|
||||
if (!voice.hasSamples) continue;
|
||||
auto& fx = voice.userFx[m];
|
||||
@@ -280,6 +313,7 @@ void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t samp
|
||||
Ngs2UserFx2ProcessContext context{channels.data(), voice.rack->userFxCommon[m].data(), fx.param.data(), fx.work.data(), fx.state.data(),
|
||||
module.user_data, fx.flags, voice.channels, voice.channels, grain, sampleRate, 0, {}};
|
||||
const int result = module.process_handler(&context);
|
||||
checkLayout();
|
||||
if (result != SCE_NGS2_OK) throw std::runtime_error("NGS2: the UserFx2 process handler failed with " + std::to_string(result));
|
||||
fx.flags = 0;
|
||||
} else if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX) {
|
||||
@@ -288,6 +322,7 @@ void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t samp
|
||||
Ngs2UserFxProcessContext context{channels.data(), fx.data[0], fx.data[1], fx.data[2],
|
||||
fx.flags, voice.channels, grain, sampleRate};
|
||||
const int result = fx.handler(&context);
|
||||
checkLayout();
|
||||
if (result != SCE_NGS2_OK) throw std::runtime_error("NGS2: the UserFx process handler failed with " + std::to_string(result));
|
||||
fx.flags = 0;
|
||||
voice.hasSamples = std::any_of(voice.samples.begin(), voice.samples.end(), [](float sample) { return sample != 0.0f; });
|
||||
|
||||
@@ -93,6 +93,7 @@ struct Ngs2Port {
|
||||
Ngs2Voice* dest = nullptr;
|
||||
float volume = 1.0f;
|
||||
std::int32_t matrix = -1;
|
||||
std::uint32_t destInput = 0;
|
||||
};
|
||||
|
||||
struct Ngs2UserFx2 {
|
||||
@@ -111,6 +112,8 @@ struct Ngs2VoiceCustomUserFx {
|
||||
struct Ngs2VoiceCustomGain {
|
||||
std::array<float, SCE_NGS2_CUSTOM_SUBMIXER_MAX_CHANNELS> current{};
|
||||
std::array<float, SCE_NGS2_CUSTOM_SUBMIXER_MAX_CHANNELS> target{};
|
||||
bool configured = false;
|
||||
bool lifecycleUnknown = false;
|
||||
};
|
||||
|
||||
struct Ngs2Voice {
|
||||
@@ -147,6 +150,7 @@ struct Ngs2Voice {
|
||||
std::array<std::uintptr_t, 3> userFxData{};
|
||||
std::uint32_t userFxFlags = 0;
|
||||
std::vector<float> samples;
|
||||
std::vector<std::vector<float>> customBuffers;
|
||||
bool rendering = false;
|
||||
bool rendered = false;
|
||||
bool hasSamples = false;
|
||||
@@ -155,6 +159,8 @@ struct Ngs2Voice {
|
||||
void ResetSetup();
|
||||
const std::uint8_t* WaveformData() const;
|
||||
std::uint32_t InputChannels() const { return inputChannels != 0 ? inputChannels : channels; }
|
||||
std::vector<float>& Samples(std::uint32_t buffer) { return buffer == 0 ? samples : customBuffers.at(buffer - 1); }
|
||||
const std::vector<float>& Samples(std::uint32_t buffer) const { return buffer == 0 ? samples : customBuffers.at(buffer - 1); }
|
||||
};
|
||||
|
||||
struct Ngs2Rack {
|
||||
@@ -175,6 +181,7 @@ struct Ngs2Rack {
|
||||
std::vector<std::vector<std::uint8_t>> userFxCommon;
|
||||
std::vector<std::uint32_t> customModuleIds;
|
||||
std::vector<Ngs2CustomModuleInfo> customModules;
|
||||
std::vector<std::uint32_t> customPortBuffers;
|
||||
std::thread::id lockOwner{};
|
||||
std::uint32_t lockDepth = 0;
|
||||
};
|
||||
@@ -243,6 +250,7 @@ bool Ngs2FinishBlock(Ngs2Voice& voice);
|
||||
int Ngs2CheckCustomRack(std::uint32_t rackId, const Ngs2CustomRackOption& option, std::uint32_t maxInputs);
|
||||
void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option);
|
||||
void Ngs2CleanupUserFx(Ngs2Rack& rack);
|
||||
int Ngs2ApplyCustomGainParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param);
|
||||
void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param);
|
||||
void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate);
|
||||
void Ngs2SetReverbParams(Ngs2Voice& voice, const Ngs2ReverbI3DL2Param& params);
|
||||
|
||||
@@ -211,7 +211,13 @@ float Ngs2DefaultLevel(std::uint32_t sourceChannels, std::uint32_t source, std::
|
||||
}
|
||||
}
|
||||
|
||||
static void MixPort(Ngs2Voice& voice, const Ngs2Voice& source, const Ngs2Port& port, std::uint32_t grain) {
|
||||
static void MixPort(Ngs2Voice& voice, const Ngs2Voice& source, const Ngs2Port& port, std::uint32_t sourceBuffer, std::uint32_t grain) {
|
||||
const auto& sourceSamples = source.Samples(sourceBuffer);
|
||||
auto& destSamples = voice.Samples(port.destInput);
|
||||
if (sourceSamples.size() != static_cast<std::size_t>(grain) * source.channels ||
|
||||
destSamples.size() != static_cast<std::size_t>(grain) * voice.InputChannels()) {
|
||||
throw std::runtime_error("NGS2: channel layout changes during input rendering are not implemented");
|
||||
}
|
||||
const auto* matrix = port.matrix < 0 || source.matrices[port.matrix].empty() ? nullptr : &source.matrices[port.matrix];
|
||||
const auto channels = voice.InputChannels();
|
||||
const std::size_t outputs = matrix == nullptr ? channels : std::min<std::size_t>(channels, matrix->size() / source.channels);
|
||||
@@ -219,18 +225,24 @@ static void MixPort(Ngs2Voice& voice, const Ngs2Voice& source, const Ngs2Port& p
|
||||
for (std::uint32_t src = 0; src < source.channels; src++) {
|
||||
const float level = port.volume * (matrix == nullptr ? Ngs2DefaultLevel(source.channels, src, channels, static_cast<std::uint32_t>(dst)) : (*matrix)[dst * source.channels + src]);
|
||||
if (level == 0.0f) continue;
|
||||
for (std::uint32_t i = 0; i < grain; i++) voice.samples[dst * grain + i] += source.samples[src * grain + i] * level;
|
||||
for (std::uint32_t i = 0; i < grain; i++) destSamples[dst * grain + i] += sourceSamples[src * grain + i] * level;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void MixInputs(Ngs2Voice& voice, const std::vector<Ngs2Voice*>& voices, std::uint32_t grain, std::uint32_t systemRate) {
|
||||
for (auto* source : voices) {
|
||||
for (const auto& port : source->ports) {
|
||||
for (std::size_t p = 0; p < source->ports.size(); p++) {
|
||||
const auto& port = source->ports[p];
|
||||
if (port.dest != &voice || port.volume == 0.0f) continue;
|
||||
const auto destInput = port.destInput;
|
||||
RenderVoice(*source, voices, grain, systemRate);
|
||||
if (port.dest != &voice || port.destInput != destInput) {
|
||||
throw std::runtime_error("NGS2: patch changes during source rendering are not implemented");
|
||||
}
|
||||
if (!source->hasSamples) continue;
|
||||
MixPort(voice, *source, port, grain);
|
||||
const auto sourceBuffer = source->rack->customPortBuffers.empty() ? 0u : source->rack->customPortBuffers[p];
|
||||
MixPort(voice, *source, port, sourceBuffer, grain);
|
||||
voice.hasSamples = true;
|
||||
}
|
||||
}
|
||||
@@ -245,6 +257,7 @@ static void RenderVoice(Ngs2Voice& voice, const std::vector<Ngs2Voice*>& voices,
|
||||
~RenderingScope() { active = false; }
|
||||
} renderingScope{voice.rendering};
|
||||
voice.samples.assign(static_cast<std::size_t>(grain) * voice.InputChannels(), 0.0f);
|
||||
for (auto& buffer : voice.customBuffers) buffer.assign(static_cast<std::size_t>(grain) * voice.InputChannels(), 0.0f);
|
||||
voice.hasSamples = false;
|
||||
if (voice.state == Ngs2PlayState::Playing && voice.channels != 0) {
|
||||
if (voice.rack->rackId == SCE_NGS2_RACK_ID_SAMPLER || voice.rack->rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) {
|
||||
@@ -291,7 +304,7 @@ static void WriteOutput(const Ngs2RenderBufferInfo& output, const std::vector<fl
|
||||
}
|
||||
auto* pcm = static_cast<std::int16_t*>(output.buffer);
|
||||
for (std::size_t i = 0; i < mix.size(); i++) {
|
||||
if (std::isnan(mix[i])) throw std::runtime_error("NGS2: a NaN sample in a PCM_I16L render buffer is not implemented");
|
||||
if (!std::isfinite(mix[i])) throw std::runtime_error("NGS2: integer rendering of nonfinite samples is not implemented");
|
||||
pcm[i] = static_cast<std::int16_t>(std::min(std::lrint(std::clamp(mix[i], -1.0f, 1.0f) * 32768.0f), 32767L));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,10 @@ static void SendEvent(Ngs2Voice& voice, std::uint32_t eventId) {
|
||||
}
|
||||
|
||||
void Ngs2Voice::SetEvent(std::uint32_t eventId) {
|
||||
if (state != Ngs2PlayState::Empty &&
|
||||
(eventId == SCE_NGS2_VOICE_EVENT_STOP || eventId == SCE_NGS2_VOICE_EVENT_STOP_IMM || eventId == SCE_NGS2_VOICE_EVENT_KILL)) {
|
||||
for (auto& gain : customGain) if (gain.configured) gain.lifecycleUnknown = true;
|
||||
}
|
||||
switch (eventId) {
|
||||
case SCE_NGS2_VOICE_EVENT_PLAY:
|
||||
if (state == Ngs2PlayState::Empty || state == Ngs2PlayState::Stopped) {
|
||||
@@ -59,6 +63,7 @@ void Ngs2Voice::SetEvent(std::uint32_t eventId) {
|
||||
}
|
||||
|
||||
void Ngs2Voice::ResetSetup() {
|
||||
for (auto& gain : customGain) if (gain.configured) gain.lifecycleUnknown = true;
|
||||
++waveformRevision;
|
||||
SetEvent(SCE_NGS2_VOICE_EVENT_STOP_IMM);
|
||||
std::fill(ports.begin(), ports.end(), Ngs2Port{});
|
||||
@@ -130,13 +135,15 @@ static void SetPortMatrix(Ngs2Voice& voice, std::uint32_t port, std::int32_t mat
|
||||
|
||||
static void Patch(Ngs2Voice& voice, const Ngs2VoicePatchParam& patch) {
|
||||
auto& port = PortAt(voice, patch.port);
|
||||
if (patch.dest_input_id != 0) throw std::runtime_error("NGS2: voice input " + std::to_string(patch.dest_input_id) + " is not implemented");
|
||||
if (patch.dest_handle == 0) {
|
||||
if (patch.dest_input_id != 0) throw std::runtime_error("NGS2: a disconnected port with a nonzero input id is not implemented");
|
||||
port.dest = nullptr;
|
||||
port.destInput = 0;
|
||||
return;
|
||||
}
|
||||
auto* dest = Ngs2FindVoice(patch.dest_handle);
|
||||
if (dest == nullptr) {
|
||||
if (patch.dest_input_id != 0) throw std::runtime_error("NGS2: a rack destination with a nonzero input id is not implemented");
|
||||
auto* rack = Ngs2FindRack(patch.dest_handle);
|
||||
if (rack != nullptr && !rack->voices.empty()) dest = &rack->voices.front();
|
||||
}
|
||||
@@ -144,7 +151,12 @@ static void Patch(Ngs2Voice& voice, const Ngs2VoicePatchParam& patch) {
|
||||
dest->rack->rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) {
|
||||
APS5_INVALID_ARG_EX;
|
||||
}
|
||||
if (patch.dest_input_id != 0 &&
|
||||
(dest->rack->rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER || patch.dest_input_id >= dest->rack->maxInputs)) {
|
||||
throw std::runtime_error("NGS2: voice input " + std::to_string(patch.dest_input_id) + " is not implemented for this rack");
|
||||
}
|
||||
port.dest = dest;
|
||||
port.destInput = patch.dest_input_id;
|
||||
}
|
||||
|
||||
static void ApplyCommonParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) {
|
||||
@@ -421,7 +433,12 @@ int APS5_VABI sceNgs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamHe
|
||||
auto& voice = *found;
|
||||
try {
|
||||
for (const auto* param = param_list;; param = reinterpret_cast<const Ngs2VoiceParamHeader*>(reinterpret_cast<const std::uint8_t*>(param) + param->next)) {
|
||||
ApplyParam(voice, *param);
|
||||
if ((param->id & ~0xffu) == SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN) {
|
||||
const auto result = Ngs2ApplyCustomGainParam(voice, *param);
|
||||
if (result != SCE_NGS2_OK) return result;
|
||||
} else {
|
||||
ApplyParam(voice, *param);
|
||||
}
|
||||
if (param->next == 0) break;
|
||||
}
|
||||
} catch (const Ngs2ReturnCode& result) {
|
||||
@@ -521,6 +538,7 @@ int APS5_VABI sceNgs2VoiceGetPortInfo(uintptr_t voice_handle, uint32_t port, Ngs
|
||||
info->matrix_id = source.matrix;
|
||||
info->volume = source.volume;
|
||||
info->dest_handle = reinterpret_cast<Ngs2Handle>(source.dest);
|
||||
info->dest_input_id = source.destInput;
|
||||
return SCE_NGS2_OK;
|
||||
}
|
||||
|
||||
|
||||
@@ -521,8 +521,6 @@ static void TestCustomRackChecks() {
|
||||
Require(rack == 0);
|
||||
sampler.custom_rack_option.module[0].extra_buffer_id = 0;
|
||||
Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &sampler.custom_rack_option.rack_option, &info, &rack) == SCE_NGS2_OK);
|
||||
sampler.custom_rack_option.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_GAIN;
|
||||
Require(CreateSamplerThrows<std::runtime_error>(sampler));
|
||||
Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK);
|
||||
for (const auto count : {1u, 2u, 24u}) {
|
||||
sampler.custom_rack_option.num_modules = count;
|
||||
@@ -757,17 +755,14 @@ static void TestCustomGain() {
|
||||
Patch(sampler, custom);
|
||||
Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY);
|
||||
|
||||
auto out = Render(system);
|
||||
for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.0f && out[i * 2 + 1] == 0.0f);
|
||||
|
||||
Ngs2CustomVoiceGainParam gain{};
|
||||
gain.gain[0] = 2.0f;
|
||||
gain.gain[1] = -0.5f;
|
||||
Control(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 0, gain);
|
||||
out = Render(system);
|
||||
auto out = Render(system);
|
||||
for (std::uint32_t i = 0; i < Grain; i++) {
|
||||
Require(out[i * 2] == 0.5f * 2.0f * static_cast<float>(i + 1) / Grain);
|
||||
Require(out[i * 2 + 1] == 0.5f * -0.5f * static_cast<float>(i + 1) / Grain);
|
||||
Require(out[i * 2] == 0.5f * 2.0f * static_cast<float>(i) / Grain);
|
||||
Require(out[i * 2 + 1] == 0.5f * -0.5f * static_cast<float>(i) / Grain);
|
||||
}
|
||||
out = Render(system);
|
||||
for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 1.0f && out[i * 2 + 1] == -0.25f);
|
||||
@@ -775,13 +770,15 @@ static void TestCustomGain() {
|
||||
gain.gain[0] = 1.0f;
|
||||
Control(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 0, gain);
|
||||
out = Render(system);
|
||||
for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.5f * (2.0f - static_cast<float>(i + 1) / Grain));
|
||||
for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.5f * (2.0f - static_cast<float>(i) / Grain));
|
||||
|
||||
auto outside = gain;
|
||||
outside.gain[2] = 33.0f;
|
||||
Require(ControlThrows<std::runtime_error>(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 0, outside));
|
||||
Require(ControlThrows<std::invalid_argument>(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 1, gain));
|
||||
Require(ControlThrows<std::invalid_argument>(sampler, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 0, gain));
|
||||
Control(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 0, outside);
|
||||
gain.header = {sizeof(gain), 0, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 1};
|
||||
Require(sceNgs2VoiceControl(custom, &gain.header) == SCE_NGS2_ERROR_INVALID_PARAM_ID);
|
||||
gain.header.id = SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN;
|
||||
Require(sceNgs2VoiceControl(sampler, &gain.header) == SCE_NGS2_ERROR_INVALID_PARAM_ID);
|
||||
struct ShortGainParam {
|
||||
Ngs2VoiceParamHeader header;
|
||||
float gain[2];
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
#include "prx/libSceNgs2.native/tests/Ngs2Test.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
#include <string_view>
|
||||
|
||||
static std::uint32_t processCalls = 0;
|
||||
|
||||
static int APS5_VABI Generate(Ngs2UserFxProcessContext* context) {
|
||||
Require(++processCalls == 1);
|
||||
std::fill_n(context->channel_data[0], context->num_grain_samples, 1.0f);
|
||||
std::fill_n(context->channel_data[1], context->num_grain_samples, -0.5f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uintptr_t Custom(uintptr_t system, std::uint32_t count, bool chain, std::uint32_t outputBuffer = 0) {
|
||||
Ngs2CustomSubmixerRackOption option{};
|
||||
auto& custom = option.custom_rack_option;
|
||||
custom.rack_option.size = sizeof(option);
|
||||
custom.rack_option.max_grain_samples = 512;
|
||||
custom.rack_option.max_voices = 1;
|
||||
custom.rack_option.max_ports = 1;
|
||||
custom.num_buffers = count;
|
||||
custom.port[0].source_buffer_id = outputBuffer;
|
||||
custom.num_modules = chain ? 2 : 0;
|
||||
custom.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_USER_FX;
|
||||
custom.module[1].module_id = SCE_NGS2_CUSTOM_MODULE_ID_GAIN;
|
||||
option.max_channels = 8;
|
||||
option.max_inputs = count;
|
||||
Ngs2ContextBufferInfo query{};
|
||||
Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &custom.rack_option, &query) == 0);
|
||||
const auto storage = Buffer(query);
|
||||
uintptr_t rack = 0;
|
||||
Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &custom.rack_option, &storage, &rack) == 0);
|
||||
const auto voice = Voice(rack);
|
||||
Control(voice, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 2, 2, 0, 0});
|
||||
Event(voice, SCE_NGS2_VOICE_EVENT_PLAY);
|
||||
return voice;
|
||||
}
|
||||
|
||||
static void ExpectOutput(uintptr_t system, float left, float right) {
|
||||
std::array<float, Grain * 2> out{};
|
||||
const Ngs2RenderBufferInfo info{out.data(), sizeof(out), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2};
|
||||
Require(sceNgs2SystemRender(system, &info, 1) == 0);
|
||||
for (std::uint32_t i = 0; i < Grain; i++) {
|
||||
Require(out[i * 2] == left && out[i * 2 + 1] == right);
|
||||
}
|
||||
}
|
||||
|
||||
static void TestCustomCallback() {
|
||||
processCalls = 0;
|
||||
const auto system = CreateSystem();
|
||||
const auto master = Mastering(system, 2);
|
||||
const auto target = Custom(system, 1, true);
|
||||
Patch(target, master);
|
||||
Control(target, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX, Ngs2CustomVoiceUserFxParam{{}, Generate, 0, 0, 0});
|
||||
Control(target, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 1, Ngs2CustomVoiceGainParam{});
|
||||
ExpectOutput(system, 0.0f, 0.0f);
|
||||
Require(processCalls == 1);
|
||||
Require(sceNgs2SystemDestroy(system, nullptr) == 0);
|
||||
}
|
||||
|
||||
static void TestUpstreamCallback() {
|
||||
processCalls = 0;
|
||||
const auto system = CreateSystem();
|
||||
const auto master = Mastering(system, 2);
|
||||
const auto target = Custom(system, 2, false, 1);
|
||||
Patch(target, master);
|
||||
const auto source = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SUBMIXER));
|
||||
Control(source, SCE_NGS2_SUBMIXER_VOICE_PARAM_SETUP, Ngs2SubmixerVoiceSetupParam{{}, 2, 0});
|
||||
Control(source, SCE_NGS2_SUBMIXER_VOICE_PARAM_USER_FX, Ngs2SubmixerVoiceUserFxParam{{}, Generate, 0, 0, 0});
|
||||
Control(source, SCE_NGS2_VOICE_PARAM_PATCH, Ngs2VoicePatchParam{{}, 0, 1, target});
|
||||
Event(source, SCE_NGS2_VOICE_EVENT_PLAY);
|
||||
ExpectOutput(system, 1.0f, -0.5f);
|
||||
Require(processCalls == 1);
|
||||
Require(sceNgs2SystemDestroy(system, nullptr) == 0);
|
||||
}
|
||||
|
||||
static void TestConfiguredGain() {
|
||||
const auto system = CreateSystem();
|
||||
const auto master = Mastering(system, 2);
|
||||
const auto target = Custom(system, 1, true);
|
||||
Patch(target, master);
|
||||
Control(target, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 1, Ngs2CustomVoiceGainParam{});
|
||||
ExpectOutput(system, 0.0f, 0.0f);
|
||||
Require(sceNgs2SystemDestroy(system, nullptr) == 0);
|
||||
}
|
||||
|
||||
struct CallbackContext {
|
||||
std::uint32_t calls = 0;
|
||||
};
|
||||
|
||||
static void APS5_VABI CountBlockEnd(const Ngs2VoiceCallbackInfo* info) {
|
||||
auto& context = *reinterpret_cast<CallbackContext*>(info->callback_data);
|
||||
Require(++context.calls == 1);
|
||||
}
|
||||
|
||||
static void TestRouting(std::uint32_t oldBuffers) {
|
||||
const auto system = CreateSystem();
|
||||
const auto firstMaster = Mastering(system, 2);
|
||||
const auto secondMaster = Mastering(system, 2);
|
||||
Control(secondMaster, SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT, Ngs2MasteringVoiceOutputParam{{}, 1, 0});
|
||||
const auto first = Custom(system, oldBuffers, false, oldBuffers - 1);
|
||||
const auto second = Custom(system, 2, false, 1);
|
||||
Patch(first, firstMaster);
|
||||
Patch(second, secondMaster);
|
||||
const auto source = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER));
|
||||
Control(source, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP,
|
||||
Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, 48000, 0, 0, 0}});
|
||||
std::array<std::int16_t, Grain * 2> pcm{};
|
||||
pcm.fill(8192);
|
||||
const Ngs2WaveformBlock block{0, sizeof(pcm), 0, 0, Grain, 0, 0};
|
||||
Control(source, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS,
|
||||
Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block});
|
||||
CallbackContext context;
|
||||
Control(source, SCE_NGS2_VOICE_PARAM_CALLBACK,
|
||||
Ngs2VoiceCallbackParam{{}, CountBlockEnd, reinterpret_cast<std::uintptr_t>(&context), SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0});
|
||||
Patch(source, first);
|
||||
Control(source, SCE_NGS2_VOICE_PARAM_PATCH, Ngs2VoicePatchParam{{}, 0, 1, second});
|
||||
Event(source, SCE_NGS2_VOICE_EVENT_PLAY);
|
||||
std::array<std::array<float, Grain * 2>, 2> outputs{};
|
||||
std::array<Ngs2RenderBufferInfo, 2> info{};
|
||||
for (std::size_t i = 0; i < outputs.size(); i++) {
|
||||
outputs[i].fill(9.0f);
|
||||
info[i] = {outputs[i].data(), sizeof(outputs[i]), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2};
|
||||
}
|
||||
Require(sceNgs2SystemRender(system, info.data(), info.size()) == SCE_NGS2_OK);
|
||||
Require(context.calls == 1);
|
||||
Ngs2VoicePortInfo port{};
|
||||
Require(sceNgs2VoiceGetPortInfo(source, 0, &port, sizeof(port)) == SCE_NGS2_OK);
|
||||
Require(port.dest_handle == second && port.dest_input_id == 1);
|
||||
for (const auto sample : outputs[0]) Require(sample == 0.0f);
|
||||
for (const auto sample : outputs[1]) Require(sample == 0.25f);
|
||||
Require(sceNgs2SystemDestroy(system, nullptr) == 0);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
Require(argc == 2);
|
||||
const std::string_view test = argv[1];
|
||||
if (test == "custom_callback") TestCustomCallback();
|
||||
else if (test == "upstream_callback") TestUpstreamCallback();
|
||||
else if (test == "configured_gain") TestConfiguredGain();
|
||||
else if (test == "routing_one_buffer") TestRouting(1);
|
||||
else if (test == "routing_two_buffers") TestRouting(2);
|
||||
else return 1;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
#include "Ngs2Test.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <initializer_list>
|
||||
#include <limits>
|
||||
|
||||
static std::int32_t APS5_VABI Generate(Ngs2UserFxProcessContext* context) {
|
||||
const auto* signal = reinterpret_cast<const float*>(context->user_data0);
|
||||
for (std::uint32_t c = 0; c < context->num_channels; c++)
|
||||
std::fill_n(context->channel_data[c], context->num_grain_samples, signal[c]);
|
||||
return SCE_NGS2_OK;
|
||||
}
|
||||
|
||||
struct GainGraph {
|
||||
std::uint32_t channels;
|
||||
uintptr_t system = CreateSystem();
|
||||
std::array<uintptr_t, 2> voices{};
|
||||
std::array<float, 8> signal{0.5f, -0.25f, 0.125f, -0.0625f, 0.03125f, -0.015625f, 0.0078125f, -0.00390625f};
|
||||
|
||||
GainGraph(std::uint32_t channelCount = 2, std::uint32_t voiceCount = 1)
|
||||
: channels(channelCount) {
|
||||
const auto master = Mastering(system, channels);
|
||||
Ngs2CustomSubmixerRackOption mixerOption{};
|
||||
auto& custom = mixerOption.custom_rack_option;
|
||||
custom.rack_option.size = sizeof(mixerOption);
|
||||
custom.rack_option.max_grain_samples = 512;
|
||||
custom.rack_option.max_voices = voiceCount;
|
||||
custom.rack_option.max_matrices = 1;
|
||||
custom.rack_option.max_ports = 1;
|
||||
custom.num_buffers = 1;
|
||||
custom.num_modules = 1;
|
||||
custom.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_GAIN;
|
||||
mixerOption.max_channels = channels;
|
||||
mixerOption.max_inputs = 1;
|
||||
const auto rackId = SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER;
|
||||
Ngs2ContextBufferInfo query{};
|
||||
Require(sceNgs2RackQueryBufferSize(rackId, &custom.rack_option, &query) == SCE_NGS2_OK);
|
||||
const auto buffer = Buffer(query);
|
||||
uintptr_t rack = 0;
|
||||
Require(sceNgs2RackCreate(system, rackId, &custom.rack_option, &buffer, &rack) == SCE_NGS2_OK);
|
||||
for (std::uint32_t v = 0; v < voiceCount; v++) {
|
||||
Require(sceNgs2RackGetVoiceHandle(rack, v, &voices[v]) == SCE_NGS2_OK);
|
||||
Control(voices[v], SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP,
|
||||
Ngs2CustomSubmixerVoiceSetupParam{{}, channels, channels, 0, 0});
|
||||
const auto source = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SUBMIXER));
|
||||
Control(source, SCE_NGS2_SUBMIXER_VOICE_PARAM_SETUP, Ngs2SubmixerVoiceSetupParam{{}, channels, 0});
|
||||
Control(source, SCE_NGS2_SUBMIXER_VOICE_PARAM_USER_FX,
|
||||
Ngs2SubmixerVoiceUserFxParam{{}, Generate, reinterpret_cast<std::uintptr_t>(signal.data()), 0, 0});
|
||||
Patch(source, voices[v]);
|
||||
Event(source, SCE_NGS2_VOICE_EVENT_PLAY);
|
||||
Patch(voices[v], master);
|
||||
Event(voices[v], SCE_NGS2_VOICE_EVENT_PLAY);
|
||||
}
|
||||
}
|
||||
|
||||
~GainGraph() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); }
|
||||
|
||||
void Set(std::initializer_list<float> values, std::uint32_t voice = 0) {
|
||||
Ngs2CustomVoiceGainParam param{};
|
||||
std::fill_n(param.gain, 37, std::numeric_limits<float>::quiet_NaN());
|
||||
std::copy(values.begin(), values.end(), param.gain);
|
||||
Control(voices[voice], SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN, param);
|
||||
}
|
||||
|
||||
std::vector<float> Render(std::uint32_t grain = Grain) {
|
||||
Require(sceNgs2SystemSetGrainSamples(system, grain) == SCE_NGS2_OK);
|
||||
std::vector<float> out(grain * channels, -123.0f);
|
||||
const Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, channels};
|
||||
Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK);
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
static void ExpectChannels(const std::vector<float>& out, std::initializer_list<std::vector<float>> channels) {
|
||||
std::size_t c = 0;
|
||||
for (const auto& samples : channels) {
|
||||
Require(out.size() == samples.size() * channels.size());
|
||||
for (std::size_t i = 0; i < samples.size(); i++) Require(out[i * channels.size() + c] == samples[i]);
|
||||
c++;
|
||||
}
|
||||
}
|
||||
|
||||
static void ExpectFrame(const std::vector<float>& out, std::size_t index, std::initializer_list<float> frame) {
|
||||
Require((index + 1) * frame.size() <= out.size());
|
||||
Require(std::equal(frame.begin(), frame.end(), out.begin() + index * frame.size()));
|
||||
}
|
||||
|
||||
static void ExpectConstant(const std::vector<float>& out, std::initializer_list<float> frame) {
|
||||
Require(out.size() % frame.size() == 0);
|
||||
for (std::size_t i = 0; i < out.size() / frame.size(); i++) ExpectFrame(out, i, frame);
|
||||
}
|
||||
|
||||
static void TestRamps() {
|
||||
GainGraph graph;
|
||||
graph.Set({64.0f, -64.0f});
|
||||
graph.Set({8.0f, -16.0f});
|
||||
ExpectChannels(graph.Render(), {{0, 0.5f, 1, 1.5f, 2, 2.5f, 3, 3.5f}, {0, 0.5f, 1, 1.5f, 2, 2.5f, 3, 3.5f}});
|
||||
ExpectConstant(graph.Render(), {4, 4});
|
||||
graph.Set({8.0f, -16.0f});
|
||||
ExpectConstant(graph.Render(), {4, 4});
|
||||
graph.Set({16.0f, 16.0f});
|
||||
graph.Set({-80.0f, 96.0f});
|
||||
ExpectChannels(graph.Render(), {{4, -1.5f, -7, -12.5f, -18, -23.5f, -29, -34.5f}, {4, 0.5f, -3, -6.5f, -10, -13.5f, -17, -20.5f}});
|
||||
ExpectConstant(graph.Render(), {-40, -24});
|
||||
graph.Set({0, 0});
|
||||
ExpectChannels(graph.Render(), {{-40, -35, -30, -25, -20, -15, -10, -5}, {-24, -21, -18, -15, -12, -9, -6, -3}});
|
||||
ExpectConstant(graph.Render(), {0, 0});
|
||||
}
|
||||
|
||||
static void TestEightChannels() {
|
||||
GainGraph graph(8);
|
||||
graph.Set({2, 8, 24, 64, 160, 384, 896, 2048});
|
||||
const auto out = graph.Render();
|
||||
ExpectFrame(out, 0, {0, 0, 0, 0, 0, 0, 0, 0});
|
||||
ExpectFrame(out, 4, {0.5f, -1, 1.5f, -2, 2.5f, -3, 3.5f, -4});
|
||||
ExpectFrame(out, 7, {7.0f / 8, -7.0f / 4, 21.0f / 8, -7.0f / 2, 35.0f / 8, -21.0f / 4, 49.0f / 8, -7});
|
||||
ExpectConstant(graph.Render(), {1, -2, 3, -4, 5, -6, 7, -8});
|
||||
}
|
||||
|
||||
static void TestVoicesAndPause() {
|
||||
GainGraph graph(1, 2);
|
||||
graph.Set({8}, 0);
|
||||
graph.Set({-4}, 1);
|
||||
ExpectChannels(graph.Render(), {{0, 0.25f, 0.5f, 0.75f, 1, 1.25f, 1.5f, 1.75f}});
|
||||
ExpectConstant(graph.Render(), {2});
|
||||
Event(graph.voices[0], SCE_NGS2_VOICE_EVENT_PAUSE);
|
||||
graph.Set({16}, 0);
|
||||
ExpectConstant(graph.Render(), {-2});
|
||||
Event(graph.voices[0], SCE_NGS2_VOICE_EVENT_RESUME);
|
||||
ExpectChannels(graph.Render(), {{2, 2.5f, 3, 3.5f, 4, 4.5f, 5, 5.5f}});
|
||||
ExpectConstant(graph.Render(), {6});
|
||||
graph.Set({-12}, 1);
|
||||
ExpectChannels(graph.Render(), {{6, 5.5f, 5, 4.5f, 4, 3.5f, 3, 2.5f}});
|
||||
ExpectConstant(graph.Render(), {2});
|
||||
}
|
||||
|
||||
static void TestGrainChanges() {
|
||||
GainGraph graph(1);
|
||||
graph.Set({8});
|
||||
auto out = graph.Render(64);
|
||||
Require(out.front() == 0 && out[1] == 1.0f / 16 && out[32] == 2 && out.back() == 63.0f / 16);
|
||||
ExpectConstant(graph.Render(64), {4});
|
||||
graph.Set({-8});
|
||||
out = graph.Render(128);
|
||||
Require(out.front() == 4 && out[1] == 63.0f / 16 && out[64] == 0 && out.back() == -63.0f / 16);
|
||||
ExpectConstant(graph.Render(), {-4});
|
||||
}
|
||||
|
||||
static void TestNonfiniteGains() {
|
||||
GainGraph graph(1);
|
||||
graph.Set({std::numeric_limits<float>::infinity()});
|
||||
graph.Render();
|
||||
graph.Set({std::numeric_limits<float>::infinity()});
|
||||
for (float sample : graph.Render()) Require(std::isinf(sample) && sample > 0);
|
||||
graph.Set({std::numeric_limits<float>::quiet_NaN()});
|
||||
for (float sample : graph.Render()) Require(std::isnan(sample));
|
||||
for (float sample : graph.Render()) Require(std::isnan(sample));
|
||||
graph.Set({2});
|
||||
for (float sample : graph.Render()) Require(std::isnan(sample));
|
||||
ExpectConstant(graph.Render(), {1});
|
||||
}
|
||||
|
||||
static void TestZeroClearsNanSource() {
|
||||
GainGraph graph(1);
|
||||
graph.signal[0] = std::numeric_limits<float>::quiet_NaN();
|
||||
graph.Set({1});
|
||||
for (float sample : graph.Render()) Require(std::isnan(sample));
|
||||
graph.Set({0});
|
||||
for (float sample : graph.Render()) Require(std::isnan(sample));
|
||||
ExpectConstant(graph.Render(), {0});
|
||||
}
|
||||
|
||||
static void TestParameterPositions() {
|
||||
GainGraph graph(1);
|
||||
graph.Set({8});
|
||||
Ngs2CustomVoiceGainParam param{};
|
||||
param.header = {sizeof(param), 0, 0};
|
||||
param.gain[0] = 16;
|
||||
for (std::uint32_t position = 0; position < 256; position++) {
|
||||
if (position == 0) continue;
|
||||
param.header.id = SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | position;
|
||||
Require(sceNgs2VoiceControl(graph.voices[0], ¶m.header) == SCE_NGS2_ERROR_INVALID_PARAM_ID);
|
||||
}
|
||||
ExpectChannels(graph.Render(), {{0, 0.5f, 1, 1.5f, 2, 2.5f, 3, 3.5f}});
|
||||
ExpectConstant(graph.Render(), {4});
|
||||
}
|
||||
|
||||
static void TestLinkedParameters() {
|
||||
GainGraph graph(1);
|
||||
std::array<Ngs2CustomVoiceGainParam, 3> params{};
|
||||
for (auto& param : params) param.header = {sizeof(param), sizeof(param), SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN};
|
||||
params[0].gain[0] = 8;
|
||||
params[1].header.id |= 255;
|
||||
params[2].gain[0] = 16;
|
||||
params[2].header.next = 0;
|
||||
Require(sceNgs2VoiceControl(graph.voices[0], ¶ms[0].header) == SCE_NGS2_ERROR_INVALID_PARAM_ID);
|
||||
ExpectChannels(graph.Render(), {{0, 0.5f, 1, 1.5f, 2, 2.5f, 3, 3.5f}});
|
||||
ExpectConstant(graph.Render(), {4});
|
||||
}
|
||||
|
||||
static void TestIntegerOutput() {
|
||||
for (const float gain : {1.0f, -1.0f}) {
|
||||
GainGraph graph(1);
|
||||
graph.Set({gain});
|
||||
graph.Render();
|
||||
std::array<std::int16_t, Grain> out{};
|
||||
const Ngs2RenderBufferInfo info{out.data(), sizeof(out), SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1};
|
||||
Require(sceNgs2SystemRender(graph.system, &info, 1) == SCE_NGS2_OK);
|
||||
for (const auto sample : out) Require(sample == (gain > 0.0f ? 16384 : -16384));
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
TestRamps();
|
||||
TestEightChannels();
|
||||
TestVoicesAndPause();
|
||||
TestGrainChanges();
|
||||
TestNonfiniteGains();
|
||||
TestParameterPositions();
|
||||
TestZeroClearsNanSource();
|
||||
TestLinkedParameters();
|
||||
TestIntegerOutput();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
#include "prx/libSceNgs2.native/tests/Ngs2Test.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
static Ngs2CustomSubmixerRackOption RackOption(std::uint32_t count, std::uint32_t channels = 2) {
|
||||
Ngs2CustomSubmixerRackOption option{};
|
||||
auto& custom = option.custom_rack_option;
|
||||
custom.rack_option.size = sizeof(option);
|
||||
custom.rack_option.max_grain_samples = 512;
|
||||
custom.rack_option.max_voices = 2;
|
||||
custom.rack_option.max_matrices = 1;
|
||||
custom.rack_option.max_ports = count;
|
||||
custom.num_buffers = count;
|
||||
for (std::uint32_t port = 0; port < count; port++) custom.port[port].source_buffer_id = port;
|
||||
option.max_channels = channels;
|
||||
option.max_inputs = count == 1 ? 1 : 2;
|
||||
return option;
|
||||
}
|
||||
|
||||
static void AddGain(Ngs2CustomSubmixerRackOption& option, std::uint32_t source, std::uint32_t dest) {
|
||||
auto& custom = option.custom_rack_option;
|
||||
custom.num_modules = 1;
|
||||
custom.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_GAIN;
|
||||
custom.module[0].source_buffer_id = source;
|
||||
custom.module[0].dest_buffer_id = dest;
|
||||
custom.module[0].extra_buffer_id = custom.num_buffers == 1 ? 0 : 1;
|
||||
}
|
||||
|
||||
static uintptr_t CustomRack(uintptr_t system, const Ngs2CustomSubmixerRackOption& option) {
|
||||
Ngs2ContextBufferInfo query{};
|
||||
Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &option.custom_rack_option.rack_option, &query) == SCE_NGS2_OK);
|
||||
const auto info = Buffer(query);
|
||||
uintptr_t rack = 0;
|
||||
Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &option.custom_rack_option.rack_option, &info, &rack) == SCE_NGS2_OK);
|
||||
return rack;
|
||||
}
|
||||
|
||||
static uintptr_t CustomVoice(uintptr_t rack, std::uint32_t index = 0, std::uint32_t channels = 2) {
|
||||
uintptr_t voice = 0;
|
||||
Require(sceNgs2RackGetVoiceHandle(rack, index, &voice) == SCE_NGS2_OK);
|
||||
Control(voice, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, channels, channels, 0, 0});
|
||||
Event(voice, SCE_NGS2_VOICE_EVENT_PLAY);
|
||||
return voice;
|
||||
}
|
||||
|
||||
static void SetGain(uintptr_t voice, float level) {
|
||||
Ngs2CustomVoiceGainParam param{};
|
||||
for (auto& gain : param.gain) gain = level;
|
||||
Control(voice, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN, param);
|
||||
}
|
||||
|
||||
static void Route(uintptr_t source, std::uint32_t port, uintptr_t dest, std::uint32_t input = 0, std::uint32_t channels = 2) {
|
||||
std::array<float, 64> identity{};
|
||||
for (std::uint32_t channel = 0; channel < channels; channel++) identity[channel * channels + channel] = 1.0f;
|
||||
Control(source, SCE_NGS2_VOICE_PARAM_MATRIX_LEVELS, Ngs2VoiceMatrixLevelsParam{{}, 0, channels * channels, identity.data()});
|
||||
Control(source, SCE_NGS2_VOICE_PARAM_PORT_MATRIX, Ngs2VoicePortMatrixParam{{}, port, 0});
|
||||
Control(source, SCE_NGS2_VOICE_PARAM_PATCH, Ngs2VoicePatchParam{{}, port, input, dest});
|
||||
Ngs2VoicePortInfo info{};
|
||||
Require(sceNgs2VoiceGetPortInfo(source, port, &info, sizeof(info)) == SCE_NGS2_OK);
|
||||
Require(info.dest_handle == dest && info.dest_input_id == input);
|
||||
}
|
||||
|
||||
static void Output(uintptr_t system, uintptr_t source, std::uint32_t port, std::uint32_t output, std::uint32_t channels = 2) {
|
||||
const auto master = Mastering(system, channels);
|
||||
Control(master, SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT, Ngs2MasteringVoiceOutputParam{{}, output, 0});
|
||||
Route(source, port, master, 0, channels);
|
||||
}
|
||||
|
||||
static uintptr_t Input(uintptr_t system, const std::vector<std::int16_t>& pcm, std::uint32_t channels = 2) {
|
||||
const auto voice = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER));
|
||||
Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP,
|
||||
Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, channels, 48000, 0, 0, 0}});
|
||||
const Ngs2WaveformBlock block{0, pcm.size() * sizeof(std::int16_t), 0, 0, static_cast<std::uint32_t>(pcm.size() / channels), 0, 0};
|
||||
Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block});
|
||||
Event(voice, SCE_NGS2_VOICE_EVENT_PLAY);
|
||||
return voice;
|
||||
}
|
||||
|
||||
static void Render(uintptr_t system, const std::vector<float>& expected, std::uint32_t channels = 2, std::uint32_t grain = Grain) {
|
||||
Require(sceNgs2SystemSetGrainSamples(system, grain) == SCE_NGS2_OK);
|
||||
std::vector<std::vector<float>> samples(expected.size(), std::vector<float>(grain * channels, 9.0f));
|
||||
std::vector<Ngs2RenderBufferInfo> info;
|
||||
for (auto& output : samples) info.push_back({output.data(), output.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, channels});
|
||||
Require(sceNgs2SystemRender(system, info.data(), static_cast<std::uint32_t>(info.size())) == SCE_NGS2_OK);
|
||||
for (std::size_t port = 0; port < expected.size(); port++) {
|
||||
for (const auto sample : samples[port]) Require(std::fabs(sample - expected[port]) <= 0.000001f);
|
||||
}
|
||||
}
|
||||
|
||||
static void TestNoModules(std::uint32_t count, std::vector<float> expected) {
|
||||
const auto system = CreateSystem();
|
||||
auto option = RackOption(count);
|
||||
for (std::uint32_t port = 0; port < count; port++) option.custom_rack_option.port[port].source_buffer_id = count - port - 1;
|
||||
const auto custom = CustomVoice(CustomRack(system, option));
|
||||
for (std::uint32_t port = 0; port < count; port++) Output(system, custom, port, port);
|
||||
const std::vector<std::int16_t> pcm0(Grain * 2 * 16, 8192);
|
||||
const std::vector<std::int16_t> pcm1(Grain * 2 * 16, -24576);
|
||||
const auto input0 = Input(system, pcm0);
|
||||
Route(input0, 0, custom);
|
||||
uintptr_t input1 = 0;
|
||||
if (count > 1) {
|
||||
input1 = Input(system, pcm1);
|
||||
Route(input1, 0, custom, 1);
|
||||
}
|
||||
Render(system, expected);
|
||||
if (count > 1) {
|
||||
Route(input1, 0, custom, 0);
|
||||
expected[count - 1] = -0.5f;
|
||||
expected[count - 2] = 0.0f;
|
||||
Render(system, expected);
|
||||
Route(input1, 0, 0);
|
||||
expected[count - 1] = 0.25f;
|
||||
Render(system, expected);
|
||||
}
|
||||
Route(input0, 0, 0);
|
||||
Render(system, std::vector<float>(count, 0.0f));
|
||||
Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK);
|
||||
}
|
||||
|
||||
static void TestGainRouting() {
|
||||
struct Case {
|
||||
std::uint32_t count, source, dest;
|
||||
std::vector<float> expected;
|
||||
};
|
||||
const Case cases[] = {
|
||||
{1, 0, 0, {0.5f}},
|
||||
{2, 0, 1, {0.25f, 0.5f}},
|
||||
{2, 1, 0, {-1.5f, -0.75f}},
|
||||
{3, 0, 2, {0.25f, -0.75f, 0.5f}},
|
||||
{3, 2, 1, {0.25f, 0.0f, 0.0f}},
|
||||
{4, 1, 3, {0.25f, -0.75f, 0.0f, -1.5f}},
|
||||
{4, 3, 0, {0.0f, -0.75f, 0.0f, 0.0f}},
|
||||
{4, 1, 1, {0.25f, -1.5f, 0.0f, 0.0f}},
|
||||
};
|
||||
for (const auto& test : cases) {
|
||||
const auto system = CreateSystem();
|
||||
auto option = RackOption(test.count);
|
||||
AddGain(option, test.source, test.dest);
|
||||
const auto custom = CustomVoice(CustomRack(system, option));
|
||||
SetGain(custom, 2.0f);
|
||||
for (std::uint32_t port = 0; port < test.count; port++) Output(system, custom, port, port);
|
||||
const std::vector<float> silence(test.count, 0.0f);
|
||||
Render(system, silence);
|
||||
const std::vector<std::int16_t> pcm0(Grain * 2 * 16, 8192);
|
||||
const std::vector<std::int16_t> pcm1(Grain * 2 * 16, -24576);
|
||||
const auto input0 = Input(system, pcm0);
|
||||
Route(input0, 0, custom);
|
||||
uintptr_t input1 = 0;
|
||||
if (test.count > 1) {
|
||||
input1 = Input(system, pcm1);
|
||||
Route(input1, 0, custom, 1);
|
||||
}
|
||||
Render(system, test.expected);
|
||||
Render(system, test.expected, 2, 64);
|
||||
Route(input0, 0, 0);
|
||||
if (input1 != 0) Route(input1, 0, 0);
|
||||
Render(system, silence);
|
||||
Render(system, silence);
|
||||
Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK);
|
||||
}
|
||||
}
|
||||
|
||||
static void TestUnityCopy() {
|
||||
const auto system = CreateSystem();
|
||||
auto option = RackOption(2);
|
||||
AddGain(option, 0, 1);
|
||||
const auto custom = CustomVoice(CustomRack(system, option));
|
||||
SetGain(custom, 1.0f);
|
||||
Output(system, custom, 0, 0);
|
||||
Output(system, custom, 1, 1);
|
||||
Render(system, {0.0f, 0.0f});
|
||||
std::vector<std::int16_t> source(Grain * 2), dest(Grain * 2);
|
||||
for (std::uint32_t i = 0; i < Grain; i++) {
|
||||
source[i * 2] = 8192;
|
||||
source[i * 2 + 1] = -16384;
|
||||
dest[i * 2] = -24576;
|
||||
dest[i * 2 + 1] = 4096;
|
||||
}
|
||||
Route(Input(system, source), 0, custom);
|
||||
Route(Input(system, dest), 0, custom, 1);
|
||||
std::array<std::array<float, Grain * 2>, 2> outputs{};
|
||||
std::array<Ngs2RenderBufferInfo, 2> info{};
|
||||
for (std::size_t i = 0; i < outputs.size(); i++) {
|
||||
info[i] = {outputs[i].data(), sizeof(outputs[i]), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2};
|
||||
}
|
||||
Require(sceNgs2SystemRender(system, info.data(), info.size()) == SCE_NGS2_OK);
|
||||
for (const auto& output : outputs) {
|
||||
for (std::uint32_t i = 0; i < Grain; i++) Require(output[i * 2] == 0.25f && output[i * 2 + 1] == -0.5f);
|
||||
}
|
||||
Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK);
|
||||
}
|
||||
|
||||
static void TestIndependentVoicesAndSilentRamp(std::uint32_t channels) {
|
||||
const auto system = CreateSystem();
|
||||
auto option = RackOption(4, channels);
|
||||
AddGain(option, 0, 3);
|
||||
const auto rack = CustomRack(system, option);
|
||||
const auto first = CustomVoice(rack, 0, channels);
|
||||
const auto second = CustomVoice(rack, 1, channels);
|
||||
SetGain(first, 2.0f);
|
||||
SetGain(second, -1.0f);
|
||||
Output(system, first, 3, 0, channels);
|
||||
Output(system, second, 3, 1, channels);
|
||||
Render(system, {0.0f, 0.0f}, channels);
|
||||
const std::vector<std::int16_t> pcm0(Grain * channels * 16, 8192);
|
||||
const std::vector<std::int16_t> pcm1(Grain * channels * 16, -24576);
|
||||
const auto input0 = Input(system, pcm0, channels);
|
||||
const auto input1 = Input(system, pcm1, channels);
|
||||
Route(input0, 0, first, 0, channels);
|
||||
Route(input1, 0, second, 0, channels);
|
||||
Render(system, {0.5f, 0.75f}, channels);
|
||||
Route(input0, 0, 0, 0, channels);
|
||||
Render(system, {0.0f, 0.75f}, channels);
|
||||
SetGain(first, 4.0f);
|
||||
Render(system, {0.0f, 0.75f}, channels);
|
||||
Route(input0, 0, first, 0, channels);
|
||||
Render(system, {1.0f, 0.75f}, channels);
|
||||
Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK);
|
||||
}
|
||||
|
||||
int main() {
|
||||
TestNoModules(1, {0.25f});
|
||||
TestNoModules(2, {-0.75f, 0.25f});
|
||||
TestNoModules(3, {0.0f, -0.75f, 0.25f});
|
||||
TestNoModules(4, {0.0f, 0.0f, -0.75f, 0.25f});
|
||||
TestGainRouting();
|
||||
TestUnityCopy();
|
||||
for (std::uint32_t channels = 1; channels <= 8; channels++) TestIndependentVoicesAndSilentRamp(channels);
|
||||
return 0;
|
||||
}
|
||||
@@ -3,6 +3,30 @@ add_sce_ngs2_library(libSceNgs2)
|
||||
target_sources(libSceNgs2 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/PendingExports.cpp)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_test_executable(ngs2_core_custom_safety_tests ${CMAKE_CURRENT_SOURCE_DIR}/../libSceNgs2.native/tests/Ngs2CustomSafety.cpp)
|
||||
target_include_directories(ngs2_core_custom_safety_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(ngs2_core_custom_safety_tests PRIVATE libSceNgs2 libkernel libc)
|
||||
configure_windows_unwind(ngs2_core_custom_safety_tests)
|
||||
foreach(safetyCase IN ITEMS custom_callback upstream_callback configured_gain routing_one_buffer routing_two_buffers)
|
||||
add_test(NAME ngs2_core_custom_safety_${safetyCase} COMMAND ngs2_core_custom_safety_tests ${safetyCase})
|
||||
set_tests_properties(ngs2_core_custom_safety_${safetyCase} PROPERTIES TIMEOUT 10)
|
||||
if(WIN32)
|
||||
set_tests_properties(ngs2_core_custom_safety_${safetyCase} PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNgs2>")
|
||||
endif()
|
||||
endforeach()
|
||||
foreach(ngs2Test IN ITEMS Gain Routing)
|
||||
string(TOLOWER ${ngs2Test} testName)
|
||||
add_test_executable(ngs2_core_${testName}_tests ${CMAKE_CURRENT_SOURCE_DIR}/../libSceNgs2.native/tests/Ngs2${ngs2Test}.cpp)
|
||||
target_include_directories(ngs2_core_${testName}_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src)
|
||||
target_link_libraries(ngs2_core_${testName}_tests PRIVATE libSceNgs2 atrac9 libkernel libc)
|
||||
configure_windows_unwind(ngs2_core_${testName}_tests)
|
||||
add_test(NAME ngs2_core_${testName} COMMAND ngs2_core_${testName}_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(ngs2_core_${testName} PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNgs2>")
|
||||
endif()
|
||||
endforeach()
|
||||
add_test_executable(ngs2_core_render_tests ${CMAKE_CURRENT_SOURCE_DIR}/../libSceNgs2.native/tests/Ngs2Render.cpp)
|
||||
target_include_directories(ngs2_core_render_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src)
|
||||
target_link_libraries(ngs2_core_render_tests PRIVATE libSceNgs2 atrac9 libkernel libc)
|
||||
|
||||
@@ -26,13 +26,13 @@
|
||||
- [NGS2 sampler voice state flag EMPTY](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a playing sampler voice that still accepts blocks (`CONTINUE`) and has played all queued blocks reports `INUSE|PLAYING|EMPTY` (0x20) and keeps playing, so blocks added later play on. PPSA18391 (engine: Clickteam Fusion) reads `state_flags & 0x22 != 2` from `sceNgs2VoiceGetState` as the end of a sound and only then reuses the voice. When the console sets and clears EMPTY, and whether it also reports it for other voice types, is unverified on hardware.
|
||||
- [NGS2 reverb channel conversion](../../../core/libs/prx/libSceNgs2.native/src/Reverb.cpp) (libSceNgs2.native) - a reverb voice whose `num_input_channels` differs from `num_output_channels` (PPSA03747 sets up 2 inputs and 8 outputs) mixes its ports in the input layout and folds that to mono with the input count's gains, as an equal-count voice does, and runs the comb and allpass stages per output channel. The PS4 library's handling of the dry signal in this case is not checked: here it reaches the output layout through the default channel map, so a stereo input keeps its dry signal on the front left and right and the other outputs carry only the reverb
|
||||
- [NGS2 port matrix](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a port without a matrix, or with a selected matrix whose levels were never set, mixes through a default channel map: mono reaches both stereo channels at -3 dB and the 5.1 and 7.1 centre, stereo folds to mono at -3 dB per side, and wider sources fold down with 0.5 and -3 dB gains. The map and the equivalence of an unset matrix with no matrix were measured from the PS4 firmware 11.00 `libSceNgs2.sprx`, not yet from the PS5 library. That library only accepts submixer and mastering voices with 1, 2, 6 or 8 channels, so the map into 3, 4, 5 or 7 channels throws. A matrix set with fewer levels than the destination needs still mixes only the rows it has
|
||||
- [NGS2 custom submixer rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - UserFx2 process flags and setup/process/cleanup order follow only [KytyPS5](https://github.com/KytyPS5/KytyPS5), not a console measurement; layouts follow [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`. A module control handler, unequal input and output channels, setup flags and default rack options throw. Module state is a separate buffer, not the `state_offset` slice of voice state; `sceNgs2VoiceGetState` throws for this rack. UserFx2 runs only on grains with input samples, so an effect tail stops with the input
|
||||
- [NGS2 custom submixer rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - UserFx2 process flags and setup/process/cleanup order follow only [KytyPS5](https://github.com/KytyPS5/KytyPS5), not a console measurement; layouts follow [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`. A module control handler, unequal input and output channels, setup flags and default rack options throw. Module state is a separate buffer, not the `state_offset` slice of voice state; `sceNgs2VoiceGetState` throws for this rack. UserFx2 runs only on grains with input samples, so an effect tail stops with the input. Setup changes from custom process callbacks throw before later modules can use the changed layout; inconsistent input-mixing layouts after callbacks also throw. Console reentry behavior is unknown
|
||||
- [NGS2 custom UserFx](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - the 48-byte process context, process flags, callback error handling and null-handler behavior retain the existing submixer callback model and are not console-verified. Non-null module options throw because their console error is unknown. UserFx on custom sampler rack 0x4001 throws: its parameter indexing and routing are not implemented. Module id 0x19, the 40-byte voice parameter (handler and three user-data words), parameter id `0x40001900 | index`, and lack of option and module state follow [fma-rc's PS5 12.70 module reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943), including parameter dispatch at 0x25ec4.
|
||||
- [NGS2 custom Delay](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, 0x1d) - processing and defaults remain unverified on PS5. The [agreed compatibility path](https://github.com/boykopovar/AnyPS5/pull/9337#issuecomment-6351205140) preserves main's PCM pass-through for Void Terrarium (PPSA03061) until any Delay parameter is set; every `0x40001dxx` parameter throws. Only one-buffer custom submixers with no module state and a 20-byte option are accepted. Option type 0/1, taps 1..8, length 1..20000 and reserved 0 follow [fma-rc's 12.70 module reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943). Other shapes and values throw because their console errors are unknown. This preserves host behavior, not console-equivalent Delay processing
|
||||
- [NGS2 custom module ids](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - Reverb (0x1b), PitchShift (0x20) and Convolution (0x23) are not processed: a rack that passes their creation checks throws. Gain (0x15) on custom sampler racks also throws. Per [fma-rc's 12.70 module reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A8302` for a wrong non-zero state size, and `0x804A8013`, `0x804A8099` and `0x804A8098` for a bad Reverb option size or word and PitchShift word. Unknown: check order, errors for a Gain option, missing Reverb or PitchShift options, other PitchShift and Convolution option sizes, and Convolution's state (64 bytes from SDK 3.0). PPSA04540 uses 0x15, 0x19, 0x1b and 0x23 (#8039), PPSA35169 0x20 (#4246) and PPSA33655 0x15, 0x19 and 0x1b
|
||||
- [NGS2 custom Gain module](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - follows [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943): parameter `0x40001500 | module index` holds 37 linear levels, one per channel, default 0, and the module writes source × level into dest, ramping linearly over the grain from the previous level. Unverified: where the ramp starts and ends (here the grain's first sample is one step past the previous level and its last sample is at the new level), whether stopping or setting up the voice again resets the levels, and what the library does with a level outside -32 to 32 (here it throws). On a custom sampler rack, whose parameter index is one higher per fma-rc, Gain throws. A Gain module with an option throws: fma-rc reads Gain as taking none, and the error the library returns for one is unknown. The extra buffer is ignored, as fma-rc reads it
|
||||
- [NGS2 custom module ids](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - Reverb (0x1b), PitchShift (0x20) and Convolution (0x23) are not processed: a rack that passes their creation checks throws. Gain (0x15) on custom sampler racks still throws: its processor initialization and lifecycle are unverified. Per [fma-rc's 12.70 module reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A8302` for a wrong non-zero state size, and `0x804A8013`, `0x804A8099` and `0x804A8098` for a bad Reverb option size or word and PitchShift word. Unknown: check order, errors for a Gain option, missing Reverb or PitchShift options, other PitchShift and Convolution option sizes, and Convolution's state (64 bytes from SDK 3.0). PPSA04540 uses 0x15, 0x19, 0x1b and 0x23 (#8039), PPSA35169 0x20 (#4246) and PPSA33655 0x15, 0x19 and 0x1b
|
||||
- [NGS2 custom Gain](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) - PS5 rounding and first-parameter processor state are unverified. The initial previous gain retains main's zero convention; the schema default does not verify processor initialization. Processing before a parameter, after re-setup or after stopping a configured voice throws. The unclamped levels, i/N ramp and equal-gain paths follow [the PS5 12.70 reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6391072574) (0x33ce0, 0x5310); invalid parameter positions return 0x804A835A (0x250b0). Custom sampler Gain and module options throw: their initialization, lifecycle and option errors remain unknown. Integer rendering of nonfinite samples throws because the console conversion is unknown.
|
||||
- [NGS2 custom mastering rack](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native, rack 0x4003) - follows [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943): option size is 0x508, matching the custom submixer rack layout. Creation returns `0x804A8052` above 37 channels; channels 9 to 37 throw because voices hold at most 8 channels. Mastering racks with modules throw: attached modules and non-default option fields are unverified on PS5. Voice control parameters 0x40030000 (setup), 0x40030004 (gain) and 0x40030005 (output) configure channels, levels and destination buffer. `sceNgs2VoiceGetState` fills common voice state flags. PPSA10278 and PPSA04540 create rack 0x4003 without modules (#8039)
|
||||
- [NGS2 custom rack buffers and channels](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, racks 0x4001 and 0x4002) - racks with 2 to 4 buffers, custom submixers with 9 to 37 channels and submixer `max_inputs` other than 1 or 2 throw: how modules and ports route several buffers is unknown, and voices hold at most 8 channels. Per [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A830B` for a buffer count outside 1 to 4 or below the submixer's `max_inputs`, `0x804A8306` for a module source, extra or dest buffer id at or above the count, and `0x804A8052` for more than 37 submixer channels; the check order is not known. `sceNgs2RackQueryBufferSize` throws for these options, as no source says whether it checks them. A port buffer id other than 0 throws. PPSA04540 creates racks with up to 4 buffers and a 16-channel custom submixer (#8039)
|
||||
- [NGS2 custom buffers](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) - multiple buffers on samplers, submixer channels above 8, inputs beyond 0/1 and multi-buffer chains beyond zero modules or one Gain throw. Default custom module order, UserFx extra-buffer selection and invalid port-buffer errors remain unknown. Input-to-buffer routing and grain clearing follow [the PS5 12.70 reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6391072574) (0x181a0); output-port selection follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/83f8bfd3c9c35fcb135ef6c33dfd88bbfe066ee2/src/libs/ngs2.cpp#L417-L419). These paths have not been compared with console output. Creation validation order and QueryBufferSize validation remain unverified; the query throws for options whose creation returns an error.
|
||||
- [NGS2 reverb rack](../../../core/libs/prx/libSceNgs2.native/src/Reverb.cpp) (libSceNgs2.native, rack 0x2001) - follows the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output at 11.025 to 48 kHz, including parameter changes during playback (differences at float rounding level); not yet compared with the PS5 library. Structure layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_reverb.h` and the param ids 0x20010000 (setup) and 0x20010001 (I3DL2) from PPSA22520. Only the default `reverb_size` of 1 is handled, with its 14400-sample reflection buffer; other sizes and the library's half-rate mode throw. System sample rates below about 7.5 kHz, where an allpass delay would have no samples, throw. Setup only accepts equal input and output channel counts
|
||||
- [sceNgs2ReportRegisterHandler, sceNgs2ReportUnregisterHandler](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native, NIDs `uBIN24Tv2MI`, `nPzb7Ly-VjE`) keep a registry of report handlers. Register rejects a null handler with an invalid argument and returns `INVALID_OUT_ADDRESS` for a null out handle, then stores the report type, handler and user data under a freshly allocated opaque handle; unregister removes and frees the matching record and throws for a handle that is not registered. The signatures (report type, handler, user data, out handle) and (report handle) follow [SharpProspero](https://github.com/SvenGDK/SharpProspero) and [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_report.h`, and the 24-byte `Ngs2ReportDataHeader` passed to a handler is its `OrbisNgs2ReportDataHeader`. No report is produced yet, so no registered handler is ever invoked and PPSA04540, which imports both and registers a CPU load handler (type 5), still receives none.
|
||||
- [sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A null user data pointer throws. `sceNgs2SystemLock` and `sceNgs2SystemUnlock` follow [prosper's notes](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp#L4367-L4384) on the PS5 module: Lock takes a per-system lock and counts nested holds, Unlock releases one hold from any thread and returns 0 without a hold, and `sceNgs2SystemRender` only tries the lock: while another thread holds it, Render clears one grain of each output, leaves the render count and voices unchanged and returns 0. The other calls do not wait for the system lock. Peace Walker (PPSA18216) holds it while it spins on a flag that its audio thread clears only after `sceNgs2SystemRender` and eight `sceNgs2VoiceGetStateFlags` calls, so on the console neither can wait for it. Whether the console's voice and rack calls take the lock at all is not known. `sceNgs2RackLock` still holds the library's lock until `sceNgs2RackUnlock`, as before
|
||||
@@ -46,8 +46,8 @@
|
||||
- [sceNgs2VoiceGetOwner](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - the signature `(voiceHandle, outOwnerRack, outVoiceIndex)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L439-L440) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2.cpp#L322-L326), which only logs it; neither gives an error. The errors are unmeasured: a handle that is not a live voice answers `INVALID_VOICE_HANDLE` before the outputs are checked, as in `sceNgs2RackGetVoiceHandle`, and either output pointer null answers `INVALID_OUT_ADDRESS` without writing the other
|
||||
- [sceNgs2VoiceGetMatrixInfo](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a matrix id past the rack's `max_matrices` throws, as the PS5 code for shadPS4's PS4 [`INVALID_MATRIX_INDEX`](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2_error.h#L92) is unknown. A matrix without levels reports 0 levels, though it mixes through the default channel map. Signature from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L442), layout (a count and 64 levels) from [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2.h#L263-L266); levels are the stored, clamped ones. Unmeasured: a null info answers `INVALID_OUT_ADDRESS` and a size other than 260 `INVALID_OUT_SIZE` before the voice is checked, as in `sceNgs2VoiceGetPortInfo`
|
||||
- [sceNgs2CustomRackGetModuleInfo](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - a size over 32 bytes throws, as the longer per-module info in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2_custom.h#L419-L442) is not kept. A size under 32 and an index past the rack's modules (any on a non-custom rack) also throw: the PS5 codes for shadPS4's PS4 [`INVALID_MODULE_INDEX` and `INVALID_MODULE_INFO_SIZE`](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2_error.h#L117-L118) are unknown. It repeats the ids, state offset and size from rack creation, reserved words 0, as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L323-L341) lays it out. Unmeasured: a null info gives `INVALID_OUT_ADDRESS`, then a bad handle `INVALID_RACK_HANDLE`
|
||||
- [sceNgs2VoiceControl patch](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a destination rack handle with input id 0 resolves to that rack's first voice; this matches the Dead Cells call in #4334, but selection for racks with multiple voices has not been checked against a console. Nonzero destination input ids still throw. `sceNgs2VoiceGetPortInfo` reports the resolved voice handle and zero delay.
|
||||
- [NGS2 PCM waveform parsing](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - `sceNgs2ParseWaveformData` reads integer PCM (tag 1, or `WAVE_FORMAT_EXTENSIBLE` with the PCM GUID) of 8, 16, 24 and 32 bits as types 0x11, 0x12, 0x14 and 0x16, and float (tag 3) of 32 and 64 bits as 0x18 and 0x1a, and returns 0x804A8432 for other depths, after [prosper's reading of `libSceNgs2.native.sprx`](https://github.com/mattias800/prosper/blob/30572df43ae20ff28f995db56149f7af42235741/prosper/src/hle/audio/ngs2_waveform.cpp#L194-L223); not measured on a console. Every depth keeps the checks of the 16-bit path (channels, rate, block align, whole frames, a looped `smpl` chunk throws), which that reading does not apply. Voices still play only 16-bit PCM and ATRAC9
|
||||
- [NGS2 patch destinations](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) - a rack handle with input 0 resolves to its first voice, matching the Dead Cells call in #4334; selection on multi-voice racks remains unverified. Input 1 is supported only for a voice handle in a two-input custom submixer. Nonzero rack-handle inputs, other unsupported inputs and nonzero inputs on disconnected ports throw because their contracts are unknown. GetPortInfo reports the resolved voice handle and selected input, with zero delay. Changes to a port destination or input while its source is rendered throw before mixing: callback routing order is unverified, and combining the old destination with the new input is invalid.
|
||||
- [NGS2 PCM waveform parsing](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - `sceNgs2ParseWaveformData` reads integer PCM (tag 1, or `WAVE_FORMAT_EXTENSIBLE` with the PCM GUID) of 8, 16, 24 and 32 bits as types 0x11, 0x12, 0x14 and 0x16, and float (tag 3) of 32 and 64 bits as 0x18 and 0x1a, and returns 0x804A8432 for other depths, after [prosper's reading of `libSceNgs2.native.sprx`](https://github.com/mattias800/prosper/blob/30572df43ae20ff28f995db56149f7af42235741/prosper/src/hle/audio/ngs2_waveform.cpp#L194-L223); not measured on a console. Every depth keeps the checks of the 16-bit path (channels, rate, block align, whole frames, a looped `smpl` chunk throws), which that reading does not apply
|
||||
- [sceNgs2ParseWaveformFile](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature `(path, offset, info)` is taken from the call site of PPSA21402, which sets only three argument registers; the file is read from `offset` to its end, the data and block offsets are taken to be relative to the start of the file, not of `offset`, and an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame; `INVALID_WAVEFORM_DATA` for a `data` chunk that runs past the buffer or ends inside a frame is assumed, and looped PCM and other PCM formats throw
|
||||
- [sceNgs2VoiceControl sampler filter](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown
|
||||
- [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - inputs on which the PS5 `libSceNgs2.native.sprx` of firmware 3.20 and 4.03 did not return within 60 s throw; whether it returns at all for them is unknown. They are an infinite speaker or param angle, an angle too large for a turn to change it, a speaker angle that wraps to exactly the unit angle, and a param angle that wraps onto the first speaker plus one turn, such as one float below the first speaker in radians
|
||||
|
||||
Reference in new issue
Block a user