diff --git a/core/libs/prx/libSceNgs2.native/src/Render.cpp b/core/libs/prx/libSceNgs2.native/src/Render.cpp index cae8b38ae..8cb166fc7 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -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& 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 / 2.0f; +static constexpr float QUARTER_POWER = std::numbers::sqrt2_v / 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(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(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/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index fc0a60250..27c57054e 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -423,6 +424,74 @@ static void TestLock() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static constexpr float HalfPower = std::numbers::sqrt2_v / 2.0f; + +static std::vector 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 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 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(out.begin(), out.begin() + destChannels); +} + +static void TestDefaultChannelMap() { + const float half = 0.5f * HalfPower; + const float quarter = 0.5f * (std::numbers::sqrt2_v / 4.0f); + Require(MixFrame(1, 0, 1) == std::vector{0.5f}); + Require(MixFrame(1, 0, 2) == (std::vector{half, half})); + Require(MixFrame(1, 0, 6) == (std::vector{0, 0, 0.5f, 0, 0, 0})); + Require(MixFrame(1, 0, 8) == (std::vector{0, 0, 0.5f, 0, 0, 0, 0, 0})); + Require(MixFrame(2, 0, 1) == std::vector{half} && MixFrame(2, 1, 1) == std::vector{half}); + Require(MixFrame(2, 1, 2) == (std::vector{0, 0.5f})); + Require(MixFrame(2, 1, 6) == (std::vector{0, 0.5f, 0, 0, 0, 0})); + Require(MixFrame(3, 2, 2) == (std::vector{0, 0})); + Require(MixFrame(3, 2, 6) == (std::vector{0, 0, 0, 0.5f, 0, 0})); + Require(MixFrame(4, 2, 1) == std::vector{0.25f}); + Require(MixFrame(4, 2, 2) == (std::vector{half, 0})); + Require(MixFrame(4, 3, 6) == (std::vector{0, 0, 0, 0, 0, 0.5f})); + Require(MixFrame(5, 2, 2) == (std::vector{half, half})); + Require(MixFrame(5, 3, 1) == std::vector{0.25f}); + Require(MixFrame(5, 3, 2) == (std::vector{0, 0})); + Require(MixFrame(6, 3, 1) == std::vector{0}); + Require(MixFrame(6, 4, 2) == (std::vector{0.25f, 0})); + Require(MixFrame(6, 5, 8) == (std::vector{0, 0, 0, 0, 0, 0.5f, 0, 0})); + Require(MixFrame(7, 6, 1) == std::vector{quarter}); + Require(MixFrame(7, 6, 2) == (std::vector{quarter, quarter})); + Require(MixFrame(7, 6, 6) == (std::vector{0, 0, 0, 0, half, half})); + Require(MixFrame(7, 6, 8) == (std::vector{0, 0, 0, 0, 0, 0, half, half})); + Require(MixFrame(8, 6, 2) == (std::vector{0.25f, 0})); + Require(MixFrame(8, 7, 6) == (std::vector{0, 0, 0, 0, 0, 0.5f})); + Require(MixFrame(8, 7, 8) == (std::vector{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 pcm(Grain, 16384); + const auto sampler = Sampler(system, pcm, 0); + Patch(sampler, submixer); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + std::vector 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 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(); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index c0b18b3c9..e4cfcaf3b 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -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