diff --git a/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake b/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake index 453e4d6a1..1d9d416b5 100644 --- a/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake +++ b/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake @@ -3,6 +3,7 @@ function(add_sce_ngs2_library target) add_library(${target} SHARED EXCLUDE_FROM_ALL ${ngs2Dir}/src/Atrac9.cpp ${ngs2Dir}/src/Custom.cpp + ${ngs2Dir}/src/Pan.cpp ${ngs2Dir}/src/Rack.cpp ${ngs2Dir}/src/Render.cpp ${ngs2Dir}/src/System.cpp diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index 3496f022f..5cdad17a2 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -548,12 +548,19 @@ struct Ngs2WaveformInfo { static_assert(sizeof(Ngs2WaveformInfo) == 232); struct Ngs2PanParam { - std::uint32_t reserved[16]; + float angle; + float distance; + float fbw_level; + float lfe_level; }; +static_assert(sizeof(Ngs2PanParam) == 16); struct Ngs2PanWork { - std::uint32_t reserved[64]; + float speaker_angles[8]; + float unit_angle; + std::uint32_t num_speakers; }; +static_assert(sizeof(Ngs2PanWork) == 40); struct Ngs2GeomListenerParam { std::uint32_t reserved[32]; diff --git a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp index ab1486207..12bc2f52c 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -182,5 +182,8 @@ void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param); void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate); void Ngs2ProcessLegacyUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate); void Ngs2RenderSystem(Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo, std::uint32_t numBufferInfo); +float Ngs2DefaultLevel(std::uint32_t sourceChannels, std::uint32_t source, std::uint32_t destChannels, std::uint32_t dest); +int Ngs2PanInit(Ngs2PanWork* work, const float* speaker_angles, float unit_angle, uint32_t num_speakers); +int Ngs2PanGetVolumeMatrix(Ngs2PanWork* work, const Ngs2PanParam* params, uint32_t num_params, uint32_t matrix_format, float* out_volume_matrix); #endif diff --git a/core/libs/prx/libSceNgs2.native/src/Pan.cpp b/core/libs/prx/libSceNgs2.native/src/Pan.cpp new file mode 100644 index 000000000..af88b2320 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/src/Pan.cpp @@ -0,0 +1,135 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "prx/libc/include/General.hpp" +#include "Ngs2Internal.hpp" + +static constexpr float DEGREES = 360.0f; +static constexpr float RADIANS = 2.0f * std::numbers::pi_v; +static constexpr std::uint32_t MAX_PAN_PARAMS = 8; +static constexpr std::uint32_t LFE_CHANNEL = 3; + +struct PanLayout { + std::uint32_t inputOrder[7]; + std::uint32_t channels[7]; + float defaultDegrees[7]; + std::uint32_t mixChannels; +}; + +static constexpr PanLayout STEREO_LAYOUT{{1, 0}, {1, 0}, {90, 270}, 2}; +static constexpr PanLayout QUAD_LAYOUT{{1, 3, 2, 0}, {1, 5, 4, 0}, {40, 110, 250, 320}, 6}; +static constexpr PanLayout FIVE_LAYOUT{{2, 1, 4, 3, 0}, {2, 1, 5, 4, 0}, {0, 40, 110, 250, 320}, 6}; +static constexpr PanLayout SEVEN_LAYOUT{{2, 1, 4, 6, 5, 3, 0}, {2, 1, 5, 7, 6, 4, 0}, {0, 40, 110, 150, 210, 250, 320}, 8}; + +static const PanLayout* LayoutFor(std::uint32_t numSpeakers) { + switch (numSpeakers) { + case 2: return &STEREO_LAYOUT; + case 4: return &QUAD_LAYOUT; + case 5: return &FIVE_LAYOUT; + case 7: return &SEVEN_LAYOUT; + default: return nullptr; + } +} + +static bool IsUnitAngle(float unitAngle) { + return unitAngle == DEGREES || unitAngle == RADIANS; +} + +static float EqualPower(float position) { + return std::sin(position * (std::numbers::pi_v / 2.0f)); +} + +static void AddBetweenSpeakers(const Ngs2PanWork& work, std::uint32_t speaker, float angle, float weight, float* gains) { + const float from = work.speaker_angles[speaker - 1]; + const float to = work.speaker_angles[speaker]; + if (!(angle >= from) || !(to > angle)) return; + const float position = (angle - from) / (to - from); + gains[speaker - 1] += EqualPower(1.0f - position) * weight; + gains[speaker] += EqualPower(position) * weight; +} + +int Ngs2PanInit(Ngs2PanWork* work, const float* speaker_angles, float unit_angle, uint32_t num_speakers) { + if (work == nullptr) APS5_INVALID_ARG_EX; + if (speaker_angles != nullptr && !IsUnitAngle(unit_angle)) APS5_INVALID_ARG_EX; + const auto* layout = LayoutFor(num_speakers); + if (layout == nullptr) APS5_INVALID_ARG_EX; + *work = {}; + work->num_speakers = num_speakers; + if (speaker_angles == nullptr) { + for (std::uint32_t i = 0; i < num_speakers; i++) work->speaker_angles[i] = layout->defaultDegrees[i]; + work->speaker_angles[num_speakers] = layout->defaultDegrees[0] + DEGREES; + work->unit_angle = DEGREES; + if (unit_angle == RADIANS) { + for (std::uint32_t i = 0; i < num_speakers; i++) work->speaker_angles[i] = work->speaker_angles[i] / 180.0f * std::numbers::pi_v; + work->unit_angle = RADIANS; + } + return SCE_NGS2_OK; + } + for (std::uint32_t i = 0; i < num_speakers; i++) { + float angle = speaker_angles[layout->inputOrder[i]]; + if (!std::isfinite(angle)) APS5_INVALID_ARG_EX; + while (angle < 0.0f) { + if (angle + unit_angle == angle) throw std::runtime_error("NGS2: pan angles too large to wrap are not implemented"); + angle += unit_angle; + } + while (angle >= unit_angle) { + if (!(unit_angle < angle)) APS5_INVALID_ARG_EX; + if (angle - unit_angle == angle) throw std::runtime_error("NGS2: pan angles too large to wrap are not implemented"); + angle -= unit_angle; + } + work->speaker_angles[i] = angle; + } + work->speaker_angles[num_speakers] = work->speaker_angles[0] + unit_angle; + work->unit_angle = unit_angle; + return SCE_NGS2_OK; +} + +int Ngs2PanGetVolumeMatrix(Ngs2PanWork* work, const Ngs2PanParam* params, uint32_t num_params, uint32_t matrix_format, float* out_volume_matrix) { + if (work == nullptr || params == nullptr) APS5_INVALID_ARG_EX; + if (out_volume_matrix == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + if (matrix_format != 1 && matrix_format != 2 && matrix_format != 6 && matrix_format != 8) APS5_INVALID_ARG_EX; + const auto* layout = LayoutFor(work->num_speakers); + if (layout == nullptr || !IsUnitAngle(work->unit_angle) || !std::isfinite(work->speaker_angles[0]) || num_params > MAX_PAN_PARAMS) APS5_INVALID_ARG_EX; + for (std::uint32_t p = 0; p < num_params; p++) { + if (!std::isfinite(params[p].angle) || std::isnan(params[p].distance)) APS5_INVALID_ARG_EX; + } + std::memset(out_volume_matrix, 0, static_cast(num_params) * matrix_format * sizeof(float)); + const std::uint32_t numSpeakers = work->num_speakers; + const float unit = work->unit_angle; + const float first = work->speaker_angles[0]; + for (std::uint32_t p = 0; p < num_params; p++) { + const auto& param = params[p]; + const float distance = std::max(-1.0f, std::min(1.0f, param.distance)); + const float towardWeight = std::sin((distance + 1.0f) * 0.5f * std::numbers::pi_v * 0.5f); + const float oppositeWeight = std::sin((1.0f - distance) * 0.5f * std::numbers::pi_v * 0.5f); + float angle = param.angle; + while (angle < first || angle >= first + unit) { + const float before = angle; + if (angle >= first + unit) angle -= unit; + if (angle < first) angle += unit; + if (angle == before) throw std::runtime_error("NGS2: pan angles too large to wrap are not implemented"); + } + float opposite = angle + unit * 0.5f; + if (!(opposite < first + unit)) opposite -= unit; + float gains[8] = {}; + for (std::uint32_t speaker = 1; speaker <= numSpeakers; speaker++) { + AddBetweenSpeakers(*work, speaker, angle, towardWeight, gains); + AddBetweenSpeakers(*work, speaker, opposite, oppositeWeight, gains); + } + float mix[8] = {}; + mix[layout->channels[0]] = gains[0] + gains[numSpeakers]; + for (std::uint32_t speaker = 1; speaker < numSpeakers; speaker++) mix[layout->channels[speaker]] = gains[speaker]; + for (std::uint32_t channel = 0; channel < 8; channel++) mix[channel] = channel == LFE_CHANNEL ? param.lfe_level : mix[channel] * param.fbw_level; + for (std::uint32_t dest = 0; dest < matrix_format; dest++) { + float& level = out_volume_matrix[static_cast(dest) * num_params + p]; + for (std::uint32_t channel = 0; channel < layout->mixChannels; channel++) level += Ngs2DefaultLevel(layout->mixChannels, channel, matrix_format, dest) * mix[channel]; + } + } + return SCE_NGS2_OK; +} diff --git a/core/libs/prx/libSceNgs2.native/src/Render.cpp b/core/libs/prx/libSceNgs2.native/src/Render.cpp index 8cb166fc7..63527dc5f 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -196,7 +196,7 @@ static constexpr Ngs2DefaultRow DEFAULT_MAP[NGS2_MAX_CHANNELS][NGS2_MAX_CHANNELS {FRONT_LEFT, FRONT_RIGHT, MONO_CENTER, LFE, SURROUND_LEFT, SURROUND_RIGHT, BACK_LEFT, BACK_RIGHT}, }; -static float DefaultLevel(std::uint32_t sourceChannels, std::uint32_t source, std::uint32_t destChannels, std::uint32_t dest) { +float Ngs2DefaultLevel(std::uint32_t sourceChannels, std::uint32_t source, std::uint32_t destChannels, std::uint32_t dest) { const auto& row = DEFAULT_MAP[sourceChannels - 1][source]; switch (destChannels) { case 1: return row.mono; @@ -212,7 +212,7 @@ static void MixPort(Ngs2Voice& voice, const Ngs2Voice& source, const Ngs2Port& p const std::size_t outputs = matrix == nullptr ? voice.channels : std::min(voice.channels, matrix->size() / source.channels); for (std::size_t dst = 0; dst < outputs; dst++) { for (std::uint32_t src = 0; src < source.channels; src++) { - const float level = port.volume * (matrix == nullptr ? DefaultLevel(source.channels, src, voice.channels, static_cast(dst)) : (*matrix)[dst * source.channels + src]); + const float level = port.volume * (matrix == nullptr ? Ngs2DefaultLevel(source.channels, src, voice.channels, static_cast(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; } diff --git a/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp b/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp index 15fb19513..62b848071 100644 --- a/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp @@ -37,25 +37,6 @@ int APS5_VABI sceNgs2GeomResetSourceParam(Ngs2GeomSourceParam* out_source_param) return 0; } -int APS5_VABI sceNgs2PanGetVolumeMatrix(Ngs2PanWork* work, const Ngs2PanParam* params, uint32_t num_params, uint32_t matrix_format, float* out_volume_matrix) { - (void)work; - (void)params; - (void)num_params; - (void)matrix_format; - (void)out_volume_matrix; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNgs2PanInit(Ngs2PanWork* work, const float* speaker_angles, float unit_angle, uint32_t num_speakers) { - (void)work; - (void)speaker_angles; - (void)unit_angle; - (void)num_speakers; - NotImplemented_nid_no_patch(__func__); - return 0; -} - } #pragma GCC visibility pop diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index fb21bc22d..424fa975f 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -431,6 +431,14 @@ int APS5_VABI sceNgs2VoiceGetStateFlags(uintptr_t voice_handle, uint32_t* state_ return SCE_NGS2_OK; } +int APS5_VABI sceNgs2PanInit(Ngs2PanWork* work, const float* speaker_angles, float unit_angle, uint32_t num_speakers) { + return Ngs2PanInit(work, speaker_angles, unit_angle, num_speakers); +} + +int APS5_VABI sceNgs2PanGetVolumeMatrix(Ngs2PanWork* work, const Ngs2PanParam* params, uint32_t num_params, uint32_t matrix_format, float* out_volume_matrix) { + return Ngs2PanGetVolumeMatrix(work, params, num_params, matrix_format, out_volume_matrix); +} + } #pragma GCC visibility pop diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index 5b328841e..5797ac60d 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -1,10 +1,12 @@ #include "Ngs2Test.hpp" +#include #include #include #include #include #include +#include #include #include #include @@ -145,6 +147,110 @@ static void TestPcmBlockEnd() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static bool Near(float a, float b) { + return std::fabs(a - b) < 1e-4f; +} + +static bool PanThrows(Ngs2PanWork* work, const Ngs2PanParam* params, std::uint32_t numParams, std::uint32_t format) { + float out[64] = {}; + try { sceNgs2PanGetVolumeMatrix(work, params, numParams, format, out); } catch (const std::exception&) { return true; } + return false; +} + +static bool PanInitThrows(Ngs2PanWork* work, const float* angles, float unitAngle, std::uint32_t numSpeakers) { + try { sceNgs2PanInit(work, angles, unitAngle, numSpeakers); } catch (const std::exception&) { return true; } + return false; +} + +struct PanCase { + std::vector angles; + float unitAngle; + std::uint32_t numSpeakers; + std::vector params; + std::uint32_t format; + std::vector expected; +}; + +static void TestPan() { + const float pi = std::numbers::pi_v; + Ngs2PanWork work{}; + Require(sceNgs2PanInit(&work, nullptr, 360.0f, 2) == SCE_NGS2_OK); + Require(work.num_speakers == 2 && work.unit_angle == 360.0f); + Require(work.speaker_angles[0] == 90.0f && work.speaker_angles[1] == 270.0f && work.speaker_angles[2] == 450.0f); + Require(sceNgs2PanInit(&work, nullptr, 2.0f * pi, 2) == SCE_NGS2_OK && work.unit_angle == 2.0f * pi); + Require(work.speaker_angles[0] == 90.0f / 180.0f * pi && work.speaker_angles[1] == 270.0f / 180.0f * pi && work.speaker_angles[2] == 450.0f); + Require(sceNgs2PanInit(&work, nullptr, 100.0f, 4) == SCE_NGS2_OK && work.unit_angle == 360.0f && work.speaker_angles[4] == 400.0f); + const float frontBack[] = {-30.0f, 30.0f, 0.0f, -110.0f, 470.0f}; + Require(sceNgs2PanInit(&work, frontBack, 360.0f, 5) == SCE_NGS2_OK); + const float circle[] = {0.0f, 30.0f, 110.0f, 250.0f, 330.0f, 360.0f}; + for (std::uint32_t i = 0; i < 6; i++) Require(work.speaker_angles[i] == circle[i]); + + const std::vector cases = { + {{}, 360.0f, 2, {{0, 1, 1, 0}, {45, 1, 1, 0}, {90, 1, 1, 0}, {135, 1, 1, 0}, {180, 1, 1, 0}, {225, 1, 1, 0}, {270, 1, 1, 0}, {315, 1, 1, 0}}, 2, + {0.707106769f, 0.382683456f, 0.0f, 0.382683456f, 0.707106769f, 0.923879504f, 1.0f, 0.923879504f, 0.707106769f, 0.923879504f, 1.0f, 0.923879504f, 0.707106769f, 0.382683456f, 0.0f, 0.382683456f}}, + {{}, 360.0f, 2, {{30, 0, 1, 0}, {30, -1, 1, 0}, {30, 0.5f, 0.5f, 0}, {30, 3, 1, 0}, {-30, -0.25f, 1, 0}}, 2, + {0.965925813f, 0.866025448f, 0.39667666f, 0.49999997f, 0.896872759f, 0.965925813f, 0.49999997f, 0.495722443f, 0.866025448f, 0.997858942f}}, + {{}, 360.0f, 2, {{60, 1, 1, 0.5f}}, 1, {0.866025388f}}, + {{}, 360.0f, 2, {{60, 1, 1, 0.5f}}, 6, {0.258819073f, 0.965925813f, 0.0f, 0.0f, 0.0f, 0.0f}}, + {{}, 2.0f * pi, 2, {{0.25f * pi, 1, 1, 0}, {1.5f * pi, 1, 1, 0}}, 2, {0.999965429f, 1.0f, 0.00831161533f, 0.0f}}, + {{}, 360.0f, 4, {{100, 1, 1, 0.25f}}, 6, {0.0f, 0.222520918f, 0.0f, 0.25f, 0.0f, 0.974927902f}}, + {{}, 360.0f, 4, {{100, 1, 1, 0.25f}}, 2, {0.0f, 0.709984899f}}, + {{}, 360.0f, 5, {{70, 1, 0.8f, 0.5f}, {200, 0.5f, 1, 0}}, 6, + {0.0f, 0.0f, 0.625465155f, 0.270598054f, 0.0f, 0.270598054f, 0.5f, 0.0f, 0.0f, 0.782271147f, 0.498791903f, 0.491533577f}}, + {{}, 360.0f, 5, {{70, 1, 0.8f, 0.5f}}, 1, {0.691666603f}}, + {{}, 360.0f, 7, {{180, 1, 1, 0.25f}}, 8, {0.0f, 0.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.707106769f, 0.707106769f}}, + {{}, 360.0f, 7, {{180, 1, 1, 0.25f}}, 6, {0.0f, 0.0f, 0.0f, 0.25f, 0.707106769f, 0.707106769f}}, + {{-pi / 6, pi / 6, 0, -110 * pi / 180, 110 * pi / 180}, 2.0f * pi, 5, {{1.0f, 1, 1, 0}, {-2.5f, 0.2f, 1, 0.1f}}, 6, + {0.0f, 0.0f, 0.859783232f, 0.582614243f, 0.0f, 0.0f, 0.0f, 0.100000001f, 0.0f, 0.753403723f, 0.510659218f, 0.372568965f}}, + {{-30, 30, 0, -90, 90, -150, 150}, 360.0f, 7, {{-120, 1, 1, 0}}, 1, {0.707106769f}}, + {{-45, 45}, 360.0f, 2, {{10, 1, 1, 0}, {720 + 170, 1, 1, 0}}, 2, {0.57357651f, 0.664795876f, 0.819152057f, 0.747025073f}}, + {{-40, 40, -120, 120}, 360.0f, 4, {{-80, 0.75f, 1, 0}}, 8, {0.69351995f, 0.0746578425f, 0.0f, 0.0f, 0.69351995f, 0.180239946f, 0.0f, 0.0f}}, + }; + for (const auto& panCase : cases) { + Require(sceNgs2PanInit(&work, panCase.angles.empty() ? nullptr : panCase.angles.data(), panCase.unitAngle, panCase.numSpeakers) == SCE_NGS2_OK); + std::vector out(panCase.expected.size(), -1.0f); + Require(sceNgs2PanGetVolumeMatrix(&work, panCase.params.data(), static_cast(panCase.params.size()), panCase.format, out.data()) == SCE_NGS2_OK); + for (std::size_t i = 0; i < out.size(); i++) Require(std::fabs(out[i] - panCase.expected[i]) < 1e-6f); + } + + const Ngs2PanParam front{0.0f, 1.0f, 1.0f, 0.0f}; + float out[2] = {}; + Require(sceNgs2PanInit(&work, nullptr, 360.0f, 2) == SCE_NGS2_OK); + Require(sceNgs2PanGetVolumeMatrix(&work, &front, 1, 2, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2PanGetVolumeMatrix(&work, &front, 0, 2, out) == SCE_NGS2_OK); + Require(PanThrows(nullptr, &front, 1, 2) && PanThrows(&work, nullptr, 0, 2)); + Require(PanThrows(&work, &front, 1, 4) && PanThrows(&work, &front, 1, 3) && !PanThrows(&work, &front, 1, 8)); + const Ngs2PanParam many[9] = {}; + Require(PanThrows(&work, many, 9, 2) && !PanThrows(&work, many, 8, 2)); + const Ngs2PanParam nanAngle{NAN, 1.0f, 1.0f, 0.0f}; + Require(PanThrows(&work, &nanAngle, 1, 2)); + const Ngs2PanParam laterNan[2] = {front, nanAngle}; + float untouched[4] = {-1.0f, -1.0f, -1.0f, -1.0f}; + bool rejected = false; + try { sceNgs2PanGetVolumeMatrix(&work, laterNan, 2, 2, untouched); } catch (const std::invalid_argument&) { rejected = true; } + Require(rejected && std::all_of(std::begin(untouched), std::end(untouched), [](float level) { return level == -1.0f; })); + const Ngs2PanParam huge{1e30f, 1.0f, 1.0f, 0.0f}; + Require(PanThrows(&work, &huge, 1, 2)); + const float hugeSpeaker[] = {1e30f, 30.0f}; + const float hugeNegative[] = {-1e30f, 30.0f}; + Require(PanInitThrows(&work, hugeSpeaker, 360.0f, 2) && PanInitThrows(&work, hugeNegative, 360.0f, 2)); + Require(sceNgs2PanInit(&work, nullptr, 360.0f, 2) == SCE_NGS2_OK); + work.speaker_angles[0] = -INFINITY; + Require(PanThrows(&work, &front, 1, 2)); + Require(sceNgs2PanInit(&work, nullptr, 360.0f, 2) == SCE_NGS2_OK); + Require(PanInitThrows(nullptr, nullptr, 360.0f, 2)); + for (std::uint32_t count : {0u, 1u, 3u, 6u, 8u}) Require(PanInitThrows(&work, nullptr, 360.0f, count)); + const float pair[] = {-30.0f, 30.0f}; + Require(PanInitThrows(&work, pair, 180.0f, 2) && !PanInitThrows(&work, pair, 2.0f * pi, 2)); + const float invalid[] = {0.0f, NAN}; + Require(PanInitThrows(&work, invalid, 360.0f, 2)); + const float onUnit[] = {720.0f, 30.0f}; + const float belowZero[] = {-360.0f, 30.0f}; + Require(PanInitThrows(&work, onUnit, 360.0f, 2) && !PanInitThrows(&work, belowZero, 360.0f, 2)); + work.unit_angle = 100.0f; + Require(PanThrows(&work, &front, 1, 2)); +} + static void TestPitchAndRepeat() { const auto system = CreateSystem(); const auto master = Mastering(system, 1); @@ -559,6 +665,7 @@ int main() { Require(std::atexit(RenderAfterStaticTeardown) == 0); TestErrorsAndInfo(); TestPcmBlockEnd(); + TestPan(); TestPitchAndRepeat(); TestSubmixerMatrix(); TestSampleRate(); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp index 97843654b..51a50d512 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp @@ -25,6 +25,8 @@ int APS5_VABI sceNgs2SystemGetUserData(uintptr_t, uintptr_t*); int APS5_VABI sceNgs2SystemLock(uintptr_t); int APS5_VABI sceNgs2SystemUnlock(uintptr_t); int APS5_VABI sceNgs2SystemRender(uintptr_t, const Ngs2RenderBufferInfo*, uint32_t); +int APS5_VABI sceNgs2PanInit(Ngs2PanWork*, const float*, float, uint32_t); +int APS5_VABI sceNgs2PanGetVolumeMatrix(Ngs2PanWork*, const Ngs2PanParam*, uint32_t, uint32_t, float*); int APS5_VABI sceNgs2RackQueryBufferSize(uint32_t, const Ngs2RackOption*, Ngs2ContextBufferInfo*); int APS5_VABI sceNgs2RackCreate(uintptr_t, uint32_t, const Ngs2RackOption*, const Ngs2ContextBufferInfo*, uintptr_t*); int APS5_VABI sceNgs2RackCreateWithAllocator(uintptr_t, uint32_t, const Ngs2RackOption*, const Ngs2BufferAllocator*, uintptr_t*); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index e4cfcaf3b..7bdae2e12 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -229,6 +229,7 @@ Throughout the project, every function at every stage either **does exactly what - [Resource registration](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified. `sceAgcDriverGetResourceRegistrationMaxNameLength` writes 0xfc and returns 0, as [prosper](https://github.com/mattias800/prosper) does from a title's registration configuration; the `uint32_t*` parameter matches the SDK wrapper `sce::Agc::ResourceRegistration::getMaxNameLength(unsigned int*)`. sharpemu writes 256 and KytyPS5 returns `RESOURCE_REGISTRATION_UNAVAILABLE`, so the value is unverified; a null output throws - [Shader registration](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a shader header is accepted at any address: PPSA26344 passes headers 4 and 1 bytes past an 8-byte boundary; whether the console refuses an alignment is unknown - [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) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. Error codes are not known for the PS5, so invalid arguments throw, including a speaker angle equal to the unit angle, on which that library loops forever. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason - [sceRtcGetDosTime, sceRtcSetDosTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). A year outside 1980-2107 makes `GetDosTime` return `INVALID_YEAR` after writing 0 for an earlier one and `0xff9fbf7d` (2107-12-31 23:59:58, the largest encodable time) for a later one, and `SetDosTime` decodes the year as the unsigned 7-bit FAT field (1980-2107) without validating the fields, as the disassembly-derived [fpPS4](https://github.com/red-prig/fpPS4) reimplementation of the PS4 library does; not confirmed on a PS5 title, and shadPS4 instead masks out-of-range years into the packed value and sign-extends the 16-bit date - [sce::Json::InitParameter2](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layout inferred, not known: assumed to start with the allocator, its user data and the file buffer size, in that order, and the whole struct is assumed to fit in 40 bytes; `Initializer::initialize(InitParameter2)` ignores them as `initialize(InitParameter)` does - [sceAjmBatchJobSetResampleParameters, sceAjmBatchJobGetResampleInfo](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm) - the ratio is taken as decoded samples per output sample and num_samples as the decoded samples the resampler holds, both inferred from FMOD (Hades II); the console's interpolation is unknown, a 4-point Catmull-Rom is used. The flags argument is ignored, as in shadPS4 and Kyty