feat(libSceNgs2.native): implement sceNgs2ParseWaveformFile and parse 16-bit PCM waveforms
This commit is contained in:
parent
87ab98b9e9
commit
64230cf95a
|
|
@ -10,7 +10,7 @@ function(add_sce_ngs2_library target)
|
|||
${ngs2Dir}/src/Voice.cpp
|
||||
)
|
||||
target_include_directories(${target} PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src)
|
||||
target_link_libraries(${target} PRIVATE atrac9)
|
||||
target_link_libraries(${target} PRIVATE atrac9 libc libkernel)
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
target_link_options(${target} PRIVATE LINKER:--exclude-libs,ALL)
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -2,15 +2,19 @@
|
|||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "libatrac9.h"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
#include "Ngs2Internal.hpp"
|
||||
|
||||
static constexpr std::uint16_t WAVE_FORMAT_PCM = 0x1;
|
||||
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::size_t FMT_ATRAC9_SIZE = 52;
|
||||
|
|
@ -181,6 +185,33 @@ static int ParseAtrac9(const RiffChunks& chunks, Ngs2WaveformInfo& info) {
|
|||
return SCE_NGS2_OK;
|
||||
}
|
||||
|
||||
static int ParsePcm16(const RiffChunks& chunks, std::size_t size, Ngs2WaveformInfo& info) {
|
||||
const auto* format = chunks.format;
|
||||
if (chunks.formatSize < 16) return SCE_NGS2_ERROR_INVALID_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) {
|
||||
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");
|
||||
const std::size_t dataSize = std::min<std::size_t>(chunks.dataSize, size - chunks.dataOffset) / bytesPerFrame * bytesPerFrame;
|
||||
info.format = {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, channels, sampleRate, 0, 0, 0};
|
||||
info.data_offset = static_cast<std::uint32_t>(chunks.dataOffset);
|
||||
info.data_size = static_cast<std::uint32_t>(dataSize);
|
||||
info.num_samples = static_cast<std::uint32_t>(dataSize / bytesPerFrame);
|
||||
info.audio_unit_size = bytesPerFrame;
|
||||
info.num_audio_unit_samples = 1;
|
||||
info.num_audio_unit_per_frame = 1;
|
||||
info.audio_frame_size = bytesPerFrame;
|
||||
info.num_audio_frame_samples = 1;
|
||||
info.num_blocks = 1;
|
||||
info.block[0].data_offset = chunks.dataOffset;
|
||||
info.block[0].data_size = dataSize;
|
||||
info.block[0].num_samples = info.num_samples;
|
||||
return SCE_NGS2_OK;
|
||||
}
|
||||
|
||||
#pragma GCC visibility push(default)
|
||||
|
||||
extern "C" {
|
||||
|
|
@ -197,9 +228,31 @@ int APS5_VABI sceNgs2ParseWaveformData(const void* data, size_t data_size, Ngs2W
|
|||
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, data_size, *info);
|
||||
throw std::runtime_error("NGS2: parsing waveform format tag " + Ngs2Hex(tag) + " is not implemented");
|
||||
}
|
||||
|
||||
int APS5_VABI sceNgs2ParseWaveformFile(const char* path, uint32_t offset, uint32_t size, Ngs2WaveformInfo* info) {
|
||||
if (info == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS;
|
||||
*info = {};
|
||||
if (path == nullptr) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA;
|
||||
const auto host = ResolvePath_nid_no_patch(path);
|
||||
std::ifstream file(host, std::ios::binary | std::ios::ate);
|
||||
if (!file) throw std::runtime_error(std::string(__func__) + ": cannot open " + host.string());
|
||||
const auto fileSize = static_cast<std::uint64_t>(file.tellg());
|
||||
if (offset > fileSize) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA;
|
||||
const std::uint64_t length = size != 0 ? size : fileSize - offset;
|
||||
if (length > fileSize - offset) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA;
|
||||
std::vector<std::uint8_t> bytes(static_cast<std::size_t>(length));
|
||||
file.seekg(offset);
|
||||
if (!file.read(reinterpret_cast<char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()))) throw std::runtime_error(std::string(__func__) + ": read failed for " + host.string());
|
||||
const int result = sceNgs2ParseWaveformData(bytes.data(), bytes.size(), info);
|
||||
if (result != SCE_NGS2_OK) return result;
|
||||
info->data_offset += offset;
|
||||
for (std::uint32_t i = 0; i < info->num_blocks; i++) info->block[i].data_offset += offset;
|
||||
return SCE_NGS2_OK;
|
||||
}
|
||||
|
||||
int APS5_VABI sceNgs2CalcWaveformBlock(const Ngs2WaveformFormat* format, uint32_t sample_pos, uint32_t num_samples, Ngs2WaveformBlock* block) {
|
||||
if (block == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS;
|
||||
*block = {};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <random>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
|
@ -176,6 +178,67 @@ static void TestParse() {
|
|||
Require(sceNgs2ParseWaveformData(wrongRate.data(), wrongRate.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT);
|
||||
}
|
||||
|
||||
static std::vector<std::uint8_t> PcmFile(std::uint32_t channels, std::uint32_t sampleRate, std::uint32_t bits, std::uint32_t frames) {
|
||||
const std::uint32_t frameBytes = channels * bits / 8;
|
||||
std::vector<std::uint8_t> file;
|
||||
PutTag(file, "RIFF");
|
||||
Put32(file, 36 + frames * frameBytes);
|
||||
PutTag(file, "WAVE");
|
||||
PutTag(file, "fmt ");
|
||||
Put32(file, 16);
|
||||
Put16(file, 1);
|
||||
Put16(file, channels);
|
||||
Put32(file, sampleRate);
|
||||
Put32(file, sampleRate * frameBytes);
|
||||
Put16(file, frameBytes);
|
||||
Put16(file, bits);
|
||||
PutTag(file, "data");
|
||||
Put32(file, frames * frameBytes);
|
||||
file.resize(file.size() + frames * frameBytes, 0x11);
|
||||
return file;
|
||||
}
|
||||
|
||||
static void TestParsePcm() {
|
||||
const auto file = PcmFile(2, 44100, 16, 100);
|
||||
Ngs2WaveformInfo info{};
|
||||
Require(sceNgs2ParseWaveformData(file.data(), file.size(), &info) == SCE_NGS2_OK);
|
||||
Require(info.format.waveform_type == SCE_NGS2_WAVEFORM_TYPE_PCM_I16L && info.format.num_channels == 2 && info.format.sample_rate == 44100);
|
||||
Require(info.data_offset == 44 && info.data_size == 400 && info.num_samples == 100);
|
||||
Require(info.num_blocks == 1 && info.block[0].data_offset == 44 && info.block[0].data_size == 400 && info.block[0].num_samples == 100);
|
||||
|
||||
auto truncated = file;
|
||||
truncated.resize(44 + 202);
|
||||
Require(sceNgs2ParseWaveformData(truncated.data(), truncated.size(), &info) == SCE_NGS2_OK);
|
||||
Require(info.data_size == 200 && info.num_samples == 50);
|
||||
|
||||
const auto eightBit = PcmFile(1, 44100, 8, 10);
|
||||
Require(sceNgs2ParseWaveformData(eightBit.data(), eightBit.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT);
|
||||
const auto noChannels = PcmFile(0, 44100, 16, 10);
|
||||
Require(sceNgs2ParseWaveformData(noChannels.data(), noChannels.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT);
|
||||
}
|
||||
|
||||
static void TestParseFile() {
|
||||
const auto wave = PcmFile(1, 22050, 16, 64);
|
||||
const auto path = std::filesystem::temp_directory_path() / "aps5_ngs2_parse_file.wav";
|
||||
{
|
||||
std::ofstream out(path, std::ios::binary);
|
||||
out.write("", 3);
|
||||
out.write(reinterpret_cast<const char*>(wave.data()), static_cast<std::streamsize>(wave.size()));
|
||||
}
|
||||
const auto name = path.string();
|
||||
Ngs2WaveformInfo info{};
|
||||
Require(sceNgs2ParseWaveformFile(name.c_str(), 3, 0, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS);
|
||||
Require(sceNgs2ParseWaveformFile(nullptr, 0, 0, &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA);
|
||||
Require(sceNgs2ParseWaveformFile(name.c_str(), 3, 0, &info) == SCE_NGS2_OK);
|
||||
Require(info.format.num_channels == 1 && info.format.sample_rate == 22050 && info.num_samples == 64);
|
||||
Require(info.data_offset == 3 + 44 && info.block[0].data_offset == 3 + 44 && info.data_size == 128);
|
||||
Require(sceNgs2ParseWaveformFile(name.c_str(), 3, static_cast<std::uint32_t>(wave.size()), &info) == SCE_NGS2_OK);
|
||||
Require(sceNgs2ParseWaveformFile(name.c_str(), 3, static_cast<std::uint32_t>(wave.size()) + 1, &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA);
|
||||
Require(sceNgs2ParseWaveformFile(name.c_str(), static_cast<std::uint32_t>(wave.size()) + 4, 0, &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA);
|
||||
Require(sceNgs2ParseWaveformFile(name.c_str(), 0, 0, &info) == SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT);
|
||||
std::filesystem::remove(path);
|
||||
}
|
||||
|
||||
static void TestCalcBlock() {
|
||||
const Ngs2WaveformFormat format{SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 1, 48000, Config, 0, 0};
|
||||
Ngs2WaveformBlock block{};
|
||||
|
|
@ -235,6 +298,8 @@ int main() {
|
|||
TestSkipAndBlockEnd(reference);
|
||||
TestRepeatAndState(reference);
|
||||
TestParse();
|
||||
TestParsePcm();
|
||||
TestParseFile();
|
||||
TestCalcBlock();
|
||||
TestCorruptSuperframeAtPageEnd();
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
extern "C" {
|
||||
int APS5_VABI sceNgs2ParseWaveformData(const void*, size_t, Ngs2WaveformInfo*);
|
||||
int APS5_VABI sceNgs2ParseWaveformFile(const char*, uint32_t, uint32_t, Ngs2WaveformInfo*);
|
||||
int APS5_VABI sceNgs2CalcWaveformBlock(const Ngs2WaveformFormat*, uint32_t, uint32_t, Ngs2WaveformBlock*);
|
||||
int APS5_VABI sceNgs2SystemResetOption(Ngs2SystemOption*);
|
||||
int APS5_VABI sceNgs2SystemQueryBufferSize(const Ngs2SystemOption*, Ngs2ContextBufferInfo*);
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@ Throughout the project, every function at every stage either **does exactly what
|
|||
- [sceNgs2RackGetInfo](../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - field order from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2RackInfo` with the 64-byte name of the other info structures (216 bytes); `type` is assumed to be the rack id divided by 0x1000, `min_grain_samples` 64 (the system minimum) and `state_flags` 1; `uid` comes from a per-process rack counter, `render_count` is the owning system's render count and `active_voice_count` counts voices that are not empty (paused and stopped voices included). The last process ratio and tick and `active_channel_work_count` are left zero
|
||||
- [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
|
||||
- [sceNgs2VoiceGetPortInfo](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - layout from shadPS4; the delay and destination input are zero because ports have neither yet
|
||||
- [sceNgs2ParseWaveformFile](../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature is assumed to be `(path, offset, size, info)` with a zero size meaning the rest of the file, and the data and block offsets are taken to be relative to the start of the file, not of the requested range; 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, and the info of looped PCM and of other PCM formats throw
|
||||
- [sceSaveDataInitialize3](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a repeated initialize succeeds: PPSA12544's executable and its Unity SaveData plugin both initialize, and the plugin fails on any error. Initializations are counted and `sceSaveDataTerminate` ends the session at the last one; how the console pairs them is unknown
|
||||
- [sceVoiceSetMuteFlag](../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature
|
||||
- [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - only shadPS4 (PS4) has bodies. A null tick returns `INVALID_POINTER` like the existing `sceRtcFormatRFC3339`, where shadPS4 formats the current time instead; offsets outside ±1439 minutes return `INVALID_VALUE` like `sceRtcFormatRFC3339`; the local-time variants use the host's offset at the given instant (shadPS4 uses `sceKernelGettimezone`, the current offset)
|
||||
|
|
|
|||
Loading…
Reference in New Issue