fix(libSceNgs2): return INVALID_VOICE_ACCESS for PLAY on a voice that was never set up, as the PS5 module does

The PS5 libSceNgs2.native module (firmware 3.20 and 4.03) fails a play event on a voice with no setup with 0x804A8351 on every rack I tried and leaves it idle, so the guard now covers every rack and returns that code.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
JAB-devandClaude Sonnet 5.5 committed 2026-10-23 00:32:00 +02:00
1 parent 3e8e844932
commit 32cff4e0b3
4 files changed
+16 -11

No files matched your search

@@ -9,7 +9,7 @@
static constexpr int SCE_NGS2_OK = 0;
static constexpr int SCE_NGS2_ERROR_INVALID_OUT_ADDRESS = static_cast<int>(0x804A8010);
static constexpr int SCE_NGS2_ERROR_INVALID_OUT_SIZE = static_cast<int>(0x804A8011);
static constexpr int SCE_NGS2_ERROR_INVALID_NUM_CHANNELS = static_cast<int>(0x804A8052);
static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_ACCESS = static_cast<int>(0x804A8351);
static constexpr int SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE = static_cast<int>(0x804A8201);
static constexpr int SCE_NGS2_ERROR_INVALID_RACK_HANDLE = static_cast<int>(0x804A8202);
static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_INDEX = static_cast<int>(0x804A8305);
@@ -25,7 +25,7 @@ struct Ngs2ReturnCode {
};
static void SendEvent(Ngs2Voice& voice, std::uint32_t eventId) {
if (eventId == SCE_NGS2_VOICE_EVENT_PLAY && voice.rack->rackId == SCE_NGS2_RACK_ID_SAMPLER && voice.channels == 0) throw Ngs2ReturnCode{SCE_NGS2_ERROR_INVALID_NUM_CHANNELS};
if (eventId == SCE_NGS2_VOICE_EVENT_PLAY && voice.channels == 0) throw Ngs2ReturnCode{SCE_NGS2_ERROR_INVALID_VOICE_ACCESS};
voice.SetEvent(eventId);
}
@@ -23,8 +23,8 @@ static void TestPlayWithoutSetup() {
const Ngs2VoiceCommand playCommand{2, 0, 4, 0, {.u = SCE_NGS2_VOICE_EVENT_PLAY}};
Event(voice, SCE_NGS2_VOICE_EVENT_STOP_IMM);
Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, nullptr, 0, 0, nullptr});
Require(sceNgs2VoiceControl(voice, &play.header) == SCE_NGS2_ERROR_INVALID_NUM_CHANNELS);
Require(sceNgs2VoiceRunCommands(voice, &playCommand, 1) == SCE_NGS2_ERROR_INVALID_NUM_CHANNELS);
Require(sceNgs2VoiceControl(voice, &play.header) == SCE_NGS2_ERROR_INVALID_VOICE_ACCESS);
Require(sceNgs2VoiceRunCommands(voice, &playCommand, 1) == SCE_NGS2_ERROR_INVALID_VOICE_ACCESS);
std::vector<float> out(Grain, -1.0f);
const Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 1};
@@ -61,22 +61,27 @@ static void TestPlayAfterSetupInOneList() {
Reversed reversed{PlayParam(), SamplerSetup};
reversed.play.header = {sizeof(reversed.play), static_cast<std::int16_t>(sizeof(reversed.play)), SCE_NGS2_VOICE_PARAM_EVENT};
reversed.setup.header = {sizeof(reversed.setup), 0, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP};
Require(sceNgs2VoiceControl(other, &reversed.play.header) == SCE_NGS2_ERROR_INVALID_NUM_CHANNELS);
Require(sceNgs2VoiceControl(other, &reversed.play.header) == SCE_NGS2_ERROR_INVALID_VOICE_ACCESS);
Require(Flags(other) == 0);
Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK);
}
static void TestOtherRacksStillPlay() {
static void TestNeverSetUpVoicesOfOtherRacks() {
const auto system = CreateSystem();
const auto submixer = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SUBMIXER));
Event(submixer, SCE_NGS2_VOICE_EVENT_PLAY);
Require(Flags(submixer) == SCE_NGS2_VOICE_STATE_FLAG_INUSE);
const auto play = PlayParam();
for (const auto rackId : {SCE_NGS2_RACK_ID_SUBMIXER, SCE_NGS2_RACK_ID_REVERB, SCE_NGS2_RACK_ID_MASTERING}) {
const auto voice = Voice(CreateRack(system, rackId));
Require(sceNgs2VoiceControl(voice, &play.header) == SCE_NGS2_ERROR_INVALID_VOICE_ACCESS);
Require(Flags(voice) == 0);
}
const auto master = Mastering(system, 1);
Require(Flags(master) == SCE_NGS2_VOICE_STATE_FLAG_INUSE);
Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK);
}
int main() {
TestPlayWithoutSetup();
TestPlayAfterSetupInOneList();
TestOtherRacksStillPlay();
TestNeverSetUpVoicesOfOtherRacks();
return 0;
}
+1 -1
View File
@@ -14,7 +14,7 @@
- [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
- [NGS2 voices without a format](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a sampler voice whose setup has no format (all zero) accepts an add of no waveform blocks without `APPEND`, which only applies `RESET` and the `CONTINUE` state, and matrix levels with no levels, which clear that matrix. PPSA36591 sets such a voice up, adds no blocks and sets no levels when a button is pressed at its title screen. Whether the console returns success or an error there is not measured; blocks with data, `APPEND` and an empty matrix on a voice with a format are still refused
- [NGS2 play without setup](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - not confirmed on the PS5 library: a `PLAY` event on a sampler voice that has no channels returns `0x804A8052` (`SCE_NGS2_ERROR_INVALID_NUM_CHANNELS` in the PS5 SDK) from Control and RunCommands and the voice stays idle (state flags 0), so the voice slots of PPSA03061 (Void Terrarium), which it frees by polling the state flags, are released. The PS4 firmware 11.00 `libSceNgs2.sprx` was measured through Control only (it returns `0x804A0052`) for a sampler voice patched to a mastering voice at 48000 Hz and grain 256, after a stop-immediately event and an add of no waveform blocks; the RunCommands result is inferred. Which voices count as set up (an all-zero format and a voice after a setup reset have no channels, see NGS2 voices without a format), what a failing list of parameters leaves applied before the failure and other racks were not measured; other racks keep their current behaviour
- [NGS2 play without setup](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - differs from PS5 firmware 3.20 and 4.03 `libSceNgs2.native.sprx` in three cases: a set-up voice given an add of no waveform blocks and played keeps state flags 1 there for as long as it was watched and only until the next render here; an add of no blocks to a voice that already played and ended returns `0x804A8054` there and succeeds here; a playing voice reset by an all-zero setup keeps its state there and is stopped here. A `PLAY` on a voice with no channels returns `0x804A8351` (`SCE_NGS2_ERROR_INVALID_VOICE_ACCESS`) from Control and RunCommands, leaves it idle and skips the rest of the list, as that module does for every rack; PPSA03061 (Void Terrarium) frees its sound slots that way
- [NGS2 silent waveform blocks](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native): flag `0x10` (named `SILENCE` here; no public reference names it) lets a waveform block declare samples without data. PPSA04867 adds one block with flags `0x11`, `data_size` 0 and `num_samples` 16385, then another with flags `0x11` and a fully specified 0x800-byte, 1024-sample mono block, which takes the ordinary path. A dataless block under the flag outputs `num_samples` frames of silence, reports them through `decoded_data_size`, and finishes like a complete block; a dataless block without the flag still throws as not fitting its data. The flag's real name, its interaction with ATRAC9 and repeats, and the console's `waveform_data` for silent blocks are unverified on hardware
- [NGS2 streaming PCM waveforms](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - PPSA21402 streams its music by adding one block whose `num_samples` is the whole track and whose `data_size` is a 16 KiB first piece, with `CONTINUE`, and then adds each following piece with flag `0x2` (called `APPEND` here) and the same `num_samples`. No reference describes flag `0x2`. A 16-bit PCM block without skipped samples or repeats whose `num_samples` exceed its data is assumed to start such a stream; flag `0x2` is assumed to append the block's data to the last streaming block, ignoring its `num_samples`; the voice is assumed to output silence without advancing while it has played all appended data and `CONTINUE` is set; and the waveform is assumed to end at `num_samples` or, once `CONTINUE` is cleared, at the end of the appended data. `sceNgs2VoiceGetState` reports the bytes played as `decoded_data_size`, which the title uses to release its queued data, and the read position as `waveform_data`. Appending to a block that is not a stream, or other formats, skipped samples, repeats and partial frames, throw
- [NGS2 matrix levels](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - levels are clamped to -4..4 and NaN is kept, as the PS4 firmware 11.00 `libSceNgs2.sprx` stores them; not yet confirmed on the PS5 library. That library rejects a level count of 0 and keeps the matrix; the PS5 error code is unknown, so a count of 0 throws, except on a sampler voice without a format, where it clears the matrix (see NGS2 voices without a format). `sceNgs2VoiceRunCommands` is not in that library, so its matrix levels command is assumed to clamp the same way