Files
AnyPS5/core/libs/prx/libSceNgs2.native/src/Pan.cpp
T
JAB-devandClaude Opus 5.5 00e13ecfa7 feat(libSceNgs2.native): implement sceNgs2PanInit and sceNgs2PanGetVolumeMatrix
Pan follows the PS4 firmware 11.00 libSceNgs2.sprx: the speaker layout
from the init angles, then each param's angle and distance into a
volume matrix for the voice. Every param is checked before anything is
written, and angles too large to wrap throw. The pan functions are
exported from the existing Voice.cpp export block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 23:26:24 +02:00

136 lines
6.4 KiB
C++

#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <numbers>
#include <stdexcept>
#include <string>
#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<float>;
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<float> / 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<float>;
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<std::size_t>(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<float> * 0.5f);
const float oppositeWeight = std::sin((1.0f - distance) * 0.5f * std::numbers::pi_v<float> * 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<std::size_t>(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;
}