mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-27 00:28:14 +00:00
feat(ngs2): play additional PCM sampler formats
This commit is contained in:
1 parent
c661d9fae2
commit
08587dfa7e
10 files changed
+350
-18
No files matched your search
@@ -14,4 +14,5 @@ if(BUILD_TESTING)
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNgs2.native>")
|
||||
endif()
|
||||
endforeach()
|
||||
add_sce_ngs2_test(libSceNgs2.native ngs2_pcm Ngs2Pcm.cpp)
|
||||
endif()
|
||||
@@ -5,6 +5,7 @@ function(add_sce_ngs2_library target)
|
||||
${ngs2Dir}/src/Custom.cpp
|
||||
${ngs2Dir}/src/Geom.cpp
|
||||
${ngs2Dir}/src/Pan.cpp
|
||||
${ngs2Dir}/src/Pcm.cpp
|
||||
${ngs2Dir}/src/Rack.cpp
|
||||
${ngs2Dir}/src/Render.cpp
|
||||
${ngs2Dir}/src/Reverb.cpp
|
||||
@@ -23,3 +24,15 @@ function(add_sce_ngs2_library target)
|
||||
)
|
||||
configure_windows_unwind(${target})
|
||||
endfunction()
|
||||
|
||||
function(add_sce_ngs2_test library name source)
|
||||
add_test_executable(${name}_tests ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/tests/${source})
|
||||
target_include_directories(${name}_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src)
|
||||
target_link_libraries(${name}_tests PRIVATE ${library} atrac9 libkernel libc)
|
||||
configure_windows_unwind(${name}_tests)
|
||||
add_test(NAME ${name} COMMAND ${name}_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(${name} PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:${library}>")
|
||||
endif()
|
||||
endfunction()
|
||||
@@ -46,10 +46,14 @@ static constexpr std::uint32_t SCE_NGS2_RACK_ID_CUSTOM_SAMPLER = 0x4001;
|
||||
static constexpr std::uint32_t SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER = 0x4002;
|
||||
static constexpr std::uint32_t SCE_NGS2_RACK_ID_CUSTOM_MASTERING = 0x4003;
|
||||
|
||||
static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_U8 = 0x11;
|
||||
static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_I16L = 0x12;
|
||||
static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_I16B = 0x13;
|
||||
static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_I24L = 0x14;
|
||||
static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_I32L = 0x16;
|
||||
static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_F32L = 0x18;
|
||||
static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_F32B = 0x19;
|
||||
static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_F64L = 0x1a;
|
||||
static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_ATRAC9 = 0x40;
|
||||
|
||||
static constexpr std::uint32_t SCE_NGS2_VOICE_EVENT_PLAY = 1;
|
||||
|
||||
@@ -191,6 +191,8 @@ struct Ngs2System {
|
||||
};
|
||||
|
||||
std::string Ngs2Hex(std::uint32_t value);
|
||||
std::size_t Ngs2PcmSampleBytes(std::uint32_t waveformType);
|
||||
void Ngs2ReadPcmFrame(std::uint32_t waveformType, std::uint32_t channels, const std::uint8_t* frame, float* output);
|
||||
const std::uint8_t* Ngs2StreamEnd(const Ngs2Voice& voice, const Ngs2Block& block);
|
||||
std::recursive_mutex& Ngs2Mutex();
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
#include <bit>
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "Ngs2Internal.hpp"
|
||||
|
||||
std::size_t Ngs2PcmSampleBytes(std::uint32_t waveformType) {
|
||||
switch (waveformType) {
|
||||
case SCE_NGS2_WAVEFORM_TYPE_PCM_U8: return 1;
|
||||
case SCE_NGS2_WAVEFORM_TYPE_PCM_I16L: return 2;
|
||||
case SCE_NGS2_WAVEFORM_TYPE_PCM_I24L: return 3;
|
||||
case SCE_NGS2_WAVEFORM_TYPE_PCM_I32L:
|
||||
case SCE_NGS2_WAVEFORM_TYPE_PCM_F32L: return 4;
|
||||
case SCE_NGS2_WAVEFORM_TYPE_PCM_F64L: return 8;
|
||||
default: throw std::runtime_error("NGS2: PCM waveform type " + Ngs2Hex(waveformType) + " is not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
static std::uint64_t ReadLittleEndian(const std::uint8_t* data, std::size_t bytes) {
|
||||
std::uint64_t value = 0;
|
||||
for (std::size_t i = 0; i < bytes; i++) value |= static_cast<std::uint64_t>(data[i]) << (i * 8);
|
||||
return value;
|
||||
}
|
||||
|
||||
static void CheckFloatRange(std::uint64_t magnitude, std::uint64_t minimum, std::uint64_t maximum) {
|
||||
if (magnitude > maximum || (magnitude != 0 && magnitude < minimum)) {
|
||||
throw std::runtime_error("NGS2: nonfinite, subnormal or out-of-range floating-point PCM is not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
static float ReadSample(std::uint32_t waveformType, const std::uint8_t* sample, std::size_t bytes) {
|
||||
const auto value = ReadLittleEndian(sample, bytes);
|
||||
switch (waveformType) {
|
||||
case SCE_NGS2_WAVEFORM_TYPE_PCM_U8:
|
||||
return (static_cast<int>(value) - 128) / 128.0f;
|
||||
case SCE_NGS2_WAVEFORM_TYPE_PCM_I16L:
|
||||
return std::bit_cast<std::int16_t>(static_cast<std::uint16_t>(value)) / 32768.0f;
|
||||
case SCE_NGS2_WAVEFORM_TYPE_PCM_I24L:
|
||||
return (static_cast<std::int32_t>(value ^ 0x800000) - 0x800000) / 8388608.0f;
|
||||
case SCE_NGS2_WAVEFORM_TYPE_PCM_I32L:
|
||||
return static_cast<float>(std::bit_cast<std::int32_t>(static_cast<std::uint32_t>(value)) / 2147483648.0);
|
||||
case SCE_NGS2_WAVEFORM_TYPE_PCM_F32L:
|
||||
CheckFloatRange(value & 0x7fffffff, 0x00800000, 0x3f800000);
|
||||
return std::bit_cast<float>(static_cast<std::uint32_t>(value));
|
||||
case SCE_NGS2_WAVEFORM_TYPE_PCM_F64L:
|
||||
CheckFloatRange(value & 0x7fffffffffffffff, 0x3810000000000000, 0x3ff0000000000000);
|
||||
return static_cast<float>(std::bit_cast<double>(value));
|
||||
default: throw std::runtime_error("NGS2: PCM sample conversion is not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
void Ngs2ReadPcmFrame(std::uint32_t waveformType, std::uint32_t channels, const std::uint8_t* frame, float* output) {
|
||||
const auto bytes = Ngs2PcmSampleBytes(waveformType);
|
||||
for (std::uint32_t channel = 0; channel < channels; channel++) {
|
||||
output[channel] = frame == nullptr ? 0.0f : ReadSample(waveformType, frame + channel * bytes, bytes);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ static constexpr double PHASE_ONE = 4294967296.0;
|
||||
static constexpr std::uint64_t PHASE_FRACTION = 0xffffffff;
|
||||
|
||||
static const std::uint8_t* FrameAt(const Ngs2Voice& voice, const Ngs2Block& block, std::uint32_t frame) {
|
||||
const std::size_t frameBytes = voice.channels * sizeof(std::int16_t);
|
||||
const std::size_t frameBytes = voice.channels * Ngs2PcmSampleBytes(voice.waveformType);
|
||||
if (block.data == nullptr) return nullptr;
|
||||
if (block.streaming) {
|
||||
for (const auto& piece : block.pieces) {
|
||||
@@ -27,15 +27,7 @@ static const std::uint8_t* FrameAt(const Ngs2Voice& voice, const Ngs2Block& bloc
|
||||
}
|
||||
|
||||
static void ReadPcm(const Ngs2Voice& voice, const std::uint8_t* frame, float* out) {
|
||||
if (frame == nullptr) {
|
||||
for (std::uint32_t channel = 0; channel < voice.channels; channel++) out[channel] = 0.0f;
|
||||
return;
|
||||
}
|
||||
for (std::uint32_t channel = 0; channel < voice.channels; channel++) {
|
||||
std::int16_t sample;
|
||||
std::memcpy(&sample, frame + channel * sizeof(sample), sizeof(sample));
|
||||
out[channel] = sample / 32768.0f;
|
||||
}
|
||||
Ngs2ReadPcmFrame(voice.waveformType, voice.channels, frame, out);
|
||||
}
|
||||
|
||||
static void ReadFrames(Ngs2Voice& voice, float* current, float* next) {
|
||||
@@ -93,7 +85,7 @@ static void Advance(Ngs2Voice& voice, std::uint64_t frames) {
|
||||
const auto step = std::min<std::uint64_t>(frames, limit - block.cursor);
|
||||
block.cursor += static_cast<std::uint32_t>(step);
|
||||
voice.decodedSamples += step;
|
||||
if (voice.waveformType != SCE_NGS2_WAVEFORM_TYPE_ATRAC9) voice.decodedBytes += step * voice.channels * sizeof(std::int16_t);
|
||||
if (voice.waveformType != SCE_NGS2_WAVEFORM_TYPE_ATRAC9) voice.decodedBytes += step * voice.channels * Ngs2PcmSampleBytes(voice.waveformType);
|
||||
frames -= step;
|
||||
if (block.streaming) {
|
||||
while (block.pieces.size() > 1 && block.cursor >= block.pieces.front().firstFrame + block.pieces.front().frames) block.pieces.erase(block.pieces.begin());
|
||||
|
||||
@@ -87,7 +87,7 @@ void Ngs2Voice::ResetSetup() {
|
||||
|
||||
const std::uint8_t* Ngs2StreamEnd(const Ngs2Voice& voice, const Ngs2Block& block) {
|
||||
const auto& last = block.pieces.back();
|
||||
return last.data + last.frames * voice.channels * sizeof(std::int16_t);
|
||||
return last.data + last.frames * voice.channels * Ngs2PcmSampleBytes(voice.waveformType);
|
||||
}
|
||||
|
||||
const std::uint8_t* Ngs2Voice::WaveformData() const {
|
||||
@@ -96,11 +96,12 @@ const std::uint8_t* Ngs2Voice::WaveformData() const {
|
||||
if (waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9) return block.data + block.dataCursor;
|
||||
if (block.streaming) {
|
||||
for (const auto& piece : block.pieces) {
|
||||
if (block.cursor >= piece.firstFrame && block.cursor - piece.firstFrame < piece.frames) return piece.data + (block.cursor - piece.firstFrame) * channels * sizeof(std::int16_t);
|
||||
if (block.cursor >= piece.firstFrame && block.cursor - piece.firstFrame < piece.frames) return piece.data + (block.cursor - piece.firstFrame) * channels * Ngs2PcmSampleBytes(waveformType);
|
||||
}
|
||||
return Ngs2StreamEnd(*this, block);
|
||||
}
|
||||
return block.data + (static_cast<std::size_t>(block.info.num_skip_samples) + block.cursor) * channels * sizeof(std::int16_t);
|
||||
if (block.data == nullptr) return nullptr;
|
||||
return block.data + (static_cast<std::size_t>(block.info.num_skip_samples) + block.cursor) * channels * Ngs2PcmSampleBytes(waveformType);
|
||||
}
|
||||
|
||||
static Ngs2Port& PortAt(Ngs2Voice& voice, std::uint32_t port) {
|
||||
@@ -187,8 +188,9 @@ static void SetupSampler(Ngs2Voice& voice, const Ngs2WaveformFormat& format) {
|
||||
return;
|
||||
}
|
||||
voice.ResetSetup();
|
||||
if (format.waveform_type != SCE_NGS2_WAVEFORM_TYPE_PCM_I16L && format.waveform_type != SCE_NGS2_WAVEFORM_TYPE_ATRAC9) {
|
||||
throw std::runtime_error("NGS2: waveform type " + Ngs2Hex(format.waveform_type) + " is not implemented");
|
||||
if (format.waveform_type != SCE_NGS2_WAVEFORM_TYPE_ATRAC9) Ngs2PcmSampleBytes(format.waveform_type);
|
||||
if (format.waveform_type != SCE_NGS2_WAVEFORM_TYPE_PCM_I16L && format.waveform_type != SCE_NGS2_WAVEFORM_TYPE_ATRAC9 && format.config_data != 0) {
|
||||
throw std::runtime_error("NGS2: configured PCM waveforms are not implemented");
|
||||
}
|
||||
if (format.frame_offset != 0 || format.frame_margin != 0) throw std::runtime_error("NGS2: waveform frame offset and margin are not implemented");
|
||||
if (format.num_channels == 0 || format.num_channels > NGS2_MAX_CHANNELS || format.sample_rate == 0) APS5_INVALID_ARG_EX;
|
||||
@@ -239,7 +241,7 @@ static void AddWaveformBlocks(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBl
|
||||
voice.waveformEnd = nullptr;
|
||||
if (voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9) Ngs2RestartAtrac9(voice);
|
||||
}
|
||||
const std::size_t frameBytes = voice.channels * sizeof(std::int16_t);
|
||||
const std::size_t frameBytes = voice.channels == 0 || voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9 ? 0 : voice.channels * Ngs2PcmSampleBytes(voice.waveformType);
|
||||
if ((param.flags & SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND) != 0) {
|
||||
AppendStreamData(voice, param, frameBytes);
|
||||
voice.acceptsBlocks = (param.flags & SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE) != 0;
|
||||
@@ -251,7 +253,7 @@ static void AddWaveformBlocks(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBl
|
||||
const auto& block = param.blocks[i];
|
||||
if (block.num_samples == 0 && block.data_size == 0) continue;
|
||||
if (allowsSilence && block.data_size == 0) {
|
||||
if (voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9) throw std::runtime_error("NGS2: a dataless waveform block under the silence flag is not implemented for ATRAC9 voices");
|
||||
if (voice.waveformType != SCE_NGS2_WAVEFORM_TYPE_PCM_I16L) throw std::runtime_error("NGS2: a dataless waveform block under the silence flag is not implemented for this format");
|
||||
voice.blocks.push_back({nullptr, block});
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
#include "Ngs2Test.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <xmmintrin.h>
|
||||
|
||||
struct PcmFixture {
|
||||
std::uint32_t type;
|
||||
std::uint32_t bytes;
|
||||
std::vector<std::uint8_t> data;
|
||||
std::array<float, 8> samples;
|
||||
};
|
||||
|
||||
static const PcmFixture Fixtures[] = {
|
||||
{SCE_NGS2_WAVEFORM_TYPE_PCM_U8, 1,
|
||||
{0x00, 0x40, 0x60, 0x80, 0xa0, 0xc0, 0xe0, 0xff},
|
||||
{-1, -0.5f, -0.25f, 0, 0.25f, 0.5f, 0.75f, 127.0f / 128}},
|
||||
{SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2,
|
||||
{0x00, 0x80, 0x00, 0xc0, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x20, 0x00, 0x40, 0x00, 0x60, 0xff, 0x7f},
|
||||
{-1, -0.5f, -0.25f, 0, 0.25f, 0.5f, 0.75f, 32767.0f / 32768}},
|
||||
{SCE_NGS2_WAVEFORM_TYPE_PCM_I24L, 3,
|
||||
{0x00, 0x00, 0x80, 0x00, 0x00, 0xc0, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x60, 0xff, 0xff, 0x7f},
|
||||
{-1, -0.5f, -0.25f, 0, 0.25f, 0.5f, 0.75f, 8388607.0f / 8388608}},
|
||||
{SCE_NGS2_WAVEFORM_TYPE_PCM_I32L, 4,
|
||||
{0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x60, 0xff, 0xff, 0xff, 0x7f},
|
||||
{-1, -0.5f, -0.25f, 0, 0.25f, 0.5f, 0.75f, 1}},
|
||||
{SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 4,
|
||||
{0x00, 0x00, 0x80, 0xbf, 0x00, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x80, 0xbe, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x80, 0x3e, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x40, 0x3f, 0x00, 0x00, 0x80, 0x3f},
|
||||
{-1, -0.5f, -0.25f, 0, 0.25f, 0.5f, 0.75f, 1}},
|
||||
{SCE_NGS2_WAVEFORM_TYPE_PCM_F64L, 8,
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xbf,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd0, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd0, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x3f,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe8, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f},
|
||||
{-1, -0.5f, -0.25f, 0, 0.25f, 0.5f, 0.75f, 1}},
|
||||
};
|
||||
|
||||
class Playback {
|
||||
public:
|
||||
Playback(std::uint32_t type, std::uint32_t channels = 1, std::uint32_t rate = 48000)
|
||||
: firstBuffer(usedBuffers), system(CreateSystem()), channels(channels), voice(Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER))) {
|
||||
Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {type, channels, rate, 0, 0, 0}});
|
||||
Patch(voice, Mastering(system, channels));
|
||||
Event(voice, SCE_NGS2_VOICE_EVENT_PLAY);
|
||||
}
|
||||
|
||||
~Playback() {
|
||||
Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK);
|
||||
usedBuffers = firstBuffer;
|
||||
}
|
||||
|
||||
void Add(const void* data, const Ngs2WaveformBlock* blocks, std::uint32_t count, std::uint32_t flags = 0) {
|
||||
Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, data, flags, count, blocks});
|
||||
}
|
||||
|
||||
std::vector<float> Render() {
|
||||
std::vector<float> output(Grain * channels, 123);
|
||||
const Ngs2RenderBufferInfo info{output.data(), output.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, channels};
|
||||
Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK);
|
||||
return output;
|
||||
}
|
||||
|
||||
Ngs2SamplerVoiceState State() {
|
||||
Ngs2SamplerVoiceState state{};
|
||||
Require(sceNgs2VoiceGetState(voice, &state.voice_state, sizeof(state)) == SCE_NGS2_OK);
|
||||
return state;
|
||||
}
|
||||
|
||||
const std::size_t firstBuffer;
|
||||
const std::uintptr_t system;
|
||||
const std::uint32_t channels;
|
||||
const std::uintptr_t voice;
|
||||
};
|
||||
|
||||
template <typename TFunction>
|
||||
static void RequireThrows(TFunction function) {
|
||||
bool threw = false;
|
||||
try { function(); } catch (const std::exception&) { threw = true; }
|
||||
Require(threw);
|
||||
}
|
||||
|
||||
static void TestFramesAndPositions(const PcmFixture& fixture, std::uint32_t channels) {
|
||||
Playback playback(fixture.type, channels);
|
||||
std::vector<std::uint8_t> data(3, 0xa5);
|
||||
constexpr std::uint32_t skip = 3;
|
||||
for (std::uint32_t frame = 0; frame < skip + Grain * 2; frame++) {
|
||||
for (std::uint32_t channel = 0; channel < channels; channel++) {
|
||||
const auto sample = (frame + channel) % fixture.samples.size();
|
||||
const auto first = fixture.data.begin() + sample * fixture.bytes;
|
||||
data.insert(data.end(), first, first + fixture.bytes);
|
||||
}
|
||||
}
|
||||
const auto frameBytes = channels * fixture.bytes;
|
||||
const Ngs2WaveformBlock block{2, data.size() - 3, 0, skip, Grain * 2, 0, 0};
|
||||
playback.Add(data.data() + 1, &block, 1);
|
||||
Require(playback.State().waveform_data == data.data() + 3 + skip * frameBytes);
|
||||
for (std::uint32_t pass = 0; pass < 2; pass++) {
|
||||
const auto output = playback.Render();
|
||||
for (std::uint32_t frame = 0; frame < Grain; frame++) {
|
||||
for (std::uint32_t channel = 0; channel < channels; channel++) {
|
||||
const auto actual = output[frame * channels + channel];
|
||||
const auto expected = fixture.samples[(skip + pass * Grain + frame + channel) % 8];
|
||||
if (actual != expected) {
|
||||
std::fprintf(stderr, "PCM type %#x frame %u channel %u: got %a, expected %a\n", fixture.type, pass * Grain + frame, channel, actual, expected);
|
||||
}
|
||||
Require(actual == expected);
|
||||
}
|
||||
}
|
||||
const auto state = playback.State();
|
||||
Require(state.num_decoded_samples == (pass + 1) * Grain);
|
||||
Require(state.decoded_data_size == (pass + 1) * Grain * frameBytes);
|
||||
Require(state.waveform_data == data.data() + 3 + (skip + (pass + 1) * Grain) * frameBytes);
|
||||
}
|
||||
Require(Flags(playback.voice) == 0);
|
||||
}
|
||||
|
||||
static std::vector<Ngs2VoiceCallbackInfo> callbacks;
|
||||
|
||||
static void APS5_VABI OnBlock(const Ngs2VoiceCallbackInfo* info) {
|
||||
callbacks.push_back(*info);
|
||||
}
|
||||
|
||||
static void TestRepeatAndTransition(const PcmFixture& fixture) {
|
||||
Playback playback(fixture.type);
|
||||
callbacks.clear();
|
||||
Control(playback.voice, SCE_NGS2_VOICE_PARAM_CALLBACK,
|
||||
Ngs2VoiceCallbackParam{{}, OnBlock, 7, SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_REPEAT | SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0});
|
||||
const Ngs2WaveformBlock blocks[] = {{0, fixture.bytes * 4, 1, 1, 3, 0, 101}, {fixture.bytes * 4, fixture.bytes * 4, 0, 0, 4, 0, 202}};
|
||||
playback.Add(fixture.data.data(), blocks, 2);
|
||||
const auto first = playback.Render();
|
||||
constexpr std::uint32_t indices[] = {1, 2, 3, 1, 2, 3, 4, 5};
|
||||
for (std::uint32_t i = 0; i < Grain; i++) Require(first[i] == fixture.samples[indices[i]]);
|
||||
auto state = playback.State();
|
||||
Require(state.num_decoded_samples == 8 && state.decoded_data_size == 8 * fixture.bytes);
|
||||
Require(state.waveform_data == fixture.data.data() + 6 * fixture.bytes);
|
||||
const auto last = playback.Render();
|
||||
Require(last[0] == fixture.samples[6] && last[1] == fixture.samples[7]);
|
||||
for (std::uint32_t i = 2; i < Grain; i++) Require(last[i] == 0);
|
||||
state = playback.State();
|
||||
Require(state.num_decoded_samples == 10 && state.decoded_data_size == 10 * fixture.bytes);
|
||||
Require(state.waveform_data == fixture.data.data() + fixture.data.size());
|
||||
Require(callbacks.size() == 3);
|
||||
Require(callbacks[0].flag == SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_REPEAT && callbacks[0].user_data == 101 && callbacks[0].num_repeated == 1);
|
||||
Require(callbacks[1].flag == SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END && callbacks[1].user_data == 101);
|
||||
Require(callbacks[2].flag == SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END && callbacks[2].user_data == 202);
|
||||
Require(callbacks[0].block_data == fixture.data.data() && callbacks[2].block_data == fixture.data.data() + fixture.bytes * 4);
|
||||
}
|
||||
|
||||
static void TestResampling(const PcmFixture& fixture) {
|
||||
Playback playback(fixture.type, 1, 24000);
|
||||
const Ngs2WaveformBlock blocks[] = {{0, fixture.bytes * 3, 0, 0, 3, 0, 0}, {fixture.bytes * 3, fixture.bytes * 5, 0, 0, 5, 0, 0}};
|
||||
playback.Add(fixture.data.data(), blocks, 2);
|
||||
for (std::uint32_t pass = 0; pass < 2; pass++) {
|
||||
const auto output = playback.Render();
|
||||
for (std::uint32_t i = 0; i < Grain; i++) {
|
||||
const auto sample = (pass * Grain + i) / 2;
|
||||
const auto next = sample < 7 ? sample + 1 : sample;
|
||||
const auto expected = i % 2 == 0 ? fixture.samples[sample] : std::lerp(fixture.samples[sample], fixture.samples[next], 0.5f);
|
||||
Require(output[i] == expected);
|
||||
}
|
||||
Require(playback.State().decoded_data_size == (pass + 1) * 4 * fixture.bytes);
|
||||
}
|
||||
Require(playback.State().num_decoded_samples == 8);
|
||||
}
|
||||
|
||||
static void TestPitch(const PcmFixture& fixture) {
|
||||
Playback playback(fixture.type);
|
||||
Control(playback.voice, SCE_NGS2_SAMPLER_VOICE_PARAM_PITCH, Ngs2SamplerVoicePitchParam{{}, 2});
|
||||
const Ngs2WaveformBlock block{0, fixture.data.size(), 1, 0, 8, 0, 0};
|
||||
playback.Add(fixture.data.data(), &block, 1);
|
||||
const auto output = playback.Render();
|
||||
for (std::uint32_t i = 0; i < Grain; i++) Require(output[i] == fixture.samples[(i * 2) % 8]);
|
||||
Require(playback.State().num_decoded_samples == 16 && playback.State().decoded_data_size == 16 * fixture.bytes);
|
||||
Require(Flags(playback.voice) == 0);
|
||||
}
|
||||
|
||||
static void TestBounds(const PcmFixture& fixture) {
|
||||
Playback playback(fixture.type, 2);
|
||||
const Ngs2WaveformBlock shortBlock{0, fixture.data.size() - 1, 1, 0, 4, 0, 0};
|
||||
RequireThrows([&] { playback.Add(fixture.data.data(), &shortBlock, 1); });
|
||||
const Ngs2WaveformBlock overflow{0, fixture.data.size(), 0, UINT32_MAX, UINT32_MAX, 0, 0};
|
||||
RequireThrows([&] { playback.Add(fixture.data.data(), &overflow, 1, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET); });
|
||||
if (fixture.type != SCE_NGS2_WAVEFORM_TYPE_PCM_I16L) {
|
||||
const Ngs2WaveformBlock incomplete{0, fixture.data.size(), 0, 0, 20, 0, 0};
|
||||
RequireThrows([&] { playback.Add(fixture.data.data(), &incomplete, 1, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET | SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE); });
|
||||
const Ngs2WaveformBlock silence{0, 0, 0, 0, Grain, 0, 0};
|
||||
RequireThrows([&] { playback.Add(fixture.data.data(), &silence, 1, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET | SCE_NGS2_WAVEFORM_BLOCKS_FLAG_SILENCE); });
|
||||
RequireThrows([&] {
|
||||
Control(playback.voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {fixture.type, 2, 48000, 1, 0, 0}});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static void TestUnsupportedFloat(std::uint32_t type, std::uint64_t bits, std::uint32_t bytes) {
|
||||
Playback playback(type);
|
||||
std::vector<std::uint8_t> data(bytes * Grain);
|
||||
for (std::uint32_t i = 0; i < bytes; i++) data[i] = static_cast<std::uint8_t>(bits >> (i * 8));
|
||||
const Ngs2WaveformBlock block{0, data.size(), 0, 0, Grain, 0, 0};
|
||||
playback.Add(data.data(), &block, 1);
|
||||
RequireThrows([&] { playback.Render(); });
|
||||
}
|
||||
|
||||
static void TestNumericEdges() {
|
||||
const PcmFixture signed24{SCE_NGS2_WAVEFORM_TYPE_PCM_I24L, 3,
|
||||
{0x01, 0x00, 0x00, 0xff, 0xff, 0xff, 0x01, 0x00, 0x80, 0xfe, 0xff, 0x7f,
|
||||
0x56, 0x34, 0x12, 0xaa, 0xcb, 0xed, 0x80, 0x00, 0x00, 0x80, 0xff, 0xff},
|
||||
{0x1p-23f, -0x1p-23f, -8388607.0f / 8388608, 8388606.0f / 8388608,
|
||||
1193046.0f / 8388608, -1193046.0f / 8388608, 0x1p-16f, -0x1p-16f}};
|
||||
TestFramesAndPositions(signed24, 1);
|
||||
const PcmFixture signed32{SCE_NGS2_WAVEFORM_TYPE_PCM_I32L, 4,
|
||||
{0x01, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x01, 0x00, 0x00, 0x80, 0xfe, 0xff, 0xff, 0x7f,
|
||||
0x01, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x01, 0xfd, 0xff, 0xff, 0xfe},
|
||||
{0x1p-31f, -0x1p-31f, -1, 1, 0x1p-7f, 0x1.000002p-7f, 0x1.000004p-7f, -0x1.000004p-7f}};
|
||||
TestFramesAndPositions(signed32, 1);
|
||||
const PcmFixture floating64{SCE_NGS2_WAVEFORM_TYPE_PCM_F64L, 8,
|
||||
{0x9a, 0x99, 0x99, 0x99, 0x99, 0x99, 0xb9, 0x3f, 0x9a, 0x99, 0x99, 0x99, 0x99, 0x99, 0xb9, 0xbf,
|
||||
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0xe0, 0x3f, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xe0, 0x3f,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xb8,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80},
|
||||
{0.1f, -0.1f, 0.5f, 0x1.000004p-1f, 0x1p-126f, -0x1p-126f, 0, 0}};
|
||||
TestFramesAndPositions(floating64, 1);
|
||||
}
|
||||
|
||||
int main() {
|
||||
for (const auto& fixture : Fixtures) {
|
||||
for (const auto channels : {1u, 2u, 6u, 8u}) TestFramesAndPositions(fixture, channels);
|
||||
TestRepeatAndTransition(fixture);
|
||||
TestResampling(fixture);
|
||||
TestPitch(fixture);
|
||||
TestBounds(fixture);
|
||||
}
|
||||
TestNumericEdges();
|
||||
for (const auto bits : {0x7fc00000u, 0x7f800001u, 0x7f800000u, 0xff800000u, 0x40000000u, 0xc0000000u, 1u}) {
|
||||
TestUnsupportedFloat(SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, bits, 4);
|
||||
}
|
||||
for (const auto bits : {0x7ff8000000000000ull, 0x7ff0000000000001ull, 0x7ff0000000000000ull, 0xfff0000000000000ull,
|
||||
0x4000000000000000ull, 0xc000000000000000ull, 0x3800000000000000ull, 1ull}) {
|
||||
TestUnsupportedFloat(SCE_NGS2_WAVEFORM_TYPE_PCM_F64L, bits, 8);
|
||||
}
|
||||
for (const auto type : {0x10u, 0x13u, 0x15u, 0x17u, 0x19u, 0x1bu, UINT32_MAX}) {
|
||||
Playback playback(SCE_NGS2_WAVEFORM_TYPE_PCM_I16L);
|
||||
RequireThrows([&] {
|
||||
Control(playback.voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {type, 1, 48000, 0, 0, 0}});
|
||||
});
|
||||
}
|
||||
const auto control = _mm_getcsr();
|
||||
_mm_setcsr(control | 0x8040);
|
||||
TestUnsupportedFloat(SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 1, 4);
|
||||
TestUnsupportedFloat(SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 0x80000001, 4);
|
||||
TestUnsupportedFloat(SCE_NGS2_WAVEFORM_TYPE_PCM_F64L, 1, 8);
|
||||
_mm_setcsr(control);
|
||||
return 0;
|
||||
}
|
||||
@@ -12,4 +12,5 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(ngs2_core_render PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNgs2>")
|
||||
endif()
|
||||
add_sce_ngs2_test(libSceNgs2 ngs2_core_pcm Ngs2Pcm.cpp)
|
||||
endif()
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
### Unknown function info
|
||||
|
||||
- [NGS2 PCM sampler playback](../../../core/libs/prx/libSceNgs2.native/src/Pcm.cpp) is not console-verified. Types 0x11, 0x12, 0x14, 0x16, 0x18 and 0x1a follow [prosper's waveform parser analysis](https://github.com/mattias800/prosper/blob/30572df43ae20ff28f995db56149f7af42235741/prosper/src/hle/audio/ngs2_waveform.cpp#L194-L223) and the WAVE representations it selects: unsigned 8-bit, signed little-endian 16/24/32-bit and little-endian IEEE float/double. Integer samples use the existing signed-16 normalization generalized to their width; double samples narrow to host float. Console rounding, float clipping, nonfinite and subnormal handling are unknown: floating input outside -1..1, nonfinite values and nonzero magnitudes below the smallest normal float throw. Other types and nonzero configuration words on the new formats throw. Incomplete streamed blocks and dataless silence blocks remain supported only for signed-16 PCM; the new formats require complete blocks.
|
||||
|
||||
- [NGS2 voice validation](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) returns `0x804A8203` for an invalid voice in Control, RunCommands, GetState, GetStateFlags, GetPortInfo and QueryInfo, following the type-4 handle error in [prosper's PS5 module analysis](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp#L4478). Control checks a null parameter list first (`0x804A8360`); GetState resolves the voice before output validation, and GetState rejects sizes below 8 before a null output. Existing rack-specific exact state sizes, other argument precedence and command layouts are retained; arbitrary-sized state copies and full console behavior remain unverified. These values follow module analysis, not new console execution.
|
||||
|
||||
- [NGS2 system and rack handle errors](../../../core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp) use `0x804A8201` and `0x804A8202`, following the native handle-type error helper recorded in [prosper's PS5 module analysis](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp). [sceNgs2RackGetVoiceHandle](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) resolves the rack before checking the output pointer, matching the export in firmware 1.00 through 12.70 reported in [#5916](https://github.com/boykopovar/AnyPS5/pull/5916#issuecomment-6172832590). Other exports retain their existing validation order; complete invalid-input precedence and console execution remain unverified.
|
||||
|
||||
Reference in new issue
Block a user