feat(libSceNgs2.native): stream 16-bit PCM waveforms through appended waveform blocks
A 16-bit PCM block whose num_samples exceed its data starts a stream, and waveform blocks with flag 0x2 append more data to it. The voice outputs silence while it has played all appended data and CONTINUE is set, and ends at num_samples or when CONTINUE is cleared.
This commit is contained in:
parent
c8a2f06325
commit
88e77da57b
|
|
@ -2,7 +2,7 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/Ngs2Library.cmake)
|
|||
add_sce_ngs2_library(libSceNgs2.native)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Parse)
|
||||
foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Parse Stream)
|
||||
string(TOLOWER ${ngs2Test} testName)
|
||||
add_test_executable(ngs2_${testName}_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Ngs2${ngs2Test}.cpp)
|
||||
target_include_directories(ngs2_${testName}_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src)
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ static constexpr std::uint32_t SCE_NGS2_CUSTOM_MAX_PORTS = 16;
|
|||
static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2 = 0x1f;
|
||||
|
||||
static constexpr std::uint32_t SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE = 1;
|
||||
static constexpr std::uint32_t SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND = 2;
|
||||
static constexpr std::uint32_t SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET = 4;
|
||||
|
||||
static constexpr std::uint32_t SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END = 1;
|
||||
|
|
|
|||
|
|
@ -25,12 +25,21 @@ enum class Ngs2PlayState : std::uint32_t {
|
|||
Stopped = SCE_NGS2_VOICE_STATE_FLAG_INUSE | SCE_NGS2_VOICE_STATE_FLAG_PLAYING | SCE_NGS2_VOICE_STATE_FLAG_STOPPED,
|
||||
};
|
||||
|
||||
struct Ngs2Piece {
|
||||
const std::uint8_t* data;
|
||||
std::uint64_t firstFrame;
|
||||
std::uint64_t frames;
|
||||
};
|
||||
|
||||
struct Ngs2Block {
|
||||
const std::uint8_t* data;
|
||||
Ngs2WaveformBlock info;
|
||||
std::uint32_t cursor = 0;
|
||||
std::uint32_t numRepeated = 0;
|
||||
std::size_t dataCursor = 0;
|
||||
bool streaming = false;
|
||||
std::uint64_t availableFrames = 0;
|
||||
std::vector<Ngs2Piece> pieces;
|
||||
};
|
||||
|
||||
struct Ngs2Atrac9DecoderDeleter {
|
||||
|
|
@ -143,6 +152,7 @@ struct Ngs2System {
|
|||
};
|
||||
|
||||
std::string Ngs2Hex(std::uint32_t value);
|
||||
const std::uint8_t* Ngs2StreamEnd(const Ngs2Voice& voice, const Ngs2Block& block);
|
||||
std::recursive_mutex& Ngs2Mutex();
|
||||
Ngs2System* Ngs2FindSystem(Ngs2Handle handle);
|
||||
|
||||
|
|
|
|||
|
|
@ -14,10 +14,21 @@ 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) {
|
||||
return block.data + (static_cast<std::size_t>(block.info.num_skip_samples) + frame) * voice.channels * sizeof(std::int16_t);
|
||||
const std::size_t frameBytes = voice.channels * sizeof(std::int16_t);
|
||||
if (block.streaming) {
|
||||
for (const auto& piece : block.pieces) {
|
||||
if (frame >= piece.firstFrame && frame - piece.firstFrame < piece.frames) return piece.data + (frame - piece.firstFrame) * frameBytes;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
return block.data + (static_cast<std::size_t>(block.info.num_skip_samples) + frame) * frameBytes;
|
||||
}
|
||||
|
||||
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));
|
||||
|
|
@ -34,7 +45,8 @@ static void ReadFrames(Ngs2Voice& voice, float* current, float* next) {
|
|||
return;
|
||||
}
|
||||
ReadPcm(voice, FrameAt(voice, block, block.cursor), current);
|
||||
if (block.cursor + 1 < block.info.num_samples) ReadPcm(voice, FrameAt(voice, block, block.cursor + 1), next);
|
||||
if (block.streaming && block.cursor + 1 >= block.availableFrames) std::memcpy(next, current, voice.channels * sizeof(float));
|
||||
else if (block.cursor + 1 < block.info.num_samples) ReadPcm(voice, FrameAt(voice, block, block.cursor + 1), next);
|
||||
else if (block.info.num_repeats != 0) ReadPcm(voice, FrameAt(voice, block, 0), next);
|
||||
else if (voice.blocks.size() > 1) ReadPcm(voice, FrameAt(voice, voice.blocks[1], 0), next);
|
||||
else std::memcpy(next, current, voice.channels * sizeof(float));
|
||||
|
|
@ -65,7 +77,7 @@ static void FinishBlock(Ngs2Voice& voice) {
|
|||
0,
|
||||
};
|
||||
if (!repeat) {
|
||||
voice.waveformEnd = voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9 ? block.data + block.dataCursor : FrameAt(voice, block, block.info.num_samples);
|
||||
voice.waveformEnd = voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9 ? block.data + block.dataCursor : block.streaming ? Ngs2StreamEnd(voice, block) : FrameAt(voice, block, block.info.num_samples);
|
||||
voice.blocks.pop_front();
|
||||
}
|
||||
if (voice.callback != nullptr && (voice.callbackFlags & info.flag) != 0) voice.callback(&info);
|
||||
|
|
@ -74,12 +86,17 @@ static void FinishBlock(Ngs2Voice& voice) {
|
|||
static void Advance(Ngs2Voice& voice, std::uint64_t frames) {
|
||||
while (frames != 0 && voice.state == Ngs2PlayState::Playing && !voice.blocks.empty()) {
|
||||
auto& block = voice.blocks.front();
|
||||
const auto step = std::min<std::uint64_t>(frames, block.info.num_samples - block.cursor);
|
||||
const std::uint64_t limit = block.streaming ? std::min<std::uint64_t>(block.info.num_samples, block.availableFrames) : block.info.num_samples;
|
||||
if (block.cursor >= limit && block.cursor != block.info.num_samples && !(block.streaming && !voice.acceptsBlocks)) break;
|
||||
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);
|
||||
frames -= step;
|
||||
if (block.cursor == block.info.num_samples) FinishBlock(voice);
|
||||
if (block.streaming) {
|
||||
while (block.pieces.size() > 1 && block.cursor >= block.pieces.front().firstFrame + block.pieces.front().frames) block.pieces.erase(block.pieces.begin());
|
||||
}
|
||||
if (block.cursor == block.info.num_samples || (block.streaming && !voice.acceptsBlocks && block.cursor == block.availableFrames)) FinishBlock(voice);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -64,10 +64,21 @@ void Ngs2Voice::ResetSetup() {
|
|||
waveformEnd = nullptr;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
const std::uint8_t* Ngs2Voice::WaveformData() const {
|
||||
if (blocks.empty()) return waveformEnd;
|
||||
const auto& block = blocks.front();
|
||||
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);
|
||||
}
|
||||
return Ngs2StreamEnd(*this, block);
|
||||
}
|
||||
return block.data + (static_cast<std::size_t>(block.info.num_skip_samples) + block.cursor) * channels * sizeof(std::int16_t);
|
||||
}
|
||||
|
||||
|
|
@ -150,8 +161,22 @@ static void SetupSampler(Ngs2Voice& voice, const Ngs2WaveformFormat& format) {
|
|||
if (format.waveform_type == SCE_NGS2_WAVEFORM_TYPE_ATRAC9) Ngs2SetupAtrac9(voice, format);
|
||||
}
|
||||
|
||||
static void AppendStreamData(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBlocksParam& param, std::size_t frameBytes) {
|
||||
if (voice.blocks.empty() || !voice.blocks.back().streaming) throw std::runtime_error("NGS2: appending waveform data to a voice without a streaming waveform is not implemented");
|
||||
auto& stream = voice.blocks.back();
|
||||
for (std::uint32_t i = 0; i < param.num_blocks; i++) {
|
||||
const auto& block = param.blocks[i];
|
||||
if (block.data_size == 0) continue;
|
||||
if (block.data_size % frameBytes != 0 || block.num_skip_samples != 0 || block.num_repeats != 0) {
|
||||
throw std::runtime_error("NGS2: appending a waveform block with skipped samples, repeats or a partial frame is not implemented");
|
||||
}
|
||||
stream.pieces.push_back({static_cast<const std::uint8_t*>(param.data) + block.data_offset, stream.availableFrames, block.data_size / frameBytes});
|
||||
stream.availableFrames += block.data_size / frameBytes;
|
||||
}
|
||||
}
|
||||
|
||||
static void AddWaveformBlocks(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBlocksParam& param) {
|
||||
constexpr std::uint32_t knownFlags = SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE | SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET;
|
||||
constexpr std::uint32_t knownFlags = SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE | SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND | SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET;
|
||||
if ((param.flags & ~knownFlags) != 0) throw std::runtime_error("NGS2: waveform block flags " + Ngs2Hex(param.flags) + " are not implemented");
|
||||
if (voice.channels == 0 || (param.num_blocks != 0 && (param.blocks == nullptr || param.data == nullptr))) APS5_INVALID_ARG_EX;
|
||||
const bool reset = (param.flags & SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET) != 0;
|
||||
|
|
@ -162,17 +187,31 @@ static void AddWaveformBlocks(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBl
|
|||
voice.waveformEnd = nullptr;
|
||||
if (voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9) Ngs2RestartAtrac9(voice);
|
||||
}
|
||||
voice.acceptsBlocks = (param.flags & SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE) != 0;
|
||||
const std::size_t frameBytes = voice.channels * sizeof(std::int16_t);
|
||||
if ((param.flags & SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND) != 0) {
|
||||
if (voice.waveformType != SCE_NGS2_WAVEFORM_TYPE_PCM_I16L) throw std::runtime_error("NGS2: appending waveform data to a voice that is not 16-bit PCM is not implemented");
|
||||
AppendStreamData(voice, param, frameBytes);
|
||||
voice.acceptsBlocks = (param.flags & SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE) != 0;
|
||||
return;
|
||||
}
|
||||
voice.acceptsBlocks = (param.flags & SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE) != 0;
|
||||
for (std::uint32_t i = 0; i < param.num_blocks; i++) {
|
||||
const auto& block = param.blocks[i];
|
||||
if (block.num_samples == 0 && block.data_size == 0) continue;
|
||||
const std::uint64_t bytes = voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9 ? Ngs2Atrac9BlockBytes(voice, block)
|
||||
: (static_cast<std::uint64_t>(block.num_skip_samples) + block.num_samples) * frameBytes;
|
||||
if (block.num_samples == 0 || bytes > block.data_size) {
|
||||
const bool streaming = block.num_samples != 0 && bytes > block.data_size && voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_PCM_I16L &&
|
||||
block.num_skip_samples == 0 && block.num_repeats == 0 && block.data_size >= frameBytes && block.data_size % frameBytes == 0;
|
||||
if (!streaming && (block.num_samples == 0 || bytes > block.data_size)) {
|
||||
throw std::invalid_argument("NGS2: waveform block " + std::to_string(i) + " does not fit its data");
|
||||
}
|
||||
voice.blocks.push_back({static_cast<const std::uint8_t*>(param.data) + block.data_offset, block});
|
||||
if (streaming) {
|
||||
auto& stream = voice.blocks.back();
|
||||
stream.streaming = true;
|
||||
stream.availableFrames = block.data_size / frameBytes;
|
||||
stream.pieces.push_back({stream.data, 0, stream.availableFrames});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,136 @@
|
|||
#include "Ngs2Test.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
static std::vector<std::int16_t> RenderI16(uintptr_t system) {
|
||||
std::vector<std::int16_t> out(Grain, -1);
|
||||
const Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(std::int16_t), SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1};
|
||||
Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK);
|
||||
return out;
|
||||
}
|
||||
|
||||
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 StreamVoice(uintptr_t rack, std::uint32_t index, uintptr_t master) {
|
||||
const auto voice = RackVoice(rack, index);
|
||||
Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1, 48000, 0, 0, 0}});
|
||||
Patch(voice, master);
|
||||
return voice;
|
||||
}
|
||||
|
||||
static void AddBlock(uintptr_t voice, const void* data, std::uint64_t bytes, std::uint32_t samples, std::uint32_t flags) {
|
||||
const Ngs2WaveformBlock block{0, bytes, 0, 0, samples, 0, 0};
|
||||
Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, data, flags, 1, &block});
|
||||
}
|
||||
|
||||
template <typename TFunction>
|
||||
static bool Throws(TFunction function) {
|
||||
try {
|
||||
function();
|
||||
} catch (const std::exception&) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void TestStreamEndsAtDeclaredSamples() {
|
||||
const auto system = CreateSystem();
|
||||
const auto master = Mastering(system, 1);
|
||||
const auto rack = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER);
|
||||
const auto voice = StreamVoice(rack, 0, master);
|
||||
const std::vector<std::int16_t> first(6, 16384);
|
||||
const std::vector<std::int16_t> second(8, 8192);
|
||||
const std::vector<std::int16_t> last(6, 4096);
|
||||
|
||||
AddBlock(voice, first.data(), first.size() * 2, 20, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE);
|
||||
Event(voice, SCE_NGS2_VOICE_EVENT_PLAY);
|
||||
auto out = RenderI16(system);
|
||||
for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == (i < 6 ? 16384 : 0));
|
||||
Require(Flags(voice) == (SCE_NGS2_VOICE_STATE_FLAG_INUSE | SCE_NGS2_VOICE_STATE_FLAG_PLAYING));
|
||||
Ngs2SamplerVoiceState state{};
|
||||
Require(sceNgs2VoiceGetState(voice, &state.voice_state, sizeof(state)) == SCE_NGS2_OK);
|
||||
Require(state.num_decoded_samples == 6 && state.decoded_data_size == 12 && state.waveform_data == first.data() + 6);
|
||||
|
||||
out = RenderI16(system);
|
||||
for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == 0);
|
||||
Require(sceNgs2VoiceGetState(voice, &state.voice_state, sizeof(state)) == SCE_NGS2_OK);
|
||||
Require(state.num_decoded_samples == 6 && state.decoded_data_size == 12);
|
||||
|
||||
AddBlock(voice, second.data(), second.size() * 2, 20, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE | SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND);
|
||||
Require(sceNgs2VoiceGetState(voice, &state.voice_state, sizeof(state)) == SCE_NGS2_OK && state.waveform_data == second.data());
|
||||
out = RenderI16(system);
|
||||
for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == 8192);
|
||||
Require(sceNgs2VoiceGetState(voice, &state.voice_state, sizeof(state)) == SCE_NGS2_OK);
|
||||
Require(state.num_decoded_samples == 14 && state.decoded_data_size == 28 && state.waveform_data == second.data() + 8);
|
||||
|
||||
AddBlock(voice, last.data(), last.size() * 2, 20, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND);
|
||||
out = RenderI16(system);
|
||||
for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == (i < 6 ? 4096 : 0));
|
||||
Require(Flags(voice) == 0);
|
||||
Require(sceNgs2VoiceGetState(voice, &state.voice_state, sizeof(state)) == SCE_NGS2_OK);
|
||||
Require(state.num_decoded_samples == 20 && state.decoded_data_size == 40 && state.waveform_data == last.data() + 6);
|
||||
Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK);
|
||||
}
|
||||
|
||||
static void TestStreamClosedBeforeDeclaredSamples() {
|
||||
const auto system = CreateSystem();
|
||||
const auto master = Mastering(system, 1);
|
||||
const auto rack = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER);
|
||||
const auto voice = StreamVoice(rack, 0, master);
|
||||
const std::vector<std::int16_t> first(6, 16384);
|
||||
const std::vector<std::int16_t> closing(4, 8192);
|
||||
|
||||
AddBlock(voice, first.data(), first.size() * 2, 1000, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE);
|
||||
Event(voice, SCE_NGS2_VOICE_EVENT_PLAY);
|
||||
AddBlock(voice, closing.data(), closing.size() * 2, 1000, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND);
|
||||
auto out = RenderI16(system);
|
||||
for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == (i < 6 ? 16384 : 8192));
|
||||
Require(Flags(voice) == (SCE_NGS2_VOICE_STATE_FLAG_INUSE | SCE_NGS2_VOICE_STATE_FLAG_PLAYING));
|
||||
out = RenderI16(system);
|
||||
for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == (i < 2 ? 8192 : 0));
|
||||
Require(Flags(voice) == 0);
|
||||
Ngs2SamplerVoiceState state{};
|
||||
Require(sceNgs2VoiceGetState(voice, &state.voice_state, sizeof(state)) == SCE_NGS2_OK);
|
||||
Require(state.num_decoded_samples == 10 && state.decoded_data_size == 20);
|
||||
Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK);
|
||||
}
|
||||
|
||||
static void TestStreamRejections() {
|
||||
const auto system = CreateSystem();
|
||||
const auto master = Mastering(system, 1);
|
||||
const auto rack = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER);
|
||||
const std::vector<std::int16_t> pcm(6, 1000);
|
||||
|
||||
const auto empty = StreamVoice(rack, 0, master);
|
||||
Require(Throws([&] { AddBlock(empty, pcm.data(), pcm.size() * 2, 6, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND); }));
|
||||
|
||||
const auto complete = StreamVoice(rack, 1, master);
|
||||
AddBlock(complete, pcm.data(), pcm.size() * 2, 6, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE);
|
||||
Require(Throws([&] { AddBlock(complete, pcm.data(), pcm.size() * 2, 6, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE | SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND); }));
|
||||
|
||||
const auto repeating = StreamVoice(rack, 2, master);
|
||||
const Ngs2WaveformBlock repeated{0, pcm.size() * 2, 1, 0, 20, 0, 0};
|
||||
Require(Throws([&] { Control(repeating, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE, 1, &repeated}); }));
|
||||
|
||||
const auto skipping = StreamVoice(rack, 3, master);
|
||||
const Ngs2WaveformBlock skipped{0, pcm.size() * 2, 0, 1, 20, 0, 0};
|
||||
Require(Throws([&] { Control(skipping, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE, 1, &skipped}); }));
|
||||
|
||||
const auto partial = StreamVoice(rack, 4, master);
|
||||
AddBlock(partial, pcm.data(), pcm.size() * 2, 20, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE);
|
||||
Require(Throws([&] { AddBlock(partial, pcm.data(), 3, 20, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE | SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND); }));
|
||||
Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK);
|
||||
}
|
||||
|
||||
int main() {
|
||||
TestStreamEndsAtDeclaredSamples();
|
||||
TestStreamClosedBeforeDeclaredSamples();
|
||||
TestStreamRejections();
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -153,6 +153,7 @@ Throughout the project, every function at every stage either **does exactly what
|
|||
- [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
|
||||
- [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
|
||||
- [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
|
||||
|
|
|
|||
Loading…
Reference in New Issue