fix(libSceNgs2.native): mix ports without set levels through the default channel map
A port without a matrix sent source channel n to destination channel n, so a mono voice reached only the left channel of a stereo mix. The PS4 firmware 11.00 libSceNgs2.sprx mixes such ports, and ports whose matrix levels were never set, through a fixed channel map instead: mono reaches both stereo channels at -3 dB and the centre of 5.1 and 7.1, stereo folds to mono at -3 dB per side, and wider sources fold down with 0.5 and -3 dB gains. The map was measured for every source width into 1, 2, 6 and 8 channels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
56d8e5c4df
commit
ed9a70175e
|
|
@ -3,6 +3,7 @@
|
|||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <numbers>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
|
@ -160,12 +161,58 @@ static void RenderSampler(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t s
|
|||
|
||||
static void RenderVoice(Ngs2Voice& voice, const std::vector<Ngs2Voice*>& voices, std::uint32_t grain, std::uint32_t systemRate);
|
||||
|
||||
struct Ngs2DefaultRow {
|
||||
float mono;
|
||||
float stereo[2];
|
||||
float surround51[6];
|
||||
float surround71[8];
|
||||
};
|
||||
|
||||
static constexpr float HALF_POWER = std::numbers::sqrt2_v<float> / 2.0f;
|
||||
static constexpr float QUARTER_POWER = std::numbers::sqrt2_v<float> / 4.0f;
|
||||
|
||||
static constexpr Ngs2DefaultRow MONO_CENTER{1.0f, {HALF_POWER, HALF_POWER}, {0, 0, 1, 0, 0, 0}, {0, 0, 1, 0, 0, 0, 0, 0}};
|
||||
static constexpr Ngs2DefaultRow FRONT_LEFT{HALF_POWER, {1, 0}, {1, 0, 0, 0, 0, 0}, {1, 0, 0, 0, 0, 0, 0, 0}};
|
||||
static constexpr Ngs2DefaultRow FRONT_RIGHT{HALF_POWER, {0, 1}, {0, 1, 0, 0, 0, 0}, {0, 1, 0, 0, 0, 0, 0, 0}};
|
||||
static constexpr Ngs2DefaultRow LFE{0.0f, {0, 0}, {0, 0, 0, 1, 0, 0}, {0, 0, 0, 1, 0, 0, 0, 0}};
|
||||
static constexpr Ngs2DefaultRow QUAD_LEFT{0.5f, {HALF_POWER, 0}, {0, 0, 0, 0, 1, 0}, {0, 0, 0, 0, 1, 0, 0, 0}};
|
||||
static constexpr Ngs2DefaultRow QUAD_RIGHT{0.5f, {0, HALF_POWER}, {0, 0, 0, 0, 0, 1}, {0, 0, 0, 0, 0, 1, 0, 0}};
|
||||
static constexpr Ngs2DefaultRow FIVE_SURROUND_LEFT{0.5f, {0, 0}, {0, 0, 0, 0, 1, 0}, {0, 0, 0, 0, 1, 0, 0, 0}};
|
||||
static constexpr Ngs2DefaultRow FIVE_SURROUND_RIGHT{0.5f, {0, 0}, {0, 0, 0, 0, 0, 1}, {0, 0, 0, 0, 0, 1, 0, 0}};
|
||||
static constexpr Ngs2DefaultRow SURROUND_LEFT{0.5f, {0.5f, 0}, {0, 0, 0, 0, 1, 0}, {0, 0, 0, 0, 1, 0, 0, 0}};
|
||||
static constexpr Ngs2DefaultRow SURROUND_RIGHT{0.5f, {0, 0.5f}, {0, 0, 0, 0, 0, 1}, {0, 0, 0, 0, 0, 1, 0, 0}};
|
||||
static constexpr Ngs2DefaultRow BACK_CENTER{QUARTER_POWER, {QUARTER_POWER, QUARTER_POWER}, {0, 0, 0, 0, HALF_POWER, HALF_POWER}, {0, 0, 0, 0, 0, 0, HALF_POWER, HALF_POWER}};
|
||||
static constexpr Ngs2DefaultRow BACK_LEFT{0.5f, {0.5f, 0}, {0, 0, 0, 0, 1, 0}, {0, 0, 0, 0, 0, 0, 1, 0}};
|
||||
static constexpr Ngs2DefaultRow BACK_RIGHT{0.5f, {0, 0.5f}, {0, 0, 0, 0, 0, 1}, {0, 0, 0, 0, 0, 0, 0, 1}};
|
||||
|
||||
static constexpr Ngs2DefaultRow DEFAULT_MAP[NGS2_MAX_CHANNELS][NGS2_MAX_CHANNELS] = {
|
||||
{MONO_CENTER},
|
||||
{FRONT_LEFT, FRONT_RIGHT},
|
||||
{FRONT_LEFT, FRONT_RIGHT, LFE},
|
||||
{FRONT_LEFT, FRONT_RIGHT, QUAD_LEFT, QUAD_RIGHT},
|
||||
{FRONT_LEFT, FRONT_RIGHT, MONO_CENTER, FIVE_SURROUND_LEFT, FIVE_SURROUND_RIGHT},
|
||||
{FRONT_LEFT, FRONT_RIGHT, MONO_CENTER, LFE, SURROUND_LEFT, SURROUND_RIGHT},
|
||||
{FRONT_LEFT, FRONT_RIGHT, MONO_CENTER, LFE, SURROUND_LEFT, SURROUND_RIGHT, BACK_CENTER},
|
||||
{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) {
|
||||
const auto& row = DEFAULT_MAP[sourceChannels - 1][source];
|
||||
switch (destChannels) {
|
||||
case 1: return row.mono;
|
||||
case 2: return row.stereo[dest];
|
||||
case 6: return row.surround51[dest];
|
||||
case 8: return row.surround71[dest];
|
||||
default: throw std::runtime_error("NGS2: the default channel map into " + std::to_string(destChannels) + " channels is not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
static void MixPort(Ngs2Voice& voice, const Ngs2Voice& source, const Ngs2Port& port, std::uint32_t grain) {
|
||||
const auto* matrix = port.matrix < 0 || source.matrices[port.matrix].empty() ? nullptr : &source.matrices[port.matrix];
|
||||
const std::size_t outputs = matrix == nullptr ? voice.channels : std::min<std::size_t>(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 ? (src == dst ? 1.0f : 0.0f) : (*matrix)[dst * source.channels + src]);
|
||||
const float level = port.volume * (matrix == nullptr ? DefaultLevel(source.channels, src, voice.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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <numbers>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
|
@ -423,6 +424,74 @@ static void TestLock() {
|
|||
Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK);
|
||||
}
|
||||
|
||||
static constexpr float HalfPower = std::numbers::sqrt2_v<float> / 2.0f;
|
||||
|
||||
static std::vector<float> MixFrame(std::uint32_t sourceChannels, std::uint32_t sourceChannel, std::uint32_t destChannels) {
|
||||
const auto firstBuffer = usedBuffers;
|
||||
const auto system = CreateSystem();
|
||||
const auto master = Mastering(system, destChannels);
|
||||
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, sourceChannels, 48000, 0, 0, 0}});
|
||||
std::vector<std::int16_t> frames(Grain * sourceChannels, 0);
|
||||
for (std::uint32_t i = 0; i < Grain; i++) frames[i * sourceChannels + sourceChannel] = 16384;
|
||||
const Ngs2WaveformBlock block{0, frames.size() * sizeof(std::int16_t), 0, 0, Grain, 0, 0};
|
||||
Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, frames.data(), 0, 1, &block});
|
||||
Patch(voice, master);
|
||||
Event(voice, SCE_NGS2_VOICE_EVENT_PLAY);
|
||||
std::vector<float> out(Grain * destChannels, -1.0f);
|
||||
const Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, destChannels};
|
||||
Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK);
|
||||
Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK);
|
||||
usedBuffers = firstBuffer;
|
||||
return std::vector<float>(out.begin(), out.begin() + destChannels);
|
||||
}
|
||||
|
||||
static void TestDefaultChannelMap() {
|
||||
const float half = 0.5f * HalfPower;
|
||||
const float quarter = 0.5f * (std::numbers::sqrt2_v<float> / 4.0f);
|
||||
Require(MixFrame(1, 0, 1) == std::vector<float>{0.5f});
|
||||
Require(MixFrame(1, 0, 2) == (std::vector<float>{half, half}));
|
||||
Require(MixFrame(1, 0, 6) == (std::vector<float>{0, 0, 0.5f, 0, 0, 0}));
|
||||
Require(MixFrame(1, 0, 8) == (std::vector<float>{0, 0, 0.5f, 0, 0, 0, 0, 0}));
|
||||
Require(MixFrame(2, 0, 1) == std::vector<float>{half} && MixFrame(2, 1, 1) == std::vector<float>{half});
|
||||
Require(MixFrame(2, 1, 2) == (std::vector<float>{0, 0.5f}));
|
||||
Require(MixFrame(2, 1, 6) == (std::vector<float>{0, 0.5f, 0, 0, 0, 0}));
|
||||
Require(MixFrame(3, 2, 2) == (std::vector<float>{0, 0}));
|
||||
Require(MixFrame(3, 2, 6) == (std::vector<float>{0, 0, 0, 0.5f, 0, 0}));
|
||||
Require(MixFrame(4, 2, 1) == std::vector<float>{0.25f});
|
||||
Require(MixFrame(4, 2, 2) == (std::vector<float>{half, 0}));
|
||||
Require(MixFrame(4, 3, 6) == (std::vector<float>{0, 0, 0, 0, 0, 0.5f}));
|
||||
Require(MixFrame(5, 2, 2) == (std::vector<float>{half, half}));
|
||||
Require(MixFrame(5, 3, 1) == std::vector<float>{0.25f});
|
||||
Require(MixFrame(5, 3, 2) == (std::vector<float>{0, 0}));
|
||||
Require(MixFrame(6, 3, 1) == std::vector<float>{0});
|
||||
Require(MixFrame(6, 4, 2) == (std::vector<float>{0.25f, 0}));
|
||||
Require(MixFrame(6, 5, 8) == (std::vector<float>{0, 0, 0, 0, 0, 0.5f, 0, 0}));
|
||||
Require(MixFrame(7, 6, 1) == std::vector<float>{quarter});
|
||||
Require(MixFrame(7, 6, 2) == (std::vector<float>{quarter, quarter}));
|
||||
Require(MixFrame(7, 6, 6) == (std::vector<float>{0, 0, 0, 0, half, half}));
|
||||
Require(MixFrame(7, 6, 8) == (std::vector<float>{0, 0, 0, 0, 0, 0, half, half}));
|
||||
Require(MixFrame(8, 6, 2) == (std::vector<float>{0.25f, 0}));
|
||||
Require(MixFrame(8, 7, 6) == (std::vector<float>{0, 0, 0, 0, 0, 0.5f}));
|
||||
Require(MixFrame(8, 7, 8) == (std::vector<float>{0, 0, 0, 0, 0, 0, 0, 0.5f}));
|
||||
|
||||
const auto system = CreateSystem();
|
||||
const auto master = Mastering(system, 2);
|
||||
const auto submixer = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SUBMIXER));
|
||||
Control(submixer, SCE_NGS2_SUBMIXER_VOICE_PARAM_SETUP, Ngs2SubmixerVoiceSetupParam{{}, 4, 0});
|
||||
Patch(submixer, master);
|
||||
Event(submixer, SCE_NGS2_VOICE_EVENT_PLAY);
|
||||
const std::vector<std::int16_t> pcm(Grain, 16384);
|
||||
const auto sampler = Sampler(system, pcm, 0);
|
||||
Patch(sampler, submixer);
|
||||
Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY);
|
||||
std::vector<float> out(Grain * 2, -1.0f);
|
||||
const Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2};
|
||||
bool unsupported = false;
|
||||
try { sceNgs2SystemRender(system, &info, 1); } catch (const std::runtime_error&) { unsupported = true; }
|
||||
Require(unsupported);
|
||||
}
|
||||
|
||||
static void TestUnsetMatrix() {
|
||||
const auto system = CreateSystem();
|
||||
const auto master = Mastering(system, 2);
|
||||
|
|
@ -441,7 +510,8 @@ static void TestUnsetMatrix() {
|
|||
std::vector<float> out(Grain * 2, -1.0f);
|
||||
const Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2};
|
||||
Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK);
|
||||
for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.25f && out[i * 2 + 1] == 0.0f);
|
||||
const float spread = 0.25f * HalfPower;
|
||||
for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == spread && out[i * 2 + 1] == spread);
|
||||
Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK);
|
||||
|
||||
const auto stereoSystem = CreateSystem();
|
||||
|
|
@ -495,6 +565,7 @@ int main() {
|
|||
TestMasteringGain();
|
||||
TestStereoIntoSurround();
|
||||
TestLock();
|
||||
TestDefaultChannelMap();
|
||||
TestUnsetMatrix();
|
||||
TestAllocator();
|
||||
exitSystem = CreateSystem();
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ Throughout the project, every function at every stage either **does exactly what
|
|||
- [sceRazorCpuPlotValue, sceRazorCpuWriteBookmark](../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures (series, float value) and (label, optional description) from [SharpProspero](https://github.com/SvenGDK/SharpProspero), matching PPSA06320's calls; both do nothing without a capture and return 0
|
||||
- [sceAjmBatchJobGetInfo](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - writes the result and format sideband of a run job without buffers with the `SIDEBAND_FORMAT` flag (0x400000000000), as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does, not confirmed on a PS5 title; the format reports bitrate 0 for ATRAC9 as in KytyPS5, while [shadPS4](https://github.com/shadps4-emu/shadPS4) derives it from the superframe size; before the instance is initialized it returns the not-initialized result (1) and no format, like the other run jobs here, while KytyPS5 returns result 0 with the format of the uninitialized decoder
|
||||
- [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: the layer field is not checked and MPEG-2.5 stops at 64 kbps; the original file length (`parse_ofl`) layout (Xing/Info + LAME tag, VBRI, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description
|
||||
- [NGS2 port matrix](../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a port that selects a matrix whose levels were never set mixes as a port without a matrix (source channel n to destination channel n, so a mono source reaches only channel 0). PPSA22520 selects matrix 0 for its title music without setting its levels, and the title is expected to have music on the console, so an unset matrix is assumed not to be silent; the console's initial levels are unknown. A matrix set with fewer levels than the destination needs still mixes only the rows it has
|
||||
- [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
|
||||
- [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title; a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts
|
||||
- [AJM Opus decoder](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout (`u32 channels, u32 sample rate, u32`, seen as `2, 48000, 0`) and the little-endian `u16` byte count before each packet come from one title (The Smurfs Dreams); only 48000 Hz and a zero third word are accepted
|
||||
- [sceAjmBatchJobControl](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET (bit 13), INITIALIZE (bit 14) and the sideband input order (gapless decode, then initialize parameters) come from [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4); other flags throw
|
||||
|
|
|
|||
Loading…
Reference in New Issue