diff --git a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp index 6c08a090c1..716eb61cbf 100644 --- a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp @@ -16,8 +16,10 @@ #include "Ngs2Internal.hpp" static constexpr std::uint16_t WAVE_FORMAT_PCM = 0x1; +static constexpr std::uint16_t WAVE_FORMAT_IEEE_FLOAT = 0x3; static constexpr std::uint16_t WAVE_FORMAT_EXTENSIBLE = 0xfffe; static constexpr std::uint8_t ATRAC9_GUID[16] = {0xd2, 0x42, 0xe1, 0x47, 0xba, 0x36, 0x8d, 0x4d, 0x88, 0xfc, 0x61, 0x65, 0x4f, 0x8c, 0x83, 0x6c}; +static constexpr std::uint8_t PCM_GUID[16] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}; static constexpr std::size_t FMT_ATRAC9_SIZE = 52; static constexpr std::size_t FMT_GUID_OFFSET = 24; static constexpr std::size_t FMT_CONFIG_OFFSET = 44; @@ -342,19 +344,27 @@ static int ParseAtrac9(const RiffChunks& chunks, Ngs2WaveformInfo& info) { return ParseAtrac9Loop(chunks, info); } -static int ParsePcm16(const RiffChunks& chunks, std::size_t size, Ngs2WaveformInfo& info) { +static std::uint32_t PcmWaveformType(std::uint32_t bits, bool floating) { + if (floating) return bits == 32 ? 0x18 : bits == 64 ? 0x1a : 0; + return bits == 8 ? 0x11 : bits == 16 ? SCE_NGS2_WAVEFORM_TYPE_PCM_I16L : bits == 24 ? 0x14 : bits == 32 ? 0x16 : 0; +} + +static int ParsePcm(const RiffChunks& chunks, std::size_t size, Ngs2WaveformInfo& info, bool floating) { const auto* format = chunks.format; if (chunks.formatSize < 16) return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; + const std::uint32_t bits = ReadLe16(format + 14); + const std::uint32_t type = PcmWaveformType(bits, floating); + if (type == 0) return SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT; const std::uint32_t channels = ReadLe16(format + 2); const std::uint32_t sampleRate = ReadLe32(format + 4); - const std::uint32_t bytesPerFrame = channels * sizeof(std::int16_t); - if (ReadLe16(format + 14) != 16 || channels == 0 || channels > NGS2_MAX_CHANNELS || sampleRate == 0 || sampleRate > MAX_SAMPLE_RATE || ReadLe16(format + 12) != bytesPerFrame) { + const std::uint32_t bytesPerFrame = channels * (bits / 8); + if (channels == 0 || channels > NGS2_MAX_CHANNELS || sampleRate == 0 || sampleRate > MAX_SAMPLE_RATE || ReadLe16(format + 12) != bytesPerFrame) { return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; } if (chunks.sampler != nullptr && chunks.samplerSize >= 32 && ReadLe32(chunks.sampler + 28) != 0) throw std::runtime_error("NGS2: parsing looped waveforms is not implemented"); if (chunks.dataSize > size - chunks.dataOffset || chunks.dataSize % bytesPerFrame != 0) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA; const std::size_t dataSize = chunks.dataSize; - info.format = {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, channels, sampleRate, 0, 0, 0}; + info.format = {type, channels, sampleRate, 0, 0, 0}; info.data_offset = static_cast(chunks.dataOffset); info.data_size = static_cast(dataSize); info.num_samples = static_cast(dataSize / bytesPerFrame); @@ -377,7 +387,12 @@ static int ParseWaveform(const RiffChunks& chunks, std::size_t size, Ngs2Wavefor std::memcmp(chunks.format + FMT_GUID_OFFSET, ATRAC9_GUID, sizeof(ATRAC9_GUID)) == 0) { return ParseAtrac9(chunks, info); } - if (tag == WAVE_FORMAT_PCM) return ParsePcm16(chunks, size, info); + if (tag == WAVE_FORMAT_EXTENSIBLE && chunks.formatSize >= FMT_GUID_OFFSET + sizeof(PCM_GUID) && + std::memcmp(chunks.format + FMT_GUID_OFFSET, PCM_GUID, sizeof(PCM_GUID)) == 0) { + return ParsePcm(chunks, size, info, false); + } + if (tag == WAVE_FORMAT_PCM) return ParsePcm(chunks, size, info, false); + if (tag == WAVE_FORMAT_IEEE_FLOAT) return ParsePcm(chunks, size, info, true); throw std::runtime_error("NGS2: parsing waveform format tag " + Ngs2Hex(tag) + " is not implemented"); } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp index a2545f52e6..37ef362c57 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp @@ -22,20 +22,28 @@ static void PutTag(std::vector& out, const char* tag) { out.insert(out.end(), tag, tag + 4); } -static std::vector PcmFile(std::uint32_t channels, std::uint32_t sampleRate, std::uint32_t bits, std::uint32_t frames) { +static std::vector PcmFile(std::uint32_t channels, std::uint32_t sampleRate, std::uint32_t bits, std::uint32_t frames, std::uint32_t tag = 1) { const std::uint32_t frameBytes = channels * bits / 8; + const std::uint32_t formatSize = tag == 0xfffe ? 40 : 16; std::vector file; PutTag(file, "RIFF"); - Put32(file, 36 + frames * frameBytes); + Put32(file, 20 + formatSize + frames * frameBytes); PutTag(file, "WAVE"); PutTag(file, "fmt "); - Put32(file, 16); - Put16(file, 1); + Put32(file, formatSize); + Put16(file, tag); Put16(file, channels); Put32(file, sampleRate); Put32(file, sampleRate * frameBytes); Put16(file, frameBytes); Put16(file, bits); + if (tag == 0xfffe) { + Put16(file, 22); + Put16(file, bits); + Put32(file, 0); + const std::uint8_t pcm[16] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}; + file.insert(file.end(), pcm, pcm + sizeof(pcm)); + } PutTag(file, "data"); Put32(file, frames * frameBytes); file.resize(file.size() + frames * frameBytes, 0x11); @@ -60,8 +68,24 @@ static void TestParsePcm() { std::memcpy(partialFrame.data() + 40, &partialSize, sizeof(partialSize)); Require(sceNgs2ParseWaveformData(partialFrame.data(), partialFrame.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); - const auto eightBit = PcmFile(1, 44100, 8, 10); - Require(sceNgs2ParseWaveformData(eightBit.data(), eightBit.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); + struct Depth { std::uint32_t tag, bits, type; }; + for (const auto depth : {Depth{1, 8, 0x11}, Depth{1, 16, 0x12}, Depth{1, 24, 0x14}, Depth{1, 32, 0x16}, Depth{3, 32, 0x18}, Depth{3, 64, 0x1a}, + Depth{0xfffe, 8, 0x11}, Depth{0xfffe, 24, 0x14}}) { + const auto wave = PcmFile(3, 48000, depth.bits, 10, depth.tag); + const std::uint32_t frameBytes = 3 * depth.bits / 8; + const std::uint32_t dataOffset = static_cast(wave.size() - 10 * frameBytes); + Ngs2WaveformInfo parsed{}; + Require(sceNgs2ParseWaveformData(wave.data(), wave.size(), &parsed) == SCE_NGS2_OK); + Require(parsed.format.waveform_type == depth.type && parsed.format.num_channels == 3 && parsed.format.sample_rate == 48000); + Require(parsed.data_offset == dataOffset && parsed.data_size == 10 * frameBytes && parsed.num_samples == 10); + Require(parsed.audio_unit_size == frameBytes && parsed.num_audio_unit_samples == 1 && parsed.audio_frame_size == frameBytes && parsed.num_audio_frame_samples == 1); + Require(parsed.num_blocks == 1 && parsed.block[0].data_offset == dataOffset && parsed.block[0].data_size == 10 * frameBytes && parsed.block[0].num_samples == 10); + } + for (const auto depth : {Depth{1, 12, 0}, Depth{1, 64, 0}, Depth{3, 16, 0}, Depth{3, 24, 0}, Depth{0xfffe, 12, 0}}) { + const auto wave = PcmFile(1, 44100, depth.bits, 8, depth.tag); + Ngs2WaveformInfo parsed{}; + Require(sceNgs2ParseWaveformData(wave.data(), wave.size(), &parsed) == SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT); + } const auto noChannels = PcmFile(0, 44100, 16, 10); Require(sceNgs2ParseWaveformData(noChannels.data(), noChannels.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); } diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index f5a4bd1856..1a599b662b 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -29,6 +29,7 @@ - [sceNgs2VoiceQueryInfo](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - only info id 0x4001 is known: PPSA22520 passes an 8-byte buffer and branches on the first word being 1 or 2, so it is assumed to be the voice channel count (0 before the voice is set up); the second word is zeroed and other ids throw - [sceNgs2VoiceGetOwner](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - the signature `(voiceHandle, outOwnerRack, outVoiceIndex)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L439-L440) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2.cpp#L322-L326), which only logs it; neither gives an error. The errors are unmeasured: a handle that is not a live voice answers `INVALID_VOICE_HANDLE` before the outputs are checked, as in `sceNgs2RackGetVoiceHandle`, and either output pointer null answers `INVALID_OUT_ADDRESS` without writing the other - [sceNgs2VoiceControl patch](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a destination rack handle with input id 0 resolves to that rack's first voice; this matches the Dead Cells call in #4334, but selection for racks with multiple voices has not been checked against a console. Nonzero destination input ids still throw. `sceNgs2VoiceGetPortInfo` reports the resolved voice handle and zero delay. +- [NGS2 PCM waveform parsing](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - `sceNgs2ParseWaveformData` reads integer PCM (tag 1, or `WAVE_FORMAT_EXTENSIBLE` with the PCM GUID) of 8, 16, 24 and 32 bits as types 0x11, 0x12, 0x14 and 0x16, and float (tag 3) of 32 and 64 bits as 0x18 and 0x1a, and returns 0x804A8432 for other depths, after [prosper's reading of `libSceNgs2.native.sprx`](https://github.com/mattias800/prosper/blob/30572df43ae20ff28f995db56149f7af42235741/prosper/src/hle/audio/ngs2_waveform.cpp#L194-L223); not measured on a console. Every depth keeps the checks of the 16-bit path (channels, rate, block align, whole frames, a looped `smpl` chunk throws), which that reading does not apply. Voices still play only 16-bit PCM and ATRAC9 - [sceNgs2ParseWaveformFile](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature `(path, offset, info)` is taken from the call site of PPSA21402, which sets only three argument registers; the file is read from `offset` to its end, the data and block offsets are taken to be relative to the start of the file, not of `offset`, and an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame; `INVALID_WAVEFORM_DATA` for a `data` chunk that runs past the buffer or ends inside a frame is assumed, and looped PCM and other PCM formats throw - [sceNgs2VoiceControl sampler filter](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown - [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. Error codes are not known for the PS5, so invalid arguments throw, including a speaker angle equal to the unit angle, on which that library loops forever. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason