From 5bae4df0b6eead1972f78124029beb9183046b0c Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Tue, 6 Oct 2026 22:20:51 -0400 Subject: [PATCH] feat(libSceNgs2.native): mix a stereo mastering voice into a 6 or 8 channel render buffer The mixing stage threw when a mastering voice and its render buffer had different channel counts. A 2-channel voice is now mixed into the first two channels of a 6- or 8-channel buffer; other mismatches still throw. --- .../libs/prx/libSceNgs2.native/src/Render.cpp | 9 ++- .../libSceNgs2.native/tests/Ngs2Render.cpp | 80 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Render.cpp b/core/libs/prx/libSceNgs2.native/src/Render.cpp index 8f307efb8..1e81d0d6b 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -188,12 +188,16 @@ static std::size_t SampleBytes(std::uint32_t waveformType) { throw std::runtime_error("NGS2: render waveform type " + Ngs2Hex(waveformType) + " is not implemented"); } +static bool IsStereoIntoSurround(const Ngs2Voice& voice, const Ngs2RenderBufferInfo& output) { + return voice.channels == 2 && (output.num_channels == 6 || output.num_channels == 8); +} + static std::vector& OutputMix(std::vector>& mixes, const Ngs2RenderBufferInfo* bufferInfo, std::uint32_t numBufferInfo, const Ngs2Voice& voice, std::uint32_t grain) { if (voice.outputId >= numBufferInfo) throw std::invalid_argument("NGS2: mastering output " + std::to_string(voice.outputId) + " has no render buffer"); const auto& output = bufferInfo[voice.outputId]; const auto sampleBytes = SampleBytes(output.waveform_type); - if (output.num_channels != voice.channels) throw std::runtime_error("NGS2: mixing " + std::to_string(voice.channels) + " mastering channels into " + std::to_string(output.num_channels) + " is not implemented"); + if (output.num_channels != voice.channels && !IsStereoIntoSurround(voice, output)) throw std::runtime_error("NGS2: mixing " + std::to_string(voice.channels) + " mastering channels into " + std::to_string(output.num_channels) + " is not implemented"); if (output.buffer == nullptr || output.buffer_size < static_cast(grain) * output.num_channels * sampleBytes) { throw std::invalid_argument("NGS2: render buffer " + std::to_string(voice.outputId) + " is missing or too small"); } @@ -227,10 +231,11 @@ void Ngs2RenderSystem(Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo RenderVoice(*voice, voices, grain, system.option.sample_rate); if (voice->rack->rackId != SCE_NGS2_RACK_ID_MASTERING || !voice->hasSamples) continue; auto& mix = OutputMix(mixes, bufferInfo, numBufferInfo, *voice, grain); + const std::uint32_t outputChannels = bufferInfo[voice->outputId].num_channels; for (std::uint32_t channel = 0; channel < voice->channels; channel++) { const bool lfe = channel == 3 && (voice->channels == 6 || voice->channels == 8); const float level = lfe ? voice->lfeLevel : voice->fbwLevel; - for (std::uint32_t i = 0; i < grain; i++) mix[i * voice->channels + channel] += voice->samples[channel * grain + i] * level; + for (std::uint32_t i = 0; i < grain; i++) mix[i * outputChannels + channel] += voice->samples[channel * grain + i] * level; } } for (std::uint32_t i = 0; i < numBufferInfo; i++) { diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index 3aa1d2cb3..c26310763 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -322,6 +322,85 @@ static void TestMasteringGain() { Require(sceNgs2SystemDestroy(loudSystem, nullptr) == SCE_NGS2_OK); } +static uintptr_t RackVoice(uintptr_t rack, std::uint32_t index) { + uintptr_t voice = 0; + Require(sceNgs2RackGetVoiceHandle(rack, index, &voice) == SCE_NGS2_OK && voice != 0); + return voice; +} + +static uintptr_t MasteringRack(uintptr_t system, std::uint32_t voices) { + Ngs2MasteringRackOption option{}; + option.rack_option.size = sizeof(option); + option.rack_option.max_grain_samples = 512; + option.rack_option.max_voices = voices; + option.rack_option.max_input_delay_blocks = 1; + option.rack_option.max_matrices = 1; + option.rack_option.max_ports = 8; + option.max_channels = 8; + Ngs2ContextBufferInfo query{}; + Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_MASTERING, &option.rack_option, &query) == SCE_NGS2_OK); + const auto buffer = Buffer(query); + uintptr_t rack = 0; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_MASTERING, &option.rack_option, &buffer, &rack) == SCE_NGS2_OK && rack != 0); + return rack; +} + +static void StereoSource(uintptr_t voice, const std::vector& pcm, uintptr_t master) { + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1, 48000, 0, 0, 0}}); + const Ngs2WaveformBlock block{0, pcm.size() * sizeof(std::int16_t), 0, 0, static_cast(pcm.size()), 0, 0}; + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block}); + const float levels[2] = {1.0f, 0.5f}; + Control(voice, SCE_NGS2_VOICE_PARAM_MATRIX_LEVELS, Ngs2VoiceMatrixLevelsParam{{}, 0, 2, levels}); + Control(voice, SCE_NGS2_VOICE_PARAM_PORT_MATRIX, Ngs2VoicePortMatrixParam{{}, 0, 0}); + Patch(voice, master); +} + +static void TestStereoIntoSurround() { + const auto system = CreateSystem(); + const auto masteringRack = MasteringRack(system, 3); + const auto samplerRack = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + uintptr_t masters[3]; + for (std::uint32_t i = 0; i < 3; i++) { + masters[i] = RackVoice(masteringRack, i); + Control(masters[i], SCE_NGS2_MASTERING_VOICE_PARAM_SETUP, Ngs2MasteringVoiceSetupParam{{}, 2, 0}); + Control(masters[i], SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT, Ngs2MasteringVoiceOutputParam{{}, i, 0}); + Event(masters[i], SCE_NGS2_VOICE_EVENT_PLAY); + } + const std::vector pcm(Grain, 16384); + for (std::uint32_t i = 0; i < 2; i++) { + const auto source = RackVoice(samplerRack, i); + StereoSource(source, pcm, masters[i]); + Event(source, SCE_NGS2_VOICE_EVENT_PLAY); + } + std::vector outFive(Grain * 6, -1.0f); + std::vector outSeven(Grain * 8, -1.0f); + std::vector outQuad(Grain * 4, -1.0f); + const Ngs2RenderBufferInfo info[3] = { + {outFive.data(), outFive.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 6}, + {outSeven.data(), outSeven.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 8}, + {outQuad.data(), outQuad.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 4}, + }; + Require(sceNgs2SystemRender(system, info, 3) == SCE_NGS2_OK); + for (std::uint32_t i = 0; i < Grain; i++) { + Require(outFive[i * 6] == 0.5f && outFive[i * 6 + 1] == 0.25f); + for (std::uint32_t channel = 2; channel < 6; channel++) Require(outFive[i * 6 + channel] == 0.0f); + Require(outSeven[i * 8] == 0.5f && outSeven[i * 8 + 1] == 0.25f); + for (std::uint32_t channel = 2; channel < 8; channel++) Require(outSeven[i * 8 + channel] == 0.0f); + } + + const auto rejectedSource = RackVoice(samplerRack, 2); + StereoSource(rejectedSource, pcm, masters[2]); + Event(rejectedSource, SCE_NGS2_VOICE_EVENT_PLAY); + bool threw = false; + try { + sceNgs2SystemRender(system, info, 3); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + static void TestLock() { const auto system = CreateSystem(); Require(sceNgs2SystemLock(0x1234) == SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE); @@ -366,6 +445,7 @@ int main() { TestSampleRate(); TestUserData(); TestMasteringGain(); + TestStereoIntoSurround(); TestLock(); TestAllocator(); return 0; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index dbd9ee2f3..b79bfb688 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -120,6 +120,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceVrSetupDialog](../../core/libs/prx/libSceVrSetupDialog/Export.cpp), and the Share, NpTrophy2, NpEntitlementAccess and WebBrowserDialog exports imported by PPSA12544's Unity plugins and added without an implementation, have unknown signatures - [libSceAudioPropagation](../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation) - signatures and struct descriptors recovered from PPSA21567's calls (#481), not from documentation. The system options layout is unknown (only checked for non-null); the fourth argument of `sceAudioPropagationSourceCalculateAudioPaths` is unknown; `sceAudioPropagationSourceGetAudioPath` is assumed to output an 8-byte path handle. The meaning of the `RenderInfo` word at +0x28 is unknown (PPSA21567 and PPSA21564 pass 2); only 2 is accepted. Both titles pass one `RenderInfo` per call: a count of 0 throws, and larger counts are handled element by element without proof. No error code is known, so invalid input throws instead of returning one - [NGS2 mastering voice gain](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - the param id 0x30000004 is taken from PPSA22520 and the layout from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2MasteringVoiceGainParam`; no reference gives the units or range. The levels are assumed to be linear factors applied when the mastering voice is mixed into the render buffer, so the voice's own samples stay unscaled; the LFE level applies to channel 3 of 6- and 8-channel voices, assuming the L R C LFE order of the AudioOut2 entries above, and the full-band level to the rest. Negative levels are accepted, and a new setup resets both levels to 1 +- [NGS2 stereo mastering voice into a surround buffer](../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - PPSA21402 renders a 2-channel mastering voice into a 6- or 8-channel render buffer, which the mixing stage rejected. The console's conversion is unknown: the left and right channels are assumed to go to the first two buffer channels (front left and right) and the remaining channels stay silent, with no centre, LFE or surround content. Any other channel count mismatch still throws - [sceAvPlayerStartEx](../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - start info layout unknown; it is ignored and playback starts as with `sceAvPlayerStart` - [sceAvPlayerInit / sceAvPlayerInitEx](../../core/libs/prx/libSceAvPlayer/src/Player.cpp) (libSceAvPlayer) - behaviour without a memory replacement unknown; frame and sample buffers then come from the guest heap - [SceAvPlayerVideoEx](../../core/libs/SceTypes.hpp) (libSceAvPlayer) - frame rate field and encoding unknown; it is left zero in frame and stream info